@adhdev/daemon-core 0.8.56 → 0.8.58

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 (35) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/commands/handler.d.ts +1 -0
  3. package/dist/commands/stream-commands.d.ts +2 -0
  4. package/dist/config/chat-history.d.ts +3 -0
  5. package/dist/config/config.d.ts +3 -0
  6. package/dist/config/provider-source-config.d.ts +23 -0
  7. package/dist/daemon/dev-server-types.d.ts +1 -0
  8. package/dist/daemon/dev-server.d.ts +4 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +316 -155
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +315 -155
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/providers/cli-provider-instance.d.ts +9 -0
  15. package/dist/providers/contracts.d.ts +1 -0
  16. package/dist/providers/provider-loader.d.ts +14 -1
  17. package/node_modules/@adhdev/session-host-core/package.json +1 -1
  18. package/package.json +1 -1
  19. package/src/boot/daemon-lifecycle.ts +13 -3
  20. package/src/commands/handler.ts +3 -0
  21. package/src/commands/router.ts +5 -4
  22. package/src/commands/stream-commands.ts +41 -0
  23. package/src/config/chat-history.ts +39 -0
  24. package/src/config/config.d.ts +3 -0
  25. package/src/config/config.ts +19 -3
  26. package/src/config/provider-source-config.ts +42 -0
  27. package/src/daemon/dev-auto-implement.ts +0 -13
  28. package/src/daemon/dev-server-types.ts +1 -0
  29. package/src/daemon/dev-server.ts +45 -20
  30. package/src/index.ts +2 -0
  31. package/src/providers/cli-provider-instance.ts +25 -0
  32. package/src/providers/contracts.ts +1 -0
  33. package/src/providers/provider-loader.d.ts +4 -1
  34. package/src/providers/provider-loader.ts +61 -23
  35. package/src/providers/provider-schema.ts +3 -0
@@ -9,6 +9,15 @@ import type { ProviderInstance, ProviderState, InstanceContext } from './provide
9
9
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
10
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
11
11
  export declare function getForcedNewSessionScriptName(provider: ProviderModule | undefined, launchMode: 'new' | 'resume' | 'manual'): string | null;
12
+ export declare function waitForCliAdapterReady(adapter: {
13
+ isReady?: () => boolean;
14
+ getStatus?: () => {
15
+ status?: string;
16
+ };
17
+ }, options?: {
18
+ timeoutMs?: number;
19
+ pollMs?: number;
20
+ }): Promise<void>;
12
21
  export declare class CliProviderInstance implements ProviderInstance {
13
22
  private provider;
14
23
  private workingDir;
@@ -328,6 +328,7 @@ export interface ProviderModule {
328
328
  /** How the CLI adapter decides when to submit typed input */
329
329
  submitStrategy?: 'wait_for_echo' | 'immediate';
330
330
  /** Keep this provider out of the upstream auto-updated bundle */
331
+ /** @deprecated Machine-level provider source policy now lives in config.providerSourceMode. Local overrides shadow upstream by root precedence and should not rely on provider-level disableUpstream. */
331
332
  disableUpstream?: boolean;
332
333
  approvalKeys?: Record<number, string>;
333
334
  patterns?: {
@@ -14,11 +14,16 @@
14
14
  */
15
15
  import { VersionArchive } from './version-archive.js';
16
16
  import type { ProviderModule, ProviderCategory, ProviderSettingSchema, ResolvedProvider } from './contracts.js';
17
+ import type { ProviderSourceMode } from '../config/config.js';
18
+ import type { ProviderSourceConfigSnapshot } from '../config/provider-source-config.js';
17
19
  export declare class ProviderLoader {
18
20
  private providers;
19
21
  private providerAvailability;
22
+ private defaultProvidersDir;
23
+ private explicitProviderDir;
20
24
  private userDir;
21
25
  private upstreamDir;
26
+ private sourceMode;
22
27
  private disableUpstream;
23
28
  private watchers;
24
29
  private logFn;
@@ -31,7 +36,9 @@ export declare class ProviderLoader {
31
36
  constructor(options?: {
32
37
  userDir?: string;
33
38
  logFn?: (msg: string) => void;
34
- /** Disable upstream auto-download (for dev/testing/OSS) */
39
+ /** Explicit machine-level provider source policy */
40
+ sourceMode?: ProviderSourceMode;
41
+ /** Deprecated alias for sourceMode='no-upstream' */
35
42
  disableUpstream?: boolean;
36
43
  });
37
44
  private log;
@@ -48,6 +55,12 @@ export declare class ProviderLoader {
48
55
  * Highest-priority editable overrides come first.
49
56
  */
50
57
  getProviderRoots(): string[];
58
+ getSourceConfig(): ProviderSourceConfigSnapshot;
59
+ applySourceConfig(options?: {
60
+ userDir?: string;
61
+ sourceMode?: ProviderSourceMode;
62
+ disableUpstream?: boolean;
63
+ }): ProviderSourceConfigSnapshot;
51
64
  /**
52
65
  * Canonical provider directory shape for a given root.
53
66
  */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/session-host-core",
3
- "version": "0.8.56",
3
+ "version": "0.8.58",
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.56",
3
+ "version": "0.8.58",
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",
@@ -90,6 +90,7 @@ export interface DaemonComponents {
90
90
  cdpManagers: Map<string, DaemonCdpManager>;
91
91
  sessionRegistry: SessionRegistry;
92
92
  detectedIdes: { value: IDEInfo[] };
93
+ refreshProviderAvailability: (providerType?: string) => Promise<void>;
93
94
  }
94
95
 
95
96
  export interface DaemonDevSupportOptions {
@@ -116,12 +117,13 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
116
117
  // 1. Global log interceptor
117
118
  installGlobalInterceptor();
118
119
 
119
- // 2. ProviderLoader (disableUpstream from config.json)
120
+ // 2. ProviderLoader (provider source mode from config.json)
120
121
  const appConfig = loadConfig();
121
- const disableUpstream = appConfig.disableUpstream ?? false;
122
+ const providerSourceMode = appConfig.providerSourceMode || 'normal';
123
+ const disableUpstream = providerSourceMode === 'no-upstream';
122
124
  const providerLoader = new ProviderLoader({
123
125
  logFn: config.providerLogFn,
124
- disableUpstream,
126
+ sourceMode: providerSourceMode,
125
127
  userDir: appConfig.providerDir,
126
128
  });
127
129
 
@@ -253,6 +255,10 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
253
255
  await refreshProviderAvailability(providerType);
254
256
  config.onStatusChange?.();
255
257
  },
258
+ onProviderSourceConfigChanged: async () => {
259
+ await refreshProviderAvailability();
260
+ config.onStatusChange?.();
261
+ },
256
262
  });
257
263
 
258
264
  // 8. AgentStreamManager
@@ -312,6 +318,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
312
318
  cdpManagers,
313
319
  sessionRegistry,
314
320
  detectedIdes: detectedIdesRef,
321
+ refreshProviderAvailability,
315
322
  };
316
323
  }
317
324
 
@@ -327,6 +334,9 @@ export async function startDaemonDevSupport(options: DaemonDevSupportOptions): P
327
334
  instanceManager: options.components.instanceManager,
328
335
  cliManager: options.components.cliManager,
329
336
  logFn: options.logFn,
337
+ onProviderSourceConfigChanged: async () => {
338
+ await options.components.refreshProviderAvailability();
339
+ },
330
340
  });
331
341
  await devServer.start();
332
342
  options.components.providerLoader.watch();
@@ -46,6 +46,7 @@ export interface CommandContext {
46
46
  instanceManager?: ProviderInstanceManager;
47
47
  sessionRegistry?: SessionRegistry;
48
48
  onProviderSettingChanged?: (providerType: string, key: string, value: any) => Promise<void> | void;
49
+ onProviderSourceConfigChanged?: () => Promise<void> | void;
49
50
  }
50
51
 
51
52
  /**
@@ -502,6 +503,8 @@ export class DaemonCommandHandler implements CommandHelpers {
502
503
  // ─── Provider Settings (stream-commands.ts) ──────────
503
504
  case 'get_provider_settings': return Stream.handleGetProviderSettings(this, args);
504
505
  case 'set_provider_setting': return Stream.handleSetProviderSetting(this, args);
506
+ case 'get_provider_source_config': return Stream.handleGetProviderSourceConfig(this, args);
507
+ case 'set_provider_source_config': return Stream.handleSetProviderSourceConfig(this, args);
505
508
 
506
509
  // ─── IDE Extension Settings (stream-commands.ts) ──────────
507
510
  case 'get_ide_extensions': return Stream.handleGetIdeExtensions(this, args);
@@ -382,8 +382,9 @@ export class DaemonCommandRouter {
382
382
  return { success: false, error: 'providerType required' };
383
383
  }
384
384
 
385
- const offset = Math.max(0, Number(args?.offset) || 0);
386
- const limit = Math.max(1, Math.min(100, Number(args?.limit) || 30));
385
+ const wantsAll = args?.all === true;
386
+ const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
387
+ const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
387
388
  const { sessions: historySessions, hasMore } = listSavedHistorySessions(providerType, { offset, limit });
388
389
  const state = loadState();
389
390
  const savedSessions = getSavedProviderSessions(state, { providerType, kind });
@@ -406,13 +407,13 @@ export class DaemonCommandRouter {
406
407
  providerName: saved?.providerName || recent?.providerName || providerType,
407
408
  kind: saved?.kind || recent?.kind || kind,
408
409
  title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
409
- workspace: saved?.workspace || recent?.workspace,
410
+ workspace: saved?.workspace || recent?.workspace || session.workspace,
410
411
  currentModel: saved?.currentModel || recent?.currentModel,
411
412
  preview: session.preview,
412
413
  messageCount: session.messageCount,
413
414
  firstMessageAt: session.firstMessageAt,
414
415
  lastMessageAt: session.lastMessageAt,
415
- canResume: !!(saved?.workspace || recent?.workspace) && canResumeById,
416
+ canResume: !!(saved?.workspace || recent?.workspace || session.workspace) && canResumeById,
416
417
  };
417
418
  }),
418
419
  hasMore,
@@ -6,6 +6,8 @@
6
6
  import type { CommandResult, CommandHelpers } from './handler.js';
7
7
  import type { ProviderLoader } from '../providers/provider-loader.js';
8
8
  import type { ProviderInstance } from '../providers/provider-instance.js';
9
+ import { loadConfig, saveConfig } from '../config/config.js';
10
+ import { parseProviderSourceConfigUpdate } from '../config/provider-source-config.js';
9
11
  import { getCliScriptCommand, parseCliScriptResult } from '../providers/cli-script-results.js';
10
12
  import {
11
13
  normalizeControlInvokeResult,
@@ -107,6 +109,45 @@ export async function handleSetProviderSetting(h: CommandHelpers, args: any): Pr
107
109
  return { success: false, error: `Failed to set ${providerType}.${key} — invalid key, value, or not a public setting` };
108
110
  }
109
111
 
112
+ export function handleGetProviderSourceConfig(h: CommandHelpers, _args: any): CommandResult {
113
+ const loader = h.ctx.providerLoader as ProviderLoader | undefined;
114
+ if (!loader) return { success: false, error: 'providerLoader not available' };
115
+ return { success: true, ...loader.getSourceConfig() };
116
+ }
117
+
118
+ export async function handleSetProviderSourceConfig(h: CommandHelpers, args: any): Promise<CommandResult> {
119
+ const loader = h.ctx.providerLoader as ProviderLoader | undefined;
120
+ if (!loader) return { success: false, error: 'providerLoader not available' };
121
+
122
+ const parsed = parseProviderSourceConfigUpdate(args || {});
123
+ if ('error' in parsed) {
124
+ return { success: false, error: parsed.error };
125
+ }
126
+
127
+ const currentConfig = loadConfig();
128
+ const nextConfig = {
129
+ ...currentConfig,
130
+ ...(parsed.updates.providerSourceMode ? { providerSourceMode: parsed.updates.providerSourceMode } : {}),
131
+ ...(Object.prototype.hasOwnProperty.call(parsed.updates, 'providerDir') ? { providerDir: parsed.updates.providerDir } : {}),
132
+ };
133
+ saveConfig(nextConfig);
134
+
135
+ const sourceConfig = loader.applySourceConfig({
136
+ sourceMode: nextConfig.providerSourceMode,
137
+ userDir: Object.prototype.hasOwnProperty.call(parsed.updates, 'providerDir') ? parsed.updates.providerDir : loader.getSourceConfig().explicitProviderDir || undefined,
138
+ });
139
+ loader.reload();
140
+ loader.registerToDetector();
141
+ await h.ctx.onProviderSourceConfigChanged?.();
142
+
143
+ LOG.info(
144
+ 'Command',
145
+ `[set_provider_source_config] mode=${sourceConfig.sourceMode} explicitProviderDir=${sourceConfig.explicitProviderDir || '-'} userDir=${sourceConfig.userDir}`,
146
+ );
147
+
148
+ return { success: true, reloaded: true, ...sourceConfig };
149
+ }
150
+
110
151
  // ─── Extension Script Execution (Model/Mode) ─────
111
152
 
112
153
  function normalizeProviderScriptArgs(args: any): Record<string, any> {
@@ -27,6 +27,7 @@ interface HistoryMessage {
27
27
  instanceId?: string; // IDE instance UUID (distinguishes windows of the same agent type)
28
28
  historySessionId?: string; // Persistent provider-side conversation/session key
29
29
  sessionTitle?: string;
30
+ workspace?: string; // Working directory at session start (kind: 'session_start' only)
30
31
  }
31
32
 
32
33
  const CODEX_STARTER_PROMPT_RE = /^(?:[›❯]\s*)?(?:Find and fix a bug in @filename|Improve documentation in @filename|Write tests for @filename|Explain this codebase|Summarize recent commits|Implement \{feature\}|Use \/skills(?: to list available skills)?|Run \/review on my current changes)$/i;
@@ -123,6 +124,7 @@ export interface SavedHistorySessionSummary {
123
124
  firstMessageAt: number;
124
125
  lastMessageAt: number;
125
126
  preview?: string;
127
+ workspace?: string;
126
128
  }
127
129
 
128
130
  export class ChatHistoryWriter {
@@ -325,6 +327,37 @@ export class ChatHistoryWriter {
325
327
  );
326
328
  }
327
329
 
330
+ writeSessionStart(
331
+ agentType: string,
332
+ historySessionId: string,
333
+ workspace: string,
334
+ instanceId?: string,
335
+ ): void {
336
+ const id = String(historySessionId || '').trim();
337
+ const ws = String(workspace || '').trim();
338
+ if (!id || !ws) return;
339
+ try {
340
+ const dir = path.join(HISTORY_DIR, this.sanitize(agentType));
341
+ fs.mkdirSync(dir, { recursive: true });
342
+ const date = new Date().toISOString().slice(0, 10);
343
+ const filePath = path.join(dir, `${this.sanitize(id)}_${date}.jsonl`);
344
+ const record: HistoryMessage = {
345
+ ts: new Date().toISOString(),
346
+ receivedAt: Date.now(),
347
+ role: 'system',
348
+ kind: 'session_start',
349
+ content: ws,
350
+ agent: agentType,
351
+ instanceId,
352
+ historySessionId: id,
353
+ workspace: ws,
354
+ };
355
+ fs.appendFileSync(filePath, JSON.stringify(record) + '\n', 'utf-8');
356
+ } catch {
357
+ // Ignore — must not affect main functionality
358
+ }
359
+ }
360
+
328
361
  promoteHistorySession(
329
362
  agentType: string,
330
363
  previousHistorySessionId: string,
@@ -606,6 +639,7 @@ export function listSavedHistorySessions(
606
639
  let lastMessageAt = 0;
607
640
  let sessionTitle = '';
608
641
  let preview = '';
642
+ let workspace = '';
609
643
 
610
644
  for (const file of files.sort()) {
611
645
  const filePath = path.join(dir, file);
@@ -619,6 +653,10 @@ export function listSavedHistorySessions(
619
653
  parsed = null;
620
654
  }
621
655
  if (!parsed || parsed.historySessionId !== historySessionId) continue;
656
+ if (parsed.kind === 'session_start') {
657
+ if (!workspace && parsed.workspace) workspace = parsed.workspace;
658
+ continue;
659
+ }
622
660
  messageCount += 1;
623
661
  if (!firstMessageAt || parsed.receivedAt < firstMessageAt) firstMessageAt = parsed.receivedAt;
624
662
  if (!lastMessageAt || parsed.receivedAt > lastMessageAt) lastMessageAt = parsed.receivedAt;
@@ -635,6 +673,7 @@ export function listSavedHistorySessions(
635
673
  firstMessageAt,
636
674
  lastMessageAt,
637
675
  preview: preview || undefined,
676
+ workspace: workspace || undefined,
638
677
  });
639
678
  }
640
679
 
@@ -8,6 +8,8 @@ export type { WorkspaceEntry } from './workspaces.js';
8
8
  export type { RecentActivityEntry } from './recent-activity.js';
9
9
  export type { SavedProviderSessionEntry } from './saved-sessions.js';
10
10
  export type { DaemonState } from './state-store.js';
11
+ export type ProviderSourceMode = 'normal' | 'no-upstream';
12
+ export declare function resolveProviderSourceMode(providerSourceMode: unknown, legacyDisableUpstream: unknown): ProviderSourceMode;
11
13
  export interface ADHDevConfig {
12
14
  serverUrl: string;
13
15
  selectedIde: string | null;
@@ -45,6 +47,7 @@ export interface ADHDevConfig {
45
47
  }>;
46
48
  }>;
47
49
  disableUpstream?: boolean;
50
+ providerSourceMode?: ProviderSourceMode;
48
51
  providerDir?: string;
49
52
  /**
50
53
  * Browser terminal sizing behavior for dashboard CLI panes.
@@ -14,6 +14,18 @@ export type { RecentActivityEntry } from './recent-activity.js';
14
14
  export type { SavedProviderSessionEntry } from './saved-sessions.js';
15
15
  export type { DaemonState } from './state-store.js';
16
16
 
17
+ export type ProviderSourceMode = 'normal' | 'no-upstream';
18
+
19
+ export function resolveProviderSourceMode(
20
+ providerSourceMode: unknown,
21
+ legacyDisableUpstream: unknown,
22
+ ): ProviderSourceMode {
23
+ if (providerSourceMode === 'normal' || providerSourceMode === 'no-upstream') {
24
+ return providerSourceMode;
25
+ }
26
+ return legacyDisableUpstream === true ? 'no-upstream' : 'normal';
27
+ }
28
+
17
29
  export interface ADHDevConfig {
18
30
  // Server connection
19
31
  serverUrl: string;
@@ -81,9 +93,13 @@ export interface ADHDevConfig {
81
93
 
82
94
  // Disable upstream provider auto-download (use builtin only)
83
95
  // Controllable from CLI (--no-upstream) and dashboard (machine page)
96
+ // Deprecated legacy boolean; prefer providerSourceMode.
84
97
  disableUpstream?: boolean;
85
98
 
86
- // Optional custom provider directory for local development
99
+ // Explicit machine-level provider source policy.
100
+ providerSourceMode?: ProviderSourceMode;
101
+
102
+ // Optional explicit provider override root (for example a local adhdev-providers checkout)
87
103
  providerDir?: string;
88
104
 
89
105
  /**
@@ -113,7 +129,7 @@ const DEFAULT_CONFIG: ADHDevConfig = {
113
129
  registeredMachineId: undefined,
114
130
  providerSettings: {},
115
131
  ideSettings: {},
116
- disableUpstream: false,
132
+ providerSourceMode: 'normal',
117
133
  terminalSizingMode: 'measured',
118
134
  };
119
135
 
@@ -164,7 +180,7 @@ function normalizeConfig(raw: unknown): ADHDevConfig & { activeWorkspaceId?: str
164
180
  registeredMachineId: asOptionalString(parsed.registeredMachineId),
165
181
  providerSettings: isPlainObject(parsed.providerSettings) ? parsed.providerSettings : {},
166
182
  ideSettings: isPlainObject(parsed.ideSettings) ? parsed.ideSettings : {},
167
- disableUpstream: asBoolean(parsed.disableUpstream, DEFAULT_CONFIG.disableUpstream ?? false),
183
+ providerSourceMode: resolveProviderSourceMode(parsed.providerSourceMode, parsed.disableUpstream),
168
184
  providerDir: asOptionalString(parsed.providerDir),
169
185
  terminalSizingMode: parsed.terminalSizingMode === 'fit' ? 'fit' : 'measured',
170
186
  };
@@ -0,0 +1,42 @@
1
+ import type { ProviderSourceMode } from './config.js'
2
+
3
+ export interface ProviderSourceConfigSnapshot {
4
+ sourceMode: ProviderSourceMode
5
+ disableUpstream: boolean
6
+ explicitProviderDir: string | null
7
+ userDir: string
8
+ upstreamDir: string
9
+ providerRoots: string[]
10
+ }
11
+
12
+ export interface ProviderSourceConfigUpdate {
13
+ providerSourceMode?: ProviderSourceMode
14
+ providerDir?: string | undefined
15
+ }
16
+
17
+ function normalizeProviderDir(value: unknown): string | undefined {
18
+ if (typeof value !== 'string') return undefined
19
+ const trimmed = value.trim()
20
+ return trimmed ? trimmed : undefined
21
+ }
22
+
23
+ export function parseProviderSourceConfigUpdate(input: {
24
+ providerSourceMode?: unknown
25
+ providerDir?: unknown
26
+ }): { ok: true; updates: ProviderSourceConfigUpdate } | { ok: false; error: string } {
27
+ const updates: ProviderSourceConfigUpdate = {}
28
+
29
+ if (Object.prototype.hasOwnProperty.call(input, 'providerSourceMode')) {
30
+ const { providerSourceMode } = input
31
+ if (providerSourceMode !== 'normal' && providerSourceMode !== 'no-upstream') {
32
+ return { ok: false, error: "providerSourceMode must be 'normal' or 'no-upstream'" }
33
+ }
34
+ updates.providerSourceMode = providerSourceMode
35
+ }
36
+
37
+ if (Object.prototype.hasOwnProperty.call(input, 'providerDir')) {
38
+ updates.providerDir = normalizeProviderDir(input.providerDir)
39
+ }
40
+
41
+ return { ok: true, updates }
42
+ }
@@ -141,19 +141,6 @@ export function resolveAutoImplWritableProviderDir(ctx: DevServerContext,
141
141
  return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
142
142
  }
143
143
 
144
- try {
145
- const providerData = JSON.parse(fs.readFileSync(providerJson, 'utf-8'));
146
- if (providerData.disableUpstream !== true) {
147
- providerData.disableUpstream = true;
148
- fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
149
- }
150
- } catch (error) {
151
- return {
152
- dir: null,
153
- reason: `Failed to update provider.json in writable provider directory: ${(error as Error).message}`,
154
- };
155
- }
156
-
157
144
  return { dir: desiredDir };
158
145
  }
159
146
 
@@ -20,6 +20,7 @@ export interface DevServerContext {
20
20
  readonly cdpManagers: Map<string, DaemonCdpManager>;
21
21
  readonly instanceManager: ProviderInstanceManager | null;
22
22
  readonly cliManager: DaemonCliManager | null;
23
+ readonly onProviderSourceConfigChanged?: (() => Promise<void> | void) | null;
23
24
 
24
25
  // Utilities
25
26
  getCdp(ideType?: string): DaemonCdpManager | null;
@@ -22,6 +22,8 @@ import * as os from 'os';
22
22
  import type { ProviderLoader } from '../providers/provider-loader.js';
23
23
  import type { ProviderCategory, ProviderModule, ProviderScripts, ProviderSettingDef } from '../providers/contracts.js';
24
24
  import { validateProviderDefinition } from '../providers/provider-schema.js';
25
+ import { loadConfig, saveConfig } from '../config/config.js';
26
+ import { parseProviderSourceConfigUpdate } from '../config/provider-source-config.js';
25
27
  import type { ChildProcess } from 'child_process';
26
28
  import type { DaemonCdpManager } from '../cdp/manager.js';
27
29
  import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
@@ -100,6 +102,7 @@ export class DevServer implements DevServerContext {
100
102
  public cdpManagers: Map<string, DaemonCdpManager>;
101
103
  public instanceManager: ProviderInstanceManager | null;
102
104
  public cliManager: DaemonCliManager | null;
105
+ public onProviderSourceConfigChanged: (() => Promise<void> | void) | null;
103
106
  private logFn: (msg: string) => void;
104
107
  private sseClients: http.ServerResponse[] = [];
105
108
  private watchScriptPath: string | null = null;
@@ -120,11 +123,13 @@ export class DevServer implements DevServerContext {
120
123
  instanceManager?: ProviderInstanceManager;
121
124
  cliManager?: DaemonCliManager;
122
125
  logFn?: (msg: string) => void;
126
+ onProviderSourceConfigChanged?: () => Promise<void> | void;
123
127
  }) {
124
128
  this.providerLoader = options.providerLoader;
125
129
  this.cdpManagers = options.cdpManagers;
126
130
  this.instanceManager = options.instanceManager || null;
127
131
  this.cliManager = options.cliManager || null;
132
+ this.onProviderSourceConfigChanged = options.onProviderSourceConfigChanged || null;
128
133
  this.logFn = options.logFn || LOG.forComponent('DevServer').asLogFn();
129
134
  }
130
135
 
@@ -140,6 +145,8 @@ export class DevServer implements DevServerContext {
140
145
  }[] = [
141
146
  // Static routes
142
147
  { method: 'GET', pattern: '/api/providers', handler: (q, s) => this.handleListProviders(q, s) },
148
+ { method: 'GET', pattern: '/api/providers/source-config', handler: (q, s) => this.handleGetProviderSourceConfig(q, s) },
149
+ { method: 'POST', pattern: '/api/providers/source-config', handler: (q, s) => this.handleSetProviderSourceConfig(q, s) },
143
150
  { method: 'GET', pattern: '/api/providers/versions', handler: (q, s) => this.handleDetectVersions(q, s) },
144
151
  { method: 'POST', pattern: '/api/providers/reload', handler: (q, s) => this.handleReload(q, s) },
145
152
  { method: 'POST', pattern: '/api/cdp/evaluate', handler: (q, s) => this.handleCdpEvaluate(q, s) },
@@ -273,7 +280,38 @@ export class DevServer implements DevServerContext {
273
280
 
274
281
  private async handleListProviders(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
275
282
  const providers = this.providerLoader.getAll().map(toProviderListEntry);
276
- this.json(res, 200, { providers, count: providers.length });
283
+ this.json(res, 200, { providers, count: providers.length, sourceConfig: this.providerLoader.getSourceConfig() });
284
+ }
285
+
286
+ private async handleGetProviderSourceConfig(_req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
287
+ this.json(res, 200, { success: true, sourceConfig: this.providerLoader.getSourceConfig() });
288
+ }
289
+
290
+ private async handleSetProviderSourceConfig(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
291
+ const body = await this.readBody(req);
292
+ const parsed = parseProviderSourceConfigUpdate(body || {});
293
+ if (!parsed.ok) {
294
+ this.json(res, 400, { success: false, error: parsed.error });
295
+ return;
296
+ }
297
+
298
+ const currentConfig = loadConfig();
299
+ const nextConfig = {
300
+ ...currentConfig,
301
+ ...(parsed.updates.providerSourceMode ? { providerSourceMode: parsed.updates.providerSourceMode } : {}),
302
+ ...(Object.prototype.hasOwnProperty.call(parsed.updates, 'providerDir') ? { providerDir: parsed.updates.providerDir } : {}),
303
+ };
304
+ saveConfig(nextConfig);
305
+
306
+ const sourceConfig = this.providerLoader.applySourceConfig({
307
+ sourceMode: nextConfig.providerSourceMode,
308
+ userDir: Object.prototype.hasOwnProperty.call(parsed.updates, 'providerDir') ? parsed.updates.providerDir : this.providerLoader.getSourceConfig().explicitProviderDir || undefined,
309
+ });
310
+ this.providerLoader.reload();
311
+ this.providerLoader.registerToDetector();
312
+ await this.onProviderSourceConfigChanged?.();
313
+
314
+ this.json(res, 200, { success: true, reloaded: true, sourceConfig });
277
315
  }
278
316
 
279
317
  private async handleProviderConfig(type: string, _req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
@@ -994,27 +1032,14 @@ export class DevServer implements DevServerContext {
994
1032
  this.log(`Auto-implement writable copy created: ${desiredDir}`);
995
1033
  }
996
1034
 
997
- const providerJson = path.join(desiredDir, 'provider.json');
998
- if (!fs.existsSync(providerJson)) {
999
- return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
1000
- }
1001
-
1002
- try {
1003
- const providerData = JSON.parse(fs.readFileSync(providerJson, 'utf-8'));
1004
- if (providerData.disableUpstream !== true) {
1005
- providerData.disableUpstream = true;
1006
- fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
1007
- }
1008
- } catch (error) {
1009
- return {
1010
- dir: null,
1011
- reason: `Failed to update provider.json in writable provider directory: ${(error as Error).message}`,
1012
- };
1013
- }
1014
-
1015
- return { dir: desiredDir };
1035
+ const providerJson = path.join(desiredDir, 'provider.json');
1036
+ if (!fs.existsSync(providerJson)) {
1037
+ return { dir: null, reason: `provider.json not found in writable provider directory: ${desiredDir}` };
1016
1038
  }
1017
1039
 
1040
+ return { dir: desiredDir };
1041
+ }
1042
+
1018
1043
 
1019
1044
  private async handleAutoImplement(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1020
1045
  return handleAutoImplement(this, type, req, res);
package/src/index.ts CHANGED
@@ -195,6 +195,8 @@ export { IdeProviderInstance } from './providers/ide-provider-instance.js';
195
195
  export { CliProviderInstance } from './providers/cli-provider-instance.js';
196
196
  export { AcpProviderInstance } from './providers/acp-provider-instance.js';
197
197
  export type { ProviderModule, CdpTargetFilter, ProviderResumeCapability, InputEnvelope, InputPart, MessagePart, ControlListResult, ControlSetResult, ControlInvokeResult } from './providers/contracts.js';
198
+ export type { ProviderSourceConfigSnapshot, ProviderSourceConfigUpdate } from './config/provider-source-config.js';
199
+ export { parseProviderSourceConfigUpdate } from './config/provider-source-config.js';
198
200
  export { normalizeInputEnvelope, normalizeMessageParts, flattenMessageParts } from './providers/io-contracts.js';
199
201
  export { VersionArchive, detectAllVersions } from './providers/version-archive.js';
200
202
  export type { ProviderVersionInfo, VersionHistory } from './providers/version-archive.js';
@@ -55,6 +55,9 @@ export function getForcedNewSessionScriptName(
55
55
  const controls = Array.isArray((provider as any).controls) ? (provider as any).controls : [];
56
56
  for (const control of controls) {
57
57
  if (control?.type !== 'action') continue;
58
+ if (typeof control?.confirmTitle === 'string' && control.confirmTitle.trim()) continue;
59
+ if (typeof control?.confirmMessage === 'string' && control.confirmMessage.trim()) continue;
60
+ if (typeof control?.confirmLabel === 'string' && control.confirmLabel.trim()) continue;
58
61
  const invokeScript = typeof control?.invokeScript === 'string' ? control.invokeScript.trim() : '';
59
62
  if (!invokeScript) continue;
60
63
  const controlId = typeof control?.id === 'string' ? control.id.trim() : '';
@@ -66,6 +69,26 @@ export function getForcedNewSessionScriptName(
66
69
  return null;
67
70
  }
68
71
 
72
+ export async function waitForCliAdapterReady(
73
+ adapter: { isReady?: () => boolean; getStatus?: () => { status?: string } },
74
+ options?: { timeoutMs?: number; pollMs?: number },
75
+ ): Promise<void> {
76
+ const timeoutMs = Math.max(100, options?.timeoutMs ?? 15_000);
77
+ const pollMs = Math.max(10, options?.pollMs ?? 50);
78
+ const deadline = Date.now() + timeoutMs;
79
+
80
+ while (Date.now() < deadline) {
81
+ if (adapter?.isReady?.()) return;
82
+ const status = adapter?.getStatus?.()?.status;
83
+ if (status === 'stopped') {
84
+ throw new Error('CLI runtime stopped before it became ready');
85
+ }
86
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
87
+ }
88
+
89
+ throw new Error(`CLI runtime did not become ready within ${timeoutMs}ms`);
90
+ }
91
+
69
92
  export class CliProviderInstance implements ProviderInstance {
70
93
  readonly type: string;
71
94
  readonly category = 'cli' as const;
@@ -427,6 +450,7 @@ export class CliProviderInstance implements ProviderInstance {
427
450
  if (!scriptName) return;
428
451
 
429
452
  LOG.info('CLI', `[${this.type}] forcing fresh session launch via script: ${scriptName}`);
453
+ await waitForCliAdapterReady(this.adapter);
430
454
  const raw = await this.adapter.invokeScript(scriptName, {});
431
455
  const parsed = parseCliScriptResult(raw);
432
456
  if (!parsed.success) {
@@ -819,6 +843,7 @@ export class CliProviderInstance implements ProviderInstance {
819
843
  const previousProviderSessionId = this.providerSessionId;
820
844
  this.providerSessionId = nextSessionId;
821
845
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
846
+ this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
822
847
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
823
848
  this.onProviderSessionResolved?.({
824
849
  instanceId: this.instanceId,
@@ -414,6 +414,7 @@ export interface ProviderModule {
414
414
  /** How the CLI adapter decides when to submit typed input */
415
415
  submitStrategy?: 'wait_for_echo' | 'immediate';
416
416
  /** Keep this provider out of the upstream auto-updated bundle */
417
+ /** @deprecated Machine-level provider source policy now lives in config.providerSourceMode. Local overrides shadow upstream by root precedence and should not rely on provider-level disableUpstream. */
417
418
  disableUpstream?: boolean;
418
419
  approvalKeys?: Record<number, string>;
419
420
  patterns?: {