@adhdev/daemon-core 0.6.55 → 0.6.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,6 +31,7 @@
31
31
  "license": "AGPL-3.0-or-later",
32
32
  "dependencies": {
33
33
  "@agentclientprotocol/sdk": "^0.16.1",
34
+ "@xterm/xterm": "^6.0.0",
34
35
  "chalk": "^5.3.0",
35
36
  "conf": "^13.0.0",
36
37
  "ws": "^8.19.0"
@@ -68,7 +68,7 @@ export interface CliScripts {
68
68
  /** Full PTY buffer → ReadChatResult (messages, status, activeModal) */
69
69
  parseOutput?: (input: CliScriptInput) => any;
70
70
  /** Lightweight status detection (high-frequency polling) → AgentStatus string */
71
- detectStatus?: (input: { tail: string }) => string | null;
71
+ detectStatus?: (input: { tail: string; screenText?: string; rawBuffer?: string }) => string | null;
72
72
  /** Parse approval modal from PTY output → ModalInfo | null */
73
73
  parseApproval?: (input: { buffer: string; rawBuffer?: string; tail: string }) => { message: string; buttons: string[] } | null;
74
74
  /** Produce a cli-specific prompt from a dashboard action payload */
@@ -91,6 +91,8 @@ export interface CliProviderModule {
91
91
  name: string;
92
92
  category: 'cli';
93
93
  binary: string;
94
+ sendDelayMs?: number;
95
+ sendKey?: string;
94
96
  spawn: {
95
97
  command: string;
96
98
  args: string[];
@@ -191,6 +193,56 @@ function shSingleQuote(arg: string): string {
191
193
  return `'${arg.replace(/'/g, `'\\''`)}'`;
192
194
  }
193
195
 
196
+ function estimatePromptDisplayLines(text: string, cols = 100): number {
197
+ const normalized = String(text || '').replace(/\r/g, '');
198
+ if (!normalized) return 1;
199
+ return normalized
200
+ .split('\n')
201
+ .reduce((sum, line) => sum + Math.max(1, Math.ceil(Math.max(1, line.length) / cols)), 0);
202
+ }
203
+
204
+ function extractPromptRetrySnippet(text: string): string {
205
+ const lines = String(text || '')
206
+ .replace(/\r/g, '')
207
+ .split('\n')
208
+ .map(line => line.trim())
209
+ .filter(Boolean);
210
+ const candidate = lines[lines.length - 1] || lines[0] || '';
211
+ return candidate.slice(-120);
212
+ }
213
+
214
+ function normalizePromptText(text: string): string {
215
+ return String(text || '').replace(/\s+/g, ' ').trim();
216
+ }
217
+
218
+ function compactPromptText(text: string): string {
219
+ return String(text || '').replace(/\s+/g, '').trim();
220
+ }
221
+
222
+ function promptLikelyVisible(screenText: string, promptSnippet: string): boolean {
223
+ const snippet = normalizePromptText(promptSnippet);
224
+ if (!snippet) return false;
225
+
226
+ const normalizedScreen = normalizePromptText(screenText);
227
+ if (normalizedScreen.includes(snippet)) return true;
228
+
229
+ const compactScreen = compactPromptText(screenText);
230
+ const compactSnippet = compactPromptText(promptSnippet);
231
+ if (compactSnippet && compactScreen.includes(compactSnippet)) return true;
232
+
233
+ const tokens = snippet
234
+ .split(/[^A-Za-z0-9_.:/-]+/)
235
+ .map(token => token.trim())
236
+ .filter(token => token.length >= 4);
237
+ if (tokens.length === 0) return false;
238
+
239
+ const required = Math.min(tokens.length, 3);
240
+ const matched = tokens.filter(token =>
241
+ normalizedScreen.includes(token) || compactScreen.includes(compactPromptText(token)),
242
+ ).length;
243
+ return matched >= required;
244
+ }
245
+
194
246
  /**
195
247
  * Normalize provider.json for auto-implement approval detection.
196
248
  * Kept for backward compat with dev-server auto-impl pipeline only.
@@ -270,6 +322,12 @@ export class ProviderCliAdapter implements CliAdapter {
270
322
  // Output settle debounce — fires after PTY output goes quiet
271
323
  private settleTimer: NodeJS.Timeout | null = null;
272
324
  private settledBuffer: string = '';
325
+ private submitPendingUntil = 0;
326
+ private responseSettleIgnoreUntil = 0;
327
+ private responseEpoch = 0;
328
+ private submitRetryTimer: NodeJS.Timeout | null = null;
329
+ private submitRetryUsed = false;
330
+ private submitRetryPromptSnippet = '';
273
331
 
274
332
  // Resize redraw suppression
275
333
  private resizeSuppressUntil: number = 0;
@@ -302,6 +360,8 @@ export class ProviderCliAdapter implements CliAdapter {
302
360
 
303
361
  // Provider approval key mapping
304
362
  private readonly approvalKeys: Record<number, string>;
363
+ private readonly sendDelayMs: number;
364
+ private readonly sendKey: string;
305
365
 
306
366
  constructor(provider: CliProviderModule, workingDir: string, private extraArgs: string[] = []) {
307
367
  this.provider = provider;
@@ -325,6 +385,10 @@ export class ProviderCliAdapter implements CliAdapter {
325
385
 
326
386
  const rawKeys = (provider as any).approvalKeys;
327
387
  this.approvalKeys = (rawKeys && typeof rawKeys === 'object') ? rawKeys : {};
388
+ this.sendDelayMs = typeof (provider as any).sendDelayMs === 'number' ? Math.max(0, (provider as any).sendDelayMs) : 0;
389
+ this.sendKey = typeof (provider as any).sendKey === 'string' && (provider as any).sendKey.length > 0
390
+ ? (provider as any).sendKey
391
+ : '\r';
328
392
 
329
393
  // Scripts are required — loaded by ProviderLoader via compatibility array
330
394
  this.cliScripts = (provider as any).scripts || {};
@@ -450,7 +514,7 @@ export class ProviderCliAdapter implements CliAdapter {
450
514
  this.startupParseGate = true;
451
515
  this.startupBuffer = '';
452
516
  this.terminalScreen.reset(40, 120);
453
- this.ready = true;
517
+ this.ready = false;
454
518
  this.setStatus('idle', 'pty_ready');
455
519
  this.onStatusChange?.();
456
520
  }
@@ -460,6 +524,11 @@ export class ProviderCliAdapter implements CliAdapter {
460
524
  private handleOutput(rawData: string): void {
461
525
  if (Date.now() < this.resizeSuppressUntil) return;
462
526
 
527
+ if (rawData.includes('\x1b[6n') || rawData.includes('\x1b[?6n')) {
528
+ // Some TUIs probe cursor position during startup; node-pty does not answer automatically.
529
+ this.ptyProcess?.write('\x1b[1;1R');
530
+ }
531
+
463
532
  this.terminalScreen.write(rawData);
464
533
  const cleanData = stripAnsi(rawData);
465
534
 
@@ -507,7 +576,9 @@ export class ProviderCliAdapter implements CliAdapter {
507
576
 
508
577
  if (isReady) {
509
578
  this.startupParseGate = false;
579
+ this.ready = true;
510
580
  LOG.info('CLI', `[${this.cliType}] Startup gate end (${elapsed}ms, scriptStatus=${scriptStatus})`);
581
+ this.onStatusChange?.();
511
582
  } else {
512
583
  return;
513
584
  }
@@ -519,16 +590,49 @@ export class ProviderCliAdapter implements CliAdapter {
519
590
 
520
591
  private scheduleSettle(): void {
521
592
  if (this.settleTimer) clearTimeout(this.settleTimer);
593
+ const settleEpoch = this.responseEpoch;
594
+ const delay = Math.max(
595
+ this.timeouts.outputSettle,
596
+ this.submitPendingUntil > Date.now()
597
+ ? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
598
+ : 0,
599
+ );
522
600
  this.settleTimer = setTimeout(() => {
523
601
  this.settleTimer = null;
602
+ if (settleEpoch !== this.responseEpoch) return;
524
603
  this.settledBuffer = this.recentOutputBuffer;
525
604
  this.evaluateSettled();
526
- }, this.timeouts.outputSettle);
605
+ }, delay);
606
+ }
607
+
608
+ private armApprovalExitTimeout(): void {
609
+ if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
610
+ this.approvalExitTimeout = setTimeout(() => {
611
+ if (this.currentStatus !== 'waiting_approval') return;
612
+ const tail = this.recentOutputBuffer;
613
+ const modal = this.runParseApproval(tail);
614
+ const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
615
+ if (stillWaiting) {
616
+ this.activeModal = modal || this.activeModal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
617
+ this.onStatusChange?.();
618
+ this.armApprovalExitTimeout();
619
+ return;
620
+ }
621
+ LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
622
+ this.activeModal = null;
623
+ this.lastApprovalResolvedAt = Date.now();
624
+ this.setStatus('idle', 'approval_timeout');
625
+ this.onStatusChange?.();
626
+ }, 60000);
527
627
  }
528
628
 
529
629
  private evaluateSettled(): void {
630
+ if (this.submitPendingUntil > Date.now()) return;
631
+ if (this.responseSettleIgnoreUntil > Date.now()) return;
530
632
  const tail = this.settledBuffer;
531
- const scriptStatus = this.runDetectStatus(tail);
633
+ const modal = this.runParseApproval(tail);
634
+ const rawScriptStatus = this.runDetectStatus(tail);
635
+ const scriptStatus = rawScriptStatus === 'waiting_approval' || modal ? 'waiting_approval' : rawScriptStatus;
532
636
  if (!scriptStatus) return;
533
637
 
534
638
  const prevStatus = this.currentStatus;
@@ -540,20 +644,10 @@ export class ProviderCliAdapter implements CliAdapter {
540
644
  this.setStatus('waiting_approval', 'script_detect');
541
645
 
542
646
  // Use parseApproval script for modal info
543
- const modal = this.runParseApproval(tail);
544
647
  this.activeModal = modal || { message: 'Approval required', buttons: ['Allow', 'Deny'] };
545
648
 
546
649
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
547
- if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
548
- this.approvalExitTimeout = setTimeout(() => {
549
- if (this.currentStatus === 'waiting_approval') {
550
- LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
551
- this.activeModal = null;
552
- this.lastApprovalResolvedAt = Date.now();
553
- this.setStatus('idle', 'approval_timeout');
554
- this.onStatusChange?.();
555
- }
556
- }, 60000);
650
+ this.armApprovalExitTimeout();
557
651
  this.onStatusChange?.();
558
652
  return;
559
653
  }
@@ -587,7 +681,12 @@ export class ProviderCliAdapter implements CliAdapter {
587
681
  this.lastApprovalResolvedAt = Date.now();
588
682
  }
589
683
  if (this.isWaitingForResponse) {
590
- this.finishResponse();
684
+ if (this.idleTimeout) clearTimeout(this.idleTimeout);
685
+ this.idleTimeout = setTimeout(() => {
686
+ if (this.isWaitingForResponse && this.currentStatus !== 'waiting_approval') {
687
+ this.finishResponse();
688
+ }
689
+ }, this.timeouts.idleFinish);
591
690
  } else if (prevStatus !== 'idle') {
592
691
  this.setStatus('idle', 'script_detect');
593
692
  this.onStatusChange?.();
@@ -596,12 +695,18 @@ export class ProviderCliAdapter implements CliAdapter {
596
695
  }
597
696
 
598
697
  private finishResponse(): void {
698
+ if (this.submitPendingUntil > Date.now()) return;
699
+ if (this.responseSettleIgnoreUntil > Date.now()) return;
599
700
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
600
701
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
601
702
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
703
+ if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
602
704
 
603
705
  this.responseBuffer = '';
604
706
  this.isWaitingForResponse = false;
707
+ this.responseSettleIgnoreUntil = 0;
708
+ this.submitRetryUsed = false;
709
+ this.submitRetryPromptSnippet = '';
605
710
  this.activeModal = null;
606
711
  this.setStatus('idle', 'response_finished');
607
712
  this.onStatusChange?.();
@@ -612,7 +717,11 @@ export class ProviderCliAdapter implements CliAdapter {
612
717
  private runDetectStatus(text: string): string | null {
613
718
  if (!this.cliScripts?.detectStatus) return null;
614
719
  try {
615
- return this.cliScripts.detectStatus({ tail: text.slice(-500) });
720
+ return this.cliScripts.detectStatus({
721
+ tail: text.slice(-500),
722
+ screenText: this.terminalScreen.getText(),
723
+ rawBuffer: this.accumulatedRawBuffer,
724
+ });
616
725
  } catch (e: any) {
617
726
  LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
618
727
  return null;
@@ -720,6 +829,12 @@ export class ProviderCliAdapter implements CliAdapter {
720
829
 
721
830
  async sendMessage(text: string): Promise<void> {
722
831
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
832
+ if (this.startupParseGate) {
833
+ const deadline = Date.now() + 10000;
834
+ while (this.startupParseGate && Date.now() < deadline) {
835
+ await new Promise(resolve => setTimeout(resolve, 50));
836
+ }
837
+ }
723
838
  if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
724
839
  if (this.isWaitingForResponse) return;
725
840
 
@@ -727,14 +842,87 @@ export class ProviderCliAdapter implements CliAdapter {
727
842
  this.structuredMessages.push({ role: 'user', content: text, timestamp: Date.now() });
728
843
  this.isWaitingForResponse = true;
729
844
  this.responseBuffer = '';
845
+ this.submitRetryUsed = false;
846
+ this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
847
+ const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
848
+ if (this.submitRetryTimer) {
849
+ clearTimeout(this.submitRetryTimer);
850
+ this.submitRetryTimer = null;
851
+ }
852
+ const estimatedLines = estimatePromptDisplayLines(text);
853
+ const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
854
+ const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
855
+ const retryDelayMs = Math.max(350, Math.min(1500, Math.max(this.sendDelayMs, submitDelayMs)));
856
+ if (this.settleTimer) {
857
+ clearTimeout(this.settleTimer);
858
+ this.settleTimer = null;
859
+ }
860
+ this.responseEpoch += 1;
861
+ this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
730
862
  this.setStatus('generating', 'sendMessage');
731
863
  this.onStatusChange?.();
864
+ if (submitDelayMs > 0) {
865
+ this.submitPendingUntil = Date.now() + submitDelayMs;
866
+ }
867
+ this.ptyProcess.write(text);
868
+
869
+ const submit = () => {
870
+ if (!this.ptyProcess) return;
871
+ this.submitPendingUntil = 0;
872
+ this.ptyProcess.write(this.sendKey);
873
+ const retrySubmitIfStuck = (attempt: number) => {
874
+ this.submitRetryTimer = null;
875
+ if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return;
876
+ if (this.currentStatus !== 'generating') return;
877
+ if ((this.responseBuffer || '').trim()) return;
878
+ const screenText = this.terminalScreen.getText();
879
+ if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return;
880
+ if (/Esc to interrupt|Do you want to proceed|This command requires approval|Allow Codex to|Approve and run now|Always approve this session|Running…|Running\.\.\./i.test(screenText)) return;
881
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
882
+ LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
883
+ this.ptyProcess.write(this.sendKey);
884
+ if (attempt >= 3) {
885
+ this.submitRetryUsed = true;
886
+ return;
887
+ }
888
+ this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(attempt + 1), retryDelayMs);
889
+ };
890
+ this.submitRetryTimer = setTimeout(() => retrySubmitIfStuck(1), retryDelayMs);
891
+ this.responseTimeout = setTimeout(() => {
892
+ if (this.isWaitingForResponse) this.finishResponse();
893
+ }, this.timeouts.maxResponse);
894
+ };
895
+ const submitStartedAt = Date.now();
896
+ let lastNormalizedScreen = '';
897
+ let lastScreenChangeAt = submitStartedAt;
898
+ const waitForEchoAndSubmit = () => {
899
+ if (!this.ptyProcess) return;
900
+ const now = Date.now();
901
+ const elapsed = now - submitStartedAt;
902
+ const screenText = this.terminalScreen.getText();
903
+ const normalizedScreen = normalizePromptText(screenText);
904
+ if (normalizedScreen !== lastNormalizedScreen) {
905
+ lastNormalizedScreen = normalizedScreen;
906
+ lastScreenChangeAt = now;
907
+ }
908
+ const echoVisible = !normalizedPromptSnippet || promptLikelyVisible(screenText, normalizedPromptSnippet);
909
+
910
+ if (echoVisible) {
911
+ const screenSettled = (now - lastScreenChangeAt) >= 500;
912
+ if (elapsed >= submitDelayMs && screenSettled) {
913
+ submit();
914
+ return;
915
+ }
916
+ }
732
917
 
733
- this.ptyProcess.write(text + '\r');
918
+ if (elapsed >= maxEchoWaitMs) {
919
+ submit();
920
+ return;
921
+ }
734
922
 
735
- this.responseTimeout = setTimeout(() => {
736
- if (this.isWaitingForResponse) this.finishResponse();
737
- }, this.timeouts.maxResponse);
923
+ setTimeout(waitForEchoAndSubmit, 50);
924
+ };
925
+ waitForEchoAndSubmit();
738
926
  }
739
927
 
740
928
  getPartialResponse(): string {
@@ -747,6 +935,7 @@ export class ProviderCliAdapter implements CliAdapter {
747
935
  shutdown(): void {
748
936
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
749
937
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
938
+ if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
750
939
  if (this.ptyProcess) {
751
940
  this.ptyProcess.write('\x03');
752
941
  setTimeout(() => {
@@ -766,6 +955,8 @@ export class ProviderCliAdapter implements CliAdapter {
766
955
  this.structuredMessages = [];
767
956
  this.accumulatedBuffer = '';
768
957
  this.accumulatedRawBuffer = '';
958
+ this.submitRetryUsed = false;
959
+ this.submitRetryPromptSnippet = '';
769
960
  this.terminalScreen.reset();
770
961
  this.onStatusChange?.();
771
962
  }
@@ -778,7 +969,16 @@ export class ProviderCliAdapter implements CliAdapter {
778
969
  }
779
970
 
780
971
  resolveModal(buttonIndex: number): void {
781
- if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
972
+ if (!this.ptyProcess || (this.currentStatus !== 'waiting_approval' && !this.activeModal)) return;
973
+ this.activeModal = null;
974
+ this.lastApprovalResolvedAt = Date.now();
975
+ this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
976
+ if (this.approvalExitTimeout) {
977
+ clearTimeout(this.approvalExitTimeout);
978
+ this.approvalExitTimeout = null;
979
+ }
980
+ this.setStatus('generating', 'approval_resolved');
981
+ this.onStatusChange?.();
782
982
  if (buttonIndex in this.approvalKeys) {
783
983
  this.ptyProcess.write(this.approvalKeys[buttonIndex]);
784
984
  } else {
@@ -810,13 +1010,21 @@ export class ProviderCliAdapter implements CliAdapter {
810
1010
  messages: this.messages.slice(-20),
811
1011
  structuredMessages: this.structuredMessages.slice(-20),
812
1012
  messageCount: this.messages.length,
1013
+ screenText: this.terminalScreen.getText().slice(-4000),
813
1014
  startupBuffer: this.startupBuffer.slice(-4000),
814
1015
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
815
1016
  settledBuffer: this.settledBuffer.slice(-500),
816
1017
  accumulatedBufferLength: this.accumulatedBuffer.length,
1018
+ accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
1019
+ rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
1020
+ responseBuffer: this.responseBuffer.slice(-1000),
817
1021
  isWaitingForResponse: this.isWaitingForResponse,
818
1022
  activeModal: this.activeModal,
819
1023
  lastApprovalResolvedAt: this.lastApprovalResolvedAt,
1024
+ sendDelayMs: this.sendDelayMs,
1025
+ sendKey: this.sendKey,
1026
+ submitPendingUntil: this.submitPendingUntil,
1027
+ responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
820
1028
  resizeSuppressUntil: this.resizeSuppressUntil,
821
1029
  hasCliScripts: this.hasCliScripts(),
822
1030
  scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),