@yeaft/webchat-agent 0.0.2
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/claude.js +405 -0
- package/cli.js +151 -0
- package/connection.js +391 -0
- package/context.js +26 -0
- package/conversation.js +452 -0
- package/encryption.js +105 -0
- package/history.js +283 -0
- package/index.js +159 -0
- package/package.json +75 -0
- package/proxy.js +169 -0
- package/sdk/index.js +9 -0
- package/sdk/query.js +396 -0
- package/sdk/stream.js +112 -0
- package/sdk/types.js +13 -0
- package/sdk/utils.js +194 -0
- package/service.js +587 -0
- package/terminal.js +176 -0
- package/workbench.js +907 -0
package/sdk/query.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main query implementation for Claude Code SDK
|
|
3
|
+
* Handles spawning Claude process and managing message streams
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { createInterface } from 'readline';
|
|
8
|
+
import { Stream } from './stream.js';
|
|
9
|
+
import { AbortError } from './types.js';
|
|
10
|
+
import { getCleanEnv, logDebug, streamToStdin, resolveClaudeCommand } from './utils.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Query class manages Claude Code process interaction
|
|
15
|
+
* Implements AsyncIterableIterator for streaming messages
|
|
16
|
+
*/
|
|
17
|
+
export class Query {
|
|
18
|
+
constructor(childStdin, childStdout, processExitPromise, canCallTool) {
|
|
19
|
+
this.pendingControlResponses = new Map();
|
|
20
|
+
this.cancelControllers = new Map();
|
|
21
|
+
this.inputStream = new Stream();
|
|
22
|
+
this.childStdin = childStdin;
|
|
23
|
+
this.childStdout = childStdout;
|
|
24
|
+
this.processExitPromise = processExitPromise;
|
|
25
|
+
this.canCallTool = canCallTool;
|
|
26
|
+
this.claudeSessionId = null;
|
|
27
|
+
|
|
28
|
+
this.readMessages();
|
|
29
|
+
this.sdkMessages = this.readSdkMessages();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the Claude session ID
|
|
34
|
+
*/
|
|
35
|
+
getSessionId() {
|
|
36
|
+
return this.claudeSessionId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set an error on the stream
|
|
41
|
+
*/
|
|
42
|
+
setError(error) {
|
|
43
|
+
this.inputStream.error(error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* AsyncIterableIterator implementation
|
|
48
|
+
*/
|
|
49
|
+
next(...args) {
|
|
50
|
+
return this.sdkMessages.next(...args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return(value) {
|
|
54
|
+
if (this.sdkMessages.return) {
|
|
55
|
+
return this.sdkMessages.return(value);
|
|
56
|
+
}
|
|
57
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw(e) {
|
|
61
|
+
if (this.sdkMessages.throw) {
|
|
62
|
+
return this.sdkMessages.throw(e);
|
|
63
|
+
}
|
|
64
|
+
return Promise.reject(e);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[Symbol.asyncIterator]() {
|
|
68
|
+
return this.sdkMessages;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read messages from Claude process stdout
|
|
73
|
+
*/
|
|
74
|
+
async readMessages() {
|
|
75
|
+
const rl = createInterface({ input: this.childStdout });
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
for await (const line of rl) {
|
|
79
|
+
if (line.trim()) {
|
|
80
|
+
try {
|
|
81
|
+
const message = JSON.parse(line);
|
|
82
|
+
|
|
83
|
+
// Capture session ID from system messages
|
|
84
|
+
if (message.type === 'system' && message.session_id) {
|
|
85
|
+
this.claudeSessionId = message.session_id;
|
|
86
|
+
logDebug(`Session ID captured: ${this.claudeSessionId}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (message.type === 'control_response') {
|
|
90
|
+
const handler = this.pendingControlResponses.get(message.response.request_id);
|
|
91
|
+
if (handler) {
|
|
92
|
+
handler(message.response);
|
|
93
|
+
}
|
|
94
|
+
continue;
|
|
95
|
+
} else if (message.type === 'control_request') {
|
|
96
|
+
await this.handleControlRequest(message);
|
|
97
|
+
continue;
|
|
98
|
+
} else if (message.type === 'control_cancel_request') {
|
|
99
|
+
this.handleControlCancelRequest(message);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.inputStream.enqueue(message);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
logDebug(`Non-JSON line: ${line.substring(0, 100)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await this.processExitPromise;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
this.inputStream.error(error);
|
|
112
|
+
} finally {
|
|
113
|
+
this.inputStream.done();
|
|
114
|
+
this.cleanupControllers();
|
|
115
|
+
rl.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Async generator for SDK messages
|
|
121
|
+
*/
|
|
122
|
+
async *readSdkMessages() {
|
|
123
|
+
for await (const message of this.inputStream) {
|
|
124
|
+
yield message;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Send interrupt request to Claude
|
|
130
|
+
*/
|
|
131
|
+
async interrupt() {
|
|
132
|
+
if (!this.childStdin) {
|
|
133
|
+
throw new Error('Interrupt requires --input-format stream-json');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.request({
|
|
137
|
+
subtype: 'interrupt'
|
|
138
|
+
}, this.childStdin);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Send a user message
|
|
143
|
+
*/
|
|
144
|
+
sendMessage(content) {
|
|
145
|
+
if (!this.childStdin) {
|
|
146
|
+
throw new Error('sendMessage requires --input-format stream-json');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const msg = {
|
|
150
|
+
type: 'user',
|
|
151
|
+
message: {
|
|
152
|
+
role: 'user',
|
|
153
|
+
content: typeof content === 'string' ? content : content
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
this.childStdin.write(JSON.stringify(msg) + '\n');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Send control request to Claude process
|
|
162
|
+
*/
|
|
163
|
+
request(request, childStdin) {
|
|
164
|
+
const requestId = Math.random().toString(36).substring(2, 15);
|
|
165
|
+
const sdkRequest = {
|
|
166
|
+
request_id: requestId,
|
|
167
|
+
type: 'control_request',
|
|
168
|
+
request
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
this.pendingControlResponses.set(requestId, (response) => {
|
|
173
|
+
if (response.subtype === 'success') {
|
|
174
|
+
resolve(response);
|
|
175
|
+
} else {
|
|
176
|
+
reject(new Error(response.error));
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
childStdin.write(JSON.stringify(sdkRequest) + '\n');
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Handle incoming control requests for tool permissions
|
|
186
|
+
*/
|
|
187
|
+
async handleControlRequest(request) {
|
|
188
|
+
if (!this.childStdin) {
|
|
189
|
+
logDebug('Cannot handle control request - no stdin available');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const controller = new AbortController();
|
|
194
|
+
this.cancelControllers.set(request.request_id, controller);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const response = await this.processControlRequest(request, controller.signal);
|
|
198
|
+
const controlResponse = {
|
|
199
|
+
type: 'control_response',
|
|
200
|
+
response: {
|
|
201
|
+
subtype: 'success',
|
|
202
|
+
request_id: request.request_id,
|
|
203
|
+
response
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
this.childStdin.write(JSON.stringify(controlResponse) + '\n');
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const controlErrorResponse = {
|
|
209
|
+
type: 'control_response',
|
|
210
|
+
response: {
|
|
211
|
+
subtype: 'error',
|
|
212
|
+
request_id: request.request_id,
|
|
213
|
+
error: error instanceof Error ? error.message : String(error)
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
this.childStdin.write(JSON.stringify(controlErrorResponse) + '\n');
|
|
217
|
+
} finally {
|
|
218
|
+
this.cancelControllers.delete(request.request_id);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Handle control cancel requests
|
|
224
|
+
*/
|
|
225
|
+
handleControlCancelRequest(request) {
|
|
226
|
+
const controller = this.cancelControllers.get(request.request_id);
|
|
227
|
+
if (controller) {
|
|
228
|
+
controller.abort();
|
|
229
|
+
this.cancelControllers.delete(request.request_id);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Process control requests based on subtype
|
|
235
|
+
*/
|
|
236
|
+
async processControlRequest(request, signal) {
|
|
237
|
+
if (request.request.subtype === 'can_use_tool') {
|
|
238
|
+
if (!this.canCallTool) {
|
|
239
|
+
throw new Error('canCallTool callback is not provided.');
|
|
240
|
+
}
|
|
241
|
+
return this.canCallTool(request.request.tool_name, request.request.input, {
|
|
242
|
+
signal
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
throw new Error('Unsupported control request subtype: ' + request.request.subtype);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Cleanup method to abort all pending control requests
|
|
251
|
+
*/
|
|
252
|
+
cleanupControllers() {
|
|
253
|
+
for (const [requestId, controller] of this.cancelControllers.entries()) {
|
|
254
|
+
controller.abort();
|
|
255
|
+
this.cancelControllers.delete(requestId);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Main query function to interact with Claude Code
|
|
262
|
+
* @param {object} config - Configuration object
|
|
263
|
+
* @param {string|AsyncIterable} config.prompt - The prompt or async iterable of messages
|
|
264
|
+
* @param {object} config.options - Query options
|
|
265
|
+
* @returns {Query} Query instance
|
|
266
|
+
*/
|
|
267
|
+
export function query(config) {
|
|
268
|
+
const {
|
|
269
|
+
prompt,
|
|
270
|
+
options: {
|
|
271
|
+
allowedTools = [],
|
|
272
|
+
appendSystemPrompt,
|
|
273
|
+
customSystemPrompt,
|
|
274
|
+
cwd,
|
|
275
|
+
disallowedTools = [],
|
|
276
|
+
maxTurns,
|
|
277
|
+
permissionMode = 'default',
|
|
278
|
+
continue: continueConversation,
|
|
279
|
+
resume,
|
|
280
|
+
model,
|
|
281
|
+
canCallTool,
|
|
282
|
+
abort
|
|
283
|
+
} = {}
|
|
284
|
+
} = config;
|
|
285
|
+
|
|
286
|
+
// Build command arguments
|
|
287
|
+
const args = ['--output-format', 'stream-json', '--verbose'];
|
|
288
|
+
|
|
289
|
+
if (customSystemPrompt) args.push('--system-prompt', customSystemPrompt);
|
|
290
|
+
if (appendSystemPrompt) args.push('--append-system-prompt', appendSystemPrompt);
|
|
291
|
+
if (maxTurns) args.push('--max-turns', maxTurns.toString());
|
|
292
|
+
if (model) args.push('--model', model);
|
|
293
|
+
if (canCallTool) {
|
|
294
|
+
if (typeof prompt === 'string') {
|
|
295
|
+
throw new Error('canCallTool callback requires --input-format stream-json. Please set prompt as an AsyncIterable.');
|
|
296
|
+
}
|
|
297
|
+
args.push('--permission-prompt-tool', 'stdio');
|
|
298
|
+
}
|
|
299
|
+
if (continueConversation) args.push('--continue');
|
|
300
|
+
if (resume) args.push('--resume', resume);
|
|
301
|
+
if (allowedTools.length > 0) args.push('--allowedTools', ...allowedTools);
|
|
302
|
+
if (disallowedTools.length > 0) args.push('--disallowedTools', ...disallowedTools);
|
|
303
|
+
if (permissionMode) args.push('--permission-mode', permissionMode);
|
|
304
|
+
|
|
305
|
+
// Handle prompt input
|
|
306
|
+
if (typeof prompt === 'string') {
|
|
307
|
+
args.push('--print', prompt.trim());
|
|
308
|
+
} else {
|
|
309
|
+
args.push('--input-format', 'stream-json');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const { command: claudeCommand, prefixArgs, spawnOpts } = resolveClaudeCommand();
|
|
313
|
+
const spawnEnv = getCleanEnv();
|
|
314
|
+
|
|
315
|
+
console.log(`[SDK] Spawning Claude Code:`);
|
|
316
|
+
console.log(`[SDK] command: ${claudeCommand}`);
|
|
317
|
+
if (prefixArgs.length) console.log(`[SDK] entrypoint: ${prefixArgs[0]}`);
|
|
318
|
+
console.log(`[SDK] args: ${args.join(' ')}`);
|
|
319
|
+
console.log(`[SDK] cwd: ${cwd}`);
|
|
320
|
+
if (spawnOpts.shell) console.log(`[SDK] shell: ${spawnOpts.shell}`);
|
|
321
|
+
logDebug(`Spawning Claude Code: ${claudeCommand} ${[...prefixArgs, ...args].join(' ')}`);
|
|
322
|
+
|
|
323
|
+
const child = spawn(claudeCommand, [...prefixArgs, ...args], {
|
|
324
|
+
cwd,
|
|
325
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
326
|
+
signal: abort,
|
|
327
|
+
env: spawnEnv,
|
|
328
|
+
windowsHide: true,
|
|
329
|
+
...spawnOpts,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Handle stdin
|
|
333
|
+
let childStdin = null;
|
|
334
|
+
if (typeof prompt === 'string') {
|
|
335
|
+
child.stdin.end();
|
|
336
|
+
} else {
|
|
337
|
+
streamToStdin(prompt, child.stdin, abort);
|
|
338
|
+
childStdin = child.stdin;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Handle stderr - always capture for debugging
|
|
342
|
+
let stderrBuffer = '';
|
|
343
|
+
child.stderr.on('data', (data) => {
|
|
344
|
+
const text = data.toString();
|
|
345
|
+
stderrBuffer += text;
|
|
346
|
+
if (process.env.DEBUG) {
|
|
347
|
+
console.error('Claude Code stderr:', text);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Setup cleanup
|
|
352
|
+
const cleanup = () => {
|
|
353
|
+
if (!child.killed) {
|
|
354
|
+
child.kill();
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
abort?.addEventListener('abort', cleanup);
|
|
359
|
+
process.on('exit', cleanup);
|
|
360
|
+
|
|
361
|
+
// Handle process exit
|
|
362
|
+
const processExitPromise = new Promise((resolve) => {
|
|
363
|
+
child.on('close', (code) => {
|
|
364
|
+
if (abort?.aborted) {
|
|
365
|
+
queryInstance.setError(new AbortError('Claude Code process aborted by user'));
|
|
366
|
+
}
|
|
367
|
+
if (code !== 0) {
|
|
368
|
+
const errorMsg = stderrBuffer ? `Claude Code process exited with code ${code}: ${stderrBuffer.trim()}` : `Claude Code process exited with code ${code}`;
|
|
369
|
+
console.error('[SDK] Process error:', errorMsg);
|
|
370
|
+
queryInstance.setError(new Error(errorMsg));
|
|
371
|
+
} else {
|
|
372
|
+
resolve();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Create query instance
|
|
378
|
+
const queryInstance = new Query(childStdin, child.stdout, processExitPromise, canCallTool);
|
|
379
|
+
|
|
380
|
+
// Handle process errors
|
|
381
|
+
child.on('error', (error) => {
|
|
382
|
+
if (abort?.aborted) {
|
|
383
|
+
queryInstance.setError(new AbortError('Claude Code process aborted by user'));
|
|
384
|
+
} else {
|
|
385
|
+
queryInstance.setError(new Error(`Failed to spawn Claude Code process: ${error.message}`));
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Cleanup on exit
|
|
390
|
+
processExitPromise.finally(() => {
|
|
391
|
+
cleanup();
|
|
392
|
+
abort?.removeEventListener('abort', cleanup);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return queryInstance;
|
|
396
|
+
}
|
package/sdk/stream.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream implementation for handling async message streams
|
|
3
|
+
* Provides an async iterable interface for processing SDK messages
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generic async stream implementation
|
|
8
|
+
* Handles queuing, error propagation, and proper cleanup
|
|
9
|
+
*/
|
|
10
|
+
export class Stream {
|
|
11
|
+
constructor(returned) {
|
|
12
|
+
this.queue = [];
|
|
13
|
+
this.readResolve = undefined;
|
|
14
|
+
this.readReject = undefined;
|
|
15
|
+
this.isDone = false;
|
|
16
|
+
this.hasError = undefined;
|
|
17
|
+
this.started = false;
|
|
18
|
+
this.returned = returned;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Implements async iterable protocol
|
|
23
|
+
*/
|
|
24
|
+
[Symbol.asyncIterator]() {
|
|
25
|
+
if (this.started) {
|
|
26
|
+
throw new Error('Stream can only be iterated once');
|
|
27
|
+
}
|
|
28
|
+
this.started = true;
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the next value from the stream
|
|
34
|
+
*/
|
|
35
|
+
async next() {
|
|
36
|
+
// Return queued items first
|
|
37
|
+
if (this.queue.length > 0) {
|
|
38
|
+
return Promise.resolve({
|
|
39
|
+
done: false,
|
|
40
|
+
value: this.queue.shift()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check terminal states
|
|
45
|
+
if (this.isDone) {
|
|
46
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.hasError) {
|
|
50
|
+
return Promise.reject(this.hasError);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Wait for new data
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
this.readResolve = resolve;
|
|
56
|
+
this.readReject = reject;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Adds a value to the stream
|
|
62
|
+
*/
|
|
63
|
+
enqueue(value) {
|
|
64
|
+
if (this.readResolve) {
|
|
65
|
+
// Direct delivery to waiting consumer
|
|
66
|
+
const resolve = this.readResolve;
|
|
67
|
+
this.readResolve = undefined;
|
|
68
|
+
this.readReject = undefined;
|
|
69
|
+
resolve({ done: false, value });
|
|
70
|
+
} else {
|
|
71
|
+
// Queue for later consumption
|
|
72
|
+
this.queue.push(value);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Marks the stream as complete
|
|
78
|
+
*/
|
|
79
|
+
done() {
|
|
80
|
+
this.isDone = true;
|
|
81
|
+
if (this.readResolve) {
|
|
82
|
+
const resolve = this.readResolve;
|
|
83
|
+
this.readResolve = undefined;
|
|
84
|
+
this.readReject = undefined;
|
|
85
|
+
resolve({ done: true, value: undefined });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Propagates an error through the stream
|
|
91
|
+
*/
|
|
92
|
+
error(error) {
|
|
93
|
+
this.hasError = error;
|
|
94
|
+
if (this.readReject) {
|
|
95
|
+
const reject = this.readReject;
|
|
96
|
+
this.readResolve = undefined;
|
|
97
|
+
this.readReject = undefined;
|
|
98
|
+
reject(error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Implements async iterator cleanup
|
|
104
|
+
*/
|
|
105
|
+
async return() {
|
|
106
|
+
this.isDone = true;
|
|
107
|
+
if (this.returned) {
|
|
108
|
+
this.returned();
|
|
109
|
+
}
|
|
110
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
111
|
+
}
|
|
112
|
+
}
|
package/sdk/types.js
ADDED
package/sdk/utils.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Claude Code SDK integration
|
|
3
|
+
* Path resolution, environment setup, and platform compatibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { platform, homedir } from 'os';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Log debug message
|
|
13
|
+
*/
|
|
14
|
+
export function logDebug(message) {
|
|
15
|
+
if (process.env.DEBUG) {
|
|
16
|
+
console.log('[SDK Debug]', message);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build the full PATH string with common bin directories included.
|
|
22
|
+
* Used by both getDefaultClaudeCodePath() and getCleanEnv().
|
|
23
|
+
*/
|
|
24
|
+
function getEnhancedPath() {
|
|
25
|
+
if (isWindows()) {
|
|
26
|
+
const systemPaths = [
|
|
27
|
+
'C:\\Windows\\system32',
|
|
28
|
+
'C:\\Windows',
|
|
29
|
+
'C:\\Windows\\System32\\Wbem'
|
|
30
|
+
];
|
|
31
|
+
const currentPath = process.env.PATH || process.env.Path || '';
|
|
32
|
+
const pathParts = currentPath.split(';').filter(p => p);
|
|
33
|
+
for (const sp of systemPaths) {
|
|
34
|
+
if (!pathParts.some(p => p.toLowerCase() === sp.toLowerCase())) {
|
|
35
|
+
pathParts.push(sp);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return pathParts.join(';');
|
|
39
|
+
} else {
|
|
40
|
+
const unixPaths = [
|
|
41
|
+
'/usr/local/bin',
|
|
42
|
+
'/usr/bin',
|
|
43
|
+
'/bin',
|
|
44
|
+
'/usr/sbin',
|
|
45
|
+
join(homedir(), '.local', 'bin'),
|
|
46
|
+
join(homedir(), '.npm-global', 'bin'),
|
|
47
|
+
];
|
|
48
|
+
if (platform() === 'darwin') {
|
|
49
|
+
unixPaths.push('/opt/homebrew/bin');
|
|
50
|
+
}
|
|
51
|
+
// Include the directory where the current node binary lives
|
|
52
|
+
// This catches nvm/fnm/volta managed node installs and their global bins
|
|
53
|
+
const nodeBinDir = join(process.execPath, '..');
|
|
54
|
+
unixPaths.push(nodeBinDir);
|
|
55
|
+
|
|
56
|
+
const currentPath = process.env.PATH || '';
|
|
57
|
+
const pathParts = currentPath.split(':').filter(p => p);
|
|
58
|
+
for (const sp of unixPaths) {
|
|
59
|
+
if (!pathParts.includes(sp)) {
|
|
60
|
+
pathParts.push(sp);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return pathParts.join(':');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get default path to Claude Code executable
|
|
69
|
+
* Tries CLAUDE_PATH env var first, then checks common install locations,
|
|
70
|
+
* then auto-discovers via which/where with enhanced PATH.
|
|
71
|
+
*/
|
|
72
|
+
export function getDefaultClaudeCodePath() {
|
|
73
|
+
if (process.env.CLAUDE_PATH) return process.env.CLAUDE_PATH;
|
|
74
|
+
|
|
75
|
+
// Check common locations first (fast, no subprocess)
|
|
76
|
+
if (!isWindows()) {
|
|
77
|
+
const candidates = [
|
|
78
|
+
'/usr/local/bin/claude',
|
|
79
|
+
join(homedir(), '.local', 'bin', 'claude'),
|
|
80
|
+
join(homedir(), '.npm-global', 'bin', 'claude'),
|
|
81
|
+
// nvm/fnm/volta: claude installed globally lives next to node
|
|
82
|
+
join(process.execPath, '..', 'claude'),
|
|
83
|
+
];
|
|
84
|
+
if (platform() === 'darwin') {
|
|
85
|
+
candidates.push('/opt/homebrew/bin/claude');
|
|
86
|
+
}
|
|
87
|
+
for (const c of candidates) {
|
|
88
|
+
if (existsSync(c)) return c;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try which/where with enhanced PATH (catches nvm, custom installs, etc.)
|
|
93
|
+
try {
|
|
94
|
+
const enhancedPath = getEnhancedPath();
|
|
95
|
+
const cmd = isWindows() ? 'where claude' : 'which claude';
|
|
96
|
+
const output = execSync(cmd, {
|
|
97
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
98
|
+
timeout: 5000,
|
|
99
|
+
env: { ...process.env, PATH: enhancedPath }
|
|
100
|
+
}).toString().trim();
|
|
101
|
+
const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
|
|
102
|
+
|
|
103
|
+
if (isWindows() && lines.length > 1) {
|
|
104
|
+
// On Windows, `where` may return multiple matches. Prefer .cmd/.exe over
|
|
105
|
+
// the extensionless Unix shell script that npm also creates.
|
|
106
|
+
const preferred = lines.find(l => /\.(cmd|exe)$/i.test(l));
|
|
107
|
+
if (preferred) return preferred;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (lines[0]) return lines[0];
|
|
111
|
+
} catch {}
|
|
112
|
+
|
|
113
|
+
// Fallback: bare command, hope it's on PATH
|
|
114
|
+
return 'claude';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a clean environment
|
|
119
|
+
* Ensures necessary environment variables and PATH entries are present
|
|
120
|
+
*/
|
|
121
|
+
export function getCleanEnv() {
|
|
122
|
+
const env = { ...process.env };
|
|
123
|
+
|
|
124
|
+
if (isWindows()) {
|
|
125
|
+
if (!env.COMSPEC) {
|
|
126
|
+
env.COMSPEC = 'C:\\Windows\\system32\\cmd.exe';
|
|
127
|
+
}
|
|
128
|
+
if (!env.SystemRoot) {
|
|
129
|
+
env.SystemRoot = 'C:\\Windows';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
env.PATH = getEnhancedPath();
|
|
134
|
+
return env;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Stream async messages to stdin
|
|
139
|
+
*/
|
|
140
|
+
export async function streamToStdin(stream, stdin, abort) {
|
|
141
|
+
for await (const message of stream) {
|
|
142
|
+
if (abort?.aborted) break;
|
|
143
|
+
stdin.write(JSON.stringify(message) + '\n');
|
|
144
|
+
}
|
|
145
|
+
stdin.end();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if running on Windows
|
|
150
|
+
*/
|
|
151
|
+
export function isWindows() {
|
|
152
|
+
return platform() === 'win32';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve Claude executable into { command, prefixArgs, spawnOpts } for spawn().
|
|
157
|
+
* On Windows (npm install): parses .cmd wrapper to find cli.js, then calls node directly.
|
|
158
|
+
* This avoids cmd.exe flash and PowerShell script execution policy issues.
|
|
159
|
+
*/
|
|
160
|
+
export function resolveClaudeCommand() {
|
|
161
|
+
const execPath = getDefaultClaudeCodePath();
|
|
162
|
+
|
|
163
|
+
if (isWindows() && execPath.toLowerCase().endsWith('.cmd')) {
|
|
164
|
+
// npm 生成的 .cmd 内容固定格式,核心行是:
|
|
165
|
+
// "%_prog%" "%dp0%\node_modules\@anthropic-ai\claude-code\cli.js" %*
|
|
166
|
+
// 解析出 cli.js 的相对路径,拼成绝对路径后用 node 直接调用
|
|
167
|
+
try {
|
|
168
|
+
const cmdContent = readFileSync(execPath, 'utf-8');
|
|
169
|
+
const match = cmdContent.match(/%dp0%\\(.+?\.js)"/i) ||
|
|
170
|
+
cmdContent.match(/%dp0%\\(.+?\.js)/i);
|
|
171
|
+
if (match) {
|
|
172
|
+
const cliJsPath = join(dirname(execPath), match[1]);
|
|
173
|
+
if (existsSync(cliJsPath)) {
|
|
174
|
+
return {
|
|
175
|
+
command: process.execPath, // node
|
|
176
|
+
prefixArgs: [cliJsPath],
|
|
177
|
+
spawnOpts: {},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
182
|
+
// 解析失败时 fallback: 用 powershell 执行 .ps1
|
|
183
|
+
const ps1Path = execPath.slice(0, -4) + '.ps1';
|
|
184
|
+
if (existsSync(ps1Path)) {
|
|
185
|
+
return {
|
|
186
|
+
command: 'powershell.exe',
|
|
187
|
+
prefixArgs: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', ps1Path],
|
|
188
|
+
spawnOpts: {},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { command: execPath, prefixArgs: [], spawnOpts: {} };
|
|
194
|
+
}
|