b7f3735b29
- 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.
1741 lines
65 KiB
JavaScript
1741 lines
65 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/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 };
|