Files
AgentHub/server/openapi-extra/misc.js
T
nntrivi2001 969b06253c feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).

Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.

C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.

C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.

D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.

Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.

Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
2026-09-29 16:09:19 +07:00

1534 lines
58 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/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.",
},
routers: {
type: "object",
properties: {
missing: {
type: "array",
items: { type: "string" },
description:
"`<lane id> (<title>): <profile>` for every lane naming an AI Router profile the new home's store (`<CLAUDE_HOME>/agenthub-routers/`) does not have — such a lane starts on its base configuration.",
},
},
},
},
},
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/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 Router layers into, 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). On success it re-writes the AI Router layer (`<name>.config.toml`) of every codex lane on a router into the new home, reported as `routers`, because `codex -p <name>` loads it from there. 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", "routers"],
properties: {
ok: { type: "boolean", enum: [true] },
codex_home: { type: "string" },
env_owned: { type: "boolean" },
routers: {
type: "object",
required: ["updated", "failed"],
properties: {
updated: { type: "integer" },
failed: { type: "array", items: { type: "string" } },
},
},
},
},
example: {
ok: true,
codex_home: "/Users/son/.codex-work",
env_owned: false,
routers: { updated: 1, failed: [] },
},
},
},
},
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 };