Skip to content

Latest commit

 

History

History
2452 lines (1798 loc) · 67.5 KB

File metadata and controls

2452 lines (1798 loc) · 67.5 KB

OpenKit REST API Reference

Overview

OpenKit exposes a REST API via a Hono HTTP server. The web UI, Electron app, and external integrations all communicate through this API. The server runs on localhost (default port 4040, auto-incremented if occupied).

All endpoints return JSON unless otherwise noted. Standard response patterns:

  • Success: { success: true, ... } or domain-specific payloads
  • Error: { success: false, error: "description" } with appropriate HTTP status code
  • CORS: Enabled for all origins (*)

Worktree Management

Core CRUD and lifecycle operations for git worktrees.

All :id worktree route params are resolved canonically: exact match first, then case-insensitive fallback (for example AVO-241 resolves to avo-241 when unique). If case-insensitive lookup is ambiguous, the API returns an explicit ambiguity error instead of guessing.

GET /api/worktrees

List all managed worktrees with their current status.

  • Response: { worktrees: WorktreeInfo[] }

Each WorktreeInfo object includes: id, path, branch, status ("running" | "stopped" | "starting" | "creating"), ports, offset, pid, lastActivity, jiraUrl, jiraStatus, githubPrUrl, githubPrState, linearUrl, linearStatus, localIssueId, localIssueStatus, hasUncommitted, hasUnpushed, commitsAhead, commitsAheadOfBase.

POST /api/worktrees

Create a new worktree.

  • Request:
    {
      "branch": "feature/my-branch",
      "id": "optional-custom-id",
      "name": "optional-display-name"
    }
    Only branch is required.
  • Response (201): { success: true, worktree: WorktreeInfo, ports?: number[], pid?: number }
  • Error (400): { success: false, error: "..." }
  • If a conflicting worktree already exists, response includes code: "WORKTREE_EXISTS" and canonical worktreeId.

POST /api/worktrees/:id/start

Start the dev server process for a worktree.

  • Response: { success: true, ... } with port and PID info
  • Error (400): { success: false, error: "..." }

POST /api/worktrees/:id/stop

Stop the running dev server process for a worktree.

  • Response: { success: true, ... }
  • Error (400): { success: false, error: "..." }

GET /api/worktrees/:id/open-targets

Detect supported local apps for opening the selected worktree path and return the currently selected target.

  • Response:
    {
      "success": true,
      "targets": [
        { "target": "cursor", "label": "Cursor" },
        { "target": "vscode", "label": "VSCode" },
        { "target": "file-manager", "label": "Finder" }
      ],
      "selectedTarget": "cursor"
    }
  • targets contains only autodetected, currently available options on this machine.
  • selectedTarget is derived from persisted config (openProjectTarget) when that value is available; otherwise it falls back to the server's priority order.
  • Error (404): { success: false, error: "Worktree \"...\" not found" }
  • Error (409): { success: false, error: "Worktree \"...\" is ambiguous. Matches: ..." }

POST /api/worktrees/:id/open

Open a worktree in a specific app target (IDE/file manager/terminal).

  • Request:
    { "target": "vscode" }
    If omitted, target defaults to "file-manager".
  • Allowed target values: "file-manager", "cursor", "vscode", "zed", "intellij", "webstorm", "terminal", "warp", "ghostty", "neovim".
  • Response: { success: true } or { success: false, error: "..." }
  • On success, the selected target is persisted to project config as openProjectTarget.
  • Error (400): invalid target or open failure, { success: false, error: "..." }
  • Error (404): { success: false, error: "Worktree \"...\" not found" }
  • Error (409): { success: false, error: "Worktree \"...\" is ambiguous. Matches: ..." }

PATCH /api/worktrees/:id

Rename a worktree (directory name and/or branch).

  • Request:
    {
      "name": "new-directory-name",
      "branch": "new-branch-name"
    }
    Both fields are optional.
  • Response: { success: true, ... }
  • Error (400): { success: false, error: "..." }

DELETE /api/worktrees/:id

Remove a worktree transactionally (graceful stop, scoped terminal teardown, filesystem removal, link cleanup, lifecycle hooks, then notify).

  • Response:
    {
      "success": true,
      "worktreeId": "LOCAL-3",
      "removedTerminalSessions": 2,
      "removedRunningProcess": true,
      "clearedLinks": 1,
      "deleteOpId": "c3b4..."
    }
  • Error (404/409): canonical resolution failure with code
    • { success: false, code: "WORKTREE_NOT_FOUND", error: "..." }
    • { success: false, code: "WORKTREE_ID_AMBIGUOUS", error: "..." }
  • Error (400): validation/delete failure
    • { success: false, code: "INVALID_WORKTREE_ID" | "WORKTREE_REMOVE_FAILED", error: "...", deleteOpId: "..." }

GET /api/worktrees/:id/logs

Get process output logs for a worktree.

  • Response: { logs: string[] }

POST /api/worktrees/:id/recover

Recover a worktree that has become inconsistent (e.g., directory deleted externally).

  • Request:
    {
      "action": "reuse",
      "branch": "optional-branch-override"
    }
    action must be "reuse" or "recreate".
  • Response: { success: true, ... }
  • Error (400): { success: false, error: "..." }

Agent Rules

Read, write, and delete project-level agent instruction files (CLAUDE.md, AGENTS.md) at the project root.

GET /api/agent-rules/:fileId

Get the content of an agent rule file.

  • URL params: fileId = claude-md | agents-md
  • Response: { exists: boolean, content: string }
  • Error (404): { error: "Unknown file" } (invalid fileId)

PUT /api/agent-rules/:fileId

Create or update an agent rule file. Creates parent directories if needed.

  • Request:
    { "content": "# CLAUDE.md\n\nInstructions here..." }
  • Response: { success: true }

DELETE /api/agent-rules/:fileId

Delete an agent rule file from disk.

  • Response: { success: true }
  • Error (404): { error: "Unknown file" } (invalid fileId)

Configuration

Manage .openkit/config.json and related settings.

GET /api/config

Get the current project configuration.

  • Response: { config: WorktreeConfig | null, projectName: string | null, hasBranchNameRule: boolean }

Returns null for config and projectName if the config file has been deleted.

PATCH /api/config

Update project configuration fields.

allowAgentCommits, allowAgentPushes, and allowAgentPRs are persisted to .openkit/config.local.json (local-only, non-committed). Other config fields are persisted to .openkit/config.json.

  • Request: Partial WorktreeConfig object (any fields to update)
  • Response: { success: true, ... }
  • Error (400): { success: false, error: "..." }

GET /api/config/detect

Auto-detect configuration values from the project directory without creating a config file. Inspects package.json, lockfiles, and git state.

  • Response: { success: true, config: { startCommand, installCommand, baseBranch, ... } }

POST /api/config/init

Initialize .openkit/config.json with provided or auto-detected values. Creates the .openkit directory, config file, and .gitignore.

  • Request (all optional, falls back to auto-detected):
    {
      "startCommand": "pnpm dev",
      "installCommand": "pnpm install",
      "baseBranch": "main",
      "force": true
    }
  • force: true allows overwriting an existing .openkit/config.json.
  • Response: { success: true, config: {...} }
  • Error (400): { success: false, error: "Config already exists" }

Also enables default project skills (currently work-on-task) by deploying them to per-agent project skill directories (best-effort, non-fatal).

GET /api/config/setup-status

Check whether .openkit config files need to be committed and/or pushed to the remote.

  • Response: { needsPush: boolean, files: string[] }

POST /api/config/retention-impact

Preview the impact of applying retention settings before committing. Returns how many entries and bytes would be removed without actually modifying the log files.

  • Request:

    {
      "logType": "activity",
      "retentionDays": 7,
      "maxSizeMB": 50
    }
    Field Type Required Description
    logType "activity" | "opsLog" Yes Which log to estimate impact for
    retentionDays number No Days threshold for time-based pruning
    maxSizeMB number No Size threshold in MB for size-based pruning
  • Response:

    {
      "entriesToRemove": 142,
      "bytesToRemove": 28400,
      "currentEntries": 500,
      "currentBytes": 102400
    }
  • Error (400): { error: "Invalid logType" }

POST /api/config/commit-setup

Commit and push the .openkit/config.json and .openkit/.gitignore files.

  • Request (optional):
    { "message": "chore: add OpenKit configuration" }
  • Response: { success: true } or { success: true, alreadyCommitted: true } or { success: true, pushFailed: true }
  • Error: { success: false, error: "..." }

GET /api/ports

Get discovered ports and offset step.

  • Response: { discovered: number[], offsetStep: number }

POST /api/discover

Run port discovery (starts the dev server, monitors with lsof, then stops it).

  • Response: { success: boolean, ports: number[], logs: string[], error?: string }

POST /api/detect-env

Auto-detect environment variable mappings that reference known ports.

  • Response: { success: true, envMapping: Record<string, string> }

GET /api/config/branch-name-rule

Get the branch name generation rule script content.

  • Query params: ?source=jira|linear|local (optional, for per-integration overrides)
  • Response: { content: string, hasOverride?: boolean }

PUT /api/config/branch-name-rule

Save or delete a branch name generation rule.

  • Request:
    {
      "content": "return `feature/${issueId}-${name}`",
      "source": "jira"
    }
    Send empty/null content to delete the rule. source is optional.
  • Response: { success: true }

GET /api/config/branch-name-rule/status

Check which per-integration branch name rule overrides exist.

  • Response: { overrides: { jira: boolean, linear: boolean, local: boolean } }

GET /api/config/commit-message-rule

Get the commit message generation rule script content.

  • Query params: ?source=jira|linear|local (optional)
  • Response: { content: string, hasOverride?: boolean }

PUT /api/config/commit-message-rule

Save or delete a commit message generation rule.

  • Request:
    {
      "content": "return `[${issueId}] ${message}`",
      "source": "jira"
    }
  • Response: { success: true }

GET /api/config/commit-message-rule/status

Check which per-integration commit message rule overrides exist.

  • Response: { overrides: { jira: boolean, linear: boolean, local: boolean } }

Local Config

Manage .openkit/config.local.json -- local-only, non-committed settings including agent git policy and keyboard shortcuts.

GET /api/local-config

Get the current local configuration.

  • Response: { config: LocalConfig }

Returns the full local config object, including allowAgentCommits, allowAgentPushes, allowAgentPRs, and shortcuts.

PATCH /api/local-config

Merge updates into the local configuration.

  • Request: Partial LocalConfig object (any fields to update)
    {
      "shortcuts": {
        "nav-worktrees": "meta+shift+w"
      }
    }
    Fields are shallow-merged at the top level; nested objects like shortcuts are deep-merged.
  • Response: { success: true, config: LocalConfig }
  • Error (400): { success: false, error: "..." }

GitHub Integration

GitHub operations via the gh CLI. Requires gh to be installed and authenticated. Worktree IDs in GitHub worktree routes (commit/push/create-pr) use canonical resolution (exact, then case-insensitive fallback).

GET /api/github/status

Get GitHub integration status.

  • Response: { installed: boolean, authenticated: boolean, repo: string | null, hasRemote: boolean, hasCommits: boolean }

POST /api/github/install

Install the gh CLI via Homebrew and start the authentication flow.

  • Response: { success: true, code: string | null } -- code is the one-time device code for browser auth

POST /api/github/login

Start the GitHub device login flow (assumes gh is already installed).

  • Response: { success: true, code: string } -- the one-time code; a browser window is also opened automatically
  • Error (400): { success: false, error: "..." }

POST /api/github/logout

Log out of GitHub via gh auth logout.

  • Response: { success: true }

POST /api/github/initial-commit

Create an initial commit in the repository (for new repos with no commits).

  • Response: { success: true, ... }

POST /api/github/create-repo

Create a new GitHub repository for the project.

  • Request:
    { "private": true }
  • Response: { success: true, ... }

POST /api/worktrees/:id/commit

Commit all changes in a worktree.

  • Request:
    { "message": "feat: add new feature" }
  • Response: { success: true, ... }
  • Error (400/404): { success: false, error: "..." }
  • Error (409): { success: false, error: "Worktree \"...\" is ambiguous. Matches: ..." }

POST /api/worktrees/:id/push

Push a worktree's branch to the remote.

  • Response: { success: true, ... }
  • Error (400/404): { success: false, error: "..." }
  • Error (409): { success: false, error: "Worktree \"...\" is ambiguous. Matches: ..." }

POST /api/worktrees/:id/create-pr

Create a GitHub pull request for a worktree's branch.

  • Request:
    {
      "title": "feat: new feature",
      "body": "Optional PR description"
    }
  • Response (201): { success: true, ... }
  • Error (400/404): { success: false, error: "..." }
  • Error (409): { success: false, error: "Worktree \"...\" is ambiguous. Matches: ..." }

GET /api/worktrees/:id/diff

List changed files in a worktree (uncommitted + optionally committed changes).

  • Query params:
    • includeCommitted (boolean, default false) — include committed changes vs origin/<baseBranch>
  • Response (200):
    {
      "success": true,
      "files": [
        {
          "path": "src/app.ts",
          "status": "modified",
          "linesAdded": 10,
          "linesRemoved": 3,
          "isBinary": false
        }
      ],
      "baseBranch": "main"
    }
  • Error (400/404): { success: false, files: [], baseBranch: "", error: "..." }

GET /api/worktrees/:id/diff/file

Get original and modified content for a single file (for Monaco DiffEditor).

  • Query params:
    • path (string, required) — file path relative to worktree root
    • status (string, default "modified") — file status (modified, added, deleted, renamed, untracked)
    • includeCommitted (boolean, default false) — use origin/<baseBranch> as ref instead of HEAD
    • oldPath (string, optional) — original path for renamed files
  • Response (200):
    {
      "success": true,
      "oldContent": "// original file content...",
      "newContent": "// modified file content..."
    }
  • Error (400/404): { success: false, oldContent: "", newContent: "", error: "..." }

Agent CLI

Agent CLI availability and installation endpoints for manual "Code with ..." launches.

agent path param must be one of: "claude", "codex", "gemini", or "opencode".

GET /api/agents/:agent/cli/status

Check whether the selected CLI command is available in PATH.

  • Response:
    {
      "success": true,
      "agent": "codex",
      "label": "Codex",
      "command": "codex",
      "installed": false,
      "brewPackage": "codex"
    }
  • Error (404): { success: false, error: "Unknown agent" }

POST /api/agents/:agent/cli/install

Install the selected CLI via Homebrew.

  • Attempts brew install <package> (some agents include fallback formulas).
  • Response:
    {
      "success": true,
      "agent": "claude",
      "label": "Claude",
      "command": "claude",
      "brewPackage": "claude"
    }
  • Error (400): { success: false, error: "..." } (for example Homebrew missing or install failure)
  • Error (404): { success: false, error: "Unknown agent" }

Jira Integration

Jira Cloud API integration with API token or OAuth authentication.

GET /api/jira/status

Get Jira integration status and configuration.

  • Response:
    {
      "configured": true,
      "defaultProjectKey": "PROJ",
      "refreshIntervalMinutes": 5,
      "email": "user@example.com",
      "domain": "yoursite.atlassian.net",
      "dataLifecycle": { ... },
      "autoStartAgent": "claude",
      "autoStartClaudeOnNewIssue": false,
      "autoStartClaudeSkipPermissions": true,
      "autoStartClaudeFocusTerminal": true,
      "autoUpdateIssueStatusOnAgentStart": false,
      "autoUpdateIssueStatusName": null
    }

POST /api/jira/setup

Configure Jira with API token credentials. Validates the connection before saving.

  • Request:
    {
      "baseUrl": "https://yoursite.atlassian.net",
      "email": "user@example.com",
      "token": "your-api-token"
    }
  • Response: { success: true }
  • Error (400): { success: false, error: "Connection failed: ..." }

PATCH /api/jira/config

Update Jira project configuration, including optional auto-start agent behavior for newly fetched issues.

  • Request:
    {
      "defaultProjectKey": "PROJ",
      "refreshIntervalMinutes": 10,
      "dataLifecycle": { ... },
      "autoStartAgent": "codex",
      "autoStartClaudeOnNewIssue": true,
      "autoStartClaudeSkipPermissions": true,
      "autoStartClaudeFocusTerminal": true,
      "autoUpdateIssueStatusOnAgentStart": true,
      "autoUpdateIssueStatusName": "In Progress"
    }
  • Response: { success: true }

GET /api/jira/status-options

List available Jira status names from the configured default project (defaultProjectKey).

  • Response: { options: [{ name }] }
  • Error (400): { options: [], error: "defaultProjectKey is not configured" }

GET /api/jira/issues/:key/status-options

Get transition statuses currently available for a specific Jira issue.

  • Response: { options: [{ id, name }] }

GET /api/jira/priorities

List available Jira priorities.

  • Response: { options: [{ id, name }] }

GET /api/jira/issues/:key/type-options

List available Jira issue types for a specific issue (from Jira edit metadata).

  • Response: { options: [{ id, name }] }

PATCH /api/jira/issues/:key/status

Transition a Jira issue to a new status.

  • Request:
    { "statusName": "In Progress" }
  • Response: { success: true }

PATCH /api/jira/issues/:key/priority

Update a Jira issue priority.

  • Request:
    { "priorityName": "High" }
  • Response: { success: true }

PATCH /api/jira/issues/:key/type

Update a Jira issue type.

  • Request:
    { "typeName": "Bug" }
  • Response: { success: true }

PATCH /api/jira/issues/:key/description

Update a Jira issue description.

  • Request:
    { "description": "Updated markdown/plain text description" }
  • Response: { success: true }

PATCH /api/jira/issues/:key/summary

Update a Jira issue summary (title).

  • Request:
    { "summary": "Updated issue summary" }
  • Response: { success: true }

POST /api/jira/issues/:key/comments

Add a comment to a Jira issue.

  • Request:
    { "comment": "Working on this now." }
  • Response: { success: true }

PATCH /api/jira/issues/:key/comments/:commentId

Update an existing Jira comment.

  • Request:
    { "comment": "Updated comment body" }
  • Response: { success: true }

DELETE /api/jira/issues/:key/comments/:commentId

Delete a Jira comment.

  • Response: { success: true }

DELETE /api/jira/credentials

Disconnect Jira by removing stored credentials.

  • Response: { success: true }

GET /api/jira/issues

List Jira issues assigned to the current user.

  • Query params: ?query=search+text (optional, filters by text match)
  • Response: { issues: [{ key, summary, status, priority, type, assignee, updated, labels, url }] }
  • Error (400/502): { issues: [], error: "..." }

Also performs background auto-cleanup of cached issue data if data lifecycle rules are configured.

GET /api/jira/issues/:key

Get detailed information for a specific Jira issue. Includes description (rendered from ADF), comments, attachments, and metadata.

  • Response: { issue: { ... } }
  • Error (400/404/500): { error: "..." }

Persists issue data to disk by default (controlled by dataLifecycle.saveOn setting). Downloads attachments in the background.

GET /api/jira/attachment

Proxy a Jira attachment URL through the server (handles authentication).

  • Query params: ?url=https://yoursite.atlassian.net/rest/api/3/attachment/content/...
  • Response: The raw attachment file with appropriate Content-Type header
  • Error: { error: "..." }

POST /api/jira/task

Create a worktree from a Jira issue. Fetches the issue, generates a branch name, and creates the worktree.

  • Request:
    {
      "issueKey": "PROJ-123",
      "branch": "optional-custom-branch"
    }
  • Response (201, created):
    { "success": true, "worktreeId": "PROJ-123", "worktreePath": "...", "reusedExisting": false }
  • Response (200, reused existing):
    { "success": true, "worktreeId": "proj-123", "worktreePath": "...", "reusedExisting": true }
  • Error (400): { success: false, error: "..." }

If a matching worktree already exists (including case-insensitive matches), the endpoint reuses it by default, links the Jira issue to that canonical worktree ID, and returns success: true.


Linear Integration

Linear issue tracker integration via API key.

GET /api/linear/status

Get Linear integration status and configuration.

  • Response:
    {
      "configured": true,
      "defaultTeamKey": "TEAM",
      "refreshIntervalMinutes": 5,
      "displayName": "User Name",
      "dataLifecycle": { ... },
      "autoStartAgent": "claude",
      "autoStartClaudeOnNewIssue": false,
      "autoStartClaudeSkipPermissions": true,
      "autoStartClaudeFocusTerminal": true,
      "autoUpdateIssueStatusOnAgentStart": false,
      "autoUpdateIssueStatusName": null
    }

POST /api/linear/setup

Configure Linear with an API key. Validates the connection before saving.

  • Request:
    { "apiKey": "lin_api_..." }
  • Response: { success: true }
  • Error (400): { success: false, error: "Connection failed: ..." }

PATCH /api/linear/config

Update Linear project configuration, including optional auto-start agent behavior for newly fetched issues.

  • Request:
    {
      "defaultTeamKey": "TEAM",
      "refreshIntervalMinutes": 10,
      "dataLifecycle": { ... },
      "autoStartAgent": "gemini",
      "autoStartClaudeOnNewIssue": true,
      "autoStartClaudeSkipPermissions": true,
      "autoStartClaudeFocusTerminal": true,
      "autoUpdateIssueStatusOnAgentStart": true,
      "autoUpdateIssueStatusName": "In Progress"
    }
  • Response: { success: true }

GET /api/linear/status-options

List available Linear workflow statuses (filtered by configured default team when present).

  • Response: { options: [{ name, type, color }] }

GET /api/linear/issues/:identifier/status-options

List available Linear workflow statuses for the issue's actual team/project.

  • Response: { options: [{ name, type, color }] }

GET /api/linear/priority-options

List available Linear priorities from Linear API metadata.

  • Response: { options: [{ value, label }] }

PATCH /api/linear/issues/:identifier/status

Update a Linear issue status using options from the issue's actual team/project.

  • Request:
    { "statusName": "In Progress" }
  • Response: { success: true }

PATCH /api/linear/issues/:identifier/description

Update a Linear issue description.

  • Request:
    { "description": "Updated markdown description" }
  • Response: { success: true }

PATCH /api/linear/issues/:identifier/title

Update a Linear issue title.

  • Request:
    { "title": "Updated issue title" }
  • Response: { success: true }

POST /api/linear/issues/:identifier/comments

Add a comment to a Linear issue.

  • Request:
    { "comment": "Taking this issue." }
  • Response: { success: true }

PATCH /api/linear/issues/:identifier/comments/:commentId

Update an existing Linear comment.

  • Request:
    { "comment": "Updated comment body" }
  • Response: { success: true }

DELETE /api/linear/issues/:identifier/comments/:commentId

Delete a Linear comment.

  • Response: { success: true }

DELETE /api/linear/credentials

Disconnect Linear by removing stored credentials.

  • Response: { success: true }

GET /api/linear/issues

List Linear issues assigned to the current user.

  • Query params: ?query=search+text (optional)
  • Response: { issues: [{ identifier, title, state, priority, priorityLabel, assignee, labels, url, ... }] }
  • Error (400/500): { issues: [], error: "..." }

GET /api/linear/issues/:identifier

Get detailed information for a specific Linear issue.

  • Response: { issue: { ... } }
  • Error (400/404/500): { error: "..." }

GET /api/linear/attachment

Proxy a Linear attachment through the OpenKit server using the configured Linear API key. Use this URL for image previews/downloads in the UI to avoid auth/CORS failures.

  • Query params: ?url=https://... (required)
  • Response: Raw attachment bytes with Content-Type + Content-Disposition
  • Error (400/500): { error: "..." }

POST /api/linear/task

Create a worktree from a Linear issue.

  • Request:
    {
      "identifier": "TEAM-123",
      "branch": "optional-custom-branch"
    }
  • Response (201, created):
    { "success": true, "worktreeId": "TEAM-123", "worktreePath": "...", "reusedExisting": false }
  • Response (200, reused existing):
    { "success": true, "worktreeId": "team-123", "worktreePath": "...", "reusedExisting": true }
  • Error (400): { success: false, error: "..." }

If a matching worktree already exists (including case-insensitive matches), the endpoint reuses it by default, links the Linear issue to that canonical worktree ID, and returns success: true.


Activity Feed

Query and stream activity events (agent actions, worktree lifecycle, git operations, etc.).

GET /api/activity

Query persisted activity events with optional filters.

  • Query params:
    • ?since=<iso> -- Only events after this ISO 8601 timestamp
    • ?category=<cat> -- Filter by category (agent, worktree, system)
    • ?limit=<n> -- Max number of events to return (default: 100)
  • Response: { events: ActivityEvent[] }

Each ActivityEvent includes: id, timestamp, category, type, severity (info | success | warning | error), title, detail?, worktreeId?, projectName?, metadata?.

Events are persisted to .openkit/activity.jsonl (JSONL format) and pruned based on the activity.retentionDays config setting.

POST /api/activity

Create a new activity event (used by the UI for app-level notifications such as Jira/Linear task detection and agent auto-start updates).

  • Request:
    {
      "category": "agent",
      "type": "auto_task_claimed",
      "severity": "info",
      "title": "Codex started working on PROJ-123",
      "detail": "Issue summary text",
      "worktreeId": "PROJ-123",
      "groupKey": "auto-task-claimed:PROJ-123",
      "metadata": {
        "source": "jira",
        "issueId": "PROJ-123",
        "issueTitle": "Issue summary text",
        "autoClaimed": true,
        "agent": "codex"
      }
    }
  • severity, detail, worktreeId, groupKey, and metadata are optional.
  • Response: { success: true, event: ActivityEvent }
  • Error (400): { success: false, error: "..." }

Operational Logs

Query and append structured operational log events (command executions, inbound/outbound request traces, internal task/terminal/worktree lifecycle operations, notification emissions, and UI error-toast reports).

GET /api/logs

Query persisted ops-log events with optional filters.

  • Query params:
    • ?since=<iso> -- Only events after this ISO 8601 timestamp
    • ?level=<level> -- Filter by level (debug, info, warning, error)
    • ?status=<status> -- Filter by status (started, succeeded, failed, info)
    • ?source=<text> -- Filter by source substring
    • ?search=<text> -- Full-text match across message/source/action/command fields
    • ?limit=<n> -- Max number of events to return (default: 200)
  • Response: { events: OpsLogEvent[] }

Each OpsLogEvent includes:

  • id, timestamp
  • source, action, message
  • level (debug | info | warning | error)
  • status (started | succeeded | failed | info)
  • optional runId, worktreeId, projectName, metadata
  • optional command payload (command, args, cwd, pid, exitCode, signal, durationMs, stdout, stderr)

For source: "http" events, metadata also includes request/response trace fields when available:

  • request: method, path, optional url, optional direction (inbound/outbound), requestContentType, requestPayload, requestPayloadTruncated, requestPayloadOmitted
  • response: statusCode, durationMs, responseContentType, responsePayload, responsePayloadTruncated, responsePayloadOmitted

Events are persisted to .openkit/ops-log.jsonl (JSONL format) and pruned automatically (default retention: 7 days).

POST /api/logs

Append a custom operational log event (used by UI error-toast reporting and other client-side telemetry).

  • Request:
    {
      "source": "ui.toast",
      "action": "toast.error",
      "message": "Failed to fetch Jira status",
      "level": "error",
      "status": "failed",
      "metadata": {
        "scope": "jira-status:fetch"
      }
    }
  • level, status, worktreeId, and metadata are optional.
  • Response: { success: true, event: OpsLogEvent }
  • Error (400): { success: false, error: "..." }

Events (SSE)

Server-Sent Events stream for real-time worktree, activity, and operational-log updates.

GET /api/events

Opens an SSE connection. Immediately sends the current worktree state plus recent activity and ops-log history, then pushes updates as they occur.

  • Headers: Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive
  • Event format:
    data: {"type":"worktrees","worktrees":[...]}
    data: {"type":"activity-history","events":[...]}
    data: {"type":"activity","event":{...}}
    data: {"type":"ops-log-history","events":[...]}
    data: {"type":"ops-log","event":{...}}
    
  • Event types:
    • worktrees -- Full worktree list on every state change (status transitions, log updates, port changes, etc.)
    • activity-history -- Sent once on connection, contains the last 50 activity events
    • activity -- Individual activity events as they occur in real-time
    • ops-log-history -- Sent once on connection, contains recent operational log events (default 200)
    • ops-log -- Individual operational log events as they occur in real-time

The connection stays open until the client disconnects.


Performance Monitoring

Real-time CPU and memory metrics for the OpenKit server, worktree dev servers, child processes, and agent sessions. Monitoring is on-demand — it starts when a client subscribes to the SSE stream and stops when the last client disconnects.

GET /api/perf

Returns the full ring buffer of performance snapshots (up to 150 entries, covering ~5 minutes of data at 2-second intervals).

  • Response: { snapshots: PerfSnapshot[] }

GET /api/perf/current

Returns the most recent performance snapshot.

  • Response: { snapshot: PerfSnapshot | null }

GET /api/perf/stream

Opens an SSE connection for live performance data. Sends the full history on connect, then streams new snapshots every 2 seconds.

  • Headers: Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive
  • Event types:
    • perf-history — Full snapshot buffer, sent once on connection: { type: "perf-history", snapshots: PerfSnapshot[] }
    • perf-snapshot — Individual snapshot on each poll interval: { type: "perf-snapshot", snapshot: PerfSnapshot }

PerfSnapshot shape:

Field Type Description
timestamp string ISO 8601 timestamp
server ProcessMetrics OpenKit server process CPU/memory
system { totalCpu, totalMemory, processCount } Aggregate across all tracked processes
worktrees WorktreeMetrics[] Per-worktree breakdown

Each WorktreeMetrics contains devServer, childProcesses, agentSessions, and aggregate totalCpu/totalMemory.


MCP Servers Registry

Manage a centralized registry of MCP servers stored at ~/.openkit/mcp-servers.json. Deploy them to any agent's configuration.

GET /api/mcp-servers

List all registered MCP servers.

  • Query params:
    • ?q=search -- Filter by name, ID, description, command, or URL
    • ?tag=tagname -- Filter by tag
  • Response: { servers: McpServerDefinition[] }

Each server includes: id, name, description, tags, optional command/args, optional type/url, env, source, createdAt, updatedAt.

GET /api/mcp-servers/deployment-status

Get deployment status of all registry servers (plus built-in OpenKit) across all agents and scopes.

  • Response:
    {
      "status": {
        "server-id": {
          "claude": { "global": true, "project": false, "globalPath": "...", "projectPath": "..." },
          ...
        }
      }
    }

GET /api/mcp-servers/:id

Get a single MCP server definition.

  • Response: { server: McpServerDefinition }
  • Error (404): { error: "Server not found" }

POST /api/mcp-servers

Register a new MCP server.

  • Request:
    {
      "id": "optional-slug",
      "name": "My MCP Server",
      "description": "What it does",
      "tags": ["search", "web"],
      "command": "npx",
      "args": ["-y", "my-mcp-server"],
      "env": { "API_KEY": "..." }
    }
    name is required and either command or url must be provided. id is auto-generated from name if omitted. URL-based server example:
    {
      "name": "remote-mcp",
      "type": "http",
      "url": "https://mcp.example.com"
    }
  • Response: { success: true, server: McpServerDefinition }
  • Error (409): { success: false, error: "Server \"id\" already exists" }

PATCH /api/mcp-servers/:id

Update an existing MCP server definition.

  • Request: Partial server fields (name, description, tags, command, args, type, url, env)
  • Response: { success: true, server: McpServerDefinition }
  • Error (404): { success: false, error: "Server not found" }

DELETE /api/mcp-servers/:id

Remove a server from the registry.

  • Response: { success: true }
  • Error (404): { success: false, error: "Server not found" }

POST /api/mcp-servers/:id/deploy

Deploy a registry server to an agent's configuration file.

  • Request:
    {
      "tool": "claude",
      "scope": "global"
    }
  • Response: { success: true }

Merges global env with per-project env overrides before writing.

POST /api/mcp-servers/:id/undeploy

Remove a server from an agent's configuration file.

  • Request:
    {
      "tool": "claude",
      "scope": "project"
    }
  • Response: { success: true }

POST /api/mcp-servers/scan

Scan the filesystem for MCP server definitions in agent config files.

  • Request:
    {
      "mode": "project",
      "scanPath": "/optional/path"
    }
    mode: "project" (default), "folder", or "device".
  • Response:
    {
      "discovered": [
        {
          "key": "server-name",
          "command": "npx",
          "args": ["..."],
          "type": "http",
          "url": "https://mcp.example.com",
          "env": {},
          "foundIn": [{ "configPath": "..." }],
          "alreadyInRegistry": false
        }
      ]
    }

POST /api/mcp-servers/import

Bulk import discovered servers into the registry.

  • Request:
    {
      "servers": [
        {
          "key": "server-name",
          "name": "Display Name",
          "command": "npx",
          "args": ["..."],
          "type": "http",
          "url": "https://mcp.example.com",
          "env": {},
          "source": "/path/to/config"
        }
      ]
    }
  • Response: { success: true, imported: ["server-name", ...] }

GET /api/mcp-env/:serverId

Get per-project environment variable overrides for an MCP server.

  • Response: { env: Record<string, string> }

PUT /api/mcp-env/:serverId

Set per-project environment variable overrides for an MCP server.

  • Request:
    { "env": { "API_KEY": "project-specific-value" } }
    Send an empty object to clear overrides.
  • Response: { success: true, env: Record<string, string> }

Skills

Manage a centralized skills registry stored at ~/.openkit/skills/. Skills are directories containing a SKILL.md file with frontmatter metadata and instructions.

GET /api/skills

List all skills in the registry.

  • Response: { skills: [{ name, displayName, description, path }] }

GET /api/skills/deployment-status

Get per-agent deployment status for all registry skills.

  • Response:
    {
      "status": {
        "skill-name": {
          "inRegistry": true,
          "agents": {
            "claude": { "global": false, "project": true },
            ...
          }
        }
      }
    }

GET /api/skills/:name

Get detailed information about a skill, including its SKILL.md content, frontmatter, and optional reference.md / examples.md.

  • Response:
    {
      "skill": {
        "name": "my-skill",
        "displayName": "My Skill",
        "description": "...",
        "path": "/Users/.../.openkit/skills/my-skill",
        "skillMd": "---\nname: ...\n---\n...",
        "frontmatter": { "name": "...", "description": "...", ... },
        "hasReference": true,
        "referenceMd": "...",
        "hasExamples": false
      }
    }
  • Error (404): { error: "Skill not found" }

POST /api/skills

Create a new skill in the registry.

  • Request:
    {
      "name": "My Skill",
      "description": "What it does",
      "allowedTools": "Bash, Read, Write",
      "context": "file://reference.md",
      "agent": "",
      "model": "",
      "argumentHint": "",
      "disableModelInvocation": false,
      "userInvocable": true,
      "mode": false,
      "instructions": "Markdown body of SKILL.md"
    }
    Only name is required.
  • Response: { success: true, skill: { name, displayName, description, path } }
  • Error (409): { success: false, error: "Skill \"name\" already exists" }

PATCH /api/skills/:name

Update a skill's content.

  • Request:
    {
      "skillMd": "Full SKILL.md content",
      "referenceMd": "Reference content",
      "examplesMd": "Examples content",
      "frontmatter": { "description": "updated" }
    }
    All fields are optional. If frontmatter is provided without skillMd, it merges into the existing SKILL.md. Send empty string for referenceMd/examplesMd to delete those files.
  • Response: { success: true }

DELETE /api/skills/:name

Delete a skill from the registry. Also removes symlinks across all agent deploy directories.

  • Response: { success: true }
  • Error (404): { success: false, error: "Skill not found" }

POST /api/skills/:name/deploy

Deploy a skill to an agent's skills directory (creates a symlink).

  • Request:
    {
      "agent": "claude",
      "scope": "project"
    }
  • Response: { success: true }

POST /api/skills/:name/undeploy

Remove a skill deployment from an agent's skills directory.

  • Request:
    {
      "agent": "claude",
      "scope": "global"
    }
  • Response: { success: true }

POST /api/skills/scan

Scan the filesystem for skill directories.

  • Request:
    {
      "mode": "project",
      "scanPath": "/optional/path"
    }
    mode: "project" (default), "folder", or "device".
  • Response: { discovered: [{ name, displayName, description, skillPath, alreadyInRegistry }] }

POST /api/skills/import

Import discovered skills into the registry (copies files).

  • Request:
    {
      "skills": [{ "name": "skill-name", "skillPath": "/path/to/skill" }]
    }
  • Response: { success: true, imported: ["skill-name", ...] }

POST /api/skills/install

Install a skill from GitHub via npx skills add.

  • Request:
    {
      "repo": "owner/repo",
      "skill": "optional-skill-name",
      "agents": ["claude", "cursor"],
      "scope": "global"
    }
  • Response: { success: true, installed: ["skill-name"] }

GET /api/skills/npx-available

Check if npx skills CLI is available on the system.

  • Response: { available: boolean }

Claude Plugins

Manage Claude Code plugins via the claude CLI. Falls back to reading settings files if the CLI is unavailable.

GET /api/claude/plugins

List installed Claude plugins with component counts and health status.

  • Response:
    {
      "plugins": [
        {
          "id": "plugin-id",
          "name": "Plugin Name",
          "description": "...",
          "version": "1.0.0",
          "scope": "user",
          "enabled": true,
          "marketplace": "...",
          "author": "...",
          "error": null,
          "warning": "Needs authentication",
          "componentCounts": {
            "commands": 2,
            "agents": 1,
            "skills": 3,
            "mcpServers": 1,
            "hooks": false,
            "lsp": false
          }
        }
      ],
      "cliAvailable": true
    }

Plugin health is probed by testing MCP server connectivity, checking command availability, and validating env vars.

GET /api/claude/plugins/debug

Get raw claude plugin list --json output for debugging.

  • Response: { success: boolean, raw: string, parsed: any, stderr?: string }

GET /api/claude/plugins/available

List available plugins from configured marketplaces.

  • Response:
    {
      "available": [
        {
          "pluginId": "...",
          "name": "...",
          "description": "...",
          "marketplaceName": "...",
          "version": "...",
          "installed": false
        }
      ]
    }

GET /api/claude/plugins/:id

Get detailed information about a specific plugin, including manifest, README, components list, and health check.

  • Response:
    {
      "plugin": {
        "id": "...", "name": "...", "description": "...",
        "version": "...", "scope": "...", "enabled": true,
        "installPath": "/path/to/plugin",
        "manifest": { ... },
        "components": {
          "commands": ["cmd1"], "agents": ["agent1"],
          "skills": ["skill1"], "mcpServers": ["server1"],
          "hasHooks": false, "hasLsp": false
        },
        "readme": "# Plugin README\n...",
        "homepage": "...", "repository": "...",
        "license": "MIT", "keywords": [...]
      }
    }
  • Error (404): { error: "Plugin not found" }
  • Error (501): { error: "Claude CLI not available", cliAvailable: false }

GET /api/claude/agents

List agent definitions discovered from installed Claude plugins (agents/*.md).

  • Response:
    {
      "agents": [
        {
          "id": "plugin-id::agent-name",
          "name": "agent-name",
          "description": "...",
          "pluginId": "plugin-id",
          "pluginName": "Plugin Name",
          "pluginScope": "user",
          "pluginEnabled": true,
          "marketplace": "...",
          "deployments": {
            "claude": { "global": true, "project": false },
            "cursor": { "global": false, "project": true },
            "gemini": { "global": false, "project": false },
            "vscode": { "global": false, "project": false },
            "codex": { "global": false, "project": false }
          }
        }
      ],
      "cliAvailable": true
    }
  • Error (500): { agents: [], cliAvailable: true, error: "..." }

GET /api/claude/agents/detail?id=:id (also /api/claude/agents/:id)

Get full agent definition content for a discovered plugin agent.

  • Response:
    {
      "agent": {
        "id": "plugin-id::agent-name",
        "name": "agent-name",
        "description": "...",
        "pluginId": "plugin-id",
        "pluginName": "Plugin Name",
        "pluginScope": "user",
        "pluginEnabled": true,
        "marketplace": "...",
        "installPath": "/path/to/plugin",
        "agentPath": "/path/to/plugin/agents/agent-name.md",
        "deployments": {
          "claude": { "global": true, "project": false },
          "cursor": { "global": false, "project": false }
        },
        "content": "# agent markdown..."
      }
    }
  • Error (400): { error: "Invalid agent id" }
  • Error (404): { error: "Plugin not found" } or { error: "Agent definition not found" }
  • Error (501): { error: "Claude CLI not available", cliAvailable: false }

POST /api/claude/agents/deploy

Deploy a plugin-provided agent to a target tool and scope.

  • Request:
    { "id": "plugin-id::agent-name", "agent": "cursor", "scope": "project" }
  • Behavior:
    • For non-Claude targets, copies the plugin agents/<name>.md into that target's agent config directory.
    • For Claude target, enables the underlying plugin in its valid scope.
  • Response: { "success": true }

POST /api/claude/agents/undeploy

Remove plugin-provided agent deployment from a target tool and scope.

  • Request:
    { "id": "plugin-id::agent-name", "agent": "cursor", "scope": "project" }
  • Behavior:
    • For non-Claude targets, removes the deployed markdown file.
    • For Claude target, disables the underlying plugin in its valid scope.
  • Response: { "success": true }

GET /api/claude/custom-agents

List custom agents from the OpenKit registry (~/.openkit/agents/*.md) with deployment status across all supported tools (Claude, Cursor, Gemini CLI, VS Code, Codex).

  • Response:
    {
      "agents": [
        {
          "id": "custom::reviewer",
          "name": "reviewer",
          "description": "...",
          "pluginId": "custom",
          "pluginName": "Custom",
          "pluginScope": "local",
          "pluginEnabled": true,
          "marketplace": "local",
          "isCustom": true,
          "deployments": {
            "claude": { "global": true, "project": false },
            "cursor": { "global": false, "project": false }
          }
        }
      ]
    }

GET /api/claude/custom-agents/:id

Get full custom agent markdown content.

  • Response:
    {
      "agent": {
        "id": "custom::reviewer",
        "name": "reviewer",
        "description": "...",
        "isCustom": true,
        "agentPath": "/abs/path/.openkit/agents/reviewer.md",
        "installPath": "/abs/path/.openkit/agents",
        "deployments": {
          "claude": { "global": false, "project": true },
          "codex": { "global": false, "project": true }
        },
        "content": "---\nname: reviewer\n---\n..."
      }
    }
  • Error (400): { error: "Invalid custom agent id" }
  • Error (404): { error: "Custom agent not found" }

POST /api/claude/custom-agents

Create a custom agent markdown file.

  • Request:
    {
      "name": "reviewer",
      "description": "Code review specialist",
      "tools": "Read, Grep, Glob",
      "model": "optional model hint",
      "instructions": "# reviewer\n...",
      "scope": "project",
      "deployAgents": ["claude", "codex", "cursor", "gemini", "vscode"]
    }
  • Response: { success: true, agent: { ... } }

DELETE /api/claude/custom-agents/:id

Delete a custom agent markdown file.

  • Response: { success: true }

PATCH /api/claude/custom-agents/:id

Update custom agent markdown content in the registry (and sync deployed copies).

  • Request:
    { "content": "---\nname: reviewer\n---\n# reviewer\n..." }
  • Response: { success: true, agent: { ... } }

POST /api/claude/custom-agents/:id/deploy

Deploy a custom agent to one tool + scope.

  • Request:
    { "agent": "codex", "scope": "project" }
  • Response: { success: true }

POST /api/claude/custom-agents/:id/undeploy

Undeploy a custom agent from one tool + scope.

  • Request:
    { "agent": "codex", "scope": "project" }
  • Response: { success: true }

POST /api/claude/custom-agents/scan

Scan filesystem for existing custom agent markdown files.

  • Request: { mode: "project" | "folder" | "device", scanPath?: string }
  • Response:
    {
      "discovered": [
        {
          "name": "reviewer",
          "description": "...",
          "agentPath": "/abs/path/.codex/agents/reviewer.md",
          "alreadyInRegistry": false
        }
      ]
    }

POST /api/claude/custom-agents/import

Import scanned agent markdown files into the registry and optionally deploy to selected tools.

  • Request:
    {
      "scope": "project",
      "deployAgents": ["claude", "codex", "cursor", "gemini", "vscode"],
      "agents": [{ "name": "reviewer", "agentPath": "/some/path/reviewer.md" }]
    }
  • Response: { success: true, imported: ["reviewer"] }

POST /api/claude/plugins/install

Install a plugin from a marketplace.

  • Request:
    {
      "ref": "plugin-reference",
      "scope": "user"
    }
  • Response: { success: true }

POST /api/claude/plugins/:id/uninstall

Uninstall a plugin.

  • Request (optional): { "scope": "user" }
  • Response: { success: true }

POST /api/claude/plugins/:id/enable

Enable a disabled plugin.

  • Request (optional): { "scope": "user" }
  • Response: { success: true }

POST /api/claude/plugins/:id/disable

Disable a plugin.

  • Request (optional): { "scope": "user" }
  • Response: { success: true }

POST /api/claude/plugins/:id/update

Update a plugin to the latest version.

  • Response: { success: true }

GET /api/claude/plugins/marketplaces

List configured plugin marketplaces.

  • Response: { marketplaces: [{ name, source, repo }] }

POST /api/claude/plugins/marketplaces

Add a new marketplace source.

  • Request:
    { "source": "https://marketplace.example.com" }
  • Response: { success: true }

DELETE /api/claude/plugins/marketplaces/:name

Remove a marketplace.

  • Response: { success: true }

POST /api/claude/plugins/marketplaces/:name/update

Update a marketplace's plugin index.

  • Response: { success: true }

Notes

Per-issue notes with personal notes, AI context, and todo lists. Notes are scoped by issue source (jira, linear, or local) and issue ID.

GET /api/notes/:source/:id

Get notes for an issue.

  • URL params: source = jira | linear | local, id = issue key/identifier
  • Response: Full notes object with personal, aiContext, todos, linkedWorktreeId, gitPolicy

PUT /api/notes/:source/:id

Update a notes section.

  • Request:
    {
      "section": "personal",
      "content": "My notes about this issue"
    }
    section must be "personal" or "aiContext".
  • Response: Updated notes object

When updating aiContext and the issue has a linked worktree, the updated context is immediately available via openkit task context.

POST /api/notes/:source/:id/todos

Add a todo item.

  • Request:
    { "text": "Implement the feature" }
  • Response: Updated notes object

PATCH /api/notes/:source/:id/todos/:todoId

Update a todo item.

  • Request:
    {
      "text": "Updated text",
      "checked": true
    }
  • Response: Updated notes object

DELETE /api/notes/:source/:id/todos/:todoId

Delete a todo item.

  • Response: Updated notes object

PATCH /api/notes/:source/:id/git-policy

Update the git policy for an issue (controls agent commit/push/PR permissions).

  • Request:
    {
      "agentCommits": "allow",
      "agentPushes": "deny",
      "agentPRs": "inherit"
    }
    Valid values: "inherit", "allow", "deny".
  • Response: Updated notes object

Local Tasks

CRUD operations for local (non-integrated) tasks stored at .openkit/issues/local/. Tasks use auto-incrementing identifiers (e.g., LOCAL-1, LOCAL-2).

GET /api/tasks

List all local tasks, enriched with linked worktree info and attachment counts.

  • Response: { tasks: [{ id, identifier, title, description, status, priority, labels, linkedWorktreeId, attachmentCount, ... }] }

GET /api/tasks/:id

Get a single task with full details and attachments.

  • Response: { task: { ..., linkedWorktreeId, attachments: [{ filename, mimeType, size, localPath, createdAt }] } }
  • Error (404): { error: "Task not found" }

POST /api/tasks

Create a new local task.

  • Request:
    {
      "title": "My task",
      "description": "Detailed description",
      "priority": "high",
      "labels": ["frontend", "urgent"]
    }
    Only title is required. priority defaults to "medium".
  • Response: { success: true, task: { ... } }

IDs remain monotonic (LOCAL-1, LOCAL-2, ...). Missing IDs are not automatically reused.

POST /api/tasks/recover-local

Recover missing local task metadata for an existing local-pattern worktree while preserving monotonic ID allocation.

  • Request:
    {
      "taskId": "LOCAL-1",
      "title": "Recovered task LOCAL-1",
      "description": "Optional details",
      "priority": "medium",
      "labels": ["bugfix"]
    }
  • Response:
    {
      "success": true,
      "task": {
        "id": "LOCAL-1",
        "title": "Recovered task LOCAL-1",
        "status": "todo"
      },
      "linkedWorktreeId": "LOCAL-1"
    }
  • Errors:
    • 400 invalid taskId format
    • 404 canonical worktree not found
    • 409 task metadata already exists

On success, this recreates task metadata and notes linked to the canonical worktree ID and bumps .openkit/issues/local/.counter to at least the recovered numeric suffix.

PATCH /api/tasks/:id

Update a task.

  • Request: Partial fields: title, description, status ("todo" | "in-progress" | "done"), priority ("high" | "medium" | "low"), labels
  • Response: { success: true, task: { ... } }
  • Error (404): { success: false, error: "Task not found" }

DELETE /api/tasks/:id

Delete a task and all its data.

  • Response: { success: true }
  • Error (404): { success: false, error: "Task not found" }

POST /api/tasks/:id/create-worktree

Create a worktree from a local task. Generates a branch name and links the task to the worktree. Task context is retrievable via openkit task context.

  • Request (optional):
    { "branch": "custom-branch-name" }
  • Response (created): { success: true, worktree: WorktreeInfo, reusedExisting: false }
  • Response (reused existing):
    { "success": true, "worktreeId": "local-1", "worktreePath": "...", "reusedExisting": true }

If an existing worktree matches the task ID (case-insensitive), this endpoint reuses and links it instead of failing with WORKTREE_EXISTS.

POST /api/tasks/:id/attachments

Upload a file attachment to a task. Uses multipart/form-data.

  • Request: Form data with file field
  • Response:
    {
      "success": true,
      "attachment": {
        "filename": "image.png",
        "mimeType": "image/png",
        "size": 12345,
        "localPath": "..."
      }
    }

Automatically deduplicates filenames (appends _1, _2, etc.).

GET /api/tasks/:id/attachments/:filename

Serve an attachment file.

  • Response: Raw file content with appropriate Content-Type header and 1-hour cache

DELETE /api/tasks/:id/attachments/:filename

Delete an attachment.

  • Response: { success: true }
  • Error (404): { success: false, error: "Attachment not found" }

Terminal

WebSocket-based PTY terminal sessions for worktrees.

POST /api/worktrees/:id/terminals

Create a new terminal session for a worktree.

  • Request (optional):
    {
      "cols": 120,
      "rows": 40,
      "startupCommand": "exec claude 'Run openkit task context to get task details, then implement'",
      "scope": "claude"
    }
    Defaults to 80 columns and 24 rows. startupCommand (optional) runs via shell startup ($SHELL -lc <command>). scope (optional: "terminal", "claude", "codex", "gemini", or "opencode") reuses a single session per worktree+scope when present.
  • Response:
    {
      "success": true,
      "sessionId": "string",
      "reusedScopedSession": false,
      "replacedScopedShellSession": true
    }
    reusedScopedSession and replacedScopedShellSession are additive reconcile flags for scoped sessions:
    • reusedScopedSession = true when an existing scoped session was reused.
    • replacedScopedShellSession = true when a shell-only scoped session was replaced by an explicit startup-command launch.
  • Error (404): { success: false, error: "Worktree not found" }
  • Error (409): { success: false, error: "Worktree \"...\" is ambiguous. Matches: ..." }

DELETE /api/terminals/:sessionId

Destroy a terminal session.

  • Response: { success: true }
  • Error (404): { success: false, error: "Session not found" }

GET /api/worktrees/:id/terminals/active?scope=terminal|claude|codex|gemini|opencode

Look up the currently active scoped terminal session for a worktree (used to reattach after refresh).

  • Response: { success: true, sessionId: "string" | null }
  • Error (400): { success: false, error: "scope is required (\"terminal\", \"claude\", \"codex\", \"gemini\", or \"opencode\")" }
  • Error (404/409): canonical worktree resolution failure (not found or ambiguous)

GET /api/worktrees/:id/agents/:agent/restore

Resolve the best restore target for worktree-detail agent quick actions. This is currently supported for claude and codex only.

  • Response:
    {
      "success": true,
      "activeSessionId": "term-12",
      "historyMatches": [
        {
          "sessionId": "019cc1dd-3614-7431-974f-bc92b5953da8",
          "title": "continue task with latest description",
          "updatedAt": "2026-03-05T14:52:50.000Z",
          "preview": "continue task with latest description"
        }
      ]
    }
  • activeSessionId is the current live scoped OpenKit PTY session when present.
  • historyMatches are native Claude/Codex sessions matched by exact worktree path.
  • Error (400): { success: false, error: "agent must be \"claude\" or \"codex\"" }
  • Error (404/409): canonical worktree resolution failure (not found or ambiguous)

GET /api/terminals/:sessionId/ws (WebSocket)

WebSocket endpoint for bidirectional terminal communication. Upgrades the HTTP connection to a WebSocket.

Protocol:

  • Client to Server: Send raw terminal input as text/binary frames
  • Server to Client: Receive PTY output as text/binary frames
  • Server to Client control frames:
    • { "type": "restore", "payload": "..." } may be sent immediately after connect so the client can restore the current terminal screen plus bounded scrollback before live PTY output resumes
    • { "type": "exit", "exitCode": 0 } indicates the PTY process exited and the session was torn down
  • Connection: Automatically attaches to the PTY session on open; closes with code 1008 if session not found

Hooks

Automated checks and agent skills organized by trigger type. Supports shell command steps, prompt steps, and skill references from the registry across all trigger types.

GET /api/hooks/config

Get the hooks configuration.

  • Response: HooksConfig object with steps and skills arrays

PUT /api/hooks/config

Save the full hooks configuration.

  • Request: HooksConfig object
  • Response: { success: true, config: HooksConfig }

POST /api/hooks/steps

Add a hook step (command or prompt).

  • Request:
    { "name": "Type check", "command": "pnpm check:types", "kind": "command" }
    Prompt steps use: { "name": "Architecture review", "kind": "prompt", "prompt": "Review the implementation and list architectural risks." }
  • Response: { success: true, config: HooksConfig }

PATCH /api/hooks/steps/:stepId

Update a step.

  • Request: Partial fields: name, command, prompt, kind, enabled, trigger, condition
  • Response: { success: true, config: HooksConfig }

DELETE /api/hooks/steps/:stepId

Remove a step.

  • Response: { success: true, config: HooksConfig }

POST /api/hooks/skills/import

Import a skill from the registry into hooks.

  • Request:
    {
      "skillName": "review-changes",
      "trigger": "post-implementation",
      "condition": "optional"
    }
    The same skill can be imported into multiple trigger types. Deduplication is by skillName + trigger.
  • Response: { success: true, config: HooksConfig }

GET /api/hooks/skills/available

List available skills from the ~/.openkit/skills/ registry.

  • Response: { available: [{ name, displayName, description }] }

PATCH /api/hooks/skills/:name

Toggle a skill's enabled state.

  • Request:
    { "enabled": true, "trigger": "post-implementation" }
    trigger identifies which instance to toggle when the same skill exists in multiple trigger types.
  • Response: { success: true, config: HooksConfig }

DELETE /api/hooks/skills/:name

Remove a skill from hooks.

  • Query params: ?trigger=post-implementation (identifies which instance to remove)
  • Response: { success: true, config: HooksConfig }

POST /api/worktrees/:id/hooks/run

Run all enabled steps for a worktree.

  • Request (optional):
    { "trigger": "pre-implementation" }
    Trigger defaults to "post-implementation" when omitted. Accepted values: pre-implementation, post-implementation, custom, on-demand, worktree-created, worktree-removed.
  • Response: PipelineRun object with id, worktreeId, status, startedAt, steps

POST /api/worktrees/:id/hooks/run/:stepId

Run a single step for a worktree.

  • Response: StepResult object

GET /api/worktrees/:id/hooks/status

Get the latest hook run status for a worktree.

  • Response: { status: PipelineRun | null }

POST /api/worktrees/:id/hooks/report

Report a skill hook result from an agent.

  • Request:
    {
      "skillName": "review-changes",
      "trigger": "post-implementation",
      "success": true,
      "summary": "No critical issues found",
      "content": "Optional detailed markdown content"
    }
  • Response: { success: true }

Ngrok Connect (Experimental)

Remote access flow for exposing a local OpenKit server through ngrok with one-time QR pairing.

GET /api/ngrok/status

Return current ngrok project + tunnel state.

  • Response:
    {
      "success": true,
      "project": {
        "id": "project-id",
        "name": "project-name"
      },
      "tunnel": {
        "enabled": false,
        "status": "stopped",
        "publicUrl": null,
        "localPort": null,
        "startedAt": null,
        "error": null
      }
    }

GET /api/ngrok/tunnel/status

Alias of GET /api/ngrok/status.

POST /api/ngrok/tunnel/enable

Start (or re-use) ngrok for the current local OpenKit port.

  • Request:
    {
      "regenerateUrl": false
    }
  • Response: same shape as GET /api/ngrok/status
  • Error:
    • 500 ngrok_start_failed

POST /api/ngrok/tunnel/disable

Stop the active ngrok process.

  • Response: same shape as GET /api/ngrok/status

POST /api/ngrok/pairing/start

Create a one-time pairing URL (QR target) for mobile login. This will ensure a tunnel exists first.

  • Request:
    {
      "regenerateUrl": false,
      "next": "/"
    }
  • Response:
    {
      "success": true,
      "project": { "id": "project-id", "name": "project-name" },
      "pairUrl": "https://example.ngrok.app/_ok/pair?token=...",
      "gatewayApiBase": "https://example.ngrok.app/_ok/p/project-id",
      "expiresAt": "2026-02-24T12:00:00.000Z",
      "expiresIn": 90
    }
  • Notes:
    • Pairing tokens are random, one-time, and short-lived.
    • next is sanitized to a local path and defaults to /.
    • regenerateUrl=true forces a tunnel restart to mint a new public ngrok URL.

POST /api/ngrok/pairing/exchange

Exchange a one-time pairing token for a short-lived bearer session token (programmatic clients).

  • Request:
    { "token": "opaque-token" }
  • Response:
    {
      "success": true,
      "sessionJwt": "signed-token",
      "expiresIn": 900,
      "project": { "id": "project-id", "name": "project-name" }
    }

GET /_ok/health

Gateway liveness endpoint.

  • Response: { "ok": true, "service": "openkit-gateway" }

GET /_ok/pair

Consume a one-time QR pairing token directly on the ngrok-exposed OpenKit server.

  • Query params: token (required), next (optional)
  • Response: 302 redirect (on success)
  • Behavior:
    • validates token (single-use + short TTL)
    • sets ok_session cookie
    • applies rate limiting to pairing attempts
  • Error:
    • 400 pair_invalid (invalid/expired/used token)
    • 429 pair_rate_limited

GET /_ok/me

Get current local gateway identity.

  • Auth: ok_session cookie or Authorization: Bearer <sessionJwt>
  • Response:
    {
      "user": { "id": "uuid-or-paired-id", "email": "user@example.com-or-null" },
      "projectId": "project-id"
    }

POST /_ok/logout

Clear local gateway session cookie.

  • Response: { "success": true }

ANY /_ok/p/:projectId/*

Authenticated gateway proxy for programmatic access.

  • Auth: ok_session cookie or Authorization: Bearer <sessionJwt>
  • Behavior:
    • Verifies session project matches :projectId
    • Proxies to an internal allowlist only: /api/*
    • Injects:
      • X-OpenKit-User-Id
      • X-OpenKit-User-Email (when available)
      • X-OpenKit-Project-Id
  • Error:
    • 401 unauthenticated
    • 403 project_forbidden
    • 404 route_forbidden

Integration Verification

GET /api/integrations/verify

Background verification of all integration connections. Tests GitHub (gh auth status), Jira (API call), and Linear (GraphQL query) in parallel.

  • Response:
    {
      "github": { "ok": true },
      "jira": { "ok": true },
      "linear": null
    }
    Returns null for integrations that are not configured.