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.
- package/README.md +6 -3
- package/dist/assets/index-CLuQVRRb.css +1 -0
- package/dist/assets/index-JVnFWiSw.js +185 -0
- package/dist/index.html +2 -2
- package/package.json +4 -2
- package/server/dist/anthropic-models.d.ts +40 -0
- package/server/dist/anthropic-models.js +212 -0
- package/server/dist/anthropic-models.js.map +1 -0
- package/server/dist/claude-process.d.ts +31 -3
- package/server/dist/claude-process.js +126 -9
- package/server/dist/claude-process.js.map +1 -1
- package/server/dist/codex-process.d.ts +147 -0
- package/server/dist/codex-process.js +741 -0
- package/server/dist/codex-process.js.map +1 -0
- package/server/dist/coding-process.d.ts +16 -4
- package/server/dist/coding-process.js +10 -0
- package/server/dist/coding-process.js.map +1 -1
- package/server/dist/config.d.ts +13 -0
- package/server/dist/config.js +18 -0
- package/server/dist/config.js.map +1 -1
- package/server/dist/opencode-process.d.ts +142 -5
- package/server/dist/opencode-process.js +664 -84
- package/server/dist/opencode-process.js.map +1 -1
- package/server/dist/orchestrator-children.d.ts +53 -6
- package/server/dist/orchestrator-children.js +292 -68
- package/server/dist/orchestrator-children.js.map +1 -1
- package/server/dist/orchestrator-manager.d.ts +10 -0
- package/server/dist/orchestrator-manager.js +70 -18
- package/server/dist/orchestrator-manager.js.map +1 -1
- package/server/dist/orchestrator-monitor.d.ts +7 -1
- package/server/dist/orchestrator-monitor.js +49 -19
- package/server/dist/orchestrator-monitor.js.map +1 -1
- package/server/dist/orchestrator-notify.d.ts +16 -3
- package/server/dist/orchestrator-notify.js +22 -7
- package/server/dist/orchestrator-notify.js.map +1 -1
- package/server/dist/orchestrator-outbox.d.ts +48 -0
- package/server/dist/orchestrator-outbox.js +154 -0
- package/server/dist/orchestrator-outbox.js.map +1 -0
- package/server/dist/orchestrator-session-router.js +40 -1
- package/server/dist/orchestrator-session-router.js.map +1 -1
- package/server/dist/prompt-router.d.ts +22 -1
- package/server/dist/prompt-router.js +94 -11
- package/server/dist/prompt-router.js.map +1 -1
- package/server/dist/session-archive.js +11 -1
- package/server/dist/session-archive.js.map +1 -1
- package/server/dist/session-lifecycle.js +36 -0
- package/server/dist/session-lifecycle.js.map +1 -1
- package/server/dist/session-manager.d.ts +19 -2
- package/server/dist/session-manager.js +116 -27
- package/server/dist/session-manager.js.map +1 -1
- package/server/dist/session-naming.d.ts +4 -0
- package/server/dist/session-naming.js +26 -5
- package/server/dist/session-naming.js.map +1 -1
- package/server/dist/session-routes.js +38 -1
- package/server/dist/session-routes.js.map +1 -1
- package/server/dist/stepflow-handler.js +2 -2
- package/server/dist/stepflow-handler.js.map +1 -1
- package/server/dist/tsconfig.tsbuildinfo +1 -1
- package/server/dist/types.d.ts +24 -3
- package/server/dist/types.js +1 -9
- package/server/dist/types.js.map +1 -1
- package/server/dist/upload-routes.d.ts +7 -0
- package/server/dist/upload-routes.js +53 -24
- package/server/dist/upload-routes.js.map +1 -1
- package/server/dist/webhook-handler.js +3 -3
- package/server/dist/webhook-handler.js.map +1 -1
- package/server/dist/workflow-config.d.ts +2 -2
- package/server/dist/workflow-engine.d.ts +19 -0
- package/server/dist/workflow-engine.js +27 -3
- package/server/dist/workflow-engine.js.map +1 -1
- package/server/dist/workflow-loader.d.ts +5 -5
- package/server/dist/workflow-loader.js +90 -59
- package/server/dist/workflow-loader.js.map +1 -1
- package/server/dist/ws-message-handler.js +19 -8
- package/server/dist/ws-message-handler.js.map +1 -1
- package/server/dist/ws-server.js +25 -1
- package/server/dist/ws-server.js.map +1 -1
- package/dist/assets/index-B0xIzdCK.js +0 -187
- 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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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)
|