@adhdev/daemon-core 0.8.30 → 0.8.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-stream/manager.d.ts +1 -0
- package/dist/agent-stream/provider-adapter.d.ts +1 -0
- package/dist/agent-stream/types.d.ts +3 -0
- package/dist/boot/daemon-lifecycle.d.ts +2 -1
- package/dist/cdp/manager.d.ts +2 -0
- package/dist/cli-adapter-types.d.ts +34 -5
- package/dist/cli-adapters/provider-cli-adapter.d.ts +4 -158
- package/dist/cli-adapters/provider-cli-config.d.ts +30 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +42 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +29 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +158 -0
- package/dist/commands/handler.d.ts +4 -3
- package/dist/config/config.d.ts +4 -3
- package/dist/index.js +866 -592
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +868 -595
- package/dist/index.mjs.map +1 -1
- package/dist/providers/contracts.d.ts +10 -1
- package/dist/providers/provider-loader.d.ts +3 -0
- package/dist/status/reporter.d.ts +2 -3
- package/dist/status/snapshot.d.ts +2 -1
- package/node_modules/@adhdev/session-host-core/package.json +1 -1
- package/package.json +3 -1
- package/src/agent-stream/manager.ts +8 -2
- package/src/agent-stream/poller.ts +19 -5
- package/src/agent-stream/provider-adapter.ts +11 -7
- package/src/agent-stream/types.ts +3 -0
- package/src/boot/daemon-lifecycle.ts +7 -6
- package/src/cdp/initializer.ts +2 -2
- package/src/cdp/manager.ts +5 -0
- package/src/cdp/setup.ts +1 -1
- package/src/cli-adapter-types.ts +37 -5
- package/src/cli-adapters/provider-cli-adapter.ts +212 -795
- package/src/cli-adapters/provider-cli-config.ts +66 -0
- package/src/cli-adapters/provider-cli-parse.ts +202 -0
- package/src/cli-adapters/provider-cli-runtime.ts +142 -0
- package/src/cli-adapters/provider-cli-shared.ts +439 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +1 -1
- package/src/commands/cdp-commands.ts +6 -1
- package/src/commands/chat-commands.ts +45 -29
- package/src/commands/cli-manager.ts +28 -9
- package/src/commands/handler.ts +14 -10
- package/src/commands/router.ts +23 -10
- package/src/commands/stream-commands.ts +11 -5
- package/src/config/config.ts +4 -10
- package/src/daemon/dev-auto-implement.ts +22 -18
- package/src/daemon/dev-cli-debug.ts +59 -16
- package/src/daemon/dev-server.ts +67 -43
- package/src/providers/acp-provider-instance.ts +1 -1
- package/src/providers/cli-provider-instance.ts +2 -2
- package/src/providers/contracts.ts +12 -1
- package/src/providers/extension-provider-instance.ts +1 -1
- package/src/providers/ide-provider-instance.ts +39 -18
- package/src/providers/provider-loader.ts +85 -54
- package/src/providers/version-archive.ts +23 -5
- package/src/status/reporter.ts +18 -14
- 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 =
|
|
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
|
|
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
|
|
39
|
+
return (h.ctx.instanceManager?.getInstance(sessionId) as ApprovalSelectableInstance | undefined) || null;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
function getTargetTransport(h: CommandHelpers, provider?:
|
|
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:
|
|
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)
|
|
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: ${
|
|
188
|
-
const status =
|
|
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: ${
|
|
350
|
+
_log(`${transport} adapter: ${adapter.cliType}`);
|
|
337
351
|
try {
|
|
338
352
|
await adapter.sendMessage(text);
|
|
339
|
-
return _logSendSuccess(`${transport}-adapter`,
|
|
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
|
|
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
|
|
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
|
|
559
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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=${
|
|
752
|
-
|
|
753
|
-
|
|
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
|
|
850
|
+
if (args?.data && typeof adapter.resolveAction === 'function') {
|
|
838
851
|
try {
|
|
839
|
-
await
|
|
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 =
|
|
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
|
|
870
|
-
|
|
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
|
-
|
|
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');
|