@adhdev/daemon-core 0.8.29 → 0.8.31

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 (63) hide show
  1. package/dist/agent-stream/manager.d.ts +1 -0
  2. package/dist/agent-stream/provider-adapter.d.ts +1 -0
  3. package/dist/agent-stream/types.d.ts +3 -0
  4. package/dist/boot/daemon-lifecycle.d.ts +2 -1
  5. package/dist/cdp/manager.d.ts +2 -0
  6. package/dist/cli-adapter-types.d.ts +34 -5
  7. package/dist/cli-adapters/provider-cli-adapter.d.ts +4 -158
  8. package/dist/cli-adapters/provider-cli-config.d.ts +30 -0
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +42 -0
  10. package/dist/cli-adapters/provider-cli-runtime.d.ts +29 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +158 -0
  12. package/dist/commands/handler.d.ts +4 -3
  13. package/dist/config/config.d.ts +4 -3
  14. package/dist/index.js +1033 -621
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +1035 -624
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/providers/acp-provider-instance.d.ts +1 -0
  19. package/dist/providers/approval-utils.d.ts +7 -0
  20. package/dist/providers/cli-provider-instance.d.ts +2 -0
  21. package/dist/providers/contracts.d.ts +12 -1
  22. package/dist/providers/ide-provider-instance.d.ts +1 -0
  23. package/dist/providers/provider-loader.d.ts +3 -0
  24. package/dist/status/reporter.d.ts +2 -3
  25. package/dist/status/snapshot.d.ts +2 -1
  26. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  27. package/package.json +3 -1
  28. package/src/agent-stream/manager.ts +8 -2
  29. package/src/agent-stream/poller.ts +57 -6
  30. package/src/agent-stream/provider-adapter.ts +11 -7
  31. package/src/agent-stream/types.ts +3 -0
  32. package/src/boot/daemon-lifecycle.ts +7 -6
  33. package/src/cdp/initializer.ts +2 -2
  34. package/src/cdp/manager.ts +5 -0
  35. package/src/cdp/setup.ts +1 -1
  36. package/src/cli-adapter-types.ts +37 -5
  37. package/src/cli-adapters/provider-cli-adapter.ts +212 -795
  38. package/src/cli-adapters/provider-cli-config.ts +66 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +202 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +142 -0
  41. package/src/cli-adapters/provider-cli-shared.ts +439 -0
  42. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +1 -1
  43. package/src/commands/cdp-commands.ts +6 -1
  44. package/src/commands/chat-commands.ts +45 -29
  45. package/src/commands/cli-manager.ts +28 -9
  46. package/src/commands/handler.ts +14 -10
  47. package/src/commands/router.ts +23 -10
  48. package/src/commands/stream-commands.ts +11 -5
  49. package/src/config/config.ts +4 -10
  50. package/src/daemon/dev-auto-implement.ts +22 -18
  51. package/src/daemon/dev-cli-debug.ts +59 -16
  52. package/src/daemon/dev-server.ts +67 -43
  53. package/src/providers/acp-provider-instance.ts +18 -3
  54. package/src/providers/approval-utils.ts +66 -0
  55. package/src/providers/cli-provider-instance.ts +32 -6
  56. package/src/providers/contracts.d.ts +1 -0
  57. package/src/providers/contracts.ts +15 -2
  58. package/src/providers/extension-provider-instance.ts +1 -1
  59. package/src/providers/ide-provider-instance.ts +67 -41
  60. package/src/providers/provider-loader.ts +110 -55
  61. package/src/providers/version-archive.ts +23 -5
  62. package/src/status/reporter.ts +18 -14
  63. package/src/status/snapshot.ts +5 -4
@@ -15,128 +15,74 @@
15
15
  */
16
16
 
17
17
  import * as os from 'os';
18
- import * as path from 'path';
19
- import { execSync } from 'child_process';
20
18
  import type { CliAdapter } from '../cli-adapter-types.js';
21
19
  import { LOG } from '../logging/logger.js';
22
20
  import { TerminalScreen } from './terminal-screen.js';
23
- import type { ProviderResumeCapability } from '../providers/contracts.js';
24
21
  import {
25
22
  NodePtyTransportFactory,
26
23
  type PtyRuntimeMetadata,
27
24
  type PtyRuntimeTransport,
28
25
  type PtyTransportFactory,
29
26
  } from './pty-transport.js';
30
- import { sanitizeSpawnEnv } from './spawn-env.js';
31
-
32
- // ─── Types ──────────────────────────────────────────
33
-
34
- export interface CliChatMessage {
35
- role: 'user' | 'assistant';
36
- content: string;
37
- timestamp?: number;
38
- receivedAt?: number;
39
- kind?: string;
40
- id?: string;
41
- index?: number;
42
- meta?: Record<string, any>;
43
- senderName?: string;
44
- }
27
+ import {
28
+ buildCliScreenSnapshot,
29
+ compactPromptText,
30
+ estimatePromptDisplayLines,
31
+ extractPromptRetrySnippet,
32
+ getLastUserPromptText,
33
+ listCliScriptNames,
34
+ looksLikeConfirmOnlyLabel,
35
+ normalizePromptText,
36
+ normalizeScreenSnapshot,
37
+ promptLikelyVisible,
38
+ sanitizeTerminalText,
39
+ trimPromptEchoPrefix,
40
+ type CliChatMessage,
41
+ type CliProviderModule,
42
+ type CliScriptInput,
43
+ type CliScripts,
44
+ type CliSessionStatus,
45
+ type CliTraceEntry,
46
+ } from './provider-cli-shared.js';
47
+ import {
48
+ buildCliParseInput,
49
+ buildCliTraceParseSnapshot,
50
+ hydrateCliParsedMessages,
51
+ normalizeCliParsedMessages,
52
+ summarizeCliTraceMessages,
53
+ summarizeCliTraceText,
54
+ type TurnParseScope,
55
+ } from './provider-cli-parse.js';
56
+ import {
57
+ resolveCliAdapterConfig,
58
+ type ProviderResolutionMeta,
59
+ } from './provider-cli-config.js';
60
+ import {
61
+ buildCliLoginShellRetry,
62
+ getCliSpawnErrorHint,
63
+ resolveCliSpawnPlan,
64
+ respondToCliTerminalQueries,
65
+ } from './provider-cli-runtime.js';
66
+
67
+ export {
68
+ normalizeCliProviderForRuntime,
69
+ type CliApprovalInput,
70
+ type CliChatMessage,
71
+ type CliProviderModule,
72
+ type CliScreenLine,
73
+ type CliScreenSnapshot,
74
+ type CliScriptInput,
75
+ type CliScripts,
76
+ type CliSessionStatus,
77
+ type CliStatusInput,
78
+ type CliTraceEntry,
79
+ } from './provider-cli-shared.js';
45
80
 
46
81
  type SeedCliChatMessage = Omit<Partial<CliChatMessage>, 'role'> & {
47
82
  role?: string;
48
83
  content?: string;
49
84
  };
50
85
 
51
- export interface CliSessionStatus {
52
- status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
53
- messages: CliChatMessage[];
54
- workingDir: string;
55
- activeModal: { message: string; buttons: string[] } | null;
56
- }
57
-
58
- /**
59
- * CLI Script Functions.
60
- * Unlike IDE scripts (which return JS code strings for CDP evaluate),
61
- * CLI scripts are Node.js functions that receive PTY buffer data and return structured results.
62
- */
63
- export interface CliScripts {
64
- /** Full PTY buffer → ReadChatResult (messages, status, activeModal) */
65
- parseOutput?: (input: CliScriptInput) => any;
66
- /** Lightweight status detection (high-frequency polling) → AgentStatus string */
67
- detectStatus?: (input: CliStatusInput) => string | null;
68
- /** Parse approval modal from PTY output → ModalInfo | null */
69
- parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
70
- /** Produce a cli-specific prompt from a dashboard action payload */
71
- resolveAction?: (data: any) => string;
72
- /** Custom scripts */
73
- [name: string]: ((input: any) => any) | undefined;
74
- }
75
-
76
- export interface CliScreenLine {
77
- index: number;
78
- fromTop: number;
79
- fromBottom: number;
80
- text: string;
81
- trimmed: string;
82
- isEmpty: boolean;
83
- }
84
-
85
- export interface CliScreenSnapshot {
86
- text: string;
87
- lineCount: number;
88
- lines: CliScreenLine[];
89
- nonEmptyLines: CliScreenLine[];
90
- firstNonEmptyLineIndex: number;
91
- lastNonEmptyLineIndex: number;
92
- firstNonEmptyLine: CliScreenLine | null;
93
- lastNonEmptyLine: CliScreenLine | null;
94
- promptLineIndex: number;
95
- promptLine: CliScreenLine | null;
96
- linesAbovePrompt: CliScreenLine[];
97
- linesBelowPrompt: CliScreenLine[];
98
- }
99
-
100
- export interface CliScriptInput {
101
- buffer: string; // Full ANSI-stripped accumulated PTY output
102
- rawBuffer: string; // Raw PTY output (with ANSI)
103
- recentBuffer: string; // Recent 1000 chars (ANSI-stripped)
104
- screenText: string; // Current visible screen snapshot
105
- screen: CliScreenSnapshot;
106
- bufferScreen: CliScreenSnapshot;
107
- recentScreen: CliScreenSnapshot;
108
- messages: CliChatMessage[]; // Previously parsed messages
109
- partialResponse: string; // Current partial response being generated
110
- promptText?: string; // Current turn prompt when available
111
- settings?: Record<string, any>;
112
- args?: Record<string, any>;
113
- }
114
-
115
- export interface CliStatusInput {
116
- tail: string;
117
- screenText?: string;
118
- rawBuffer?: string;
119
- screen: CliScreenSnapshot;
120
- tailScreen: CliScreenSnapshot;
121
- }
122
-
123
- export interface CliApprovalInput {
124
- buffer: string;
125
- screenText?: string;
126
- rawBuffer?: string;
127
- tail: string;
128
- screen: CliScreenSnapshot;
129
- bufferScreen: CliScreenSnapshot;
130
- tailScreen: CliScreenSnapshot;
131
- }
132
-
133
- interface TurnParseScope {
134
- prompt: string;
135
- startedAt: number;
136
- bufferStart: number;
137
- rawBufferStart: number;
138
- }
139
-
140
86
  interface IdleFinishCandidate {
141
87
  armedAt: number;
142
88
  lastOutputAt: number;
@@ -145,369 +91,6 @@ interface IdleFinishCandidate {
145
91
  assistantLength: number;
146
92
  }
147
93
 
148
- export interface CliTraceEntry {
149
- id: number;
150
- at: number;
151
- type: string;
152
- status: CliSessionStatus['status'];
153
- isWaitingForResponse: boolean;
154
- activeModal: { message: string; buttons: string[] } | null;
155
- payload: Record<string, any>;
156
- }
157
-
158
- export interface CliProviderModule {
159
- type: string;
160
- name: string;
161
- category: 'cli';
162
- binary: string;
163
- sendDelayMs?: number;
164
- sendKey?: string;
165
- submitStrategy?: 'wait_for_echo' | 'immediate';
166
- spawn: {
167
- command: string;
168
- args: string[];
169
- shell: boolean;
170
- env: Record<string, string>;
171
- };
172
- timeouts?: {
173
- /** PTY output batch transmit interval (default 50ms) */
174
- ptyFlush?: number;
175
- /** Wait for startup dialog auto-proceed (default 300ms) */
176
- dialogAccept?: number;
177
- /** Approval detect cooldown (default 2000ms) */
178
- approvalCooldown?: number;
179
- /** Check for completion on no-response during generating (default 6000ms) */
180
- generatingIdle?: number;
181
- /** Check for completion on no-response (default 5000ms) */
182
- idleFinish?: number;
183
- /** Max response wait (default 300000ms = 5min) */
184
- maxResponse?: number;
185
- /** shutdown after kill wait (default 1000ms) */
186
- shutdownGrace?: number;
187
- /** Output settle debounce before evaluating status (default 300ms) */
188
- outputSettle?: number;
189
- };
190
- resume?: ProviderResumeCapability;
191
- }
192
-
193
- // ─── Utility Functions ──────────────────────────────
194
-
195
- function stripAnsi(str: string): string {
196
- // eslint-disable-next-line no-control-regex
197
- return str
198
- // OSC sequences (title bar etc) — strip before generic ESC removal so payload cannot leak.
199
- .replace(/\x1B\][^\x07]*\x07/g, '')
200
- .replace(/\x1B\][\s\S]*?\x1B\\/g, '')
201
- // DCS / APC / PM / SOS control strings terminated by ST or BEL.
202
- .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
203
- // Cursor movement sequences → space (prevents word concatenation)
204
- .replace(/\x1B\[\d*[A-HJKSTfG]/g, ' ')
205
- // SGR and other CSI sequences → remove
206
- .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
207
- // Collapse multiple spaces
208
- .replace(/ +/g, ' ');
209
- }
210
-
211
- function stripTerminalNoise(str: string): string {
212
- return String(str || '')
213
- // Remove remaining C0/C1 control chars except newlines/tabs.
214
- // eslint-disable-next-line no-control-regex
215
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
216
- // Drop common terminal negotiation/report fragments that can remain after ANSI stripping.
217
- .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
218
- .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
219
- .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
220
- // Drop common leftover DCS/OSC payload fragments when a control string was split across PTY chunks.
221
- .replace(/(^|[\s([])(?:\d+\$r[0-9;\" ]*[A-Za-z]?)(?=$|[\s)\]])/g, '$1')
222
- .replace(/(^|[\s([])(?:>\|[A-Za-z0-9_.:-]+(?:\([^)]*\))?)(?=$|[\s)\]])/g, '$1')
223
- .replace(/(^|[\s([])(?:[A-Z]\d(?:\s+[A-Z]\d)+)(?=$|[\s)\]])/g, '$1')
224
- .replace(/(^|[\s([])(?:\d+;[^\s)\]]+)(?=$|[\s)\]])/g, '$1')
225
- .replace(/\r+/g, '\n')
226
- .replace(/[ \t]+\n/g, '\n')
227
- .replace(/\n{3,}/g, '\n\n')
228
- .replace(/ {2,}/g, ' ');
229
- }
230
-
231
- function sanitizeTerminalText(str: string): string {
232
- return stripTerminalNoise(stripAnsi(str));
233
- }
234
-
235
- function splitCliScreenLines(text: string): string[] {
236
- return String(text || '')
237
- .replace(/\u0007/g, '')
238
- .replace(/\r\n/g, '\n')
239
- .replace(/\r/g, '\n')
240
- .split('\n')
241
- .map((line) => line.replace(/\s+$/, ''));
242
- }
243
-
244
- function isPromptLikeCliLine(line: string): boolean {
245
- const trimmed = String(line || '').trim();
246
- if (!trimmed) return false;
247
- return /^[❯›>]\s*(?:$|\S.*)$/.test(trimmed);
248
- }
249
-
250
- function buildCliScreenSnapshot(text: string): CliScreenSnapshot {
251
- const normalizedText = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
252
- const rawLines = splitCliScreenLines(normalizedText);
253
- const lines = rawLines.map((line, index, arr) => {
254
- const trimmed = String(line || '').trim();
255
- return {
256
- index,
257
- fromTop: index,
258
- fromBottom: arr.length - index - 1,
259
- text: line,
260
- trimmed,
261
- isEmpty: trimmed.length === 0,
262
- };
263
- });
264
- const nonEmptyLines = lines.filter((line) => !line.isEmpty);
265
- const firstNonEmptyLine = nonEmptyLines[0] ?? null;
266
- const lastNonEmptyLine = nonEmptyLines[nonEmptyLines.length - 1] ?? null;
267
- let promptLineIndex = -1;
268
- for (let i = lines.length - 1; i >= 0; i -= 1) {
269
- if (isPromptLikeCliLine(lines[i].text)) {
270
- promptLineIndex = i;
271
- break;
272
- }
273
- }
274
- return {
275
- text: normalizedText,
276
- lineCount: lines.length,
277
- lines,
278
- nonEmptyLines,
279
- firstNonEmptyLineIndex: firstNonEmptyLine?.index ?? -1,
280
- lastNonEmptyLineIndex: lastNonEmptyLine?.index ?? -1,
281
- firstNonEmptyLine,
282
- lastNonEmptyLine,
283
- promptLineIndex,
284
- promptLine: promptLineIndex >= 0 ? lines[promptLineIndex] : null,
285
- linesAbovePrompt: promptLineIndex >= 0 ? lines.slice(0, promptLineIndex) : [...lines],
286
- linesBelowPrompt: promptLineIndex >= 0 ? lines.slice(promptLineIndex + 1) : [],
287
- };
288
- }
289
-
290
- // Re-export sanitizeSpawnEnv under the local alias for backward compat within this file
291
- const buildCliSpawnEnv = sanitizeSpawnEnv;
292
-
293
- function computeTerminalQueryTail(buffer: string): string {
294
- const prefixes = ['\x1b[6n', '\x1b[?6n'];
295
- const maxLength = prefixes.reduce((n, value) => Math.max(n, value.length), 0) - 1;
296
- const start = Math.max(0, buffer.length - maxLength);
297
- for (let i = start; i < buffer.length; i++) {
298
- const suffix = buffer.slice(i);
299
- if (prefixes.some((pattern) => suffix.length < pattern.length && pattern.startsWith(suffix))) {
300
- return suffix;
301
- }
302
- }
303
- return '';
304
- }
305
-
306
- function findBinary(name: string): string {
307
- const trimmed = String(name || '').trim();
308
- if (!trimmed) return trimmed;
309
- const expanded = trimmed.startsWith('~')
310
- ? path.join(os.homedir(), trimmed.slice(1))
311
- : trimmed;
312
- if (path.isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\')) {
313
- return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
314
- }
315
- const isWin = os.platform() === 'win32';
316
- try {
317
- const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
318
- return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
319
- } catch {
320
- return isWin ? `${trimmed}.cmd` : trimmed;
321
- }
322
- }
323
-
324
- /** True if file starts with a UTF-8 BOM then #!, or plain #!. */
325
- function isScriptBinary(binaryPath: string): boolean {
326
- if (!path.isAbsolute(binaryPath)) return false;
327
- try {
328
- const fs = require('fs');
329
- const resolved = fs.realpathSync(binaryPath);
330
- const head = Buffer.alloc(8);
331
- const fd = fs.openSync(resolved, 'r');
332
- fs.readSync(fd, head, 0, 8, 0);
333
- fs.closeSync(fd);
334
- let i = 0;
335
- if (head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) i = 3;
336
- return head[i] === 0x23 && head[i + 1] === 0x21; // '#!'
337
- } catch {
338
- return false;
339
- }
340
- }
341
-
342
- /** True only for Mach-O / ELF — npm shims and shell scripts return false. */
343
- function looksLikeMachOOrElf(filePath: string): boolean {
344
- if (!path.isAbsolute(filePath)) return false;
345
- try {
346
- const fs = require('fs');
347
- const resolved = fs.realpathSync(filePath);
348
- const buf = Buffer.alloc(8);
349
- const fd = fs.openSync(resolved, 'r');
350
- fs.readSync(fd, buf, 0, 8, 0);
351
- fs.closeSync(fd);
352
- let i = 0;
353
- if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) i = 3;
354
- const b = buf.subarray(i);
355
- if (b.length < 4) return false;
356
- // ELF
357
- if (b[0] === 0x7f && b[1] === 0x45 && b[2] === 0x4c && b[3] === 0x46) return true;
358
- const le = b.readUInt32LE(0);
359
- const be = b.readUInt32BE(0);
360
- const magics = [0xfeedface, 0xfeedfacf, 0xcafebabe, 0xbebafeca];
361
- return magics.some(m => m === le || m === be);
362
- } catch {
363
- return false;
364
- }
365
- }
366
-
367
- function shSingleQuote(arg: string): string {
368
- if (/^[a-zA-Z0-9@%_+=:,./-]+$/.test(arg)) return arg;
369
- if (os.platform() === 'win32') {
370
- return `"${arg.replace(/"/g, '""')}"`;
371
- }
372
- return `'${arg.replace(/'/g, `'\\''`)}'`;
373
- }
374
-
375
- function estimatePromptDisplayLines(text: string, cols = 80): number {
376
- const normalized = String(text || '').replace(/\r/g, '');
377
- if (!normalized) return 1;
378
- return normalized
379
- .split('\n')
380
- .reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / cols)), 0);
381
- }
382
-
383
- function extractPromptRetrySnippet(text: string): string {
384
- const lines = String(text || '')
385
- .replace(/\r/g, '')
386
- .split('\n')
387
- .map(line => line.trim())
388
- .filter(Boolean);
389
- const candidate = lines[lines.length - 1] || lines[0] || '';
390
- return candidate.slice(-120);
391
- }
392
-
393
- function normalizePromptText(text: string): string {
394
- return String(text || '').replace(/\s+/g, ' ').trim();
395
- }
396
-
397
- function compactPromptText(text: string): string {
398
- return String(text || '').replace(/\s+/g, '').trim();
399
- }
400
-
401
- function promptLikelyVisible(screenText: string, promptSnippet: string): boolean {
402
- const snippet = normalizePromptText(promptSnippet);
403
- if (!snippet) return false;
404
-
405
- const normalizedScreen = normalizePromptText(screenText);
406
- if (normalizedScreen.includes(snippet)) return true;
407
-
408
- const compactScreen = compactPromptText(screenText);
409
- const compactSnippet = compactPromptText(promptSnippet);
410
- if (compactSnippet && compactScreen.includes(compactSnippet)) return true;
411
-
412
- const tokens = snippet
413
- .split(/[^A-Za-z0-9_.:/-]+/)
414
- .map(token => token.trim())
415
- .filter(token => token.length >= 4);
416
- if (tokens.length === 0) return false;
417
-
418
- const required = Math.min(tokens.length, 3);
419
- const matched = tokens.filter(token =>
420
- normalizedScreen.includes(token) || compactScreen.includes(compactPromptText(token)),
421
- ).length;
422
- return matched >= required;
423
- }
424
-
425
- function normalizeScreenSnapshot(text: string): string {
426
- return sanitizeTerminalText(String(text || ''))
427
- .replace(/\s+/g, ' ')
428
- .trim();
429
- }
430
-
431
- function normalizeComparableMessageContent(text: string): string {
432
- return String(text || '')
433
- .replace(/\s+/g, ' ')
434
- .trim();
435
- }
436
-
437
- function trimPromptEchoPrefix(text: string, promptText?: string | null): string {
438
- const prompt = normalizeComparableMessageContent(String(promptText || ''));
439
- if (!prompt) return String(text || '');
440
-
441
- const lines = String(text || '').split(/\r\n|\n|\r/g);
442
- let dropCount = 0;
443
- for (let index = 0; index < Math.min(lines.length, 6); index += 1) {
444
- const fragment = normalizeComparableMessageContent(lines[index].replace(/^[.…]+\s*/, ''));
445
- if (!fragment) {
446
- if (dropCount === index) dropCount = index + 1;
447
- continue;
448
- }
449
- const fragmentWordCount = fragment ? fragment.split(/\s+/).filter(Boolean).length : 0;
450
- const canBePromptEcho = fragment.length >= 16 || fragmentWordCount >= 4;
451
- if (canBePromptEcho && prompt.includes(fragment)) {
452
- dropCount = index + 1;
453
- continue;
454
- }
455
- break;
456
- }
457
-
458
- return lines.slice(dropCount).join('\n').trim();
459
- }
460
-
461
- function getLastUserPromptText(messages: Array<{ role?: string; content?: string }> | null | undefined): string {
462
- const items = Array.isArray(messages) ? messages : [];
463
- for (let index = items.length - 1; index >= 0; index -= 1) {
464
- const message = items[index];
465
- if (message?.role === 'user' && typeof message.content === 'string' && message.content.trim()) {
466
- return message.content;
467
- }
468
- }
469
- return '';
470
- }
471
-
472
- function looksLikeConfirmOnlyLabel(label: string): boolean {
473
- return /^(?:continue|confirm|ok|yes|trust|proceed|enter)$/i.test(String(label || '').trim());
474
- }
475
-
476
- /**
477
- * Normalize provider.json for auto-implement approval detection.
478
- * Kept for backward compat with dev-server auto-impl pipeline only.
479
- */
480
- function parsePatternEntry(x: unknown): RegExp | null {
481
- if (x instanceof RegExp) return x;
482
- if (x && typeof x === 'object' && typeof (x as { source?: string }).source === 'string') {
483
- try {
484
- const s = x as { source: string; flags?: string };
485
- return new RegExp(s.source, s.flags || '');
486
- } catch {
487
- return null;
488
- }
489
- }
490
- return null;
491
- }
492
-
493
- function coercePatternArray(raw: unknown): RegExp[] {
494
- if (!Array.isArray(raw)) return [];
495
- return raw.map(parsePatternEntry).filter((r): r is RegExp => r != null);
496
- }
497
-
498
- /**
499
- * Normalize raw provider JSON for auto-implement approval patterns.
500
- * Used by dev-server only — ProviderCliAdapter itself uses scripts.
501
- */
502
- export function normalizeCliProviderForRuntime(raw: any): { patterns: { approval: RegExp[] } } {
503
- const patterns = raw?.patterns || {};
504
- return {
505
- patterns: {
506
- approval: coercePatternArray(patterns.approval),
507
- },
508
- };
509
- }
510
-
511
94
  // ─── Adapter ────────────────────────────────────────
512
95
 
513
96
  export class ProviderCliAdapter implements CliAdapter {
@@ -598,7 +181,7 @@ export class ProviderCliAdapter implements CliAdapter {
598
181
  private traceSeq = 0;
599
182
  private traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
600
183
  private static readonly MAX_TRACE_ENTRIES = 250;
601
- private readonly providerResolutionMeta: Record<string, any>;
184
+ private readonly providerResolutionMeta: ProviderResolutionMeta;
602
185
  private static readonly IDLE_FINISH_CONFIRM_MS = 2000;
603
186
  private static readonly STATUS_ACTIVITY_HOLD_MS = 2000;
604
187
  private static readonly FINISH_RETRY_DELAY_MS = 300;
@@ -609,117 +192,6 @@ export class ProviderCliAdapter implements CliAdapter {
609
192
  this.structuredMessages = [...this.committedMessages];
610
193
  }
611
194
 
612
- private hydrateParsedMessages(parsedMessages: any[], scope?: TurnParseScope | null): any[] {
613
- const referenceMessages = [...this.committedMessages];
614
- const usedReferenceIndexes = new Set<number>();
615
- const now = Date.now();
616
-
617
- const findReferenceTimestamp = (role: 'user' | 'assistant', content: string, parsedIndex: number): number | undefined => {
618
- const normalizedContent = normalizeComparableMessageContent(content);
619
- if (!normalizedContent) return undefined;
620
-
621
- const sameIndex = referenceMessages[parsedIndex];
622
- if (
623
- sameIndex
624
- && !usedReferenceIndexes.has(parsedIndex)
625
- && sameIndex.role === role
626
- && normalizeComparableMessageContent(sameIndex.content) === normalizedContent
627
- && typeof sameIndex.timestamp === 'number'
628
- && Number.isFinite(sameIndex.timestamp)
629
- ) {
630
- usedReferenceIndexes.add(parsedIndex);
631
- return sameIndex.timestamp;
632
- }
633
-
634
- for (let i = 0; i < referenceMessages.length; i++) {
635
- if (usedReferenceIndexes.has(i)) continue;
636
- const candidate = referenceMessages[i];
637
- if (!candidate || candidate.role !== role) continue;
638
- const candidateContent = normalizeComparableMessageContent(candidate.content);
639
- if (!candidateContent) continue;
640
- const exactMatch = candidateContent === normalizedContent;
641
- const fuzzyMatch = candidateContent.includes(normalizedContent) || normalizedContent.includes(candidateContent);
642
- if (!exactMatch && !fuzzyMatch) continue;
643
- if (typeof candidate.timestamp === 'number' && Number.isFinite(candidate.timestamp)) {
644
- usedReferenceIndexes.add(i);
645
- return candidate.timestamp;
646
- }
647
- }
648
-
649
- return undefined;
650
- };
651
-
652
- return parsedMessages
653
- .filter((message) => message && (message.role === 'user' || message.role === 'assistant'))
654
- .map((message, index) => {
655
- const role = message.role as 'user' | 'assistant';
656
- const content = typeof message.content === 'string' ? message.content : String(message.content || '');
657
- const parsedTimestamp = typeof message.timestamp === 'number' && Number.isFinite(message.timestamp)
658
- ? message.timestamp
659
- : undefined;
660
- const referenceTimestamp = parsedTimestamp ?? findReferenceTimestamp(role, content, index);
661
- const fallbackTimestamp = role === 'user'
662
- ? (scope?.startedAt || now)
663
- : (this.lastOutputAt || scope?.startedAt || now);
664
- const timestamp = referenceTimestamp ?? fallbackTimestamp;
665
- return {
666
- ...message,
667
- role,
668
- content,
669
- timestamp,
670
- receivedAt: typeof message.receivedAt === 'number' && Number.isFinite(message.receivedAt)
671
- ? message.receivedAt
672
- : timestamp,
673
- };
674
- });
675
- }
676
-
677
- private normalizeParsedMessages(parsedMessages: any[], scope?: TurnParseScope | null): CliChatMessage[] {
678
- return this.hydrateParsedMessages(parsedMessages, scope).map((message) => ({
679
- role: message.role,
680
- content: message.content,
681
- timestamp: message.timestamp,
682
- receivedAt: message.receivedAt,
683
- kind: message.kind,
684
- id: message.id,
685
- index: message.index,
686
- meta: message.meta,
687
- senderName: message.senderName,
688
- }));
689
- }
690
-
691
- private sliceFromOffset(text: string, start: number): string {
692
- if (!text) return '';
693
- if (!Number.isFinite(start) || start <= 0) return text;
694
- if (start >= text.length) return '';
695
- return text.slice(start);
696
- }
697
-
698
- private buildParseInput(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): CliScriptInput {
699
- const buffer = scope
700
- ? (this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart) || this.accumulatedBuffer)
701
- : this.accumulatedBuffer;
702
- const rawBuffer = scope
703
- ? (this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer)
704
- : this.accumulatedRawBuffer;
705
- const screenText = this.terminalScreen.getText();
706
- const recentBuffer = buffer.slice(-1000) || this.recentOutputBuffer;
707
-
708
- return {
709
- buffer,
710
- rawBuffer,
711
- recentBuffer,
712
- screenText,
713
- screen: buildCliScreenSnapshot(screenText),
714
- bufferScreen: buildCliScreenSnapshot(buffer),
715
- recentScreen: buildCliScreenSnapshot(recentBuffer),
716
- messages: [...baseMessages],
717
- partialResponse,
718
- promptText: scope?.prompt || '',
719
- settings: { ...this.runtimeSettings },
720
- };
721
- }
722
-
723
195
  private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
724
196
  const prev = this.currentStatus;
725
197
  if (prev === status) return;
@@ -754,7 +226,13 @@ export class ProviderCliAdapter implements CliAdapter {
754
226
  this.recordTrace('idle_candidate_armed', {
755
227
  confirmMs: ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS,
756
228
  candidate: this.idleFinishCandidate,
757
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
229
+ ...buildCliTraceParseSnapshot({
230
+ accumulatedBuffer: this.accumulatedBuffer,
231
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
232
+ responseBuffer: this.responseBuffer,
233
+ partialResponse: this.responseBuffer,
234
+ scope: this.currentTurnScope,
235
+ }),
758
236
  });
759
237
  if (this.settleTimer) clearTimeout(this.settleTimer);
760
238
  this.settleTimer = setTimeout(() => {
@@ -764,36 +242,6 @@ export class ProviderCliAdapter implements CliAdapter {
764
242
  }, ProviderCliAdapter.IDLE_FINISH_CONFIRM_MS);
765
243
  }
766
244
 
767
- private summarizeTraceText(text: string, max = 800): string {
768
- const value = sanitizeTerminalText(String(text || ''));
769
- if (value.length <= max) return value;
770
- return `…${value.slice(-max)}`;
771
- }
772
-
773
- private summarizeTraceMessages(messages: CliChatMessage[], limit = 3): { role: string; content: string; timestamp?: number }[] {
774
- return messages.slice(-limit).map((message) => ({
775
- role: message.role,
776
- content: this.summarizeTraceText(message.content, 240),
777
- timestamp: message.timestamp,
778
- }));
779
- }
780
-
781
- private buildTraceParseSnapshot(scope?: TurnParseScope | null, partialResponse = ''): Record<string, any> {
782
- const scopedBuffer = scope
783
- ? (this.sliceFromOffset(this.accumulatedBuffer, scope.bufferStart) || this.accumulatedBuffer)
784
- : this.accumulatedBuffer;
785
- const scopedRawBuffer = scope
786
- ? (this.sliceFromOffset(this.accumulatedRawBuffer, scope.rawBufferStart) || this.accumulatedRawBuffer)
787
- : this.accumulatedRawBuffer;
788
- return {
789
- currentTurnScope: scope || null,
790
- responseBuffer: this.summarizeTraceText(this.responseBuffer, 1200),
791
- partialResponse: this.summarizeTraceText(partialResponse || this.responseBuffer, 1200),
792
- turnBuffer: this.summarizeTraceText(scopedBuffer, 1600),
793
- turnRawPreview: this.summarizeTraceText(scopedRawBuffer, 1600),
794
- turnSanitizedRawPreview: this.summarizeTraceText(sanitizeTerminalText(scopedRawBuffer), 1600),
795
- };
796
- }
797
245
 
798
246
  private recordTrace(type: string, payload: Record<string, any> = {}): void {
799
247
  const entry: CliTraceEntry = {
@@ -847,40 +295,17 @@ export class ProviderCliAdapter implements CliAdapter {
847
295
  ? workingDir.replace(/^~/, os.homedir())
848
296
  : workingDir;
849
297
 
850
- const t = provider.timeouts || {};
851
- this.timeouts = {
852
- ptyFlush: t.ptyFlush ?? 50,
853
- dialogAccept: t.dialogAccept ?? 300,
854
- approvalCooldown: t.approvalCooldown ?? 3000,
855
- generatingIdle: t.generatingIdle ?? 6000,
856
- idleFinish: t.idleFinish ?? 5000,
857
- maxResponse: t.maxResponse ?? 300000,
858
- shutdownGrace: t.shutdownGrace ?? 1000,
859
- outputSettle: t.outputSettle ?? 300,
860
- };
861
-
862
- const rawKeys = (provider as any).approvalKeys;
863
- this.approvalKeys = (rawKeys && typeof rawKeys === 'object') ? rawKeys : {};
864
- this.sendDelayMs = typeof (provider as any).sendDelayMs === 'number' ? Math.max(0, (provider as any).sendDelayMs) : 0;
865
- this.sendKey = typeof (provider as any).sendKey === 'string' && (provider as any).sendKey.length > 0
866
- ? (provider as any).sendKey
867
- : '\r';
868
- this.submitStrategy = (provider as any).submitStrategy === 'immediate' ? 'immediate' : 'wait_for_echo';
869
- this.providerResolutionMeta = {
870
- type: provider.type,
871
- name: provider.name,
872
- resolvedVersion: (provider as any)._resolvedVersion || null,
873
- resolvedOs: (provider as any)._resolvedOs || null,
874
- providerDir: (provider as any)._resolvedProviderDir || null,
875
- scriptDir: (provider as any)._resolvedScriptDir || null,
876
- scriptsPath: (provider as any)._resolvedScriptsPath || null,
877
- scriptsSource: (provider as any)._resolvedScriptsSource || null,
878
- versionWarning: (provider as any)._versionWarning || null,
879
- };
298
+ const resolvedConfig = resolveCliAdapterConfig(provider);
299
+ this.timeouts = resolvedConfig.timeouts;
300
+ this.approvalKeys = resolvedConfig.approvalKeys;
301
+ this.sendDelayMs = resolvedConfig.sendDelayMs;
302
+ this.sendKey = resolvedConfig.sendKey;
303
+ this.submitStrategy = resolvedConfig.submitStrategy;
304
+ this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
880
305
 
881
306
  // Scripts are required — loaded by ProviderLoader via compatibility array
882
- this.cliScripts = (provider as any).scripts || {};
883
- const scriptNames = Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function');
307
+ this.cliScripts = provider.scripts || {};
308
+ const scriptNames = listCliScriptNames(this.cliScripts);
884
309
  if (scriptNames.length > 0) {
885
310
  LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
886
311
  LOG.info(
@@ -895,7 +320,7 @@ export class ProviderCliAdapter implements CliAdapter {
895
320
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
896
321
  setCliScripts(scripts: CliScripts): void {
897
322
  this.cliScripts = scripts;
898
- const scriptNames = Object.keys(scripts).filter(k => typeof (scripts as any)[k] === 'function');
323
+ const scriptNames = listCliScriptNames(scripts);
899
324
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
900
325
  }
901
326
 
@@ -935,99 +360,44 @@ export class ProviderCliAdapter implements CliAdapter {
935
360
  async spawn(): Promise<void> {
936
361
  if (this.ptyProcess) return;
937
362
 
938
- const { spawn: spawnConfig } = this.provider;
939
- const configuredCommand = typeof this.runtimeSettings.executablePath === 'string' && this.runtimeSettings.executablePath.trim()
940
- ? this.runtimeSettings.executablePath.trim()
941
- : spawnConfig.command;
942
- const binaryPath = findBinary(configuredCommand);
943
- const isWin = os.platform() === 'win32';
944
- const allArgs = [...spawnConfig.args, ...this.extraArgs];
363
+ const spawnPlan = resolveCliSpawnPlan({
364
+ provider: this.provider,
365
+ runtimeSettings: this.runtimeSettings,
366
+ workingDir: this.workingDir,
367
+ extraArgs: this.extraArgs,
368
+ });
945
369
 
946
370
  LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
947
371
  this.resetTraceSession();
948
-
949
- let shellCmd: string;
950
- let shellArgs: string[];
951
- const useShellUnix = !isWin && (
952
- !!spawnConfig.shell
953
- || !path.isAbsolute(binaryPath)
954
- || isScriptBinary(binaryPath)
955
- || !looksLikeMachOOrElf(binaryPath)
956
- );
957
- // On Windows, .cmd/.bat shims cannot be spawned directly — must go through cmd.exe
958
- const isCmdShim = isWin && /\.(cmd|bat)$/i.test(binaryPath);
959
- const useShellWin = !!spawnConfig.shell
960
- || isCmdShim
961
- || !path.isAbsolute(binaryPath)
962
- || isScriptBinary(binaryPath);
963
- const useShell = isWin ? useShellWin : useShellUnix;
964
-
965
- if (useShell) {
966
- if (!spawnConfig.shell && !isWin) {
967
- LOG.info('CLI', `[${this.cliType}] Using login shell (script shim or non-native binary)`);
968
- }
969
- if (isCmdShim) {
970
- LOG.info('CLI', `[${this.cliType}] Using cmd.exe shell for .cmd/.bat shim: ${binaryPath}`);
971
- } else if (isWin) {
972
- LOG.info('CLI', `[${this.cliType}] Using cmd.exe shell on Windows: ${binaryPath}`);
973
- }
974
- shellCmd = isWin ? 'cmd.exe' : (process.env.SHELL || '/bin/zsh');
975
- if (isWin) {
976
- // On Windows, pass binaryPath and args as separate items so node-pty's
977
- // argvToCommandLine quotes each one individually. Joining them into a
978
- // single pre-quoted string causes cmd.exe to receive \"path\" (backslash-
979
- // escaped quotes) which it does not recognise as a valid executable name.
980
- shellArgs = ['/c', binaryPath, ...allArgs];
981
- } else {
982
- const fullCmd = [binaryPath, ...allArgs].map(shSingleQuote).join(' ');
983
- shellArgs = ['-l', '-c', fullCmd];
984
- }
985
- } else {
986
- if (isWin && spawnConfig.shell) {
987
- LOG.info('CLI', `[${this.cliType}] Spawning Windows binary directly without cmd.exe: ${binaryPath}`);
988
- }
989
- shellCmd = binaryPath;
990
- shellArgs = allArgs;
991
- }
992
-
993
- const ptyOpts = {
994
- cols: 80,
995
- rows: 24,
996
- cwd: this.workingDir,
997
- env: buildCliSpawnEnv(process.env, spawnConfig.env),
998
- };
999
372
  this.recordTrace('spawn', {
1000
- shellCommand: shellCmd,
1001
- shellArgs,
1002
- cwd: ptyOpts.cwd,
1003
- cols: ptyOpts.cols,
1004
- rows: ptyOpts.rows,
373
+ shellCommand: spawnPlan.shellCmd,
374
+ shellArgs: spawnPlan.shellArgs,
375
+ cwd: spawnPlan.ptyOptions.cwd,
376
+ cols: spawnPlan.ptyOptions.cols,
377
+ rows: spawnPlan.ptyOptions.rows,
1005
378
  providerResolution: this.providerResolutionMeta,
1006
379
  });
1007
380
 
1008
381
  try {
1009
- this.ptyProcess = this.transportFactory.spawn(shellCmd, shellArgs, ptyOpts);
382
+ this.ptyProcess = this.transportFactory.spawn(
383
+ spawnPlan.shellCmd,
384
+ spawnPlan.shellArgs,
385
+ spawnPlan.ptyOptions,
386
+ );
1010
387
  } catch (err: any) {
1011
388
  const msg = err?.message || String(err);
1012
- if (!isWin && !useShell && /posix_spawn|spawn/i.test(msg)) {
389
+ if (!spawnPlan.isWin && !spawnPlan.useShell && /posix_spawn|spawn/i.test(msg)) {
1013
390
  LOG.warn('CLI', `[${this.cliType}] Direct spawn failed (${msg}), retrying via login shell`);
1014
- shellCmd = process.env.SHELL || '/bin/zsh';
1015
- const fullCmd = [binaryPath, ...allArgs].map(shSingleQuote).join(' ');
1016
- shellArgs = ['-l', '-c', fullCmd];
1017
- this.ptyProcess = this.transportFactory.spawn(shellCmd, shellArgs, ptyOpts);
391
+ const retryPlan = buildCliLoginShellRetry(spawnPlan);
392
+ this.ptyProcess = this.transportFactory.spawn(
393
+ retryPlan.shellCmd,
394
+ retryPlan.shellArgs,
395
+ spawnPlan.ptyOptions,
396
+ );
1018
397
  } else {
1019
- // Windows native error codes are often cryptic — provide helpful hints
1020
- if (isWin) {
1021
- const hint = /error code 267|ERROR_DIRECTORY/i.test(msg)
1022
- ? ' (working directory does not exist or is not a directory)'
1023
- : /error code 740|elevation/i.test(msg)
1024
- ? ' (requires administrator privileges)'
1025
- : /error code 2|ENOENT|not found/i.test(msg)
1026
- ? ` (executable not found: ${shellCmd})`
1027
- : '';
1028
- if (hint) {
1029
- throw new Error(`Failed to spawn CLI${hint}: ${msg}`);
1030
- }
398
+ const hint = getCliSpawnErrorHint(msg, spawnPlan.shellCmd, spawnPlan.isWin);
399
+ if (hint) {
400
+ throw new Error(`Failed to spawn CLI${hint}: ${msg}`);
1031
401
  }
1032
402
  throw err;
1033
403
  }
@@ -1037,7 +407,12 @@ export class ProviderCliAdapter implements CliAdapter {
1037
407
  if (Date.now() < this.resizeSuppressUntil) return;
1038
408
 
1039
409
  if (!this.ptyProcess?.terminalQueriesHandled) {
1040
- this.respondToTerminalQueries(data);
410
+ this.pendingTerminalQueryTail = respondToCliTerminalQueries({
411
+ ptyProcess: this.ptyProcess,
412
+ pendingTail: this.pendingTerminalQueryTail,
413
+ data,
414
+ terminalScreen: this.terminalScreen,
415
+ });
1041
416
  }
1042
417
 
1043
418
  this.pendingOutputParseBuffer += data;
@@ -1116,9 +491,9 @@ export class ProviderCliAdapter implements CliAdapter {
1116
491
  this.recordTrace('output', {
1117
492
  rawLength: rawData.length,
1118
493
  cleanLength: cleanData.length,
1119
- rawPreview: this.summarizeTraceText(rawData, 300),
1120
- cleanPreview: this.summarizeTraceText(cleanData, 300),
1121
- screenText: this.summarizeTraceText(this.terminalScreen.getText(), 1200),
494
+ rawPreview: summarizeCliTraceText(rawData, 300),
495
+ cleanPreview: summarizeCliTraceText(cleanData, 300),
496
+ screenText: summarizeCliTraceText(this.terminalScreen.getText(), 1200),
1122
497
  });
1123
498
 
1124
499
  if (this.startupParseGate) {
@@ -1411,7 +786,7 @@ export class ProviderCliAdapter implements CliAdapter {
1411
786
  loggedWait = true;
1412
787
  LOG.info(
1413
788
  'CLI',
1414
- `[${this.cliType}] Waiting for interactive prompt: hasPrompt=${hasPrompt} stableMs=${stableMs} recentOutputMs=${recentlyOutput} status=${status} startup=${startupLikelyActive} screen=${JSON.stringify(this.summarizeTraceText(screenText, 220)).slice(0, 260)}`
789
+ `[${this.cliType}] Waiting for interactive prompt: hasPrompt=${hasPrompt} stableMs=${stableMs} recentOutputMs=${recentlyOutput} status=${status} startup=${startupLikelyActive} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)}`
1415
790
  );
1416
791
  }
1417
792
  await new Promise(resolve => setTimeout(resolve, 50));
@@ -1420,7 +795,7 @@ export class ProviderCliAdapter implements CliAdapter {
1420
795
  const finalScreenText = this.terminalScreen.getText() || '';
1421
796
  LOG.warn(
1422
797
  'CLI',
1423
- `[${this.cliType}] Interactive prompt wait timed out after ${maxWaitMs}ms; proceeding with screen=${JSON.stringify(this.summarizeTraceText(finalScreenText, 240)).slice(0, 280)}`
798
+ `[${this.cliType}] Interactive prompt wait timed out after ${maxWaitMs}ms; proceeding with screen=${JSON.stringify(summarizeCliTraceText(finalScreenText, 240)).slice(0, 280)}`
1424
799
  );
1425
800
  }
1426
801
 
@@ -1453,24 +828,34 @@ export class ProviderCliAdapter implements CliAdapter {
1453
828
  this.currentTurnScope,
1454
829
  );
1455
830
  const parsedMessages = Array.isArray(parsedTranscript?.messages)
1456
- ? this.normalizeParsedMessages(parsedTranscript.messages)
831
+ ? normalizeCliParsedMessages(parsedTranscript.messages, {
832
+ committedMessages: this.committedMessages,
833
+ scope: this.currentTurnScope,
834
+ lastOutputAt: this.lastOutputAt,
835
+ })
1457
836
  : [];
1458
837
  const lastParsedAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
1459
838
  this.recordTrace('settled', {
1460
- tail: this.summarizeTraceText(tail, 500),
1461
- screenText: this.summarizeTraceText(screenText, 1200),
839
+ tail: summarizeCliTraceText(tail, 500),
840
+ screenText: summarizeCliTraceText(screenText, 1200),
1462
841
  detectStatus: scriptStatus,
1463
842
  parsedStatus: parsedTranscript?.status || null,
1464
843
  parsedMessageCount: parsedMessages.length,
1465
- parsedLastAssistant: lastParsedAssistant ? this.summarizeTraceText(lastParsedAssistant.content, 280) : '',
844
+ parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
1466
845
  parsedActiveModal: parsedTranscript?.activeModal ?? null,
1467
846
  approval: modal,
1468
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
847
+ ...buildCliTraceParseSnapshot({
848
+ accumulatedBuffer: this.accumulatedBuffer,
849
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
850
+ responseBuffer: this.responseBuffer,
851
+ partialResponse: this.responseBuffer,
852
+ scope: this.currentTurnScope,
853
+ }),
1469
854
  });
1470
855
  if (this.currentTurnScope && !lastParsedAssistant) {
1471
856
  LOG.info(
1472
857
  'CLI',
1473
- `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(this.summarizeTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(this.summarizeTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
858
+ `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1474
859
  );
1475
860
  }
1476
861
  if (!scriptStatus) return;
@@ -1537,7 +922,13 @@ export class ProviderCliAdapter implements CliAdapter {
1537
922
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1538
923
  lastScreenChangeAt: this.lastScreenChangeAt,
1539
924
  holdMs: ProviderCliAdapter.STATUS_ACTIVITY_HOLD_MS,
1540
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
925
+ ...buildCliTraceParseSnapshot({
926
+ accumulatedBuffer: this.accumulatedBuffer,
927
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
928
+ responseBuffer: this.responseBuffer,
929
+ partialResponse: this.responseBuffer,
930
+ scope: this.currentTurnScope,
931
+ }),
1541
932
  });
1542
933
  this.onStatusChange?.();
1543
934
  return;
@@ -1654,7 +1045,13 @@ export class ProviderCliAdapter implements CliAdapter {
1654
1045
  canFinishImmediately,
1655
1046
  submitPendingUntil: this.submitPendingUntil,
1656
1047
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1657
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1048
+ ...buildCliTraceParseSnapshot({
1049
+ accumulatedBuffer: this.accumulatedBuffer,
1050
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1051
+ responseBuffer: this.responseBuffer,
1052
+ partialResponse: this.responseBuffer,
1053
+ scope: this.currentTurnScope,
1054
+ }),
1658
1055
  });
1659
1056
 
1660
1057
  if (canFinishImmediately) {
@@ -1693,7 +1090,13 @@ export class ProviderCliAdapter implements CliAdapter {
1693
1090
  if (this.responseSettleIgnoreUntil > Date.now()) return;
1694
1091
  this.clearIdleFinishCandidate('finish_response_enter');
1695
1092
  this.recordTrace('finish_response', {
1696
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1093
+ ...buildCliTraceParseSnapshot({
1094
+ accumulatedBuffer: this.accumulatedBuffer,
1095
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1096
+ responseBuffer: this.responseBuffer,
1097
+ partialResponse: this.responseBuffer,
1098
+ scope: this.currentTurnScope,
1099
+ }),
1697
1100
  });
1698
1101
  const commitResult = this.commitCurrentTranscript();
1699
1102
  if (this.shouldRetryFinishResponse(commitResult)) {
@@ -1701,8 +1104,14 @@ export class ProviderCliAdapter implements CliAdapter {
1701
1104
  this.recordTrace('finish_response_retry', {
1702
1105
  retryCount: this.finishRetryCount,
1703
1106
  retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
1704
- assistantContent: this.summarizeTraceText(commitResult.assistantContent, 220),
1705
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1107
+ assistantContent: summarizeCliTraceText(commitResult.assistantContent, 220),
1108
+ ...buildCliTraceParseSnapshot({
1109
+ accumulatedBuffer: this.accumulatedBuffer,
1110
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1111
+ responseBuffer: this.responseBuffer,
1112
+ partialResponse: this.responseBuffer,
1113
+ scope: this.currentTurnScope,
1114
+ }),
1706
1115
  });
1707
1116
  if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
1708
1117
  this.finishRetryTimer = setTimeout(() => {
@@ -1738,7 +1147,11 @@ export class ProviderCliAdapter implements CliAdapter {
1738
1147
  this.currentTurnScope,
1739
1148
  );
1740
1149
  if (parsed && Array.isArray(parsed.messages)) {
1741
- this.committedMessages = this.normalizeParsedMessages(parsed.messages, this.currentTurnScope);
1150
+ this.committedMessages = normalizeCliParsedMessages(parsed.messages, {
1151
+ committedMessages: this.committedMessages,
1152
+ scope: this.currentTurnScope,
1153
+ lastOutputAt: this.lastOutputAt,
1154
+ });
1742
1155
  const promptForTrim = this.currentTurnScope?.prompt || getLastUserPromptText(this.committedMessages);
1743
1156
  if (promptForTrim) {
1744
1157
  const lastAssistantForTrim = [...this.committedMessages].reverse().find((message) => message.role === 'assistant');
@@ -1751,14 +1164,20 @@ export class ProviderCliAdapter implements CliAdapter {
1751
1164
  this.recordTrace('commit_transcript', {
1752
1165
  parsedStatus: parsed.status || null,
1753
1166
  messageCount: this.committedMessages.length,
1754
- lastAssistant: lastAssistant ? this.summarizeTraceText(lastAssistant.content, 320) : '',
1755
- messages: this.summarizeTraceMessages(this.committedMessages),
1756
- ...this.buildTraceParseSnapshot(this.currentTurnScope, this.responseBuffer),
1167
+ lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
1168
+ messages: summarizeCliTraceMessages(this.committedMessages),
1169
+ ...buildCliTraceParseSnapshot({
1170
+ accumulatedBuffer: this.accumulatedBuffer,
1171
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1172
+ responseBuffer: this.responseBuffer,
1173
+ partialResponse: this.responseBuffer,
1174
+ scope: this.currentTurnScope,
1175
+ }),
1757
1176
  });
1758
1177
  if (!lastAssistant && this.currentTurnScope) {
1759
1178
  LOG.warn(
1760
1179
  'CLI',
1761
- `[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(this.summarizeTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1180
+ `[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1762
1181
  );
1763
1182
  }
1764
1183
  return {
@@ -1863,14 +1282,18 @@ export class ProviderCliAdapter implements CliAdapter {
1863
1282
  const hydratedMessages = shouldPreferCommittedMessages
1864
1283
  ? this.committedMessages.map((message, index) => ({
1865
1284
  ...message,
1866
- id: (message as any).id || `msg_${index}`,
1867
- index: typeof (message as any).index === 'number' ? (message as any).index : index,
1868
- kind: (message as any).kind || 'standard',
1869
- receivedAt: typeof (message as any).receivedAt === 'number'
1870
- ? (message as any).receivedAt
1285
+ id: message.id || `msg_${index}`,
1286
+ index: typeof message.index === 'number' ? message.index : index,
1287
+ kind: message.kind || 'standard',
1288
+ receivedAt: typeof message.receivedAt === 'number'
1289
+ ? message.receivedAt
1871
1290
  : message.timestamp,
1872
1291
  }))
1873
- : this.hydrateParsedMessages(parsed.messages, this.currentTurnScope);
1292
+ : hydrateCliParsedMessages(parsed.messages, {
1293
+ committedMessages: this.committedMessages,
1294
+ scope: this.currentTurnScope,
1295
+ lastOutputAt: this.lastOutputAt,
1296
+ });
1874
1297
  return {
1875
1298
  id: parsed.id || 'cli_session',
1876
1299
  status: parsed.status || this.currentStatus,
@@ -1903,11 +1326,16 @@ export class ProviderCliAdapter implements CliAdapter {
1903
1326
  if (typeof fn !== 'function') {
1904
1327
  throw new Error(`CLI script '${scriptName}' not available`);
1905
1328
  }
1906
- const input = this.buildParseInput(
1907
- this.committedMessages,
1908
- this.responseBuffer,
1909
- this.currentTurnScope,
1910
- );
1329
+ const input = buildCliParseInput({
1330
+ accumulatedBuffer: this.accumulatedBuffer,
1331
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1332
+ recentOutputBuffer: this.recentOutputBuffer,
1333
+ terminalScreenText: this.terminalScreen.getText(),
1334
+ baseMessages: this.committedMessages,
1335
+ partialResponse: this.responseBuffer,
1336
+ scope: this.currentTurnScope,
1337
+ runtimeSettings: this.runtimeSettings,
1338
+ });
1911
1339
  return await Promise.resolve(fn({
1912
1340
  ...input,
1913
1341
  args: args && typeof args === 'object' ? { ...args } : {},
@@ -1917,7 +1345,16 @@ export class ProviderCliAdapter implements CliAdapter {
1917
1345
  private parseCurrentTranscript(baseMessages: CliChatMessage[], partialResponse: string, scope?: TurnParseScope | null): any {
1918
1346
  if (!this.cliScripts?.parseOutput) return null;
1919
1347
  try {
1920
- const input = this.buildParseInput(baseMessages, partialResponse, scope);
1348
+ const input = buildCliParseInput({
1349
+ accumulatedBuffer: this.accumulatedBuffer,
1350
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1351
+ recentOutputBuffer: this.recentOutputBuffer,
1352
+ terminalScreenText: this.terminalScreen.getText(),
1353
+ baseMessages,
1354
+ partialResponse,
1355
+ scope,
1356
+ runtimeSettings: this.runtimeSettings,
1357
+ });
1921
1358
  const parsed = this.cliScripts.parseOutput(input);
1922
1359
  const refinedStatus = this.refineDetectedStatus(typeof parsed?.status === 'string' ? parsed.status : null, input.recentBuffer, input.screenText);
1923
1360
  if (parsed && refinedStatus && parsed.status !== refinedStatus) {
@@ -2005,7 +1442,7 @@ export class ProviderCliAdapter implements CliAdapter {
2005
1442
  rawBufferStart: this.accumulatedRawBuffer.length,
2006
1443
  };
2007
1444
  this.recordTrace('send_message', {
2008
- text: this.summarizeTraceText(text, 500),
1445
+ text: summarizeCliTraceText(text, 500),
2009
1446
  estimatedLines: estimatePromptDisplayLines(text),
2010
1447
  turnScope: this.currentTurnScope,
2011
1448
  });
@@ -2040,7 +1477,7 @@ export class ProviderCliAdapter implements CliAdapter {
2040
1477
  this.recordTrace('submit_write', {
2041
1478
  mode: 'submit_key',
2042
1479
  sendKey: this.sendKey,
2043
- screenText: this.summarizeTraceText(this.terminalScreen.getText(), 500),
1480
+ screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2044
1481
  });
2045
1482
  this.ptyProcess.write(this.sendKey);
2046
1483
  const retrySubmitIfStuck = (attempt: number) => {
@@ -2057,7 +1494,7 @@ export class ProviderCliAdapter implements CliAdapter {
2057
1494
  mode: 'submit_retry',
2058
1495
  attempt,
2059
1496
  sendKey: this.sendKey,
2060
- screenText: this.summarizeTraceText(screenText, 500),
1497
+ screenText: summarizeCliTraceText(screenText, 500),
2061
1498
  });
2062
1499
  this.ptyProcess.write(this.sendKey);
2063
1500
  if (attempt >= 3) {
@@ -2074,9 +1511,9 @@ export class ProviderCliAdapter implements CliAdapter {
2074
1511
  this.submitPendingUntil = 0;
2075
1512
  this.recordTrace('submit_write', {
2076
1513
  mode: 'immediate',
2077
- text: this.summarizeTraceText(text, 500),
1514
+ text: summarizeCliTraceText(text, 500),
2078
1515
  sendKey: this.sendKey,
2079
- screenText: this.summarizeTraceText(this.terminalScreen.getText(), 500),
1516
+ screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2080
1517
  });
2081
1518
  this.ptyProcess.write(text + this.sendKey);
2082
1519
  this.submitRetryTimer = setTimeout(() => {
@@ -2092,7 +1529,7 @@ export class ProviderCliAdapter implements CliAdapter {
2092
1529
  mode: 'immediate_retry',
2093
1530
  attempt: 1,
2094
1531
  sendKey: this.sendKey,
2095
- screenText: this.summarizeTraceText(screenText, 500),
1532
+ screenText: summarizeCliTraceText(screenText, 500),
2096
1533
  });
2097
1534
  this.ptyProcess.write(this.sendKey);
2098
1535
  this.submitRetryUsed = true;
@@ -2107,9 +1544,9 @@ export class ProviderCliAdapter implements CliAdapter {
2107
1544
  this.ptyProcess.write(text);
2108
1545
  this.recordTrace('submit_write', {
2109
1546
  mode: 'type_then_submit',
2110
- text: this.summarizeTraceText(text, 500),
1547
+ text: summarizeCliTraceText(text, 500),
2111
1548
  sendKey: this.sendKey,
2112
- screenText: this.summarizeTraceText(this.terminalScreen.getText(), 500),
1549
+ screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2113
1550
  });
2114
1551
  const submitStartedAt = Date.now();
2115
1552
  let lastNormalizedScreen = '';
@@ -2394,7 +1831,7 @@ export class ProviderCliAdapter implements CliAdapter {
2394
1831
  responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2395
1832
  resizeSuppressUntil: this.resizeSuppressUntil,
2396
1833
  hasCliScripts: this.hasCliScripts(),
2397
- scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),
1834
+ scriptNames: listCliScriptNames(this.cliScripts),
2398
1835
  traceSessionId: this.traceSessionId,
2399
1836
  traceEntryCount: this.traceEntries.length,
2400
1837
  statusHistory: this.statusHistory.slice(-30),
@@ -2412,37 +1849,17 @@ export class ProviderCliAdapter implements CliAdapter {
2412
1849
  providerResolution: this.providerResolutionMeta,
2413
1850
  entryCount: this.traceEntries.length,
2414
1851
  entries: this.traceEntries.slice(-cappedLimit),
2415
- screenText: this.summarizeTraceText(this.terminalScreen.getText(), 4000),
2416
- recentOutputBuffer: this.summarizeTraceText(this.recentOutputBuffer, 1000),
2417
- responseBuffer: this.summarizeTraceText(this.responseBuffer, 1200),
1852
+ screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
1853
+ recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
1854
+ responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
2418
1855
  status: this.currentStatus,
2419
1856
  activeModal: this.activeModal,
2420
1857
  currentTurnScope: this.currentTurnScope,
2421
- messages: this.summarizeTraceMessages(this.committedMessages, 5),
1858
+ messages: summarizeCliTraceMessages(this.committedMessages, 5),
2422
1859
  };
2423
1860
  }
2424
1861
 
2425
- getProviderResolutionMeta(): Record<string, any> {
1862
+ getProviderResolutionMeta(): ProviderResolutionMeta {
2426
1863
  return { ...this.providerResolutionMeta };
2427
1864
  }
2428
-
2429
- private respondToTerminalQueries(data: string): void {
2430
- if (!this.ptyProcess || !data) return;
2431
-
2432
- const combined = this.pendingTerminalQueryTail + data;
2433
- const regex = /\x1b\[(\?)?6n/g;
2434
- let match: RegExpExecArray | null;
2435
-
2436
- while ((match = regex.exec(combined)) !== null) {
2437
- const cursor = this.terminalScreen.getCursorPosition();
2438
- const row = Math.max(1, (cursor.row | 0) + 1);
2439
- const col = Math.max(1, (cursor.col | 0) + 1);
2440
- const response = match[1]
2441
- ? `\x1b[?${row};${col}R`
2442
- : `\x1b[${row};${col}R`;
2443
- this.ptyProcess.write(response);
2444
- }
2445
-
2446
- this.pendingTerminalQueryTail = computeTerminalQueryTail(combined);
2447
- }
2448
1865
  }