@adhdev/daemon-core 0.9.76-rc.66 → 0.9.76-rc.67
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/cli-adapters/provider-cli-adapter.d.ts +3 -1
- package/dist/cli-adapters/provider-cli-shared.d.ts +24 -0
- package/dist/index.js +193 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +193 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +18 -7
- package/src/cli-adapters/provider-cli-shared.ts +199 -15
- package/src/mesh/mesh-events.ts +20 -3
package/package.json
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
normalizeScreenSnapshot,
|
|
36
36
|
promptLikelyVisible,
|
|
37
37
|
sanitizeTerminalText,
|
|
38
|
+
TerminalTranscriptAccumulator,
|
|
38
39
|
type CliChatMessage,
|
|
39
40
|
type CliProviderModule,
|
|
40
41
|
type CliScriptInput,
|
|
@@ -195,8 +196,10 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
195
196
|
// ─── CLI Scripts (script-based parsing) ───
|
|
196
197
|
private cliScripts: CliScripts;
|
|
197
198
|
private runtimeSettings: Record<string, any> = {};
|
|
198
|
-
/** Full accumulated
|
|
199
|
+
/** Full accumulated rendered PTY transcript for parser/readback use */
|
|
199
200
|
private accumulatedBuffer: string = '';
|
|
201
|
+
/** Stateful rendered transcript accumulator; raw debug remains in accumulatedRawBuffer. */
|
|
202
|
+
private transcriptAccumulator = new TerminalTranscriptAccumulator();
|
|
200
203
|
/** Full accumulated raw PTY output (with ANSI) */
|
|
201
204
|
private accumulatedRawBuffer: string = '';
|
|
202
205
|
/** Current visible terminal screen snapshot */
|
|
@@ -287,6 +290,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
287
290
|
|
|
288
291
|
private resetTerminalScreen(rows?: number, cols?: number): void {
|
|
289
292
|
this.terminalScreen.reset(rows, cols);
|
|
293
|
+
this.transcriptAccumulator.reset();
|
|
290
294
|
this.lastScreenText = '';
|
|
291
295
|
this.lastScreenSnapshot = '';
|
|
292
296
|
this.lastScreenChangeAt = 0;
|
|
@@ -634,6 +638,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
634
638
|
private handleOutput(rawData: string): void {
|
|
635
639
|
this.terminalScreen.write(rawData);
|
|
636
640
|
const cleanData = sanitizeTerminalText(rawData);
|
|
641
|
+
const renderedTranscript = this.transcriptAccumulator.append(rawData);
|
|
637
642
|
const now = Date.now();
|
|
638
643
|
const shouldReadScreen = this.shouldReadTerminalScreenSnapshot(now);
|
|
639
644
|
const screenText = shouldReadScreen ? this.readTerminalScreenText(now) : this.lastScreenText;
|
|
@@ -680,15 +685,21 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
680
685
|
}
|
|
681
686
|
}
|
|
682
687
|
|
|
683
|
-
// Rolling buffers
|
|
688
|
+
// Rolling parser/readback buffers. `accumulatedBuffer` and
|
|
689
|
+
// `recentOutputBuffer` intentionally use the rendered transcript state,
|
|
690
|
+
// not raw PTY append text, so overwritten CLI status/tool lines do not
|
|
691
|
+
// leak stale cells into read_chat / mesh_read_chat compact summaries.
|
|
684
692
|
const prevRecentLen = this.recentOutputBuffer.length;
|
|
685
|
-
const prevAccumulatedLen = this.accumulatedBuffer.length;
|
|
686
693
|
const prevAccumulatedRawLen = this.accumulatedRawBuffer.length;
|
|
687
|
-
|
|
688
|
-
|
|
694
|
+
const nextAccumulatedBuffer = renderedTranscript.length <= ProviderCliAdapter.MAX_ACCUMULATED_BUFFER
|
|
695
|
+
? renderedTranscript
|
|
696
|
+
: renderedTranscript.slice(-ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
|
|
697
|
+
const nextRecentOutputBuffer = nextAccumulatedBuffer.slice(-ProviderCliAdapter.MAX_RECENT_OUTPUT_BUFFER);
|
|
698
|
+
this.recentOutputBuffer = nextRecentOutputBuffer;
|
|
699
|
+
this.accumulatedBuffer = nextAccumulatedBuffer;
|
|
689
700
|
this.accumulatedRawBuffer = appendBoundedText(this.accumulatedRawBuffer, rawData, ProviderCliAdapter.MAX_ACCUMULATED_BUFFER);
|
|
690
|
-
const droppedRecent =
|
|
691
|
-
const droppedClean =
|
|
701
|
+
const droppedRecent = Math.max(0, prevRecentLen - this.recentOutputBuffer.length);
|
|
702
|
+
const droppedClean = Math.max(0, renderedTranscript.length - this.accumulatedBuffer.length);
|
|
692
703
|
const droppedRaw = this.recordBoundedAppendDrop(prevAccumulatedRawLen, rawData.length, this.accumulatedRawBuffer.length);
|
|
693
704
|
this.recentOutputDroppedChars += droppedRecent;
|
|
694
705
|
this.accumulatedBufferDroppedChars += droppedClean;
|
|
@@ -173,32 +173,216 @@ export interface CliProviderModule {
|
|
|
173
173
|
function stripAnsi(str: string): string {
|
|
174
174
|
// eslint-disable-next-line no-control-regex
|
|
175
175
|
return str
|
|
176
|
-
.replace(/\x1B\][^\x07]
|
|
177
|
-
.replace(/\x1B\][\s\S]*?\x1B\\/g, '')
|
|
176
|
+
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
|
|
178
177
|
.replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
|
|
179
|
-
.replace(/\x1B
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type SavedCursor = { row: number; col: number };
|
|
182
|
+
|
|
183
|
+
function parseCount(params: string, fallback = 1): number {
|
|
184
|
+
const first = Number(String(params || '').split(';')[0] || fallback);
|
|
185
|
+
return Math.max(1, Number.isFinite(first) ? first : fallback);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function isCombiningMark(ch: string): boolean {
|
|
189
|
+
return /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/.test(ch);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isWideCodePoint(ch: string): boolean {
|
|
193
|
+
const cp = ch.codePointAt(0) || 0;
|
|
194
|
+
return cp >= 0x1100 && (
|
|
195
|
+
cp <= 0x115F || cp === 0x2329 || cp === 0x232A ||
|
|
196
|
+
(cp >= 0x2E80 && cp <= 0xA4CF && cp !== 0x303F) ||
|
|
197
|
+
(cp >= 0xAC00 && cp <= 0xD7A3) ||
|
|
198
|
+
(cp >= 0xF900 && cp <= 0xFAFF) ||
|
|
199
|
+
(cp >= 0xFE10 && cp <= 0xFE19) ||
|
|
200
|
+
(cp >= 0xFE30 && cp <= 0xFE6F) ||
|
|
201
|
+
(cp >= 0xFF00 && cp <= 0xFF60) ||
|
|
202
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) ||
|
|
203
|
+
(cp >= 0x1F300 && cp <= 0x1FAFF)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Stateful, transcript-oriented terminal cell accumulator.
|
|
209
|
+
*
|
|
210
|
+
* CLI transcript parsing must not consume raw PTY append text for user-visible
|
|
211
|
+
* readback: CLIs rewrite prompts/status/tool lines with CR, BS, CSI cursor
|
|
212
|
+
* motion and erase-line. This accumulator preserves parser state across chunks
|
|
213
|
+
* and mutates rendered cells before exposing plain transcript text. It is a
|
|
214
|
+
* deliberately small terminal model for readback buffers; live UI rendering still
|
|
215
|
+
* uses TerminalScreen's ghostty/xterm backend.
|
|
216
|
+
*/
|
|
217
|
+
export class TerminalTranscriptAccumulator {
|
|
218
|
+
private lines: string[][] = [[]];
|
|
219
|
+
private row = 0;
|
|
220
|
+
private col = 0;
|
|
221
|
+
private savedCursor: SavedCursor | null = null;
|
|
222
|
+
private pendingEscape = '';
|
|
223
|
+
|
|
224
|
+
append(data: string): string {
|
|
225
|
+
const input = this.pendingEscape + String(data || '');
|
|
226
|
+
this.pendingEscape = '';
|
|
227
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
228
|
+
let ch = input[i];
|
|
229
|
+
if (ch === '\x1B') {
|
|
230
|
+
const consumed = this.consumeEscape(input.slice(i));
|
|
231
|
+
if (consumed === 0) {
|
|
232
|
+
this.pendingEscape = input.slice(i);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
i += consumed - 1;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const cp = input.codePointAt(i);
|
|
239
|
+
if (cp && cp > 0xFFFF) {
|
|
240
|
+
ch = String.fromCodePoint(cp);
|
|
241
|
+
i += 1;
|
|
242
|
+
}
|
|
243
|
+
this.writeControlOrChar(ch);
|
|
244
|
+
}
|
|
245
|
+
return this.getText();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
reset(): void {
|
|
249
|
+
this.lines = [[]];
|
|
250
|
+
this.row = 0;
|
|
251
|
+
this.col = 0;
|
|
252
|
+
this.savedCursor = null;
|
|
253
|
+
this.pendingEscape = '';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getText(): string {
|
|
257
|
+
return this.lines.map(line => line.join('').replace(/[ \t]+$/g, '')).join('\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private ensureRow(row = this.row): void {
|
|
261
|
+
while (this.lines.length <= row) this.lines.push([]);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private writeControlOrChar(ch: string): void {
|
|
265
|
+
if (ch === '\r') {
|
|
266
|
+
this.col = 0;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (ch === '\n') {
|
|
270
|
+
this.row += 1;
|
|
271
|
+
this.col = 0;
|
|
272
|
+
this.ensureRow();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (ch === '\b') {
|
|
276
|
+
this.col = Math.max(0, this.col - 1);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (ch < ' ' || ch === '\x7F') return;
|
|
280
|
+
|
|
281
|
+
this.ensureRow();
|
|
282
|
+
const line = this.lines[this.row];
|
|
283
|
+
if (isCombiningMark(ch) && this.col > 0) {
|
|
284
|
+
line[this.col - 1] = `${line[this.col - 1] || ''}${ch}`;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
while (line.length < this.col) line.push(' ');
|
|
288
|
+
line[this.col] = ch;
|
|
289
|
+
this.col += isWideCodePoint(ch) ? 2 : 1;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private consumeEscape(seq: string): number {
|
|
293
|
+
if (seq.length < 2) return 0;
|
|
294
|
+
const next = seq[1];
|
|
295
|
+
if (next === '7') {
|
|
296
|
+
this.savedCursor = { row: this.row, col: this.col };
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
if (next === '8') {
|
|
300
|
+
if (this.savedCursor) {
|
|
301
|
+
this.row = this.savedCursor.row;
|
|
302
|
+
this.col = this.savedCursor.col;
|
|
303
|
+
this.ensureRow();
|
|
304
|
+
}
|
|
305
|
+
return 2;
|
|
306
|
+
}
|
|
307
|
+
if (next === ']') {
|
|
308
|
+
const bel = seq.indexOf('\x07', 2);
|
|
309
|
+
const st = seq.indexOf('\x1B\\', 2);
|
|
310
|
+
const end = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st >= 0 ? st + 2 : 0;
|
|
311
|
+
return end;
|
|
312
|
+
}
|
|
313
|
+
if (next === '[') {
|
|
314
|
+
const match = seq.match(/^\x1B\[([0-?]*)([ -/]*)([@-~])/);
|
|
315
|
+
if (!match) return seq.length < 32 ? 0 : 1;
|
|
316
|
+
this.applyCsi(match[1] || '', match[3]);
|
|
317
|
+
return match[0].length;
|
|
318
|
+
}
|
|
319
|
+
if (/[P^_X]/.test(next)) {
|
|
320
|
+
const bel = seq.indexOf('\x07', 2);
|
|
321
|
+
const st = seq.indexOf('\x1B\\', 2);
|
|
322
|
+
const end = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st >= 0 ? st + 2 : 0;
|
|
323
|
+
return end;
|
|
324
|
+
}
|
|
325
|
+
return 2;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private applyCsi(params: string, final: string): void {
|
|
329
|
+
const count = parseCount(params);
|
|
330
|
+
this.ensureRow();
|
|
331
|
+
if (final === 'A') this.row = Math.max(0, this.row - count);
|
|
332
|
+
else if (final === 'B') this.row += count;
|
|
333
|
+
else if (final === 'C') this.col += count;
|
|
334
|
+
else if (final === 'D') this.col = Math.max(0, this.col - count);
|
|
335
|
+
else if (final === 'G') this.col = Math.max(0, count - 1);
|
|
336
|
+
else if (final === 'H' || final === 'f') {
|
|
337
|
+
const parts = String(params || '').split(';');
|
|
338
|
+
this.row = Math.max(0, (Number(parts[0] || 1) || 1) - 1);
|
|
339
|
+
this.col = Math.max(0, (Number(parts[1] || 1) || 1) - 1);
|
|
340
|
+
} else if (final === 'J') {
|
|
341
|
+
const mode = Number(params || 0) || 0;
|
|
342
|
+
if (mode === 2 || mode === 3) {
|
|
343
|
+
this.lines = [[]];
|
|
344
|
+
this.row = 0;
|
|
345
|
+
this.col = 0;
|
|
346
|
+
} else if (mode === 0) {
|
|
347
|
+
this.lines[this.row] = this.lines[this.row].slice(0, this.col);
|
|
348
|
+
this.lines.splice(this.row + 1);
|
|
349
|
+
} else if (mode === 1) {
|
|
350
|
+
for (let r = 0; r < this.row; r += 1) this.lines[r] = [];
|
|
351
|
+
const line = this.lines[this.row];
|
|
352
|
+
for (let c = 0; c <= Math.min(this.col, line.length - 1); c += 1) line[c] = ' ';
|
|
353
|
+
}
|
|
354
|
+
} else if (final === 'K') {
|
|
355
|
+
const mode = Number(params || 0) || 0;
|
|
356
|
+
const line = this.lines[this.row];
|
|
357
|
+
if (mode === 2) this.lines[this.row] = [];
|
|
358
|
+
else if (mode === 1) {
|
|
359
|
+
for (let c = 0; c <= Math.min(this.col, line.length - 1); c += 1) line[c] = ' ';
|
|
360
|
+
} else {
|
|
361
|
+
this.lines[this.row] = line.slice(0, this.col);
|
|
362
|
+
}
|
|
363
|
+
} else if (final === 's') {
|
|
364
|
+
this.savedCursor = { row: this.row, col: this.col };
|
|
365
|
+
} else if (final === 'u') {
|
|
366
|
+
if (this.savedCursor) {
|
|
367
|
+
this.row = this.savedCursor.row;
|
|
368
|
+
this.col = this.savedCursor.col;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.ensureRow();
|
|
372
|
+
}
|
|
182
373
|
}
|
|
183
374
|
|
|
184
375
|
function stripTerminalNoise(str: string): string {
|
|
185
376
|
return String(str || '')
|
|
186
377
|
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
|
|
187
|
-
.replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
|
|
188
|
-
.replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
|
|
189
|
-
.replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
|
|
190
|
-
.replace(/(^|[\s([])(?:\d+\$r[0-9;\" ]*[A-Za-z]?)(?=$|[\s)\]])/g, '$1')
|
|
191
|
-
.replace(/(^|[\s([])(?:>\|[A-Za-z0-9_.:-]+(?:\([^)]*\))?)(?=$|[\s)\]])/g, '$1')
|
|
192
|
-
.replace(/(^|[\s([])(?:[A-Z]\d(?:\s+[A-Z]\d)+)(?=$|[\s)\]])/g, '$1')
|
|
193
|
-
.replace(/(^|[\s([])(?:\d+;[^\s)\]]+)(?=$|[\s)\]])/g, '$1')
|
|
194
378
|
.replace(/\r+/g, '\n')
|
|
195
379
|
.replace(/[ \t]+\n/g, '\n')
|
|
196
|
-
.replace(/\n{
|
|
197
|
-
.replace(/ {2,}/g, ' ');
|
|
380
|
+
.replace(/\n{4,}/g, '\n\n\n');
|
|
198
381
|
}
|
|
199
382
|
|
|
200
383
|
export function sanitizeTerminalText(str: string): string {
|
|
201
|
-
|
|
384
|
+
const accumulator = new TerminalTranscriptAccumulator();
|
|
385
|
+
return stripTerminalNoise(stripAnsi(accumulator.append(str)));
|
|
202
386
|
}
|
|
203
387
|
|
|
204
388
|
export function listCliScriptNames(scripts: CliScripts | undefined): string[] {
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -6,6 +6,17 @@ function readNonEmptyString(value: unknown): string {
|
|
|
6
6
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const MESH_COORDINATOR_EVENTS = new Set([
|
|
10
|
+
'agent:generating_completed',
|
|
11
|
+
'agent:waiting_approval',
|
|
12
|
+
'agent:stopped',
|
|
13
|
+
'monitor:long_generating',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
17
|
+
return typeof eventName === 'string' && MESH_COORDINATOR_EVENTS.has(eventName);
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
10
21
|
const parts = [
|
|
11
22
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
@@ -27,6 +38,12 @@ function buildMeshSystemMessage(args: {
|
|
|
27
38
|
if (args.event === 'agent:waiting_approval') {
|
|
28
39
|
return `[System] ${args.nodeLabel} is waiting for approval to proceed${metadata}. You may use mesh_read_chat and mesh_approve to handle it.`;
|
|
29
40
|
}
|
|
41
|
+
if (args.event === 'agent:stopped') {
|
|
42
|
+
return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
|
|
43
|
+
}
|
|
44
|
+
if (args.event === 'monitor:long_generating') {
|
|
45
|
+
return `[System] ${args.nodeLabel} has been generating for a long time${metadata}. Use mesh_read_chat once for a status check, but do not poll repeatedly.`;
|
|
46
|
+
}
|
|
30
47
|
return '';
|
|
31
48
|
}
|
|
32
49
|
|
|
@@ -63,7 +80,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
63
80
|
|
|
64
81
|
export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
|
|
65
82
|
const eventName = readNonEmptyString(payload.event);
|
|
66
|
-
if (eventName
|
|
83
|
+
if (!isMeshCoordinatorEvent(eventName)) {
|
|
67
84
|
return { success: false, error: 'unsupported mesh event' };
|
|
68
85
|
}
|
|
69
86
|
const meshId = readNonEmptyString(payload.meshId);
|
|
@@ -86,8 +103,8 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
86
103
|
|
|
87
104
|
export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
88
105
|
components.instanceManager.onEvent((event) => {
|
|
89
|
-
// We only care about
|
|
90
|
-
if (event.event
|
|
106
|
+
// We only care about lightweight Repo Mesh coordinator control/status hints.
|
|
107
|
+
if (!isMeshCoordinatorEvent(event.event)) return;
|
|
91
108
|
|
|
92
109
|
const instanceId = readNonEmptyString(event.instanceId);
|
|
93
110
|
if (!instanceId) return;
|