@adhdev/daemon-core 0.5.5 → 0.5.6

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.5.5",
3
+ "version": "0.5.6",
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",
@@ -185,29 +185,30 @@ function coercePatternArray(raw: unknown, fallbacks: RegExp[]): RegExp[] {
185
185
  /** Defaults tuned for Claude Code / similar agent CLIs when provider.json patterns are empty. */
186
186
  const FALLBACK_PROMPT: RegExp[] = [
187
187
  /Type your message/i,
188
- />\s*$/,
188
+ /^>\s*$/m, // '>' alone on its own line
189
189
  /[›➤]\s*$/,
190
190
  /for shortcuts/i,
191
191
  /\?\s*for help/i,
192
192
  /Press enter/i,
193
- /^[\s\u2500-\u257f]*>\s*$/m,
193
+ // NOTE: removed /^[\s\u2500-\u257f]*>\s*$/m — the box-drawing char range is too wide and
194
+ // can match dialog-clearing ANSI output, causing false prompt detection in approval state.
194
195
  ];
195
196
 
196
197
  const FALLBACK_GENERATING: RegExp[] = [
197
- /thinking/i,
198
- /writing/i,
199
- /Claude is/i,
200
- /Opus|Sonnet|Haiku/i,
201
- /[\u2800-\u28ff]/, // Braille spinner blocks
198
+ /[\u2800-\u28ff]/, // Braille spinner blocks (universal TUI)
199
+ /esc to (cancel|interrupt|stop)/i, // Common TUI generation status line
200
+ /generating\.\.\./i,
201
+ /Claude is (?:thinking|processing|working)/i, // Specific Claude Code status
202
202
  ];
203
203
 
204
204
  const FALLBACK_APPROVAL: RegExp[] = [
205
- /approve/i,
206
- /Allow( once)?/i,
205
+ /Allow\s+once/i,
206
+ /Always\s+allow/i,
207
207
  /\(y\/n\)/i,
208
208
  /\[Y\/n\]/i,
209
- /continue\?/i,
210
- /Run this command/i,
209
+ /Run\s+this\s+command/i,
210
+ // NOTE: removed /Do you want to (?:run|execute|allow)/i — too broad, matches AI explanation
211
+ // text like "Do you want to allow this feature?" causing false approval notifications.
211
212
  ];
212
213
 
213
214
  function defaultCleanOutput(raw: string, _lastUserInput?: string): string {
@@ -260,6 +261,13 @@ export class ProviderCliAdapter implements CliAdapter {
260
261
  // Approval cooldown
261
262
  private lastApprovalResolvedAt: number = 0;
262
263
 
264
+ // Approval state machine
265
+ private approvalTransitionBuffer: string = '';
266
+ private approvalExitTimeout: NodeJS.Timeout | null = null;
267
+
268
+ // Resize redraw suppression
269
+ private resizeSuppressUntil: number = 0;
270
+
263
271
  // Resolved timeouts (provider defaults + overrides)
264
272
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
265
273
 
@@ -394,6 +402,9 @@ export class ProviderCliAdapter implements CliAdapter {
394
402
  // ─── Output state machine ────────────────────────────
395
403
 
396
404
  private handleOutput(rawData: string): void {
405
+ // Suppress output processing briefly after resize to avoid false triggers from screen redraws
406
+ if (Date.now() < this.resizeSuppressUntil) return;
407
+
397
408
  const cleanData = stripAnsi(rawData);
398
409
  const { patterns } = this.provider;
399
410
 
@@ -439,32 +450,57 @@ export class ProviderCliAdapter implements CliAdapter {
439
450
  if (hasApproval && this.currentStatus !== 'waiting_approval') {
440
451
  if (this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown) return;
441
452
 
453
+ // Capture context before clearing (recentOutputBuffer still has content here)
454
+ const ctxLines = this.recentOutputBuffer.split('\n')
455
+ .map(l => l.trim())
456
+ .filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
442
457
  this.isWaitingForResponse = true;
443
458
  this.currentStatus = 'waiting_approval';
444
459
  this.recentOutputBuffer = '';
445
- const ctxLines = cleanData.split('\n').map(l => l.trim()).filter(l => l && !/^[─═╭╮╰╯│]+$/.test(l));
460
+ this.approvalTransitionBuffer = '';
446
461
  this.activeModal = {
447
462
  message: ctxLines.slice(-5).join(' ').slice(0, 200) || 'Approval required',
448
463
  buttons: ['Allow once', 'Always allow', 'Deny'],
449
464
  };
450
465
  if (this.idleTimeout) clearTimeout(this.idleTimeout);
466
+ // Safety timeout — if stuck in waiting_approval, auto-exit after 60s
467
+ if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
468
+ this.approvalExitTimeout = setTimeout(() => {
469
+ if (this.currentStatus === 'waiting_approval') {
470
+ LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-exiting waiting_approval`);
471
+ this.activeModal = null;
472
+ this.lastApprovalResolvedAt = Date.now();
473
+ this.recentOutputBuffer = '';
474
+ this.approvalTransitionBuffer = '';
475
+ this.approvalExitTimeout = null;
476
+ this.currentStatus = this.isWaitingForResponse ? 'generating' : 'idle';
477
+ this.onStatusChange?.();
478
+ }
479
+ }, 60000);
451
480
  this.onStatusChange?.();
452
481
  return;
453
482
  }
454
483
 
455
484
  // ─── Phase 3: Approval release
485
+ // Accumulate chunks into approvalTransitionBuffer — the approval dialog clears via ANSI
486
+ // sequences that strip to nothing, so we can't rely on a single cleanData chunk matching.
456
487
  if (this.currentStatus === 'waiting_approval') {
457
- const genResume = patterns.generating.some(p => p.test(cleanData));
458
- const promptResume = patterns.prompt.some(p => p.test(cleanData));
488
+ this.approvalTransitionBuffer = (this.approvalTransitionBuffer + cleanData).slice(-500);
489
+ const genResume = patterns.generating.some(p => p.test(this.approvalTransitionBuffer));
490
+ const promptResume = patterns.prompt.some(p => p.test(this.approvalTransitionBuffer));
459
491
  if (genResume) {
492
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
460
493
  this.currentStatus = 'generating';
461
494
  this.activeModal = null;
462
495
  this.recentOutputBuffer = '';
496
+ this.approvalTransitionBuffer = '';
463
497
  this.lastApprovalResolvedAt = Date.now();
464
498
  this.onStatusChange?.();
465
499
  } else if (promptResume) {
500
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
466
501
  this.activeModal = null;
467
502
  this.recentOutputBuffer = '';
503
+ this.approvalTransitionBuffer = '';
468
504
  this.lastApprovalResolvedAt = Date.now();
469
505
  this.finishResponse();
470
506
  }
@@ -497,7 +533,11 @@ export class ProviderCliAdapter implements CliAdapter {
497
533
  }
498
534
 
499
535
  // Prompt → response complete
500
- if (patterns.prompt.some(p => p.test(this.responseBuffer))) {
536
+ // Only check the LAST 2 lines of cleanData — the prompt appears at the very end of the
537
+ // output stream. Checking the full chunk causes false positives when response content
538
+ // has '>' in code blocks, shell examples, or mid-response lines.
539
+ const trailingLines = cleanData.split('\n').slice(-2).join('\n');
540
+ if (patterns.prompt.some(p => p.test(trailingLines))) {
501
541
  this.finishResponse();
502
542
  } else {
503
543
  this.idleTimeout = setTimeout(() => {
@@ -513,6 +553,7 @@ export class ProviderCliAdapter implements CliAdapter {
513
553
  private finishResponse(): void {
514
554
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
515
555
  if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
556
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
516
557
 
517
558
  const lastUserText = this.messages.filter(m => m.role === 'user').pop()?.content;
518
559
  let response = this.provider.cleanOutput(this.responseBuffer, lastUserText);
@@ -576,6 +617,7 @@ export class ProviderCliAdapter implements CliAdapter {
576
617
  cancel(): void { this.shutdown(); }
577
618
 
578
619
  shutdown(): void {
620
+ if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
579
621
  if (this.ptyProcess) {
580
622
  this.ptyProcess.write('\x03');
581
623
  setTimeout(() => {
@@ -600,9 +642,26 @@ export class ProviderCliAdapter implements CliAdapter {
600
642
  this.ptyProcess?.write(data);
601
643
  }
602
644
 
645
+ /**
646
+ * Resolve an approval modal by navigating to the button at `buttonIndex` and pressing Enter.
647
+ * Index 0 = first option (already selected by default — just Enter).
648
+ * Index N = press Arrow Down N times, then Enter.
649
+ */
650
+ resolveModal(buttonIndex: number): void {
651
+ if (!this.ptyProcess || this.currentStatus !== 'waiting_approval') return;
652
+ const DOWN = '\x1B[B'; // Arrow Down
653
+ const keys = DOWN.repeat(Math.max(0, buttonIndex)) + '\r';
654
+ this.ptyProcess.write(keys);
655
+ }
656
+
603
657
  resize(cols: number, rows: number): void {
604
658
  if (this.ptyProcess) {
605
- try { this.ptyProcess.resize(cols, rows); } catch { }
659
+ try {
660
+ this.ptyProcess.resize(cols, rows);
661
+ // Suppress output for 300ms after resize — PTY redraws the screen and
662
+ // the redrawn content (spinners, status text) would falsely trigger generating detection
663
+ this.resizeSuppressUntil = Date.now() + 300;
664
+ } catch { }
606
665
  }
607
666
  }
608
667
  }
@@ -601,6 +601,42 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
601
601
 
602
602
  LOG.info('Command', `[resolveAction] action=${action} button="${button}" provider=${provider?.type}`);
603
603
 
604
+ // 0. CLI / ACP category: navigate approval dialog via PTY arrow keys + Enter
605
+ if (provider?.category === 'cli') {
606
+ const adapter = h.getCliAdapter(provider.type);
607
+ if (!adapter) return { success: false, error: 'CLI adapter not running' };
608
+ const status = (adapter as any).getStatus?.();
609
+ if (status?.status !== 'waiting_approval') {
610
+ return { success: false, error: 'Not in approval state' };
611
+ }
612
+ const buttons: string[] = status.activeModal?.buttons || ['Allow once', 'Always allow', 'Deny'];
613
+ // Resolve button index: explicit buttonIndex arg → button text match → action fallback
614
+ let buttonIndex = typeof args?.buttonIndex === 'number' ? args.buttonIndex : -1;
615
+ if (buttonIndex < 0) {
616
+ const btnLower = button.toLowerCase();
617
+ buttonIndex = buttons.findIndex(b => b.toLowerCase().includes(btnLower));
618
+ }
619
+ if (buttonIndex < 0) {
620
+ if (action === 'reject' || action === 'deny') {
621
+ buttonIndex = buttons.findIndex(b => /deny|reject|no/i.test(b));
622
+ if (buttonIndex < 0) buttonIndex = buttons.length - 1;
623
+ } else if (action === 'always' || /always/i.test(button)) {
624
+ buttonIndex = buttons.findIndex(b => /always/i.test(b));
625
+ if (buttonIndex < 0) buttonIndex = 1;
626
+ } else {
627
+ buttonIndex = 0; // approve → first option (default selected)
628
+ }
629
+ }
630
+ if (typeof (adapter as any).resolveModal === 'function') {
631
+ (adapter as any).resolveModal(buttonIndex);
632
+ } else {
633
+ const keys = '\x1B[B'.repeat(Math.max(0, buttonIndex)) + '\r';
634
+ (adapter as any).writeRaw?.(keys);
635
+ }
636
+ LOG.info('Command', `[resolveAction] CLI PTY → buttonIndex=${buttonIndex} "${buttons[buttonIndex] ?? '?'}"`);
637
+ return { success: true, buttonIndex, button: buttons[buttonIndex] ?? button };
638
+ }
639
+
604
640
  // 1. Extension: via AgentStreamManager
605
641
  if (provider?.category === 'extension' && h.agentStream && h.getCdp()) {
606
642
  const ok = await h.agentStream.resolveAgentAction(
@@ -13,6 +13,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
13
13
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
14
14
  import { StatusMonitor } from './status-monitor.js';
15
15
  import { ChatHistoryWriter } from '../config/chat-history.js';
16
+ import { LOG } from '../logging/logger.js';
16
17
 
17
18
  export class CliProviderInstance implements ProviderInstance {
18
19
  readonly type: string;
@@ -156,18 +157,22 @@ export class CliProviderInstance implements ProviderInstance {
156
157
  const chatTitle = `${this.provider.name} · ${dirName}`;
157
158
 
158
159
  if (newStatus !== this.lastStatus) {
160
+ LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
159
161
  if (this.lastStatus === 'idle' && newStatus === 'generating') {
160
162
  this.generatingStartedAt = now;
161
163
  this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now });
162
164
  } else if (newStatus === 'waiting_approval') {
163
165
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
166
+ const modal = adapterStatus.activeModal;
167
+ LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
164
168
  this.pushEvent({
165
169
  event: 'agent:waiting_approval', chatTitle, timestamp: now,
166
- modalMessage: adapterStatus.activeModal?.message,
167
- modalButtons: adapterStatus.activeModal?.buttons,
170
+ modalMessage: modal?.message,
171
+ modalButtons: modal?.buttons,
168
172
  });
169
173
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
170
174
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
175
+ LOG.info('CLI', `[${this.type}] completed in ${duration}s`);
171
176
  this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
172
177
  this.generatingStartedAt = 0;
173
178
  } else if (newStatus === 'stopped') {