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
Trigger Environment Scan
/api/environment/scan Run a new environment scan against a project directory. Discovers artifacts, tools, and framework personas.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| projectDir | string | — | Directory to scan (defaults to launch CWD) |
| projectId | string | — | Project ID to associate the scan with |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| scan | object | * | Scan record with ID, timestamps, and metadata |
| summary.totalArtifacts | number | * | Total artifacts discovered |
| summary.personas | string[] | * | Detected tool personas (e.g., node, python, rust) |
| summary.categoryCounts | object | * | Artifact counts by category |
| summary.toolCounts | object | * | Artifact counts by tool/framework |
| summary.durationMs | number | * | Scan duration in milliseconds |
| summary.errors | number | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| scan | object | null | * | Scan record or null if none found |
| summary.categoryCounts | object | — | Artifact counts by category |
| summary.toolCounts | object | — | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| artifacts | object[] | * | Array of artifact objects |
| count | number | * | 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
/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
Checkpoints
List Checkpoints
/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
| Field | Type | Req | Description |
|---|---|---|---|
| checkpoints | object[] | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| label | string | — | Checkpoint label (auto-generated if omitted) |
| checkpointType | enum | — | pre-sync, manual, or pre-onboard (default: manual) |
| projectDir | string | — | Project directory for git checkpoint |
| globalPaths | string[] | — | Specific file paths to back up (defaults to common config files) |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| checkpoint.id | string | * | Checkpoint identifier |
| checkpoint.label | string | * | Checkpoint label |
| checkpoint.gitTag | string | — | Git tag name if applicable |
| checkpoint.gitCommitSha | string | — | Git commit SHA |
| checkpoint.backupPath | string | — | Path to file backup directory |
| checkpoint.filesCount | number | * | 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": "stagent-cp-4d5e-6f7g",
"gitCommitSha": "a1b2c3d4e5f6",
"backupPath": "/Users/team/.stagent/checkpoints/cp-4d5e-6f7g",
"filesCount": 8,
"createdAt": "2026-04-03T10:00:00.000Z"
}
} Get Checkpoint Detail
/api/environment/checkpoints/{id} Get checkpoint details including sync operations and git diff from the checkpoint commit to current HEAD.
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| checkpoint | object | * | Checkpoint record |
| syncOps | object[] | * | Sync operations associated with this checkpoint |
| diff | string | 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
/api/environment/checkpoints/{id} Rollback to a checkpoint. Restores git state and file backups. Only works on checkpoints with status 'active'.
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| message | string | * | Rollback confirmation message |
| results.git | object | — | Git rollback result (success, error) |
| results.backup | object | — | 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
/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)
| Field | Type | Req | Description |
|---|---|---|---|
| suggestions | object[] | * | Array of profile suggestions |
| count | number | * | Total suggestions |
Response Body (tiered)
| Field | Type | Req | Description |
|---|---|---|---|
| curated | object[] | * | High-confidence profile suggestions |
| discovered | object[] | * | Lower-confidence discovered suggestions |
| curatedCount | number | * | Number of curated suggestions |
| discoveredCount | number | * | 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
/api/environment/profiles/create Create an agent profile from a scan-based suggestion with optional overrides.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| suggestion | object | * | Profile suggestion object from /profiles/suggest |
| overrides.name | string | — | Override the suggested profile name |
| overrides.description | string | — | Override the suggested description |
| overrides.systemPrompt | string | — | 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
/api/environment/sync/preview Dry-run sync operations to see what would change without modifying any files.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| operations | SyncRequest[] | * | Array of sync operations to preview |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| previews | object[] | * | Preview results with diffs and conflict flags |
| summary.total | number | * | Total operations |
| summary.conflicts | number | * | Operations with conflicts |
| summary.newFiles | number | * | New files that would be created |
| summary.totalAdditions | number | * | Total lines added |
| summary.totalDeletions | number | * | 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
/api/environment/sync Execute sync operations with an automatic pre-sync checkpoint for safety. Previews all operations first, then applies changes.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| operations | SyncRequest[] | * | Array of sync operations to execute |
| label | string | — | Label for the auto-created checkpoint |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| checkpointId | string | * | ID of the auto-created pre-sync checkpoint |
| operations | object[] | * | Operation results with status per operation |
| summary.applied | number | * | Successfully applied operations |
| summary.failed | number | * | Failed operations |
| summary.total | number | * | 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
/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
| Field | Type | Req | Description |
|---|---|---|---|
| history | object[] | * | Array of { checkpoint, operations } objects |
| count | number | * | 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
/api/environment/templates List all captured environment templates.
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| templates | object[] | * | 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
/api/environment/templates Capture a new template from an existing scan, preserving the environment configuration for reuse.
Request Body
| Field | Type | Req | Description |
|---|---|---|---|
| scanId | string | * | Scan ID to capture from |
| name | string | * | Template name |
| description | string | — | 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
/api/environment/templates/{id} Get template detail with parsed manifest showing all captured configuration.
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| template | object | * | Template record |
| manifest | object | * | 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
/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