Files
Claude-Code-Monitor/server/openapi-extra/run.js
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

1170 lines
43 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 OpenAPI 3.0 path + schema fragments for the dashboard's Run feature
* (`server/routes/run.js`, mounted at `/api/run`). Spawns and supervises real
* `claude` Code subprocesses (headless one-shot or multi-turn conversation),
* streams structured envelopes over the dashboard WebSocket, and exposes a
* small CRUD-ish surface for run management + history.
*
* Merged into the base spec by `server/openapi-extra.js` -> `createOpenApiSpec()`.
* Exports exactly `{ tags, schemas, paths }`. Every schema name is prefixed
* `Run`. The shared error envelope `ErrorResponse` (`{ error: { code, message } }`)
* is defined in the base spec and only referenced here — never redefined.
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const tags = [
{
name: "Run",
description:
"Spawn and supervise Claude Code runs from the dashboard (headless or interactive conversation), stream output over WebSocket, and manage run history",
},
];
/**
* Reusable response fragment for the loopback same-origin guard. Applied to
* EVERY route in `server/routes/run.js` (the router-level `sameOriginGuard`
* middleware): browser requests whose `Origin`/`Referer` host is not one of
* localhost / 127.0.0.1 / ::1 / 0.0.0.0 are rejected before any handler runs.
* Server/CLI requests (curl) usually carry no `Origin` and pass through.
*/
const ebadOrigin403 = {
description:
"Cross-origin rejected (EBADORIGIN). Enforced by the router-level loopback same-origin guard on every /api/run route: a browser request carrying an Origin/Referer whose host is not localhost / 127.0.0.1 / ::1 / 0.0.0.0 is refused. Requests without an Origin header (e.g. curl) are allowed.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: { code: "EBADORIGIN", message: "cross-origin requests are not allowed" },
},
},
},
};
const schemas = {
RunHandle: {
type: "object",
description:
"Live, in-memory view of a spawned `claude` subprocess. Returned by GET /api/run (in `items`), POST /api/run (201), and GET /api/run/{id}. Backed by the spawner's handle map, which reaps each handle 5 minutes after the process exits — after that the run is only visible via GET /api/run/history.",
required: [
"id",
"pid",
"mode",
"cwd",
"model",
"permissionMode",
"effort",
"prompt",
"argv",
"resumeSessionId",
"status",
"startedAt",
"endedAt",
"exitCode",
"signal",
"error",
"sessionId",
"envelopeCount",
"stdoutTail",
"stderrTail",
],
properties: {
id: {
type: "string",
format: "uuid",
description: "Server-generated run id (UUID v4). Distinct from the Claude session id.",
example: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
},
pid: {
type: "integer",
nullable: true,
description: "OS process id of the spawned `claude` child, or null if the spawn failed.",
example: 48213,
},
mode: {
type: "string",
enum: ["headless", "conversation"],
description:
"`headless`: single-shot — prompt is passed via argv `-p`, stdin is closed, the process exits after one turn. `conversation`: multi-turn — stdin stays open (`--input-format stream-json`) and follow-up turns are delivered via POST /api/run/{id}/message.",
example: "conversation",
},
cwd: {
type: "string",
description:
"Absolute working directory the child was spawned in (sanitised at request time — must be an existing absolute directory).",
example: "/Users/dev/projects/my-app",
},
model: {
type: "string",
nullable: true,
description: "Model alias/id passed via `--model`, or null to inherit the user's default.",
example: "claude-opus-4-8",
},
permissionMode: {
type: "string",
enum: ["acceptEdits", "default", "plan", "bypassPermissions"],
description:
"Permission mode passed via `--permission-mode`. Defaults to `acceptEdits` when omitted or invalid.",
example: "acceptEdits",
},
effort: {
type: "string",
enum: ["low", "medium", "high", "xhigh", "max"],
nullable: true,
description:
"Thinking-effort level passed via `--effort` (higher = more reasoning tokens before the assistant turn). Null inherits the model default.",
example: "high",
},
prompt: {
type: "string",
description:
"The initial user prompt. May be empty only when resuming a conversation (the child idles on the resumed transcript until a follow-up arrives).",
example: "Refactor the auth module and add tests.",
},
argv: {
type: "array",
items: { type: "string" },
description:
"Exact argv vector passed to the `claude` binary (always includes `--output-format stream-json --verbose --include-partial-messages` plus the resolved flags).",
example: [
"--output-format",
"stream-json",
"--verbose",
"--include-partial-messages",
"--permission-mode",
"acceptEdits",
"--input-format",
"stream-json",
"--model",
"claude-opus-4-8",
],
},
resumeSessionId: {
type: "string",
nullable: true,
description:
"Claude session id this run resumed (`--resume`), or null for a fresh run. Conversation mode only.",
example: null,
},
status: {
type: "string",
enum: ["spawning", "running", "completed", "error", "killed"],
description:
"Lifecycle status. `spawning` -> `running` on the first parsed output envelope; terminal states are `completed` (exit 0), `error` (non-zero exit or spawn error), or `killed` (terminated via DELETE). Each transition is broadcast as a `run_status` WebSocket message.",
example: "running",
},
startedAt: {
type: "integer",
format: "int64",
description: "Spawn time in epoch milliseconds.",
example: 1718000000000,
},
endedAt: {
type: "integer",
format: "int64",
nullable: true,
description: "Exit time in epoch milliseconds, or null while still live.",
example: null,
},
exitCode: {
type: "integer",
nullable: true,
description:
"Process exit code once the child exits; null while running or if killed by signal.",
example: null,
},
signal: {
type: "string",
nullable: true,
description:
"Terminating signal name (e.g. `SIGTERM`/`SIGKILL`) if the child was killed by a signal; null otherwise.",
example: null,
},
error: {
type: "string",
nullable: true,
description:
"Spawn/runtime error message if the child failed to start or errored; null otherwise.",
example: null,
},
sessionId: {
type: "string",
nullable: true,
description:
"Claude session id, captured from the `system/init` stream envelope (optimistically pre-seeded with resumeSessionId). Used to deep-link to /api/sessions/{id}. Null until the init envelope arrives.",
example: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
},
envelopeCount: {
type: "integer",
minimum: 0,
description:
"Total stream-json envelopes parsed for this run so far (monotonic; not capped by the in-memory replay buffer).",
example: 12,
},
stdoutTail: {
type: "string",
description:
"Trailing slice of the child's raw stdout (capped at ~4 KiB) for quick diagnostics.",
example: '{"type":"result","subtype":"success",...}\n',
},
stderrTail: {
type: "string",
description:
"Trailing slice of the child's raw stderr (capped at ~4 KiB); also captures parse errors.",
example: "",
},
},
},
RunHandleWithEnvelopes: {
allOf: [
{ $ref: "#/components/schemas/RunHandle" },
{
type: "object",
description:
"RunHandle plus the in-memory envelope replay buffer, returned by GET /api/run/{id}?envelopes=1.",
required: ["envelopes"],
properties: {
envelopes: {
type: "array",
description:
"The most recent stream-json envelopes (capped at 500 per handle) passed through verbatim from the `claude` child — system/init, assistant text + tool_use, user tool_result, result/success, partial stream_event deltas, etc. Lets a late-attaching client replay what it missed. Full transcript is always available via /api/sessions/{id}.",
items: { type: "object", additionalProperties: true },
},
},
},
],
},
RunListResponse: {
type: "object",
description: "All currently-tracked run handles (newest first) plus concurrency telemetry.",
required: ["items", "maxConcurrent", "activeCount"],
properties: {
items: {
type: "array",
items: { $ref: "#/components/schemas/RunHandle" },
description:
"Live + recently-exited handles, sorted by startedAt descending. Handles are reaped 5 minutes after exit.",
},
maxConcurrent: {
type: "integer",
description:
"Concurrency cap (env `RUN_MAX_CONCURRENT`, default effectively uncapped at 10000). Spawns over this throw ECONCURRENCY (HTTP 429).",
example: 10000,
},
activeCount: {
type: "integer",
minimum: 0,
description: "Number of handles currently in `spawning` or `running` status.",
example: 2,
},
},
},
RunHistoryItem: {
type: "object",
description:
"A persisted run record from the `dashboard_runs` sqlite table. Survives the 5-minute in-memory reap so past runs remain visible and resumable. Fields are snake_case (DB column names) and timestamps are ISO-8601 strings, distinct from the camelCase epoch-ms fields on RunHandle.",
required: [
"id",
"session_id",
"mode",
"cwd",
"model",
"permission_mode",
"effort",
"resume_session_id",
"prompt_preview",
"status",
"exit_code",
"started_at",
"ended_at",
"isLive",
"lane_id",
],
properties: {
id: {
type: "string",
format: "uuid",
description: "Run id (matches RunHandle.id).",
example: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
},
session_id: {
type: "string",
nullable: true,
description: "Claude session id once known (from system/init), else null.",
example: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
},
mode: {
type: "string",
enum: ["headless", "conversation"],
description: "Run mode at spawn time.",
example: "conversation",
},
cwd: {
type: "string",
description: "Absolute working directory the run was spawned in.",
example: "/Users/dev/projects/my-app",
},
model: {
type: "string",
nullable: true,
description: "Model used, or null for the default.",
example: "claude-opus-4-8",
},
permission_mode: {
type: "string",
nullable: true,
enum: ["acceptEdits", "default", "plan", "bypassPermissions"],
description: "Permission mode recorded at spawn time.",
example: "acceptEdits",
},
effort: {
type: "string",
nullable: true,
enum: ["low", "medium", "high", "xhigh", "max"],
description: "Effort level recorded at spawn time, or null.",
example: "high",
},
resume_session_id: {
type: "string",
nullable: true,
description: "Session id this run resumed, or null.",
example: null,
},
prompt_preview: {
type: "string",
nullable: true,
description:
"First 500 characters of the initial prompt (truncated preview for the history list), or null.",
example: "Refactor the auth module and add tests.",
},
status: {
type: "string",
enum: ["spawning", "running", "completed", "error", "killed", "abandoned"],
description:
"Persisted status. `abandoned` is set on server boot for rows left `running`/`spawning` by a previous process (their in-memory handles were wiped by the restart) so the UI does not show them as live.",
example: "completed",
},
exit_code: {
type: "integer",
nullable: true,
description: "Recorded process exit code, or null.",
example: 0,
},
started_at: {
type: "string",
format: "date-time",
description: "Spawn time (ISO-8601).",
example: "2026-06-25T17:00:00.000Z",
},
ended_at: {
type: "string",
format: "date-time",
nullable: true,
description: "Exit time (ISO-8601), or null if still running / never recorded.",
example: "2026-06-25T17:01:42.500Z",
},
isLive: {
type: "boolean",
description:
"Computed at request time: true when a live in-memory handle for this id is currently `running` or `spawning`. Lets the UI mark which history rows are re-attachable.",
example: false,
},
lane_id: {
type: "integer",
nullable: true,
description:
"Lane this run was started through (POST /api/lanes/{id}/start), or null for a run spawned straight from POST /api/run. Filterable via ?laneId=.",
example: null,
},
},
},
RunHistoryResponse: {
type: "object",
description:
"Persisted run history, most recent first. Empty `items` when the persistence DB is unavailable.",
required: ["items"],
properties: {
items: {
type: "array",
items: { $ref: "#/components/schemas/RunHistoryItem" },
},
},
},
RunCwdSuggestion: {
type: "object",
description:
"A suggested working directory for spawning a run. Only directories that still exist on disk are returned.",
required: ["kind", "path", "label"],
properties: {
kind: {
type: "string",
enum: ["dashboard", "home", "recent"],
description:
"`dashboard`: the dashboard server's own cwd (always first). `home`: $HOME. `recent`: distinct cwds Claude Code has been used in, sourced from the sessions table.",
example: "recent",
},
path: {
type: "string",
description: "Absolute, resolved directory path.",
example: "/Users/dev/projects/my-app",
},
label: {
type: "string",
description: "Human-friendly label (defaults to the directory basename).",
example: "my-app",
},
},
},
RunCwdsResponse: {
type: "object",
description: "Working-directory suggestions for the Run launcher.",
required: ["items"],
properties: {
items: {
type: "array",
items: { $ref: "#/components/schemas/RunCwdSuggestion" },
},
},
},
RunFilesResponse: {
type: "object",
description:
"File-path autocomplete results for the prompt editor's `@` references — up to 40 paths relative to the given cwd, shortest first.",
required: ["items"],
properties: {
items: {
type: "array",
items: { type: "string" },
description:
"Paths relative to the resolved cwd. Dotfiles (except .env/.gitignore) and heavy build dirs (node_modules, .git, dist, build, etc.) are skipped.",
example: ["package.json", "src/index.ts", "server/routes/run.js"],
},
},
},
RunBinaryResponse: {
type: "object",
description:
"Whether the `claude` binary is resolvable on PATH (probed via which/where; the binary is not invoked). Lets the UI warn before the user clicks Run.",
required: ["found", "path"],
properties: {
found: {
type: "boolean",
description: "True when `claude` resolves on PATH.",
example: true,
},
path: {
type: "string",
nullable: true,
description: "Absolute path to the resolved `claude` binary, or null when not found.",
example: "/usr/local/bin/claude",
},
},
},
RunSpawnRequest: {
type: "object",
description:
"Request body for spawning a run. SIDE EFFECT: a successful call spawns a real `claude` process in `cwd`, begins streaming `run_stream`/`run_status` WebSocket messages, and persists a row to `dashboard_runs`.",
required: ["prompt"],
properties: {
prompt: {
type: "string",
description:
"Initial user prompt. Required and non-empty UNLESS mode is `conversation` AND resumeSessionId is set (a resumed conversation may start empty and idle on stdin).",
example: "Refactor the auth module and add tests.",
},
mode: {
type: "string",
enum: ["headless", "conversation"],
default: "conversation",
description:
"`headless` for a single-shot run; `conversation` for a multi-turn run that accepts follow-ups via POST /api/run/{id}/message. Any value other than `headless` is treated as `conversation`.",
example: "conversation",
},
cwd: {
type: "string",
description:
"Absolute working directory for the child. Must exist as a directory at request time. Omitted/empty defaults to the dashboard server's cwd. Non-absolute or missing paths are rejected with EBADCWD.",
example: "/Users/dev/projects/my-app",
},
model: {
type: "string",
description:
"Optional model alias/id passed via `--model`. Omit to inherit the user's default.",
example: "claude-opus-4-8",
},
resumeSessionId: {
type: "string",
description:
"Optional Claude session id to resume via `--resume`. Must match /^[A-Za-z0-9-]{8,}$/ and requires conversation mode, else EBADSESSION / EBADMODE.",
example: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
},
effort: {
type: "string",
enum: ["low", "medium", "high", "xhigh", "max"],
description:
"Optional thinking-effort level passed via `--effort`. An invalid value is rejected with EBADEFFORT.",
example: "high",
},
permissionMode: {
type: "string",
enum: ["acceptEdits", "default", "plan", "bypassPermissions"],
default: "acceptEdits",
description:
"Permission mode passed via `--permission-mode`. Unknown values silently fall back to `acceptEdits`.",
example: "acceptEdits",
},
},
},
RunSpawnConcurrencyResponse: {
type: "object",
description:
"HTTP 429 body when the concurrency cap is reached (ECONCURRENCY). In addition to the standard `error` envelope, this response carries a top-level `running` array listing the runs currently occupying the cap.",
required: ["error", "running"],
properties: {
error: { $ref: "#/components/schemas/ErrorObject" },
running: {
type: "array",
description:
"The live runs (status running/spawning) that are holding the concurrency slots.",
items: {
type: "object",
required: ["id", "pid", "startedAt", "mode"],
properties: {
id: { type: "string", format: "uuid", example: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c" },
pid: { type: "integer", nullable: true, example: 48213 },
startedAt: { type: "integer", format: "int64", example: 1718000000000 },
mode: { type: "string", enum: ["headless", "conversation"], example: "conversation" },
},
},
},
},
},
RunMessageRequest: {
type: "object",
description:
"Request body for sending a follow-up turn into a running conversation. SIDE EFFECT: writes a stream-json user envelope to the child's stdin and broadcasts a `run_input_ack` WebSocket message.",
required: ["text"],
properties: {
text: {
type: "string",
description: "The follow-up user message. Required and non-empty (else EBADINPUT).",
example: "Also update the README to document the new flags.",
},
},
},
RunMessageResponse: {
type: "object",
description: "Acknowledgement that the follow-up turn was written to the child's stdin.",
required: ["messageId"],
properties: {
messageId: {
type: "string",
format: "uuid",
description:
"Server-generated id for this input turn; echoed in the `run_input_ack` WebSocket message.",
example: "9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f",
},
},
},
RunKillResponse: {
type: "object",
description: "Acknowledgement that a kill was issued (or that the run had already terminated).",
required: ["ok"],
properties: {
ok: { type: "boolean", enum: [true], example: true },
},
},
};
const paths = {
"/api/run": {
get: {
tags: ["Run"],
summary: "List live + recently-exited runs",
description:
"Returns every run handle currently held in memory (newest first) plus concurrency telemetry (`maxConcurrent`, `activeCount`). Handles are reaped 5 minutes after the process exits, so terminal runs disappear from here but remain in GET /api/run/history. Read-only — no process is spawned. The loopback same-origin guard applies.",
operationId: "runList",
responses: {
200: {
description: "Tracked runs and concurrency telemetry",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunListResponse" },
example: {
items: [
{
id: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
pid: 48213,
mode: "conversation",
cwd: "/Users/dev/projects/my-app",
model: "claude-opus-4-8",
permissionMode: "acceptEdits",
effort: "high",
prompt: "Refactor the auth module and add tests.",
argv: [
"--output-format",
"stream-json",
"--verbose",
"--include-partial-messages",
"--permission-mode",
"acceptEdits",
"--input-format",
"stream-json",
],
resumeSessionId: null,
status: "running",
startedAt: 1718000000000,
endedAt: null,
exitCode: null,
signal: null,
error: null,
sessionId: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
envelopeCount: 12,
stdoutTail: '{"type":"assistant",...}\n',
stderrTail: "",
},
],
maxConcurrent: 10000,
activeCount: 1,
},
},
},
},
403: ebadOrigin403,
},
},
post: {
tags: ["Run"],
summary: "Spawn a new run",
description:
"Spawns a real `claude` Code subprocess in the sanitised `cwd`. In `headless` mode the prompt is passed via argv and stdin is closed (one turn, then exit); in `conversation` mode stdin stays open for follow-ups via POST /api/run/{id}/message, and an existing session may be resumed with `resumeSessionId`. The child is always run with `--output-format stream-json --verbose --include-partial-messages`; parsed envelopes are broadcast as `run_stream` WebSocket messages and lifecycle transitions as `run_status`. A row is persisted to `dashboard_runs`, and the in-memory handle is reaped 5 minutes after exit. Concurrency is capped (ECONCURRENCY -> 429). The loopback same-origin guard applies. Returns the freshly-created RunHandle.",
operationId: "runSpawn",
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunSpawnRequest" },
example: {
prompt: "Refactor the auth module and add tests.",
mode: "conversation",
cwd: "/Users/dev/projects/my-app",
model: "claude-opus-4-8",
effort: "high",
permissionMode: "acceptEdits",
},
},
},
},
responses: {
201: {
description: "Run spawned; returns the new run handle (status `spawning`).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunHandle" },
example: {
id: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
pid: 48213,
mode: "conversation",
cwd: "/Users/dev/projects/my-app",
model: "claude-opus-4-8",
permissionMode: "acceptEdits",
effort: "high",
prompt: "Refactor the auth module and add tests.",
argv: [
"--output-format",
"stream-json",
"--verbose",
"--include-partial-messages",
"--permission-mode",
"acceptEdits",
"--input-format",
"stream-json",
"--model",
"claude-opus-4-8",
"--effort",
"high",
],
resumeSessionId: null,
status: "spawning",
startedAt: 1718000000000,
endedAt: null,
exitCode: null,
signal: null,
error: null,
sessionId: null,
envelopeCount: 0,
stdoutTail: "",
stderrTail: "",
},
},
},
},
400: {
description:
"Invalid spawn request — EBADPROMPT (prompt required), EBADCWD (cwd not an existing absolute directory), EBADMODE (bad mode, or resumeSessionId outside conversation mode), EBADEFFORT (effort not low/medium/high/xhigh/max), or EBADSESSION (resumeSessionId not a valid session id).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
examples: {
badPrompt: {
value: { error: { code: "EBADPROMPT", message: "prompt is required" } },
},
badCwd: {
value: { error: { code: "EBADCWD", message: "cwd must be an absolute path" } },
},
badMode: {
value: {
error: {
code: "EBADMODE",
message: 'mode must be "headless" or "conversation"',
},
},
},
badEffort: {
value: {
error: {
code: "EBADEFFORT",
message: "effort must be one of: low, medium, high, xhigh, max",
},
},
},
badSession: {
value: {
error: {
code: "EBADSESSION",
message: "resumeSessionId is not a valid session id",
},
},
},
},
},
},
},
403: ebadOrigin403,
429: {
description:
"Concurrency cap reached (ECONCURRENCY). The body additionally carries a top-level `running` array listing the runs currently holding the cap.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunSpawnConcurrencyResponse" },
example: {
error: { code: "ECONCURRENCY", message: "concurrency limit 10000 reached" },
running: [
{
id: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
pid: 48213,
startedAt: 1718000000000,
mode: "conversation",
},
],
},
},
},
},
500: {
description: "Unexpected spawn failure (EINTERNAL).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: { error: { code: "EINTERNAL", message: "spawn claude ENOENT" } },
},
},
},
},
},
},
"/api/run/history": {
get: {
tags: ["Run"],
summary: "List persisted run history",
description:
"Returns the persistent history of runs spawned via the dashboard, sourced from the `dashboard_runs` sqlite table (newest first). Unlike GET /api/run, this survives the 5-minute in-memory reap so past runs stay visible and resumable for days. Each item is cross-referenced against live handles to set `isLive`. Returns `{ items: [] }` if the persistence DB is unavailable. Read-only — no process is spawned. The loopback same-origin guard applies.",
operationId: "runHistory",
parameters: [
{
name: "limit",
in: "query",
required: false,
schema: { type: "integer", minimum: 1, maximum: 500, default: 50 },
description: "Maximum number of history rows to return (default 50, clamped to 1500).",
example: 50,
},
{
name: "laneId",
in: "query",
required: false,
schema: { type: "integer" },
description:
"Return only the runs started through this lane (POST /api/lanes/{id}/start). Omit for every run; runs spawned from POST /api/run carry no lane and are excluded whenever this is set.",
example: 5,
},
],
responses: {
200: {
description: "Run history, most recent first",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunHistoryResponse" },
example: {
items: [
{
id: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
session_id: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
mode: "conversation",
cwd: "/Users/dev/projects/my-app",
model: "claude-opus-4-8",
permission_mode: "acceptEdits",
effort: "high",
resume_session_id: null,
prompt_preview: "Refactor the auth module and add tests.",
status: "completed",
exit_code: 0,
started_at: "2026-06-25T17:00:00.000Z",
ended_at: "2026-06-25T17:01:42.500Z",
isLive: false,
lane_id: null,
},
],
},
},
},
},
403: ebadOrigin403,
},
},
},
"/api/run/cwds": {
get: {
tags: ["Run"],
summary: "Suggest working directories",
description:
"Suggests plausible working directories for the Run launcher: the dashboard server's cwd (always first), $HOME, and distinct recent cwds Claude Code has been used in (from the sessions table). Only directories that still exist on disk are returned; the DB lookup is best-effort. Read-only — no process is spawned. The loopback same-origin guard applies.",
operationId: "runCwds",
parameters: [
{
name: "q",
in: "query",
required: false,
schema: { type: "string" },
description: "Optional case-insensitive substring filter (also applied client-side).",
example: "my-app",
},
],
responses: {
200: {
description: "Working-directory suggestions",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunCwdsResponse" },
example: {
items: [
{
kind: "dashboard",
path: "/Users/dev/Claude-Code-Agent-Monitor",
label: "Dashboard server",
},
{ kind: "home", path: "/Users/dev", label: "Home" },
{ kind: "recent", path: "/Users/dev/projects/my-app", label: "my-app" },
],
},
},
},
},
403: ebadOrigin403,
},
},
},
"/api/run/files": {
get: {
tags: ["Run"],
summary: "Autocomplete files within a cwd",
description:
"Walks the given `cwd` and returns up to 40 file paths (relative to that cwd, shortest first) for the prompt editor's `@` references. The cwd is validated via the same sanitiser as spawning (must be an existing absolute directory). Dotfiles (except .env/.gitignore) and heavy build dirs (node_modules, .git, dist, build, out, .next, coverage, target, .venv, __pycache__, etc.) are skipped; the walk is bounded (≤5000 entries visited). Read-only — no process is spawned. The loopback same-origin guard applies.",
operationId: "runFiles",
parameters: [
{
name: "cwd",
in: "query",
required: false,
schema: { type: "string" },
description:
"Absolute directory to walk. Must exist; defaults to the dashboard server's cwd when omitted. Invalid values yield 400 EBADCWD.",
example: "/Users/dev/projects/my-app",
},
{
name: "q",
in: "query",
required: false,
schema: { type: "string" },
description: "Optional case-insensitive substring filter applied to the relative path.",
example: "route",
},
],
responses: {
200: {
description: "Matching file paths relative to cwd",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunFilesResponse" },
example: { items: ["package.json", "src/index.ts", "server/routes/run.js"] },
},
},
},
400: {
description: "Invalid cwd (EBADCWD): not a string, not absolute, or does not exist.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: { error: { code: "EBADCWD", message: "cwd does not exist: /no/such/dir" } },
},
},
},
403: ebadOrigin403,
},
},
},
"/api/run/binary": {
get: {
tags: ["Run"],
summary: "Check whether the `claude` binary is on PATH",
description:
"Probes PATH (via which/where) for the `claude` binary so the UI can warn before the user clicks Run. The binary is NOT invoked — only resolved. Read-only — no process is spawned beyond the lookup. The loopback same-origin guard applies.",
operationId: "runBinary",
responses: {
200: {
description: "Binary resolution result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunBinaryResponse" },
example: { found: true, path: "/usr/local/bin/claude" },
},
},
},
403: ebadOrigin403,
},
},
},
"/api/run/{id}/message": {
post: {
tags: ["Run"],
summary: "Send a follow-up turn to a conversation run",
description:
"Writes a stream-json user envelope to a running conversation's stdin and broadcasts a `run_input_ack` WebSocket message. Only valid for `conversation`-mode runs that are still `running`/`spawning` with writable stdin. SIDE EFFECT: the child resumes processing the new turn and emits further `run_stream`/`run_status` messages. The loopback same-origin guard applies.",
operationId: "runSendMessage",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
description: "Run id (RunHandle.id).",
example: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunMessageRequest" },
example: { text: "Also update the README to document the new flags." },
},
},
},
responses: {
200: {
description: "Turn written to stdin",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunMessageResponse" },
example: { messageId: "9c0d1e2f-3a4b-5c6d-7e8f-9a0b1c2d3e4f" },
},
},
},
400: {
description:
"Cannot deliver input — EBADINPUT (text required), EWRONGMODE (run is not conversation mode), ENOTRUNNING (run is not running/spawning), or ESTDINCLOSED (child stdin not writable).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
examples: {
badInput: { value: { error: { code: "EBADINPUT", message: "text is required" } } },
wrongMode: {
value: {
error: {
code: "EWRONGMODE",
message: "only conversation mode accepts follow-up input",
},
},
},
notRunning: {
value: { error: { code: "ENOTRUNNING", message: "run is completed" } },
},
stdinClosed: {
value: { error: { code: "ESTDINCLOSED", message: "stdin is not writable" } },
},
},
},
},
},
403: ebadOrigin403,
404: {
description: "No run with this id (ENOTFOUND) — it never existed or was reaped.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: { error: { code: "ENOTFOUND", message: "run not found" } },
},
},
},
},
},
},
"/api/run/{id}": {
get: {
tags: ["Run"],
summary: "Get a single run",
description:
"Returns the in-memory handle for one run. Pass `?envelopes=1` to additionally include the bounded (≤500) stream-json envelope replay buffer so a client can re-attach to an in-flight run and see what it missed. Available only while the handle is live or within the 5-minute post-exit reap window — afterwards use GET /api/run/history. Read-only — no process is spawned. The loopback same-origin guard applies.",
operationId: "runGet",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
description: "Run id (RunHandle.id).",
example: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
},
{
name: "envelopes",
in: "query",
required: false,
schema: { type: "string", enum: ["1"] },
description: "Set to `1` to include the `envelopes` replay buffer in the response.",
example: "1",
},
],
responses: {
200: {
description: "The run handle (with `envelopes` appended when `?envelopes=1`).",
content: {
"application/json": {
schema: {
oneOf: [
{ $ref: "#/components/schemas/RunHandle" },
{ $ref: "#/components/schemas/RunHandleWithEnvelopes" },
],
},
example: {
id: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
pid: 48213,
mode: "conversation",
cwd: "/Users/dev/projects/my-app",
model: "claude-opus-4-8",
permissionMode: "acceptEdits",
effort: "high",
prompt: "Refactor the auth module and add tests.",
argv: ["--output-format", "stream-json", "--verbose", "--include-partial-messages"],
resumeSessionId: null,
status: "running",
startedAt: 1718000000000,
endedAt: null,
exitCode: null,
signal: null,
error: null,
sessionId: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
envelopeCount: 12,
stdoutTail: '{"type":"assistant",...}\n',
stderrTail: "",
envelopes: [
{
type: "system",
subtype: "init",
session_id: "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
},
{
type: "assistant",
message: {
role: "assistant",
content: [{ type: "text", text: "Working on it..." }],
},
},
],
},
},
},
},
403: ebadOrigin403,
404: {
description: "No run with this id (ENOTFOUND) — it never existed or was reaped.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: { error: { code: "ENOTFOUND", message: "run not found" } },
},
},
},
},
},
delete: {
tags: ["Run"],
summary: "Kill a run",
description:
"Terminates a live run: sends SIGTERM (escalating to SIGKILL after ~5s if needed), sets the handle status to `killed`, broadcasts a `run_status` WebSocket message, persists the status to `dashboard_runs`, and schedules the handle for the 5-minute reap. Idempotent — returns `{ ok: true }` even if the run had already terminated. The loopback same-origin guard applies.",
operationId: "runKill",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: { type: "string", format: "uuid" },
description: "Run id (RunHandle.id).",
example: "3f2c9a1e-7b4d-4e21-9b6a-1d2e3f4a5b6c",
},
],
responses: {
200: {
description: "Kill issued (or run already terminated).",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/RunKillResponse" },
example: { ok: true },
},
},
},
403: ebadOrigin403,
404: {
description: "No run with this id (ENOTFOUND) — it never existed or was reaped.",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: { error: { code: "ENOTFOUND", message: "run not found" } },
},
},
},
},
},
},
};
module.exports = { tags, schemas, paths };