Files
AgentHub/server/openapi-extra/misc.js
T
nntrivi2001 b7f3735b29 feat: v2.5.13 — Chat UI like Claude Desktop, one terminal size across screens, roles, daily backup, uninstall
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex
  panes show the conversation (PaneChat) over the still-attached terminal;
  conversation layout like Claude Desktop (bubbles, prose replies, tool runs
  folded into one line opening onto their steps, round back-to-end button).
- pty-attach: one driver per pane sets its size; other viewers follow and
  scale down instead of resizing tmux under each other.
- Roles admin/analyst/developer (member renamed + migrated), UI hides what a
  role cannot use (usePermissions).
- Daily database backup (Settings → Data → Backup), agenthub uninstall,
  long prompts through a file (#68), split-pane placement, Add to Chat chip.

Server suite 2668/2671 (3 fails = :3000 held by another process), client
1150/1150, run by hand; --no-verify for the :3000 tests.
2026-10-08 23:04:57 +07:00

1741 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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/settings/autostart (Settings)
* - PUT /api/settings/autostart (Settings)
* - GET /api/settings/tray (Settings)
* - PUT /api/settings/tray (Settings)
* - GET /api/settings/backup (Settings)
* - PUT /api/settings/backup (Settings)
* - POST /api/settings/backup/run (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/dev/AgentHub", "/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",
},
env_owned: {
type: "boolean",
description:
"True when the real environment, or the checkout's own `.env`, set this variable at startup: either wins over the settings file, so a saved value is overridden again at the next start.",
},
},
},
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 settings file (`$XDG_CONFIG_HOME/agenthub/settings.env`, default `~/.config/agenthub/`, or AGENTHUB_SETTINGS_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",
},
env_owned: {
type: "boolean",
description:
"True when the real environment, or the checkout's own `.env`, set this variable at startup: either wins over the settings file, so a saved value is overridden again at the next start.",
},
},
},
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/dev/AgentHub", "/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/autostart": (() => {
const status = {
description: "Start-at-login state",
content: {
"application/json": {
schema: {
type: "object",
required: ["available", "kind", "enabled", "foreign"],
properties: {
available: {
type: "boolean",
description: "False with no systemd --user (Linux) or GUI login (macOS)",
},
kind: { type: "string", enum: ["systemd", "launchd"] },
enabled: { type: "boolean" },
foreign: {
type: "boolean",
description: "A unit named agenthub exists that AgentHub did not write",
},
},
},
},
},
};
return {
get: {
tags: ["Settings"],
summary: "Whether the dashboard starts at boot/login",
description:
"The per-user service's enabled state (a systemd `--user` unit, or a LaunchAgent on macOS). Off by default.",
operationId: "getAutostart",
responses: { 200: status },
},
put: {
tags: ["Settings"],
summary: "Turn start-at-login on or off",
description:
"Enables or disables the unit, writing one for the running server first when none exists. Never starts or stops the server. Same-origin only.",
operationId: "setAutostart",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["enabled"],
properties: { enabled: { type: "boolean" } },
},
},
},
},
responses: {
200: { ...status, description: "The new state" },
400: {
description:
"EBADVALUE: `enabled` is not a boolean; EUNAVAILABLE: no systemd --user / launchd GUI session",
},
403: { description: "EBADORIGIN: another site asked" },
409: { description: "EFOREIGN: a unit named agenthub that AgentHub did not write" },
},
},
};
})(),
"/api/settings/tray": (() => {
const status = {
description: "Status-area icon state",
content: {
"application/json": {
schema: {
type: "object",
required: ["available", "enabled", "running"],
properties: {
available: {
type: "boolean",
description: "False in server mode, or on Linux with no graphical session",
},
enabled: { type: "boolean", description: "On by default" },
running: { type: "boolean", description: "The icon process is heartbeating" },
},
},
},
},
};
return {
get: {
tags: ["Settings"],
summary: "Whether AgentHub's icon shows in the OS status area",
operationId: "getTray",
responses: { 200: status },
},
put: {
tags: ["Settings"],
summary: "Show or hide the status-area icon",
description:
"Clears or writes `<data dir>/runtime/tray-off` and starts or stops the icon process. Never touches the server. Same-origin only.",
operationId: "setTray",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["enabled"],
properties: { enabled: { type: "boolean" } },
},
},
},
},
responses: {
200: { ...status, description: "The new state" },
400: { description: "EBADVALUE: `enabled` is not a boolean" },
403: { description: "EBADORIGIN: another site asked" },
},
},
};
})(),
"/api/settings/backup": (() => {
const state = {
description: "Daily database backup settings, the folder's backups and the last run",
content: {
"application/json": {
schema: {
type: "object",
required: ["dir", "hour", "keep", "running", "files", "env_owned"],
properties: {
dir: { type: "string", description: "Backup folder; empty = backups off" },
hour: { type: "integer", minimum: 0, maximum: 23, description: "Server local hour" },
keep: { type: "integer", minimum: 1, maximum: 365 },
running: { type: "boolean" },
last: {
type: "object",
nullable: true,
description:
"This process's last run: {at, ok, file?, size?, ms?, removed?, error?}",
},
files: {
type: "array",
description: "AgentHub's own `agenthub-YYYYMMDD-HHMMSS.db` files, newest first",
items: {
type: "object",
properties: {
name: { type: "string" },
size: { type: "integer" },
mtime: { type: "string", format: "date-time" },
},
},
},
dir_error: { type: "string", nullable: true },
env_owned: {
type: "object",
properties: {
dir: { type: "boolean" },
hour: { type: "boolean" },
keep: { type: "boolean" },
},
},
},
},
},
},
};
return {
get: {
tags: ["Settings"],
summary: "Daily database backup settings and backups",
operationId: "getBackup",
responses: { 200: state },
},
put: {
tags: ["Settings"],
summary: "Set the backup folder, hour and number kept",
description:
"Any subset of the three fields, persisted to the settings file as AGENTHUB_BACKUP_DIR / AGENTHUB_BACKUP_HOUR / AGENTHUB_BACKUP_KEEP. An empty `dir` turns backups off. Same-origin only.",
operationId: "setBackup",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: {
dir: { type: "string" },
hour: { type: "integer", minimum: 0, maximum: 23 },
keep: { type: "integer", minimum: 1, maximum: 365 },
},
},
},
},
},
responses: {
200: { ...state, description: "The new state" },
400: {
description:
"INVALID_PATH: the folder is relative, missing, not a directory or not writable; INVALID_INPUT: hour/keep out of range",
},
403: { description: "EBADORIGIN: another site asked" },
409: { description: "EENVOWNED: the environment set that field" },
500: { description: "ESAVE: the settings file could not be written" },
},
},
};
})(),
"/api/settings/backup/run": {
post: {
tags: ["Settings"],
summary: "Back up the database now",
description:
"Starts a backup and answers at once; poll GET /api/settings/backup while `running`. Same-origin only.",
operationId: "runBackup",
responses: {
202: { description: "Started (or already running); the backup state" },
400: { description: "ENOBACKUPDIR: no backup folder is set" },
403: { description: "EBADORIGIN: another site asked" },
},
},
},
"/api/settings/codex-hooks": (() => {
const status = {
description: "Where AgentHub's Codex hooks stand",
content: {
"application/json": {
schema: {
type: "object",
required: ["supported", "readable", "path", "script", "installed", "trusted", "events"],
properties: {
supported: {
type: "boolean",
description: "False on Windows (the command is a POSIX script)",
},
readable: {
type: "boolean",
description: "False when hooks.json is not a JSON object",
},
error: { type: "string" },
path: { type: "string", description: "`$CODEX_HOME/hooks.json`" },
script: {
type: "string",
description: "`$CODEX_HOME/agenthub-hook.sh`, the forwarder",
},
installed: { type: "boolean" },
trusted: {
type: "boolean",
description:
"Codex's config.toml holds a trust entry for every one of AgentHub's hooks",
},
events: {
type: "object",
additionalProperties: {
type: "object",
properties: { installed: { type: "boolean" }, trusted: { type: "boolean" } },
},
},
},
},
},
},
};
const refused = {
description:
"EBADJSON (hooks.json is not a JSON object), EFOREIGN (the forwarder path holds a script that is not AgentHub's) or EUNSUPPORTED (Windows)",
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } },
};
const failed = {
description: "ESAVE: the file system refused the write",
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } },
};
return {
get: {
tags: ["Settings"],
summary: "Status of AgentHub's hooks in Codex",
description:
"Per event (SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, Interrupt, SessionEnd): whether AgentHub's group is in `$CODEX_HOME/hooks.json`, and whether Codex's `config.toml` holds a trust entry for it. Codex runs a hook only once the user trusts it; AgentHub only reads that trust.",
operationId: "getCodexHooks",
responses: { 200: status, 500: failed },
},
post: {
tags: ["Settings"],
summary: "Install (or reinstall) AgentHub's hooks in Codex",
description:
"Refuses a cross-origin request (403 EBADORIGIN). Writes the forwarder script and adds one hook of AgentHub's per event to `$CODEX_HOME/hooks.json`, keeping every other entry. The command is the script's stable path, so Codex's trust survives AgentHub updates; the server rewrites the script's content at every start. Codex asks the user to trust the new hooks the next time it starts.",
operationId: "installCodexHooks",
responses: { 200: status, 400: refused, 500: failed },
},
delete: {
tags: ["Settings"],
summary: "Remove AgentHub's hooks from Codex",
description:
"Refuses a cross-origin request (403 EBADORIGIN). Takes AgentHub's own hooks (matched by their exact command) out of `$CODEX_HOME/hooks.json` (deleting the file when nothing else is left, or emptying it through the link when it is a symlink) and deletes the forwarder script when it is AgentHub's. 400 EBADJSON when hooks.json is not a JSON object.",
operationId: "removeCodexHooks",
responses: { 200: status, 400: refused, 500: failed },
},
};
})(),
"/api/settings/antigravity-hooks": (() => {
const status = {
description: "Where AgentHub's Antigravity hook stands",
content: {
"application/json": {
schema: {
type: "object",
required: ["supported", "readable", "path", "script", "installed", "enabled", "events"],
properties: {
supported: {
type: "boolean",
description: "False on Windows (the command is a POSIX script)",
},
readable: {
type: "boolean",
description: "False when hooks.json is not a JSON object",
},
error: { type: "string" },
path: { type: "string", description: "`~/.gemini/config/hooks.json`" },
script: {
type: "string",
description: "`~/.gemini/config/agenthub-hook.sh`, the forwarder",
},
installed: { type: "boolean" },
enabled: {
type: "boolean",
description: 'Installed and not switched off (`"enabled": false`) in the file',
},
events: {
type: "object",
additionalProperties: {
type: "object",
properties: { installed: { type: "boolean" } },
},
},
},
},
},
},
};
const refused = {
description:
"EBADJSON (hooks.json is not a JSON object), EFOREIGN (the forwarder path holds a script that is not AgentHub's) or EUNSUPPORTED (Windows)",
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } },
};
const failed = {
description: "ESAVE: the file system refused the write",
content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } },
};
return {
get: {
tags: ["Settings"],
summary: "Status of AgentHub's hook in Antigravity",
description:
"Per event (PreInvocation, PostToolUse, Stop): whether AgentHub's named hook `agenthub` in `~/.gemini/config/hooks.json` runs its forwarder. PreToolUse is never installed: agy treats its answer as a permission decision.",
operationId: "getAntigravityHooks",
responses: { 200: status, 500: failed },
},
post: {
tags: ["Settings"],
summary: "Install (or reinstall) AgentHub's hook in Antigravity",
description:
"Refuses a cross-origin request (403 EBADORIGIN). Writes the forwarder script and sets the named hook `agenthub` in `~/.gemini/config/hooks.json`, keeping every other named hook. agy loads it at its next start, with no trust step.",
operationId: "installAntigravityHooks",
responses: { 200: status, 400: refused, 500: failed },
},
delete: {
tags: ["Settings"],
summary: "Remove AgentHub's hook from Antigravity",
description:
"Refuses a cross-origin request (403 EBADORIGIN). Deletes the named hook `agenthub` (and the file when nothing else is left, unless it is a symlink, which is emptied through the link) and the forwarder script when it is AgentHub's. 400 EBADJSON when hooks.json is not a JSON object.",
operationId: "removeAntigravityHooks",
responses: { 200: status, 400: refused, 500: failed },
},
};
})(),
"/api/settings/codex-home": {
get: {
tags: ["Settings"],
summary: "Get the active Codex home directory",
description:
"Returns the Codex home directory (CODEX_HOME, else `<homedir>/.codex`) the dashboard reads Codex config and rollout journals from, writes AI Proxy layers into at each spawn, and hands every codex lane pane through tmux `-e`.",
operationId: "getCodexHome",
responses: {
200: {
description: "Current Codex home directory",
content: {
"application/json": {
schema: {
type: "object",
required: ["codex_home", "env_owned"],
properties: {
codex_home: { type: "string" },
env_owned: { type: "boolean" },
},
},
example: { codex_home: "/Users/son/.codex", env_owned: false },
},
},
},
},
},
put: {
tags: ["Settings"],
summary: "Update the Codex home directory",
description:
"Same validation and persistence as PUT /api/settings/claude-home (403 EBADORIGIN from another site, settings file, EBADHOME for a path that is or contains the OS home, INVALID_PATH otherwise). Nothing is re-written here: a session's AI Proxy layer (`<name>.config.toml`) is written into the current home at its spawn. A codex pane already running keeps its old home until restarted.",
operationId: "updateCodexHome",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["path"],
properties: { path: { type: "string" } },
},
example: { path: "~/.codex-work" },
},
},
},
responses: {
200: {
description: "Codex home updated",
content: {
"application/json": {
schema: {
type: "object",
required: ["ok", "codex_home", "env_owned"],
properties: {
ok: { type: "boolean", enum: [true] },
codex_home: { type: "string" },
env_owned: { type: "boolean" },
},
},
example: {
ok: true,
codex_home: "/Users/son/.codex-work",
env_owned: false,
},
},
},
},
400: {
description: "Invalid path (INVALID_PATH) or too broad (EBADHOME).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "EBADHOME",
message: "Too broad for CODEX_HOME: / contains your home directory",
},
},
},
},
},
500: {
description: "The settings file could not be written (ESAVE).",
content: {
"application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
},
},
},
},
},
"/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 settings file (`$XDG_CONFIG_HOME/agenthub/settings.env`, default `~/.config/agenthub/`, or AGENTHUB_SETTINGS_FILE — never the repo `.env`, which a plugin update replaces). The data dir (database, runtime) is derived from CLAUDE_HOME, so the first change also pins the one in use as DASHBOARD_DATA_DIR unless the environment already sets it. Refuses a cross-origin request (403 EBADORIGIN). Returns 400 EBADHOME when the path is, or contains (symlinks resolved), the OS user's home directory — `/`, `/home`, `~` — because everything under CLAUDE_HOME becomes readable through the config panel; 400 INVALID_PATH when `path` is missing/not a string, or 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), or it is or contains the OS home directory (code EBADHOME).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "INVALID_PATH",
message: "Directory does not exist: /Users/son/.codefuse/engine/cc",
},
},
},
},
},
500: {
description: "The settings file could not be written (ESAVE).",
content: {
"application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
},
},
},
},
},
"/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 };