cli-link 0.0.1

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.
Files changed (34) hide show
  1. package/README.md +271 -0
  2. package/bin/agentpilot.js +239 -0
  3. package/dist/client/assets/History-DR_K6WbO.js +3 -0
  4. package/dist/client/assets/MarkdownRenderer-D9IwexPM.js +1 -0
  5. package/dist/client/assets/PageTopBar-SnTIrSb5.js +1 -0
  6. package/dist/client/assets/Session-EFYFIC_X.js +11 -0
  7. package/dist/client/assets/Settings-DgmHC_Hw.js +1 -0
  8. package/dist/client/assets/Workspace-CJvVQVzU.js +8 -0
  9. package/dist/client/assets/WorkspaceLinkedText-D6hNg0T9.js +2 -0
  10. package/dist/client/assets/code-highlight-CEcsuMpw.js +1 -0
  11. package/dist/client/assets/index-Bk4_acsd.css +1 -0
  12. package/dist/client/assets/index-C89UCwGk.js +2 -0
  13. package/dist/client/assets/vendor-icons-S_ObYVVf.js +331 -0
  14. package/dist/client/assets/vendor-markdown-BDwu-Ux6.js +35 -0
  15. package/dist/client/assets/vendor-motion-n6Lx6G4a.js +9 -0
  16. package/dist/client/assets/vendor-react-DSV5aFEg.js +67 -0
  17. package/dist/client/assets/vendor-virtual-CcftJrIC.js +4 -0
  18. package/dist/client/favicon.svg +18 -0
  19. package/dist/client/icons/apple-touch-icon.png +0 -0
  20. package/dist/client/icons/icon-192.png +0 -0
  21. package/dist/client/icons/icon-512.png +0 -0
  22. package/dist/client/index.html +34 -0
  23. package/dist/client/manifest.webmanifest +59 -0
  24. package/dist/client/sw.js +143 -0
  25. package/dist/client//344/273/243/347/240/201/351/241/265/351/235/242.png +0 -0
  26. package/dist/client//345/216/206/345/217/262/350/256/260/345/275/225.png +0 -0
  27. package/dist/client//345/257/271/350/257/235/351/241/265/351/235/242.png +0 -0
  28. package/dist/client//350/256/276/347/275/256/351/241/265/351/235/242.png +0 -0
  29. package/dist/server/cli-manager.js +1532 -0
  30. package/dist/server/codex-history.js +280 -0
  31. package/dist/server/index.js +2097 -0
  32. package/dist/server/store.js +594 -0
  33. package/dist/server/terminal-qr.js +317 -0
  34. package/package.json +71 -0
@@ -0,0 +1,1532 @@
1
+ import { spawn } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as os from 'os';
4
+ const DEFAULT_CLAUDE_CLI_ARGS = ['--dangerously-skip-permissions'];
5
+ const DEFAULT_CODEX_CLI_ARGS = [
6
+ '--dangerously-bypass-approvals-and-sandbox',
7
+ '-m',
8
+ 'gpt-5.5',
9
+ '-c',
10
+ 'model_reasoning_effort="xhigh"',
11
+ ];
12
+ const CONFIRM_PATTERNS = [
13
+ /\?\s*\[[yY]\/[nN]\]/,
14
+ /\(yes\/no\)/i,
15
+ /\(y\/n\)/i,
16
+ /allow.*\?/i,
17
+ /proceed.*\?/i,
18
+ /confirm.*\?/i,
19
+ /approve.*\?/i,
20
+ /\[Y\/n\]/,
21
+ ];
22
+ const ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)|\r/g;
23
+ function stripAnsi(text) {
24
+ return text.replace(ANSI_REGEX, '');
25
+ }
26
+ const FILE_TOOLS = ['Write', 'Edit', 'MultiEdit', 'writeFile', 'editFile', 'readFile', 'Read'];
27
+ const COMMAND_TOOLS = ['Bash', 'bash', 'Shell', 'shell', 'Exec', 'exec'];
28
+ const ASK_USER_QUESTION_TOOL = 'AskUserQuestion';
29
+ function inferCLIType(cliCommand) {
30
+ const command = cliCommand || '';
31
+ if (!command.trim())
32
+ return 'codex';
33
+ const name = command.split(/[\\/]/).pop()?.toLowerCase() || command.toLowerCase();
34
+ return name.includes('codex') ? 'codex' : 'claude';
35
+ }
36
+ function getDefaultCLIArgs(cliType) {
37
+ if (cliType === 'codex') {
38
+ return [...DEFAULT_CODEX_CLI_ARGS];
39
+ }
40
+ return [...DEFAULT_CLAUDE_CLI_ARGS];
41
+ }
42
+ function hasArg(args, shortName, longName) {
43
+ return args.some((arg) => arg === shortName || arg === longName || arg.startsWith(`${longName}=`));
44
+ }
45
+ function getToolCategory(toolName) {
46
+ if (FILE_TOOLS.includes(toolName))
47
+ return 'file';
48
+ if (COMMAND_TOOLS.includes(toolName))
49
+ return 'command';
50
+ return 'other';
51
+ }
52
+ export class CLIManager {
53
+ process = null;
54
+ printProcess = null;
55
+ _status = 'disconnected';
56
+ config = {
57
+ cliType: 'codex',
58
+ cliCommand: 'codex',
59
+ workDir: process.cwd(),
60
+ confirmMode: 'key',
61
+ cliArgs: getDefaultCLIArgs('codex'),
62
+ runMode: 'print',
63
+ };
64
+ onEvent = null;
65
+ stdoutBuffer = '';
66
+ stderrBuffer = '';
67
+ pendingOutput = '';
68
+ hasStreamedText = false;
69
+ sessionId = null;
70
+ allowedTools = new Set();
71
+ pendingDenials = [];
72
+ lastInput = '';
73
+ isRetrying = false;
74
+ pendingQuestionToolUseId = null;
75
+ pendingQuestionNeedsPermissionApproval = false;
76
+ answeredQuestionToolUseIds = new Set();
77
+ get status() {
78
+ return this._status;
79
+ }
80
+ getConfig() {
81
+ return { ...this.config };
82
+ }
83
+ restoreConfig(config) {
84
+ const nextType = config.cliType || inferCLIType(config.cliCommand);
85
+ const cliCommand = typeof config.cliCommand === 'string' && config.cliCommand.trim()
86
+ ? config.cliCommand
87
+ : nextType === 'codex'
88
+ ? 'codex'
89
+ : 'claude';
90
+ this.config = {
91
+ ...this.config,
92
+ cliType: nextType,
93
+ cliCommand,
94
+ workDir: typeof config.workDir === 'string' && config.workDir.trim() ? config.workDir : this.config.workDir,
95
+ cliArgs: Array.isArray(config.cliArgs) ? config.cliArgs : getDefaultCLIArgs(nextType),
96
+ confirmMode: config.confirmMode === 'auto' ? 'auto' : 'key',
97
+ runMode: config.runMode === 'interactive' ? 'interactive' : 'print',
98
+ };
99
+ }
100
+ setEventHandler(cb) {
101
+ this.onEvent = cb;
102
+ }
103
+ emit(event) {
104
+ this.onEvent?.(event);
105
+ }
106
+ now() {
107
+ return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
108
+ }
109
+ getCLIType() {
110
+ return this.config.cliType || inferCLIType(this.config.cliCommand);
111
+ }
112
+ getCLILabel() {
113
+ return this.getCLIType() === 'codex' ? 'Codex CLI' : 'Claude CLI';
114
+ }
115
+ start(config, onEvent) {
116
+ this.onEvent = onEvent;
117
+ const filtered = Object.fromEntries(Object.entries(config).filter(([_, v]) => v !== undefined));
118
+ const nextConfig = { ...this.config, ...filtered };
119
+ const nextType = filtered.cliType || inferCLIType(nextConfig.cliCommand);
120
+ const typeChanged = nextType !== this.config.cliType;
121
+ this.config = {
122
+ ...nextConfig,
123
+ cliType: nextType,
124
+ cliArgs: Array.isArray(filtered.cliArgs)
125
+ ? filtered.cliArgs
126
+ : typeChanged
127
+ ? getDefaultCLIArgs(nextType)
128
+ : nextConfig.cliArgs,
129
+ };
130
+ console.log(`[CLI] start called, clearing sessionId (was: ${this.sessionId?.slice(0, 8) || 'null'})`);
131
+ this.sessionId = null;
132
+ this.pendingQuestionToolUseId = null;
133
+ this.pendingQuestionNeedsPermissionApproval = false;
134
+ this.answeredQuestionToolUseIds.clear();
135
+ if (this.config.runMode === 'interactive') {
136
+ this.spawnInteractive();
137
+ }
138
+ else {
139
+ this._status = 'idle';
140
+ this.emit({ type: 'status', status: 'idle' });
141
+ this.emit({ type: 'system', content: `${this.getCLILabel()} 就绪 (print 模式)`, time: this.now() });
142
+ }
143
+ }
144
+ spawnInteractive() {
145
+ this.killProcess();
146
+ this.stdoutBuffer = '';
147
+ this.stderrBuffer = '';
148
+ this.pendingOutput = '';
149
+ const args = [...(this.config.cliArgs || [])];
150
+ if (this.getCLIType() === 'codex') {
151
+ if (!hasArg(args, '-C', '--cd')) {
152
+ args.push('-C', this.config.workDir);
153
+ }
154
+ args.push('--no-alt-screen');
155
+ }
156
+ try {
157
+ this.process = spawn(this.config.cliCommand, args, {
158
+ cwd: this.config.workDir,
159
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb' },
160
+ stdio: ['pipe', 'pipe', 'pipe'],
161
+ });
162
+ }
163
+ catch (err) {
164
+ this.emit({ type: 'error', content: `CLI 启动失败: ${err.message}`, time: this.now() });
165
+ this._status = 'disconnected';
166
+ this.emit({ type: 'status', status: 'disconnected' });
167
+ return;
168
+ }
169
+ this._status = 'idle';
170
+ this.emit({ type: 'status', status: 'idle' });
171
+ this.emit({ type: 'system', content: 'CLI 进程已启动 (交互模式)', time: this.now() });
172
+ this.process.stdout.on('data', (data) => {
173
+ this.handleStdout(data.toString());
174
+ });
175
+ this.process.stderr.on('data', (data) => {
176
+ const text = stripAnsi(data.toString());
177
+ this.stderrBuffer += text;
178
+ const lines = this.stderrBuffer.split('\n');
179
+ this.stderrBuffer = lines.pop() || '';
180
+ for (const line of lines) {
181
+ const trimmed = line.trim();
182
+ if (trimmed) {
183
+ this.emit({ type: 'error', content: trimmed, time: this.now() });
184
+ }
185
+ }
186
+ });
187
+ this.process.on('close', (code) => {
188
+ this.flushPendingOutput();
189
+ this._status = 'disconnected';
190
+ this.emit({ type: 'status', status: 'disconnected' });
191
+ this.emit({ type: 'system', content: `CLI 进程已退出 (code: ${code})`, time: this.now() });
192
+ this.process = null;
193
+ });
194
+ this.process.on('error', (err) => {
195
+ this.emit({ type: 'error', content: `CLI 错误: ${err.message}`, time: this.now() });
196
+ this._status = 'disconnected';
197
+ this.emit({ type: 'status', status: 'disconnected' });
198
+ this.process = null;
199
+ });
200
+ }
201
+ buildPrintArgs(input) {
202
+ if (this.getCLIType() === 'codex') {
203
+ return this.buildCodexPrintArgs(input);
204
+ }
205
+ return this.buildClaudePrintArgs(input);
206
+ }
207
+ buildClaudePrintArgs(input) {
208
+ const args = [];
209
+ const cliArgs = this.config.cliArgs || [];
210
+ args.push(...cliArgs);
211
+ if (this.sessionId) {
212
+ args.push('--resume', this.sessionId);
213
+ }
214
+ args.push('-p', input, '--output-format', 'stream-json', '--verbose');
215
+ const allowedTools = new Set(this.allowedTools);
216
+ allowedTools.add(ASK_USER_QUESTION_TOOL);
217
+ if (allowedTools.size > 0) {
218
+ args.push('--allowedTools', Array.from(allowedTools).join(','));
219
+ }
220
+ return args;
221
+ }
222
+ buildCodexPrintArgs(_input) {
223
+ const args = [...(this.config.cliArgs || [])];
224
+ if (!hasArg(args, '-C', '--cd')) {
225
+ args.push('-C', this.config.workDir);
226
+ }
227
+ args.push('exec');
228
+ if (this.sessionId) {
229
+ args.push('resume', '--json', this.sessionId, '-');
230
+ }
231
+ else {
232
+ args.push('--json', '-');
233
+ }
234
+ return args;
235
+ }
236
+ buildIsolatedPrintArgs(input) {
237
+ if (this.getCLIType() === 'codex') {
238
+ const args = [...(this.config.cliArgs || [])];
239
+ if (!hasArg(args, '-C', '--cd')) {
240
+ args.push('-C', this.config.workDir);
241
+ }
242
+ args.push('exec', '--json', '-');
243
+ return { args, stdin: input };
244
+ }
245
+ const args = [...(this.config.cliArgs || [])];
246
+ args.push('-p', input, '--output-format', 'stream-json', '--verbose');
247
+ return { args };
248
+ }
249
+ extractOneShotText(msg) {
250
+ const type = msg?.type;
251
+ if (type === 'item.completed') {
252
+ const item = msg.item || {};
253
+ const itemType = item.type || '';
254
+ const isAgentMessage = itemType === 'agent_message' ||
255
+ itemType === 'assistant_message' ||
256
+ (itemType === 'message' && item.role === 'assistant');
257
+ return isAgentMessage ? this.extractText(item.text || item.content || item.message || item) : '';
258
+ }
259
+ if (type === 'assistant') {
260
+ return this.extractText(msg.message || msg.content || msg);
261
+ }
262
+ if (type === 'text') {
263
+ return this.extractText(msg.text || msg.content || msg.message);
264
+ }
265
+ if (type === 'content_block_delta') {
266
+ return this.extractText(msg.delta);
267
+ }
268
+ if (type === 'result' || type === 'done' || type === 'complete') {
269
+ return this.extractText(msg.content || msg.message || msg.result);
270
+ }
271
+ return '';
272
+ }
273
+ extractOneShotError(msg) {
274
+ if (!msg || typeof msg !== 'object')
275
+ return '';
276
+ if (msg.type !== 'error' && msg.type !== 'turn.failed')
277
+ return '';
278
+ return this.extractText(msg.error || msg.message || msg.content) || `${this.getCLILabel()} 执行失败`;
279
+ }
280
+ runOneShot(input, timeoutMs = 120000) {
281
+ const { args, stdin } = this.buildIsolatedPrintArgs(input);
282
+ const cliType = this.getCLIType();
283
+ const cliLabel = this.getCLILabel();
284
+ return new Promise((resolve, reject) => {
285
+ let proc;
286
+ try {
287
+ proc = spawn(this.config.cliCommand, args, {
288
+ cwd: this.config.workDir,
289
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
290
+ stdio: ['pipe', 'pipe', 'pipe'],
291
+ });
292
+ }
293
+ catch (err) {
294
+ reject(new Error(`${cliLabel} 启动失败: ${err.message}`));
295
+ return;
296
+ }
297
+ const texts = [];
298
+ const errors = [];
299
+ let stdoutBuf = '';
300
+ let stderrBuf = '';
301
+ let settled = false;
302
+ const finish = (fn) => {
303
+ if (settled)
304
+ return;
305
+ settled = true;
306
+ clearTimeout(timer);
307
+ fn();
308
+ };
309
+ const consumeLine = (line) => {
310
+ const trimmed = line.trim();
311
+ if (!trimmed)
312
+ return;
313
+ try {
314
+ const json = JSON.parse(trimmed);
315
+ const errorText = this.extractOneShotError(json);
316
+ if (errorText)
317
+ errors.push(errorText);
318
+ const text = this.extractOneShotText(json);
319
+ if (text)
320
+ texts.push(text);
321
+ }
322
+ catch {
323
+ if (cliType !== 'codex')
324
+ texts.push(trimmed);
325
+ }
326
+ };
327
+ const flushStdout = () => {
328
+ if (!stdoutBuf.trim())
329
+ return;
330
+ const remaining = stdoutBuf;
331
+ stdoutBuf = '';
332
+ consumeLine(remaining);
333
+ };
334
+ const timer = setTimeout(() => {
335
+ try {
336
+ proc.kill('SIGTERM');
337
+ }
338
+ catch { }
339
+ finish(() => reject(new Error(`${cliLabel} 生成超时`)));
340
+ }, timeoutMs);
341
+ proc.stdout?.on('data', (data) => {
342
+ stdoutBuf += data.toString();
343
+ const lines = stdoutBuf.split('\n');
344
+ stdoutBuf = lines.pop() || '';
345
+ for (const line of lines) {
346
+ consumeLine(line);
347
+ }
348
+ });
349
+ proc.stderr?.on('data', (data) => {
350
+ stderrBuf += stripAnsi(data.toString());
351
+ });
352
+ proc.on('close', (code) => {
353
+ flushStdout();
354
+ const text = texts.filter(Boolean).slice(-1)[0]?.trim() || texts.filter(Boolean).join('\n').trim();
355
+ if (code !== 0 && !text) {
356
+ const detail = errors[0] || stderrBuf.trim() || `${cliLabel} 退出码 ${code}`;
357
+ finish(() => reject(new Error(detail)));
358
+ return;
359
+ }
360
+ if (!text) {
361
+ const detail = errors[0] || stderrBuf.trim() || `${cliLabel} 未返回内容`;
362
+ finish(() => reject(new Error(detail)));
363
+ return;
364
+ }
365
+ finish(() => resolve(text));
366
+ });
367
+ proc.on('error', (err) => {
368
+ finish(() => reject(new Error(`${cliLabel} 执行错误: ${err.message}`)));
369
+ });
370
+ if (stdin != null) {
371
+ proc.stdin?.end(stdin);
372
+ }
373
+ else {
374
+ proc.stdin?.end();
375
+ }
376
+ });
377
+ }
378
+ getPrintStdio() {
379
+ return ['pipe', 'pipe', 'pipe'];
380
+ }
381
+ writePromptToStdin(proc, input) {
382
+ if (this.getCLIType() !== 'codex')
383
+ return;
384
+ proc.stdin?.end(input);
385
+ }
386
+ runPrintCommand(input, isRetry = false) {
387
+ this._status = 'running';
388
+ this.pendingDenials = [];
389
+ this.hasStreamedText = false;
390
+ this.isRetrying = isRetry;
391
+ if (!isRetry) {
392
+ this.lastInput = input;
393
+ }
394
+ this.emit({ type: 'status', status: 'running' });
395
+ const args = this.buildPrintArgs(input);
396
+ console.log(`[CLI] runPrintCommand: sessionId=${this.sessionId?.slice(0, 8) || 'null'}, resume=${!!this.sessionId}, args=${args.join(' ')}`);
397
+ let localBuf = '';
398
+ let stderrBuf = '';
399
+ let hasStructuredError = false;
400
+ try {
401
+ const proc = spawn(this.config.cliCommand, args, {
402
+ cwd: this.config.workDir,
403
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
404
+ stdio: this.getPrintStdio(),
405
+ });
406
+ this.printProcess = proc;
407
+ this.writePromptToStdin(proc, input);
408
+ proc.stdout.on('data', (data) => {
409
+ localBuf += data.toString();
410
+ const lines = localBuf.split('\n');
411
+ localBuf = lines.pop() || '';
412
+ for (const line of lines) {
413
+ const trimmed = line.trim();
414
+ if (!trimmed)
415
+ continue;
416
+ try {
417
+ const json = JSON.parse(trimmed);
418
+ if (json?.type === 'error' || json?.type === 'turn.failed') {
419
+ hasStructuredError = true;
420
+ }
421
+ this.handleJSONMessage(json);
422
+ if (this._status === 'question')
423
+ return;
424
+ }
425
+ catch { }
426
+ }
427
+ });
428
+ proc.stderr.on('data', (data) => {
429
+ stderrBuf += stripAnsi(data.toString());
430
+ });
431
+ proc.on('close', (code) => {
432
+ this.printProcess = null;
433
+ if (this._status !== 'question' && localBuf.trim()) {
434
+ try {
435
+ const json = JSON.parse(localBuf.trim());
436
+ if (json?.type === 'error' || json?.type === 'turn.failed') {
437
+ hasStructuredError = true;
438
+ }
439
+ this.handleJSONMessage(json);
440
+ }
441
+ catch { }
442
+ }
443
+ if (this._status !== 'question' && code !== 0 && stderrBuf.trim() && !hasStructuredError) {
444
+ const errLines = stderrBuf.trim().split('\n').filter(l => l.trim());
445
+ for (const line of errLines) {
446
+ this.emit({ type: 'error', content: line.trim(), time: this.now() });
447
+ }
448
+ }
449
+ if (this._status !== 'confirm' && this._status !== 'question') {
450
+ this._status = 'idle';
451
+ this.emit({ type: 'status', status: 'idle' });
452
+ }
453
+ this.isRetrying = false;
454
+ });
455
+ proc.on('error', (err) => {
456
+ this.printProcess = null;
457
+ this.emit({ type: 'error', content: `CLI 执行错误: ${err.message}`, time: this.now() });
458
+ this._status = 'idle';
459
+ this.emit({ type: 'status', status: 'idle' });
460
+ this.isRetrying = false;
461
+ });
462
+ }
463
+ catch (err) {
464
+ this.emit({ type: 'error', content: `CLI 启动失败: ${err.message}`, time: this.now() });
465
+ this._status = 'idle';
466
+ this.emit({ type: 'status', status: 'idle' });
467
+ this.isRetrying = false;
468
+ }
469
+ }
470
+ retryWithPermission() {
471
+ if (!this.sessionId) {
472
+ this.emit({ type: 'error', content: '无法重试: 没有活跃的会话', time: this.now() });
473
+ this._status = 'idle';
474
+ this.emit({ type: 'status', status: 'idle' });
475
+ return;
476
+ }
477
+ this._status = 'running';
478
+ this.pendingDenials = [];
479
+ this.hasStreamedText = false;
480
+ this.isRetrying = true;
481
+ this.emit({ type: 'status', status: 'running' });
482
+ this.emit({ type: 'system', content: `正在恢复会话并重试 (已授权: ${Array.from(this.allowedTools).join(', ')})...`, time: this.now() });
483
+ const retryInput = '请继续之前的操作,现在已经有权限了,请重新执行之前被拒绝的步骤。';
484
+ const args = this.buildPrintArgs(retryInput);
485
+ let localBuf = '';
486
+ let stderrBuf = '';
487
+ let hasStructuredError = false;
488
+ try {
489
+ const proc = spawn(this.config.cliCommand, args, {
490
+ cwd: this.config.workDir,
491
+ env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
492
+ stdio: this.getPrintStdio(),
493
+ });
494
+ this.printProcess = proc;
495
+ this.writePromptToStdin(proc, retryInput);
496
+ proc.stdout.on('data', (data) => {
497
+ localBuf += data.toString();
498
+ const lines = localBuf.split('\n');
499
+ localBuf = lines.pop() || '';
500
+ for (const line of lines) {
501
+ const trimmed = line.trim();
502
+ if (!trimmed)
503
+ continue;
504
+ try {
505
+ const json = JSON.parse(trimmed);
506
+ if (json?.type === 'error' || json?.type === 'turn.failed') {
507
+ hasStructuredError = true;
508
+ }
509
+ this.handleJSONMessage(json);
510
+ if (this._status === 'question')
511
+ return;
512
+ }
513
+ catch { }
514
+ }
515
+ });
516
+ proc.stderr.on('data', (data) => {
517
+ stderrBuf += stripAnsi(data.toString());
518
+ });
519
+ proc.on('close', (code) => {
520
+ this.printProcess = null;
521
+ if (this._status !== 'question' && localBuf.trim()) {
522
+ try {
523
+ const json = JSON.parse(localBuf.trim());
524
+ if (json?.type === 'error' || json?.type === 'turn.failed') {
525
+ hasStructuredError = true;
526
+ }
527
+ this.handleJSONMessage(json);
528
+ }
529
+ catch { }
530
+ }
531
+ if (this._status !== 'question' && code !== 0 && stderrBuf.trim() && !hasStructuredError) {
532
+ const errLines = stderrBuf.trim().split('\n').filter(l => l.trim());
533
+ for (const line of errLines) {
534
+ this.emit({ type: 'error', content: line.trim(), time: this.now() });
535
+ }
536
+ }
537
+ if (this._status !== 'confirm' && this._status !== 'question') {
538
+ this._status = 'idle';
539
+ this.emit({ type: 'status', status: 'idle' });
540
+ }
541
+ this.isRetrying = false;
542
+ });
543
+ proc.on('error', (err) => {
544
+ this.printProcess = null;
545
+ this.emit({ type: 'error', content: `CLI 执行错误: ${err.message}`, time: this.now() });
546
+ this._status = 'idle';
547
+ this.emit({ type: 'status', status: 'idle' });
548
+ this.isRetrying = false;
549
+ });
550
+ }
551
+ catch (err) {
552
+ this.emit({ type: 'error', content: `CLI 启动失败: ${err.message}`, time: this.now() });
553
+ this._status = 'idle';
554
+ this.emit({ type: 'status', status: 'idle' });
555
+ this.isRetrying = false;
556
+ }
557
+ }
558
+ handleStdout(data) {
559
+ const clean = stripAnsi(data);
560
+ this.stdoutBuffer += clean;
561
+ const lines = this.stdoutBuffer.split('\n');
562
+ this.stdoutBuffer = lines.pop() || '';
563
+ for (const line of lines) {
564
+ const trimmed = line.trim();
565
+ if (!trimmed)
566
+ continue;
567
+ this.processLine(trimmed);
568
+ }
569
+ }
570
+ processLine(line) {
571
+ try {
572
+ const json = JSON.parse(line);
573
+ this.handleJSONMessage(json);
574
+ return;
575
+ }
576
+ catch { }
577
+ if (this.isPromptLine(line)) {
578
+ this.flushPendingOutput();
579
+ this._status = 'idle';
580
+ this.emit({ type: 'status', status: 'idle' });
581
+ return;
582
+ }
583
+ if (CONFIRM_PATTERNS.some((p) => p.test(line))) {
584
+ this.flushPendingOutput();
585
+ this.handleConfirmation({ promptText: line });
586
+ return;
587
+ }
588
+ this.pendingOutput += (this.pendingOutput ? '\n' : '') + line;
589
+ }
590
+ isPromptLine(line) {
591
+ return /^\s*>\s*$/.test(line) ||
592
+ /^\s*›\s*$/.test(line) ||
593
+ /^\s*\$\s*$/.test(line);
594
+ }
595
+ flushPendingOutput() {
596
+ if (!this.pendingOutput.trim())
597
+ return;
598
+ const text = this.pendingOutput.trim();
599
+ this.pendingOutput = '';
600
+ if (this._status === 'idle' || this._status === 'running') {
601
+ this._status = 'running';
602
+ this.emit({ type: 'status', status: 'running' });
603
+ this.emit({ type: 'ai_message', content: text, time: this.now() });
604
+ }
605
+ }
606
+ tryParseJSON(val) {
607
+ if (typeof val !== 'string')
608
+ return null;
609
+ try {
610
+ const parsed = JSON.parse(val);
611
+ if (typeof parsed === 'object' && parsed !== null)
612
+ return parsed;
613
+ return null;
614
+ }
615
+ catch {
616
+ return null;
617
+ }
618
+ }
619
+ extractText(val) {
620
+ if (typeof val === 'string')
621
+ return val;
622
+ if (val == null)
623
+ return '';
624
+ if (Array.isArray(val)) {
625
+ return val
626
+ .map((block) => {
627
+ if (typeof block === 'string')
628
+ return block;
629
+ if (block?.type === 'text' && typeof block.text === 'string')
630
+ return block.text;
631
+ if (typeof block?.text === 'string')
632
+ return block.text;
633
+ if (typeof block?.content === 'string')
634
+ return block.content;
635
+ return '';
636
+ })
637
+ .filter(Boolean)
638
+ .join('\n');
639
+ }
640
+ if (typeof val === 'object') {
641
+ if (Array.isArray(val.content)) {
642
+ return this.extractText(val.content);
643
+ }
644
+ if (typeof val.text === 'string')
645
+ return val.text;
646
+ if (typeof val.message === 'string')
647
+ return val.message;
648
+ if (typeof val.content === 'string')
649
+ return val.content;
650
+ return '';
651
+ }
652
+ return String(val);
653
+ }
654
+ normalizeQuestionOptions(options) {
655
+ if (!Array.isArray(options))
656
+ return [];
657
+ return options
658
+ .map((opt) => {
659
+ if (typeof opt === 'string')
660
+ return { label: opt };
661
+ if (!opt || typeof opt !== 'object')
662
+ return null;
663
+ const label = typeof opt.label === 'string' ? opt.label : '';
664
+ if (!label)
665
+ return null;
666
+ return {
667
+ label,
668
+ description: typeof opt.description === 'string' ? opt.description : undefined,
669
+ markdown: typeof opt.markdown === 'string' ? opt.markdown : undefined,
670
+ };
671
+ })
672
+ .filter(Boolean);
673
+ }
674
+ normalizeAskUserQuestions(input) {
675
+ if (!input || typeof input !== 'object')
676
+ return [];
677
+ if (Array.isArray(input.questions)) {
678
+ return input.questions
679
+ .map((q) => {
680
+ if (typeof q === 'string') {
681
+ return { question: q, options: [] };
682
+ }
683
+ if (!q || typeof q !== 'object')
684
+ return null;
685
+ const question = typeof q.question === 'string' ? q.question : '';
686
+ if (!question)
687
+ return null;
688
+ return {
689
+ question,
690
+ header: typeof q.header === 'string' ? q.header : undefined,
691
+ options: this.normalizeQuestionOptions(q.options),
692
+ multiSelect: !!q.multiSelect,
693
+ };
694
+ })
695
+ .filter(Boolean);
696
+ }
697
+ if (typeof input.question === 'string' && input.question) {
698
+ return [{
699
+ question: input.question,
700
+ header: typeof input.header === 'string' ? input.header : undefined,
701
+ options: this.normalizeQuestionOptions(input.options),
702
+ multiSelect: !!input.multiSelect,
703
+ }];
704
+ }
705
+ return [];
706
+ }
707
+ isAskUserQuestionTool(toolName) {
708
+ return typeof toolName === 'string' && toolName === ASK_USER_QUESTION_TOOL;
709
+ }
710
+ isPermissionDenial(details) {
711
+ return /requested permissions/i.test(details);
712
+ }
713
+ shouldIgnoreAskUserQuestionDenial(toolName, toolUseId) {
714
+ return this.isAskUserQuestionTool(toolName) ||
715
+ (!!toolUseId && !!this.pendingQuestionToolUseId && toolUseId === this.pendingQuestionToolUseId);
716
+ }
717
+ buildQuestionAnswerPrompt(answer) {
718
+ return [
719
+ '用户刚刚回答了你通过 AskUserQuestion 提出的问题。',
720
+ '请将下面内容作为该问题的用户答案,并继续之前的任务。',
721
+ '',
722
+ `用户回答:${answer}`,
723
+ ].join('\n');
724
+ }
725
+ pausePrintProcessForQuestion() {
726
+ if (this.config.runMode !== 'print' || !this.printProcess)
727
+ return;
728
+ const proc = this.printProcess;
729
+ this.printProcess = null;
730
+ try {
731
+ proc.kill('SIGINT');
732
+ }
733
+ catch { }
734
+ }
735
+ emitCodexUsage(usage) {
736
+ if (!usage || typeof usage !== 'object')
737
+ return;
738
+ const resultDetails = { subtype: 'result' };
739
+ if (usage.input_tokens != null)
740
+ resultDetails.inputTokens = usage.input_tokens;
741
+ if (usage.output_tokens != null)
742
+ resultDetails.outputTokens = usage.output_tokens;
743
+ if (usage.cached_input_tokens != null)
744
+ resultDetails.cacheReadTokens = usage.cached_input_tokens;
745
+ if (usage.reasoning_output_tokens != null)
746
+ resultDetails.reasoningOutputTokens = usage.reasoning_output_tokens;
747
+ if (this.sessionId)
748
+ resultDetails.session_id = this.sessionId;
749
+ if (Object.keys(resultDetails).length > 1) {
750
+ this.emit({ type: 'system', content: '执行完成', time: this.now(), details: resultDetails });
751
+ }
752
+ }
753
+ handleCodexJSONMessage(msg) {
754
+ const type = msg.type;
755
+ if (type === 'thread.started') {
756
+ const threadId = msg.thread_id || msg.threadId;
757
+ if (threadId && typeof threadId === 'string') {
758
+ this.sessionId = threadId;
759
+ console.log(`[CLI] Codex threadId set: ${threadId.slice(0, 8)}...`);
760
+ this.emit({
761
+ type: 'system',
762
+ content: `Codex CLI 已初始化 (thread: ${threadId.slice(0, 8)}...)`,
763
+ time: this.now(),
764
+ details: { subtype: 'init', session_id: threadId },
765
+ });
766
+ }
767
+ return true;
768
+ }
769
+ if (type === 'turn.started') {
770
+ this._status = 'running';
771
+ this.emit({ type: 'status', status: 'running' });
772
+ return true;
773
+ }
774
+ if (type === 'turn.completed') {
775
+ this.emitCodexUsage(msg.usage);
776
+ if (this._status !== 'confirm' && this._status !== 'question') {
777
+ this._status = 'idle';
778
+ this.emit({ type: 'status', status: 'idle' });
779
+ }
780
+ this.hasStreamedText = false;
781
+ return true;
782
+ }
783
+ if (type === 'turn.failed' || type === 'error') {
784
+ const errorText = this.extractText(msg.error || msg.message || msg.content) || 'Codex CLI 执行失败';
785
+ this.emit({ type: 'error', content: errorText, time: this.now() });
786
+ if (this._status !== 'confirm' && this._status !== 'question') {
787
+ this._status = 'idle';
788
+ this.emit({ type: 'status', status: 'idle' });
789
+ }
790
+ return true;
791
+ }
792
+ if (type === 'item.started' || type === 'item.completed') {
793
+ const item = msg.item || {};
794
+ const itemType = item.type || 'item';
795
+ const itemId = item.id || undefined;
796
+ const isCompleted = type === 'item.completed';
797
+ if (itemType === 'command_execution') {
798
+ const command = item.command || 'command';
799
+ const output = item.aggregated_output || '';
800
+ const exitCode = item.exit_code;
801
+ const status = isCompleted
802
+ ? exitCode === 0 ? 'success' : 'failed'
803
+ : 'running';
804
+ const details = isCompleted
805
+ ? output || (exitCode != null ? `exit code: ${exitCode}` : undefined)
806
+ : command;
807
+ this.emit({
808
+ type: 'tool_call',
809
+ content: command,
810
+ toolName: 'Shell',
811
+ status,
812
+ details,
813
+ toolUseId: itemId,
814
+ time: this.now(),
815
+ });
816
+ return true;
817
+ }
818
+ const isAgentMessage = itemType === 'agent_message' ||
819
+ itemType === 'assistant_message' ||
820
+ (itemType === 'message' && item.role === 'assistant');
821
+ if (isCompleted && isAgentMessage) {
822
+ const text = this.extractText(item.text || item.content || item.message || item);
823
+ if (text) {
824
+ this.hasStreamedText = true;
825
+ this.emit({ type: 'ai_message', content: text, time: this.now() });
826
+ }
827
+ return true;
828
+ }
829
+ if (isCompleted && (itemType === 'reasoning' || itemType === 'thinking')) {
830
+ const text = this.extractText(item.text || item.content || item.message);
831
+ if (text) {
832
+ this.emit({ type: 'thinking_message', content: text, time: this.now() });
833
+ }
834
+ return true;
835
+ }
836
+ return true;
837
+ }
838
+ return typeof type === 'string' && type.includes('.');
839
+ }
840
+ handleJSONMessage(msg) {
841
+ if (this.getCLIType() === 'codex' && this.handleCodexJSONMessage(msg)) {
842
+ return;
843
+ }
844
+ const type = msg.type;
845
+ if (type === 'system') {
846
+ const inner = this.tryParseJSON(msg.content) || msg;
847
+ const sid = msg.session_id || inner.session_id || msg.id || inner.id;
848
+ if (sid && typeof sid === 'string' && sid.length > 8) {
849
+ this.sessionId = sid;
850
+ console.log(`[CLI] sessionId set from system message: ${this.sessionId.slice(0, 8)}...`);
851
+ }
852
+ if (inner.subtype === 'init') {
853
+ const model = inner.model || msg.model || '';
854
+ const toolsCount = Array.isArray(inner.tools) ? inner.tools.length : 0;
855
+ const initDetails = { subtype: 'init' };
856
+ if (model)
857
+ initDetails.model = model;
858
+ if (inner.session_id)
859
+ initDetails.session_id = inner.session_id;
860
+ if (toolsCount)
861
+ initDetails.toolsCount = toolsCount;
862
+ if (this.isRetrying) {
863
+ this.emit({ type: 'system', content: `会话已恢复 (session: ${inner.session_id?.slice(0, 8)}...)`, time: this.now(), details: initDetails });
864
+ }
865
+ else {
866
+ this.emit({ type: 'system', content: `CLI 已初始化 (session: ${inner.session_id?.slice(0, 8)}...)`, time: this.now(), details: initDetails });
867
+ }
868
+ }
869
+ else {
870
+ const text = this.extractText(msg.message || inner.message || inner.content) || '';
871
+ const sysDetails = {};
872
+ const sysUsage = inner.usage || {};
873
+ if (inner.subtype)
874
+ sysDetails.subtype = inner.subtype;
875
+ if (inner.model)
876
+ sysDetails.model = inner.model;
877
+ if (inner.session_id)
878
+ sysDetails.session_id = inner.session_id;
879
+ if (Array.isArray(inner.tools))
880
+ sysDetails.toolsCount = inner.tools.length;
881
+ if (inner.total_cost_usd != null)
882
+ sysDetails.costUsd = inner.total_cost_usd;
883
+ else if (inner.cost_usd != null)
884
+ sysDetails.costUsd = inner.cost_usd;
885
+ if (inner.duration_ms != null)
886
+ sysDetails.durationMs = inner.duration_ms;
887
+ if (inner.num_turns != null)
888
+ sysDetails.numTurns = inner.num_turns;
889
+ if (sysUsage.input_tokens != null)
890
+ sysDetails.inputTokens = sysUsage.input_tokens;
891
+ else if (inner.input_tokens != null)
892
+ sysDetails.inputTokens = inner.input_tokens;
893
+ if (sysUsage.output_tokens != null)
894
+ sysDetails.outputTokens = sysUsage.output_tokens;
895
+ else if (inner.output_tokens != null)
896
+ sysDetails.outputTokens = inner.output_tokens;
897
+ if (sysUsage.cache_read_input_tokens != null)
898
+ sysDetails.cacheReadTokens = sysUsage.cache_read_input_tokens;
899
+ if (sysUsage.cache_creation_input_tokens != null)
900
+ sysDetails.cacheCreationTokens = sysUsage.cache_creation_input_tokens;
901
+ // Generate meaningful content when extractText returns empty
902
+ let content = text;
903
+ if (!content) {
904
+ const subtype = inner.subtype;
905
+ if (subtype === 'result') {
906
+ content = '执行完成';
907
+ }
908
+ else if (subtype === 'init') {
909
+ content = `CLI 已初始化 (session: ${inner.session_id?.slice(0, 8)}...)`;
910
+ }
911
+ else if (subtype) {
912
+ const subtypeLabels = {
913
+ 'status': '状态更新',
914
+ 'error': '系统错误',
915
+ 'warning': '系统警告',
916
+ 'info': '系统通知',
917
+ };
918
+ content = subtypeLabels[subtype] || `系统事件: ${subtype}`;
919
+ }
920
+ else {
921
+ // No subtype, no text — try to describe from details
922
+ const parts = [];
923
+ if (inner.model)
924
+ parts.push(`模型 ${inner.model}`);
925
+ if (sysDetails.toolsCount)
926
+ parts.push(`${sysDetails.toolsCount} 个工具`);
927
+ if (inner.session_id)
928
+ parts.push(`session ${String(inner.session_id).slice(0, 8)}...`);
929
+ content = parts.length > 0 ? parts.join(' · ') : '系统消息';
930
+ }
931
+ }
932
+ this.emit({ type: 'system', content, time: this.now(), details: Object.keys(sysDetails).length > 0 ? sysDetails : undefined });
933
+ }
934
+ }
935
+ else if (type === 'assistant' || type === 'text' || type === 'content_block_delta') {
936
+ this._status = 'running';
937
+ this.emit({ type: 'status', status: 'running' });
938
+ if (type === 'content_block_delta' && msg.delta) {
939
+ if (msg.delta.type === 'thinking_delta' && msg.delta.thinking) {
940
+ this.emit({ type: 'thinking_message', content: msg.delta.thinking, time: this.now() });
941
+ }
942
+ else {
943
+ const text = this.extractText(msg.delta);
944
+ if (text) {
945
+ this.hasStreamedText = true;
946
+ this.emit({ type: 'ai_message', content: text, time: this.now() });
947
+ }
948
+ }
949
+ }
950
+ else {
951
+ const message = msg.message || msg;
952
+ const contentArr = Array.isArray(message.content) ? message.content : null;
953
+ if (contentArr) {
954
+ for (const block of contentArr) {
955
+ if (block.type === 'tool_use') {
956
+ this.emit({
957
+ type: 'tool_call',
958
+ content: block.name || 'tool',
959
+ toolName: block.name || 'unknown',
960
+ status: 'running',
961
+ details: block.input ? JSON.stringify(block.input, null, 2) : undefined,
962
+ toolUseId: block.id || undefined,
963
+ time: this.now(),
964
+ });
965
+ // Detect AskUserQuestion tool and present to user
966
+ if (block.name === 'AskUserQuestion' && block.input) {
967
+ this.handleAskUserQuestion(block.name, block.input, block.id);
968
+ return;
969
+ }
970
+ }
971
+ else if (block.type === 'text') {
972
+ if (block.text) {
973
+ this.hasStreamedText = true;
974
+ this.emit({ type: 'ai_message', content: block.text, time: this.now() });
975
+ }
976
+ }
977
+ else if (block.type === 'thinking') {
978
+ if (block.thinking) {
979
+ this.emit({ type: 'thinking_message', content: block.thinking, time: this.now() });
980
+ }
981
+ }
982
+ }
983
+ }
984
+ else {
985
+ const text = this.extractText(msg.content || msg.message || msg.text);
986
+ if (text) {
987
+ this.hasStreamedText = true;
988
+ this.emit({ type: 'ai_message', content: text, time: this.now() });
989
+ }
990
+ }
991
+ }
992
+ }
993
+ else if (type === 'content_block_start') {
994
+ if (msg.content_block?.type === 'tool_use') {
995
+ this._status = 'running';
996
+ this.emit({ type: 'status', status: 'running' });
997
+ this.emit({
998
+ type: 'tool_call',
999
+ content: msg.content_block.name || 'tool',
1000
+ toolName: msg.content_block.name || 'unknown',
1001
+ status: 'running',
1002
+ details: msg.content_block.input ? JSON.stringify(msg.content_block.input, null, 2) : undefined,
1003
+ toolUseId: msg.content_block.id || undefined,
1004
+ time: this.now(),
1005
+ });
1006
+ // Detect AskUserQuestion tool and present to user
1007
+ if (msg.content_block.name === 'AskUserQuestion' && msg.content_block.input) {
1008
+ this.handleAskUserQuestion(msg.content_block.name, msg.content_block.input, msg.content_block.id);
1009
+ return;
1010
+ }
1011
+ }
1012
+ }
1013
+ else if (type === 'content_block_stop') {
1014
+ // block ended
1015
+ }
1016
+ else if (type === 'tool_use' || type === 'tool') {
1017
+ this._status = 'running';
1018
+ this.emit({ type: 'status', status: 'running' });
1019
+ this.emit({
1020
+ type: 'tool_call',
1021
+ content: msg.name || 'tool',
1022
+ toolName: msg.name || 'unknown',
1023
+ status: 'running',
1024
+ details: msg.input ? JSON.stringify(msg.input, null, 2) : undefined,
1025
+ toolUseId: msg.id || undefined,
1026
+ time: this.now(),
1027
+ });
1028
+ // Detect AskUserQuestion tool and present to user
1029
+ if (msg.name === 'AskUserQuestion' && msg.input) {
1030
+ this.handleAskUserQuestion(msg.name, msg.input, msg.id);
1031
+ return;
1032
+ }
1033
+ }
1034
+ else if (type === 'user') {
1035
+ const message = msg.message || msg;
1036
+ const contentArr = Array.isArray(message.content) ? message.content : null;
1037
+ if (contentArr) {
1038
+ for (const block of contentArr) {
1039
+ if (block.type === 'tool_result') {
1040
+ if (block.tool_use_id && this.answeredQuestionToolUseIds.has(block.tool_use_id)) {
1041
+ continue;
1042
+ }
1043
+ const isError = block.is_error || false;
1044
+ const details = typeof block.content === 'string' ? block.content : this.extractText(block.content);
1045
+ if (isError && details && this.isPermissionDenial(details)) {
1046
+ const toolName = this.extractToolNameFromDenial(details);
1047
+ if (this.shouldIgnoreAskUserQuestionDenial(toolName, block.tool_use_id)) {
1048
+ continue;
1049
+ }
1050
+ const category = getToolCategory(toolName);
1051
+ this.pendingDenials.push({
1052
+ toolName,
1053
+ category,
1054
+ toolUseId: block.tool_use_id,
1055
+ input: msg.tool_input || {},
1056
+ description: details,
1057
+ });
1058
+ this.emit({
1059
+ type: 'tool_call',
1060
+ content: `权限被拒绝: ${toolName}`,
1061
+ toolName,
1062
+ status: 'failed',
1063
+ details,
1064
+ toolUseId: block.tool_use_id || undefined,
1065
+ time: this.now(),
1066
+ });
1067
+ }
1068
+ else {
1069
+ this.emit({
1070
+ type: 'tool_call',
1071
+ content: isError ? '工具执行失败' : '工具执行完成',
1072
+ toolName: block.tool_use_id || 'result',
1073
+ status: isError ? 'failed' : 'success',
1074
+ details: details || undefined,
1075
+ toolUseId: block.tool_use_id || undefined,
1076
+ time: this.now(),
1077
+ });
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+ else if (type === 'tool_result') {
1084
+ if (msg.tool_use_id && this.answeredQuestionToolUseIds.has(msg.tool_use_id)) {
1085
+ return;
1086
+ }
1087
+ const isError = msg.is_error || false;
1088
+ const details = this.extractText(msg.content);
1089
+ if (isError && details && this.isPermissionDenial(details)) {
1090
+ const toolName = msg.tool_name || this.extractToolNameFromDenial(details);
1091
+ if (this.shouldIgnoreAskUserQuestionDenial(toolName, msg.tool_use_id)) {
1092
+ return;
1093
+ }
1094
+ const category = getToolCategory(toolName);
1095
+ this.pendingDenials.push({
1096
+ toolName,
1097
+ category,
1098
+ toolUseId: msg.tool_use_id,
1099
+ input: msg.tool_input || {},
1100
+ description: details,
1101
+ });
1102
+ this.emit({
1103
+ type: 'tool_call',
1104
+ content: `权限被拒绝: ${toolName}`,
1105
+ toolName,
1106
+ status: 'failed',
1107
+ details,
1108
+ toolUseId: msg.tool_use_id || undefined,
1109
+ time: this.now(),
1110
+ });
1111
+ }
1112
+ else {
1113
+ this.emit({
1114
+ type: 'tool_call',
1115
+ content: isError ? '工具执行失败' : '工具执行完成',
1116
+ toolName: msg.tool_use_id || 'result',
1117
+ status: isError ? 'failed' : 'success',
1118
+ details: details || undefined,
1119
+ toolUseId: msg.tool_use_id || undefined,
1120
+ time: this.now(),
1121
+ });
1122
+ }
1123
+ }
1124
+ else if (type === 'permission_request') {
1125
+ this.handlePermissionRequest(msg);
1126
+ }
1127
+ else if (type === 'confirm' || type === 'approval_request') {
1128
+ this.handleConfirmation({
1129
+ promptText: this.extractText(msg.message || msg.content || msg.prompt) || 'CLI 请求确认',
1130
+ });
1131
+ }
1132
+ else if (type === 'result' || type === 'done' || type === 'complete') {
1133
+ const sid = msg.session_id || msg.id;
1134
+ if (sid && typeof sid === 'string' && sid.length > 8) {
1135
+ this.sessionId = sid;
1136
+ console.log(`[CLI] sessionId set from result message: ${this.sessionId.slice(0, 8)}...`);
1137
+ }
1138
+ const denials = (msg.permission_denials || this.pendingDenials).filter((denial) => {
1139
+ const toolName = denial.tool_name || denial.toolName || this.extractToolNameFromDenial(denial.description || '');
1140
+ return !this.shouldIgnoreAskUserQuestionDenial(toolName, denial.tool_use_id || denial.toolUseId);
1141
+ });
1142
+ if (denials && denials.length > 0 && this.config.confirmMode !== 'auto') {
1143
+ this._status = 'confirm';
1144
+ this.emit({ type: 'status', status: 'confirm' });
1145
+ const denial = denials[0];
1146
+ const toolName = denial.tool_name || denial.toolName || 'unknown';
1147
+ const category = getToolCategory(toolName);
1148
+ const input = denial.tool_input || denial.input || {};
1149
+ let filePath = '';
1150
+ let command = '';
1151
+ let diffContent = '';
1152
+ if (category === 'file') {
1153
+ filePath = input.file_path || input.path || '';
1154
+ if (input.content) {
1155
+ diffContent = typeof input.content === 'string' ? input.content : JSON.stringify(input.content, null, 2);
1156
+ }
1157
+ if (input.old_string || input.new_string) {
1158
+ diffContent = '';
1159
+ if (input.old_string)
1160
+ diffContent += `--- 原内容\n${input.old_string}\n`;
1161
+ if (input.new_string)
1162
+ diffContent += `+++ 新内容\n${input.new_string}\n`;
1163
+ }
1164
+ }
1165
+ else if (category === 'command') {
1166
+ command = input.command || input.cmd || '';
1167
+ }
1168
+ this.emit({
1169
+ type: 'confirm_request',
1170
+ content: `${toolName} 请求权限: ${denial.description || denial.tool_use_id || ''}`,
1171
+ time: this.now(),
1172
+ permission: {
1173
+ toolName,
1174
+ category,
1175
+ filePath,
1176
+ command,
1177
+ diffContent,
1178
+ input: Object.keys(input).length > 0 ? input : undefined,
1179
+ description: denial.description || '',
1180
+ },
1181
+ });
1182
+ }
1183
+ else if (this._status !== 'question') {
1184
+ this._status = 'idle';
1185
+ this.emit({ type: 'status', status: 'idle' });
1186
+ }
1187
+ const text = this.extractText(msg.content || msg.message || msg.result);
1188
+ if (text && !this.hasStreamedText) {
1189
+ this.emit({ type: 'ai_message', content: text, time: this.now() });
1190
+ }
1191
+ this.hasStreamedText = false;
1192
+ // Emit result statistics as a system message
1193
+ const resultDetails = {};
1194
+ const usage = msg.usage || {};
1195
+ if (msg.total_cost_usd != null)
1196
+ resultDetails.costUsd = msg.total_cost_usd;
1197
+ else if (msg.cost_usd != null)
1198
+ resultDetails.costUsd = msg.cost_usd;
1199
+ if (msg.duration_ms != null)
1200
+ resultDetails.durationMs = msg.duration_ms;
1201
+ if (msg.duration_api_ms != null)
1202
+ resultDetails.durationApiMs = msg.duration_api_ms;
1203
+ if (msg.num_turns != null)
1204
+ resultDetails.numTurns = msg.num_turns;
1205
+ if (msg.model)
1206
+ resultDetails.model = msg.model;
1207
+ else if (msg.modelUsage && typeof msg.modelUsage === 'object') {
1208
+ const models = Object.keys(msg.modelUsage);
1209
+ if (models.length > 0)
1210
+ resultDetails.model = models[0];
1211
+ }
1212
+ if (msg.session_id)
1213
+ resultDetails.session_id = msg.session_id;
1214
+ if (usage.input_tokens != null)
1215
+ resultDetails.inputTokens = usage.input_tokens;
1216
+ else if (msg.input_tokens != null)
1217
+ resultDetails.inputTokens = msg.input_tokens;
1218
+ if (usage.output_tokens != null)
1219
+ resultDetails.outputTokens = usage.output_tokens;
1220
+ else if (msg.output_tokens != null)
1221
+ resultDetails.outputTokens = msg.output_tokens;
1222
+ if (usage.cache_read_input_tokens != null)
1223
+ resultDetails.cacheReadTokens = usage.cache_read_input_tokens;
1224
+ if (usage.cache_creation_input_tokens != null)
1225
+ resultDetails.cacheCreationTokens = usage.cache_creation_input_tokens;
1226
+ if (Object.keys(resultDetails).length > 0) {
1227
+ this.emit({ type: 'system', content: '执行完成', time: this.now(), details: { subtype: 'result', ...resultDetails } });
1228
+ }
1229
+ }
1230
+ else {
1231
+ const text = this.extractText(msg.content || msg.message || msg.text);
1232
+ if (text) {
1233
+ this._status = 'running';
1234
+ this.emit({ type: 'status', status: 'running' });
1235
+ this.emit({ type: 'ai_message', content: text, time: this.now() });
1236
+ }
1237
+ }
1238
+ }
1239
+ handleAskUserQuestion(toolName, input, toolUseId) {
1240
+ const questions = this.normalizeAskUserQuestions(input);
1241
+ if (questions.length === 0)
1242
+ return false;
1243
+ if (toolUseId && this.pendingQuestionToolUseId === toolUseId) {
1244
+ if (this._status !== 'question') {
1245
+ this._status = 'question';
1246
+ this.emit({ type: 'status', status: 'question' });
1247
+ }
1248
+ return true;
1249
+ }
1250
+ this._status = 'question';
1251
+ this.pendingQuestionToolUseId = toolUseId || null;
1252
+ this.pendingQuestionNeedsPermissionApproval = false;
1253
+ this.emit({ type: 'status', status: 'question' });
1254
+ this.emit({
1255
+ type: 'ask_question',
1256
+ content: 'Agent 提出问题',
1257
+ questions,
1258
+ toolUseId: toolUseId || undefined,
1259
+ time: this.now(),
1260
+ });
1261
+ this.pausePrintProcessForQuestion();
1262
+ return true;
1263
+ }
1264
+ questionResponse(answer, toolUseId) {
1265
+ if (this._status !== 'question')
1266
+ return;
1267
+ const pendingToolUseId = this.pendingQuestionToolUseId;
1268
+ if (toolUseId && pendingToolUseId && toolUseId !== pendingToolUseId)
1269
+ return;
1270
+ if (this.pendingQuestionNeedsPermissionApproval) {
1271
+ this.writeToActiveProcess('y');
1272
+ this.pendingQuestionNeedsPermissionApproval = false;
1273
+ }
1274
+ const delivered = this.writeToActiveProcess(answer);
1275
+ this._status = 'running';
1276
+ this.pendingQuestionToolUseId = null;
1277
+ this.emit({ type: 'status', status: 'running' });
1278
+ this.emit({
1279
+ type: 'ask_question_result',
1280
+ approved: true,
1281
+ answer,
1282
+ toolUseId: pendingToolUseId || toolUseId,
1283
+ time: this.now(),
1284
+ });
1285
+ if (pendingToolUseId) {
1286
+ this.answeredQuestionToolUseIds.add(pendingToolUseId);
1287
+ this.emit({
1288
+ type: 'tool_call',
1289
+ content: '用户已回答',
1290
+ toolName: ASK_USER_QUESTION_TOOL,
1291
+ status: 'success',
1292
+ details: answer,
1293
+ toolUseId: pendingToolUseId,
1294
+ time: this.now(),
1295
+ });
1296
+ }
1297
+ if (this.config.runMode === 'print' && !delivered) {
1298
+ this.runPrintCommand(this.buildQuestionAnswerPrompt(answer), true);
1299
+ }
1300
+ }
1301
+ extractToolNameFromDenial(text) {
1302
+ const patterns = [
1303
+ /permissions?\s+to\s+(?:use|call|run|execute)\s+([A-Za-z0-9_.:-]+)/i,
1304
+ /permissions?\s+(?:to|for)\s+([A-Za-z0-9_.:-]+)/i,
1305
+ /tool\s+([A-Za-z0-9_.:-]+)/i,
1306
+ ];
1307
+ for (const pattern of patterns) {
1308
+ const match = text.match(pattern);
1309
+ if (match)
1310
+ return match[1];
1311
+ }
1312
+ return 'unknown';
1313
+ }
1314
+ handlePermissionRequest(msg) {
1315
+ const toolName = msg.tool_name || msg.toolName || msg.name || 'unknown';
1316
+ if (this.isAskUserQuestionTool(toolName)) {
1317
+ this.allowedTools.add(ASK_USER_QUESTION_TOOL);
1318
+ const input = msg.input || msg.tool_input || {};
1319
+ const handled = this.handleAskUserQuestion(toolName, input, msg.tool_use_id || msg.toolUseId || msg.id);
1320
+ this.pendingQuestionNeedsPermissionApproval = handled && this.config.runMode !== 'print';
1321
+ return;
1322
+ }
1323
+ if (this.config.confirmMode === 'auto') {
1324
+ this.writeToActiveProcess('y');
1325
+ this.emit({ type: 'system', content: '已自动批准操作', time: this.now() });
1326
+ return;
1327
+ }
1328
+ const category = getToolCategory(toolName);
1329
+ const input = msg.input || msg.tool_input || {};
1330
+ const description = this.extractText(msg.description || msg.message || msg.content) || '';
1331
+ let filePath = '';
1332
+ let command = '';
1333
+ let diffContent = '';
1334
+ if (category === 'file') {
1335
+ filePath = input.file_path || input.path || input.filename || '';
1336
+ if (input.content) {
1337
+ diffContent = typeof input.content === 'string' ? input.content : JSON.stringify(input.content, null, 2);
1338
+ }
1339
+ if (input.old_string || input.new_string) {
1340
+ diffContent = '';
1341
+ if (input.old_string) {
1342
+ diffContent += `--- 原内容\n${input.old_string}\n`;
1343
+ }
1344
+ if (input.new_string) {
1345
+ diffContent += `+++ 新内容\n${input.new_string}\n`;
1346
+ }
1347
+ }
1348
+ }
1349
+ else if (category === 'command') {
1350
+ command = input.command || input.cmd || '';
1351
+ }
1352
+ this._status = 'confirm';
1353
+ this.emit({ type: 'status', status: 'confirm' });
1354
+ this.emit({
1355
+ type: 'confirm_request',
1356
+ content: description || `${toolName} 请求权限`,
1357
+ time: this.now(),
1358
+ permission: {
1359
+ toolName,
1360
+ category,
1361
+ filePath,
1362
+ command,
1363
+ diffContent,
1364
+ input: Object.keys(input).length > 0 ? input : undefined,
1365
+ description,
1366
+ },
1367
+ });
1368
+ }
1369
+ handleConfirmation(opts) {
1370
+ if (this.config.confirmMode === 'auto') {
1371
+ this.writeToActiveProcess('y');
1372
+ this.emit({ type: 'system', content: '已自动批准操作', time: this.now() });
1373
+ return;
1374
+ }
1375
+ this._status = 'confirm';
1376
+ this.emit({ type: 'status', status: 'confirm' });
1377
+ this.emit({
1378
+ type: 'confirm_request',
1379
+ content: opts.promptText,
1380
+ time: this.now(),
1381
+ permission: opts.permission,
1382
+ });
1383
+ }
1384
+ writeToActiveProcess(text) {
1385
+ const target = this.printProcess || this.process;
1386
+ if (target?.stdin?.writable) {
1387
+ try {
1388
+ target.stdin.write(text + '\n');
1389
+ return true;
1390
+ }
1391
+ catch {
1392
+ return false;
1393
+ }
1394
+ }
1395
+ return false;
1396
+ }
1397
+ sendInput(text) {
1398
+ if (this._status === 'question') {
1399
+ this.questionResponse(text);
1400
+ return;
1401
+ }
1402
+ if (this.config.runMode === 'print' && this._status !== 'confirm') {
1403
+ this.runPrintCommand(text);
1404
+ return;
1405
+ }
1406
+ const target = this.printProcess || this.process;
1407
+ if (!target?.stdin?.writable) {
1408
+ this.emit({ type: 'error', content: 'CLI 未连接,无法发送', time: this.now() });
1409
+ return;
1410
+ }
1411
+ target.stdin.write(text + '\n');
1412
+ if (this._status === 'confirm') {
1413
+ this._status = 'running';
1414
+ this.emit({ type: 'status', status: 'running' });
1415
+ }
1416
+ else if (this._status === 'idle') {
1417
+ this._status = 'running';
1418
+ this.emit({ type: 'status', status: 'running' });
1419
+ }
1420
+ }
1421
+ confirmResponse(approved) {
1422
+ if (approved && this.pendingDenials.length > 0) {
1423
+ for (const denial of this.pendingDenials) {
1424
+ const toolName = denial.tool_name || denial.toolName;
1425
+ if (toolName) {
1426
+ this.allowedTools.add(toolName);
1427
+ }
1428
+ }
1429
+ this.pendingDenials = [];
1430
+ this.emit({
1431
+ type: 'confirm_result',
1432
+ approved: true,
1433
+ time: this.now(),
1434
+ });
1435
+ this.emit({ type: 'system', content: `已授权: ${Array.from(this.allowedTools).join(', ')}`, time: this.now() });
1436
+ this.retryWithPermission();
1437
+ return;
1438
+ }
1439
+ this.pendingDenials = [];
1440
+ this._status = 'idle';
1441
+ this.emit({ type: 'status', status: 'idle' });
1442
+ this.emit({
1443
+ type: 'confirm_result',
1444
+ approved,
1445
+ time: this.now(),
1446
+ });
1447
+ }
1448
+ interrupt() {
1449
+ if (this.printProcess) {
1450
+ this.printProcess.kill('SIGINT');
1451
+ this.printProcess = null;
1452
+ }
1453
+ if (this.process) {
1454
+ this.process.kill('SIGINT');
1455
+ }
1456
+ this._status = 'idle';
1457
+ this.pendingQuestionNeedsPermissionApproval = false;
1458
+ this.emit({ type: 'status', status: 'idle' });
1459
+ this.emit({ type: 'system', content: '任务已中断', time: this.now() });
1460
+ }
1461
+ restart() {
1462
+ this.sessionId = null;
1463
+ this.pendingQuestionToolUseId = null;
1464
+ this.pendingQuestionNeedsPermissionApproval = false;
1465
+ this.answeredQuestionToolUseIds.clear();
1466
+ if (this.printProcess) {
1467
+ this.printProcess.kill('SIGTERM');
1468
+ this.printProcess = null;
1469
+ }
1470
+ if (this.config.runMode === 'interactive') {
1471
+ this.spawnInteractive();
1472
+ }
1473
+ else {
1474
+ this._status = 'idle';
1475
+ this.emit({ type: 'status', status: 'idle' });
1476
+ this.emit({ type: 'system', content: 'CLI 已重置', time: this.now() });
1477
+ }
1478
+ }
1479
+ setConfirmMode(mode) {
1480
+ this.config.confirmMode = mode;
1481
+ }
1482
+ restoreSessionId(sid) {
1483
+ if (sid && typeof sid === 'string' && sid.length > 8) {
1484
+ this.sessionId = sid;
1485
+ console.log(`[CLI] Restored sessionId: ${sid.slice(0, 8)}...`);
1486
+ }
1487
+ }
1488
+ clearSessionId() {
1489
+ this.sessionId = null;
1490
+ this.pendingQuestionToolUseId = null;
1491
+ this.pendingQuestionNeedsPermissionApproval = false;
1492
+ this.answeredQuestionToolUseIds.clear();
1493
+ }
1494
+ restoreWorkDir(workDir) {
1495
+ if (workDir && typeof workDir === 'string') {
1496
+ const resolved = workDir.startsWith('~') ? workDir.replace('~', os.homedir()) : workDir;
1497
+ try {
1498
+ const stat = fs.statSync(resolved);
1499
+ if (stat.isDirectory()) {
1500
+ this.config.workDir = resolved;
1501
+ console.log(`[CLI] Restored workDir: ${resolved}`);
1502
+ }
1503
+ else {
1504
+ console.log(`[CLI] Saved workDir is not a directory, using default: ${this.config.workDir}`);
1505
+ }
1506
+ }
1507
+ catch {
1508
+ console.log(`[CLI] Saved workDir does not exist: ${resolved}, using default: ${this.config.workDir}`);
1509
+ }
1510
+ }
1511
+ }
1512
+ killProcess() {
1513
+ if (this.process) {
1514
+ try {
1515
+ this.process.kill('SIGTERM');
1516
+ }
1517
+ catch { }
1518
+ this.process = null;
1519
+ }
1520
+ if (this.printProcess) {
1521
+ try {
1522
+ this.printProcess.kill('SIGTERM');
1523
+ }
1524
+ catch { }
1525
+ this.printProcess = null;
1526
+ }
1527
+ }
1528
+ destroy() {
1529
+ this.killProcess();
1530
+ this.onEvent = null;
1531
+ }
1532
+ }