@adhdev/daemon-core 0.8.30 → 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 (57) 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 +866 -592
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +868 -595
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/providers/contracts.d.ts +10 -1
  19. package/dist/providers/provider-loader.d.ts +3 -0
  20. package/dist/status/reporter.d.ts +2 -3
  21. package/dist/status/snapshot.d.ts +2 -1
  22. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  23. package/package.json +3 -1
  24. package/src/agent-stream/manager.ts +8 -2
  25. package/src/agent-stream/poller.ts +19 -5
  26. package/src/agent-stream/provider-adapter.ts +11 -7
  27. package/src/agent-stream/types.ts +3 -0
  28. package/src/boot/daemon-lifecycle.ts +7 -6
  29. package/src/cdp/initializer.ts +2 -2
  30. package/src/cdp/manager.ts +5 -0
  31. package/src/cdp/setup.ts +1 -1
  32. package/src/cli-adapter-types.ts +37 -5
  33. package/src/cli-adapters/provider-cli-adapter.ts +212 -795
  34. package/src/cli-adapters/provider-cli-config.ts +66 -0
  35. package/src/cli-adapters/provider-cli-parse.ts +202 -0
  36. package/src/cli-adapters/provider-cli-runtime.ts +142 -0
  37. package/src/cli-adapters/provider-cli-shared.ts +439 -0
  38. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +1 -1
  39. package/src/commands/cdp-commands.ts +6 -1
  40. package/src/commands/chat-commands.ts +45 -29
  41. package/src/commands/cli-manager.ts +28 -9
  42. package/src/commands/handler.ts +14 -10
  43. package/src/commands/router.ts +23 -10
  44. package/src/commands/stream-commands.ts +11 -5
  45. package/src/config/config.ts +4 -10
  46. package/src/daemon/dev-auto-implement.ts +22 -18
  47. package/src/daemon/dev-cli-debug.ts +59 -16
  48. package/src/daemon/dev-server.ts +67 -43
  49. package/src/providers/acp-provider-instance.ts +1 -1
  50. package/src/providers/cli-provider-instance.ts +2 -2
  51. package/src/providers/contracts.ts +12 -1
  52. package/src/providers/extension-provider-instance.ts +1 -1
  53. package/src/providers/ide-provider-instance.ts +39 -18
  54. package/src/providers/provider-loader.ts +85 -54
  55. package/src/providers/version-archive.ts +23 -5
  56. package/src/status/reporter.ts +18 -14
  57. package/src/status/snapshot.ts +5 -4
@@ -0,0 +1,439 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import type { ProviderResumeCapability } from '../providers/contracts.js';
5
+ import { sanitizeSpawnEnv } from './spawn-env.js';
6
+
7
+ export interface CliChatMessage {
8
+ role: 'user' | 'assistant';
9
+ content: string;
10
+ timestamp?: number;
11
+ receivedAt?: number;
12
+ kind?: string;
13
+ id?: string;
14
+ index?: number;
15
+ meta?: Record<string, any>;
16
+ senderName?: string;
17
+ }
18
+
19
+ export interface CliSessionStatus {
20
+ status: 'idle' | 'generating' | 'waiting_approval' | 'error' | 'stopped' | 'starting';
21
+ messages: CliChatMessage[];
22
+ workingDir: string;
23
+ activeModal: { message: string; buttons: string[] } | null;
24
+ }
25
+
26
+ export interface CliScripts {
27
+ parseOutput?: (input: CliScriptInput) => any;
28
+ detectStatus?: (input: CliStatusInput) => string | null;
29
+ parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
30
+ resolveAction?: (data: any) => string;
31
+ [name: string]: ((input: any) => any) | undefined;
32
+ }
33
+
34
+ export interface CliScreenLine {
35
+ index: number;
36
+ fromTop: number;
37
+ fromBottom: number;
38
+ text: string;
39
+ trimmed: string;
40
+ isEmpty: boolean;
41
+ }
42
+
43
+ export interface CliScreenSnapshot {
44
+ text: string;
45
+ lineCount: number;
46
+ lines: CliScreenLine[];
47
+ nonEmptyLines: CliScreenLine[];
48
+ firstNonEmptyLineIndex: number;
49
+ lastNonEmptyLineIndex: number;
50
+ firstNonEmptyLine: CliScreenLine | null;
51
+ lastNonEmptyLine: CliScreenLine | null;
52
+ promptLineIndex: number;
53
+ promptLine: CliScreenLine | null;
54
+ linesAbovePrompt: CliScreenLine[];
55
+ linesBelowPrompt: CliScreenLine[];
56
+ }
57
+
58
+ export interface CliScriptInput {
59
+ buffer: string;
60
+ rawBuffer: string;
61
+ recentBuffer: string;
62
+ screenText: string;
63
+ screen: CliScreenSnapshot;
64
+ bufferScreen: CliScreenSnapshot;
65
+ recentScreen: CliScreenSnapshot;
66
+ messages: CliChatMessage[];
67
+ partialResponse: string;
68
+ promptText?: string;
69
+ settings?: Record<string, any>;
70
+ args?: Record<string, any>;
71
+ }
72
+
73
+ export interface CliStatusInput {
74
+ tail: string;
75
+ screenText?: string;
76
+ rawBuffer?: string;
77
+ screen: CliScreenSnapshot;
78
+ tailScreen: CliScreenSnapshot;
79
+ }
80
+
81
+ export interface CliApprovalInput {
82
+ buffer: string;
83
+ screenText?: string;
84
+ rawBuffer?: string;
85
+ tail: string;
86
+ screen: CliScreenSnapshot;
87
+ bufferScreen: CliScreenSnapshot;
88
+ tailScreen: CliScreenSnapshot;
89
+ }
90
+
91
+ export interface CliTraceEntry {
92
+ id: number;
93
+ at: number;
94
+ type: string;
95
+ status: CliSessionStatus['status'];
96
+ isWaitingForResponse: boolean;
97
+ activeModal: { message: string; buttons: string[] } | null;
98
+ payload: Record<string, any>;
99
+ }
100
+
101
+ export interface CliProviderModule {
102
+ type: string;
103
+ name: string;
104
+ category: 'cli';
105
+ binary: string;
106
+ approvalKeys?: Record<number, string>;
107
+ sendDelayMs?: number;
108
+ sendKey?: string;
109
+ submitStrategy?: 'wait_for_echo' | 'immediate';
110
+ scripts?: CliScripts;
111
+ spawn: {
112
+ command: string;
113
+ args: string[];
114
+ shell: boolean;
115
+ env: Record<string, string>;
116
+ };
117
+ timeouts?: {
118
+ ptyFlush?: number;
119
+ dialogAccept?: number;
120
+ approvalCooldown?: number;
121
+ generatingIdle?: number;
122
+ idleFinish?: number;
123
+ maxResponse?: number;
124
+ shutdownGrace?: number;
125
+ outputSettle?: number;
126
+ };
127
+ resume?: ProviderResumeCapability;
128
+ _resolvedVersion?: string | null;
129
+ _resolvedOs?: string | null;
130
+ _resolvedProviderDir?: string | null;
131
+ _resolvedScriptDir?: string | null;
132
+ _resolvedScriptsPath?: string | null;
133
+ _resolvedScriptsSource?: string | null;
134
+ _versionWarning?: string | null;
135
+ }
136
+
137
+ function stripAnsi(str: string): string {
138
+ // eslint-disable-next-line no-control-regex
139
+ return str
140
+ .replace(/\x1B\][^\x07]*\x07/g, '')
141
+ .replace(/\x1B\][\s\S]*?\x1B\\/g, '')
142
+ .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
143
+ .replace(/\x1B\[\d*[A-HJKSTfG]/g, ' ')
144
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
145
+ .replace(/ +/g, ' ');
146
+ }
147
+
148
+ function stripTerminalNoise(str: string): string {
149
+ return String(str || '')
150
+ .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
151
+ .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
152
+ .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
153
+ .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
154
+ .replace(/(^|[\s([])(?:\d+\$r[0-9;\" ]*[A-Za-z]?)(?=$|[\s)\]])/g, '$1')
155
+ .replace(/(^|[\s([])(?:>\|[A-Za-z0-9_.:-]+(?:\([^)]*\))?)(?=$|[\s)\]])/g, '$1')
156
+ .replace(/(^|[\s([])(?:[A-Z]\d(?:\s+[A-Z]\d)+)(?=$|[\s)\]])/g, '$1')
157
+ .replace(/(^|[\s([])(?:\d+;[^\s)\]]+)(?=$|[\s)\]])/g, '$1')
158
+ .replace(/\r+/g, '\n')
159
+ .replace(/[ \t]+\n/g, '\n')
160
+ .replace(/\n{3,}/g, '\n\n')
161
+ .replace(/ {2,}/g, ' ');
162
+ }
163
+
164
+ export function sanitizeTerminalText(str: string): string {
165
+ return stripTerminalNoise(stripAnsi(str));
166
+ }
167
+
168
+ export function listCliScriptNames(scripts: CliScripts | undefined): string[] {
169
+ if (!scripts) return [];
170
+ return Object.entries(scripts)
171
+ .filter(([, fn]) => typeof fn === 'function')
172
+ .map(([name]) => name);
173
+ }
174
+
175
+ function splitCliScreenLines(text: string): string[] {
176
+ return String(text || '')
177
+ .replace(/\u0007/g, '')
178
+ .replace(/\r\n/g, '\n')
179
+ .replace(/\r/g, '\n')
180
+ .split('\n')
181
+ .map((line) => line.replace(/\s+$/, ''));
182
+ }
183
+
184
+ function isPromptLikeCliLine(line: string): boolean {
185
+ const trimmed = String(line || '').trim();
186
+ if (!trimmed) return false;
187
+ return /^[❯›>]\s*(?:$|\S.*)$/.test(trimmed);
188
+ }
189
+
190
+ export function buildCliScreenSnapshot(text: string): CliScreenSnapshot {
191
+ const normalizedText = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
192
+ const rawLines = splitCliScreenLines(normalizedText);
193
+ const lines = rawLines.map((line, index, arr) => {
194
+ const trimmed = String(line || '').trim();
195
+ return {
196
+ index,
197
+ fromTop: index,
198
+ fromBottom: arr.length - index - 1,
199
+ text: line,
200
+ trimmed,
201
+ isEmpty: trimmed.length === 0,
202
+ };
203
+ });
204
+ const nonEmptyLines = lines.filter((line) => !line.isEmpty);
205
+ const firstNonEmptyLine = nonEmptyLines[0] ?? null;
206
+ const lastNonEmptyLine = nonEmptyLines[nonEmptyLines.length - 1] ?? null;
207
+ let promptLineIndex = -1;
208
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
209
+ if (isPromptLikeCliLine(lines[i].text)) {
210
+ promptLineIndex = i;
211
+ break;
212
+ }
213
+ }
214
+ return {
215
+ text: normalizedText,
216
+ lineCount: lines.length,
217
+ lines,
218
+ nonEmptyLines,
219
+ firstNonEmptyLineIndex: firstNonEmptyLine?.index ?? -1,
220
+ lastNonEmptyLineIndex: lastNonEmptyLine?.index ?? -1,
221
+ firstNonEmptyLine,
222
+ lastNonEmptyLine,
223
+ promptLineIndex,
224
+ promptLine: promptLineIndex >= 0 ? lines[promptLineIndex] : null,
225
+ linesAbovePrompt: promptLineIndex >= 0 ? lines.slice(0, promptLineIndex) : [...lines],
226
+ linesBelowPrompt: promptLineIndex >= 0 ? lines.slice(promptLineIndex + 1) : [],
227
+ };
228
+ }
229
+
230
+ export const buildCliSpawnEnv = sanitizeSpawnEnv;
231
+
232
+ export function computeTerminalQueryTail(buffer: string): string {
233
+ const prefixes = ['\x1b[6n', '\x1b[?6n'];
234
+ const maxLength = prefixes.reduce((n, value) => Math.max(n, value.length), 0) - 1;
235
+ const start = Math.max(0, buffer.length - maxLength);
236
+ for (let i = start; i < buffer.length; i++) {
237
+ const suffix = buffer.slice(i);
238
+ if (prefixes.some((pattern) => suffix.length < pattern.length && pattern.startsWith(suffix))) {
239
+ return suffix;
240
+ }
241
+ }
242
+ return '';
243
+ }
244
+
245
+ export function findBinary(name: string): string {
246
+ const trimmed = String(name || '').trim();
247
+ if (!trimmed) return trimmed;
248
+ const expanded = trimmed.startsWith('~')
249
+ ? path.join(os.homedir(), trimmed.slice(1))
250
+ : trimmed;
251
+ if (path.isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\')) {
252
+ return path.isAbsolute(expanded) ? expanded : path.resolve(expanded);
253
+ }
254
+ const isWin = os.platform() === 'win32';
255
+ try {
256
+ const cmd = isWin ? `where ${trimmed}` : `which ${trimmed}`;
257
+ return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim().split('\n')[0].trim();
258
+ } catch {
259
+ return isWin ? `${trimmed}.cmd` : trimmed;
260
+ }
261
+ }
262
+
263
+ export function isScriptBinary(binaryPath: string): boolean {
264
+ if (!path.isAbsolute(binaryPath)) return false;
265
+ try {
266
+ const fs = require('fs');
267
+ const resolved = fs.realpathSync(binaryPath);
268
+ const head = Buffer.alloc(8);
269
+ const fd = fs.openSync(resolved, 'r');
270
+ fs.readSync(fd, head, 0, 8, 0);
271
+ fs.closeSync(fd);
272
+ let i = 0;
273
+ if (head[0] === 0xef && head[1] === 0xbb && head[2] === 0xbf) i = 3;
274
+ return head[i] === 0x23 && head[i + 1] === 0x21;
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ export function looksLikeMachOOrElf(filePath: string): boolean {
281
+ if (!path.isAbsolute(filePath)) return false;
282
+ try {
283
+ const fs = require('fs');
284
+ const resolved = fs.realpathSync(filePath);
285
+ const buf = Buffer.alloc(8);
286
+ const fd = fs.openSync(resolved, 'r');
287
+ fs.readSync(fd, buf, 0, 8, 0);
288
+ fs.closeSync(fd);
289
+ let i = 0;
290
+ if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) i = 3;
291
+ const b = buf.subarray(i);
292
+ if (b.length < 4) return false;
293
+ if (b[0] === 0x7f && b[1] === 0x45 && b[2] === 0x4c && b[3] === 0x46) return true;
294
+ const le = b.readUInt32LE(0);
295
+ const be = b.readUInt32BE(0);
296
+ const magics = [0xfeedface, 0xfeedfacf, 0xcafebabe, 0xbebafeca];
297
+ return magics.some(m => m === le || m === be);
298
+ } catch {
299
+ return false;
300
+ }
301
+ }
302
+
303
+ export function shSingleQuote(arg: string): string {
304
+ if (/^[a-zA-Z0-9@%_+=:,./-]+$/.test(arg)) return arg;
305
+ if (os.platform() === 'win32') {
306
+ return `"${arg.replace(/"/g, '""')}"`;
307
+ }
308
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
309
+ }
310
+
311
+ export function estimatePromptDisplayLines(text: string, cols = 80): number {
312
+ const normalized = String(text || '').replace(/\r/g, '');
313
+ if (!normalized) return 1;
314
+ return normalized
315
+ .split('\n')
316
+ .reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / cols)), 0);
317
+ }
318
+
319
+ export function extractPromptRetrySnippet(text: string): string {
320
+ const lines = String(text || '')
321
+ .replace(/\r/g, '')
322
+ .split('\n')
323
+ .map(line => line.trim())
324
+ .filter(Boolean);
325
+ const candidate = lines[lines.length - 1] || lines[0] || '';
326
+ return candidate.slice(-120);
327
+ }
328
+
329
+ export function normalizePromptText(text: string): string {
330
+ return String(text || '').replace(/\s+/g, ' ').trim();
331
+ }
332
+
333
+ export function compactPromptText(text: string): string {
334
+ return String(text || '').replace(/\s+/g, '').trim();
335
+ }
336
+
337
+ export function promptLikelyVisible(screenText: string, promptSnippet: string): boolean {
338
+ const snippet = normalizePromptText(promptSnippet);
339
+ if (!snippet) return false;
340
+
341
+ const normalizedScreen = normalizePromptText(screenText);
342
+ if (normalizedScreen.includes(snippet)) return true;
343
+
344
+ const compactScreen = compactPromptText(screenText);
345
+ const compactSnippet = compactPromptText(promptSnippet);
346
+ if (compactSnippet && compactScreen.includes(compactSnippet)) return true;
347
+
348
+ const tokens = snippet
349
+ .split(/[^A-Za-z0-9_.:/-]+/)
350
+ .map(token => token.trim())
351
+ .filter(token => token.length >= 4);
352
+ if (tokens.length === 0) return false;
353
+
354
+ const required = Math.min(tokens.length, 3);
355
+ const matched = tokens.filter(token =>
356
+ normalizedScreen.includes(token) || compactScreen.includes(compactPromptText(token)),
357
+ ).length;
358
+ return matched >= required;
359
+ }
360
+
361
+ export function normalizeScreenSnapshot(text: string): string {
362
+ return sanitizeTerminalText(String(text || ''))
363
+ .replace(/\s+/g, ' ')
364
+ .trim();
365
+ }
366
+
367
+ export function normalizeComparableMessageContent(text: string): string {
368
+ return String(text || '')
369
+ .replace(/\s+/g, ' ')
370
+ .trim();
371
+ }
372
+
373
+ export function trimPromptEchoPrefix(text: string, promptText?: string | null): string {
374
+ const prompt = normalizeComparableMessageContent(String(promptText || ''));
375
+ if (!prompt) return String(text || '');
376
+
377
+ const lines = String(text || '').split(/\r\n|\n|\r/g);
378
+ let dropCount = 0;
379
+ for (let index = 0; index < Math.min(lines.length, 6); index += 1) {
380
+ const fragment = normalizeComparableMessageContent(lines[index].replace(/^[.…]+\s*/, ''));
381
+ if (!fragment) {
382
+ if (dropCount === index) dropCount = index + 1;
383
+ continue;
384
+ }
385
+ const fragmentWordCount = fragment ? fragment.split(/\s+/).filter(Boolean).length : 0;
386
+ const canBePromptEcho = fragment.length >= 16 || fragmentWordCount >= 4;
387
+ if (canBePromptEcho && prompt.includes(fragment)) {
388
+ dropCount = index + 1;
389
+ continue;
390
+ }
391
+ break;
392
+ }
393
+
394
+ return lines.slice(dropCount).join('\n').trim();
395
+ }
396
+
397
+ export function getLastUserPromptText(messages: Array<{ role?: string; content?: string }> | null | undefined): string {
398
+ const items = Array.isArray(messages) ? messages : [];
399
+ for (let index = items.length - 1; index >= 0; index -= 1) {
400
+ const message = items[index];
401
+ if (message?.role === 'user' && typeof message.content === 'string' && message.content.trim()) {
402
+ return message.content;
403
+ }
404
+ }
405
+ return '';
406
+ }
407
+
408
+ export function looksLikeConfirmOnlyLabel(label: string): boolean {
409
+ return /^(?:continue|confirm|ok|yes|trust|proceed|enter)$/i.test(String(label || '').trim());
410
+ }
411
+
412
+ function parsePatternEntry(x: unknown): RegExp | null {
413
+ if (x instanceof RegExp) return x;
414
+ if (x && typeof x === 'object' && typeof (x as { source?: string }).source === 'string') {
415
+ try {
416
+ const s = x as { source: string; flags?: string };
417
+ return new RegExp(s.source, s.flags || '');
418
+ } catch {
419
+ return null;
420
+ }
421
+ }
422
+ return null;
423
+ }
424
+
425
+ function coercePatternArray(raw: unknown): RegExp[] {
426
+ if (!Array.isArray(raw)) return [];
427
+ return raw.map(parsePatternEntry).filter((r): r is RegExp => r != null);
428
+ }
429
+
430
+ export function normalizeCliProviderForRuntime(raw: unknown): { patterns: { approval: RegExp[] } } {
431
+ const patterns = raw && typeof raw === 'object' ? (raw as { patterns?: unknown }).patterns : undefined;
432
+ return {
433
+ patterns: {
434
+ approval: coercePatternArray(
435
+ patterns && typeof patterns === 'object' ? (patterns as { approval?: unknown }).approval : undefined,
436
+ ),
437
+ },
438
+ };
439
+ }
@@ -26,7 +26,7 @@ let cachedBindingError: Error | null = null;
26
26
  function isModuleNotFoundError(error: unknown, ref: string): boolean {
27
27
  if (!(error instanceof Error)) return false;
28
28
  const message = error.message || '';
29
- const code = (error as any).code;
29
+ const code = 'code' in error ? error.code : undefined;
30
30
  return code === 'MODULE_NOT_FOUND' && message.includes(ref);
31
31
  }
32
32
 
@@ -62,12 +62,17 @@ export async function handleCdpCommand(h: CommandHelpers, args: any): Promise<Co
62
62
 
63
63
  export async function handleCdpBatch(h: CommandHelpers, args: any): Promise<CommandResult> {
64
64
  if (!h.getCdp()?.isConnected) return { success: false, error: 'CDP not connected' };
65
- const commands = args?.commands as any[];
65
+ const commands = Array.isArray(args?.commands) ? args.commands : null;
66
66
  const stopOnError = args?.stopOnError !== false;
67
67
  if (!commands?.length) return { success: false, error: 'commands array required' };
68
68
 
69
69
  const results: any[] = [];
70
70
  for (const cmd of commands) {
71
+ if (!cmd || typeof cmd !== 'object' || typeof cmd.method !== 'string') {
72
+ results.push({ method: null, success: false, error: 'Invalid command entry' });
73
+ if (stopOnError) break;
74
+ continue;
75
+ }
71
76
  try {
72
77
  const result = await h.getCdp()!.sendCdpCommand(cmd.method, cmd.params || {});
73
78
  results.push({ method: cmd.method, success: true, result });
@@ -4,6 +4,9 @@
4
4
  */
5
5
 
6
6
  import type { CommandResult, CommandHelpers } from './handler.js';
7
+ import type { CliAdapter } from '../cli-adapter-types.js';
8
+ import type { ProviderModule, ProviderScripts } from '../providers/contracts.js';
9
+ import type { ProviderInstance } from '../providers/provider-instance.js';
7
10
  import { readChatHistory } from '../config/chat-history.js';
8
11
  import { LOG } from '../logging/logger.js';
9
12
  import type { SessionTransport } from '../shared-types.js';
@@ -11,6 +14,12 @@ import type { SessionTransport } from '../shared-types.js';
11
14
  const RECENT_SEND_WINDOW_MS = 1200;
12
15
  const recentSendByTarget = new Map<string, number>();
13
16
 
17
+ interface ApprovalSelectableInstance extends ProviderInstance {
18
+ recordApprovalSelection?(buttonText: string): void;
19
+ }
20
+
21
+ type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
22
+
14
23
  function getCurrentProviderType(h: CommandHelpers, fallback = ''): string {
15
24
  return h.currentSession?.providerType || h.currentProviderType || fallback;
16
25
  }
@@ -19,18 +28,18 @@ function getCurrentManagerKey(h: CommandHelpers): string {
19
28
  return h.currentSession?.cdpManagerKey || h.currentManagerKey || '';
20
29
  }
21
30
 
22
- function getTargetedCliAdapter(h: CommandHelpers, args: any, providerType?: string) {
31
+ function getTargetedCliAdapter(h: CommandHelpers, args: any, providerType?: string): CliAdapter | null {
23
32
  return h.getCliAdapter(args?.targetSessionId || providerType || h.currentSession?.providerType || h.currentManagerKey);
24
33
  }
25
34
 
26
- function getTargetInstance(h: CommandHelpers, args: any) {
35
+ function getTargetInstance(h: CommandHelpers, args: any): ApprovalSelectableInstance | null {
27
36
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
28
37
  const sessionId = targetSessionId || h.currentSession?.sessionId || '';
29
38
  if (!sessionId) return null;
30
- return h.ctx.instanceManager?.getInstance(sessionId) as any;
39
+ return (h.ctx.instanceManager?.getInstance(sessionId) as ApprovalSelectableInstance | undefined) || null;
31
40
  }
32
41
 
33
- function getTargetTransport(h: CommandHelpers, provider?: any): SessionTransport | null {
42
+ function getTargetTransport(h: CommandHelpers, provider?: ProviderModule): SessionTransport | null {
34
43
  if (h.currentSession?.transport) return h.currentSession.transport;
35
44
  switch (provider?.category) {
36
45
  case 'cli':
@@ -54,7 +63,7 @@ function isExtensionTransport(transport: SessionTransport | null): boolean {
54
63
  return transport === 'cdp-webview';
55
64
  }
56
65
 
57
- function buildRecentSendKey(h: CommandHelpers, args: any, provider: any, text: string): string {
66
+ function buildRecentSendKey(h: CommandHelpers, args: any, provider: ProviderModule | undefined, text: string): string {
58
67
  const transport = getTargetTransport(h, provider) || 'unknown';
59
68
  const target =
60
69
  args?.targetSessionId
@@ -73,12 +82,17 @@ function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
73
82
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
74
83
  if (!targetSessionId) return undefined;
75
84
 
76
- const instance = h.ctx.instanceManager?.getInstance(targetSessionId) as any;
85
+ const instance = h.ctx.instanceManager?.getInstance(targetSessionId);
77
86
  const state = instance?.getState?.();
78
87
  const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
79
88
  return providerSessionId || targetSessionId;
80
89
  }
81
90
 
91
+ function callLegacyTextScript(script: ProviderScripts[keyof ProviderScripts] | undefined, text: string): string | null {
92
+ if (typeof script !== 'function') return null;
93
+ return (script as LegacyStringScript)(text);
94
+ }
95
+
82
96
  function isRecentDuplicateSend(key: string): boolean {
83
97
  const now = Date.now();
84
98
  for (const [candidate, ts] of recentSendByTarget.entries()) {
@@ -184,8 +198,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
184
198
  if (isCliLikeTransport(transport)) {
185
199
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
186
200
  if (adapter) {
187
- _log(`${transport} adapter: ${(adapter as any).cliType}`);
188
- const status = (adapter as any).getStatus?.();
201
+ _log(`${transport} adapter: ${adapter.cliType}`);
202
+ const status = adapter.getStatus();
189
203
  if (status) {
190
204
  return {
191
205
  success: true,
@@ -333,10 +347,10 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
333
347
  if (isCliLikeTransport(transport)) {
334
348
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
335
349
  if (adapter) {
336
- _log(`${transport} adapter: ${(adapter as any).cliType}`);
350
+ _log(`${transport} adapter: ${adapter.cliType}`);
337
351
  try {
338
352
  await adapter.sendMessage(text);
339
- return _logSendSuccess(`${transport}-adapter`, (adapter as any).cliType);
353
+ return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
340
354
  } catch (e: any) {
341
355
  return { success: false, error: `${transport} send failed: ${e.message}` };
342
356
  }
@@ -433,7 +447,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
433
447
  }
434
448
  if (parsed?.needsTypeAndSend && provider?.webviewMatchText && provider?.scripts?.webviewSendMessage) {
435
449
  try {
436
- const webviewScript = (provider.scripts as any).webviewSendMessage(text);
450
+ const webviewScript = callLegacyTextScript(provider.scripts.webviewSendMessage, text);
437
451
  if (webviewScript && targetCdp.evaluateInWebviewFrame) {
438
452
  const matchText = provider.webviewMatchText;
439
453
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
@@ -457,7 +471,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
457
471
 
458
472
  if (provider?.webviewMatchText && provider?.scripts?.webviewSendMessage) {
459
473
  try {
460
- const webviewScript = (provider.scripts as any).webviewSendMessage(text);
474
+ const webviewScript = callLegacyTextScript(provider.scripts.webviewSendMessage, text);
461
475
  if (webviewScript && targetCdp.evaluateInWebviewFrame) {
462
476
  const matchText = provider.webviewMatchText;
463
477
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
@@ -555,8 +569,8 @@ export async function handleNewChat(h: CommandHelpers, args: any): Promise<Comma
555
569
  if (transport === 'pty') {
556
570
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
557
571
  if (!adapter) return { success: false, error: 'CLI adapter not running' };
558
- if (typeof (adapter as any).clearHistory === 'function') {
559
- (adapter as any).clearHistory();
572
+ if (typeof adapter.clearHistory === 'function') {
573
+ adapter.clearHistory();
560
574
  return { success: true, cleared: true };
561
575
  }
562
576
  return { success: false, error: 'new_chat not supported by this CLI provider' };
@@ -692,12 +706,10 @@ export async function handleSetMode(h: CommandHelpers, args: any): Promise<Comma
692
706
  // ACP transport
693
707
  if (transport === 'acp') {
694
708
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
695
- if (adapter) {
696
- const acpInstance = (adapter as any)._acpInstance;
697
- if (acpInstance && typeof acpInstance.setMode === 'function') {
709
+ const acpInstance = adapter?._acpInstance;
710
+ if (acpInstance && typeof acpInstance.setMode === 'function') {
698
711
  await acpInstance.setMode(mode);
699
712
  return { success: true, mode };
700
- }
701
713
  }
702
714
  return { success: false, error: 'ACP adapter not found' };
703
715
  }
@@ -748,14 +760,12 @@ export async function handleChangeModel(h: CommandHelpers, args: any): Promise<C
748
760
  // ACP transport
749
761
  if (transport === 'acp') {
750
762
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
751
- LOG.info('Command', `[change_model] ACP adapter found: ${!!adapter}, type=${(adapter as any)?.cliType}, hasAcpInstance=${!!(adapter as any)?._acpInstance}`);
752
- if (adapter) {
753
- const acpInstance = (adapter as any)._acpInstance;
754
- if (acpInstance && typeof acpInstance.setConfigOption === 'function') {
763
+ LOG.info('Command', `[change_model] ACP adapter found: ${!!adapter}, type=${adapter?.cliType}, hasAcpInstance=${!!adapter?._acpInstance}`);
764
+ const acpInstance = adapter?._acpInstance;
765
+ if (acpInstance && typeof acpInstance.setConfigOption === 'function') {
755
766
  await acpInstance.setConfigOption('model', model);
756
767
  LOG.info('Command', `[change_model] Updated ACP model to ${model}`);
757
768
  return { success: true, model };
758
- }
759
769
  }
760
770
  return { success: false, error: 'ACP adapter not found' };
761
771
  }
@@ -809,6 +819,9 @@ export async function handleSetThoughtLevel(h: CommandHelpers, args: any): Promi
809
819
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
810
820
  const acpInstance = adapter?._acpInstance;
811
821
  if (!acpInstance) return { success: false, error: 'ACP instance not found' };
822
+ if (typeof acpInstance.setConfigOption !== 'function') {
823
+ return { success: false, error: 'ACP setConfigOption not available' };
824
+ }
812
825
 
813
826
  try {
814
827
  await acpInstance.setConfigOption(configId, value);
@@ -834,9 +847,9 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
834
847
  if (!adapter) return { success: false, error: 'CLI adapter not running' };
835
848
 
836
849
  // Handle data-driven resolve actions (like from the dashboard 'Fix' button)
837
- if (args?.data && typeof (adapter as any).resolveAction === 'function') {
850
+ if (args?.data && typeof adapter.resolveAction === 'function') {
838
851
  try {
839
- await (adapter as any).resolveAction(args.data);
852
+ await adapter.resolveAction(args.data);
840
853
  LOG.info('Command', `[resolveAction] CLI PTY → resolveAction triggered with data payload`);
841
854
  return { success: true, method: 'cli-resolve-action' };
842
855
  } catch (e: any) {
@@ -844,7 +857,7 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
844
857
  }
845
858
  }
846
859
 
847
- const status = (adapter as any).getStatus?.();
860
+ const status = adapter.getStatus();
848
861
  if (status?.status !== 'waiting_approval') {
849
862
  return { success: false, error: 'Not in approval state' };
850
863
  }
@@ -866,11 +879,11 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
866
879
  buttonIndex = 0; // approve → first option (default selected)
867
880
  }
868
881
  }
869
- if (typeof (adapter as any).resolveModal === 'function') {
870
- (adapter as any).resolveModal(buttonIndex);
882
+ if (typeof adapter.resolveModal === 'function') {
883
+ adapter.resolveModal(buttonIndex);
871
884
  } else {
872
885
  const keys = '\x1B[B'.repeat(Math.max(0, buttonIndex)) + '\r';
873
- (adapter as any).writeRaw?.(keys);
886
+ adapter.writeRaw?.(keys);
874
887
  }
875
888
  LOG.info('Command', `[resolveAction] CLI PTY → buttonIndex=${buttonIndex} "${buttons[buttonIndex] ?? '?'}"`);
876
889
  getTargetInstance(h, args)?.recordApprovalSelection?.(buttons[buttonIndex] ?? button);
@@ -888,6 +901,9 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
888
901
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
889
902
  const acpInstance = adapter?._acpInstance;
890
903
  if (!acpInstance) return { success: false, error: 'ACP instance not found' };
904
+ if (typeof acpInstance.resolvePermission !== 'function') {
905
+ return { success: false, error: 'ACP resolvePermission not available' };
906
+ }
891
907
 
892
908
  try {
893
909
  await acpInstance.resolvePermission(action === 'approve' || action === 'accept' || action === 'always');