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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| id | string | * | Profile identifier (kebab-case slug) |
| name | string | * | Display name |
| description | string | * | Brief description of the profile behavior |
| domain | enum | * | work or personal |
| tags | string[] | * | Categorization tags |
| skillMd | string | * | Full SKILL.md content (system prompt + behavioral instructions) |
| allowedTools | string[] | — | Tool allowlist for agent execution |
| mcpServers | object | — | MCP server configurations |
| canUseToolPolicy | object | — | Auto-approve/auto-deny tool policies |
| maxTurns | number | — | Maximum agent conversation turns |
| outputFormat | string | — | Expected output format |
| version | string | * | Semantic version (x.y.z) |
| author | string | — | Profile author |
| source | string (URL) | — | Source URL |
| supportedRuntimes | string[] | * | Compatible agent runtimes |
| runtimeOverrides | object | — | Per-runtime instruction, tool, and MCP overrides |
| isBuiltin | boolean | * | Whether this is a shipped built-in profile |
| scope | enum | * | builtin, user, or project |
| origin | enum | — | How created: manual, environment, import, or ai-assist |
| readOnly | boolean | * | 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
/api/profiles Create a new user profile. The request body must pass Zod validation against ProfileConfigSchema. Provide the SKILL.md content separately.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| id | string | * | Profile identifier (must be unique, kebab-case) |
| name | string | * | Display name |
| version | string | * | Semantic version (x.y.z format) |
| domain | enum | * | work or personal |
| tags | string[] | * | Categorization tags |
| skillMd | string | — | SKILL.md content (system prompt) |
| allowedTools | string[] | — | Tool allowlist |
| mcpServers | object | — | MCP server configurations |
| maxTurns | number | — | Max agent turns |
| supportedRuntimes | string[] | — | Compatible runtimes |
| tests | ProfileSmokeTest[] | — | 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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| id | string | * | Profile identifier (must match URL) |
| name | string | * | Display name |
| version | string | * | Semantic version (x.y.z) |
| domain | enum | * | work or personal |
| tags | string[] | * | Categorization tags |
| skillMd | string | — | Updated 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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| runtimeId | enum | — | Agent 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| testIndex | number | * | Zero-based index of the test to run |
| runtimeId | enum | — | Agent 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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| history | object[] | * | Array of context versions with content and metadata |
| currentSize | number | * | Current context size in bytes |
| maxSize | number | * | 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
/api/profiles/{id}/context Manually add context content to a profile (operator injection). Creates a new version in the context history.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| additions | string | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| action | enum | * | approve, reject, or rollback |
| notificationId | string | — | Notification ID (required for approve/reject) |
| targetVersion | number | — | Version number (required for rollback) |
| editedContent | string | — | Edited 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| goal | string | * | Natural language description of the desired profile behavior |
| domain | enum | — | work or personal |
| mode | enum | — | generate (new) or refine (improve existing)(default: generate) |
| existingSkillMd | string | — | Current SKILL.md content (for refine mode) |
| existingTags | string[] | — | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| url | string (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
/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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| url | string (URL) | * | GitHub URL of the repository to scan (github.com or raw.githubusercontent.com) |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| owner | string | * | GitHub repository owner |
| repo | string | * | Repository name |
| branch | string | * | Default branch name |
| commitSha | string | * | Current HEAD commit SHA |
| repoUrl | string | * | Canonical repository URL |
| skills | DiscoveredSkill[] | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| owner | string | * | Repository owner from scan result |
| repo | string | * | Repository name from scan result |
| branch | string | * | Branch name from scan result |
| commitSha | string | * | Commit SHA from scan result |
| repoUrl | string | * | Repository URL from scan result |
| repoReadme | string | — | Repository README content for enrichment context |
| skills | DiscoveredSkill[] | * | Skills to preview (subset of discovered skills) |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| previews | object[] | * | Per-skill preview with adapted config, SKILL.md, and dedup result |
| previews[].skill | object | * | Original discovered skill metadata |
| previews[].config | object | null | * | Adapted profile config (null on fetch error) |
| previews[].skillMd | string | null | * | Adapted SKILL.md content |
| previews[].dedup | object | null | * | Dedup result: status (new/duplicate/similar), matchReason, similarity, matchedProfileId |
| previews[].error | string | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| repoUrl | string | * | Repository URL |
| owner | string | * | Repository owner |
| repo | string | * | Repository name |
| branch | string | * | Branch name |
| commitSha | string | * | Commit SHA at import time |
| imports | ImportItem[] | * | Array of import items with config, skillMd, and action |
| imports[].config | ProfileConfig | * | Adapted profile config from preview |
| imports[].skillMd | string | * | Adapted SKILL.md content from preview |
| imports[].action | enum | * | import (create new), replace (overwrite existing), or skip |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| ok | boolean | * | Overall success indicator |
| imported | number | * | Number of newly created profiles |
| replaced | number | * | Number of replaced profiles |
| skipped | number | * | Number of skipped profiles |
| profileIds | string[] | * | IDs of successfully imported/replaced profiles |
| errors | string[] | — | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| repoImportId | string | — | Check all profiles from a specific repo import record |
| profileId | string | — | Check a single profile by ID (requires importMeta) |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| hasUpdates | boolean | * | True if any profile has an available update |
| updates | object[] | * | Per-profile update status |
| updates[].profileId | string | * | Profile identifier |
| updates[].profileName | string | * | Profile display name |
| updates[].localHash | string | * | Content hash of the locally stored SKILL.md |
| updates[].remoteHash | string | * | Content hash of the remote SKILL.md (or "unknown"/"fetch-error") |
| updates[].hasUpdate | boolean | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| updates | object[] | * | Array of update decisions |
| updates[].profileId | string | * | Profile ID to update |
| updates[].accept | boolean | * | Whether to apply this update (false = skip) |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| ok | boolean | * | Overall success indicator |
| applied | number | * | Number of profiles successfully updated |
| skipped | number | * | Number of profiles skipped (accept: false) |
| errors | string[] | — | 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:
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique kebab-case identifier |
| name | string | Yes | Display name |
| version | string (semver) | Yes | Must match x.y.z format |
| domain | enum | Yes | work or personal |
| tags | string[] | Yes | Categorization tags |
| allowedTools | string[] | No | Tool allowlist |
| mcpServers | object | No | MCP server configurations |
| canUseToolPolicy | object | No | { autoApprove?: string[], autoDeny?: string[] } |
| maxTurns | number | No | Maximum agent conversation turns |
| supportedRuntimes | enum[] | No | Array of runtime IDs |
| preferredRuntime | enum | No | Preferred runtime for auto-routing |
| runtimeOverrides | object | No | Per-runtime instruction/tool/MCP overrides |
| capabilityOverrides | object | No | Per-runtime model, thinking, and server tool overrides |
| tests | object[] | No | { task: string, expectedKeywords: string[] }[] |
Supported Runtimes
| Runtime ID | Label |
|---|---|
| claude-code | Claude Code |
| openai-codex-app-server | OpenAI Codex App Server |
| anthropic-direct | Anthropic Direct API |
| openai-direct | OpenAI Direct API |
| ollama | Ollama (Local) |