codekin 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +6 -3
  2. package/dist/assets/index-CLuQVRRb.css +1 -0
  3. package/dist/assets/index-JVnFWiSw.js +185 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -2
  6. package/server/dist/anthropic-models.d.ts +40 -0
  7. package/server/dist/anthropic-models.js +212 -0
  8. package/server/dist/anthropic-models.js.map +1 -0
  9. package/server/dist/claude-process.d.ts +31 -3
  10. package/server/dist/claude-process.js +126 -9
  11. package/server/dist/claude-process.js.map +1 -1
  12. package/server/dist/codex-process.d.ts +147 -0
  13. package/server/dist/codex-process.js +741 -0
  14. package/server/dist/codex-process.js.map +1 -0
  15. package/server/dist/coding-process.d.ts +16 -4
  16. package/server/dist/coding-process.js +10 -0
  17. package/server/dist/coding-process.js.map +1 -1
  18. package/server/dist/config.d.ts +13 -0
  19. package/server/dist/config.js +18 -0
  20. package/server/dist/config.js.map +1 -1
  21. package/server/dist/opencode-process.d.ts +142 -5
  22. package/server/dist/opencode-process.js +664 -84
  23. package/server/dist/opencode-process.js.map +1 -1
  24. package/server/dist/orchestrator-children.d.ts +53 -6
  25. package/server/dist/orchestrator-children.js +292 -68
  26. package/server/dist/orchestrator-children.js.map +1 -1
  27. package/server/dist/orchestrator-manager.d.ts +10 -0
  28. package/server/dist/orchestrator-manager.js +70 -18
  29. package/server/dist/orchestrator-manager.js.map +1 -1
  30. package/server/dist/orchestrator-monitor.d.ts +7 -1
  31. package/server/dist/orchestrator-monitor.js +49 -19
  32. package/server/dist/orchestrator-monitor.js.map +1 -1
  33. package/server/dist/orchestrator-notify.d.ts +16 -3
  34. package/server/dist/orchestrator-notify.js +22 -7
  35. package/server/dist/orchestrator-notify.js.map +1 -1
  36. package/server/dist/orchestrator-outbox.d.ts +48 -0
  37. package/server/dist/orchestrator-outbox.js +154 -0
  38. package/server/dist/orchestrator-outbox.js.map +1 -0
  39. package/server/dist/orchestrator-session-router.js +40 -1
  40. package/server/dist/orchestrator-session-router.js.map +1 -1
  41. package/server/dist/prompt-router.d.ts +22 -1
  42. package/server/dist/prompt-router.js +94 -11
  43. package/server/dist/prompt-router.js.map +1 -1
  44. package/server/dist/session-archive.js +11 -1
  45. package/server/dist/session-archive.js.map +1 -1
  46. package/server/dist/session-lifecycle.js +36 -0
  47. package/server/dist/session-lifecycle.js.map +1 -1
  48. package/server/dist/session-manager.d.ts +19 -2
  49. package/server/dist/session-manager.js +116 -27
  50. package/server/dist/session-manager.js.map +1 -1
  51. package/server/dist/session-naming.d.ts +4 -0
  52. package/server/dist/session-naming.js +26 -5
  53. package/server/dist/session-naming.js.map +1 -1
  54. package/server/dist/session-routes.js +38 -1
  55. package/server/dist/session-routes.js.map +1 -1
  56. package/server/dist/stepflow-handler.js +2 -2
  57. package/server/dist/stepflow-handler.js.map +1 -1
  58. package/server/dist/tsconfig.tsbuildinfo +1 -1
  59. package/server/dist/types.d.ts +24 -3
  60. package/server/dist/types.js +1 -9
  61. package/server/dist/types.js.map +1 -1
  62. package/server/dist/upload-routes.d.ts +7 -0
  63. package/server/dist/upload-routes.js +53 -24
  64. package/server/dist/upload-routes.js.map +1 -1
  65. package/server/dist/webhook-handler.js +3 -3
  66. package/server/dist/webhook-handler.js.map +1 -1
  67. package/server/dist/workflow-config.d.ts +2 -2
  68. package/server/dist/workflow-engine.d.ts +19 -0
  69. package/server/dist/workflow-engine.js +27 -3
  70. package/server/dist/workflow-engine.js.map +1 -1
  71. package/server/dist/workflow-loader.d.ts +5 -5
  72. package/server/dist/workflow-loader.js +90 -59
  73. package/server/dist/workflow-loader.js.map +1 -1
  74. package/server/dist/ws-message-handler.js +19 -8
  75. package/server/dist/ws-message-handler.js.map +1 -1
  76. package/server/dist/ws-server.js +25 -1
  77. package/server/dist/ws-server.js.map +1 -1
  78. package/dist/assets/index-B0xIzdCK.js +0 -187
  79. package/dist/assets/index-Q2WSVlHo.css +0 -1
package/dist/index.html CHANGED
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
6
6
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
7
  <title>Codekin</title>
8
- <script type="module" crossorigin src="/assets/index-B0xIzdCK.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Q2WSVlHo.css">
8
+ <script type="module" crossorigin src="/assets/index-JVnFWiSw.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CLuQVRRb.css">
10
10
  </head>
11
11
  <body class="bg-neutral-12 text-neutral-2">
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codekin",
3
- "version": "0.6.5",
3
+ "version": "0.7.0",
4
4
  "license": "MIT",
5
5
  "licenseNotes": "dompurify is dual-licensed (MPL-2.0 OR Apache-2.0); both are permissively compatible with MIT for library use. lightningcss (MPL-2.0) is a build-time-only dependency used by TailwindCSS and is not included in distributed artifacts.",
6
6
  "author": "multiplier-labs",
@@ -39,8 +39,10 @@
39
39
  "dependencies": {
40
40
  "better-sqlite3": "^12.9.0",
41
41
  "express": "^5.1.0",
42
+ "file-type": "^22.0.1",
42
43
  "multer": "^2.0.0",
43
- "ws": "^8.18.0"
44
+ "ws": "^8.21.0",
45
+ "yaml": "^2.9.0"
44
46
  },
45
47
  "overrides": {
46
48
  "undici": "^7.24.0",
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Discovers available Claude models via two strategies:
3
+ *
4
+ * 1. **Anthropic API** — `GET /v1/models` using ANTHROPIC_API_KEY (if set).
5
+ * Returns the full catalog, cached for 1 hour.
6
+ *
7
+ * 2. **CLI alias probing** — Spawns `claude -p --model <alias> "ok"` for each
8
+ * known alias (opus, sonnet, haiku) and reads the resolved model ID from
9
+ * the JSON output's `modelUsage` field. Runs once per day, triggered on
10
+ * first session creation. Works with OAuth/subscription auth (no API key).
11
+ *
12
+ * Falls back to a hardcoded list when neither strategy has completed yet.
13
+ */
14
+ export interface ClaudeModelInfo {
15
+ id: string;
16
+ label: string;
17
+ }
18
+ /** Hardcoded fallback used until dynamic discovery completes.
19
+ * Per https://platform.claude.com/docs/en/about-claude/models/overview */
20
+ export declare const FALLBACK_MODELS: ClaudeModelInfo[];
21
+ /**
22
+ * Synchronously return the best-known default Claude model ID — the first
23
+ * (newest) entry of the discovered list, or the hardcoded fallback's first
24
+ * entry when discovery hasn't completed yet. This matches the model the
25
+ * frontend auto-selects (the [0] of the same list), so new sessions start on
26
+ * it directly instead of letting the CLI pick a stale default and forcing a
27
+ * disruptive model-switch restart that drops the user's first message.
28
+ */
29
+ export declare function getDefaultClaudeModel(): string;
30
+ /**
31
+ * Return available Claude models. Uses cached results when valid.
32
+ * Called by the GET /api/claude/models endpoint.
33
+ */
34
+ export declare function fetchAnthropicModels(): Promise<ClaudeModelInfo[]>;
35
+ /**
36
+ * Trigger a background CLI alias probe if the cache is stale or empty.
37
+ * Call this on first session creation of the day. Non-blocking — returns
38
+ * immediately and updates the cache when the probe finishes.
39
+ */
40
+ export declare function triggerCliProbeIfNeeded(): void;
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Discovers available Claude models via two strategies:
3
+ *
4
+ * 1. **Anthropic API** — `GET /v1/models` using ANTHROPIC_API_KEY (if set).
5
+ * Returns the full catalog, cached for 1 hour.
6
+ *
7
+ * 2. **CLI alias probing** — Spawns `claude -p --model <alias> "ok"` for each
8
+ * known alias (opus, sonnet, haiku) and reads the resolved model ID from
9
+ * the JSON output's `modelUsage` field. Runs once per day, triggered on
10
+ * first session creation. Works with OAuth/subscription auth (no API key).
11
+ *
12
+ * Falls back to a hardcoded list when neither strategy has completed yet.
13
+ */
14
+ import { execFile } from 'child_process';
15
+ import { CLAUDE_BINARY } from './config.js';
16
+ /** Hardcoded fallback used until dynamic discovery completes.
17
+ * Per https://platform.claude.com/docs/en/about-claude/models/overview */
18
+ export const FALLBACK_MODELS = [
19
+ { id: 'claude-opus-4-8', label: 'Opus 4.8' },
20
+ { id: 'claude-fable-5', label: 'Fable 5' },
21
+ { id: 'claude-opus-4-7', label: 'Opus 4.7' },
22
+ { id: 'claude-opus-4-6', label: 'Opus 4.6' },
23
+ { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6' },
24
+ { id: 'claude-haiku-4-5-20251001', label: 'Haiku 4.5' },
25
+ ];
26
+ /**
27
+ * Candidate model IDs to probe via the CLI. We don't probe aliases (opus/sonnet/haiku)
28
+ * because the CLI's alias resolution lags — `opus` still resolves to 4-6 even when
29
+ * 4-8 is the latest. Instead, we probe specific version IDs spanning current and
30
+ * likely-future releases. Failed probes return in ~2.5s at zero cost; successful
31
+ * probes cost ~$0.04 each. Probing in parallel keeps total wall time under 5s.
32
+ */
33
+ const CANDIDATE_MODEL_IDS = [
34
+ // Fable family (5th-generation flagship; GA 2026-06-09). Dateless pinned ID.
35
+ 'claude-fable-5',
36
+ 'claude-fable-6',
37
+ // Opus family (currently 4.6, 4.7, 4.8 are live; probe ahead for new releases)
38
+ 'claude-opus-4-6',
39
+ 'claude-opus-4-7',
40
+ 'claude-opus-4-8',
41
+ 'claude-opus-4-9',
42
+ 'claude-opus-5-0',
43
+ // Sonnet family (currently 4.6 is latest; probe ahead)
44
+ 'claude-sonnet-4-6',
45
+ 'claude-sonnet-4-7',
46
+ 'claude-sonnet-4-8',
47
+ 'claude-sonnet-5-0',
48
+ // Haiku family (currently 4.5 is latest; probe ahead — note dated suffix)
49
+ 'claude-haiku-4-5-20251001',
50
+ 'claude-haiku-4-6',
51
+ 'claude-haiku-4-7',
52
+ 'claude-haiku-5-0',
53
+ ];
54
+ // ---------------------------------------------------------------------------
55
+ // Shared cache
56
+ // ---------------------------------------------------------------------------
57
+ let cache = null;
58
+ const API_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour (API is cheap, refresh often)
59
+ const CLI_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours (CLI probing costs tokens)
60
+ /** In-flight probe promise — prevents duplicate concurrent probes. */
61
+ let probeInFlight = null;
62
+ // ---------------------------------------------------------------------------
63
+ // Label helper
64
+ // ---------------------------------------------------------------------------
65
+ /**
66
+ * Build a human-friendly label from a model ID.
67
+ * e.g. "claude-opus-4-8" → "Opus 4.8"
68
+ * "claude-haiku-4-5-20251001" → "Haiku 4.5"
69
+ * "claude-fable-5" → "Fable 5" (single-version, dateless IDs)
70
+ */
71
+ function labelFromId(id) {
72
+ const rest = id.replace(/^claude-/, '');
73
+ const m = rest.match(/^(\w+?)-(\d+)(?:-(\d+))?/);
74
+ if (m) {
75
+ const family = m[1].charAt(0).toUpperCase() + m[1].slice(1);
76
+ return m[3] ? `${family} ${m[2]}.${m[3]}` : `${family} ${m[2]}`;
77
+ }
78
+ return id;
79
+ }
80
+ async function fetchViaApi() {
81
+ const apiKey = process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_API_KEY;
82
+ if (!apiKey)
83
+ return null;
84
+ const res = await fetch('https://api.anthropic.com/v1/models?limit=100', {
85
+ headers: {
86
+ 'x-api-key': apiKey,
87
+ 'anthropic-version': '2023-06-01',
88
+ },
89
+ signal: AbortSignal.timeout(10_000),
90
+ });
91
+ if (!res.ok)
92
+ return null;
93
+ const data = (await res.json());
94
+ if (!Array.isArray(data.data) || data.data.length === 0)
95
+ return null;
96
+ const models = data.data
97
+ .filter(m => m.id.startsWith('claude-') && !m.id.includes('embed'))
98
+ .sort((a, b) => {
99
+ if (a.created_at && b.created_at)
100
+ return b.created_at.localeCompare(a.created_at);
101
+ return 0;
102
+ })
103
+ .map(m => ({ id: m.id, label: m.display_name || labelFromId(m.id) }));
104
+ return models.length > 0 ? models : null;
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Strategy 2: CLI alias probing
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Probe a single model ID via the CLI. Returns the model ID if available,
111
+ * null otherwise. Failed probes return in ~2.5s without consuming tokens;
112
+ * successful probes take ~4.5s and cost ~$0.04 (one short turn).
113
+ */
114
+ function probeModel(modelId) {
115
+ return new Promise(resolve => {
116
+ const child = execFile(CLAUDE_BINARY,
117
+ // Note: do NOT pass --bare — it strips the modelUsage field we need.
118
+ ['-p', '--model', modelId, '--output-format', 'json', 'reply with only: ok'], { timeout: 30_000 }, (err, stdout) => {
119
+ if (err) {
120
+ resolve(null);
121
+ return;
122
+ }
123
+ try {
124
+ const result = JSON.parse(stdout);
125
+ if (result.is_error) {
126
+ resolve(null);
127
+ return;
128
+ }
129
+ const id = Object.keys(result.modelUsage ?? {})[0];
130
+ resolve(id || null);
131
+ }
132
+ catch {
133
+ resolve(null);
134
+ }
135
+ });
136
+ child.unref?.();
137
+ });
138
+ }
139
+ /** Probe all candidate model IDs in parallel. */
140
+ async function fetchViaCli() {
141
+ const results = await Promise.all(CANDIDATE_MODEL_IDS.map(probeModel));
142
+ const models = [];
143
+ const seen = new Set();
144
+ for (const id of results) {
145
+ if (id && !seen.has(id)) {
146
+ seen.add(id);
147
+ models.push({ id, label: labelFromId(id) });
148
+ }
149
+ }
150
+ // Sort newest version first within each family (opus > sonnet > haiku ordering preserved by input)
151
+ return models.length > 0 ? models : null;
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Public API
155
+ // ---------------------------------------------------------------------------
156
+ /**
157
+ * Synchronously return the best-known default Claude model ID — the first
158
+ * (newest) entry of the discovered list, or the hardcoded fallback's first
159
+ * entry when discovery hasn't completed yet. This matches the model the
160
+ * frontend auto-selects (the [0] of the same list), so new sessions start on
161
+ * it directly instead of letting the CLI pick a stale default and forcing a
162
+ * disruptive model-switch restart that drops the user's first message.
163
+ */
164
+ export function getDefaultClaudeModel() {
165
+ return (cache?.models[0] ?? FALLBACK_MODELS[0]).id;
166
+ }
167
+ /**
168
+ * Return available Claude models. Uses cached results when valid.
169
+ * Called by the GET /api/claude/models endpoint.
170
+ */
171
+ export async function fetchAnthropicModels() {
172
+ if (cache && Date.now() < cache.expiresAt) {
173
+ return cache.models;
174
+ }
175
+ // Strategy 1: Anthropic API (fast, full catalog)
176
+ try {
177
+ const apiModels = await fetchViaApi();
178
+ if (apiModels) {
179
+ cache = { models: apiModels, expiresAt: Date.now() + API_CACHE_TTL_MS };
180
+ return apiModels;
181
+ }
182
+ }
183
+ catch { /* fall through */ }
184
+ // If CLI probe has already cached results, use those
185
+ if (cache)
186
+ return cache.models;
187
+ return FALLBACK_MODELS;
188
+ }
189
+ /**
190
+ * Trigger a background CLI alias probe if the cache is stale or empty.
191
+ * Call this on first session creation of the day. Non-blocking — returns
192
+ * immediately and updates the cache when the probe finishes.
193
+ */
194
+ export function triggerCliProbeIfNeeded() {
195
+ // Skip if cache is still valid or a probe is already running
196
+ if (cache && Date.now() < cache.expiresAt)
197
+ return;
198
+ if (probeInFlight)
199
+ return;
200
+ // Skip if API key is available (fetchAnthropicModels will use the API instead)
201
+ if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_API_KEY)
202
+ return;
203
+ probeInFlight = fetchViaCli()
204
+ .then(models => {
205
+ if (models) {
206
+ cache = { models, expiresAt: Date.now() + CLI_CACHE_TTL_MS };
207
+ }
208
+ })
209
+ .catch(() => { })
210
+ .finally(() => { probeInFlight = null; });
211
+ }
212
+ //# sourceMappingURL=anthropic-models.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anthropic-models.js","sourceRoot":"","sources":["../anthropic-models.ts"],"names":[],"mappings":"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"}
@@ -11,7 +11,7 @@
11
11
  * - Turn results and process exit
12
12
  */
13
13
  import { EventEmitter } from 'events';
14
- import type { ClaudeEvent, TaskItem, PromptQuestion, PermissionMode } from './types.js';
14
+ import type { ClaudeEvent, TaskItem, PromptQuestion, PermissionMode, SessionUsage } from './types.js';
15
15
  import { type CodingProcess, type CodingProvider, type ProviderCapabilities } from './coding-process.js';
16
16
  /** Options for constructing a ClaudeProcess. Replaces positional constructor parameters. */
17
17
  export interface ClaudeProcessOptions {
@@ -51,6 +51,7 @@ export interface ClaudeProcessEvents {
51
51
  todo_update: [tasks: TaskItem[]];
52
52
  image: [base64: string, mediaType: string];
53
53
  result: [text: string, isError: boolean];
54
+ usage: [usage: SessionUsage];
54
55
  rate_limit: [event: Record<string, unknown>];
55
56
  error: [message: string];
56
57
  exit: [code: number | null, signal: string | null];
@@ -74,6 +75,9 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
74
75
  * skip retries when this flag is true.
75
76
  */
76
77
  private _sessionConflict;
78
+ /** Cumulative token counts across all result events in this process's lifetime. */
79
+ private cumulativeInputTokens;
80
+ private cumulativeOutputTokens;
77
81
  /**
78
82
  * Set to true when spawn() itself fails (ENOENT, EACCES, etc.).
79
83
  * Distinguished from "process started but produced no output" — the latter
@@ -91,6 +95,8 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
91
95
  private tool;
92
96
  private tasks;
93
97
  private taskSeq;
98
+ /** Outbound control requests (server → CLI, e.g. set_permission_mode) awaiting a control_response. */
99
+ private pendingOutboundControl;
94
100
  /** Additional env vars passed to the child process (session ID, port, token). */
95
101
  private extraEnv;
96
102
  /** When true, use `--resume` instead of `--session-id` to resume an existing session. */
@@ -139,13 +145,25 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
139
145
  * for user interaction, not permissions).
140
146
  */
141
147
  private handleControlRequest;
142
- /** Send a control_response back to the CLI to allow or deny a pending request. */
143
- sendControlResponse(requestId: string, behavior: 'allow' | 'deny', updatedInput?: Record<string, unknown>, message?: string): void;
148
+ /**
149
+ * Send a control_response back to the CLI to allow or deny a pending request.
150
+ * 'allow_always' is treated as 'allow' — the CLI protocol has no persistent
151
+ * grant; persistence is handled by Codekin's ApprovalManager.
152
+ */
153
+ sendControlResponse(requestId: string, behavior: 'allow' | 'deny' | 'allow_always', updatedInput?: Record<string, unknown>, message?: string): void;
144
154
  /**
145
155
  * Update internal task state from TodoWrite/TaskCreate/TaskUpdate tool calls.
146
156
  * Returns true if the task list changed (caller should emit todo_update).
147
157
  */
148
158
  private handleTaskTool;
159
+ /** Keep taskSeq ahead of any numeric id we have seen, so generated ids never collide. */
160
+ private syncTaskSeq;
161
+ /**
162
+ * Seed task state from a previous process's last known list (session restore).
163
+ * Without this, a restarted process starts with an empty map and TaskUpdate
164
+ * calls referencing pre-restart task ids would otherwise be lost.
165
+ */
166
+ seedTasks(tasks: TaskItem[]): void;
149
167
  /**
150
168
  * Extract a short summary from extended thinking text.
151
169
  * Tries to grab the first sentence (up to 120 chars), or truncates at a
@@ -154,6 +172,16 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
154
172
  private extractThinkingSummary;
155
173
  /** Send a user message to the Claude CLI via stdin (stream-json format). */
156
174
  sendMessage(content: string): void;
175
+ /**
176
+ * Change the CLI's permission mode in-place via a stream-json control request,
177
+ * without restarting the process (which would kill in-flight turns and pending
178
+ * approvals). Resolves true on CLI acknowledgement, false on timeout/error/exit —
179
+ * callers should fall back to a process restart on false.
180
+ *
181
+ * 'dangerouslySkipPermissions' is a spawn flag (--dangerously-skip-permissions),
182
+ * not a runtime mode, so it always requires a restart.
183
+ */
184
+ setPermissionMode(mode: PermissionMode): Promise<boolean>;
157
185
  /** Write raw data to stdin (used for control_response messages). */
158
186
  sendRaw(data: string): void;
159
187
  /** Gracefully stop the process (SIGTERM, then SIGKILL after 5s timeout). */
@@ -41,6 +41,9 @@ export class ClaudeProcess extends EventEmitter {
41
41
  * skip retries when this flag is true.
42
42
  */
43
43
  _sessionConflict = false;
44
+ /** Cumulative token counts across all result events in this process's lifetime. */
45
+ cumulativeInputTokens = 0;
46
+ cumulativeOutputTokens = 0;
44
47
  /**
45
48
  * Set to true when spawn() itself fails (ENOENT, EACCES, etc.).
46
49
  * Distinguished from "process started but produced no output" — the latter
@@ -60,6 +63,8 @@ export class ClaudeProcess extends EventEmitter {
60
63
  // Task/todo state: mirrors Claude's internal todo list for the UI
61
64
  tasks = new Map();
62
65
  taskSeq = 0;
66
+ /** Outbound control requests (server → CLI, e.g. set_permission_mode) awaiting a control_response. */
67
+ pendingOutboundControl = new Map;
63
68
  /** Additional env vars passed to the child process (session ID, port, token). */
64
69
  extraEnv;
65
70
  /** When true, use `--resume` instead of `--session-id` to resume an existing session. */
@@ -146,6 +151,10 @@ export class ClaudeProcess extends EventEmitter {
146
151
  // a resume. Uses the full binary path + exact session UUID to avoid
147
152
  // matching unrelated processes.
148
153
  if (this.resume) {
154
+ // Guard against non-UUID values being interpolated into the pkill pattern
155
+ if (!/^[0-9a-f-]{36}$/i.test(this.sessionId)) {
156
+ throw new Error(`[claude-spawn] sessionId is not a valid UUID: ${this.sessionId}`);
157
+ }
149
158
  try {
150
159
  const pattern = `${CLAUDE_BINARY} .*(--resume|--session-id) ${this.sessionId}(\\s|$)`;
151
160
  execFileSync('pkill', ['-f', pattern], { timeout: 2000, stdio: 'ignore' });
@@ -154,7 +163,10 @@ export class ClaudeProcess extends EventEmitter {
154
163
  // pkill exits 1 when no matching process is found — that's the happy path
155
164
  }
156
165
  }
157
- console.log(`[claude-spawn] cwd=${this.workingDir} args=${JSON.stringify(args)}`);
166
+ const redactedArgs = args.map((a, i) => i > 0 && /^[0-9a-f-]{36}$/i.test(a) && ['--session-id', '--resume'].includes(args[i - 1])
167
+ ? '<redacted>'
168
+ : a);
169
+ console.log(`[claude-spawn] cwd=${this.workingDir} args=${JSON.stringify(redactedArgs)}`);
158
170
  this.proc = spawn(CLAUDE_BINARY, args, {
159
171
  cwd: this.workingDir,
160
172
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -204,6 +216,11 @@ export class ClaudeProcess extends EventEmitter {
204
216
  this.rl = null;
205
217
  this.proc = null;
206
218
  this.tasks.clear();
219
+ // Fail any outbound control requests still awaiting a response —
220
+ // the process is gone, so callers should fall back to a restart.
221
+ for (const resolve of this.pendingOutboundControl.values())
222
+ resolve(false);
223
+ this.pendingOutboundControl.clear();
207
224
  this.emit('exit', code, signal);
208
225
  });
209
226
  }
@@ -233,7 +250,7 @@ export class ClaudeProcess extends EventEmitter {
233
250
  console.log(`[event] type=${event.type} subtype=${subtype || '-'}`);
234
251
  }
235
252
  // Log all event types we DON'T handle to catch unknown protocol messages
236
- if (!['system', 'stream_event', 'assistant', 'user', 'result', 'control_request', 'rate_limit_event'].includes(event.type)) {
253
+ if (!['system', 'stream_event', 'assistant', 'user', 'result', 'control_request', 'control_response', 'rate_limit_event'].includes(event.type)) {
237
254
  console.log(`[event-unhandled] type=${event.type} data=${JSON.stringify(event).slice(0, 300)}`);
238
255
  }
239
256
  switch (event.type) {
@@ -255,6 +272,20 @@ export class ClaudeProcess extends EventEmitter {
255
272
  break;
256
273
  case 'result': {
257
274
  const resultEvent = event;
275
+ // Surface cumulative token/cost usage. The CLI's result event carries
276
+ // per-turn usage plus a cumulative total_cost_usd for the session.
277
+ const u = resultEvent.usage;
278
+ if (u) {
279
+ this.cumulativeInputTokens += (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
280
+ this.cumulativeOutputTokens += u.output_tokens ?? 0;
281
+ }
282
+ if (u || typeof resultEvent.total_cost_usd === 'number') {
283
+ this.emit('usage', {
284
+ inputTokens: this.cumulativeInputTokens,
285
+ outputTokens: this.cumulativeOutputTokens,
286
+ ...(typeof resultEvent.total_cost_usd === 'number' ? { costUsd: resultEvent.total_cost_usd } : {}),
287
+ });
288
+ }
258
289
  this.emit('result', resultEvent.result || '', resultEvent.is_error || false);
259
290
  break;
260
291
  }
@@ -265,6 +296,18 @@ export class ClaudeProcess extends EventEmitter {
265
296
  this.handleControlRequest(ctrlEvent);
266
297
  break;
267
298
  }
299
+ case 'control_response': {
300
+ // Response to an outbound control_request we sent (e.g. set_permission_mode).
301
+ const resp = event.response;
302
+ if (resp?.request_id) {
303
+ const resolve = this.pendingOutboundControl.get(resp.request_id);
304
+ if (resolve) {
305
+ this.pendingOutboundControl.delete(resp.request_id);
306
+ resolve(resp.subtype === 'success');
307
+ }
308
+ }
309
+ break;
310
+ }
268
311
  case 'rate_limit_event':
269
312
  this.emit('rate_limit', event);
270
313
  break;
@@ -348,7 +391,9 @@ export class ClaudeProcess extends EventEmitter {
348
391
  if (this.handleTaskTool(this.tool.name, parsed)) {
349
392
  if (TOOL_DEBUG)
350
393
  console.log('[task-debug] emitting todo_update, tasks:', this.tasks.size);
351
- this.emit('todo_update', Array.from(this.tasks.values()));
394
+ // Copy items so later in-place TaskUpdate mutations don't alias into
395
+ // previously broadcast/history-stored snapshots.
396
+ this.emit('todo_update', Array.from(this.tasks.values(), t => ({ ...t })));
352
397
  }
353
398
  }
354
399
  catch (err) {
@@ -472,7 +517,11 @@ export class ClaudeProcess extends EventEmitter {
472
517
  this.emit('control_request', request_id, toolName, toolInput);
473
518
  }
474
519
  }
475
- /** Send a control_response back to the CLI to allow or deny a pending request. */
520
+ /**
521
+ * Send a control_response back to the CLI to allow or deny a pending request.
522
+ * 'allow_always' is treated as 'allow' — the CLI protocol has no persistent
523
+ * grant; persistence is handled by Codekin's ApprovalManager.
524
+ */
476
525
  sendControlResponse(requestId, behavior, updatedInput, message) {
477
526
  // The CLI expects a nested format: { type, response: { subtype, request_id, response: { behavior, ... } } }
478
527
  // Inner response schema:
@@ -505,15 +554,18 @@ export class ClaudeProcess extends EventEmitter {
505
554
  * Returns true if the task list changed (caller should emit todo_update).
506
555
  */
507
556
  handleTaskTool(toolName, input) {
508
- // TodoWrite sends the entire list at once
557
+ // TodoWrite sends the entire list at once.
558
+ // Note: taskSeq is intentionally NOT reset — keeping ids monotonic across
559
+ // list generations lets the frontend detect a brand-new list by id and
560
+ // avoids id collisions with later TaskCreate/TaskUpdate calls.
509
561
  if (toolName === 'TodoWrite') {
510
562
  const todos = input.todos;
511
563
  if (!Array.isArray(todos))
512
564
  return false;
513
565
  this.tasks.clear();
514
- this.taskSeq = 0;
515
566
  for (const item of todos) {
516
567
  const id = String(item.id || ++this.taskSeq);
568
+ this.syncTaskSeq(id);
517
569
  const status = item.status;
518
570
  if (status !== 'pending' && status !== 'in_progress' && status !== 'completed')
519
571
  continue;
@@ -528,7 +580,8 @@ export class ClaudeProcess extends EventEmitter {
528
580
  }
529
581
  // TaskCreate/TaskUpdate are the newer tool names
530
582
  if (toolName === 'TaskCreate') {
531
- const id = String(++this.taskSeq);
583
+ const id = String(input.taskId || input.id || ++this.taskSeq);
584
+ this.syncTaskSeq(id);
532
585
  this.tasks.set(id, {
533
586
  id,
534
587
  subject: String(input.subject || ''),
@@ -539,9 +592,19 @@ export class ClaudeProcess extends EventEmitter {
539
592
  }
540
593
  if (toolName === 'TaskUpdate') {
541
594
  const id = String(input.taskId || '');
542
- const task = this.tasks.get(id);
543
- if (!task)
595
+ if (!id)
544
596
  return false;
597
+ let task = this.tasks.get(id);
598
+ if (!task) {
599
+ // Unknown id — our in-memory map can diverge from the CLI's real task
600
+ // list (e.g. after a process restart mid-session). Upsert instead of
601
+ // dropping the update, otherwise the UI shows a stale list forever.
602
+ if (input.status === 'deleted')
603
+ return false;
604
+ task = { id, subject: String(input.subject || `Task ${id}`), status: 'pending' };
605
+ this.tasks.set(id, task);
606
+ this.syncTaskSeq(id);
607
+ }
545
608
  const status = input.status;
546
609
  if (status === 'deleted') {
547
610
  this.tasks.delete(id);
@@ -558,6 +621,24 @@ export class ClaudeProcess extends EventEmitter {
558
621
  }
559
622
  return false;
560
623
  }
624
+ /** Keep taskSeq ahead of any numeric id we have seen, so generated ids never collide. */
625
+ syncTaskSeq(id) {
626
+ const n = Number(id);
627
+ if (Number.isInteger(n) && n > this.taskSeq)
628
+ this.taskSeq = n;
629
+ }
630
+ /**
631
+ * Seed task state from a previous process's last known list (session restore).
632
+ * Without this, a restarted process starts with an empty map and TaskUpdate
633
+ * calls referencing pre-restart task ids would otherwise be lost.
634
+ */
635
+ seedTasks(tasks) {
636
+ this.tasks.clear();
637
+ for (const t of tasks) {
638
+ this.tasks.set(t.id, { ...t });
639
+ this.syncTaskSeq(t.id);
640
+ }
641
+ }
561
642
  /**
562
643
  * Extract a short summary from extended thinking text.
563
644
  * Tries to grab the first sentence (up to 120 chars), or truncates at a
@@ -597,6 +678,42 @@ export class ClaudeProcess extends EventEmitter {
597
678
  this.proc.stdin.once('drain', () => { });
598
679
  }
599
680
  }
681
+ /**
682
+ * Change the CLI's permission mode in-place via a stream-json control request,
683
+ * without restarting the process (which would kill in-flight turns and pending
684
+ * approvals). Resolves true on CLI acknowledgement, false on timeout/error/exit —
685
+ * callers should fall back to a process restart on false.
686
+ *
687
+ * 'dangerouslySkipPermissions' is a spawn flag (--dangerously-skip-permissions),
688
+ * not a runtime mode, so it always requires a restart.
689
+ */
690
+ setPermissionMode(mode) {
691
+ if (mode === 'dangerouslySkipPermissions' || this.permissionMode === 'dangerouslySkipPermissions') {
692
+ return Promise.resolve(false);
693
+ }
694
+ if (!this.proc?.stdin?.writable)
695
+ return Promise.resolve(false);
696
+ const requestId = randomUUID();
697
+ return new Promise((resolve) => {
698
+ const timer = setTimeout(() => {
699
+ this.pendingOutboundControl.delete(requestId);
700
+ console.warn(`[set-permission-mode] timed out waiting for CLI ack (mode=${mode})`);
701
+ resolve(false);
702
+ }, 5000);
703
+ this.pendingOutboundControl.set(requestId, (ok) => {
704
+ clearTimeout(timer);
705
+ if (ok)
706
+ this.permissionMode = mode;
707
+ console.log(`[set-permission-mode] CLI ${ok ? 'acknowledged' : 'rejected'} mode=${mode}`);
708
+ resolve(ok);
709
+ });
710
+ this.sendRaw(JSON.stringify({
711
+ type: 'control_request',
712
+ request_id: requestId,
713
+ request: { subtype: 'set_permission_mode', mode },
714
+ }));
715
+ });
716
+ }
600
717
  /** Write raw data to stdin (used for control_response messages). */
601
718
  sendRaw(data) {
602
719
  if (!this.proc?.stdin?.writable)