57dc91585d
Internal SmartGift build of a Claude Code monitoring dashboard. Lanes: a durable unit of parallel agent work, one per working directory, tracked across session restarts. Managed lanes are git worktrees the dashboard provisions and can reset or remove behind a three-check destroy guard and a counted preflight; adopted lanes are directories you already own and are never destroyable. Pipelines: a lane moves through pipeline stages. A stage the agent declares with evidence renders green; a stage inferred from the tool-event stream renders dashed amber and never counts as done. Detection is forward-only within a 30-minute window, and never writes the declared stage. Workspace: one page at /run with a lane grid, the selected lane's pipeline, and a full Claude console behind a disclosure.
1261 lines
46 KiB
JavaScript
1261 lines
46 KiB
JavaScript
/**
|
||
* @file Supplementary OpenAPI 3.0 fragments for endpoints that were previously
|
||
* undocumented in the base spec (server/openapi.js). Covers:
|
||
* - GET /api/sessions/facets (Sessions)
|
||
* - GET /api/settings/claude-home (Settings)
|
||
* - PUT /api/settings/claude-home (Settings)
|
||
* - GET /api/workflows/runs (Workflows)
|
||
* - GET /api/workflows/runs/{runId} (Workflows)
|
||
* - GET /api/remote-sources (Remote Sources)
|
||
* - POST /api/remote-sources (Remote Sources)
|
||
* - PATCH /api/remote-sources/{id} (Remote Sources)
|
||
* - DELETE /api/remote-sources/{id} (Remote Sources)
|
||
* - POST /api/remote-sources/{id}/test (Remote Sources)
|
||
* - POST /api/remote-sources/{id}/sync (Remote Sources)
|
||
*
|
||
* Exports `{ tags, schemas, paths }` and is combined into the base spec by
|
||
* server/openapi-extra.js. Schema names are prefixed (Sessions / Settings /
|
||
* Workflow / RemoteSource) so they never collide with the base
|
||
* `components.schemas`. The Sessions/Settings/Workflows tags — plus the
|
||
* `Remote Sources` tag now declared in the base literal — are all present in the
|
||
* base spec, so `tags` here is intentionally empty. Error bodies reference the
|
||
* base-defined `ErrorResponse` (shape `{ error: { code, message } }`); the
|
||
* run-detail agents and events arrays reference the base `Agent` /
|
||
* `DashboardEvent` schemas. The `SessionsFacetsResponse` schema additionally
|
||
* exposes a `sources` array (the distinct `sessions.source` values).
|
||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||
*/
|
||
|
||
const tags = [];
|
||
|
||
const schemas = {
|
||
SessionsFacetsResponse: {
|
||
type: "object",
|
||
description:
|
||
"Facet values for the Sessions page filter UI: the distinct working directories (cwd) and the distinct data-source ids seen across all sessions.",
|
||
required: ["cwds", "sources"],
|
||
properties: {
|
||
cwds: {
|
||
type: "array",
|
||
description:
|
||
"Distinct, non-empty session working directories (the `cwd` column), sorted ascending. Powers the cwd filter dropdown.",
|
||
items: { type: "string" },
|
||
example: [
|
||
"/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||
"/Users/son/code/another-project",
|
||
],
|
||
},
|
||
sources: {
|
||
type: "array",
|
||
description:
|
||
"Distinct data-source ids seen across all sessions (the `sessions.source` column). Always includes the built-in `local` history; each configured remote SSH machine contributes its `remote_sources.id`. Powers the source filter dropdown and the `sources` query param on the list/analytics endpoints.",
|
||
items: { type: "string" },
|
||
example: ["local", "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11"],
|
||
},
|
||
},
|
||
},
|
||
|
||
RemoteSource: {
|
||
type: "object",
|
||
description:
|
||
"A configured remote SSH machine the dashboard pulls Claude Code history from. NO secrets are stored on this record — SSH authentication defers entirely to the host's SSH stack (ssh-agent, `~/.ssh/config`, and key files). `host` is an SSH destination (`user@host`) or a `~/.ssh/config` alias.",
|
||
required: [
|
||
"id",
|
||
"label",
|
||
"host",
|
||
"ssh_port",
|
||
"identity_file",
|
||
"remote_home",
|
||
"enabled",
|
||
"status",
|
||
"last_error",
|
||
"last_sync_at",
|
||
"last_sync_counts",
|
||
"created_at",
|
||
"updated_at",
|
||
],
|
||
properties: {
|
||
id: {
|
||
type: "string",
|
||
description: "Primary key — the remote-source id (also used as `sessions.source`).",
|
||
example: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
},
|
||
label: {
|
||
type: "string",
|
||
description: "Human-readable name shown in the UI.",
|
||
example: "Work laptop",
|
||
},
|
||
host: {
|
||
type: "string",
|
||
description:
|
||
"SSH destination (`user@host`) or a `~/.ssh/config` alias resolved by the host SSH stack.",
|
||
example: "son@studio.local",
|
||
},
|
||
ssh_port: {
|
||
type: "integer",
|
||
nullable: true,
|
||
description: "Optional SSH port; null defers to the SSH default / `~/.ssh/config`.",
|
||
example: 22,
|
||
},
|
||
identity_file: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Optional path to a private-key file passed to ssh (`-i`); null to omit.",
|
||
example: "~/.ssh/id_ed25519",
|
||
},
|
||
remote_home: {
|
||
type: "string",
|
||
nullable: true,
|
||
description:
|
||
"Optional remote Claude home to read transcripts from; null defaults to the remote `~/.claude`.",
|
||
example: "~/.claude",
|
||
},
|
||
enabled: {
|
||
type: "boolean",
|
||
description: "Whether this source is eligible for scheduled/manual syncs.",
|
||
example: true,
|
||
},
|
||
status: {
|
||
type: "string",
|
||
enum: ["idle", "syncing", "ok", "error"],
|
||
description: "Last known sync status of the source.",
|
||
example: "ok",
|
||
},
|
||
last_error: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Error message from the last failed sync/test, or null.",
|
||
example: null,
|
||
},
|
||
last_sync_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
nullable: true,
|
||
description: "ISO-8601 timestamp of the last successful sync, or null.",
|
||
example: "2026-07-22T18:41:55.117Z",
|
||
},
|
||
last_sync_counts: {
|
||
type: "object",
|
||
nullable: true,
|
||
additionalProperties: true,
|
||
description:
|
||
"Counters from the last sync (imported / skipped / backfilled / errors / sessions_seen / sessions_tagged), or null if never synced.",
|
||
example: {
|
||
imported: 9,
|
||
skipped: 41,
|
||
backfilled: 0,
|
||
errors: 0,
|
||
sessions_seen: 50,
|
||
sessions_tagged: 50,
|
||
},
|
||
},
|
||
created_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
description: "ISO-8601 creation timestamp.",
|
||
example: "2026-07-20T09:15:00.000Z",
|
||
},
|
||
updated_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
description: "ISO-8601 timestamp of the last edit.",
|
||
example: "2026-07-22T18:41:55.117Z",
|
||
},
|
||
},
|
||
},
|
||
|
||
RemoteSourceCreateRequest: {
|
||
type: "object",
|
||
description:
|
||
"Request body to register a remote SSH source. `label` and `host` are required; the rest are optional. No credentials are ever accepted or stored — auth defers to the host SSH stack.",
|
||
required: ["label", "host"],
|
||
properties: {
|
||
label: { type: "string", description: "Human-readable name.", example: "Work laptop" },
|
||
host: {
|
||
type: "string",
|
||
description: "SSH destination (`user@host`) or a `~/.ssh/config` alias.",
|
||
example: "son@studio.local",
|
||
},
|
||
ssh_port: {
|
||
type: "integer",
|
||
description: "Optional SSH port.",
|
||
example: 22,
|
||
},
|
||
identity_file: {
|
||
type: "string",
|
||
description: "Optional private-key path passed to ssh (`-i`).",
|
||
example: "~/.ssh/id_ed25519",
|
||
},
|
||
remote_home: {
|
||
type: "string",
|
||
description: "Optional remote Claude home; defaults to the remote `~/.claude`.",
|
||
example: "~/.claude",
|
||
},
|
||
enabled: {
|
||
type: "boolean",
|
||
description: "Whether the source is enabled for syncing (default true).",
|
||
example: true,
|
||
},
|
||
},
|
||
},
|
||
|
||
RemoteSourceUpdateRequest: {
|
||
type: "object",
|
||
description:
|
||
"Partial update for a remote source. Only the keys present in the body are changed; omitted keys are left as-is. Same field set as create; both `label` and `host` are optional here.",
|
||
properties: {
|
||
label: { type: "string", example: "Studio Mac" },
|
||
host: { type: "string", example: "son@studio.local" },
|
||
ssh_port: { type: "integer", nullable: true, example: 2222 },
|
||
identity_file: { type: "string", nullable: true, example: "~/.ssh/id_ed25519" },
|
||
remote_home: { type: "string", nullable: true, example: "~/.claude" },
|
||
enabled: { type: "boolean", example: false },
|
||
},
|
||
},
|
||
|
||
RemoteSourceResponse: {
|
||
type: "object",
|
||
required: ["source"],
|
||
properties: { source: { $ref: "#/components/schemas/RemoteSource" } },
|
||
},
|
||
|
||
RemoteSourceListResponse: {
|
||
type: "object",
|
||
required: ["sources"],
|
||
properties: {
|
||
sources: {
|
||
type: "array",
|
||
items: { $ref: "#/components/schemas/RemoteSource" },
|
||
},
|
||
},
|
||
},
|
||
|
||
RemoteSourceTestResponse: {
|
||
type: "object",
|
||
description: "Result of an SSH connectivity probe.",
|
||
required: ["ok", "message"],
|
||
properties: {
|
||
ok: { type: "boolean", example: true },
|
||
message: {
|
||
type: "string",
|
||
description: "Human-readable probe result.",
|
||
example: "Connected; found 24 project directories under ~/.claude/projects.",
|
||
},
|
||
remoteProjects: {
|
||
type: "array",
|
||
description:
|
||
"Optional list of remote project directories discovered during the probe (present on success).",
|
||
items: { type: "string" },
|
||
example: ["-Users-son-code-foo", "-Users-son-code-bar"],
|
||
},
|
||
},
|
||
},
|
||
|
||
RemoteSourceSyncResponse: {
|
||
type: "object",
|
||
description: "Counters from a pull-now sync against the remote source.",
|
||
required: [
|
||
"ok",
|
||
"imported",
|
||
"skipped",
|
||
"backfilled",
|
||
"errors",
|
||
"sessions_seen",
|
||
"sessions_tagged",
|
||
],
|
||
properties: {
|
||
ok: { type: "boolean", example: true },
|
||
imported: { type: "integer", example: 9 },
|
||
skipped: { type: "integer", example: 41 },
|
||
backfilled: { type: "integer", example: 0 },
|
||
errors: { type: "integer", example: 0 },
|
||
sessions_seen: { type: "integer", example: 50 },
|
||
sessions_tagged: {
|
||
type: "integer",
|
||
description: "Number of imported sessions stamped with this source's id.",
|
||
example: 50,
|
||
},
|
||
},
|
||
},
|
||
|
||
SettingsClaudeHomeResponse: {
|
||
type: "object",
|
||
description:
|
||
"The Claude Code home directory the dashboard reads transcripts and settings from. Defaults to `~/.claude` unless overridden via the CLAUDE_HOME environment variable.",
|
||
required: ["claude_home"],
|
||
properties: {
|
||
claude_home: {
|
||
type: "string",
|
||
description:
|
||
"Absolute path to the active Claude Code home directory (CLAUDE_HOME, or `<homedir>/.claude` when unset).",
|
||
example: "/Users/son/.claude",
|
||
},
|
||
},
|
||
},
|
||
|
||
SettingsClaudeHomeUpdateRequest: {
|
||
type: "object",
|
||
description:
|
||
"Request body for changing the Claude Code home directory. A leading `~` is expanded to the user's home directory; the resolved path must be absolute and point to an existing directory.",
|
||
required: ["path"],
|
||
properties: {
|
||
path: {
|
||
type: "string",
|
||
description:
|
||
"New Claude Code home directory. A leading `~/` is expanded to the OS home directory before validation. Must resolve to an absolute path that exists and is a directory.",
|
||
example: "~/.codefuse/engine/cc",
|
||
},
|
||
},
|
||
},
|
||
|
||
SettingsClaudeHomeUpdateResponse: {
|
||
type: "object",
|
||
description:
|
||
"Confirmation that CLAUDE_HOME was updated. The new value is applied to process.env immediately and persisted to the project `.env` file.",
|
||
required: ["ok", "claude_home"],
|
||
properties: {
|
||
ok: { type: "boolean", enum: [true] },
|
||
claude_home: {
|
||
type: "string",
|
||
description: "The resolved absolute path now in effect (after `~` expansion).",
|
||
example: "/Users/son/.codefuse/engine/cc",
|
||
},
|
||
},
|
||
},
|
||
|
||
WorkflowToolRun: {
|
||
type: "object",
|
||
description:
|
||
"A Claude Code Workflow-tool run (issue #167): a fleet of sub-agents spawned by the 'Workflow' tool (or self-paced /loop). These emit no hooks; the source of truth is the on-disk run journal, ingested into the `workflows` table (see server/lib/workflow-ingest.js). Keyed by `run_id` and parented to the launching session. The JSON-blob columns `phases` and `progress` are parsed into arrays before serialization.",
|
||
required: [
|
||
"run_id",
|
||
"session_id",
|
||
"status",
|
||
"agent_count",
|
||
"total_tokens",
|
||
"total_tool_calls",
|
||
"phases",
|
||
"progress",
|
||
"source",
|
||
"created_at",
|
||
"updated_at",
|
||
],
|
||
properties: {
|
||
run_id: {
|
||
type: "string",
|
||
description: "Primary key — the workflow run id.",
|
||
example: "wf_a1b2c3d4",
|
||
},
|
||
session_id: {
|
||
type: "string",
|
||
description: "The session that launched this run (FK into sessions.id).",
|
||
example: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
|
||
},
|
||
task_id: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Optional task/issue identifier associated with the run.",
|
||
example: "ISSUE-167",
|
||
},
|
||
name: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Human-readable run name from the journal, if present.",
|
||
example: "Refactor pricing engine",
|
||
},
|
||
status: {
|
||
type: "string",
|
||
description:
|
||
"Open status string (e.g. running | completed | error | failed). Intentionally not constrained to an enum so new harness states never trip a stale constraint.",
|
||
example: "completed",
|
||
},
|
||
default_model: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Default model the run delegated work to, when recorded.",
|
||
example: "claude-opus-4-8",
|
||
},
|
||
started_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
nullable: true,
|
||
description: "When the run started, if known.",
|
||
example: "2026-06-25T18:04:11.122Z",
|
||
},
|
||
ended_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
nullable: true,
|
||
description: "When the run finished, if known.",
|
||
example: "2026-06-25T18:09:47.530Z",
|
||
},
|
||
duration_ms: {
|
||
type: "integer",
|
||
nullable: true,
|
||
description: "Total run duration in milliseconds, if known.",
|
||
example: 336408,
|
||
},
|
||
agent_count: {
|
||
type: "integer",
|
||
minimum: 0,
|
||
description: "Number of inner agents in this run.",
|
||
example: 6,
|
||
},
|
||
total_tokens: {
|
||
type: "integer",
|
||
minimum: 0,
|
||
description: "Aggregate token usage across the run's inner agents.",
|
||
example: 1284750,
|
||
},
|
||
total_tool_calls: {
|
||
type: "integer",
|
||
minimum: 0,
|
||
description: "Aggregate tool-call count across the run's inner agents.",
|
||
example: 412,
|
||
},
|
||
phases: {
|
||
type: "array",
|
||
description:
|
||
"Parsed `phases[]` array from the run journal (verbatim journal payload, opaque to this API). Empty array when absent or unparseable.",
|
||
items: { type: "object", additionalProperties: true },
|
||
example: [
|
||
{ name: "plan", status: "completed" },
|
||
{ name: "implement", status: "completed" },
|
||
],
|
||
},
|
||
progress: {
|
||
type: "array",
|
||
description:
|
||
"Parsed `workflowProgress[]` array from the run journal (verbatim journal payload, opaque to this API). Empty array when absent or unparseable.",
|
||
items: { type: "object", additionalProperties: true },
|
||
example: [{ step: 1, label: "scaffold", done: true }],
|
||
},
|
||
script_path: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Path to the run's driving script, if recorded.",
|
||
example: "/Users/son/.claude/projects/-Users-son-code/wf_a1b2c3d4.sh",
|
||
},
|
||
journal_path: {
|
||
type: "string",
|
||
nullable: true,
|
||
description: "Path to the on-disk run journal this row was ingested from.",
|
||
example: "/Users/son/.claude/projects/-Users-son-code/5f3c0e2a/workflows/wf_a1b2c3d4.json",
|
||
},
|
||
source: {
|
||
type: "string",
|
||
description: "Ingestion source for the row (defaults to 'journal').",
|
||
example: "journal",
|
||
},
|
||
created_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
description: "Row creation timestamp.",
|
||
example: "2026-06-25T18:09:48.001Z",
|
||
},
|
||
updated_at: {
|
||
type: "string",
|
||
format: "date-time",
|
||
description: "Row last-update timestamp.",
|
||
example: "2026-06-25T18:09:48.001Z",
|
||
},
|
||
},
|
||
},
|
||
|
||
WorkflowRunsListResponse: {
|
||
type: "object",
|
||
description:
|
||
"Paginated list of Workflow-tool runs with status counts. `total` reflects the active filter (status when supplied, otherwise the full table); `counts` is always the whole-table breakdown by status.",
|
||
required: ["runs", "total", "counts", "limit", "offset"],
|
||
properties: {
|
||
runs: {
|
||
type: "array",
|
||
items: { $ref: "#/components/schemas/WorkflowToolRun" },
|
||
},
|
||
total: {
|
||
type: "integer",
|
||
minimum: 0,
|
||
description:
|
||
"Total runs matching the current filter (independent of limit/offset). Equals the status-filtered count when `status` is supplied, otherwise the full-table count. Note: not narrowed by `session_id`.",
|
||
example: 42,
|
||
},
|
||
counts: {
|
||
type: "object",
|
||
description: "Whole-table run counts grouped by status (not affected by filters).",
|
||
additionalProperties: { type: "integer", minimum: 0 },
|
||
example: { completed: 30, error: 5, running: 7 },
|
||
},
|
||
limit: { type: "integer", description: "Effective page size used.", example: 50 },
|
||
offset: { type: "integer", description: "Effective pagination offset used.", example: 0 },
|
||
},
|
||
},
|
||
|
||
WorkflowRunDetailResponse: {
|
||
type: "object",
|
||
description:
|
||
"A single Workflow-tool run with its linked inner agents and the events attributed to those agents (chronological, capped at 5000).",
|
||
required: ["workflow", "agents", "events"],
|
||
properties: {
|
||
workflow: { $ref: "#/components/schemas/WorkflowToolRun" },
|
||
agents: {
|
||
type: "array",
|
||
description: "Inner agents linked to this run via agents.workflow_run_id.",
|
||
items: { $ref: "#/components/schemas/Agent" },
|
||
},
|
||
events: {
|
||
type: "array",
|
||
description:
|
||
"Events attributed to this run's inner agents, ordered by created_at then id. Capped at 5000 rows.",
|
||
items: { $ref: "#/components/schemas/DashboardEvent" },
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
const paths = {
|
||
"/api/sessions/facets": {
|
||
get: {
|
||
tags: ["Sessions"],
|
||
summary: "List session facet values",
|
||
description:
|
||
"Returns the distinct facet values for the Sessions page filters: the non-empty working directories (the `cwd` column, sorted ascending) in `cwds`, and the distinct data-source ids (the `sessions.source` column) in `sources`. `sources` always includes the built-in `local` history plus any configured remote SSH machines. Always returns a 200 with (possibly empty) arrays.",
|
||
operationId: "listSessionFacets",
|
||
responses: {
|
||
200: {
|
||
description: "Distinct session working directories and data-source ids",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/SessionsFacetsResponse" },
|
||
example: {
|
||
cwds: [
|
||
"/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
|
||
"/Users/son/code/another-project",
|
||
],
|
||
sources: ["local", "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11"],
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/remote-sources": {
|
||
get: {
|
||
tags: ["Remote Sources"],
|
||
summary: "List remote data sources",
|
||
description:
|
||
"Returns every configured remote SSH source the dashboard pulls Claude Code history from. NO secrets are ever returned — these records store none (SSH auth defers to the host SSH stack). Each entry carries its last sync `status`, `last_error`, `last_sync_at`, and `last_sync_counts`. Read-only; always 200.",
|
||
operationId: "listRemoteSources",
|
||
responses: {
|
||
200: {
|
||
description: "All configured remote sources",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceListResponse" },
|
||
example: {
|
||
sources: [
|
||
{
|
||
id: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
label: "Work laptop",
|
||
host: "son@studio.local",
|
||
ssh_port: 22,
|
||
identity_file: "~/.ssh/id_ed25519",
|
||
remote_home: "~/.claude",
|
||
enabled: true,
|
||
status: "ok",
|
||
last_error: null,
|
||
last_sync_at: "2026-07-22T18:41:55.117Z",
|
||
last_sync_counts: {
|
||
imported: 9,
|
||
skipped: 41,
|
||
backfilled: 0,
|
||
errors: 0,
|
||
sessions_seen: 50,
|
||
sessions_tagged: 50,
|
||
},
|
||
created_at: "2026-07-20T09:15:00.000Z",
|
||
updated_at: "2026-07-22T18:41:55.117Z",
|
||
},
|
||
],
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
post: {
|
||
tags: ["Remote Sources"],
|
||
summary: "Register a remote data source",
|
||
description:
|
||
"Registers a remote SSH source. `label` and `host` are required; `host` is an SSH destination (`user@host`) or a `~/.ssh/config` alias. Optional `ssh_port`, `identity_file`, `remote_home`, and `enabled` fine-tune the connection. No credentials are accepted or stored — auth defers to the host SSH stack. Returns the created source (201). Validation failures return 400 `{ error: { code, message } }` with one of the codes INVALID_LABEL, INVALID_HOST, INVALID_PORT, INVALID_IDENTITY_FILE, INVALID_REMOTE_HOME.",
|
||
operationId: "createRemoteSource",
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceCreateRequest" },
|
||
examples: {
|
||
minimal: {
|
||
summary: "Minimal — label + SSH config alias",
|
||
value: { label: "Work laptop", host: "studio" },
|
||
},
|
||
full: {
|
||
summary: "Full — explicit port, key, and remote home",
|
||
value: {
|
||
label: "Work laptop",
|
||
host: "son@studio.local",
|
||
ssh_port: 22,
|
||
identity_file: "~/.ssh/id_ed25519",
|
||
remote_home: "~/.claude",
|
||
enabled: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
201: {
|
||
description: "Remote source created",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceResponse" },
|
||
example: {
|
||
source: {
|
||
id: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
label: "Work laptop",
|
||
host: "son@studio.local",
|
||
ssh_port: 22,
|
||
identity_file: "~/.ssh/id_ed25519",
|
||
remote_home: "~/.claude",
|
||
enabled: true,
|
||
status: "idle",
|
||
last_error: null,
|
||
last_sync_at: null,
|
||
last_sync_counts: null,
|
||
created_at: "2026-07-22T18:41:55.117Z",
|
||
updated_at: "2026-07-22T18:41:55.117Z",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
400: {
|
||
description:
|
||
"Validation error (codes: INVALID_LABEL, INVALID_HOST, INVALID_PORT, INVALID_IDENTITY_FILE, INVALID_REMOTE_HOME).",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
examples: {
|
||
label: {
|
||
summary: "Missing/blank label",
|
||
value: { error: { code: "INVALID_LABEL", message: "`label` is required" } },
|
||
},
|
||
host: {
|
||
summary: "Missing/invalid host",
|
||
value: { error: { code: "INVALID_HOST", message: "`host` is required" } },
|
||
},
|
||
port: {
|
||
summary: "Port out of range",
|
||
value: {
|
||
error: { code: "INVALID_PORT", message: "`ssh_port` must be 1–65535" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/remote-sources/{id}": {
|
||
patch: {
|
||
tags: ["Remote Sources"],
|
||
summary: "Update a remote data source (partial)",
|
||
description:
|
||
"Partially updates a remote source. Only the keys present in the body are changed; omitted keys are left as-is. The same validation as create applies to any field that is present. Returns the updated source, or 404 when the id is unknown.",
|
||
operationId: "updateRemoteSource",
|
||
parameters: [
|
||
{
|
||
name: "id",
|
||
in: "path",
|
||
required: true,
|
||
schema: { type: "string" },
|
||
description: "Remote source id.",
|
||
example: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
},
|
||
],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceUpdateRequest" },
|
||
examples: {
|
||
disable: {
|
||
summary: "Disable a source without touching anything else",
|
||
value: { enabled: false },
|
||
},
|
||
rename: {
|
||
summary: "Rename and change port",
|
||
value: { label: "Studio Mac", ssh_port: 2222 },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: "Updated remote source",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceResponse" },
|
||
example: {
|
||
source: {
|
||
id: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
label: "Studio Mac",
|
||
host: "son@studio.local",
|
||
ssh_port: 2222,
|
||
identity_file: "~/.ssh/id_ed25519",
|
||
remote_home: "~/.claude",
|
||
enabled: false,
|
||
status: "ok",
|
||
last_error: null,
|
||
last_sync_at: "2026-07-22T18:41:55.117Z",
|
||
last_sync_counts: {
|
||
imported: 9,
|
||
skipped: 41,
|
||
backfilled: 0,
|
||
errors: 0,
|
||
sessions_seen: 50,
|
||
sessions_tagged: 50,
|
||
},
|
||
created_at: "2026-07-20T09:15:00.000Z",
|
||
updated_at: "2026-07-22T19:02:10.400Z",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
400: {
|
||
description:
|
||
"Validation error (codes: INVALID_LABEL, INVALID_HOST, INVALID_PORT, INVALID_IDENTITY_FILE, INVALID_REMOTE_HOME).",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "INVALID_PORT", message: "`ssh_port` must be 1–65535" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
404: {
|
||
description: "Remote source not found",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "NOT_FOUND", message: "Remote source not found" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
delete: {
|
||
tags: ["Remote Sources"],
|
||
summary: "Delete a remote data source",
|
||
description:
|
||
"Deletes a remote source. By default its imported sessions are DETACHED — reassigned to the built-in `local` source — so history is preserved. Pass `?purge=true` to instead permanently DELETE that source's imported sessions along with the source. The response reports whether a purge occurred.",
|
||
operationId: "deleteRemoteSource",
|
||
parameters: [
|
||
{
|
||
name: "id",
|
||
in: "path",
|
||
required: true,
|
||
schema: { type: "string" },
|
||
description: "Remote source id.",
|
||
example: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
},
|
||
{
|
||
name: "purge",
|
||
in: "query",
|
||
required: false,
|
||
schema: { type: "boolean", default: false },
|
||
description:
|
||
"When true, also delete this source's imported sessions. When false/omitted, those sessions are reattached to `local`.",
|
||
example: true,
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Deleted (with the purge outcome)",
|
||
content: {
|
||
"application/json": {
|
||
schema: {
|
||
type: "object",
|
||
required: ["ok", "purged"],
|
||
properties: {
|
||
ok: { type: "boolean", example: true },
|
||
purged: {
|
||
type: "boolean",
|
||
description:
|
||
"True when the source's imported sessions were deleted (purge=true); false when they were detached to `local`.",
|
||
example: false,
|
||
},
|
||
},
|
||
},
|
||
examples: {
|
||
detached: {
|
||
summary: "Default — sessions detached to local",
|
||
value: { ok: true, purged: false },
|
||
},
|
||
purged: {
|
||
summary: "purge=true — sessions deleted",
|
||
value: { ok: true, purged: true },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
404: {
|
||
description: "Remote source not found",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "NOT_FOUND", message: "Remote source not found" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/remote-sources/{id}/test": {
|
||
post: {
|
||
tags: ["Remote Sources"],
|
||
summary: "Probe SSH connectivity to a remote source",
|
||
description:
|
||
"Runs an SSH connectivity probe against the source and reports the outcome synchronously. The `ok` flag carries the probe result and `message` is a human-readable summary; on success `remoteProjects` may list the remote project directories discovered under the remote Claude home. This does not import anything — use POST /{id}/sync to pull.",
|
||
operationId: "testRemoteSource",
|
||
parameters: [
|
||
{
|
||
name: "id",
|
||
in: "path",
|
||
required: true,
|
||
schema: { type: "string" },
|
||
description: "Remote source id.",
|
||
example: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Probe result (ok flag carries the connectivity outcome)",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceTestResponse" },
|
||
examples: {
|
||
success: {
|
||
summary: "Reachable",
|
||
value: {
|
||
ok: true,
|
||
message: "Connected; found 24 project directories under ~/.claude/projects.",
|
||
remoteProjects: ["-Users-son-code-foo", "-Users-son-code-bar"],
|
||
},
|
||
},
|
||
failure: {
|
||
summary: "Unreachable / auth failed",
|
||
value: {
|
||
ok: false,
|
||
message: "ssh: connect to host studio.local port 22: Connection refused",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
404: {
|
||
description: "Remote source not found",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "NOT_FOUND", message: "Remote source not found" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/remote-sources/{id}/sync": {
|
||
post: {
|
||
tags: ["Remote Sources"],
|
||
summary: "Pull Claude Code history from a remote source now",
|
||
description:
|
||
"Triggers an immediate pull of Claude Code history from the remote source over SSH, importing new transcripts through the same idempotent, baseline-preserving pipeline used for local imports and tagging imported sessions with this source's id. The response reports the per-run counters. Sync progress/completion is also broadcast over the WebSocket as `remote_source.status` frames.",
|
||
operationId: "syncRemoteSource",
|
||
parameters: [
|
||
{
|
||
name: "id",
|
||
in: "path",
|
||
required: true,
|
||
schema: { type: "string" },
|
||
description: "Remote source id.",
|
||
example: "4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Sync result",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/RemoteSourceSyncResponse" },
|
||
example: {
|
||
ok: true,
|
||
imported: 9,
|
||
skipped: 41,
|
||
backfilled: 0,
|
||
errors: 0,
|
||
sessions_seen: 50,
|
||
sessions_tagged: 50,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
404: {
|
||
description: "Remote source not found",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "NOT_FOUND", message: "Remote source not found" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
500: {
|
||
description: "Sync failed",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "SYNC_FAILED", message: "ssh exited with code 255" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/remote-sources/sync-all": {
|
||
post: {
|
||
tags: ["Remote Sources"],
|
||
summary: "Sync all enabled remote sources now",
|
||
description:
|
||
"Pulls Claude Code history from every enabled remote source over SSH, sequentially (one connection at a time). Per-source failures are isolated — one unreachable source never aborts the others — and each outcome is returned in `results`. Progress/completion is also broadcast over the WebSocket as `remote_source.status` frames. Always returns 200.",
|
||
operationId: "syncAllRemoteSources",
|
||
responses: {
|
||
200: {
|
||
description: "Per-source sync outcomes",
|
||
content: {
|
||
"application/json": {
|
||
schema: {
|
||
type: "object",
|
||
properties: {
|
||
ok: { type: "boolean" },
|
||
synced: {
|
||
type: "integer",
|
||
description: "Number of enabled sources that were attempted.",
|
||
},
|
||
results: {
|
||
type: "array",
|
||
items: {
|
||
type: "object",
|
||
properties: {
|
||
id: { type: "string" },
|
||
ok: { type: "boolean" },
|
||
error: { type: "string" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
example: { ok: true, synced: 2, results: [{ id: "src_a", ok: true }] },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/settings/claude-home": {
|
||
get: {
|
||
tags: ["Settings"],
|
||
summary: "Get the active Claude Code home directory",
|
||
description:
|
||
"Returns the Claude Code home directory the dashboard uses to locate transcripts and settings. Resolves to the CLAUDE_HOME environment variable when set, otherwise `<homedir>/.claude`. Always returns 200.",
|
||
operationId: "getClaudeHome",
|
||
responses: {
|
||
200: {
|
||
description: "Current Claude Code home directory",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/SettingsClaudeHomeResponse" },
|
||
example: { claude_home: "/Users/son/.claude" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
put: {
|
||
tags: ["Settings"],
|
||
summary: "Update the Claude Code home directory",
|
||
description:
|
||
"Changes the Claude Code home directory used for transcript/settings discovery. A leading `~/` in `path` is expanded to the OS home directory; the resolved value must be an absolute path that exists and is a directory. On success the new value is applied to process.env immediately (so subsequent reads use it) and persisted to the project `.env` file. Returns 400 INVALID_PATH when `path` is missing/not a string, or when the resolved path is not absolute, does not exist, or is not a directory.",
|
||
operationId: "updateClaudeHome",
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/SettingsClaudeHomeUpdateRequest" },
|
||
example: { path: "~/.codefuse/engine/cc" },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: "Claude Code home updated",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/SettingsClaudeHomeUpdateResponse" },
|
||
example: { ok: true, claude_home: "/Users/son/.codefuse/engine/cc" },
|
||
},
|
||
},
|
||
},
|
||
400: {
|
||
description:
|
||
"Invalid path — `path` missing or not a string, or the resolved path is not absolute / does not exist / is not a directory (code INVALID_PATH).",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: {
|
||
code: "INVALID_PATH",
|
||
message: "Directory does not exist: /Users/son/.codefuse/engine/cc",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/workflows/runs": {
|
||
get: {
|
||
tags: ["Workflows"],
|
||
summary: "List Workflow-tool runs",
|
||
description:
|
||
"Returns a paginated list of Workflow-tool runs (fleets of sub-agents spawned by the Claude Code 'Workflow' tool / self-paced /loop), newest first. Filter by `status` (the literal `all` is treated as no filter) or by `session_id`; `session_id` takes precedence over `status` when both are supplied. `counts` is always the whole-table breakdown by status. JSON-blob columns (`phases`, `progress`) are parsed into arrays in each run.",
|
||
operationId: "listWorkflowRuns",
|
||
parameters: [
|
||
{
|
||
name: "limit",
|
||
in: "query",
|
||
required: false,
|
||
schema: { type: "integer", minimum: 1, maximum: 1000, default: 50 },
|
||
description: "Page size, clamped to 1–1000 (default 50).",
|
||
},
|
||
{
|
||
name: "offset",
|
||
in: "query",
|
||
required: false,
|
||
schema: { type: "integer", minimum: 0, default: 0 },
|
||
description: "Pagination offset (clamped to >= 0).",
|
||
},
|
||
{
|
||
name: "status",
|
||
in: "query",
|
||
required: false,
|
||
schema: { type: "string" },
|
||
description:
|
||
"Filter by run status (open string, e.g. running | completed | error | failed). The literal value `all` is treated as no filter.",
|
||
},
|
||
{
|
||
name: "session_id",
|
||
in: "query",
|
||
required: false,
|
||
schema: { type: "string" },
|
||
description:
|
||
"Filter to runs launched by this session. Takes precedence over `status` when both are provided.",
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Paginated list of workflow runs with status counts",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/WorkflowRunsListResponse" },
|
||
example: {
|
||
runs: [
|
||
{
|
||
run_id: "wf_a1b2c3d4",
|
||
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
|
||
task_id: "ISSUE-167",
|
||
name: "Refactor pricing engine",
|
||
status: "completed",
|
||
default_model: "claude-opus-4-8",
|
||
started_at: "2026-06-25T18:04:11.122Z",
|
||
ended_at: "2026-06-25T18:09:47.530Z",
|
||
duration_ms: 336408,
|
||
agent_count: 6,
|
||
total_tokens: 1284750,
|
||
total_tool_calls: 412,
|
||
phases: [{ name: "plan", status: "completed" }],
|
||
progress: [{ step: 1, label: "scaffold", done: true }],
|
||
script_path: null,
|
||
journal_path:
|
||
"/Users/son/.claude/projects/-Users-son-code/5f3c0e2a/workflows/wf_a1b2c3d4.json",
|
||
source: "journal",
|
||
created_at: "2026-06-25T18:09:48.001Z",
|
||
updated_at: "2026-06-25T18:09:48.001Z",
|
||
},
|
||
],
|
||
total: 42,
|
||
counts: { completed: 30, error: 5, running: 7 },
|
||
limit: 50,
|
||
offset: 0,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
500: {
|
||
description: "Failed to list workflow runs (code WORKFLOW_LIST_FAILED).",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "WORKFLOW_LIST_FAILED", message: "no such table: workflows" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
|
||
"/api/workflows/runs/{runId}": {
|
||
get: {
|
||
tags: ["Workflows"],
|
||
summary: "Get a Workflow-tool run with its agents and events",
|
||
description:
|
||
"Returns one Workflow-tool run (by `run_id`) together with its linked inner agents and the events attributed to those agents (chronological, capped at 5000 rows). The run's JSON-blob columns (`phases`, `progress`) are parsed into arrays. Returns 404 WORKFLOW_NOT_FOUND when no run matches the id.",
|
||
operationId: "getWorkflowRun",
|
||
parameters: [
|
||
{
|
||
name: "runId",
|
||
in: "path",
|
||
required: true,
|
||
schema: { type: "string" },
|
||
description: "The workflow run id (workflows.run_id).",
|
||
},
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: "Workflow run with inner agents and their events",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/WorkflowRunDetailResponse" },
|
||
example: {
|
||
workflow: {
|
||
run_id: "wf_a1b2c3d4",
|
||
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
|
||
task_id: "ISSUE-167",
|
||
name: "Refactor pricing engine",
|
||
status: "completed",
|
||
default_model: "claude-opus-4-8",
|
||
started_at: "2026-06-25T18:04:11.122Z",
|
||
ended_at: "2026-06-25T18:09:47.530Z",
|
||
duration_ms: 336408,
|
||
agent_count: 6,
|
||
total_tokens: 1284750,
|
||
total_tool_calls: 412,
|
||
phases: [{ name: "plan", status: "completed" }],
|
||
progress: [{ step: 1, label: "scaffold", done: true }],
|
||
script_path: null,
|
||
journal_path:
|
||
"/Users/son/.claude/projects/-Users-son-code/5f3c0e2a/workflows/wf_a1b2c3d4.json",
|
||
source: "journal",
|
||
created_at: "2026-06-25T18:09:48.001Z",
|
||
updated_at: "2026-06-25T18:09:48.001Z",
|
||
},
|
||
agents: [
|
||
{
|
||
id: "agent-7f1c",
|
||
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
|
||
name: "implementer",
|
||
type: "subagent",
|
||
subagent_type: "general-purpose",
|
||
status: "completed",
|
||
task: "Implement pricing changes",
|
||
current_tool: null,
|
||
started_at: "2026-06-25T18:04:30.000Z",
|
||
ended_at: "2026-06-25T18:08:12.000Z",
|
||
parent_agent_id: null,
|
||
metadata: null,
|
||
updated_at: "2026-06-25T18:08:12.000Z",
|
||
awaiting_input_since: null,
|
||
awaiting_reason: null,
|
||
},
|
||
],
|
||
events: [
|
||
{
|
||
id: 90211,
|
||
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
|
||
agent_id: "agent-7f1c",
|
||
event_type: "PostToolUse",
|
||
tool_name: "Edit",
|
||
summary: "Edited server/routes/pricing.js",
|
||
data: null,
|
||
created_at: "2026-06-25T18:05:02.144Z",
|
||
},
|
||
],
|
||
},
|
||
},
|
||
},
|
||
},
|
||
404: {
|
||
description: "No workflow run matches the id (code WORKFLOW_NOT_FOUND).",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "WORKFLOW_NOT_FOUND", message: "Workflow run not found" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
500: {
|
||
description: "Failed to load workflow run detail (code WORKFLOW_DETAIL_FAILED).",
|
||
content: {
|
||
"application/json": {
|
||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||
example: {
|
||
error: { code: "WORKFLOW_DETAIL_FAILED", message: "database is locked" },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
module.exports = { tags, schemas, paths };
|