@adhdev/daemon-core 0.8.29 → 0.8.31

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.
Files changed (63) hide show
  1. package/dist/agent-stream/manager.d.ts +1 -0
  2. package/dist/agent-stream/provider-adapter.d.ts +1 -0
  3. package/dist/agent-stream/types.d.ts +3 -0
  4. package/dist/boot/daemon-lifecycle.d.ts +2 -1
  5. package/dist/cdp/manager.d.ts +2 -0
  6. package/dist/cli-adapter-types.d.ts +34 -5
  7. package/dist/cli-adapters/provider-cli-adapter.d.ts +4 -158
  8. package/dist/cli-adapters/provider-cli-config.d.ts +30 -0
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +42 -0
  10. package/dist/cli-adapters/provider-cli-runtime.d.ts +29 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +158 -0
  12. package/dist/commands/handler.d.ts +4 -3
  13. package/dist/config/config.d.ts +4 -3
  14. package/dist/index.js +1033 -621
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +1035 -624
  17. package/dist/index.mjs.map +1 -1
  18. package/dist/providers/acp-provider-instance.d.ts +1 -0
  19. package/dist/providers/approval-utils.d.ts +7 -0
  20. package/dist/providers/cli-provider-instance.d.ts +2 -0
  21. package/dist/providers/contracts.d.ts +12 -1
  22. package/dist/providers/ide-provider-instance.d.ts +1 -0
  23. package/dist/providers/provider-loader.d.ts +3 -0
  24. package/dist/status/reporter.d.ts +2 -3
  25. package/dist/status/snapshot.d.ts +2 -1
  26. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  27. package/package.json +3 -1
  28. package/src/agent-stream/manager.ts +8 -2
  29. package/src/agent-stream/poller.ts +57 -6
  30. package/src/agent-stream/provider-adapter.ts +11 -7
  31. package/src/agent-stream/types.ts +3 -0
  32. package/src/boot/daemon-lifecycle.ts +7 -6
  33. package/src/cdp/initializer.ts +2 -2
  34. package/src/cdp/manager.ts +5 -0
  35. package/src/cdp/setup.ts +1 -1
  36. package/src/cli-adapter-types.ts +37 -5
  37. package/src/cli-adapters/provider-cli-adapter.ts +212 -795
  38. package/src/cli-adapters/provider-cli-config.ts +66 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +202 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +142 -0
  41. package/src/cli-adapters/provider-cli-shared.ts +439 -0
  42. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +1 -1
  43. package/src/commands/cdp-commands.ts +6 -1
  44. package/src/commands/chat-commands.ts +45 -29
  45. package/src/commands/cli-manager.ts +28 -9
  46. package/src/commands/handler.ts +14 -10
  47. package/src/commands/router.ts +23 -10
  48. package/src/commands/stream-commands.ts +11 -5
  49. package/src/config/config.ts +4 -10
  50. package/src/daemon/dev-auto-implement.ts +22 -18
  51. package/src/daemon/dev-cli-debug.ts +59 -16
  52. package/src/daemon/dev-server.ts +67 -43
  53. package/src/providers/acp-provider-instance.ts +18 -3
  54. package/src/providers/approval-utils.ts +66 -0
  55. package/src/providers/cli-provider-instance.ts +32 -6
  56. package/src/providers/contracts.d.ts +1 -0
  57. package/src/providers/contracts.ts +15 -2
  58. package/src/providers/extension-provider-instance.ts +1 -1
  59. package/src/providers/ide-provider-instance.ts +67 -41
  60. package/src/providers/provider-loader.ts +110 -55
  61. package/src/providers/version-archive.ts +23 -5
  62. package/src/status/reporter.ts +18 -14
  63. package/src/status/snapshot.ts +5 -4
@@ -19,6 +19,25 @@ 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';
23
+
24
+ type ReadChatModal = {
25
+ message?: string;
26
+ buttons?: string[];
27
+ width?: number;
28
+ height?: number;
29
+ };
30
+
31
+ type ReadChatMessage = ChatMessage & { content: string };
32
+
33
+ type ReadChatPayload = {
34
+ activeModal?: ReadChatModal;
35
+ messages?: ReadChatMessage[];
36
+ controlValues?: Record<string, string | number | boolean>;
37
+ status?: string;
38
+ title?: string;
39
+ [key: string]: unknown;
40
+ };
22
41
 
23
42
  export class IdeProviderInstance implements ProviderInstance {
24
43
  readonly type: string;
@@ -103,6 +122,11 @@ export class IdeProviderInstance implements ProviderInstance {
103
122
 
104
123
  getState(): ProviderState {
105
124
  const cdp = this.context?.cdp;
125
+ const autoApproveActive = (
126
+ this.currentStatus === 'waiting_approval'
127
+ || this.cachedChat?.status === 'waiting_approval'
128
+ ) && this.canAutoApprove();
129
+ const visibleStatus = (autoApproveActive ? 'generating' : this.currentStatus) as ProviderState['status'];
106
130
 
107
131
  // Collect extension status
108
132
  const extensionStates: ProviderState[] = [];
@@ -114,13 +138,15 @@ export class IdeProviderInstance implements ProviderInstance {
114
138
  type: this.type,
115
139
  name: this.provider.name,
116
140
  category: 'ide',
117
- status: this.currentStatus as ProviderState['status'],
141
+ status: visibleStatus,
118
142
  activeChat: this.cachedChat ? {
119
143
  id: this.cachedChat.id || 'active_session',
120
144
  title: this.cachedChat.title || this.type,
121
- status: this.cachedChat.status || this.currentStatus,
145
+ status: autoApproveActive && this.cachedChat.status === 'waiting_approval'
146
+ ? 'generating'
147
+ : (this.cachedChat.status || visibleStatus),
122
148
  messages: this.mergeConversationMessages(this.cachedChat.messages || []),
123
- activeModal: this.cachedChat.activeModal || null,
149
+ activeModal: autoApproveActive ? null : (this.cachedChat.activeModal || null),
124
150
  inputContent: this.cachedChat.inputContent || '',
125
151
  } : null,
126
152
  workspace: this.workspace || null,
@@ -130,7 +156,7 @@ export class IdeProviderInstance implements ProviderInstance {
130
156
  currentPlan: this.cachedChat?.mode || undefined,
131
157
  currentAutoApprove: this.cachedChat?.autoApprove || undefined,
132
158
  controlValues: this.cachedChat?.controlValues || undefined,
133
- providerControls: this.provider.controls as any,
159
+ providerControls: this.provider.controls,
134
160
  instanceId: this.instanceId,
135
161
  lastUpdated: Date.now(),
136
162
  settings: this.settings,
@@ -253,14 +279,14 @@ export class IdeProviderInstance implements ProviderInstance {
253
279
  if (!cdp?.isConnected) return;
254
280
 
255
281
  try {
256
- let raw: any = null;
282
+ let raw: unknown = null;
257
283
 
258
284
  // path 1: webview iframe internal (Kiro, PearAI etc)
259
- const webviewFn = (this.provider.scripts as any)?.webviewReadChat;
285
+ const webviewFn = this.provider.scripts?.webviewReadChat;
260
286
  if (typeof webviewFn === 'function' && cdp.evaluateInWebviewFrame) {
261
287
  const webviewScript = webviewFn();
262
288
  if (webviewScript) {
263
- const matchText = (this.provider as any).webviewMatchText;
289
+ const matchText = this.provider.webviewMatchText;
264
290
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
265
291
  const webviewRaw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
266
292
  if (webviewRaw) {
@@ -273,16 +299,17 @@ export class IdeProviderInstance implements ProviderInstance {
273
299
  if (!raw) {
274
300
  const readChatScript = this.getReadChatScript();
275
301
  if (!readChatScript) return;
276
- raw = await cdp.evaluate(readChatScript, 30000) as any;
302
+ raw = await cdp.evaluate(readChatScript, 30000);
277
303
  if (typeof raw === 'string') {
278
304
  try { raw = JSON.parse(raw); } catch { return; }
279
305
  }
280
306
  }
281
307
 
282
308
  if (!raw || typeof raw !== 'object') return;
309
+ const chat = raw as ReadChatPayload;
283
310
 
284
311
  // Modal filter
285
- let { activeModal } = raw;
312
+ let { activeModal } = chat;
286
313
  if (activeModal) {
287
314
  const w = activeModal.width ?? Infinity;
288
315
  const h = activeModal.height ?? Infinity;
@@ -304,33 +331,35 @@ export class IdeProviderInstance implements ProviderInstance {
304
331
  if (pm.receivedAt) prevByHash.set(h, pm.receivedAt);
305
332
  }
306
333
  const now = Date.now();
307
- for (const msg of (raw.messages || [])) {
334
+ const messages = chat.messages || [];
335
+ for (const msg of messages) {
308
336
  const h = `${msg.role}:${(msg.content || '').slice(0, 100)}`;
309
337
  msg.receivedAt = prevByHash.get(h) || now;
310
338
  }
311
339
 
312
340
  // Filter messages by provider settings (showThinking, showToolCalls, showTerminal)
313
- if (raw.messages?.length > 0) {
341
+ if (messages.length > 0) {
314
342
  const hiddenKinds = new Set<string>();
315
343
  if (this.settings.showThinking === false) hiddenKinds.add('thought');
316
344
  if (this.settings.showToolCalls === false) hiddenKinds.add('tool');
317
345
  if (this.settings.showTerminal === false) hiddenKinds.add('terminal');
318
346
  if (hiddenKinds.size > 0) {
319
- raw.messages = raw.messages.filter((m: any) => !hiddenKinds.has(m.kind));
347
+ chat.messages = messages.filter((m) => !hiddenKinds.has(m.kind || ''));
320
348
  }
321
349
  }
322
350
 
323
- const controlValues = extractProviderControlValues(this.provider.controls, raw);
324
- if (controlValues) raw.controlValues = controlValues;
351
+ const controlValues = extractProviderControlValues(this.provider.controls, chat);
352
+ if (controlValues) chat.controlValues = controlValues;
325
353
 
326
- this.cachedChat = { ...raw, activeModal };
327
- this.detectAgentTransitions(raw, now);
354
+ this.cachedChat = { ...chat, activeModal };
355
+ this.detectAgentTransitions(chat, now);
328
356
 
329
357
  // Save history (new messageonly append)
330
358
  // Exclude last incomplete assistant message during generating status
331
- if (raw.messages?.length > 0) {
332
- let toSave = raw.messages;
333
- if (raw.status === 'generating' || raw.status === 'long_generating') {
359
+ const persistedMessages = chat.messages || messages;
360
+ if (persistedMessages.length > 0) {
361
+ let toSave = persistedMessages;
362
+ if (chat.status === 'generating' || chat.status === 'long_generating') {
334
363
  // Find and exclude last assistant message
335
364
  const lastIdx = toSave.length - 1;
336
365
  if (lastIdx >= 0 && toSave[lastIdx].role === 'assistant') {
@@ -341,7 +370,7 @@ export class IdeProviderInstance implements ProviderInstance {
341
370
  this.historyWriter.appendNewMessages(
342
371
  this.type,
343
372
  toSave,
344
- raw.title,
373
+ chat.title,
345
374
  this.instanceId,
346
375
  );
347
376
  }
@@ -360,7 +389,7 @@ export class IdeProviderInstance implements ProviderInstance {
360
389
  private getReadChatScript(): string | null {
361
390
  const scripts = this.provider.scripts;
362
391
  if (!scripts?.readChat) return null;
363
- return typeof scripts.readChat === 'function' ? scripts.readChat({}) : scripts.readChat as any;
392
+ return scripts.readChat({});
364
393
  }
365
394
 
366
395
  // ─── status transition detect ─────────────────────────────
@@ -370,9 +399,11 @@ export class IdeProviderInstance implements ProviderInstance {
370
399
  if (!chatStatus) return;
371
400
 
372
401
  const agentKey = `${this.type}:native`;
373
- const agentStatus = (chatStatus === 'streaming' || chatStatus === 'generating') ? 'generating'
402
+ const rawAgentStatus = (chatStatus === 'streaming' || chatStatus === 'generating') ? 'generating'
374
403
  : chatStatus === 'waiting_approval' ? 'waiting_approval'
375
404
  : 'idle';
405
+ const autoApproveActive = rawAgentStatus === 'waiting_approval' && this.canAutoApprove();
406
+ const agentStatus = autoApproveActive ? 'generating' : rawAgentStatus;
376
407
  const lastMsg = Array.isArray(chatData?.messages) && chatData.messages.length > 0
377
408
  ? chatData.messages[chatData.messages.length - 1]
378
409
  : null;
@@ -414,7 +445,7 @@ export class IdeProviderInstance implements ProviderInstance {
414
445
  });
415
446
 
416
447
  // Auto-approve: when waiting_approval + settings.autoApprove → auto-click approve via CDP
417
- if (agentStatus === 'waiting_approval' && this.settings.autoApprove && !this.autoApproveBusy) {
448
+ if (rawAgentStatus === 'waiting_approval' && autoApproveActive && !this.autoApproveBusy) {
418
449
  this.autoApproveViaScript(chatData);
419
450
  }
420
451
 
@@ -590,6 +621,12 @@ export class IdeProviderInstance implements ProviderInstance {
590
621
  if (this.context) this.context.cdp = cdp;
591
622
  }
592
623
 
624
+ private canAutoApprove(): boolean {
625
+ return this.settings.autoApprove !== false
626
+ && typeof this.provider.scripts?.resolveAction === 'function'
627
+ && !!this.context?.cdp?.isConnected;
628
+ }
629
+
593
630
  // ─── Auto-approve via CDP script ────────────────────
594
631
 
595
632
  private async autoApproveViaScript(_chatData: any): Promise<void> {
@@ -605,20 +642,16 @@ export class IdeProviderInstance implements ProviderInstance {
605
642
 
606
643
  this.autoApproveBusy = true;
607
644
  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
- }
645
+ const { label: targetButton } = pickApprovalButton(_chatData?.activeModal?.buttons, this.provider);
619
646
 
620
647
  const script = scriptFn({ action: 'approve', button: targetButton, buttonText: targetButton });
621
648
  if (!script) return;
649
+ const now = Date.now();
650
+ this.appendRuntimeSystemMessage(
651
+ formatAutoApprovalMessage(_chatData?.activeModal?.message, targetButton),
652
+ `auto_approval:${now}:${targetButton}`,
653
+ now,
654
+ );
622
655
 
623
656
  LOG.info('IdeInstance', `[IdeInstance:${this.type}] autoApprove: executing resolveAction for "${targetButton}"`);
624
657
  let rawResult = await cdp.evaluate(script, 10000);
@@ -641,13 +674,6 @@ export class IdeProviderInstance implements ProviderInstance {
641
674
  LOG.warn('IdeInstance', `[IdeInstance:${this.type}] autoApprove: cdp.send() not available for coordinate click`);
642
675
  }
643
676
  }
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
677
  } catch (e: any) {
652
678
  LOG.warn('IdeInstance', `[IdeInstance:${this.type}] autoApprove error: ${e?.message}`);
653
679
  } finally {
@@ -21,6 +21,7 @@ import { registerIDEDefinition } from '../detection/ide-detector.js';
21
21
  import { LOG } from '../logging/logger.js';
22
22
  import { VersionArchive } from './version-archive.js';
23
23
  import type {
24
+ ProviderCompatibilityEntry,
24
25
  ProviderModule,
25
26
  ProviderCategory,
26
27
  ProviderScripts,
@@ -43,7 +44,7 @@ export class ProviderLoader {
43
44
  private watchers: any[] = [];
44
45
  private logFn: (msg: string) => void;
45
46
  private versionArchive: VersionArchive | null = null;
46
- private scriptsCache = new Map<string, Record<string, any>>();
47
+ private scriptsCache = new Map<string, Partial<ProviderScripts>>();
47
48
 
48
49
  /** Inject VersionArchive so resolve() can auto-detect installed versions */
49
50
  setVersionArchive(archive: VersionArchive): void {
@@ -240,10 +241,7 @@ export class ProviderLoader {
240
241
  const result: { id: string; displayName: string; icon: string; command: string; category: string; versionCommand?: string }[] = [];
241
242
  for (const p of this.providers.values()) {
242
243
  if ((p.category === 'cli' || p.category === 'acp') && p.spawn?.command) {
243
- const verCmdConfig = (p as any).versionCommand;
244
- const versionCommand = typeof verCmdConfig === 'object' && verCmdConfig !== null
245
- ? verCmdConfig[process.platform]
246
- : verCmdConfig;
244
+ const versionCommand = this.getPlatformVersionCommand(p.versionCommand);
247
245
  const command = this.getSpawnCommand(p.type, p.spawn.command);
248
246
  result.push({
249
247
  id: p.type,
@@ -304,8 +302,8 @@ export class ProviderLoader {
304
302
  * that runtime attach/remove uses.
305
303
  */
306
304
  getIdeExtensionEnabledState(ideType: string, extensionType: string): boolean {
307
- const { loadConfig } = require('../config/config.js');
308
- const config = loadConfig();
305
+ const config = this.readConfig();
306
+ if (!config) return false;
309
307
  const baseIdeType = ideType.split('_')[0];
310
308
  const val = config.ideSettings?.[baseIdeType]?.extensions?.[extensionType]?.enabled;
311
309
  return val === true;
@@ -315,15 +313,16 @@ export class ProviderLoader {
315
313
  * Save IDE extension enabled setting
316
314
  */
317
315
  setIdeExtensionEnabled(ideType: string, extensionType: string, enabled: boolean): boolean {
316
+ const config = this.readConfig();
317
+ if (!config) return false;
318
+
318
319
  try {
319
- const { loadConfig, saveConfig } = require('../config/config.js');
320
- const config = loadConfig();
321
320
  const baseIdeType = ideType.split('_')[0];
322
321
  if (!config.ideSettings) config.ideSettings = {};
323
322
  if (!config.ideSettings[baseIdeType]) config.ideSettings[baseIdeType] = {};
324
323
  if (!config.ideSettings[baseIdeType].extensions) config.ideSettings[baseIdeType].extensions = {};
325
324
  config.ideSettings[baseIdeType].extensions[extensionType] = { enabled };
326
- saveConfig(config);
325
+ this.writeConfig(config);
327
326
  this.log(`IDE extension setting: ${ideType}.${extensionType}.enabled = ${enabled}`);
328
327
  return true;
329
328
  } catch (e) {
@@ -545,8 +544,8 @@ export class ProviderLoader {
545
544
  resolved._resolvedVersion = currentVersion;
546
545
 
547
546
  // --- New format: compatibility array ---
548
- if (Array.isArray((base as any).compatibility)) {
549
- const compat = (base as any).compatibility as { ideVersion: string; scriptDir: string }[];
547
+ if (base.compatibility) {
548
+ const compat = base.compatibility;
550
549
  let matched = false;
551
550
 
552
551
  for (const entry of compat) {
@@ -570,15 +569,15 @@ export class ProviderLoader {
570
569
  }
571
570
 
572
571
  // No compatibility match → defaultScriptDir
573
- if (!matched && (base as any).defaultScriptDir) {
574
- const loaded = this.loadScriptsFromDir(type, (base as any).defaultScriptDir);
572
+ if (!matched && base.defaultScriptDir) {
573
+ const loaded = this.loadScriptsFromDir(type, base.defaultScriptDir);
575
574
  if (loaded) {
576
575
  resolved.scripts = loaded;
577
- this.log(` [compatibility] ${type} v${currentVersion} → default: ${(base as any).defaultScriptDir}`);
578
- resolved._resolvedScriptDir = (base as any).defaultScriptDir;
576
+ this.log(` [compatibility] ${type} v${currentVersion} → default: ${base.defaultScriptDir}`);
577
+ resolved._resolvedScriptDir = base.defaultScriptDir;
579
578
  resolved._resolvedScriptsSource = 'defaultScriptDir:version_miss';
580
579
  if (providerDir) {
581
- const fullDir = path.join(providerDir, (base as any).defaultScriptDir);
580
+ const fullDir = path.join(providerDir, base.defaultScriptDir);
582
581
  resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
583
582
  ? path.join(fullDir, 'scripts.js')
584
583
  : fullDir;
@@ -592,7 +591,7 @@ export class ProviderLoader {
592
591
  for (const [range, override] of Object.entries(base.versions)) {
593
592
  if (!this.matchesVersion(currentVersion, range)) continue;
594
593
 
595
- const dirOverride = (override as any).__dir as string | undefined;
594
+ const dirOverride = override.__dir;
596
595
  if (dirOverride) {
597
596
  const loaded = this.loadScriptsFromDir(type, dirOverride);
598
597
  if (loaded) {
@@ -612,16 +611,16 @@ export class ProviderLoader {
612
611
  }
613
612
  }
614
613
  }
615
- } else if (Array.isArray((base as any).compatibility) && (base as any).defaultScriptDir) {
614
+ } else if (base.compatibility && base.defaultScriptDir) {
616
615
  // No version detected but compatibility format → use defaultScriptDir
617
- const loaded = this.loadScriptsFromDir(type, (base as any).defaultScriptDir);
616
+ const loaded = this.loadScriptsFromDir(type, base.defaultScriptDir);
618
617
  if (loaded) {
619
618
  resolved.scripts = loaded;
620
- this.log(` [compatibility] ${type} no version detected → default: ${(base as any).defaultScriptDir}`);
621
- resolved._resolvedScriptDir = (base as any).defaultScriptDir;
619
+ this.log(` [compatibility] ${type} no version detected → default: ${base.defaultScriptDir}`);
620
+ resolved._resolvedScriptDir = base.defaultScriptDir;
622
621
  resolved._resolvedScriptsSource = 'defaultScriptDir:no_version';
623
622
  if (providerDir) {
624
- const fullDir = path.join(providerDir, (base as any).defaultScriptDir);
623
+ const fullDir = path.join(providerDir, base.defaultScriptDir);
625
624
  resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
626
625
  ? path.join(fullDir, 'scripts.js')
627
626
  : fullDir;
@@ -647,7 +646,7 @@ export class ProviderLoader {
647
646
  * Load scripts from a scriptDir within a provider directory.
648
647
  * Tries scripts.js first, then individual .js files.
649
648
  */
650
- private loadScriptsFromDir(type: string, scriptDir: string): Record<string, any> | null {
649
+ private loadScriptsFromDir(type: string, scriptDir: string): Partial<ProviderScripts> | null {
651
650
  const providerDir = this.findProviderDirInternal(type);
652
651
  if (!providerDir) {
653
652
  this.log(` [loadScriptsFromDir] ${type}: providerDir not found`);
@@ -679,7 +678,7 @@ export class ProviderLoader {
679
678
  }
680
679
 
681
680
  // Fallback: build from individual .js files
682
- const result = this.buildScriptWrappersFromDir(dir) as Record<string, any>;
681
+ const result = this.buildScriptWrappersFromDir(dir);
683
682
  this.scriptsCache.set(dir, result);
684
683
  return result;
685
684
  }
@@ -973,8 +972,8 @@ export class ProviderLoader {
973
972
  getPublicSettings(type: string): ProviderSettingSchema[] {
974
973
  const settings = this.getSettingsSchema(type);
975
974
  return Object.entries(settings)
976
- .filter(([, def]) => (def as any).public === true)
977
- .map(([key, def]) => ({ key, ...(def as any) }));
975
+ .filter(([, def]) => def.public === true)
976
+ .map(([key, def]) => ({ key, ...def }));
978
977
  }
979
978
 
980
979
  /**
@@ -994,17 +993,15 @@ export class ProviderLoader {
994
993
  */
995
994
  getSettingValue(type: string, key: string): any {
996
995
  const schemaDef = this.getSettingsSchema(type)[key];
997
- const defaultVal = schemaDef ? (schemaDef as any).default : undefined;
998
-
999
- // Load user-saved value
1000
- try {
1001
- const { loadConfig } = require('../config/config.js');
1002
- const config = loadConfig();
1003
- const userVal = config.providerSettings?.[type]?.[key];
1004
- return userVal !== undefined ? userVal : defaultVal;
1005
- } catch {
1006
- return defaultVal;
1007
- }
996
+ const defaultVal = schemaDef
997
+ ? (key === 'autoApprove' && schemaDef.type === 'boolean'
998
+ ? true
999
+ : schemaDef.default)
1000
+ : undefined;
1001
+
1002
+ const config = this.readConfig();
1003
+ const userVal = config?.providerSettings?.[type]?.[key];
1004
+ return userVal !== undefined ? userVal : defaultVal;
1008
1005
  }
1009
1006
 
1010
1007
  /**
@@ -1023,7 +1020,7 @@ export class ProviderLoader {
1023
1020
  * Save provider setting value (writes to config.json)
1024
1021
  */
1025
1022
  setSetting(type: string, key: string, value: any): boolean {
1026
- const schemaDef = this.getSettingsSchema(type)[key] as any;
1023
+ const schemaDef = this.getSettingsSchema(type)[key];
1027
1024
  if (!schemaDef) return false;
1028
1025
 
1029
1026
  // Non-public settings cannot be modified externally
@@ -1039,13 +1036,14 @@ export class ProviderLoader {
1039
1036
  }
1040
1037
  if (schemaDef.type === 'select' && schemaDef.options && !schemaDef.options.includes(value)) return false;
1041
1038
 
1039
+ const config = this.readConfig();
1040
+ if (!config) return false;
1041
+
1042
1042
  try {
1043
- const { loadConfig, saveConfig } = require('../config/config.js');
1044
- const config = loadConfig();
1045
1043
  if (!config.providerSettings) config.providerSettings = {};
1046
1044
  if (!config.providerSettings[type]) config.providerSettings[type] = {};
1047
1045
  config.providerSettings[type][key] = value;
1048
- saveConfig(config);
1046
+ this.writeConfig(config);
1049
1047
  this.log(`Setting updated: ${type}.${key} = ${JSON.stringify(value)}`);
1050
1048
  return true;
1051
1049
  } catch (e) {
@@ -1061,18 +1059,69 @@ export class ProviderLoader {
1061
1059
  return trimmed ? trimmed : null;
1062
1060
  }
1063
1061
 
1062
+ protected readConfig(): any | null {
1063
+ try {
1064
+ const { loadConfig } = require('../config/config.js');
1065
+ return loadConfig();
1066
+ } catch {
1067
+ return null;
1068
+ }
1069
+ }
1070
+
1071
+ protected writeConfig(config: any): void {
1072
+ const { saveConfig } = require('../config/config.js');
1073
+ saveConfig(config);
1074
+ }
1075
+
1076
+ private getPlatformVersionCommand(versionCommand?: ProviderModule['versionCommand']): string | undefined {
1077
+ if (!versionCommand) return undefined;
1078
+ if (typeof versionCommand === 'string') {
1079
+ const trimmed = versionCommand.trim();
1080
+ return trimmed || undefined;
1081
+ }
1082
+ const platformValue = versionCommand[process.platform];
1083
+ if (typeof platformValue === 'string' && platformValue.trim()) {
1084
+ return platformValue.trim();
1085
+ }
1086
+ const defaultValue = versionCommand.default;
1087
+ if (typeof defaultValue === 'string' && defaultValue.trim()) {
1088
+ return defaultValue.trim();
1089
+ }
1090
+ return undefined;
1091
+ }
1092
+
1064
1093
  private getSettingsSchema(type: string): Record<string, ProviderSettingDef> {
1065
1094
  const provider = this.providers.get(type);
1066
1095
  if (!provider) return {};
1067
- return {
1096
+ const result = {
1068
1097
  ...this.getSyntheticSettings(type, provider),
1069
1098
  ...(provider.settings || {}),
1070
1099
  };
1100
+ if (result.autoApprove?.type === 'boolean') {
1101
+ result.autoApprove = {
1102
+ ...result.autoApprove,
1103
+ default: true,
1104
+ public: true,
1105
+ label: result.autoApprove.label || 'Auto Approve',
1106
+ description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
1107
+ };
1108
+ }
1109
+ return result;
1071
1110
  }
1072
1111
 
1073
1112
  private getSyntheticSettings(type: string, provider: ProviderModule): Record<string, ProviderSettingDef> {
1074
1113
  const result: Record<string, ProviderSettingDef> = {};
1075
1114
 
1115
+ if (!provider.settings?.autoApprove) {
1116
+ result.autoApprove = {
1117
+ type: 'boolean',
1118
+ default: true,
1119
+ public: true,
1120
+ label: 'Auto Approve',
1121
+ description: 'Automatically approve actionable prompts without sending approval alerts.',
1122
+ };
1123
+ }
1124
+
1076
1125
  if ((provider.category === 'cli' || provider.category === 'acp') && provider.spawn?.command && !provider.settings?.executablePath) {
1077
1126
  result.executablePath = {
1078
1127
  type: 'string',
@@ -1168,7 +1217,7 @@ export class ProviderLoader {
1168
1217
  if (!file.endsWith('.js')) continue;
1169
1218
  const scriptName = toCamel(file.replace('.js', ''));
1170
1219
  const filePath = path.join(dir, file);
1171
- (result as any)[scriptName] = (...args: any[]): string => {
1220
+ result[scriptName] = (...args: any[]): string => {
1172
1221
  try {
1173
1222
  let content = fs.readFileSync(filePath, 'utf-8');
1174
1223
  if (args[0] && typeof args[0] === 'object') {
@@ -1235,40 +1284,46 @@ export class ProviderLoader {
1235
1284
  const jsonPath = path.join(d, 'provider.json');
1236
1285
  try {
1237
1286
  const raw = fs.readFileSync(jsonPath, 'utf-8');
1238
- const mod = JSON.parse(raw) as ProviderModule;
1287
+ const mod = JSON.parse(raw) as Omit<ProviderModule, 'extensionIdPattern'> & {
1288
+ extensionIdPattern?: RegExp | string;
1289
+ };
1239
1290
 
1240
1291
  if (!mod.type || !mod.name || !mod.category) {
1241
1292
  this.log(`⚠ Invalid provider at ${jsonPath}: missing type/name/category`);
1242
1293
  } else {
1243
1294
  // Restore RegExp fields from JSON (extensionIdPattern)
1244
- if ((mod as any).extensionIdPattern && typeof (mod as any).extensionIdPattern === 'string') {
1245
- const flags = (mod as any).extensionIdPattern_flags || '';
1246
- (mod as any).extensionIdPattern = new RegExp((mod as any).extensionIdPattern, flags);
1247
- delete (mod as any).extensionIdPattern_flags;
1295
+ if (typeof mod.extensionIdPattern === 'string') {
1296
+ const flags = mod.extensionIdPattern_flags || '';
1297
+ mod.extensionIdPattern = new RegExp(mod.extensionIdPattern, flags);
1248
1298
  }
1299
+ const { extensionIdPattern_flags, extensionIdPattern, ...providerFields } = mod;
1300
+ const normalizedProvider: ProviderModule = {
1301
+ ...providerFields,
1302
+ ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1303
+ };
1249
1304
 
1250
1305
  // Load scripts.js if exists (IDE/Extension)
1251
1306
  // Skip for compatibility-format providers — scripts loaded lazily in resolve()
1252
- const hasCompatibility = Array.isArray((mod as any).compatibility);
1307
+ const hasCompatibility = Array.isArray(normalizedProvider.compatibility);
1253
1308
  const scriptsPath = path.join(d, 'scripts.js');
1254
1309
  if (!hasCompatibility && fs.existsSync(scriptsPath)) {
1255
1310
  try {
1256
1311
  delete require.cache[require.resolve(scriptsPath)];
1257
- const scripts = require(scriptsPath);
1258
- mod.scripts = scripts;
1312
+ const scripts = require(scriptsPath) as Partial<ProviderScripts>;
1313
+ normalizedProvider.scripts = scripts;
1259
1314
  } catch (e) {
1260
1315
  this.log(`⚠ Failed to load scripts: ${scriptsPath}: ${(e as Error).message}`);
1261
1316
  }
1262
1317
  }
1263
1318
 
1264
- const existed = this.providers.has(mod.type);
1265
- this.providers.set(mod.type, mod);
1319
+ const existed = this.providers.has(normalizedProvider.type);
1320
+ this.providers.set(normalizedProvider.type, normalizedProvider);
1266
1321
  count++;
1267
1322
  // Identify source tier for debugging
1268
1323
  const source = d.startsWith(this.userDir) && !d.includes('.upstream')
1269
1324
  ? 'user' : 'upstream';
1270
1325
  const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES upstream' : '';
1271
- this.log(` ${existed ? '🔄' : '✅'} ${mod.type} (${mod.category}) — ${mod.name} [${source}]${overrideWarning}`);
1326
+ this.log(` ${existed ? '🔄' : '✅'} ${normalizedProvider.type} (${normalizedProvider.category}) — ${normalizedProvider.name} [${source}]${overrideWarning}`);
1272
1327
  }
1273
1328
  } catch (e) {
1274
1329
  this.log(`⚠ Failed to load ${jsonPath}: ${(e as Error).message}`);
@@ -15,6 +15,7 @@ import * as os from 'os';
15
15
  import { execSync } from 'child_process';
16
16
  import { platform } from 'os';
17
17
  import type { ProviderLoader } from './provider-loader.js';
18
+ import type { ProviderModule } from './contracts.js';
18
19
 
19
20
  // ─── Types ──────────────────────────────────────
20
21
 
@@ -141,6 +142,26 @@ function parseVersion(raw: string): string {
141
142
  return match ? match[1] : raw.split('\n')[0].substring(0, 100);
142
143
  }
143
144
 
145
+ function getPlatformVersionCommand(
146
+ versionCommand: ProviderModule['versionCommand'],
147
+ currentOs: string,
148
+ ): string | undefined {
149
+ if (!versionCommand) return undefined;
150
+ if (typeof versionCommand === 'string') {
151
+ const trimmed = versionCommand.trim();
152
+ return trimmed || undefined;
153
+ }
154
+ const platformValue = versionCommand[currentOs];
155
+ if (typeof platformValue === 'string' && platformValue.trim()) {
156
+ return platformValue.trim();
157
+ }
158
+ const defaultValue = versionCommand.default;
159
+ if (typeof defaultValue === 'string' && defaultValue.trim()) {
160
+ return defaultValue.trim();
161
+ }
162
+ return undefined;
163
+ }
164
+
144
165
  function getVersion(binary: string, versionCommand?: string): string | null {
145
166
  // Custom version command from provider.json
146
167
  if (versionCommand) {
@@ -200,10 +221,7 @@ export async function detectAllVersions(
200
221
  detectedAt: new Date().toISOString(),
201
222
  };
202
223
 
203
- const verCmdConfig = (provider as any).versionCommand;
204
- const versionCommand = typeof verCmdConfig === 'object' && verCmdConfig !== null
205
- ? verCmdConfig[currentOs]
206
- : verCmdConfig;
224
+ const versionCommand = getPlatformVersionCommand(provider.versionCommand, currentOs);
207
225
 
208
226
  if (provider.category === 'ide') {
209
227
  // IDE: check app path + CLI
@@ -256,7 +274,7 @@ export async function detectAllVersions(
256
274
 
257
275
  // Check testedVersions — warn if installed version is not documented
258
276
  if (info.version && info.installed) {
259
- const testedVersions: string[] = (provider as any).testedVersions || [];
277
+ const testedVersions = provider.testedVersions || [];
260
278
  if (testedVersions.length > 0 && !testedVersions.includes(info.version)) {
261
279
  info.warning = `Version ${info.version} is not in testedVersions [${testedVersions.join(', ')}]. Scripts may not work correctly.`;
262
280
  }