@adhdev/daemon-core 0.6.71 → 0.6.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.6.71",
3
+ "version": "0.6.73",
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",
@@ -33,6 +33,7 @@
33
33
  "@agentclientprotocol/sdk": "^0.16.1",
34
34
  "@xterm/xterm": "^6.0.0",
35
35
  "chalk": "^5.3.0",
36
+ "chokidar": "^5.0.0",
36
37
  "conf": "^13.0.0",
37
38
  "node-pty": "^1.1.0",
38
39
  "ws": "^8.19.0"
@@ -18,6 +18,7 @@ import { AgentStreamPoller } from '../agent-stream/poller.js';
18
18
  import { ProviderLoader } from '../providers/provider-loader.js';
19
19
  import { VersionArchive, detectAllVersions } from '../providers/version-archive.js';
20
20
  import { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
21
+ import { DevServer } from '../daemon/dev-server.js';
21
22
  import { detectIDEs } from '../detection/ide-detector.js';
22
23
  import { installGlobalInterceptor, LOG } from '../logging/logger.js';
23
24
  import { loadConfig } from '../config/config.js';
@@ -73,6 +74,11 @@ export interface DaemonComponents {
73
74
  detectedIdes: { value: any[] };
74
75
  }
75
76
 
77
+ export interface DaemonDevSupportOptions {
78
+ components: DaemonComponents;
79
+ logFn?: (msg: string) => void;
80
+ }
81
+
76
82
  // ─── Init ───
77
83
 
78
84
  /**
@@ -240,6 +246,24 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
240
246
  };
241
247
  }
242
248
 
249
+ /**
250
+ * Start shared dev-only helpers:
251
+ * - DevServer on port 19280
252
+ * - Provider hot-reload watcher
253
+ */
254
+ export async function startDaemonDevSupport(options: DaemonDevSupportOptions): Promise<DevServer> {
255
+ const devServer = new DevServer({
256
+ providerLoader: options.components.providerLoader,
257
+ cdpManagers: options.components.cdpManagers,
258
+ instanceManager: options.components.instanceManager,
259
+ cliManager: options.components.cliManager,
260
+ logFn: options.logFn,
261
+ });
262
+ await devServer.start();
263
+ options.components.providerLoader.watch();
264
+ return devServer;
265
+ }
266
+
243
267
  // ─── Shutdown ───
244
268
 
245
269
  /**
@@ -355,6 +355,8 @@ export class ProviderCliAdapter implements CliAdapter {
355
355
 
356
356
  // PTY I/O
357
357
  private onPtyDataCallback: ((data: string) => void) | null = null;
358
+ private pendingOutputParseBuffer = '';
359
+ private pendingOutputParseTimer: NodeJS.Timeout | null = null;
358
360
  private ptyOutputBuffer = '';
359
361
  private ptyOutputFlushTimer: NodeJS.Timeout | null = null;
360
362
 
@@ -516,6 +518,17 @@ export class ProviderCliAdapter implements CliAdapter {
516
518
  this.onPtyDataCallback = callback;
517
519
  }
518
520
 
521
+ private flushPendingOutputParse(): void {
522
+ if (this.pendingOutputParseTimer) {
523
+ clearTimeout(this.pendingOutputParseTimer);
524
+ this.pendingOutputParseTimer = null;
525
+ }
526
+ if (!this.pendingOutputParseBuffer) return;
527
+ const rawData = this.pendingOutputParseBuffer;
528
+ this.pendingOutputParseBuffer = '';
529
+ this.handleOutput(rawData);
530
+ }
531
+
519
532
  async spawn(): Promise<void> {
520
533
  if (this.ptyProcess) return;
521
534
  if (!pty) throw new Error('node-pty is not installed');
@@ -576,7 +589,21 @@ export class ProviderCliAdapter implements CliAdapter {
576
589
  }
577
590
 
578
591
  this.ptyProcess.onData((data: string) => {
579
- this.handleOutput(data);
592
+ if (Date.now() < this.resizeSuppressUntil) return;
593
+
594
+ if (data.includes('\x1b[6n') || data.includes('\x1b[?6n')) {
595
+ // Some TUIs probe cursor position during startup; reply quickly even when batching parsing.
596
+ this.ptyProcess?.write('\x1b[1;1R');
597
+ }
598
+
599
+ this.pendingOutputParseBuffer += data;
600
+ if (!this.pendingOutputParseTimer) {
601
+ this.pendingOutputParseTimer = setTimeout(() => {
602
+ this.pendingOutputParseTimer = null;
603
+ this.flushPendingOutputParse();
604
+ }, this.timeouts.ptyFlush);
605
+ }
606
+
580
607
  if (this.onPtyDataCallback) {
581
608
  this.ptyOutputBuffer += data;
582
609
  if (!this.ptyOutputFlushTimer) {
@@ -593,6 +620,7 @@ export class ProviderCliAdapter implements CliAdapter {
593
620
 
594
621
  this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
595
622
  LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
623
+ this.flushPendingOutputParse();
596
624
  this.ptyProcess = null;
597
625
  this.setStatus('stopped', 'pty_exit');
598
626
  this.ready = false;
@@ -615,13 +643,6 @@ export class ProviderCliAdapter implements CliAdapter {
615
643
  // ─── Output Handling ────────────────────────────
616
644
 
617
645
  private handleOutput(rawData: string): void {
618
- if (Date.now() < this.resizeSuppressUntil) return;
619
-
620
- if (rawData.includes('\x1b[6n') || rawData.includes('\x1b[?6n')) {
621
- // Some TUIs probe cursor position during startup; node-pty does not answer automatically.
622
- this.ptyProcess?.write('\x1b[1;1R');
623
- }
624
-
625
646
  this.terminalScreen.write(rawData);
626
647
  this.terminalHistory = mergeTerminalHistory(this.terminalHistory, this.terminalScreen.getText());
627
648
  const cleanData = stripAnsi(rawData);
@@ -1136,6 +1157,10 @@ export class ProviderCliAdapter implements CliAdapter {
1136
1157
  if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
1137
1158
  if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1138
1159
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1160
+ if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
1161
+ this.pendingOutputParseBuffer = '';
1162
+ if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
1163
+ this.ptyOutputBuffer = '';
1139
1164
  if (this.ptyProcess) {
1140
1165
  this.ptyProcess.write('\x03');
1141
1166
  setTimeout(() => {
@@ -1159,6 +1184,10 @@ export class ProviderCliAdapter implements CliAdapter {
1159
1184
  this.currentTurnScope = null;
1160
1185
  this.submitRetryUsed = false;
1161
1186
  this.submitRetryPromptSnippet = '';
1187
+ if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
1188
+ this.pendingOutputParseBuffer = '';
1189
+ if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
1190
+ this.ptyOutputBuffer = '';
1162
1191
  this.terminalScreen.reset();
1163
1192
  this.onStatusChange?.();
1164
1193
  }
@@ -1236,6 +1265,8 @@ export class ProviderCliAdapter implements CliAdapter {
1236
1265
  scriptNames: Object.keys(this.cliScripts).filter(k => typeof (this.cliScripts as any)[k] === 'function'),
1237
1266
  statusHistory: this.statusHistory.slice(-30),
1238
1267
  timeouts: this.timeouts,
1268
+ pendingOutputParseBufferLength: this.pendingOutputParseBuffer.length,
1269
+ pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
1239
1270
  ptyAlive: !!this.ptyProcess,
1240
1271
  };
1241
1272
  }
@@ -65,13 +65,27 @@ export interface ADHDevConfig {
65
65
  // Machine nickname (user-customizable label for this machine)
66
66
  machineNickname: string | null;
67
67
 
68
- // Stable local machine ID shared by standalone and cloud daemon modes
68
+ /**
69
+ * Stable local machine ID (prefix: `mach_`) — generated locally on first run.
70
+ * Used as daemon instance key (`daemon_<machineId>`) and in status reports.
71
+ * NOT the same as the server-side D1 `machines.id` — see `registeredMachineId`.
72
+ */
69
73
  machineId?: string;
70
74
 
71
75
  // Machine secret for server auth (replaces connectionToken)
72
76
  machineSecret?: string | null;
73
77
 
74
- // Account-scoped registered machine row ID (cloud-side)
78
+ /**
79
+ * Server-side D1 `machines.id` — the row ID assigned when daemon registers via
80
+ * `POST /cli/complete`. Corresponds to `machineId` in server DO context
81
+ * (`DaemonConnection.machineId`, `StatusContext.machineId`).
82
+ *
83
+ * Naming differs from server-side `machineId` to avoid confusion with the local
84
+ * `config.machineId` (mach_ prefix) which is a different value.
85
+ *
86
+ * @deprecated Legacy bridge field — will be removed after 2026-04-06.
87
+ * Modern auth flow uses `machineSecret` (adm_) to identify machines.
88
+ */
75
89
  registeredMachineId?: string;
76
90
 
77
91
  // CLI launch history
@@ -1876,26 +1876,78 @@ export class DevServer {
1876
1876
  if (ref?.category === category) return desired;
1877
1877
 
1878
1878
  const all = this.providerLoader.getAll();
1879
- const fallback = all.find((p: any) => p.category === category && p.type !== targetType);
1879
+ const fallback = all
1880
+ .filter((p: any) => p.category === category && p.type !== targetType)
1881
+ .sort((a: any, b: any) => String(a.type || '').localeCompare(String(b.type || ''), undefined, { numeric: true, sensitivity: 'base' }))[0];
1880
1882
  return fallback?.type || null;
1881
1883
  }
1882
1884
 
1883
- private loadAutoImplReferenceScripts(category: ProviderCategory, referenceType: string | null): Record<string, string> {
1885
+ private getLatestScriptVersionDir(scriptsDir: string): string | null {
1886
+ if (!fs.existsSync(scriptsDir)) return null;
1887
+
1888
+ const versions = fs.readdirSync(scriptsDir)
1889
+ .filter((d: string) => {
1890
+ try { return fs.statSync(path.join(scriptsDir, d)).isDirectory(); } catch { return false; }
1891
+ })
1892
+ .sort((a: string, b: string) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
1893
+
1894
+ if (versions.length === 0) return null;
1895
+ return path.join(scriptsDir, versions[0]);
1896
+ }
1897
+
1898
+ private resolveAutoImplWritableProviderDir(category: ProviderCategory, type: string, requestedDir?: string): string | null {
1899
+ const canonicalUserDir = path.resolve(this.providerLoader.getUserProviderDir(category, type));
1900
+ const desiredDir = requestedDir ? path.resolve(requestedDir) : canonicalUserDir;
1901
+
1902
+ if (desiredDir !== canonicalUserDir) {
1903
+ return null;
1904
+ }
1905
+
1906
+ const userRoot = path.resolve(this.providerLoader.getUserDir());
1907
+ if (desiredDir !== userRoot && !desiredDir.startsWith(`${userRoot}${path.sep}`)) {
1908
+ return null;
1909
+ }
1910
+
1911
+ const sourceDir = this.findProviderDir(type);
1912
+ if (!sourceDir) {
1913
+ return null;
1914
+ }
1915
+
1916
+ if (!fs.existsSync(desiredDir)) {
1917
+ fs.mkdirSync(path.dirname(desiredDir), { recursive: true });
1918
+ fs.cpSync(sourceDir, desiredDir, { recursive: true });
1919
+ this.log(`Auto-implement writable copy created: ${desiredDir}`);
1920
+ }
1921
+
1922
+ const providerJson = path.join(desiredDir, 'provider.json');
1923
+ if (!fs.existsSync(providerJson)) {
1924
+ return null;
1925
+ }
1926
+
1927
+ try {
1928
+ const providerData = JSON.parse(fs.readFileSync(providerJson, 'utf-8'));
1929
+ if (providerData.disableUpstream !== true) {
1930
+ providerData.disableUpstream = true;
1931
+ fs.writeFileSync(providerJson, JSON.stringify(providerData, null, 2));
1932
+ }
1933
+ } catch {
1934
+ return null;
1935
+ }
1936
+
1937
+ return desiredDir;
1938
+ }
1939
+
1940
+ private loadAutoImplReferenceScripts(referenceType: string | null): Record<string, string> {
1884
1941
  if (!referenceType) return {};
1885
1942
 
1886
- const refDir = this.providerLoader.getUpstreamProviderDir(category, referenceType);
1887
- if (!fs.existsSync(refDir)) return {};
1943
+ const refDir = this.findProviderDir(referenceType);
1944
+ if (!refDir || !fs.existsSync(refDir)) return {};
1888
1945
 
1889
1946
  const referenceScripts: Record<string, string> = {};
1890
1947
  const scriptsDir = path.join(refDir, 'scripts');
1891
- if (!fs.existsSync(scriptsDir)) return referenceScripts;
1948
+ const latestDir = this.getLatestScriptVersionDir(scriptsDir);
1949
+ if (!latestDir) return referenceScripts;
1892
1950
 
1893
- const versions = fs.readdirSync(scriptsDir).filter((d: string) => {
1894
- try { return fs.statSync(path.join(scriptsDir, d)).isDirectory(); } catch { return false; }
1895
- }).sort().reverse();
1896
- if (versions.length === 0) return referenceScripts;
1897
-
1898
- const latestDir = path.join(scriptsDir, versions[0]);
1899
1951
  for (const file of fs.readdirSync(latestDir)) {
1900
1952
  if (!file.endsWith('.js')) continue;
1901
1953
  try {
@@ -1909,7 +1961,7 @@ export class DevServer {
1909
1961
 
1910
1962
  private async handleAutoImplement(type: string, req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
1911
1963
  const body = await this.readBody(req);
1912
- const { agent = 'claude-cli', functions, reference = 'antigravity', model, comment } = body;
1964
+ const { agent = 'claude-cli', functions, reference, model, comment, providerDir: requestedProviderDir } = body;
1913
1965
  if (!functions || !Array.isArray(functions) || functions.length === 0) {
1914
1966
  this.json(res, 400, { error: 'functions[] is required (e.g. ["readChat", "sendMessage"])' });
1915
1967
  return;
@@ -1923,8 +1975,13 @@ export class DevServer {
1923
1975
  const provider = this.providerLoader.resolve(type);
1924
1976
  if (!provider) { this.json(res, 404, { error: `Provider not found: ${type}` }); return; }
1925
1977
 
1926
- const providerDir = this.findProviderDir(type);
1927
- if (!providerDir) { this.json(res, 404, { error: `Provider directory not found: ${type}` }); return; }
1978
+ const providerDir = this.resolveAutoImplWritableProviderDir(provider.category, type, requestedProviderDir);
1979
+ if (!providerDir) {
1980
+ this.json(res, 409, {
1981
+ error: `Auto-implement only writes to the canonical user provider directory for '${type}'.`,
1982
+ });
1983
+ return;
1984
+ }
1928
1985
 
1929
1986
  try {
1930
1987
  // 1. Collect DOM context
@@ -1952,7 +2009,7 @@ export class DevServer {
1952
2009
  }
1953
2010
  });
1954
2011
 
1955
- const referenceScripts = this.loadAutoImplReferenceScripts(provider.category, resolvedReference);
2012
+ const referenceScripts = this.loadAutoImplReferenceScripts(resolvedReference);
1956
2013
 
1957
2014
  // 3. Build the prompt
1958
2015
  const prompt = this.buildAutoImplPrompt(type, provider, providerDir, functions, domContext, referenceScripts, comment, resolvedReference);
@@ -2339,25 +2396,20 @@ export class DevServer {
2339
2396
  lines.push('');
2340
2397
 
2341
2398
  const scriptsDir = path.join(providerDir, 'scripts');
2342
- if (fs.existsSync(scriptsDir)) {
2343
- const versions = fs.readdirSync(scriptsDir).filter((d: string) => {
2344
- try { return fs.statSync(path.join(scriptsDir, d)).isDirectory(); } catch { return false; }
2345
- }).sort().reverse();
2346
- if (versions.length > 0) {
2347
- const vDir = path.join(scriptsDir, versions[0]);
2348
- lines.push(`Scripts version directory: \`${vDir}\``);
2349
- lines.push('');
2350
- for (const file of fs.readdirSync(vDir)) {
2351
- if (file.endsWith('.js')) {
2352
- try {
2353
- const content = fs.readFileSync(path.join(vDir, file), 'utf-8');
2354
- lines.push(`### \`${file}\``);
2355
- lines.push('```javascript');
2356
- lines.push(content);
2357
- lines.push('```');
2358
- lines.push('');
2359
- } catch { /* skip */ }
2360
- }
2399
+ const latestScriptsDir = this.getLatestScriptVersionDir(scriptsDir);
2400
+ if (latestScriptsDir) {
2401
+ lines.push(`Scripts version directory: \`${latestScriptsDir}\``);
2402
+ lines.push('');
2403
+ for (const file of fs.readdirSync(latestScriptsDir)) {
2404
+ if (file.endsWith('.js')) {
2405
+ try {
2406
+ const content = fs.readFileSync(path.join(latestScriptsDir, file), 'utf-8');
2407
+ lines.push(`### \`${file}\``);
2408
+ lines.push('```javascript');
2409
+ lines.push(content);
2410
+ lines.push('```');
2411
+ lines.push('');
2412
+ } catch { /* skip */ }
2361
2413
  }
2362
2414
  }
2363
2415
  }
@@ -2442,7 +2494,7 @@ export class DevServer {
2442
2494
  lines.push('## Rules');
2443
2495
  lines.push('1. **Scripts WITHOUT params** → IIFE: `(() => { ... })()`');
2444
2496
  lines.push('2. **Scripts WITH params** → arrow: `(params) => { ... }` — router calls `(${script})(${JSON.stringify(params)})`');
2445
- lines.push('3. Use CSS selectors from the DOM analysis above');
2497
+ lines.push('3. If live DOM analysis is included above, use it. Otherwise, discover selectors yourself via CDP before coding.');
2446
2498
  lines.push('4. Always wrap in try-catch, return `JSON.stringify(result)`');
2447
2499
  lines.push('5. Do NOT modify `scripts.js` router — only edit individual `*.js` files');
2448
2500
  lines.push('6. All scripts run in the browser (CDP evaluate) — use DOM APIs only');
@@ -2498,8 +2550,12 @@ export class DevServer {
2498
2550
  lines.push('');
2499
2551
 
2500
2552
  // ── DevConsole API for verification ──
2501
- lines.push('## YOU MUST EXPLORE THE DOM YOURSELF!');
2502
- lines.push('I have NOT provided you with the DOM snapshot. You MUST use your command-line tools to discover the IDE structure dynamically!');
2553
+ lines.push('## DOM Exploration');
2554
+ if (domContext) {
2555
+ lines.push('A lightweight DOM snapshot is included above, but you MUST still verify selectors yourself before finalizing the scripts.');
2556
+ } else {
2557
+ lines.push('No DOM snapshot is included here. You MUST use your command-line tools to discover the IDE structure dynamically.');
2558
+ }
2503
2559
  lines.push('');
2504
2560
  lines.push('### 1. Evaluate JS to explore IDE DOM');
2505
2561
  lines.push('Use cURL to run JavaScript inside the IDE:');
@@ -2604,26 +2660,21 @@ export class DevServer {
2604
2660
  lines.push('');
2605
2661
 
2606
2662
  const scriptsDir = path.join(providerDir, 'scripts');
2607
- if (fs.existsSync(scriptsDir)) {
2608
- const versions = fs.readdirSync(scriptsDir).filter((d: string) => {
2609
- try { return fs.statSync(path.join(scriptsDir, d)).isDirectory(); } catch { return false; }
2610
- }).sort().reverse();
2611
- if (versions.length > 0) {
2612
- const vDir = path.join(scriptsDir, versions[0]);
2613
- lines.push(`Scripts version directory: \`${vDir}\``);
2614
- lines.push('');
2615
- for (const file of fs.readdirSync(vDir)) {
2616
- if (!file.endsWith('.js')) continue;
2617
- try {
2618
- const content = fs.readFileSync(path.join(vDir, file), 'utf-8');
2619
- lines.push(`### \`${file}\``);
2620
- lines.push('```javascript');
2621
- lines.push(content);
2622
- lines.push('```');
2623
- lines.push('');
2624
- } catch {
2625
- // ignore
2626
- }
2663
+ const latestScriptsDir = this.getLatestScriptVersionDir(scriptsDir);
2664
+ if (latestScriptsDir) {
2665
+ lines.push(`Scripts version directory: \`${latestScriptsDir}\``);
2666
+ lines.push('');
2667
+ for (const file of fs.readdirSync(latestScriptsDir)) {
2668
+ if (!file.endsWith('.js')) continue;
2669
+ try {
2670
+ const content = fs.readFileSync(path.join(latestScriptsDir, file), 'utf-8');
2671
+ lines.push(`### \`${file}\``);
2672
+ lines.push('```javascript');
2673
+ lines.push(content);
2674
+ lines.push('```');
2675
+ lines.push('');
2676
+ } catch {
2677
+ // ignore
2627
2678
  }
2628
2679
  }
2629
2680
  }
package/src/index.ts CHANGED
@@ -123,5 +123,5 @@ export { getAIExtensions, installExtensions, launchIDE, isExtensionInstalled } f
123
123
  export type { ExtensionInfo as InstallerExtensionInfo } from './installer.js';
124
124
 
125
125
  // ── Boot / Lifecycle ──
126
- export { initDaemonComponents, shutdownDaemonComponents } from './boot/daemon-lifecycle.js';
127
- export type { DaemonInitConfig, DaemonComponents } from './boot/daemon-lifecycle.js';
126
+ export { initDaemonComponents, startDaemonDevSupport, shutdownDaemonComponents } from './boot/daemon-lifecycle.js';
127
+ export type { DaemonInitConfig, DaemonComponents, DaemonDevSupportOptions } from './boot/daemon-lifecycle.js';
@@ -16,6 +16,7 @@
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
18
  import * as os from 'os';
19
+ import * as chokidar from 'chokidar';
19
20
  import { registerIDEDefinition } from '../detection/ide-detector.js';
20
21
  import { LOG } from '../logging/logger.js';
21
22
  import { VersionArchive } from './version-archive.js';
@@ -29,11 +30,10 @@ import type {
29
30
 
30
31
  export class ProviderLoader {
31
32
  private providers = new Map<string, ProviderModule>();
32
- private builtinDirs: string[];
33
33
  private userDir: string;
34
34
  private upstreamDir: string;
35
35
  private disableUpstream: boolean;
36
- private watchers: fs.FSWatcher[] = [];
36
+ private watchers: any[] = [];
37
37
  private logFn: (msg: string) => void;
38
38
  private versionArchive: VersionArchive | null = null;
39
39
  private scriptsCache = new Map<string, Record<string, any>>();
@@ -47,28 +47,34 @@ export class ProviderLoader {
47
47
  private static readonly META_FILE = '.meta.json';
48
48
 
49
49
  constructor(options?: {
50
- builtinDir?: string | string[];
51
50
  userDir?: string;
52
51
  logFn?: (msg: string) => void;
53
52
  /** Disable upstream auto-download (for dev/testing/OSS) */
54
53
  disableUpstream?: boolean;
55
54
  }) {
56
- // Legacy builtin directories (no longer shipped; providers come from upstream auto-download)
57
- if (options?.builtinDir) {
58
- this.builtinDirs = Array.isArray(options.builtinDir) ? options.builtinDir : [options.builtinDir];
59
- } else {
60
- this.builtinDirs = [];
61
- }
62
- // Default directory for auto-downloads
55
+ this.logFn = options?.logFn || LOG.forComponent('Provider').asLogFn();
56
+
57
+ // Default directory for auto-downloads
63
58
  const defaultProvidersDir = path.join(os.homedir(), '.adhdev', 'providers');
64
59
 
65
- // User custom directory: ~/.adhdev/providers/ or custom via config
66
- this.userDir = options?.userDir || defaultProvidersDir;
60
+ if (options?.userDir) {
61
+ this.userDir = options.userDir;
62
+ this.log(`Config 'providerDir' applied: ${this.userDir}`);
63
+ } else {
64
+ // Local dev overrides: Auto-detect local adhdev-providers repo for speed
65
+ const localRepoPath = path.resolve(__dirname, '../../../../../adhdev-providers');
66
+ if (fs.existsSync(localRepoPath)) {
67
+ this.userDir = localRepoPath;
68
+ this.log(`Auto-detected local public repository: ${this.userDir} (Dev workspace speedup)`);
69
+ } else {
70
+ this.userDir = defaultProvidersDir;
71
+ this.log(`Using default user providers directory: ${this.userDir}`);
72
+ }
73
+ }
67
74
 
68
- // Upstream auto-download directory is always in the default location to avoid polluting custom dirs
75
+ // Upstream auto-download directory is always in the default location
69
76
  this.upstreamDir = path.join(defaultProvidersDir, '.upstream');
70
77
  this.disableUpstream = options?.disableUpstream ?? false;
71
- this.logFn = options?.logFn || LOG.forComponent('Provider').asLogFn();
72
78
  }
73
79
 
74
80
  private log(msg: string): void {
@@ -77,23 +83,9 @@ export class ProviderLoader {
77
83
 
78
84
  // ─── Public API ────────────────────────────────
79
85
 
80
- /**
81
- * Ordered builtin roots used for local fallback/reference data.
82
- */
83
- getBuiltinDirs(): string[] {
84
- return [...this.builtinDirs];
85
- }
86
-
87
- /**
88
- * Primary builtin root used for local scaffolding/reference flows.
89
- */
90
- getPrimaryBuiltinDir(): string {
91
- return this.builtinDirs[0];
92
- }
93
-
94
- /**
95
- * User override root (~/.adhdev/providers by default).
96
- */
86
+ /**
87
+ * User override root (~/.adhdev/providers by default).
88
+ */
97
89
  getUserDir(): string {
98
90
  return this.userDir;
99
91
  }
@@ -105,12 +97,12 @@ export class ProviderLoader {
105
97
  return this.upstreamDir;
106
98
  }
107
99
 
108
- /**
109
- * Provider search order for on-disk lookups.
110
- * Highest-priority editable overrides come first.
111
- */
100
+ /**
101
+ * Provider search order for on-disk lookups.
102
+ * Highest-priority editable overrides come first.
103
+ */
112
104
  getProviderRoots(): string[] {
113
- return [this.userDir, this.upstreamDir, ...this.builtinDirs];
105
+ return [this.userDir, this.upstreamDir];
114
106
  }
115
107
 
116
108
  /**
@@ -135,17 +127,9 @@ export class ProviderLoader {
135
127
  }
136
128
 
137
129
  /**
138
- * Canonical builtin directory for a provider.
130
+ * Find the on-disk directory for a provider by type.
131
+ * Search order: user override → upstream.
139
132
  */
140
- getBuiltinProviderDir(category: ProviderCategory, type: string): string {
141
- const builtinRoot = this.getPrimaryBuiltinDir();
142
- return builtinRoot ? this.getProviderDir(builtinRoot, category, type) : '';
143
- }
144
-
145
- /**
146
- * Find the on-disk directory for a provider by type.
147
- * Search order: user override → upstream → builtin fallback.
148
- */
149
133
  findProviderDir(type: string): string | null {
150
134
  return this.findProviderDirInternal(type);
151
135
  }
@@ -571,34 +555,42 @@ export class ProviderLoader {
571
555
 
572
556
  // Fallback: build from individual .js files
573
557
  const result = this.buildScriptWrappersFromDir(dir) as Record<string, any>;
574
- this.log(` [loadScriptsFromDir] ${type}: built wrappers from ${dir} (${Object.keys(result).length} scripts)`);
575
558
  this.scriptsCache.set(dir, result);
576
559
  return result;
577
560
  }
578
561
 
579
- /**
580
- * Hot-reload: start watching for file changes
581
- */
562
+ /**
563
+ * Hot-reload: start watching for file changes
564
+ */
582
565
  watch(): void {
583
566
  this.stopWatch();
584
567
  const watchDir = (dir: string) => {
585
568
  if (!fs.existsSync(dir)) {
586
- // Create directory if missing (so user can drop files)
587
569
  try { fs.mkdirSync(dir, { recursive: true }); } catch { return; }
588
570
  }
589
571
  try {
590
- const watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
591
- if (filename?.endsWith('.js') || filename?.endsWith('.json')) {
592
- this.log(`File changed: ${filename}, reloading...`);
593
- this.loadAll();
594
- }
572
+ const watcher = chokidar.watch(dir, {
573
+ ignored: /(^|[\/\\])\.\./, // ignore dotfiles
574
+ persistent: true,
575
+ ignoreInitial: true,
576
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
595
577
  });
578
+
579
+ const handleChange = (filePath: string) => {
580
+ if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
581
+ this.log(`File changed: ${path.basename(filePath)}, reloading...`);
582
+ this.reload();
583
+ }
584
+ };
585
+
586
+ watcher.on('add', handleChange).on('change', handleChange).on('unlink', handleChange);
587
+ watcher.on('error', (err: unknown) => this.log(`Watch error: ${(err as Error).message}`));
596
588
  this.watchers.push(watcher);
589
+ this.log(`Hot-reload watcher active: ${dir}`);
597
590
  } catch (e) {
598
591
  this.log(`Watch failed for ${dir}: ${(e as Error).message}`);
599
592
  }
600
593
  };
601
- this.builtinDirs.forEach(dir => watchDir(dir));
602
594
  watchDir(this.userDir);
603
595
  }
604
596
 
@@ -1096,8 +1088,8 @@ export class ProviderLoader {
1096
1088
  count++;
1097
1089
  // Identify source tier for debugging
1098
1090
  const source = d.startsWith(this.userDir) && !d.includes('.upstream')
1099
- ? 'user' : d.startsWith(this.upstreamDir) ? 'upstream' : 'builtin';
1100
- const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES builtin/upstream' : '';
1091
+ ? 'user' : 'upstream';
1092
+ const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES upstream' : '';
1101
1093
  this.log(` ${existed ? '🔄' : '✅'} ${mod.type} (${mod.category}) — ${mod.name} [${source}]${overrideWarning}`);
1102
1094
  }
1103
1095
  } catch (e) {