Profiles API

Profiles define agent behavior — the system prompt (SKILL.md), allowed tools, MCP servers, and per-runtime overrides. Profiles are stored as YAML + Markdown on disk and support three scopes: built-in (shipped with ainative-business), user (~/.claude/skills/), and project (.claude/skills/). The Profiles API covers CRUD, behavioral testing, learned context management, AI-assisted generation, and GitHub import.

Quick Start

List available profiles, run behavioral smoke tests against a runtime, and check the results:

// 1. List profiles to find ones compatible with your runtime
const profiles: Profile[] = await fetch('/api/profiles?scope=all&projectId=proj-8f3a-4b2c')
.then(r => r.json());

const claudeProfiles = profiles.filter(p =>
p.supportedRuntimes.includes('claude-code')
);
console.log(`${claudeProfiles.length} profiles support claude-code`);
claudeProfiles.forEach(p => console.log(`  ${p.name} (${p.scope}) — ${p.tags.join(', ')}`));

// 2. Run smoke tests for a profile against claude-code
const testReport: TestReport = await fetch('/api/profiles/data-analyst/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ runtimeId: 'claude-code' }),
}).then(r => r.json());

// 3. Check test results — each test sends a prompt and checks for expected keywords
console.log(`Tests: ${testReport.passed}/${testReport.total} passed`);
testReport.results.forEach(t => {
const icon = t.pass ? 'PASS' : 'FAIL';
console.log(`  [${icon}] ${t.task}`);
if (!t.pass) console.log(`    Missing: ${t.missingKeywords.join(', ')}`);
});

// 4. Retrieve persisted test results later
const savedResults: TestReport = await fetch(
'/api/profiles/data-analyst/test-results?runtimeId=claude-code'
).then(r => r.json());

Base URL

/api/profiles

Endpoints

List Profiles

GET /api/profiles

Retrieve profiles with optional scope and project filtering. Default returns user + built-in profiles. Use scope=all with a projectId to include project-scoped profiles.

Query Parameters

Param Type Req Description
scope enum Filter scope: project (project-only) or all (builtin + user + project)
projectId string Project UUID — required when scope is project or all

Response 200 — Array of profile objects sorted by name

Profile Object

FieldTypeReqDescription
idstring*Profile identifier (kebab-case slug)
namestring*Display name
descriptionstring*Brief description of the profile behavior
domainenum*work or personal
tagsstring[]*Categorization tags
skillMdstring*Full SKILL.md content (system prompt + behavioral instructions)
allowedToolsstring[]Tool allowlist for agent execution
mcpServersobjectMCP server configurations
canUseToolPolicyobjectAuto-approve/auto-deny tool policies
maxTurnsnumberMaximum agent conversation turns
outputFormatstringExpected output format
versionstring*Semantic version (x.y.z)
authorstringProfile author
sourcestring (URL)Source URL
supportedRuntimesstring[]*Compatible agent runtimes
runtimeOverridesobjectPer-runtime instruction, tool, and MCP overrides
isBuiltinboolean*Whether this is a shipped built-in profile
scopeenum*builtin, user, or project
originenumHow created: manual, environment, import, or ai-assist
readOnlyboolean*Whether the profile is read-only (true for project-scoped)

List all profiles including project-scoped ones — useful for building a profile selector when creating tasks:

// List profiles and filter by runtime compatibility
const profiles: Profile[] = await fetch('/api/profiles?scope=all&projectId=proj-8f3a-4b2c')
.then(r => r.json());

// Group by scope for display
const grouped = Object.groupBy(profiles, p => p.scope);
for (const [scope, items] of Object.entries(grouped)) {
console.log(`${scope} (${items.length}):`);
items.forEach(p => console.log(`  ${p.name} — ${p.description}`));
}

Example response:

[
  {
    "id": "data-analyst",
    "name": "Data Analyst",
    "description": "Statistical analysis with Python, pandas, and visualization best practices",
    "domain": "work",
    "tags": ["analysis", "data", "python"],
    "version": "1.2.0",
    "supportedRuntimes": ["claude-code", "anthropic-direct"],
    "isBuiltin": true,
    "scope": "builtin",
    "readOnly": true
  },
  {
    "id": "custom-reviewer",
    "name": "Custom Code Reviewer",
    "description": "Security-focused code review for the payments service",
    "domain": "work",
    "tags": ["code-review", "security"],
    "version": "1.0.0",
    "supportedRuntimes": ["claude-code"],
    "isBuiltin": false,
    "scope": "user",
    "origin": "manual",
    "readOnly": false
  }
]

Create Profile

POST /api/profiles

Create a new user profile. The request body must pass Zod validation against ProfileConfigSchema. Provide the SKILL.md content separately.

Request Body

FieldTypeReqDescription
idstring*Profile identifier (must be unique, kebab-case)
namestring*Display name
versionstring*Semantic version (x.y.z format)
domainenum*work or personal
tagsstring[]*Categorization tags
skillMdstringSKILL.md content (system prompt)
allowedToolsstring[]Tool allowlist
mcpServersobjectMCP server configurations
maxTurnsnumberMax agent turns
supportedRuntimesstring[]Compatible runtimes
testsProfileSmokeTest[]Behavioral smoke tests

Response 201 Created{ "ok": true }

Errors: 400 — Zod validation failure or duplicate ID

Create a profile with a system prompt and behavioral smoke tests:

// Create a profile with smoke tests for verification
const res: Response = await fetch('/api/profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  id: 'security-reviewer',
  name: 'Security Reviewer',
  version: '1.0.0',
  domain: 'work',
  tags: ['security', 'code-review', 'owasp'],
  skillMd: 'You are a security-focused code reviewer. Check every change against the OWASP Top 10. Flag SQL injection, XSS, CSRF, and auth bypass risks. Always suggest remediations.',
  supportedRuntimes: ['claude-code', 'anthropic-direct'],
  tests: [
    {
      task: 'Review this code for SQL injection: db.query("SELECT * FROM users WHERE id = " + userId)',
      expectedKeywords: ['SQL injection', 'parameterized', 'prepared statement'],
    },
  ],
}),
});

if (res.status === 201) {
console.log('Profile created — run /test to verify behavior');
}

Get Profile

GET /api/profiles/{id}

Retrieve a single profile with scope and read-only metadata.

Response 200 — Full profile object with isBuiltin, scope, and readOnly fields

Errors: 404 — Profile not found

Fetch a profile to inspect its full SKILL.md and configuration before assigning it to a task:

// Fetch the full profile including SKILL.md content
const profile: Profile = await fetch('/api/profiles/security-reviewer')
.then(r => r.json());

console.log(`${profile.name} v${profile.version} [${profile.scope}]`);
console.log(`Runtimes: ${profile.supportedRuntimes.join(', ')}`);
console.log(`SKILL.md: ${profile.skillMd.substring(0, 100)}...`);

Update Profile

PUT /api/profiles/{id}

Replace a user profile's configuration and SKILL.md. Built-in and project-scoped profiles cannot be modified through this endpoint.

Request Body

FieldTypeReqDescription
idstring*Profile identifier (must match URL)
namestring*Display name
versionstring*Semantic version (x.y.z)
domainenum*work or personal
tagsstring[]*Categorization tags
skillMdstringUpdated SKILL.md content

Response 200{ "ok": true }

Errors: 400 — Validation failure, 403 — Built-in or project-scoped profile, 404 — Not found

Update a profile’s system prompt and bump the version:

// Update the profile and bump version
await fetch('/api/profiles/security-reviewer', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  id: 'security-reviewer',
  name: 'Security Reviewer',
  version: '1.1.0',
  domain: 'work',
  tags: ['security', 'code-review', 'owasp', 'sast'],
  skillMd: 'You are a senior security reviewer. Check all changes against OWASP Top 10 and CWE Top 25...',
}),
});

Delete Profile

DELETE /api/profiles/{id}

Permanently delete a user profile. Built-in and project-scoped profiles cannot be deleted.

Response 200{ "ok": true }

Errors: 400 — Deletion failure, 403 — Built-in or project-scoped profile

// Delete a user profile — built-ins and project profiles are protected
const res: Response = await fetch('/api/profiles/security-reviewer', { method: 'DELETE' });

if (res.status === 403) {
console.log('Cannot delete built-in or project-scoped profiles');
}

Run Profile Tests

POST /api/profiles/{id}/test

Execute all behavioral smoke tests for a profile against a specified runtime. Each test sends a task prompt and checks for expected keywords in the response. Results are persisted for later retrieval.

Request Body

FieldTypeReqDescription
runtimeIdenumAgent runtime to test against(default: claude-code)

Response 200 — Test report with per-test pass/fail results

Errors: 400 — Invalid runtime or test execution failure, 429 — Budget limit exceeded

Run all smoke tests for a profile to verify it produces the expected output keywords:

// Run all profile tests and report results
const report: TestReport = await fetch('/api/profiles/security-reviewer/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ runtimeId: 'claude-code' }),
}).then(r => r.json());

console.log(`${report.passed}/${report.total} tests passed`);
report.results.forEach(t => {
console.log(`  [${t.pass ? 'PASS' : 'FAIL'}] ${t.task.substring(0, 60)}...`);
});

Example response:

{
  "profileId": "security-reviewer",
  "runtimeId": "claude-code",
  "total": 2,
  "passed": 2,
  "results": [
    {
      "task": "Review this code for SQL injection: db.query(\"SELECT * FROM users WHERE id = \" + userId)",
      "pass": true,
      "matchedKeywords": ["SQL injection", "parameterized", "prepared statement"],
      "missingKeywords": [],
      "durationMs": 4200
    },
    {
      "task": "Check this login form for XSS vulnerabilities",
      "pass": true,
      "matchedKeywords": ["XSS", "sanitize", "escape"],
      "missingKeywords": [],
      "durationMs": 3800
    }
  ]
}

Run Single Test

POST /api/profiles/{id}/test-single

Run a single profile test by index. Used by the client for real-time progress during test execution.

Request Body

FieldTypeReqDescription
testIndexnumber*Zero-based index of the test to run
runtimeIdenumAgent runtime to test against(default: claude-code)

Response 200 — Single test result with pass/fail and keyword matches

Errors: 400 — Invalid test index or unsupported runtime, 404 — Profile not found, 429 — Budget limit exceeded

Run a single test for real-time progress feedback in the UI:

// Run tests one by one for real-time progress display
const profile: Profile = await fetch('/api/profiles/security-reviewer').then(r => r.json());
const testCount: number = profile.tests?.length ?? 0;

for (let i = 0; i < testCount; i++) {
console.log(`Running test ${i + 1}/${testCount}...`);

const result: TestResult = await fetch('/api/profiles/security-reviewer/test-single', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ testIndex: i, runtimeId: 'claude-code' }),
}).then(r => r.json());

console.log(`  [${result.pass ? 'PASS' : 'FAIL'}] ${result.durationMs}ms`);
}

Get Test Results

GET /api/profiles/{id}/test-results

Retrieve the most recent persisted test report for a profile and runtime combination.

Query Parameters

Param Type Req Description
runtimeId enum Runtime to filter results for

Response 200 — Test report object

Errors: 404 — No test results found

// Check saved test results without re-running tests
const results: TestReport = await fetch(
'/api/profiles/security-reviewer/test-results?runtimeId=claude-code'
).then(r => r.json());

console.log(`Last tested: ${results.passed}/${results.total} passed`);

Get Learned Context

GET /api/profiles/{id}/context

Retrieve the version history and size info for a profile's learned context. Learned context accumulates facts, preferences, and patterns from agent execution.

Response 200 — Version history and size information

Response Body

FieldTypeReqDescription
historyobject[]*Array of context versions with content and metadata
currentSizenumber*Current context size in bytes
maxSizenumber*Maximum allowed context size

Check how much learned context a profile has accumulated and view its version history:

// Check learned context size and version history
const ctx: ContextInfo = await fetch('/api/profiles/data-analyst/context')
.then(r => r.json());

const pct: string = ((ctx.currentSize / ctx.maxSize) * 100).toFixed(1);
console.log(`Context: ${ctx.currentSize} / ${ctx.maxSize} bytes (${pct}%)`);
console.log(`Versions: ${ctx.history.length}`);

Example response:

{
  "history": [
    {
      "version": 3,
      "content": "- User prefers seaborn over matplotlib\n- Always use pandas for CSV processing\n- Include confidence intervals in reports",
      "source": "agent",
      "createdAt": "2026-04-02T16:30:00.000Z"
    },
    {
      "version": 2,
      "content": "- User prefers seaborn over matplotlib\n- Always use pandas for CSV processing",
      "source": "operator",
      "createdAt": "2026-03-28T11:00:00.000Z"
    }
  ],
  "currentSize": 1842,
  "maxSize": 10240
}

Add Learned Context

POST /api/profiles/{id}/context

Manually add context content to a profile (operator injection). Creates a new version in the context history.

Request Body

FieldTypeReqDescription
additionsstring*Context content to add

Response 200{ "ok": true }

Errors: 400 — Missing or empty additions

Manually inject context into a profile — useful for bootstrapping preferences before the agent learns them naturally:

// Inject context manually — creates a new version
await fetch('/api/profiles/data-analyst/context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  additions: 'Always use pandas for CSV processing. Prefer seaborn for visualizations. Include confidence intervals in statistical reports.',
}),
});

Manage Learned Context

PATCH /api/profiles/{id}/context

Approve, reject, or rollback learned context proposals. Approve and reject require a notificationId from a pending context proposal. Rollback requires a target version number.

Request Body

FieldTypeReqDescription
actionenum*approve, reject, or rollback
notificationIdstringNotification ID (required for approve/reject)
targetVersionnumberVersion number (required for rollback)
editedContentstringEdited content to replace the proposal (approve only)

Response 200{ "ok": true }

Errors: 400 — Missing required fields or invalid action

Approve an agent’s learned context proposal, or rollback to an earlier version if something went wrong:

// Approve a pending context proposal from the agent
await fetch('/api/profiles/data-analyst/context', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  action: 'approve',
  notificationId: 'notif-ctx-789',
}),
});

// Or rollback to a previous version if the latest context is wrong
await fetch('/api/profiles/data-analyst/context', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  action: 'rollback',
  targetVersion: 2,
}),
});

AI Profile Assist

POST /api/profiles/assist

Use AI to generate or refine a profile's SKILL.md, tags, and configuration from a natural language goal description. Supports generate mode (new profile) and refine mode (improve existing).

Request Body

FieldTypeReqDescription
goalstring*Natural language description of the desired profile behavior
domainenumwork or personal
modeenumgenerate (new) or refine (improve existing)(default: generate)
existingSkillMdstringCurrent SKILL.md content (for refine mode)
existingTagsstring[]Current tags (for refine mode)

Response 200 — Generated profile configuration and SKILL.md

Errors: 400 — Missing goal, 429 — Budget limit exceeded, 500 — Generation failure

Generate a complete profile from a natural language description — the AI produces a SKILL.md, tags, and configuration:

// Generate a new profile from a natural language goal
const generated: GeneratedProfile = await fetch('/api/profiles/assist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  goal: 'A profile that reviews pull requests for security issues, focusing on OWASP Top 10 and input validation',
  domain: 'work',
  mode: 'generate',
}),
}).then(r => r.json());

console.log(`Suggested name: ${generated.name}`);
console.log(`Tags: ${generated.tags.join(', ')}`);
console.log(`SKILL.md preview: ${generated.skillMd.substring(0, 200)}...`);

// Use the generated config to create the profile
await fetch('/api/profiles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(generated),
});

Example response:

{
  "id": "security-pr-reviewer",
  "name": "Security PR Reviewer",
  "version": "1.0.0",
  "domain": "work",
  "tags": ["security", "code-review", "owasp", "input-validation"],
  "skillMd": "You are a security-focused code reviewer specializing in pull request analysis...",
  "supportedRuntimes": ["claude-code", "anthropic-direct"],
  "allowedTools": ["Read", "Grep", "Glob", "Bash"]
}

Import from URL

POST /api/profiles/import

Import a profile from a GitHub URL. Fetches profile.yaml and optionally SKILL.md from the repository. Supports raw GitHub URLs and github.com tree/blob URLs.

Request Body

FieldTypeReqDescription
urlstring (URL)*GitHub URL to a profile directory or profile.yaml file

Response 201 Created{ "ok": true, "id": "...", "name": "..." }

Errors: 400 — Invalid URL, non-GitHub URL, fetch failure, or invalid profile.yaml

Import a profile from a GitHub repository — fetches the YAML config and SKILL.md:

// Import a profile from GitHub
const { id, name }: { id: string; name: string } = await fetch('/api/profiles/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  url: 'https://github.com/acme/ainative-profiles/tree/main/.claude/skills/security-reviewer',
}),
}).then(r => r.json());

console.log(`Imported "${name}" as ${id}`);

List Repo Imports

GET /api/profiles/import-repo

List all repository import records, most recent first.

Response 200 — Array of repo import records with parsed profileIds

// List all repository imports
const imports: RepoImport[] = await fetch('/api/profiles/import-repo')
.then(r => r.json());

imports.forEach(i => {
console.log(`${i.url} → ${i.profileIds.join(', ')}`);
});

Repository Import

Import entire GitHub repositories of skills in a multi-step flow: scan the repo to discover available skills, preview adaptations with dedup detection, confirm the batch import, then check and apply upstream updates later. Each step feeds into the next.

Scan Repository

POST /api/profiles/import-repo/scan

Scan a GitHub repository to discover importable skill directories. Returns a list of discovered skills with their detected formats (ainative-native or SKILL.md-only).

Request Body

FieldTypeReqDescription
urlstring (URL)*GitHub URL of the repository to scan (github.com or raw.githubusercontent.com)

Response Body

FieldTypeReqDescription
ownerstring*GitHub repository owner
repostring*Repository name
branchstring*Default branch name
commitShastring*Current HEAD commit SHA
repoUrlstring*Canonical repository URL
skillsDiscoveredSkill[]*Array of discovered skills with path, name, and format

Scan a skills repository to see what profiles are available before committing to an import:

// Step 1: Scan a GitHub repository for importable skills
interface DiscoveredSkill {
path: string;
name: string;
format: 'ainative-business' | 'skill-md-only';
}
interface ScanResult {
owner: string;
repo: string;
branch: string;
commitSha: string;
repoUrl: string;
skills: DiscoveredSkill[];
}

const scan: ScanResult = await fetch('/api/profiles/import-repo/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  url: 'https://github.com/acme/ainative-skills',
}),
}).then(r => r.json());

console.log(`Found ${scan.skills.length} skills in ${scan.owner}/${scan.repo}@${scan.branch}`);
scan.skills.forEach(s => console.log(`  ${s.name} (${s.format}) — ${s.path}`));

Example response:

{
  "owner": "acme",
  "repo": "ainative-skills",
  "branch": "main",
  "commitSha": "a1b2c3d4e5f6789012345678901234567890abcd",
  "repoUrl": "https://github.com/acme/ainative-skills",
  "skills": [
    { "path": ".claude/skills/security-reviewer", "name": "security-reviewer", "format": "`ainative-business`" },
    { "path": ".claude/skills/data-analyst", "name": "data-analyst", "format": "skill-md-only" }
  ]
}

Errors: 400 — Invalid or missing URL, scan failure

Preview Repository Import

POST /api/profiles/import-repo/preview

Fetch selected skills from a scanned repository, run format adaptation, and check for duplicates against existing profiles. Returns previews with adapted configs and dedup status.

Request Body

FieldTypeReqDescription
ownerstring*Repository owner from scan result
repostring*Repository name from scan result
branchstring*Branch name from scan result
commitShastring*Commit SHA from scan result
repoUrlstring*Repository URL from scan result
repoReadmestringRepository README content for enrichment context
skillsDiscoveredSkill[]*Skills to preview (subset of discovered skills)

Response Body

FieldTypeReqDescription
previewsobject[]*Per-skill preview with adapted config, SKILL.md, and dedup result
previews[].skillobject*Original discovered skill metadata
previews[].configobject | null*Adapted profile config (null on fetch error)
previews[].skillMdstring | null*Adapted SKILL.md content
previews[].dedupobject | null*Dedup result: status (new/duplicate/similar), matchReason, similarity, matchedProfileId
previews[].errorstring | null*Error message if fetch/adaptation failed

Preview skills before importing — inspect dedup warnings and adapted configs to decide which to import:

// Step 2: Preview selected skills with dedup detection
interface Preview {
skill: { name: string; path: string };
config: { id: string; name: string } | null;
dedup: { status: string; similarity?: number; matchedProfileId?: string } | null;
error: string | null;
}

const { previews }: { previews: Preview[] } = await fetch('/api/profiles/import-repo/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  owner: scan.owner,
  repo: scan.repo,
  branch: scan.branch,
  commitSha: scan.commitSha,
  repoUrl: scan.repoUrl,
  skills: scan.skills,
}),
}).then(r => r.json());

previews.forEach(p => {
if (p.error) {
  console.log(`  [ERROR] ${p.skill.name}: ${p.error}`);
} else if (p.dedup?.status === 'duplicate') {
  console.log(`  [SKIP] ${p.config?.id} — duplicate of ${p.dedup.matchedProfileId}`);
} else if (p.dedup?.status === 'similar') {
  console.log(`  [WARN] ${p.config?.id} — ${p.dedup.similarity?.toFixed(0)}% similar to ${p.dedup.matchedProfileId}`);
} else {
  console.log(`  [NEW] ${p.config?.id}`);
}
});

Errors: 400 — Missing required fields or preview failure

Confirm Repository Import

POST /api/profiles/import-repo/confirm

Execute a batch import of previewed profiles. Each item specifies an action: import (create new), replace (overwrite existing), or skip. Records a repo import entry in the database. Returns counts and any per-profile errors.

Request Body

FieldTypeReqDescription
repoUrlstring*Repository URL
ownerstring*Repository owner
repostring*Repository name
branchstring*Branch name
commitShastring*Commit SHA at import time
importsImportItem[]*Array of import items with config, skillMd, and action
imports[].configProfileConfig*Adapted profile config from preview
imports[].skillMdstring*Adapted SKILL.md content from preview
imports[].actionenum*import (create new), replace (overwrite existing), or skip

Response Body

FieldTypeReqDescription
okboolean*Overall success indicator
importednumber*Number of newly created profiles
replacednumber*Number of replaced profiles
skippednumber*Number of skipped profiles
profileIdsstring[]*IDs of successfully imported/replaced profiles
errorsstring[]Per-profile error messages (omitted if none)

Confirm the import after reviewing previews — decide the action for each profile:

// Step 3: Confirm the import with per-profile actions
interface ImportResult {
ok: boolean;
imported: number;
replaced: number;
skipped: number;
profileIds: string[];
errors?: string[];
}

const result: ImportResult = await fetch('/api/profiles/import-repo/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  repoUrl: 'https://github.com/acme/ainative-skills',
  owner: 'acme',
  repo: 'ainative-skills',
  branch: 'main',
  commitSha: 'a1b2c3d4e5f6789012345678901234567890abcd',
  imports: previews
    .filter(p => p.config && !p.error)
    .map(p => ({
      config: p.config,
      skillMd: p.skillMd,
      // Skip duplicates, replace similar, import new
      action: p.dedup?.status === 'duplicate' ? 'skip'
            : p.dedup?.status === 'similar'    ? 'replace'
            : 'import',
    })),
}),
}).then(r => r.json());

console.log(`Imported: ${result.imported}, Replaced: ${result.replaced}, Skipped: ${result.skipped}`);
console.log(`Profile IDs: ${result.profileIds.join(', ')}`);
if (result.errors?.length) {
console.error(`Errors: ${result.errors.join(', ')}`);
}

Example response:

{
  "ok": true,
  "imported": 2,
  "replaced": 0,
  "skipped": 1,
  "profileIds": ["acme-security-reviewer", "acme-data-analyst"]
}

Errors: 400 — Missing required fields or validation failure

Check for Updates

POST /api/profiles/import-repo/check-updates

Check whether imported profiles have new versions available in their source repositories. Compares local content hashes against the current remote SKILL.md. Groups profiles by repo to minimize GitHub API calls.

Request Body

FieldTypeReqDescription
repoImportIdstringCheck all profiles from a specific repo import record
profileIdstringCheck a single profile by ID (requires importMeta)

Response Body

FieldTypeReqDescription
hasUpdatesboolean*True if any profile has an available update
updatesobject[]*Per-profile update status
updates[].profileIdstring*Profile identifier
updates[].profileNamestring*Profile display name
updates[].localHashstring*Content hash of the locally stored SKILL.md
updates[].remoteHashstring*Content hash of the remote SKILL.md (or "unknown"/"fetch-error")
updates[].hasUpdateboolean*True if remote hash differs from local hash

Check for updates after a repo import — useful for a periodic “keep profiles fresh” workflow:

// Step 4: Check if any imported profiles have updates available
interface UpdateCheck {
profileId: string;
profileName: string;
localHash: string;
remoteHash: string;
hasUpdate: boolean;
}

const { hasUpdates, updates }: { hasUpdates: boolean; updates: UpdateCheck[] } =
await fetch('/api/profiles/import-repo/check-updates', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ repoImportId: 'reimport-uuid-here' }),
}).then(r => r.json());

if (!hasUpdates) {
console.log('All profiles are up to date');
} else {
updates.filter(u => u.hasUpdate).forEach(u => {
  console.log(`${u.profileName} — update available`);
  console.log(`  Local:  ${u.localHash}`);
  console.log(`  Remote: ${u.remoteHash}`);
});
}

Example response:

{
  "hasUpdates": true,
  "updates": [
    {
      "profileId": "acme-security-reviewer",
      "profileName": "Acme Security Reviewer",
      "localHash": "sha256-abc123",
      "remoteHash": "sha256-def456",
      "hasUpdate": true
    },
    {
      "profileId": "acme-data-analyst",
      "profileName": "Acme Data Analyst",
      "localHash": "sha256-789xyz",
      "remoteHash": "sha256-789xyz",
      "hasUpdate": false
    }
  ]
}

Errors: 400 — Neither repoImportId nor profileId provided, or profile has no import metadata, 404 — Repo import record not found

Apply Updates

POST /api/profiles/import-repo/apply-updates

Apply accepted updates to imported profiles. Fetches the latest SKILL.md from the source repository, re-runs format enrichment, updates the local profile, and refreshes the content hash. Profiles where accept is false are skipped without modification.

Request Body

FieldTypeReqDescription
updatesobject[]*Array of update decisions
updates[].profileIdstring*Profile ID to update
updates[].acceptboolean*Whether to apply this update (false = skip)

Response Body

FieldTypeReqDescription
okboolean*Overall success indicator
appliednumber*Number of profiles successfully updated
skippednumber*Number of profiles skipped (accept: false)
errorsstring[]Per-profile error messages (omitted if none)

Apply updates for profiles where updates are available — selectively accept or decline each update:

// Step 5: Apply accepted updates, skip declined ones
interface ApplyResult {
ok: boolean;
applied: number;
skipped: number;
errors?: string[];
}

// Only apply updates for profiles where hasUpdate is true
const updateDecisions = updates.map(u => ({
profileId: u.profileId,
accept: u.hasUpdate,  // accept available updates, skip unchanged
}));

const result: ApplyResult = await fetch('/api/profiles/import-repo/apply-updates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ updates: updateDecisions }),
}).then(r => r.json());

console.log(`Applied: ${result.applied}, Skipped: ${result.skipped}`);
if (result.errors?.length) {
console.error(`Errors: ${result.errors.join(', ')}`);
}

Errors: 400 — No updates provided or missing import metadata on a profile

Profile Config Schema

Profiles are validated with Zod. The key fields for the ProfileConfigSchema:

FieldTypeRequiredDescription
idstringYesUnique kebab-case identifier
namestringYesDisplay name
versionstring (semver)YesMust match x.y.z format
domainenumYeswork or personal
tagsstring[]YesCategorization tags
allowedToolsstring[]NoTool allowlist
mcpServersobjectNoMCP server configurations
canUseToolPolicyobjectNo{ autoApprove?: string[], autoDeny?: string[] }
maxTurnsnumberNoMaximum agent conversation turns
supportedRuntimesenum[]NoArray of runtime IDs
preferredRuntimeenumNoPreferred runtime for auto-routing
runtimeOverridesobjectNoPer-runtime instruction/tool/MCP overrides
capabilityOverridesobjectNoPer-runtime model, thinking, and server tool overrides
testsobject[]No{ task: string, expectedKeywords: string[] }[]

Supported Runtimes

Runtime IDLabel
claude-codeClaude Code
openai-codex-app-serverOpenAI Codex App Server
anthropic-directAnthropic Direct API
openai-directOpenAI Direct API
ollamaOllama (Local)