Environment API

The Environment API scans project directories to discover tools, frameworks, and configuration artifacts. It powers profile suggestions, checkpoint/rollback safety, config file synchronization, and template capture for repeatable setups.

Quick Start

Scan a project directory, review discovered artifacts, and sync a config file — a typical onboarding flow:

// 1. Trigger an environment scan on a project directory
interface ScanSummary {
totalArtifacts: number;
personas: string[];
durationMs: number;
}

const { scan, summary }: { scan: { id: string }; summary: ScanSummary } =
await fetch('/api/environment/scan', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectDir: '/Users/team/my-app',
    projectId: 'proj-8f3a-4b2c',
  }),
}).then((r: Response) => r.json());

console.log(`Found ${summary.totalArtifacts} artifacts in ${summary.durationMs}ms`);
console.log(`Personas: ${summary.personas.join(', ')}`);

// 2. Browse discovered artifacts (e.g. config files, dependencies)
const { artifacts }: { artifacts: { name: string; tool: string }[] } =
await fetch('/api/environment/artifacts?category=config').then((r: Response) => r.json());
artifacts.forEach((a) => console.log(`  ${a.name} (${a.tool})`));

// 3. Sync a configuration file (with automatic checkpoint for safety)
const syncResult: { checkpointId: string; summary: { applied: number; failed: number } } =
await fetch('/api/environment/sync', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    operations: [{ targetPath: '.prettierrc', content: '{ "semi": false, "singleQuote": true }' }],
    label: 'Apply prettier config',
  }),
}).then((r: Response) => r.json());

// 4. Get profile suggestions based on the scan
const { curated, discovered }: { curated: unknown[]; discovered: unknown[] } =
await fetch('/api/environment/profiles/suggest?tiered=true').then((r: Response) => r.json());
console.log(`${curated.length} curated + ${discovered.length} discovered suggestions`);

Base URL

/api/environment

Scanning

Rescan If Stale

POST /api/environment/rescan-if-stale

Fire-and-forget rescan for chat session activation. Checks whether the last scan is fresh (under 5 minutes). Returns immediately without blocking — the scan runs in the background. Never returns 500; environment issues are logged but never surface to the caller.

Response Body

FieldTypeReqDescription
scannedboolean*True if a new scan was triggered; false if the last scan was fresh enough

Call this at chat session start to keep the environment scan fresh without blocking the UI:

// Trigger a background rescan on chat session activation
const { scanned }: { scanned: boolean } = await fetch('/api/environment/rescan-if-stale', {
method: 'POST',
}).then((r: Response) => r.json());

if (scanned) {
console.log('Background rescan triggered — environment data will refresh shortly');
} else {
console.log('Scan is fresh — no rescan needed');
}

Trigger Environment Scan

POST /api/environment/scan

Run a new environment scan against a project directory. Discovers artifacts, tools, and framework personas.

Request Body

FieldTypeReqDescription
projectDirstring—Directory to scan (defaults to launch CWD)
projectIdstring—Project ID to associate the scan with

Response Body

FieldTypeReqDescription
scanobject*Scan record with ID, timestamps, and metadata
summary.totalArtifactsnumber*Total artifacts discovered
summary.personasstring[]*Detected tool personas (e.g., node, python, rust)
summary.categoryCountsobject*Artifact counts by category
summary.toolCountsobject*Artifact counts by tool/framework
summary.durationMsnumber*Scan duration in milliseconds
summary.errorsnumber*Number of scan errors

Scan a project to discover its toolchain and generate profile suggestions:

// Scan a project directory to discover its environment
interface ScanResult {
scan: { id: string };
summary: {
  totalArtifacts: number;
  personas: string[];
  categoryCounts: Record<string, number>;
  durationMs: number;
};
}

const { scan, summary }: ScanResult = await fetch('/api/environment/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  projectDir: '/Users/team/my-app',
  projectId: 'proj-8f3a-4b2c',
}),
}).then((r: Response) => r.json());

console.log(`Scan ${scan.id}: ${summary.totalArtifacts} artifacts in ${summary.durationMs}ms`);
console.log(`Personas: ${summary.personas.join(', ')}`);
console.log(`Categories: ${JSON.stringify(summary.categoryCounts)}`);

Example response:

{
  "scan": {
    "id": "scan-1a2b-3c4d",
    "projectDir": "/Users/team/my-app",
    "projectId": "proj-8f3a-4b2c",
    "createdAt": "2026-04-03T10:00:00.000Z"
  },
  "summary": {
    "totalArtifacts": 24,
    "personas": ["node", "typescript", "react"],
    "categoryCounts": { "config": 12, "dependency": 8, "build": 4 },
    "toolCounts": { "node": 14, "typescript": 6, "eslint": 4 },
    "durationMs": 342,
    "errors": 0
  }
}

Get Latest Scan

GET /api/environment/scan

Retrieve the latest scan result. Auto-triggers a fresh scan if projectDir is provided and the cached scan is stale.

Query Parameters

Param Type Req Description
projectId string — Filter by project ID
scanId string — Fetch a specific scan by ID
projectDir string — Project directory (triggers auto-scan if stale)

Response Body

FieldTypeReqDescription
scanobject | null*Scan record or null if none found
summary.categoryCountsobject—Artifact counts by category
summary.toolCountsobject—Artifact counts by tool/framework

Fetch the latest scan for a project — or pass projectDir to auto-trigger a fresh scan if stale:

// Get the latest scan, auto-refresh if stale
const { scan, summary }: {
scan: { createdAt: string } | null;
summary: { categoryCounts: Record<string, number> };
} = await fetch('/api/environment/scan?projectId=proj-8f3a-4b2c')
.then((r: Response) => r.json());

if (scan) {
console.log(`Last scanned: ${scan.createdAt}`);
console.log(`Artifacts: ${JSON.stringify(summary.categoryCounts)}`);
} else {
console.log('No scan found — trigger one with POST /api/environment/scan');
}

Artifacts

List Artifacts

GET /api/environment/artifacts

Filtered artifact list from the latest (or specified) scan.

Query Parameters

Param Type Req Description
scanId string — Scan ID (defaults to latest)
category string — Filter by artifact category
tool string — Filter by tool persona
scope string — Filter by artifact scope
search string — Free-text search across artifact names

Response Body

FieldTypeReqDescription
artifactsobject[]*Array of artifact objects
countnumber*Total matching artifacts

Browse discovered config files for a specific tool — useful for reviewing what the scanner found:

// List Node.js config artifacts
interface Artifact {
name: string;
filePath: string;
}

const { artifacts, count }: { artifacts: Artifact[]; count: number } =
await fetch('/api/environment/artifacts?category=config&tool=node')
  .then((r: Response) => r.json());

console.log(`${count} Node.js config artifacts:`);
artifacts.forEach((a: Artifact) => console.log(`  ${a.name} — ${a.filePath}`));

Example response:

{
  "artifacts": [
    {
      "id": "art-5e6f-7g8h",
      "scanId": "scan-1a2b-3c4d",
      "name": "package.json",
      "category": "config",
      "tool": "node",
      "scope": "project",
      "filePath": "/Users/team/my-app/package.json"
    },
    {
      "id": "art-9i0j-1k2l",
      "scanId": "scan-1a2b-3c4d",
      "name": ".nvmrc",
      "category": "config",
      "tool": "node",
      "scope": "project",
      "filePath": "/Users/team/my-app/.nvmrc"
    }
  ],
  "count": 2
}

Get Artifact

GET /api/environment/artifacts/{id}

Retrieve a single artifact with full detail.

Response 200 — { "artifact": <object> }

const { artifact }: { artifact: { name: string; tool: string; filePath: string } } =
await fetch('/api/environment/artifacts/art-5e6f-7g8h').then((r: Response) => r.json());
console.log(`${artifact.name} (${artifact.tool}) at ${artifact.filePath}`);

Errors: 404 — Artifact not found

Skills

List Skills

GET /api/environment/skills

List all available skills enriched with environment scan context. Returns skills discovered from the current project directory scan, providing a unified view of what agent profiles are active in the environment.

Response 200 — Array of enriched skill objects

// List all skills enriched with environment context
interface EnrichedSkill {
id: string;
name: string;
description: string;
filePath: string;
scope: string;
}

const skills: EnrichedSkill[] = await fetch('/api/environment/skills')
.then((r: Response) => r.json());

console.log(`${skills.length} skills in environment:`);
skills.forEach((s: EnrichedSkill) => {
console.log(`  ${s.name} [${s.scope}] — ${s.filePath}`);
});

Example response:

[
  {
    "id": "data-analyst",
    "name": "Data Analyst",
    "description": "Statistical analysis with Python, pandas, and visualization best practices",
    "filePath": "/Users/team/.claude/skills/data-analyst",
    "scope": "user",
    "tags": ["analysis", "data", "python"],
    "version": "1.2.0"
  },
  {
    "id": "project-security-reviewer",
    "name": "Project Security Reviewer",
    "description": "OWASP-focused review for the payments service",
    "filePath": "/Users/team/my-app/.claude/skills/project-security-reviewer",
    "scope": "project",
    "tags": ["security", "code-review"],
    "version": "1.0.0"
  }
]

Errors: 500 — Scan or enrichment failure

Checkpoints

List Checkpoints

GET /api/environment/checkpoints

List all checkpoints, optionally filtered by project.

Query Parameters

Param Type Req Description
projectId string — Filter checkpoints by project ID

Response Body

FieldTypeReqDescription
checkpointsobject[]*Array of checkpoint records
// List checkpoints for a project
interface Checkpoint {
label: string;
status: string;
filesCount: number;
createdAt: string;
}

const { checkpoints }: { checkpoints: Checkpoint[] } =
await fetch('/api/environment/checkpoints?projectId=proj-8f3a-4b2c')
  .then((r: Response) => r.json());

checkpoints.forEach((cp: Checkpoint) => {
console.log(`${cp.label} [${cp.status}] — ${cp.filesCount} files — ${cp.createdAt}`);
});

Create Checkpoint

POST /api/environment/checkpoints

Create a new checkpoint with git tag and/or file backup. Git repos get a tagged commit; global config files are backed up to a timestamped directory.

Request Body

FieldTypeReqDescription
labelstring—Checkpoint label (auto-generated if omitted)
checkpointTypeenum—pre-sync, manual, or pre-onboard (default: manual)
projectDirstring—Project directory for git checkpoint
globalPathsstring[]—Specific file paths to back up (defaults to common config files)

Response Body

FieldTypeReqDescription
checkpoint.idstring*Checkpoint identifier
checkpoint.labelstring*Checkpoint label
checkpoint.gitTagstring—Git tag name if applicable
checkpoint.gitCommitShastring—Git commit SHA
checkpoint.backupPathstring—Path to file backup directory
checkpoint.filesCountnumber*Number of files captured

Create a checkpoint before making risky changes — captures git state and config files for rollback:

// Create a manual checkpoint before risky changes
interface CheckpointResponse {
checkpoint: {
  id: string;
  filesCount: number;
  gitTag?: string;
  gitCommitSha?: string;
};
}

const { checkpoint }: CheckpointResponse = await fetch('/api/environment/checkpoints', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  label: 'Before dependency upgrade',
  projectDir: '/Users/team/my-app',
}),
}).then((r: Response) => r.json());

console.log(`Checkpoint ${checkpoint.id}: ${checkpoint.filesCount} files captured`);
if (checkpoint.gitTag) {
console.log(`Git tag: ${checkpoint.gitTag} (${checkpoint.gitCommitSha})`);
}

Example response:

{
  "checkpoint": {
    "id": "cp-4d5e-6f7g",
    "label": "Before dependency upgrade",
    "checkpointType": "manual",
    "status": "active",
    "gitTag": "ainative-cp-4d5e-6f7g",
    "gitCommitSha": "a1b2c3d4e5f6",
    "backupPath": "/Users/team/.`ainative-business`/checkpoints/cp-4d5e-6f7g",
    "filesCount": 8,
    "createdAt": "2026-04-03T10:00:00.000Z"
  }
}

Get Checkpoint Detail

GET /api/environment/checkpoints/{id}

Get checkpoint details including sync operations and git diff from the checkpoint commit to current HEAD.

Response Body

FieldTypeReqDescription
checkpointobject*Checkpoint record
syncOpsobject[]*Sync operations associated with this checkpoint
diffstring | null*Git diff since checkpoint (null if no git data)

Review what changed since a checkpoint — the diff shows all git changes since the checkpoint commit:

// Review changes since checkpoint
const { checkpoint, syncOps, diff }: {
checkpoint: { label: string; status: string };
syncOps: unknown[];
diff: string | null;
} = await fetch('/api/environment/checkpoints/cp-4d5e-6f7g')
.then((r: Response) => r.json());

console.log(`${checkpoint.label} [${checkpoint.status}]`);
console.log(`${syncOps.length} sync operations`);
if (diff) {
console.log(`Git changes since checkpoint:\n${diff}`);
}

Errors: 404 — Checkpoint not found

Rollback to Checkpoint

POST /api/environment/checkpoints/{id}

Rollback to a checkpoint. Restores git state and file backups. Only works on checkpoints with status 'active'.

Response Body

FieldTypeReqDescription
messagestring*Rollback confirmation message
results.gitobject—Git rollback result (success, error)
results.backupobject—File restore result (restored count, errors)

Rollback to a checkpoint when changes went wrong — restores both git state and backed-up files:

// Rollback to a checkpoint
interface RollbackResult {
message: string;
results: {
  git?: { success: boolean; error?: string };
  backup?: { restored: number };
};
}

const result: RollbackResult = await fetch('/api/environment/checkpoints/cp-4d5e-6f7g', {
method: 'POST',
}).then((r: Response) => r.json());

console.log(result.message);
if (result.results.git) {
console.log(`Git rollback: ${result.results.git.success ? 'OK' : result.results.git.error}`);
}
if (result.results.backup) {
console.log(`Restored ${result.results.backup.restored} files`);
}

Errors: 400 — Checkpoint not in active status, 404 — Not found, 500 — Git rollback failure

Profile Suggestions

Suggest Profiles

GET /api/environment/profiles/suggest

Suggest agent profiles based on the latest environment scan. Supports flat or tiered (curated vs. discovered) response formats.

Query Parameters

Param Type Req Description
scanId string — Scan ID (defaults to latest)
tiered string — When "true", return curated and discovered tiers separately

Response Body (flat)

FieldTypeReqDescription
suggestionsobject[]*Array of profile suggestions
countnumber*Total suggestions

Response Body (tiered)

FieldTypeReqDescription
curatedobject[]*High-confidence profile suggestions
discoveredobject[]*Lower-confidence discovered suggestions
curatedCountnumber*Number of curated suggestions
discoveredCountnumber*Number of discovered suggestions

Get tiered profile suggestions — curated suggestions are high confidence based on detected toolchains:

// Get tiered profile suggestions
interface Suggestion {
name: string;
description: string;
}

const { curated, discovered }: { curated: Suggestion[]; discovered: Suggestion[] } =
await fetch('/api/environment/profiles/suggest?tiered=true')
  .then((r: Response) => r.json());

console.log('Recommended profiles:');
curated.forEach((s: Suggestion) => console.log(`  [curated] ${s.name} — ${s.description}`));
discovered.forEach((s: Suggestion) => console.log(`  [discovered] ${s.name} — ${s.description}`));

Example response:

{
  "curated": [
    {
      "ruleId": "node-fullstack",
      "name": "Node.js Fullstack",
      "description": "Full-stack Node.js development with TypeScript and React",
      "confidence": 0.95,
      "matchedArtifacts": ["package.json", "tsconfig.json", "next.config.js"]
    }
  ],
  "discovered": [
    {
      "ruleId": "docker-ops",
      "name": "Docker Ops",
      "description": "Container management and Dockerfile optimization",
      "confidence": 0.6,
      "matchedArtifacts": ["Dockerfile"]
    }
  ],
  "curatedCount": 1,
  "discoveredCount": 1
}

Create Profile from Suggestion

POST /api/environment/profiles/create

Create an agent profile from a scan-based suggestion with optional overrides.

Request Body

FieldTypeReqDescription
suggestionobject*Profile suggestion object from /profiles/suggest
overrides.namestring—Override the suggested profile name
overrides.descriptionstring—Override the suggested description
overrides.systemPromptstring—Override the suggested system prompt

Response 201 — { "message": "Profile created", "profileId": "env-<ruleId>" }

Create a profile from a suggestion — optionally override the name or system prompt:

// Create a profile from the top curated suggestion
const profile: { message: string; profileId: string } = await fetch('/api/environment/profiles/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  suggestion: curated[0],
  overrides: { name: 'My Node Profile' },
}),
}).then((r: Response) => r.json());

console.log(`Profile created: ${profile.profileId}`);

Errors: 400 — Missing suggestion or creation failure

Config Sync

Preview Sync

POST /api/environment/sync/preview

Dry-run sync operations to see what would change without modifying any files.

Request Body

FieldTypeReqDescription
operationsSyncRequest[]*Array of sync operations to preview

Response Body

FieldTypeReqDescription
previewsobject[]*Preview results with diffs and conflict flags
summary.totalnumber*Total operations
summary.conflictsnumber*Operations with conflicts
summary.newFilesnumber*New files that would be created
summary.totalAdditionsnumber*Total lines added
summary.totalDeletionsnumber*Total lines removed

Preview a sync before executing — check for conflicts and see what would change:

// Preview what would change before applying
interface PreviewResult {
previews: unknown[];
summary: {
  total: number;
  conflicts: number;
  totalAdditions: number;
  totalDeletions: number;
};
}

const preview: PreviewResult = await fetch('/api/environment/sync/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  operations: [
    { targetPath: '.prettierrc', content: '{ "semi": false, "singleQuote": true }' },
  ],
}),
}).then((r: Response) => r.json());

console.log(`${preview.summary.total} operations, ${preview.summary.conflicts} conflicts`);
console.log(`+${preview.summary.totalAdditions} / -${preview.summary.totalDeletions} lines`);

Example response:

{
  "previews": [
    {
      "targetPath": ".prettierrc",
      "status": "new_file",
      "hasConflict": false,
      "diff": "+{ \"semi\": false, \"singleQuote\": true }"
    }
  ],
  "summary": {
    "total": 1,
    "conflicts": 0,
    "newFiles": 1,
    "totalAdditions": 1,
    "totalDeletions": 0
  }
}

Execute Sync

POST /api/environment/sync

Execute sync operations with an automatic pre-sync checkpoint for safety. Previews all operations first, then applies changes.

Request Body

FieldTypeReqDescription
operationsSyncRequest[]*Array of sync operations to execute
labelstring—Label for the auto-created checkpoint

Response Body

FieldTypeReqDescription
checkpointIdstring*ID of the auto-created pre-sync checkpoint
operationsobject[]*Operation results with status per operation
summary.appliednumber*Successfully applied operations
summary.failednumber*Failed operations
summary.totalnumber*Total operations attempted

Execute sync with a labeled checkpoint — the checkpoint is created automatically before any files are modified:

// Sync config files (automatic checkpoint created for safety)
interface SyncResult {
checkpointId: string;
summary: { applied: number; failed: number };
}

const result: SyncResult = await fetch('/api/environment/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  operations: [
    { targetPath: '.prettierrc', content: '{ "semi": false, "singleQuote": true }' },
  ],
  label: 'Apply prettier config',
}),
}).then((r: Response) => r.json());

console.log(`Checkpoint: ${result.checkpointId}`);
console.log(`Applied: ${result.summary.applied}, Failed: ${result.summary.failed}`);

Errors: 400 — No operations provided or no valid operations

Sync History

GET /api/environment/sync/history

List past sync operations grouped by their pre-sync checkpoints.

Query Parameters

Param Type Req Description
projectId string — Filter by project ID
limit number — Max checkpoints to return (default: 20)

Response Body

FieldTypeReqDescription
historyobject[]*Array of { checkpoint, operations } objects
countnumber*Number of history entries

Review past sync operations to see what was changed and when:

// Review recent sync history
interface SyncHistoryEntry {
checkpoint: { label: string; createdAt: string };
operations: unknown[];
}

const { history }: { history: SyncHistoryEntry[] } =
await fetch('/api/environment/sync/history?limit=5').then((r: Response) => r.json());

history.forEach((entry: SyncHistoryEntry) => {
console.log(`${entry.checkpoint.label} — ${entry.operations.length} ops — ${entry.checkpoint.createdAt}`);
});

Templates

List Templates

GET /api/environment/templates

List all captured environment templates.

Response Body

FieldTypeReqDescription
templatesobject[]*Array of template records
// List available environment templates
interface Template {
name: string;
description: string;
}

const { templates }: { templates: Template[] } =
await fetch('/api/environment/templates').then((r: Response) => r.json());
templates.forEach((t: Template) => console.log(`${t.name} — ${t.description}`));

Capture Template

POST /api/environment/templates

Capture a new template from an existing scan, preserving the environment configuration for reuse.

Request Body

FieldTypeReqDescription
scanIdstring*Scan ID to capture from
namestring*Template name
descriptionstring—Template description

Response 201 — { "template": <object> }

Capture a well-configured project environment as a reusable template:

// Capture the current environment as a template
const { template }: { template: { id: string } } = await fetch('/api/environment/templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  scanId: 'scan-1a2b-3c4d',
  name: 'Next.js Starter',
  description: 'Next.js 15 + TypeScript + Tailwind baseline configuration',
}),
}).then((r: Response) => r.json());

console.log(`Template saved: ${template.id}`);

Errors: 400 — Missing scanId or name

Get Template

GET /api/environment/templates/{id}

Get template detail with parsed manifest showing all captured configuration.

Response Body

FieldTypeReqDescription
templateobject*Template record
manifestobject*Parsed manifest with artifacts and config
const { template, manifest }: {
template: { name: string };
manifest: { artifacts: unknown[] };
} = await fetch('/api/environment/templates/tpl-1a2b-3c4d')
.then((r: Response) => r.json());
console.log(`${template.name}: ${manifest.artifacts.length} artifacts`);

Errors: 404 — Template not found

Delete Template

DELETE /api/environment/templates/{id}

Permanently delete a template.

Response 200 — { "message": "Template deleted" }

await fetch('/api/environment/templates/tpl-1a2b-3c4d', { method: 'DELETE' });

Errors: 404 — Template not found