codekin 0.6.4 → 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 +8 -5
- package/bin/codekin.mjs +69 -6
- 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 +6 -4
- 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 +32 -3
- package/server/dist/claude-process.js +129 -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/commit-event-handler.d.ts +14 -1
- package/server/dist/commit-event-handler.js +40 -8
- package/server/dist/commit-event-handler.js.map +1 -1
- package/server/dist/config.d.ts +25 -0
- package/server/dist/config.js +42 -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 +94 -7
- package/server/dist/orchestrator-children.js +375 -65
- 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 +62 -19
- package/server/dist/orchestrator-monitor.js.map +1 -1
- package/server/dist/orchestrator-notify.d.ts +42 -0
- package/server/dist/orchestrator-notify.js +42 -0
- package/server/dist/orchestrator-notify.js.map +1 -0
- 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 +43 -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.d.ts +1 -0
- package/server/dist/session-lifecycle.js +37 -0
- package/server/dist/session-lifecycle.js.map +1 -1
- package/server/dist/session-manager.d.ts +49 -2
- package/server/dist/session-manager.js +221 -33
- 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 +42 -2
- 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 +85 -28
- 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 +20 -0
- package/server/dist/workflow-engine.js +52 -15
- package/server/dist/workflow-engine.js.map +1 -1
- package/server/dist/workflow-loader.d.ts +5 -5
- package/server/dist/workflow-loader.js +169 -54
- package/server/dist/workflow-loader.js.map +1 -1
- package/server/dist/workflow-routes.js +36 -2
- package/server/dist/workflow-routes.js.map +1 -1
- package/server/dist/ws-message-handler.js +24 -9
- package/server/dist/ws-message-handler.js.map +1 -1
- package/server/dist/ws-server.js +53 -11
- package/server/dist/ws-server.js.map +1 -1
- package/dist/assets/index-BRB_Ksyk.js +0 -182
- 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",
|
|
@@ -69,8 +71,8 @@
|
|
|
69
71
|
"globals": "^17.4.0",
|
|
70
72
|
"highlight.js": "^11.11.1",
|
|
71
73
|
"jsdom": "^29.0.1",
|
|
72
|
-
"marked": "^
|
|
73
|
-
"marked-highlight": "^2.2.
|
|
74
|
+
"marked": "^18.0.2",
|
|
75
|
+
"marked-highlight": "^2.2.4",
|
|
74
76
|
"react": "^19.2.0",
|
|
75
77
|
"react-diff-view": "^3.3.2",
|
|
76
78
|
"react-dom": "^19.2.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,8 @@ 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];
|
|
55
|
+
rate_limit: [event: Record<string, unknown>];
|
|
54
56
|
error: [message: string];
|
|
55
57
|
exit: [code: number | null, signal: string | null];
|
|
56
58
|
}
|
|
@@ -73,6 +75,9 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
|
|
|
73
75
|
* skip retries when this flag is true.
|
|
74
76
|
*/
|
|
75
77
|
private _sessionConflict;
|
|
78
|
+
/** Cumulative token counts across all result events in this process's lifetime. */
|
|
79
|
+
private cumulativeInputTokens;
|
|
80
|
+
private cumulativeOutputTokens;
|
|
76
81
|
/**
|
|
77
82
|
* Set to true when spawn() itself fails (ENOENT, EACCES, etc.).
|
|
78
83
|
* Distinguished from "process started but produced no output" — the latter
|
|
@@ -90,6 +95,8 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
|
|
|
90
95
|
private tool;
|
|
91
96
|
private tasks;
|
|
92
97
|
private taskSeq;
|
|
98
|
+
/** Outbound control requests (server → CLI, e.g. set_permission_mode) awaiting a control_response. */
|
|
99
|
+
private pendingOutboundControl;
|
|
93
100
|
/** Additional env vars passed to the child process (session ID, port, token). */
|
|
94
101
|
private extraEnv;
|
|
95
102
|
/** When true, use `--resume` instead of `--session-id` to resume an existing session. */
|
|
@@ -138,13 +145,25 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
|
|
|
138
145
|
* for user interaction, not permissions).
|
|
139
146
|
*/
|
|
140
147
|
private handleControlRequest;
|
|
141
|
-
/**
|
|
142
|
-
|
|
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;
|
|
143
154
|
/**
|
|
144
155
|
* Update internal task state from TodoWrite/TaskCreate/TaskUpdate tool calls.
|
|
145
156
|
* Returns true if the task list changed (caller should emit todo_update).
|
|
146
157
|
*/
|
|
147
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;
|
|
148
167
|
/**
|
|
149
168
|
* Extract a short summary from extended thinking text.
|
|
150
169
|
* Tries to grab the first sentence (up to 120 chars), or truncates at a
|
|
@@ -153,6 +172,16 @@ export declare class ClaudeProcess extends EventEmitter<ClaudeProcessEvents> imp
|
|
|
153
172
|
private extractThinkingSummary;
|
|
154
173
|
/** Send a user message to the Claude CLI via stdin (stream-json format). */
|
|
155
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>;
|
|
156
185
|
/** Write raw data to stdin (used for control_response messages). */
|
|
157
186
|
sendRaw(data: string): void;
|
|
158
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'].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,21 @@ 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
|
+
}
|
|
311
|
+
case 'rate_limit_event':
|
|
312
|
+
this.emit('rate_limit', event);
|
|
313
|
+
break;
|
|
268
314
|
}
|
|
269
315
|
}
|
|
270
316
|
/**
|
|
@@ -345,7 +391,9 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
345
391
|
if (this.handleTaskTool(this.tool.name, parsed)) {
|
|
346
392
|
if (TOOL_DEBUG)
|
|
347
393
|
console.log('[task-debug] emitting todo_update, tasks:', this.tasks.size);
|
|
348
|
-
|
|
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 })));
|
|
349
397
|
}
|
|
350
398
|
}
|
|
351
399
|
catch (err) {
|
|
@@ -469,7 +517,11 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
469
517
|
this.emit('control_request', request_id, toolName, toolInput);
|
|
470
518
|
}
|
|
471
519
|
}
|
|
472
|
-
/**
|
|
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
|
+
*/
|
|
473
525
|
sendControlResponse(requestId, behavior, updatedInput, message) {
|
|
474
526
|
// The CLI expects a nested format: { type, response: { subtype, request_id, response: { behavior, ... } } }
|
|
475
527
|
// Inner response schema:
|
|
@@ -502,15 +554,18 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
502
554
|
* Returns true if the task list changed (caller should emit todo_update).
|
|
503
555
|
*/
|
|
504
556
|
handleTaskTool(toolName, input) {
|
|
505
|
-
// 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.
|
|
506
561
|
if (toolName === 'TodoWrite') {
|
|
507
562
|
const todos = input.todos;
|
|
508
563
|
if (!Array.isArray(todos))
|
|
509
564
|
return false;
|
|
510
565
|
this.tasks.clear();
|
|
511
|
-
this.taskSeq = 0;
|
|
512
566
|
for (const item of todos) {
|
|
513
567
|
const id = String(item.id || ++this.taskSeq);
|
|
568
|
+
this.syncTaskSeq(id);
|
|
514
569
|
const status = item.status;
|
|
515
570
|
if (status !== 'pending' && status !== 'in_progress' && status !== 'completed')
|
|
516
571
|
continue;
|
|
@@ -525,7 +580,8 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
525
580
|
}
|
|
526
581
|
// TaskCreate/TaskUpdate are the newer tool names
|
|
527
582
|
if (toolName === 'TaskCreate') {
|
|
528
|
-
const id = String(++this.taskSeq);
|
|
583
|
+
const id = String(input.taskId || input.id || ++this.taskSeq);
|
|
584
|
+
this.syncTaskSeq(id);
|
|
529
585
|
this.tasks.set(id, {
|
|
530
586
|
id,
|
|
531
587
|
subject: String(input.subject || ''),
|
|
@@ -536,9 +592,19 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
536
592
|
}
|
|
537
593
|
if (toolName === 'TaskUpdate') {
|
|
538
594
|
const id = String(input.taskId || '');
|
|
539
|
-
|
|
540
|
-
if (!task)
|
|
595
|
+
if (!id)
|
|
541
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
|
+
}
|
|
542
608
|
const status = input.status;
|
|
543
609
|
if (status === 'deleted') {
|
|
544
610
|
this.tasks.delete(id);
|
|
@@ -555,6 +621,24 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
555
621
|
}
|
|
556
622
|
return false;
|
|
557
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
|
+
}
|
|
558
642
|
/**
|
|
559
643
|
* Extract a short summary from extended thinking text.
|
|
560
644
|
* Tries to grab the first sentence (up to 120 chars), or truncates at a
|
|
@@ -594,6 +678,42 @@ export class ClaudeProcess extends EventEmitter {
|
|
|
594
678
|
this.proc.stdin.once('drain', () => { });
|
|
595
679
|
}
|
|
596
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
|
+
}
|
|
597
717
|
/** Write raw data to stdin (used for control_response messages). */
|
|
598
718
|
sendRaw(data) {
|
|
599
719
|
if (!this.proc?.stdin?.writable)
|