@adhdev/daemon-core 0.8.29 → 0.8.30

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.
@@ -93,6 +93,7 @@ export declare class AcpProviderInstance implements ProviderInstance {
93
93
  private convertToolCallContent;
94
94
  private detectStatusTransition;
95
95
  private pushEvent;
96
+ private appendSystemMessage;
96
97
  private flushEvents;
97
98
  get cliType(): string;
98
99
  get cliName(): string;
@@ -0,0 +1,7 @@
1
+ import type { ProviderModule } from './contracts.js';
2
+ export declare function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[];
3
+ export declare function pickApprovalButton(buttons: string[] | null | undefined, provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): {
4
+ index: number;
5
+ label: string;
6
+ };
7
+ export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string;
@@ -71,6 +71,8 @@ export declare class CliProviderInstance implements ProviderInstance {
71
71
  getAdapter(): ProviderCliAdapter;
72
72
  get cliType(): string;
73
73
  get cliName(): string;
74
+ private shouldAutoApprove;
75
+ private recordAutoApproval;
74
76
  recordApprovalSelection(buttonText: string): void;
75
77
  private formatMarkerTimestamp;
76
78
  private maybeAppendRuntimeRecoveryMessage;
@@ -313,6 +313,8 @@ export interface ProviderModule {
313
313
  resume?: ProviderResumeCapability;
314
314
  /** Session ID probe config — auto-discovers provider session ID from local SQLite DB */
315
315
  sessionProbe?: ProviderSessionProbe;
316
+ /** Approval button priority hints used when auto-approve must pick a positive action */
317
+ approvalPositiveHints?: string[];
316
318
  scripts?: ProviderScripts;
317
319
  vscodeCommands?: {
318
320
  focusPanel?: string;
@@ -65,5 +65,6 @@ export declare class IdeProviderInstance implements ProviderInstance {
65
65
  private getEffectDedupKey;
66
66
  private flushEvents;
67
67
  updateCdp(cdp: InstanceContext['cdp']): void;
68
+ private canAutoApprove;
68
69
  private autoApproveViaScript;
69
70
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.29",
3
+ "version": "0.8.30",
4
4
  "description": "ADHDev local session host core — session registry, protocol, buffers",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.8.29",
3
+ "version": "0.8.30",
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",
@@ -19,6 +19,7 @@ import type { SessionRegistry } from '../sessions/registry.js';
19
19
  import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import type { AgentStreamState } from './types.js';
22
+ import { formatAutoApprovalMessage, pickApprovalButton } from '../providers/approval-utils.js';
22
23
 
23
24
  // ─── Types ───
24
25
 
@@ -189,7 +190,43 @@ export class AgentStreamPoller {
189
190
 
190
191
  try {
191
192
  await agentStreamManager.syncActiveSession(cdp, parentSessionId);
192
- const stream = await agentStreamManager.collectActiveSession(cdp, parentSessionId);
193
+ let stream = await agentStreamManager.collectActiveSession(cdp, parentSessionId);
194
+ if (stream?.status === 'waiting_approval') {
195
+ const autoApprove = providerLoader.getSettings(stream.agentType).autoApprove !== false;
196
+ if (autoApprove && resolvedActiveSessionId) {
197
+ const provider = providerLoader.getMeta(stream.agentType);
198
+ const { label: buttonLabel } = pickApprovalButton(stream.activeModal?.buttons, provider);
199
+ const approved = await agentStreamManager.resolveSessionAction(cdp, resolvedActiveSessionId, 'approve', buttonLabel);
200
+ if (approved) {
201
+ const effectId = [
202
+ 'auto_approval',
203
+ resolvedActiveSessionId,
204
+ String(stream.messages?.length || 0),
205
+ buttonLabel,
206
+ String(stream.activeModal?.message || '').trim(),
207
+ ].join(':');
208
+ stream = {
209
+ ...stream,
210
+ status: 'streaming',
211
+ activeModal: undefined,
212
+ effects: [
213
+ ...(stream.effects || []),
214
+ {
215
+ type: 'message',
216
+ id: effectId,
217
+ persist: true,
218
+ message: {
219
+ role: 'system',
220
+ senderName: 'System',
221
+ kind: 'system',
222
+ content: formatAutoApprovalMessage(stream.activeModal?.message, buttonLabel),
223
+ },
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ }
229
+ }
193
230
  this.deps.onStreamsUpdated?.(ideType, stream ? [stream] : []);
194
231
  } catch { }
195
232
  }
@@ -600,8 +600,10 @@ export class AcpProviderInstance implements ProviderInstance {
600
600
  }
601
601
 
602
602
  // ─── Auto-approve: skip user confirmation ───
603
- if (this.settings.autoApprove) {
604
- this.log.info(`[${this.type}] Auto-approving: ${tc.title || tc.toolCallId}`);
603
+ if (this.settings.autoApprove !== false) {
604
+ const toolTitle = tc.title || tc.toolCallId || 'tool call';
605
+ this.log.info(`[${this.type}] Auto-approving: ${toolTitle}`);
606
+ this.appendSystemMessage(`Auto-approved: ${toolTitle}`);
605
607
  const allowOption = params.options.find(o => o.kind === 'allow_once') || params.options.find(o => o.kind === 'allow_always');
606
608
  if (allowOption) {
607
609
  return { outcome: { outcome: 'selected', optionId: allowOption.optionId } };
@@ -1143,6 +1145,19 @@ export class AcpProviderInstance implements ProviderInstance {
1143
1145
  if (this.events.length > 50) this.events = this.events.slice(-50);
1144
1146
  }
1145
1147
 
1148
+ private appendSystemMessage(content: string, timestamp = Date.now()): void {
1149
+ const normalizedContent = String(content || '').trim();
1150
+ if (!normalizedContent) return;
1151
+ this.messages.push({
1152
+ role: 'system',
1153
+ content: normalizedContent,
1154
+ timestamp,
1155
+ });
1156
+ if (this.messages.length > 200) {
1157
+ this.messages = this.messages.slice(-100);
1158
+ }
1159
+ }
1160
+
1146
1161
  private flushEvents(): ProviderEvent[] {
1147
1162
  const events = [...this.events];
1148
1163
  this.events = [];
@@ -0,0 +1,66 @@
1
+ import type { ProviderModule } from './contracts.js';
2
+
3
+ const DEFAULT_APPROVAL_POSITIVE_HINTS = [
4
+ 'run',
5
+ 'approve',
6
+ 'accept',
7
+ 'allow once',
8
+ 'always allow',
9
+ 'allow',
10
+ 'yes',
11
+ 'proceed',
12
+ 'continue',
13
+ 'confirm',
14
+ 'save',
15
+ 'ok',
16
+ 'trust',
17
+ ];
18
+
19
+ function normalizeApprovalLabel(value: string): string {
20
+ return String(value || '')
21
+ .toLowerCase()
22
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
23
+ .trim();
24
+ }
25
+
26
+ export function getApprovalPositiveHints(provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null): string[] {
27
+ const customHints = Array.isArray(provider?.approvalPositiveHints)
28
+ ? provider.approvalPositiveHints
29
+ .map((hint) => normalizeApprovalLabel(String(hint || '')))
30
+ .filter(Boolean)
31
+ : [];
32
+ return customHints.length > 0 ? customHints : DEFAULT_APPROVAL_POSITIVE_HINTS;
33
+ }
34
+
35
+ export function pickApprovalButton(
36
+ buttons: string[] | null | undefined,
37
+ provider?: Pick<ProviderModule, 'approvalPositiveHints'> | null,
38
+ ): { index: number; label: string } {
39
+ const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
40
+ if (labels.length === 0) {
41
+ return { index: 0, label: 'Approve' };
42
+ }
43
+
44
+ const normalizedButtons = labels.map((label) => normalizeApprovalLabel(label));
45
+ const hints = getApprovalPositiveHints(provider);
46
+
47
+ for (const hint of hints) {
48
+ const exactIndex = normalizedButtons.findIndex((label) => label === hint);
49
+ if (exactIndex >= 0) return { index: exactIndex, label: labels[exactIndex] };
50
+
51
+ const prefixIndex = normalizedButtons.findIndex((label) => label.startsWith(hint));
52
+ if (prefixIndex >= 0) return { index: prefixIndex, label: labels[prefixIndex] };
53
+
54
+ const includeIndex = normalizedButtons.findIndex((label) => label.includes(hint));
55
+ if (includeIndex >= 0) return { index: includeIndex, label: labels[includeIndex] };
56
+ }
57
+
58
+ return { index: 0, label: labels[0] };
59
+ }
60
+
61
+ export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string {
62
+ const lines = [`Auto-approved${buttonLabel ? `: ${buttonLabel}` : ''}`];
63
+ const cleanMessage = String(modalMessage || '').trim();
64
+ if (cleanMessage) lines.push(cleanMessage);
65
+ return lines.join('\n');
66
+ }
@@ -20,6 +20,7 @@ import { ChatHistoryWriter, readChatHistory } from '../config/chat-history.js';
20
20
  import { LOG } from '../logging/logger.js';
21
21
  import type { ChatMessage } from '../types.js';
22
22
  import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
23
+ import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
23
24
 
24
25
  let CachedDatabaseSync: (new (path: string, options?: { readOnly?: boolean }) => {
25
26
  prepare(sql: string): { get(...params: Array<string | number>): unknown };
@@ -249,6 +250,8 @@ export class CliProviderInstance implements ProviderInstance {
249
250
  getState(): ProviderState {
250
251
  const adapterStatus = this.adapter.getStatus();
251
252
  const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
253
+ const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
254
+ const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
252
255
  const parsedProviderSessionId = typeof parsedStatus?.providerSessionId === 'string'
253
256
  ? parsedStatus.providerSessionId.trim()
254
257
  : '';
@@ -297,14 +300,16 @@ export class CliProviderInstance implements ProviderInstance {
297
300
  type: this.type,
298
301
  name: this.provider.name,
299
302
  category: 'cli',
300
- status: adapterStatus.status,
303
+ status: visibleStatus,
301
304
  mode: this.presentationMode,
302
305
  activeChat: {
303
306
  id: `${this.type}_${this.workingDir}`,
304
307
  title: parsedStatus?.title || dirName,
305
- status: parsedStatus?.status || adapterStatus.status,
308
+ status: autoApproveActive && parsedStatus?.status === 'waiting_approval'
309
+ ? 'generating'
310
+ : (parsedStatus?.status || visibleStatus),
306
311
  messages: mergedMessages,
307
- activeModal: parsedStatus?.activeModal ?? adapterStatus.activeModal,
312
+ activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
308
313
  inputContent: '',
309
314
  },
310
315
  workspace: this.workingDir,
@@ -375,7 +380,16 @@ export class CliProviderInstance implements ProviderInstance {
375
380
  const now = Date.now();
376
381
  const adapterStatus = this.adapter.getStatus();
377
382
  const parsedStatus = this.adapter.getScriptParsedStatus?.() || null;
378
- const newStatus = adapterStatus.status;
383
+ const rawStatus = adapterStatus.status;
384
+ const autoApproveActive = rawStatus === 'waiting_approval' && this.shouldAutoApprove();
385
+ if (autoApproveActive) {
386
+ const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(adapterStatus.activeModal?.buttons, this.provider);
387
+ this.recordAutoApproval(adapterStatus.activeModal?.message, buttonLabel, now);
388
+ setTimeout(() => {
389
+ this.adapter.resolveModal(buttonIndex);
390
+ }, 0);
391
+ }
392
+ const newStatus = autoApproveActive ? 'generating' : rawStatus;
379
393
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
380
394
  const chatTitle = `${this.provider.name} · ${dirName}`;
381
395
  const partial = this.adapter.getPartialResponse();
@@ -603,6 +617,18 @@ export class CliProviderInstance implements ProviderInstance {
603
617
  get cliType(): string { return this.type; }
604
618
  get cliName(): string { return this.provider.name; }
605
619
 
620
+ private shouldAutoApprove(): boolean {
621
+ return this.settings.autoApprove !== false;
622
+ }
623
+
624
+ private recordAutoApproval(modalMessage?: string, buttonLabel?: string, now = Date.now()): void {
625
+ this.appendRuntimeSystemMessage(
626
+ formatAutoApprovalMessage(modalMessage, buttonLabel),
627
+ `auto_approval:${now}:${buttonLabel || 'approve'}`,
628
+ now,
629
+ );
630
+ }
631
+
606
632
  recordApprovalSelection(buttonText: string): void {
607
633
  const cleanButton = String(buttonText || '').trim();
608
634
  if (!cleanButton) return;
@@ -253,6 +253,7 @@ export interface ProviderModule {
253
253
  };
254
254
  cleanOutput?: (raw: string, lastUserInput?: string) => string;
255
255
  resume?: ProviderResumeCapability;
256
+ approvalPositiveHints?: string[];
256
257
  scripts?: ProviderScripts;
257
258
  vscodeCommands?: {
258
259
  focusPanel?: string;
@@ -388,8 +388,10 @@ export interface ProviderModule {
388
388
  };
389
389
  cleanOutput?: (raw: string, lastUserInput?: string) => string;
390
390
  resume?: ProviderResumeCapability;
391
- /** Session ID probe config — auto-discovers provider session ID from local SQLite DB */
391
+ /** Session ID probe config — auto-discovers provider session ID from local SQLite DB */
392
392
  sessionProbe?: ProviderSessionProbe;
393
+ /** Approval button priority hints used when auto-approve must pick a positive action */
394
+ approvalPositiveHints?: string[];
393
395
 
394
396
  // ─── CDP scripts (ide/extension category) ───
395
397
  scripts?: ProviderScripts;
@@ -19,6 +19,7 @@ import { ChatHistoryWriter } from '../config/chat-history.js';
19
19
  import { LOG } from '../logging/logger.js';
20
20
  import { extractProviderControlValues, normalizeProviderEffects } from './control-effects.js';
21
21
  import type { ChatMessage } from '../types.js';
22
+ import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
22
23
 
23
24
  export class IdeProviderInstance implements ProviderInstance {
24
25
  readonly type: string;
@@ -103,6 +104,11 @@ export class IdeProviderInstance implements ProviderInstance {
103
104
 
104
105
  getState(): ProviderState {
105
106
  const cdp = this.context?.cdp;
107
+ const autoApproveActive = (
108
+ this.currentStatus === 'waiting_approval'
109
+ || this.cachedChat?.status === 'waiting_approval'
110
+ ) && this.canAutoApprove();
111
+ const visibleStatus = (autoApproveActive ? 'generating' : this.currentStatus) as ProviderState['status'];
106
112
 
107
113
  // Collect extension status
108
114
  const extensionStates: ProviderState[] = [];
@@ -114,13 +120,15 @@ export class IdeProviderInstance implements ProviderInstance {
114
120
  type: this.type,
115
121
  name: this.provider.name,
116
122
  category: 'ide',
117
- status: this.currentStatus as ProviderState['status'],
123
+ status: visibleStatus,
118
124
  activeChat: this.cachedChat ? {
119
125
  id: this.cachedChat.id || 'active_session',
120
126
  title: this.cachedChat.title || this.type,
121
- status: this.cachedChat.status || this.currentStatus,
127
+ status: autoApproveActive && this.cachedChat.status === 'waiting_approval'
128
+ ? 'generating'
129
+ : (this.cachedChat.status || visibleStatus),
122
130
  messages: this.mergeConversationMessages(this.cachedChat.messages || []),
123
- activeModal: this.cachedChat.activeModal || null,
131
+ activeModal: autoApproveActive ? null : (this.cachedChat.activeModal || null),
124
132
  inputContent: this.cachedChat.inputContent || '',
125
133
  } : null,
126
134
  workspace: this.workspace || null,
@@ -370,9 +378,11 @@ export class IdeProviderInstance implements ProviderInstance {
370
378
  if (!chatStatus) return;
371
379
 
372
380
  const agentKey = `${this.type}:native`;
373
- const agentStatus = (chatStatus === 'streaming' || chatStatus === 'generating') ? 'generating'
381
+ const rawAgentStatus = (chatStatus === 'streaming' || chatStatus === 'generating') ? 'generating'
374
382
  : chatStatus === 'waiting_approval' ? 'waiting_approval'
375
383
  : 'idle';
384
+ const autoApproveActive = rawAgentStatus === 'waiting_approval' && this.canAutoApprove();
385
+ const agentStatus = autoApproveActive ? 'generating' : rawAgentStatus;
376
386
  const lastMsg = Array.isArray(chatData?.messages) && chatData.messages.length > 0
377
387
  ? chatData.messages[chatData.messages.length - 1]
378
388
  : null;
@@ -414,7 +424,7 @@ export class IdeProviderInstance implements ProviderInstance {
414
424
  });
415
425
 
416
426
  // Auto-approve: when waiting_approval + settings.autoApprove → auto-click approve via CDP
417
- if (agentStatus === 'waiting_approval' && this.settings.autoApprove && !this.autoApproveBusy) {
427
+ if (rawAgentStatus === 'waiting_approval' && autoApproveActive && !this.autoApproveBusy) {
418
428
  this.autoApproveViaScript(chatData);
419
429
  }
420
430
 
@@ -590,6 +600,12 @@ export class IdeProviderInstance implements ProviderInstance {
590
600
  if (this.context) this.context.cdp = cdp;
591
601
  }
592
602
 
603
+ private canAutoApprove(): boolean {
604
+ return this.settings.autoApprove !== false
605
+ && typeof this.provider.scripts?.resolveAction === 'function'
606
+ && !!this.context?.cdp?.isConnected;
607
+ }
608
+
593
609
  // ─── Auto-approve via CDP script ────────────────────
594
610
 
595
611
  private async autoApproveViaScript(_chatData: any): Promise<void> {
@@ -605,20 +621,16 @@ export class IdeProviderInstance implements ProviderInstance {
605
621
 
606
622
  this.autoApproveBusy = true;
607
623
  try {
608
- let targetButton = _chatData?.activeModal?.buttons?.[0] || 'Run';
609
- const buttons = _chatData?.activeModal?.buttons || [];
610
-
611
- // Prefer buttons like 'Run', 'Approve', 'Yes'
612
- for (const b of buttons) {
613
- const lower = String(b).toLowerCase().replace(/[^\w]/g, '');
614
- if (/^(run|approve|accept|yes|allow|always|proceed|save)/.test(lower)) {
615
- targetButton = b;
616
- break;
617
- }
618
- }
624
+ const { label: targetButton } = pickApprovalButton(_chatData?.activeModal?.buttons, this.provider);
619
625
 
620
626
  const script = scriptFn({ action: 'approve', button: targetButton, buttonText: targetButton });
621
627
  if (!script) return;
628
+ const now = Date.now();
629
+ this.appendRuntimeSystemMessage(
630
+ formatAutoApprovalMessage(_chatData?.activeModal?.message, targetButton),
631
+ `auto_approval:${now}:${targetButton}`,
632
+ now,
633
+ );
622
634
 
623
635
  LOG.info('IdeInstance', `[IdeInstance:${this.type}] autoApprove: executing resolveAction for "${targetButton}"`);
624
636
  let rawResult = await cdp.evaluate(script, 10000);
@@ -641,13 +653,6 @@ export class IdeProviderInstance implements ProviderInstance {
641
653
  LOG.warn('IdeInstance', `[IdeInstance:${this.type}] autoApprove: cdp.send() not available for coordinate click`);
642
654
  }
643
655
  }
644
-
645
- this.pushEvent({
646
- event: 'agent:auto_approved',
647
- chatTitle: _chatData?.title || this.provider.name,
648
- timestamp: Date.now(),
649
- ideType: this.type,
650
- });
651
656
  } catch (e: any) {
652
657
  LOG.warn('IdeInstance', `[IdeInstance:${this.type}] autoApprove error: ${e?.message}`);
653
658
  } finally {
@@ -994,7 +994,11 @@ export class ProviderLoader {
994
994
  */
995
995
  getSettingValue(type: string, key: string): any {
996
996
  const schemaDef = this.getSettingsSchema(type)[key];
997
- const defaultVal = schemaDef ? (schemaDef as any).default : undefined;
997
+ const defaultVal = schemaDef
998
+ ? (key === 'autoApprove' && (schemaDef as any).type === 'boolean'
999
+ ? true
1000
+ : (schemaDef as any).default)
1001
+ : undefined;
998
1002
 
999
1003
  // Load user-saved value
1000
1004
  try {
@@ -1064,15 +1068,35 @@ export class ProviderLoader {
1064
1068
  private getSettingsSchema(type: string): Record<string, ProviderSettingDef> {
1065
1069
  const provider = this.providers.get(type);
1066
1070
  if (!provider) return {};
1067
- return {
1071
+ const result = {
1068
1072
  ...this.getSyntheticSettings(type, provider),
1069
1073
  ...(provider.settings || {}),
1070
1074
  };
1075
+ if (result.autoApprove?.type === 'boolean') {
1076
+ result.autoApprove = {
1077
+ ...result.autoApprove,
1078
+ default: true,
1079
+ public: true,
1080
+ label: result.autoApprove.label || 'Auto Approve',
1081
+ description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
1082
+ };
1083
+ }
1084
+ return result;
1071
1085
  }
1072
1086
 
1073
1087
  private getSyntheticSettings(type: string, provider: ProviderModule): Record<string, ProviderSettingDef> {
1074
1088
  const result: Record<string, ProviderSettingDef> = {};
1075
1089
 
1090
+ if (!provider.settings?.autoApprove) {
1091
+ result.autoApprove = {
1092
+ type: 'boolean',
1093
+ default: true,
1094
+ public: true,
1095
+ label: 'Auto Approve',
1096
+ description: 'Automatically approve actionable prompts without sending approval alerts.',
1097
+ };
1098
+ }
1099
+
1076
1100
  if ((provider.category === 'cli' || provider.category === 'acp') && provider.spawn?.command && !provider.settings?.executablePath) {
1077
1101
  result.executablePath = {
1078
1102
  type: 'string',