@exreve/exk 1.0.61 → 1.0.63
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/dist/cli/agentBackend.js +12 -0
- package/dist/cli/agentSession.js +253 -685
- package/dist/cli/claudeBackend.js +535 -0
- package/dist/cli/moduleMcpServer.js +50 -311
- package/dist/cli/piBackend.js +384 -0
- package/dist/cli/sessionHandlers.js +3 -2
- package/dist/cli/sharedTools.js +259 -0
- package/dist/ttc-cli.tar.gz +0 -0
- package/package.json +2 -1
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Agent SDK Backend
|
|
3
|
+
*
|
|
4
|
+
* Implements AgentBackend using @anthropic-ai/claude-agent-sdk.
|
|
5
|
+
* This is the original backend — all Claude SDK-specific code lives here
|
|
6
|
+
* so the queue manager (agentSession.ts) stays SDK-agnostic.
|
|
7
|
+
*/
|
|
8
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
9
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
10
|
+
import { execSync, spawn } from 'child_process';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
import { createModuleMcpServer } from './moduleMcpServer.js';
|
|
15
|
+
// ============ Claude Executable Resolution ============
|
|
16
|
+
function resolveSdkCliPath() {
|
|
17
|
+
try {
|
|
18
|
+
const req = typeof globalThis.require === 'function'
|
|
19
|
+
? globalThis.require
|
|
20
|
+
: createRequire(import.meta.url);
|
|
21
|
+
const pkgPath = req.resolve('@anthropic-ai/claude-agent-sdk/package.json');
|
|
22
|
+
const cliPath = path.join(path.dirname(pkgPath), 'cli.js');
|
|
23
|
+
return existsSync(cliPath) ? cliPath : undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const CACHED_CLAUDE_PATH = (() => {
|
|
30
|
+
const envPath = process.env.TTC_CLAUDE_PATH;
|
|
31
|
+
if (envPath)
|
|
32
|
+
return envPath;
|
|
33
|
+
const sdkPath = resolveSdkCliPath();
|
|
34
|
+
if (sdkPath)
|
|
35
|
+
return sdkPath;
|
|
36
|
+
const localPath = path.join(os.homedir(), '.local', 'bin', 'claude');
|
|
37
|
+
if (existsSync(localPath))
|
|
38
|
+
return localPath;
|
|
39
|
+
return undefined;
|
|
40
|
+
})();
|
|
41
|
+
// ============ Tool Name Detection ============
|
|
42
|
+
// (Moved from agentSession.ts — these are Claude SDK–specific heuristics)
|
|
43
|
+
function extractToolName(toolResult) {
|
|
44
|
+
if (!toolResult || typeof toolResult !== 'object')
|
|
45
|
+
return 'unknown';
|
|
46
|
+
const r = toolResult;
|
|
47
|
+
if (r.file && typeof r.file === 'object'
|
|
48
|
+
&& ['text', 'image', 'notebook', 'pdf', 'parts', 'file_unchanged'].includes(r.type)) {
|
|
49
|
+
return 'Read';
|
|
50
|
+
}
|
|
51
|
+
if (r.filePath && r.oldString !== undefined && r.newString !== undefined
|
|
52
|
+
&& Array.isArray(r.structuredPatch)) {
|
|
53
|
+
return 'Edit';
|
|
54
|
+
}
|
|
55
|
+
if (r.filePath && (r.type === 'create' || r.type === 'update')
|
|
56
|
+
&& r.content !== undefined) {
|
|
57
|
+
return 'Write';
|
|
58
|
+
}
|
|
59
|
+
if (typeof r.numFiles === 'number' && Array.isArray(r.filenames)
|
|
60
|
+
&& r.mode !== undefined) {
|
|
61
|
+
return 'Grep';
|
|
62
|
+
}
|
|
63
|
+
if (typeof r.numFiles === 'number' && Array.isArray(r.filenames)
|
|
64
|
+
&& typeof r.durationMs === 'number' && r.truncated !== undefined) {
|
|
65
|
+
return 'Glob';
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(r.oldTodos) && Array.isArray(r.newTodos)) {
|
|
68
|
+
return 'TodoWrite';
|
|
69
|
+
}
|
|
70
|
+
if (r.stdout !== undefined || r.stderr !== undefined) {
|
|
71
|
+
return 'Bash';
|
|
72
|
+
}
|
|
73
|
+
if (typeof r.query === 'string' && Array.isArray(r.results)) {
|
|
74
|
+
return 'WebSearch';
|
|
75
|
+
}
|
|
76
|
+
if (typeof r.url === 'string' && typeof r.result === 'string'
|
|
77
|
+
&& typeof r.code === 'number') {
|
|
78
|
+
return 'WebFetch';
|
|
79
|
+
}
|
|
80
|
+
if (r.content && typeof r.content === 'string' && r.type === 'text') {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(r.content);
|
|
83
|
+
if (parsed._type === 'send_file')
|
|
84
|
+
return 'send_file';
|
|
85
|
+
}
|
|
86
|
+
catch { /* not JSON */ }
|
|
87
|
+
return 'unknown';
|
|
88
|
+
}
|
|
89
|
+
if (r.agentId && Array.isArray(r.content) && r.status) {
|
|
90
|
+
return 'Task';
|
|
91
|
+
}
|
|
92
|
+
return 'unknown';
|
|
93
|
+
}
|
|
94
|
+
function lookupToolNameFromHistory(messages, toolUseId) {
|
|
95
|
+
if (!toolUseId)
|
|
96
|
+
return null;
|
|
97
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
98
|
+
const msg = messages[i];
|
|
99
|
+
if (msg.role !== 'assistant')
|
|
100
|
+
continue;
|
|
101
|
+
let content = typeof msg.content === 'string' ? null : msg.content;
|
|
102
|
+
if (content && !Array.isArray(content) && Array.isArray(content.content)) {
|
|
103
|
+
content = content.content;
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(content))
|
|
106
|
+
continue;
|
|
107
|
+
const toolUse = content.find((c) => c.type === 'tool_use' && c.id === toolUseId);
|
|
108
|
+
if (toolUse?.name)
|
|
109
|
+
return toolUse.name;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// ============ Claude Backend Class ============
|
|
114
|
+
export class ClaudeBackend {
|
|
115
|
+
id = 'claude';
|
|
116
|
+
/** Tracked child processes for force-kill on emergency stop */
|
|
117
|
+
childProcesses = new Set();
|
|
118
|
+
processGroupId;
|
|
119
|
+
/**
|
|
120
|
+
* Kill all tracked child processes.
|
|
121
|
+
* Called by the queue manager during emergency stop.
|
|
122
|
+
*/
|
|
123
|
+
killProcesses() {
|
|
124
|
+
const isWin = process.platform === 'win32';
|
|
125
|
+
for (const child of this.childProcesses) {
|
|
126
|
+
if (!child.killed) {
|
|
127
|
+
try {
|
|
128
|
+
if (isWin && child.pid) {
|
|
129
|
+
spawn('taskkill', ['/pid', child.pid.toString(), '/f', '/t'], {
|
|
130
|
+
stdio: 'ignore',
|
|
131
|
+
windowsHide: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
child.kill('SIGTERM');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch { /* already dead */ }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Force kill after a brief grace period (sync — caller should handle timing)
|
|
142
|
+
for (const child of this.childProcesses) {
|
|
143
|
+
if (!child.killed) {
|
|
144
|
+
try {
|
|
145
|
+
if (!isWin && child.pid) {
|
|
146
|
+
child.kill('SIGKILL');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch { /* already dead */ }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.childProcesses.clear();
|
|
153
|
+
if (!isWin && this.processGroupId) {
|
|
154
|
+
try {
|
|
155
|
+
process.kill(-this.processGroupId, 'SIGKILL');
|
|
156
|
+
}
|
|
157
|
+
catch { /* already dead */ }
|
|
158
|
+
this.processGroupId = undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get the Claude executable path (for diagnostics).
|
|
163
|
+
*/
|
|
164
|
+
getClaudePath() {
|
|
165
|
+
return CACHED_CLAUDE_PATH;
|
|
166
|
+
}
|
|
167
|
+
async *executePrompt(prompt, config) {
|
|
168
|
+
const { cwd, apiKey, model, env, settings, signal, attachmentDir, routingSessionId, routingPromptId, resumeSessionId } = config;
|
|
169
|
+
// Build MCP server for this query
|
|
170
|
+
const mcpServer = createModuleMcpServer({
|
|
171
|
+
attachmentDir,
|
|
172
|
+
sessionId: routingSessionId,
|
|
173
|
+
promptId: routingPromptId,
|
|
174
|
+
});
|
|
175
|
+
const pathToClaudeCodeExecutable = CACHED_CLAUDE_PATH;
|
|
176
|
+
// Build query options
|
|
177
|
+
const queryOptions = {
|
|
178
|
+
signal,
|
|
179
|
+
cwd,
|
|
180
|
+
apiKey,
|
|
181
|
+
model,
|
|
182
|
+
tools: { type: 'preset', preset: 'claude_code' },
|
|
183
|
+
disallowedTools: ['AskUserQuestion', 'analyze_image'],
|
|
184
|
+
settingSources: ['project'],
|
|
185
|
+
permissionMode: 'bypassPermissions',
|
|
186
|
+
allowDangerouslySkipPermissions: true,
|
|
187
|
+
mcpServers: { 'claude-voice-modules': mcpServer },
|
|
188
|
+
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
189
|
+
spawnClaudeCodeProcess: (spawnOptions) => {
|
|
190
|
+
return this.spawnProcess(spawnOptions, signal);
|
|
191
|
+
},
|
|
192
|
+
env: env || {},
|
|
193
|
+
hooks: {
|
|
194
|
+
PostToolUse: [{
|
|
195
|
+
hooks: [() => ({ continue: true })]
|
|
196
|
+
}],
|
|
197
|
+
Notification: [{
|
|
198
|
+
hooks: [(_notification) => {
|
|
199
|
+
// Notifications are handled as progress events via the stream
|
|
200
|
+
}]
|
|
201
|
+
}],
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
// Create query stream — optionally resume session
|
|
205
|
+
const queryStream = query({
|
|
206
|
+
prompt,
|
|
207
|
+
options: {
|
|
208
|
+
...queryOptions,
|
|
209
|
+
...(settings ? { settings } : {}),
|
|
210
|
+
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Message history for tool name lookup (local to this execution)
|
|
214
|
+
const localMessages = [];
|
|
215
|
+
const toolNameMap = new Map();
|
|
216
|
+
for await (const message of queryStream) {
|
|
217
|
+
// Check abort
|
|
218
|
+
if (signal?.aborted)
|
|
219
|
+
break;
|
|
220
|
+
// ── System init ──
|
|
221
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
222
|
+
const systemMsg = message;
|
|
223
|
+
if (systemMsg.session_id) {
|
|
224
|
+
yield { type: 'init', sessionId: systemMsg.session_id };
|
|
225
|
+
}
|
|
226
|
+
// Also emit as system event
|
|
227
|
+
yield {
|
|
228
|
+
type: 'system',
|
|
229
|
+
raw: systemMsg,
|
|
230
|
+
subtype: systemMsg.subtype,
|
|
231
|
+
};
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// ── Assistant message ──
|
|
235
|
+
if (message.type === 'assistant') {
|
|
236
|
+
const msg = message;
|
|
237
|
+
// Capture session ID
|
|
238
|
+
if (msg.session_id) {
|
|
239
|
+
// Will be used by queue manager for session tracking
|
|
240
|
+
}
|
|
241
|
+
// Store for tool name lookup
|
|
242
|
+
localMessages.push({ role: 'assistant', content: msg.message });
|
|
243
|
+
// Populate toolNameMap
|
|
244
|
+
const toolUses = [];
|
|
245
|
+
if (Array.isArray(msg.message?.content)) {
|
|
246
|
+
for (const block of msg.message.content) {
|
|
247
|
+
if (block.type === 'tool_use' && block.id && block.name) {
|
|
248
|
+
toolNameMap.set(block.id, block.name);
|
|
249
|
+
toolUses.push({ id: block.id, name: block.name });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Detect context window limit error
|
|
254
|
+
const isContextWindowError = msg.error === 'max_output_tokens' && ((typeof msg.message === 'string' && msg.message.includes('context window limit')) ||
|
|
255
|
+
(Array.isArray(msg.message?.content) && msg.message.content.some((c) => typeof c?.text === 'string' && c.text.includes('context window limit'))));
|
|
256
|
+
yield {
|
|
257
|
+
type: 'assistant_message',
|
|
258
|
+
raw: msg.message,
|
|
259
|
+
sdkSessionId: msg.session_id,
|
|
260
|
+
error: msg.error,
|
|
261
|
+
toolUses,
|
|
262
|
+
isContextWindowError,
|
|
263
|
+
};
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// ── Result ──
|
|
267
|
+
if (message.type === 'result') {
|
|
268
|
+
const msg = message;
|
|
269
|
+
let usage;
|
|
270
|
+
if (msg.usage) {
|
|
271
|
+
const u = msg.usage;
|
|
272
|
+
const inputTokens = u.input_tokens || u.inputTokens || 0;
|
|
273
|
+
const outputTokens = u.output_tokens || u.outputTokens || 0;
|
|
274
|
+
usage = {
|
|
275
|
+
inputTokens,
|
|
276
|
+
outputTokens,
|
|
277
|
+
totalCostUsd: msg.total_cost_usd || 0,
|
|
278
|
+
durationMs: msg.duration_ms,
|
|
279
|
+
numTurns: msg.num_turns,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
yield {
|
|
283
|
+
type: 'result',
|
|
284
|
+
raw: msg,
|
|
285
|
+
isError: !!msg.is_error,
|
|
286
|
+
usage,
|
|
287
|
+
};
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// ── User message (tool results) ──
|
|
291
|
+
if (message.type === 'user') {
|
|
292
|
+
const msg = message;
|
|
293
|
+
let toolResult = null;
|
|
294
|
+
let toolUseId = null;
|
|
295
|
+
// Extract tool_use_id from message.content
|
|
296
|
+
if (Array.isArray(msg.message?.content)) {
|
|
297
|
+
const contentBlocks = msg.message.content;
|
|
298
|
+
const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
|
|
299
|
+
if (toolResultBlock?.tool_use_id) {
|
|
300
|
+
toolUseId = toolResultBlock.tool_use_id;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Fallback: parent_tool_use_id
|
|
304
|
+
if (!toolUseId && msg.parent_tool_use_id) {
|
|
305
|
+
toolUseId = msg.parent_tool_use_id;
|
|
306
|
+
}
|
|
307
|
+
// Extract tool result data
|
|
308
|
+
if (msg.tool_use_result) {
|
|
309
|
+
const raw = msg.tool_use_result;
|
|
310
|
+
if (Array.isArray(raw)) {
|
|
311
|
+
const textParts = raw
|
|
312
|
+
.filter((c) => c.type === 'text')
|
|
313
|
+
.map((c) => c.text);
|
|
314
|
+
const rawContent = textParts.join('\n');
|
|
315
|
+
try {
|
|
316
|
+
toolResult = JSON.parse(rawContent);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
toolResult = { content: rawContent, type: 'text' };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (typeof raw === 'object' && raw !== null) {
|
|
323
|
+
toolResult = raw;
|
|
324
|
+
}
|
|
325
|
+
else if (typeof raw === 'string') {
|
|
326
|
+
try {
|
|
327
|
+
toolResult = JSON.parse(raw);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
toolResult = { content: raw, type: 'text' };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Fallback to parsing message.content
|
|
335
|
+
if (!toolResult && Array.isArray(msg.message?.content)) {
|
|
336
|
+
const contentBlocks = msg.message.content;
|
|
337
|
+
const toolResultBlock = contentBlocks.find((c) => c.type === 'tool_result');
|
|
338
|
+
if (toolResultBlock) {
|
|
339
|
+
if (typeof toolResultBlock.content === 'string') {
|
|
340
|
+
try {
|
|
341
|
+
toolResult = JSON.parse(toolResultBlock.content);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
toolResult = { content: toolResultBlock.content, type: 'text' };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (Array.isArray(toolResultBlock.content)) {
|
|
348
|
+
const textParts = toolResultBlock.content
|
|
349
|
+
.filter((c) => c.type === 'text')
|
|
350
|
+
.map((c) => c.text);
|
|
351
|
+
const rawContent = textParts.join('\n');
|
|
352
|
+
try {
|
|
353
|
+
toolResult = JSON.parse(rawContent);
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
toolResult = { content: rawContent, type: 'text' };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
toolResult = toolResultBlock;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (toolResult) {
|
|
365
|
+
// Resolve tool name: map lookup → history scan → heuristic
|
|
366
|
+
const mappedName = toolUseId ? toolNameMap.get(toolUseId) ?? null : null;
|
|
367
|
+
const historyName = mappedName || lookupToolNameFromHistory(localMessages, toolUseId);
|
|
368
|
+
const detectedName = historyName || extractToolName(toolResult);
|
|
369
|
+
yield {
|
|
370
|
+
type: 'tool_result',
|
|
371
|
+
result: toolResult,
|
|
372
|
+
toolName: detectedName,
|
|
373
|
+
toolUseId,
|
|
374
|
+
parentToolUseId: msg.parent_tool_use_id,
|
|
375
|
+
isSynthetic: msg.isSynthetic,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// No tool result — emit as raw user message (rare)
|
|
380
|
+
yield {
|
|
381
|
+
type: 'tool_result',
|
|
382
|
+
result: msg.message,
|
|
383
|
+
toolName: null,
|
|
384
|
+
toolUseId: null,
|
|
385
|
+
parentToolUseId: msg.parent_tool_use_id,
|
|
386
|
+
isSynthetic: msg.isSynthetic,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
// ── System (non-init) ──
|
|
392
|
+
if (message.type === 'system') {
|
|
393
|
+
const sysMsg = message;
|
|
394
|
+
yield {
|
|
395
|
+
type: 'system',
|
|
396
|
+
raw: sysMsg,
|
|
397
|
+
subtype: sysMsg.subtype,
|
|
398
|
+
};
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
// ── Tool progress ──
|
|
402
|
+
if (message.type === 'tool_progress') {
|
|
403
|
+
const msg = message;
|
|
404
|
+
yield {
|
|
405
|
+
type: 'tool_progress',
|
|
406
|
+
raw: msg,
|
|
407
|
+
toolName: msg.tool_name,
|
|
408
|
+
toolUseId: msg.tool_use_id,
|
|
409
|
+
elapsedTimeSeconds: msg.elapsed_time_seconds,
|
|
410
|
+
parentToolUseId: msg.parent_tool_use_id,
|
|
411
|
+
};
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
// ── Stream event ──
|
|
415
|
+
if (message.type === 'stream_event') {
|
|
416
|
+
yield {
|
|
417
|
+
type: 'stream_event',
|
|
418
|
+
raw: message.event || message,
|
|
419
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
420
|
+
};
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// ── Tool use summary ──
|
|
424
|
+
if (message.type === 'tool_use_summary') {
|
|
425
|
+
// Treat as system event for simplicity
|
|
426
|
+
const msg = message;
|
|
427
|
+
yield {
|
|
428
|
+
type: 'system',
|
|
429
|
+
raw: msg,
|
|
430
|
+
subtype: 'tool_use_summary',
|
|
431
|
+
};
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
// ── Rate limit ──
|
|
435
|
+
if (message.type === 'rate_limit_event') {
|
|
436
|
+
yield {
|
|
437
|
+
type: 'rate_limit',
|
|
438
|
+
raw: message,
|
|
439
|
+
};
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// ── Prompt suggestion ──
|
|
443
|
+
if (message.type === 'prompt_suggestion') {
|
|
444
|
+
const msg = message;
|
|
445
|
+
yield {
|
|
446
|
+
type: 'prompt_suggestion',
|
|
447
|
+
suggestion: msg.suggestion || '',
|
|
448
|
+
raw: msg,
|
|
449
|
+
};
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
// ── Auth status ──
|
|
453
|
+
if (message.type === 'auth_status') {
|
|
454
|
+
yield {
|
|
455
|
+
type: 'system',
|
|
456
|
+
raw: message,
|
|
457
|
+
subtype: 'auth_status',
|
|
458
|
+
};
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
// ── Keepalive — ignore ──
|
|
462
|
+
if (message.type === 'keep_alive') {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
// ── Unknown — emit as system event ──
|
|
466
|
+
yield {
|
|
467
|
+
type: 'system',
|
|
468
|
+
raw: message,
|
|
469
|
+
subtype: 'unknown',
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Spawn a Claude Code child process with proper environment.
|
|
475
|
+
*/
|
|
476
|
+
spawnProcess(spawnOptions, signal) {
|
|
477
|
+
const { command, args, cwd, env } = spawnOptions;
|
|
478
|
+
console.log(`[ClaudeBackend] Spawn ANTHROPIC_BASE_URL:`, env?.ANTHROPIC_BASE_URL || '(not set)');
|
|
479
|
+
console.log(`[ClaudeBackend] Spawn ANTHROPIC_API_KEY:`, env?.ANTHROPIC_API_KEY ? '(set)' : '(not set)');
|
|
480
|
+
const hasPathSep = command.includes(path.sep) || command.includes('/') || command.includes('\\');
|
|
481
|
+
if (hasPathSep && !existsSync(command)) {
|
|
482
|
+
throw new Error(`Executable not found at ${command}. Set path with: ttc config --claude-path "<path>" (or TTC_CLAUDE_PATH)`);
|
|
483
|
+
}
|
|
484
|
+
if (cwd && !existsSync(cwd)) {
|
|
485
|
+
try {
|
|
486
|
+
mkdirSync(cwd, { recursive: true });
|
|
487
|
+
}
|
|
488
|
+
catch { /* ignore */ }
|
|
489
|
+
}
|
|
490
|
+
const isWin = process.platform === 'win32';
|
|
491
|
+
const defaultPath = isWin
|
|
492
|
+
? (process.env.Path || process.env.PATH || '')
|
|
493
|
+
: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
494
|
+
const spawnEnv = {
|
|
495
|
+
...env,
|
|
496
|
+
IS_SANDBOX: '1',
|
|
497
|
+
PATH: env.PATH || process.env.PATH || defaultPath,
|
|
498
|
+
...(isWin
|
|
499
|
+
? {
|
|
500
|
+
USERPROFILE: env.USERPROFILE || process.env.USERPROFILE || os.homedir(),
|
|
501
|
+
USERNAME: env.USERNAME || process.env.USERNAME || 'user',
|
|
502
|
+
HOME: env.USERPROFILE || process.env.USERPROFILE || os.homedir(),
|
|
503
|
+
}
|
|
504
|
+
: { HOME: env.HOME || process.env.HOME || os.homedir(), USER: env.USER || process.env.USER || 'user' }),
|
|
505
|
+
};
|
|
506
|
+
// Resolve 'node' if needed
|
|
507
|
+
let effectiveCommand = command;
|
|
508
|
+
if (command === 'node' && !hasPathSep) {
|
|
509
|
+
try {
|
|
510
|
+
const nodePath = execSync('which node', { encoding: 'utf-8', env: spawnEnv }).trim();
|
|
511
|
+
if (nodePath)
|
|
512
|
+
effectiveCommand = nodePath;
|
|
513
|
+
}
|
|
514
|
+
catch { /* fall through */ }
|
|
515
|
+
}
|
|
516
|
+
const child = spawn(effectiveCommand, args, {
|
|
517
|
+
cwd: cwd || process.cwd(),
|
|
518
|
+
stdio: ['pipe', 'pipe', env.DEBUG_CLAUDE_AGENT_SDK ? 'pipe' : 'ignore'],
|
|
519
|
+
signal,
|
|
520
|
+
env: spawnEnv,
|
|
521
|
+
windowsHide: true,
|
|
522
|
+
detached: !isWin,
|
|
523
|
+
});
|
|
524
|
+
// Track for cleanup
|
|
525
|
+
this.childProcesses.add(child);
|
|
526
|
+
if (!isWin && child.pid) {
|
|
527
|
+
this.processGroupId = child.pid;
|
|
528
|
+
}
|
|
529
|
+
child.on('exit', () => { this.childProcesses.delete(child); });
|
|
530
|
+
child.on('error', () => { this.childProcesses.delete(child); });
|
|
531
|
+
return child;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/** Singleton instance */
|
|
535
|
+
export const claudeBackend = new ClaudeBackend();
|