ever-terminal 1.0.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/LICENSE +21 -0
- package/README.md +266 -0
- package/dist/claude/provider.js +234 -0
- package/dist/claude/session.js +719 -0
- package/dist/claude/summarize.js +97 -0
- package/dist/cli.js +414 -0
- package/dist/codex/app-server.js +300 -0
- package/dist/codex/memory.js +61 -0
- package/dist/codex/provider.js +362 -0
- package/dist/codex/session.js +1091 -0
- package/dist/codex/status.js +16 -0
- package/dist/codex/storage.js +83 -0
- package/dist/codex/summarize.js +69 -0
- package/dist/debug.js +9 -0
- package/dist/expose/providers/bore.js +14 -0
- package/dist/expose/providers/ngrok.js +35 -0
- package/dist/expose/providers/pinggy.js +22 -0
- package/dist/expose/registry.js +22 -0
- package/dist/expose/run.js +75 -0
- package/dist/expose/types.js +1 -0
- package/dist/index.js +78 -0
- package/dist/logger.js +44 -0
- package/dist/routes/core.js +290 -0
- package/dist/routes/events.js +104 -0
- package/dist/session.js +18 -0
- package/dist/startup/common.js +318 -0
- package/dist/startup/instance.js +89 -0
- package/dist/summary-format.js +17 -0
- package/dist/types.js +1 -0
- package/dist/update.js +56 -0
- package/dist/util/spawn-shim.js +25 -0
- package/package.json +79 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { summarizeClaudeToolCall } from "./summarize.js";
|
|
4
|
+
import { debugLog } from "../debug.js";
|
|
5
|
+
function buildClaudePermissionOptions(suggestions, description) {
|
|
6
|
+
const options = [{ text: "Yes", key: "allow" }];
|
|
7
|
+
const alwaysText = describeClaudePermissionSuggestions(suggestions, description);
|
|
8
|
+
if (alwaysText) {
|
|
9
|
+
options.push({ text: alwaysText, key: "allowAlways" });
|
|
10
|
+
}
|
|
11
|
+
options.push({ text: "No", key: "deny" });
|
|
12
|
+
return options;
|
|
13
|
+
}
|
|
14
|
+
function describeClaudePermissionSuggestions(suggestions, placeholderText) {
|
|
15
|
+
const first = suggestions?.[0];
|
|
16
|
+
if (!first)
|
|
17
|
+
return null;
|
|
18
|
+
if (first.type === "addDirectories" &&
|
|
19
|
+
Array.isArray(first.directories) &&
|
|
20
|
+
first.directories.length > 0) {
|
|
21
|
+
return `Yes, and always allow access to ${joinQuoted(first.directories)} ${describeClaudeDestination(first.destination)}`;
|
|
22
|
+
}
|
|
23
|
+
if ((first.type === "addRules" || first.type === "replaceRules") &&
|
|
24
|
+
Array.isArray(first.rules) &&
|
|
25
|
+
first.rules.length > 0) {
|
|
26
|
+
const rule = first.rules[0] ?? {};
|
|
27
|
+
const toolName = String(rule.toolName ?? "this tool");
|
|
28
|
+
const ruleContent = typeof rule.ruleContent === "string" && rule.ruleContent.trim()
|
|
29
|
+
? ` rule ${quoteInline(rule.ruleContent.trim())}`
|
|
30
|
+
: "";
|
|
31
|
+
const behavior = typeof first.behavior === "string" ? first.behavior : "allow";
|
|
32
|
+
return `Yes, and always ${behavior} ${toolName}${ruleContent} ${describeClaudeDestination(first.destination)}`;
|
|
33
|
+
}
|
|
34
|
+
if (first.type === "setMode" && typeof first.mode === "string") {
|
|
35
|
+
return `Yes, and use ${quoteInline(first.mode)} permission mode ${describeClaudeDestination(first.destination)}`;
|
|
36
|
+
}
|
|
37
|
+
return suggestions?.length ? placeholderText : null;
|
|
38
|
+
}
|
|
39
|
+
function describeClaudeDestination(destination) {
|
|
40
|
+
switch (destination) {
|
|
41
|
+
case "session":
|
|
42
|
+
return "for this session";
|
|
43
|
+
case "projectSettings":
|
|
44
|
+
return "for this project";
|
|
45
|
+
case "localSettings":
|
|
46
|
+
return "in local settings";
|
|
47
|
+
case "userSettings":
|
|
48
|
+
return "in user settings";
|
|
49
|
+
case "cliArg":
|
|
50
|
+
return "from CLI settings";
|
|
51
|
+
default:
|
|
52
|
+
return "for this project";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function quoteInline(value) {
|
|
56
|
+
return `\`${String(value)}\``;
|
|
57
|
+
}
|
|
58
|
+
function joinQuoted(values) {
|
|
59
|
+
return values.map((value) => quoteInline(value)).join(", ");
|
|
60
|
+
}
|
|
61
|
+
export class ClaudeSession {
|
|
62
|
+
sessionId;
|
|
63
|
+
lockedCwd;
|
|
64
|
+
emit;
|
|
65
|
+
_busy = false;
|
|
66
|
+
busyEmitted = false;
|
|
67
|
+
turnStartMs = 0;
|
|
68
|
+
runningInputTokens = 0;
|
|
69
|
+
runningOutputTokens = 0;
|
|
70
|
+
currentMsgOutputTokens = 0;
|
|
71
|
+
statsTimer = null;
|
|
72
|
+
queryHandle = null;
|
|
73
|
+
runningQuery = null;
|
|
74
|
+
pendingPermissions = [];
|
|
75
|
+
pendingQuestions = [];
|
|
76
|
+
alwaysAllowedTools = new Set();
|
|
77
|
+
pendingToolCalls = new Map();
|
|
78
|
+
/** Tracks the type of the currently-open content block ("thinking" | "text" | null). */
|
|
79
|
+
currentBlockType = null;
|
|
80
|
+
idResolve = null;
|
|
81
|
+
idPromise = null;
|
|
82
|
+
idReadyCallbacks = [];
|
|
83
|
+
promptQueue = [];
|
|
84
|
+
constructor(emit) {
|
|
85
|
+
this.emit = emit;
|
|
86
|
+
}
|
|
87
|
+
get id() {
|
|
88
|
+
return this.sessionId;
|
|
89
|
+
}
|
|
90
|
+
get cwd() {
|
|
91
|
+
return this.lockedCwd;
|
|
92
|
+
}
|
|
93
|
+
get busy() {
|
|
94
|
+
return this._busy;
|
|
95
|
+
}
|
|
96
|
+
get alive() {
|
|
97
|
+
return !!this.sessionId;
|
|
98
|
+
}
|
|
99
|
+
/** Tracked session status: 'awaiting' if there's an unanswered permission
|
|
100
|
+
* request or user question, otherwise 'busy'/'idle' based on _busy. */
|
|
101
|
+
get status() {
|
|
102
|
+
if (this.pendingPermissions.length > 0 ||
|
|
103
|
+
this.pendingQuestions.length > 0) {
|
|
104
|
+
return "awaiting";
|
|
105
|
+
}
|
|
106
|
+
return this._busy ? "busy" : "idle";
|
|
107
|
+
}
|
|
108
|
+
waitForId(timeoutMs = 10000) {
|
|
109
|
+
if (this.sessionId)
|
|
110
|
+
return Promise.resolve(this.sessionId);
|
|
111
|
+
if (!this.idPromise) {
|
|
112
|
+
this.idPromise = new Promise((resolve) => {
|
|
113
|
+
this.idResolve = resolve;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return Promise.race([
|
|
117
|
+
this.idPromise,
|
|
118
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Timed out waiting for session ID")), timeoutMs)),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
onIdReady(cb) {
|
|
122
|
+
if (this.sessionId) {
|
|
123
|
+
cb(this.sessionId);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.idReadyCallbacks.push(cb);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
setSessionId(id) {
|
|
130
|
+
// Don't overwrite an existing session ID. SDK hook messages (e.g.
|
|
131
|
+
// SessionStart:resume) carry their own ephemeral session_id which would
|
|
132
|
+
// otherwise clobber the real conversation session ID.
|
|
133
|
+
if (this.sessionId)
|
|
134
|
+
return;
|
|
135
|
+
this.sessionId = id;
|
|
136
|
+
if (this.idResolve) {
|
|
137
|
+
this.idResolve(id);
|
|
138
|
+
this.idResolve = null;
|
|
139
|
+
}
|
|
140
|
+
for (const cb of this.idReadyCallbacks)
|
|
141
|
+
cb(id);
|
|
142
|
+
this.idReadyCallbacks = [];
|
|
143
|
+
}
|
|
144
|
+
get runningStats() {
|
|
145
|
+
return {
|
|
146
|
+
durationMs: this.turnStartMs ? Date.now() - this.turnStartMs : 0,
|
|
147
|
+
inputTokens: this.runningInputTokens,
|
|
148
|
+
outputTokens: this.runningOutputTokens + this.currentMsgOutputTokens,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
send(msg) {
|
|
152
|
+
this.emit(this.sessionId ?? "", msg);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Wait for a user response with timeout and SDK abort signal as fallbacks.
|
|
156
|
+
* Multiple requests can be pending (e.g. subagents); resolved FIFO.
|
|
157
|
+
*/
|
|
158
|
+
waitForUser(queue, signal, timeoutMs, defaultValue) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
let settled = false;
|
|
161
|
+
const entry = (value) => finish(value);
|
|
162
|
+
const finish = (value) => {
|
|
163
|
+
if (settled)
|
|
164
|
+
return;
|
|
165
|
+
settled = true;
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
signal.removeEventListener("abort", onAbort);
|
|
168
|
+
const idx = queue.indexOf(entry);
|
|
169
|
+
if (idx !== -1)
|
|
170
|
+
queue.splice(idx, 1);
|
|
171
|
+
resolve(value);
|
|
172
|
+
};
|
|
173
|
+
const timer = setTimeout(() => finish(defaultValue), timeoutMs);
|
|
174
|
+
const onAbort = () => finish(defaultValue);
|
|
175
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
176
|
+
queue.push(entry);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
respondPermission(decision) {
|
|
180
|
+
this.pendingPermissions.shift()?.({
|
|
181
|
+
allow: decision === "allow" || decision === "allowAlways",
|
|
182
|
+
allowAlways: decision === "allowAlways",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
respondQuestion(answer) {
|
|
186
|
+
this.pendingQuestions.shift()?.(answer);
|
|
187
|
+
}
|
|
188
|
+
stopStatsTimer() {
|
|
189
|
+
if (this.statsTimer) {
|
|
190
|
+
clearInterval(this.statsTimer);
|
|
191
|
+
this.statsTimer = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
emitRunningStats() {
|
|
195
|
+
if (!this._busy) {
|
|
196
|
+
this.stopStatsTimer();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const stats = this.runningStats;
|
|
200
|
+
this.send({
|
|
201
|
+
type: "running_stats",
|
|
202
|
+
durationMs: stats.durationMs,
|
|
203
|
+
inputTokens: stats.inputTokens,
|
|
204
|
+
outputTokens: stats.outputTokens,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async start(sessionId, cwd) {
|
|
208
|
+
await this.close();
|
|
209
|
+
if (sessionId) {
|
|
210
|
+
this.setSessionId(sessionId);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.sessionId = undefined;
|
|
214
|
+
}
|
|
215
|
+
const requestedCwd = cwd ?? process.cwd();
|
|
216
|
+
if (existsSync(requestedCwd)) {
|
|
217
|
+
this.lockedCwd = requestedCwd;
|
|
218
|
+
}
|
|
219
|
+
else if (sessionId) {
|
|
220
|
+
console.warn(`[session] CWD "${requestedCwd}" not found for session ${sessionId}, will attempt resume anyway`);
|
|
221
|
+
this.lockedCwd = requestedCwd;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
this.lockedCwd = process.cwd();
|
|
225
|
+
console.warn(`[session] CWD "${requestedCwd}" not found, falling back to "${this.lockedCwd}"`);
|
|
226
|
+
}
|
|
227
|
+
console.log(`[session] Session configured: resume=${sessionId ?? "new"}, cwd=${this.lockedCwd}`);
|
|
228
|
+
}
|
|
229
|
+
async run(prompt) {
|
|
230
|
+
console.log(`[session] run: alive=${this.alive}, busy=${this._busy}`);
|
|
231
|
+
if (this._busy) {
|
|
232
|
+
throw new Error("Session is busy");
|
|
233
|
+
}
|
|
234
|
+
this._busy = true;
|
|
235
|
+
this.busyEmitted = false;
|
|
236
|
+
this.currentBlockType = null;
|
|
237
|
+
this.turnStartMs = Date.now();
|
|
238
|
+
this.runningInputTokens = 0;
|
|
239
|
+
this.runningOutputTokens = 0;
|
|
240
|
+
this.currentMsgOutputTokens = 0;
|
|
241
|
+
this.stopStatsTimer();
|
|
242
|
+
this.statsTimer = setInterval(() => this.emitRunningStats(), 10000);
|
|
243
|
+
console.log(`[session] Launching query: resume=${this.sessionId ?? "new"}, cwd=${this.lockedCwd}`);
|
|
244
|
+
const q = query({
|
|
245
|
+
prompt,
|
|
246
|
+
options: {
|
|
247
|
+
resume: this.sessionId,
|
|
248
|
+
cwd: this.lockedCwd,
|
|
249
|
+
model: "claude-opus-4-6",
|
|
250
|
+
allowedTools: [
|
|
251
|
+
"Read",
|
|
252
|
+
"Edit",
|
|
253
|
+
"Glob",
|
|
254
|
+
"Grep",
|
|
255
|
+
"Agent",
|
|
256
|
+
"WebSearch",
|
|
257
|
+
"WebFetch",
|
|
258
|
+
"TaskOutput",
|
|
259
|
+
"ExitPlanMode",
|
|
260
|
+
"ListMcpResources",
|
|
261
|
+
"ReadMcpResource",
|
|
262
|
+
],
|
|
263
|
+
permissionMode: "acceptEdits",
|
|
264
|
+
canUseTool: (toolName, input, opts) => this.handleCanUseTool(toolName, input, opts),
|
|
265
|
+
hooks: {
|
|
266
|
+
Notification: [
|
|
267
|
+
{
|
|
268
|
+
hooks: [
|
|
269
|
+
async (input, _toolUseID, _options) => {
|
|
270
|
+
const title = (input.title || "Notice");
|
|
271
|
+
const message = (input.message || "");
|
|
272
|
+
console.log(`[session] Notification: ${title} — ${message}`);
|
|
273
|
+
this.send({
|
|
274
|
+
type: "notification",
|
|
275
|
+
title,
|
|
276
|
+
message,
|
|
277
|
+
});
|
|
278
|
+
return {};
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
includePartialMessages: true,
|
|
285
|
+
maxTurns: 50,
|
|
286
|
+
settingSources: ["user", "project"],
|
|
287
|
+
stderr: (data) => {
|
|
288
|
+
const trimmed = data.trim();
|
|
289
|
+
if (trimmed)
|
|
290
|
+
console.error(`[cli stderr] ${trimmed}`);
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
this.queryHandle = q;
|
|
295
|
+
this.runningQuery = (async () => {
|
|
296
|
+
try {
|
|
297
|
+
for await (const msg of q) {
|
|
298
|
+
this.processAndEmit(msg);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
if (err.name !== "AbortError") {
|
|
303
|
+
console.error(`[session] query error: ${err.message}`);
|
|
304
|
+
this.send({ type: "error", message: err.message });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
console.log("[session] Query process ended, cleaning up");
|
|
309
|
+
this._busy = false;
|
|
310
|
+
this.queryHandle = null;
|
|
311
|
+
this.stopStatsTimer();
|
|
312
|
+
if (this.promptQueue.length > 0) {
|
|
313
|
+
this.dispatchNext();
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this.send({
|
|
317
|
+
type: "status",
|
|
318
|
+
state: "idle",
|
|
319
|
+
sessionId: this.sessionId,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
})();
|
|
324
|
+
}
|
|
325
|
+
/** Queue a prompt to run when the session becomes idle. */
|
|
326
|
+
enqueue(prompt) {
|
|
327
|
+
this.promptQueue.push(prompt);
|
|
328
|
+
console.log(`[session] Enqueued prompt (queue size: ${this.promptQueue.length})`);
|
|
329
|
+
}
|
|
330
|
+
dispatchNext() {
|
|
331
|
+
if (this.promptQueue.length === 0 || this._busy)
|
|
332
|
+
return;
|
|
333
|
+
const next = this.promptQueue.shift();
|
|
334
|
+
console.log(`[session] Dispatching queued prompt (remaining: ${this.promptQueue.length})`);
|
|
335
|
+
this.run(next).catch((err) => {
|
|
336
|
+
console.error(`[session] Failed to dispatch queued prompt: ${err.message}`);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
interrupt() {
|
|
340
|
+
this.queryHandle?.interrupt().catch(() => { });
|
|
341
|
+
}
|
|
342
|
+
async close() {
|
|
343
|
+
const had = {
|
|
344
|
+
query: !!this.queryHandle,
|
|
345
|
+
running: !!this.runningQuery,
|
|
346
|
+
};
|
|
347
|
+
console.log(`[session] close: session=${this.sessionId ?? "none"} had=${JSON.stringify(had)}`);
|
|
348
|
+
this.stopStatsTimer();
|
|
349
|
+
this.pendingPermissions.length = 0;
|
|
350
|
+
this.pendingQuestions.length = 0;
|
|
351
|
+
this.promptQueue.length = 0;
|
|
352
|
+
if (this.queryHandle) {
|
|
353
|
+
this.queryHandle.close();
|
|
354
|
+
this.queryHandle = null;
|
|
355
|
+
}
|
|
356
|
+
if (this.runningQuery) {
|
|
357
|
+
await this.runningQuery.catch(() => { });
|
|
358
|
+
this.runningQuery = null;
|
|
359
|
+
}
|
|
360
|
+
this._busy = false;
|
|
361
|
+
this.alwaysAllowedTools.clear();
|
|
362
|
+
console.log("[session] close: done");
|
|
363
|
+
}
|
|
364
|
+
async reset(cwd) {
|
|
365
|
+
await this.close();
|
|
366
|
+
this.sessionId = undefined;
|
|
367
|
+
this.lockedCwd = cwd;
|
|
368
|
+
}
|
|
369
|
+
// ── Tool handlers ──────────────────────────────────
|
|
370
|
+
async handleCanUseTool(toolName, input, options) {
|
|
371
|
+
console.log(`[session] canUseTool: ${toolName}`);
|
|
372
|
+
if (toolName === "AskUserQuestion") {
|
|
373
|
+
return this.handleAskUserQuestion(input, options.toolUseID, options.signal);
|
|
374
|
+
}
|
|
375
|
+
if (this.alwaysAllowedTools.has(toolName)) {
|
|
376
|
+
console.log(`[session] Auto-approve (allowAlways): ${toolName}`);
|
|
377
|
+
return { behavior: "allow", updatedInput: input };
|
|
378
|
+
}
|
|
379
|
+
const PERMISSION_TOOLS = new Set([
|
|
380
|
+
"KillShell",
|
|
381
|
+
"Config",
|
|
382
|
+
"Mcp",
|
|
383
|
+
"RemoteTrigger",
|
|
384
|
+
]);
|
|
385
|
+
if (PERMISSION_TOOLS.has(toolName)) {
|
|
386
|
+
return this.handlePermissionConfirm(toolName, input, options.toolUseID, options.signal, options.suggestions);
|
|
387
|
+
}
|
|
388
|
+
if (toolName === "Bash") {
|
|
389
|
+
const cmd = String(input.command || "").trim();
|
|
390
|
+
if (/^\s*(ls|cat|head|tail|wc|pwd|echo|printf|date|whoami|which|where|type|file|stat|du|df|env|printenv|uname|hostname|id|git\s+(status|log|diff|branch|show|remote|rev-parse))\b/.test(cmd)) {
|
|
391
|
+
return { behavior: "allow", updatedInput: input };
|
|
392
|
+
}
|
|
393
|
+
return this.handlePermissionConfirm(toolName, input, options.toolUseID, options.signal, options.suggestions);
|
|
394
|
+
}
|
|
395
|
+
if (toolName === "TodoWrite") {
|
|
396
|
+
const todos = input.todos || [];
|
|
397
|
+
const total = todos.length;
|
|
398
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
399
|
+
const active = todos.find((t) => t.status === "in_progress");
|
|
400
|
+
const current = active
|
|
401
|
+
? active.content || active.activeForm || ""
|
|
402
|
+
: completed === total && total > 0
|
|
403
|
+
? "All done"
|
|
404
|
+
: "";
|
|
405
|
+
if (total > 0) {
|
|
406
|
+
this.send({
|
|
407
|
+
type: "task_progress",
|
|
408
|
+
completed,
|
|
409
|
+
total,
|
|
410
|
+
current,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return { behavior: "allow", updatedInput: input };
|
|
414
|
+
}
|
|
415
|
+
if (toolName === "TaskUpdate") {
|
|
416
|
+
const status = input.status;
|
|
417
|
+
const subject = input.subject || input.description || "";
|
|
418
|
+
if (status && subject) {
|
|
419
|
+
console.log(`[session] TaskUpdate: ${status} "${subject}"`);
|
|
420
|
+
}
|
|
421
|
+
return { behavior: "allow", updatedInput: input };
|
|
422
|
+
}
|
|
423
|
+
console.log(`[session] canUseTool auto-approve: ${toolName} input_keys=${Object.keys(input).join(",")}`);
|
|
424
|
+
return { behavior: "allow", updatedInput: input };
|
|
425
|
+
}
|
|
426
|
+
async handleAskUserQuestion(toolInput, toolUseID, signal) {
|
|
427
|
+
const questions = toolInput.questions || [];
|
|
428
|
+
console.log(`[session] User question request: toolUseId=${toolUseID} questions=${questions.length}`);
|
|
429
|
+
debugLog("claude-session", "question request", JSON.stringify(questions));
|
|
430
|
+
this.send({
|
|
431
|
+
type: "user_question",
|
|
432
|
+
questions: questions.map((q) => ({
|
|
433
|
+
question: q.question || "",
|
|
434
|
+
header: q.header || "",
|
|
435
|
+
options: (q.options || []).map((o) => ({
|
|
436
|
+
label: o.label || "",
|
|
437
|
+
description: o.description || "",
|
|
438
|
+
preview: o.preview || "",
|
|
439
|
+
})),
|
|
440
|
+
})),
|
|
441
|
+
toolUseId: toolUseID,
|
|
442
|
+
});
|
|
443
|
+
const answer = await this.waitForUser(this.pendingQuestions, signal, 120000, "skip");
|
|
444
|
+
let answers = {};
|
|
445
|
+
try {
|
|
446
|
+
answers = JSON.parse(answer);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
for (const q of questions) {
|
|
450
|
+
answers[q.question || q.header || ""] = answer;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
this.send({ type: "question_answer", answers });
|
|
454
|
+
return {
|
|
455
|
+
behavior: "allow",
|
|
456
|
+
updatedInput: {
|
|
457
|
+
questions: toolInput.questions,
|
|
458
|
+
answers,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
async handlePermissionConfirm(toolName, toolInput, toolUseID, signal, suggestions) {
|
|
463
|
+
const description = summarizeClaudeToolCall(toolName, toolInput);
|
|
464
|
+
let detail = "";
|
|
465
|
+
if (toolInput.command)
|
|
466
|
+
detail = String(toolInput.command).slice(0, 200);
|
|
467
|
+
else if (toolInput.file_path)
|
|
468
|
+
detail = String(toolInput.file_path).slice(0, 200);
|
|
469
|
+
else if (toolInput.url)
|
|
470
|
+
detail = String(toolInput.url).slice(0, 200);
|
|
471
|
+
else if (toolInput.prompt)
|
|
472
|
+
detail = String(toolInput.prompt).slice(0, 200);
|
|
473
|
+
else if (toolInput.query)
|
|
474
|
+
detail = String(toolInput.query).slice(0, 200);
|
|
475
|
+
else if (toolInput.content)
|
|
476
|
+
detail = String(toolInput.content).slice(0, 100) + "...";
|
|
477
|
+
console.log(`[session] Permission request: ${toolName} desc="${description}" detail="${detail.slice(0, 80)}"`);
|
|
478
|
+
debugLog("claude-session", `permission request ${toolName}`, JSON.stringify({
|
|
479
|
+
toolName,
|
|
480
|
+
toolUseID,
|
|
481
|
+
description,
|
|
482
|
+
detail,
|
|
483
|
+
suggestions: suggestions ?? null,
|
|
484
|
+
toolInput,
|
|
485
|
+
}));
|
|
486
|
+
const permissionOptions = buildClaudePermissionOptions(suggestions, description);
|
|
487
|
+
this.send({
|
|
488
|
+
type: "permission_request",
|
|
489
|
+
toolName,
|
|
490
|
+
description,
|
|
491
|
+
detail,
|
|
492
|
+
toolUseId: toolUseID,
|
|
493
|
+
options: permissionOptions,
|
|
494
|
+
suggestions: suggestions ?? null,
|
|
495
|
+
});
|
|
496
|
+
const result = await this.waitForUser(this.pendingPermissions, signal, 60000, { allow: false, allowAlways: false });
|
|
497
|
+
let sdkDecision;
|
|
498
|
+
if (result.allowAlways) {
|
|
499
|
+
this.alwaysAllowedTools.add(toolName);
|
|
500
|
+
sdkDecision = "always";
|
|
501
|
+
}
|
|
502
|
+
else if (result.allow) {
|
|
503
|
+
sdkDecision = "allowed";
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
sdkDecision = "denied";
|
|
507
|
+
}
|
|
508
|
+
this.send({
|
|
509
|
+
type: "permission_result",
|
|
510
|
+
toolName,
|
|
511
|
+
summary: description,
|
|
512
|
+
decision: sdkDecision,
|
|
513
|
+
});
|
|
514
|
+
if (result.allow) {
|
|
515
|
+
return {
|
|
516
|
+
behavior: "allow",
|
|
517
|
+
updatedInput: toolInput,
|
|
518
|
+
updatedPermissions: result.allowAlways ? suggestions : undefined,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return { behavior: "deny", message: "Denied by user" };
|
|
522
|
+
}
|
|
523
|
+
// ── Message processing ─────────────────────────────
|
|
524
|
+
processAndEmit(msg) {
|
|
525
|
+
debugLog("claude-sdk", JSON.stringify(msg));
|
|
526
|
+
if ("session_id" in msg && msg.session_id) {
|
|
527
|
+
this.setSessionId(msg.session_id);
|
|
528
|
+
}
|
|
529
|
+
// First message of the query — session ID is now known, emit busy
|
|
530
|
+
if (!this.busyEmitted) {
|
|
531
|
+
this.busyEmitted = true;
|
|
532
|
+
this.send({
|
|
533
|
+
type: "status",
|
|
534
|
+
state: "busy",
|
|
535
|
+
sessionId: this.sessionId,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
switch (msg.type) {
|
|
539
|
+
case "stream_event":
|
|
540
|
+
this.processStreamEvent(msg);
|
|
541
|
+
break;
|
|
542
|
+
case "assistant":
|
|
543
|
+
this.processAssistant(msg);
|
|
544
|
+
break;
|
|
545
|
+
case "user":
|
|
546
|
+
this.processUser(msg);
|
|
547
|
+
break;
|
|
548
|
+
case "result":
|
|
549
|
+
this.emitResult(msg);
|
|
550
|
+
break;
|
|
551
|
+
case "system":
|
|
552
|
+
this.processSystem(msg);
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
processSystem(msg) {
|
|
557
|
+
if (msg.subtype === "api_retry") {
|
|
558
|
+
const attempt = msg.attempt ?? 0;
|
|
559
|
+
const maxRetries = msg.max_retries ?? 0;
|
|
560
|
+
const delayMs = msg.retry_delay_ms ?? 0;
|
|
561
|
+
const status = msg.error_status;
|
|
562
|
+
console.log(`[session] API retry: attempt=${attempt}/${maxRetries} delay=${delayMs}ms status=${status}`);
|
|
563
|
+
this.send({
|
|
564
|
+
type: "notification",
|
|
565
|
+
title: "API Retry",
|
|
566
|
+
message: `Retrying (${attempt}/${maxRetries})${status ? `, HTTP ${status}` : ""}...`,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
processStreamEvent(msg) {
|
|
571
|
+
if (msg.type !== "stream_event")
|
|
572
|
+
return;
|
|
573
|
+
const event = msg.event;
|
|
574
|
+
if (!event)
|
|
575
|
+
return;
|
|
576
|
+
// Track content block start/stop and emit sub-status events
|
|
577
|
+
if (event.type === "content_block_start") {
|
|
578
|
+
const blockType = event.content_block?.type;
|
|
579
|
+
if (blockType === "thinking" || blockType === "text") {
|
|
580
|
+
this.currentBlockType = blockType;
|
|
581
|
+
const state = blockType === "thinking" ? "think_start" : "text_start";
|
|
582
|
+
this.send({
|
|
583
|
+
type: "status",
|
|
584
|
+
state,
|
|
585
|
+
sessionId: this.sessionId,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (event.type === "content_block_stop" && this.currentBlockType) {
|
|
590
|
+
const state = this.currentBlockType === "thinking" ? "think_end" : "text_end";
|
|
591
|
+
this.currentBlockType = null;
|
|
592
|
+
this.send({ type: "status", state, sessionId: this.sessionId });
|
|
593
|
+
}
|
|
594
|
+
if (event.type === "content_block_delta" &&
|
|
595
|
+
event.delta?.type === "text_delta") {
|
|
596
|
+
this.send({ type: "text_delta", text: event.delta.text });
|
|
597
|
+
}
|
|
598
|
+
if (event.type === "content_block_start" &&
|
|
599
|
+
event.content_block?.type === "tool_use") {
|
|
600
|
+
this.send({
|
|
601
|
+
type: "tool_start",
|
|
602
|
+
name: event.content_block.name,
|
|
603
|
+
toolId: event.content_block.id,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (event.type === "message_start" && event.message?.usage) {
|
|
607
|
+
this.runningOutputTokens += this.currentMsgOutputTokens;
|
|
608
|
+
this.currentMsgOutputTokens = 0;
|
|
609
|
+
const usage = event.message.usage;
|
|
610
|
+
const input = usage.input_tokens ?? 0;
|
|
611
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
612
|
+
const cacheCreate = usage.cache_creation_input_tokens ?? 0;
|
|
613
|
+
this.runningInputTokens += input + cacheRead + cacheCreate;
|
|
614
|
+
console.log(`[session] API call tokens: input=${input} cache_read=${cacheRead} cache_create=${cacheCreate} total_in=${input + cacheRead + cacheCreate} (running: in=${this.runningInputTokens} out=${this.runningOutputTokens})`);
|
|
615
|
+
}
|
|
616
|
+
if (event.type === "message_delta" && event.usage) {
|
|
617
|
+
this.currentMsgOutputTokens = event.usage.output_tokens ?? 0;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
processAssistant(msg) {
|
|
621
|
+
if (msg.type !== "assistant")
|
|
622
|
+
return;
|
|
623
|
+
const content = msg.message?.content;
|
|
624
|
+
if (!Array.isArray(content))
|
|
625
|
+
return;
|
|
626
|
+
for (const block of content) {
|
|
627
|
+
if (block.type === "tool_use") {
|
|
628
|
+
this.pendingToolCalls.set(block.id, {
|
|
629
|
+
name: block.name,
|
|
630
|
+
input: block.input,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
processUser(msg) {
|
|
636
|
+
if (msg.type !== "user")
|
|
637
|
+
return;
|
|
638
|
+
const content = msg.message?.content;
|
|
639
|
+
if (!Array.isArray(content))
|
|
640
|
+
return;
|
|
641
|
+
for (const block of content) {
|
|
642
|
+
if (block.type === "tool_result") {
|
|
643
|
+
const toolId = block.tool_use_id;
|
|
644
|
+
const pending = this.pendingToolCalls.get(toolId);
|
|
645
|
+
if (!pending)
|
|
646
|
+
continue;
|
|
647
|
+
this.pendingToolCalls.delete(toolId);
|
|
648
|
+
let output;
|
|
649
|
+
if (typeof block.content === "string") {
|
|
650
|
+
output = block.content;
|
|
651
|
+
}
|
|
652
|
+
else if (Array.isArray(block.content)) {
|
|
653
|
+
output = block.content
|
|
654
|
+
.filter((b) => b.type === "text")
|
|
655
|
+
.map((b) => b.text)
|
|
656
|
+
.join("\n");
|
|
657
|
+
}
|
|
658
|
+
this.send({
|
|
659
|
+
type: "tool_end",
|
|
660
|
+
name: pending.name,
|
|
661
|
+
toolId,
|
|
662
|
+
summary: summarizeClaudeToolCall(pending.name, pending.input),
|
|
663
|
+
detail: {
|
|
664
|
+
input: pending.input,
|
|
665
|
+
output,
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
emitResult(msg) {
|
|
672
|
+
this.stopStatsTimer();
|
|
673
|
+
let inputTokens = 0;
|
|
674
|
+
let outputTokens = 0;
|
|
675
|
+
if (msg.modelUsage) {
|
|
676
|
+
for (const m of Object.values(msg.modelUsage)) {
|
|
677
|
+
inputTokens += m.inputTokens ?? 0;
|
|
678
|
+
outputTokens += m.outputTokens ?? 0;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
let resultText = msg.result ?? "";
|
|
682
|
+
if (msg.subtype !== "success") {
|
|
683
|
+
const errors = msg.errors?.join("\n") ?? "";
|
|
684
|
+
console.error(`[session] Result error: subtype=${msg.subtype} terminal_reason=${msg.terminal_reason} errors=${JSON.stringify(msg.errors)}`);
|
|
685
|
+
if (msg.subtype === "error_during_execution" &&
|
|
686
|
+
msg.terminal_reason === "aborted_streaming") {
|
|
687
|
+
resultText = "Interrupted by user";
|
|
688
|
+
}
|
|
689
|
+
else
|
|
690
|
+
switch (msg.subtype) {
|
|
691
|
+
case "error_max_turns":
|
|
692
|
+
resultText =
|
|
693
|
+
errors ||
|
|
694
|
+
`Reached max turns limit (${msg.num_turns ?? 0} turns). Try breaking the task into smaller steps.`;
|
|
695
|
+
break;
|
|
696
|
+
case "error_max_budget_usd":
|
|
697
|
+
resultText = errors || "Session budget exhausted.";
|
|
698
|
+
break;
|
|
699
|
+
default:
|
|
700
|
+
resultText = errors;
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
console.log(`[session] Result: subtype=${msg.subtype} turns=${msg.num_turns ?? 0} cost=$${(msg.total_cost_usd ?? 0).toFixed(4)} input=${inputTokens} output=${outputTokens}`);
|
|
705
|
+
this.send({
|
|
706
|
+
type: "result",
|
|
707
|
+
success: msg.subtype === "success",
|
|
708
|
+
text: resultText,
|
|
709
|
+
sessionId: msg.session_id ?? this.sessionId ?? "",
|
|
710
|
+
costUsd: msg.total_cost_usd ?? 0,
|
|
711
|
+
provider: "claude",
|
|
712
|
+
turns: msg.num_turns ?? 0,
|
|
713
|
+
durationMs: msg.duration_ms ?? 0,
|
|
714
|
+
inputTokens,
|
|
715
|
+
outputTokens,
|
|
716
|
+
});
|
|
717
|
+
// idle is emitted by the finally block when the query process ends
|
|
718
|
+
}
|
|
719
|
+
}
|