@adhdev/daemon-core 0.5.3

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 (217) hide show
  1. package/dist/index.d.ts +2662 -0
  2. package/dist/index.js +11341 -0
  3. package/dist/index.js.map +1 -0
  4. package/package.json +48 -0
  5. package/providers/_builtin/.github/workflows/generate-registry.yml +57 -0
  6. package/providers/_builtin/COMPATIBILITY.md +217 -0
  7. package/providers/_builtin/CONTRIBUTING.md +200 -0
  8. package/providers/_builtin/README.md +119 -0
  9. package/providers/_builtin/_helpers/index.js +188 -0
  10. package/providers/_builtin/acp/agentpool/provider.json +54 -0
  11. package/providers/_builtin/acp/amp/provider.json +52 -0
  12. package/providers/_builtin/acp/auggie/provider.json +57 -0
  13. package/providers/_builtin/acp/autodev/provider.json +54 -0
  14. package/providers/_builtin/acp/autohand/provider.json +52 -0
  15. package/providers/_builtin/acp/blackbox-ai/provider.json +54 -0
  16. package/providers/_builtin/acp/claude-agent/provider.json +57 -0
  17. package/providers/_builtin/acp/cline-acp/provider.json +54 -0
  18. package/providers/_builtin/acp/codebuddy/provider.json +54 -0
  19. package/providers/_builtin/acp/codex-cli/provider.json +57 -0
  20. package/providers/_builtin/acp/corust-agent/provider.json +52 -0
  21. package/providers/_builtin/acp/crow-cli/provider.json +54 -0
  22. package/providers/_builtin/acp/cursor-acp/provider.json +54 -0
  23. package/providers/_builtin/acp/deepagents/provider.json +52 -0
  24. package/providers/_builtin/acp/dimcode/provider.json +54 -0
  25. package/providers/_builtin/acp/docker-cagent/provider.json +57 -0
  26. package/providers/_builtin/acp/factory-droid/provider.json +60 -0
  27. package/providers/_builtin/acp/fast-agent/provider.json +52 -0
  28. package/providers/_builtin/acp/gemini-cli/provider.json +114 -0
  29. package/providers/_builtin/acp/github-copilot/provider.json +54 -0
  30. package/providers/_builtin/acp/goose/provider.json +57 -0
  31. package/providers/_builtin/acp/junie/provider.json +52 -0
  32. package/providers/_builtin/acp/kilo/provider.json +54 -0
  33. package/providers/_builtin/acp/kimi-cli/provider.json +57 -0
  34. package/providers/_builtin/acp/minion-code/provider.json +52 -0
  35. package/providers/_builtin/acp/mistral-vibe/provider.json +57 -0
  36. package/providers/_builtin/acp/nova/provider.json +54 -0
  37. package/providers/_builtin/acp/openclaw/provider.json +54 -0
  38. package/providers/_builtin/acp/opencode/provider.json +52 -0
  39. package/providers/_builtin/acp/openhands/provider.json +54 -0
  40. package/providers/_builtin/acp/pi-acp/provider.json +52 -0
  41. package/providers/_builtin/acp/qoder/provider.json +54 -0
  42. package/providers/_builtin/acp/qwen-code/provider.json +60 -0
  43. package/providers/_builtin/acp/stakpak/provider.json +54 -0
  44. package/providers/_builtin/acp/vtcode/provider.json +54 -0
  45. package/providers/_builtin/cli/claude-cli/provider.json +100 -0
  46. package/providers/_builtin/cli/codex-cli/provider.json +89 -0
  47. package/providers/_builtin/cli/gemini-cli/provider.json +93 -0
  48. package/providers/_builtin/docs/CDP_SELECTOR_GUIDE.md +370 -0
  49. package/providers/_builtin/docs/PROVIDER_GUIDE.md +916 -0
  50. package/providers/_builtin/extension/cline/provider.json +35 -0
  51. package/providers/_builtin/extension/cline/scripts/focus_editor.js +48 -0
  52. package/providers/_builtin/extension/cline/scripts/list_chats.js +100 -0
  53. package/providers/_builtin/extension/cline/scripts/list_models.js +43 -0
  54. package/providers/_builtin/extension/cline/scripts/list_modes.js +35 -0
  55. package/providers/_builtin/extension/cline/scripts/new_session.js +85 -0
  56. package/providers/_builtin/extension/cline/scripts/open_panel.js +25 -0
  57. package/providers/_builtin/extension/cline/scripts/read_chat.js +257 -0
  58. package/providers/_builtin/extension/cline/scripts/resolve_action.js +83 -0
  59. package/providers/_builtin/extension/cline/scripts/send_message.js +95 -0
  60. package/providers/_builtin/extension/cline/scripts/set_mode.js +36 -0
  61. package/providers/_builtin/extension/cline/scripts/set_model.js +36 -0
  62. package/providers/_builtin/extension/cline/scripts/switch_session.js +206 -0
  63. package/providers/_builtin/extension/cline/scripts.js +73 -0
  64. package/providers/_builtin/extension/roo-code/provider.json +35 -0
  65. package/providers/_builtin/extension/roo-code/scripts.js +659 -0
  66. package/providers/_builtin/ide/antigravity/provider.json +68 -0
  67. package/providers/_builtin/ide/antigravity/scripts/1.106/focus_editor.js +20 -0
  68. package/providers/_builtin/ide/antigravity/scripts/1.106/list_chats.js +137 -0
  69. package/providers/_builtin/ide/antigravity/scripts/1.106/list_models.js +38 -0
  70. package/providers/_builtin/ide/antigravity/scripts/1.106/list_modes.js +48 -0
  71. package/providers/_builtin/ide/antigravity/scripts/1.106/new_session.js +75 -0
  72. package/providers/_builtin/ide/antigravity/scripts/1.106/read_chat.js +262 -0
  73. package/providers/_builtin/ide/antigravity/scripts/1.106/resolve_action.js +68 -0
  74. package/providers/_builtin/ide/antigravity/scripts/1.106/scripts.js +57 -0
  75. package/providers/_builtin/ide/antigravity/scripts/1.106/send_message.js +56 -0
  76. package/providers/_builtin/ide/antigravity/scripts/1.106/set_mode.js +34 -0
  77. package/providers/_builtin/ide/antigravity/scripts/1.106/set_model.js +47 -0
  78. package/providers/_builtin/ide/antigravity/scripts/1.106/switch_session.js +114 -0
  79. package/providers/_builtin/ide/antigravity/scripts/1.107/focus_editor.js +20 -0
  80. package/providers/_builtin/ide/antigravity/scripts/1.107/list_chats.js +137 -0
  81. package/providers/_builtin/ide/antigravity/scripts/1.107/list_models.js +61 -0
  82. package/providers/_builtin/ide/antigravity/scripts/1.107/list_modes.js +72 -0
  83. package/providers/_builtin/ide/antigravity/scripts/1.107/new_session.js +75 -0
  84. package/providers/_builtin/ide/antigravity/scripts/1.107/read_chat.js +262 -0
  85. package/providers/_builtin/ide/antigravity/scripts/1.107/resolve_action.js +68 -0
  86. package/providers/_builtin/ide/antigravity/scripts/1.107/scripts.js +67 -0
  87. package/providers/_builtin/ide/antigravity/scripts/1.107/send_message.js +56 -0
  88. package/providers/_builtin/ide/antigravity/scripts/1.107/set_mode.js +67 -0
  89. package/providers/_builtin/ide/antigravity/scripts/1.107/set_model.js +72 -0
  90. package/providers/_builtin/ide/antigravity/scripts/1.107/switch_session.js +114 -0
  91. package/providers/_builtin/ide/cursor/provider.json +70 -0
  92. package/providers/_builtin/ide/cursor/scripts/0.49/dismiss_notification.js +30 -0
  93. package/providers/_builtin/ide/cursor/scripts/0.49/focus_editor.js +13 -0
  94. package/providers/_builtin/ide/cursor/scripts/0.49/list_models.js +78 -0
  95. package/providers/_builtin/ide/cursor/scripts/0.49/list_modes.js +40 -0
  96. package/providers/_builtin/ide/cursor/scripts/0.49/list_notifications.js +23 -0
  97. package/providers/_builtin/ide/cursor/scripts/0.49/list_sessions.js +42 -0
  98. package/providers/_builtin/ide/cursor/scripts/0.49/new_session.js +20 -0
  99. package/providers/_builtin/ide/cursor/scripts/0.49/open_panel.js +23 -0
  100. package/providers/_builtin/ide/cursor/scripts/0.49/read_chat.js +75 -0
  101. package/providers/_builtin/ide/cursor/scripts/0.49/resolve_action.js +19 -0
  102. package/providers/_builtin/ide/cursor/scripts/0.49/scripts.js +78 -0
  103. package/providers/_builtin/ide/cursor/scripts/0.49/send_message.js +23 -0
  104. package/providers/_builtin/ide/cursor/scripts/0.49/set_mode.js +38 -0
  105. package/providers/_builtin/ide/cursor/scripts/0.49/set_model.js +81 -0
  106. package/providers/_builtin/ide/cursor/scripts/0.49/switch_session.js +28 -0
  107. package/providers/_builtin/ide/kiro/provider.json +67 -0
  108. package/providers/_builtin/ide/kiro/scripts/focus_editor.js +20 -0
  109. package/providers/_builtin/ide/kiro/scripts/open_panel.js +47 -0
  110. package/providers/_builtin/ide/kiro/scripts/resolve_action.js +54 -0
  111. package/providers/_builtin/ide/kiro/scripts/send_message.js +29 -0
  112. package/providers/_builtin/ide/kiro/scripts/webview_list_models.js +39 -0
  113. package/providers/_builtin/ide/kiro/scripts/webview_list_modes.js +39 -0
  114. package/providers/_builtin/ide/kiro/scripts/webview_list_sessions.js +21 -0
  115. package/providers/_builtin/ide/kiro/scripts/webview_new_session.js +34 -0
  116. package/providers/_builtin/ide/kiro/scripts/webview_read_chat.js +68 -0
  117. package/providers/_builtin/ide/kiro/scripts/webview_send_message.js +72 -0
  118. package/providers/_builtin/ide/kiro/scripts/webview_set_mode.js +15 -0
  119. package/providers/_builtin/ide/kiro/scripts/webview_set_model.js +15 -0
  120. package/providers/_builtin/ide/kiro/scripts/webview_switch_session.js +26 -0
  121. package/providers/_builtin/ide/kiro/scripts.js +62 -0
  122. package/providers/_builtin/ide/pearai/provider.json +67 -0
  123. package/providers/_builtin/ide/pearai/scripts/focus_editor.js +20 -0
  124. package/providers/_builtin/ide/pearai/scripts/list_sessions.js +38 -0
  125. package/providers/_builtin/ide/pearai/scripts/new_session.js +55 -0
  126. package/providers/_builtin/ide/pearai/scripts/open_panel.js +46 -0
  127. package/providers/_builtin/ide/pearai/scripts/resolve_action.js +54 -0
  128. package/providers/_builtin/ide/pearai/scripts/send_message.js +29 -0
  129. package/providers/_builtin/ide/pearai/scripts/webview_list_models.js +43 -0
  130. package/providers/_builtin/ide/pearai/scripts/webview_list_modes.js +35 -0
  131. package/providers/_builtin/ide/pearai/scripts/webview_list_sessions.js +62 -0
  132. package/providers/_builtin/ide/pearai/scripts/webview_new_session.js +49 -0
  133. package/providers/_builtin/ide/pearai/scripts/webview_read_chat.js +92 -0
  134. package/providers/_builtin/ide/pearai/scripts/webview_resolve_action.js +59 -0
  135. package/providers/_builtin/ide/pearai/scripts/webview_send_message.js +72 -0
  136. package/providers/_builtin/ide/pearai/scripts/webview_set_mode.js +36 -0
  137. package/providers/_builtin/ide/pearai/scripts/webview_set_model.js +36 -0
  138. package/providers/_builtin/ide/pearai/scripts/webview_switch_session.js +34 -0
  139. package/providers/_builtin/ide/pearai/scripts.js +74 -0
  140. package/providers/_builtin/ide/trae/provider.json +66 -0
  141. package/providers/_builtin/ide/trae/scripts/focus_editor.js +20 -0
  142. package/providers/_builtin/ide/trae/scripts/list_chats.js +24 -0
  143. package/providers/_builtin/ide/trae/scripts/list_models.js +39 -0
  144. package/providers/_builtin/ide/trae/scripts/list_modes.js +39 -0
  145. package/providers/_builtin/ide/trae/scripts/new_session.js +30 -0
  146. package/providers/_builtin/ide/trae/scripts/open_panel.js +44 -0
  147. package/providers/_builtin/ide/trae/scripts/read_chat.js +113 -0
  148. package/providers/_builtin/ide/trae/scripts/resolve_action.js +54 -0
  149. package/providers/_builtin/ide/trae/scripts/send_message.js +69 -0
  150. package/providers/_builtin/ide/trae/scripts/set_mode.js +15 -0
  151. package/providers/_builtin/ide/trae/scripts/set_model.js +15 -0
  152. package/providers/_builtin/ide/trae/scripts/switch_session.js +23 -0
  153. package/providers/_builtin/ide/trae/scripts.js +57 -0
  154. package/providers/_builtin/ide/vscode/provider.json +64 -0
  155. package/providers/_builtin/ide/vscode-insiders/provider.json +62 -0
  156. package/providers/_builtin/ide/vscodium/provider.json +63 -0
  157. package/providers/_builtin/ide/windsurf/provider.json +53 -0
  158. package/providers/_builtin/ide/windsurf/scripts/focus_editor.js +30 -0
  159. package/providers/_builtin/ide/windsurf/scripts/list_chats.js +117 -0
  160. package/providers/_builtin/ide/windsurf/scripts/list_models.js +39 -0
  161. package/providers/_builtin/ide/windsurf/scripts/list_modes.js +39 -0
  162. package/providers/_builtin/ide/windsurf/scripts/new_session.js +69 -0
  163. package/providers/_builtin/ide/windsurf/scripts/open_panel.js +58 -0
  164. package/providers/_builtin/ide/windsurf/scripts/read_chat.js +297 -0
  165. package/providers/_builtin/ide/windsurf/scripts/resolve_action.js +68 -0
  166. package/providers/_builtin/ide/windsurf/scripts/send_message.js +87 -0
  167. package/providers/_builtin/ide/windsurf/scripts/set_mode.js +15 -0
  168. package/providers/_builtin/ide/windsurf/scripts/set_model.js +15 -0
  169. package/providers/_builtin/ide/windsurf/scripts/switch_session.js +58 -0
  170. package/providers/_builtin/ide/windsurf/scripts.js +57 -0
  171. package/providers/_builtin/registry.json +266 -0
  172. package/providers/_builtin/validate.js +156 -0
  173. package/src/agent-stream/index.ts +6 -0
  174. package/src/agent-stream/manager.ts +286 -0
  175. package/src/agent-stream/poller.ts +154 -0
  176. package/src/agent-stream/provider-adapter.ts +138 -0
  177. package/src/agent-stream/types.ts +61 -0
  178. package/src/boot/daemon-lifecycle.ts +252 -0
  179. package/src/cdp/devtools.ts +335 -0
  180. package/src/cdp/initializer.ts +191 -0
  181. package/src/cdp/manager.ts +897 -0
  182. package/src/cdp/scanner.ts +185 -0
  183. package/src/cdp/setup.ts +150 -0
  184. package/src/cli-adapter-types.ts +25 -0
  185. package/src/cli-adapters/provider-cli-adapter.ts +448 -0
  186. package/src/commands/cdp-commands.ts +208 -0
  187. package/src/commands/chat-commands.ts +675 -0
  188. package/src/commands/cli-manager.ts +353 -0
  189. package/src/commands/handler.ts +328 -0
  190. package/src/commands/router.ts +258 -0
  191. package/src/commands/stream-commands.ts +325 -0
  192. package/src/config/chat-history.ts +211 -0
  193. package/src/config/config.ts +219 -0
  194. package/src/daemon/dev-server.ts +2378 -0
  195. package/src/daemon/scaffold-template.ts +394 -0
  196. package/src/daemon-core.ts +50 -0
  197. package/src/detection/cli-detector.ts +89 -0
  198. package/src/detection/ide-detector.ts +157 -0
  199. package/src/index.ts +103 -0
  200. package/src/installer.ts +263 -0
  201. package/src/ipc-protocol.ts +133 -0
  202. package/src/launch.ts +433 -0
  203. package/src/logging/command-log.ts +180 -0
  204. package/src/logging/logger.ts +316 -0
  205. package/src/providers/acp-provider-instance.ts +1140 -0
  206. package/src/providers/cli-provider-instance.ts +207 -0
  207. package/src/providers/contracts.ts +524 -0
  208. package/src/providers/extension-provider-instance.ts +156 -0
  209. package/src/providers/ide-provider-instance.ts +377 -0
  210. package/src/providers/index.ts +18 -0
  211. package/src/providers/provider-instance-manager.ts +182 -0
  212. package/src/providers/provider-instance.ts +112 -0
  213. package/src/providers/provider-loader.ts +1031 -0
  214. package/src/providers/status-monitor.ts +125 -0
  215. package/src/providers/version-archive.ts +266 -0
  216. package/src/status/reporter.ts +294 -0
  217. package/src/types.ts +206 -0
@@ -0,0 +1,1031 @@
1
+ /**
2
+ * ProviderLoader — Provider discovery + OS/version override resolution
3
+ *
4
+ * Role:
5
+ * 1. Load provider.js from _builtin/ directory
6
+ * 2. Load user custom from ~/.adhdev/providers/ (overrides)
7
+ * 3. Apply OS/version overrides (process.platform + detected IDE version)
8
+ * 4. Hot-reload support (fs.watch)
9
+ *
10
+ * Design principles:
11
+ * - Load JS files via require() (CJS compatible)
12
+ * - User custom can override builtin
13
+ * - provider.js files are independent, so load order doesn't matter
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+ import * as os from 'os';
19
+ import { registerIDEDefinition } from '../detection/ide-detector.js';
20
+ import { LOG } from '../logging/logger.js';
21
+ import { VersionArchive } from './version-archive.js';
22
+ import type {
23
+ ProviderModule,
24
+ ProviderCategory,
25
+ ProviderScripts,
26
+ ProviderSettingSchema,
27
+ ResolvedProvider,
28
+ } from './contracts.js';
29
+
30
+ export class ProviderLoader {
31
+ private providers = new Map<string, ProviderModule>();
32
+ private builtinDirs: string[];
33
+ private userDir: string;
34
+ private upstreamDir: string;
35
+ private watchers: fs.FSWatcher[] = [];
36
+ private logFn: (msg: string) => void;
37
+ private versionArchive: VersionArchive | null = null;
38
+ private scriptsCache = new Map<string, Record<string, any>>();
39
+
40
+ /** Inject VersionArchive so resolve() can auto-detect installed versions */
41
+ setVersionArchive(archive: VersionArchive): void {
42
+ this.versionArchive = archive;
43
+ }
44
+
45
+ private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
46
+ private static readonly META_FILE = '.meta.json';
47
+
48
+ constructor(options?: {
49
+ builtinDir?: string | string[];
50
+ userDir?: string;
51
+ logFn?: (msg: string) => void;
52
+ }) {
53
+ // Builtin directories: providers/_builtin/
54
+ if (options?.builtinDir) {
55
+ this.builtinDirs = Array.isArray(options.builtinDir) ? options.builtinDir : [options.builtinDir];
56
+ } else {
57
+ this.builtinDirs = [path.resolve(__dirname, '../providers/_builtin')];
58
+ }
59
+ // User custom directory: ~/.adhdev/providers/
60
+ this.userDir = options?.userDir ||
61
+ path.join(os.homedir(), '.adhdev', 'providers');
62
+ // Upstream auto-download directory: ~/.adhdev/providers/.upstream/
63
+ this.upstreamDir = path.join(this.userDir, '.upstream');
64
+ this.logFn = options?.logFn || LOG.forComponent('Provider').asLogFn();
65
+ }
66
+
67
+ private log(msg: string): void {
68
+ this.logFn(`[ProviderLoader] ${msg}`);
69
+ }
70
+
71
+ // ─── Public API ────────────────────────────────
72
+
73
+ /**
74
+ * Load all providers (3-tier priority)
75
+ * 1. _builtin/ (bundled fallback, or multiple array dirs)
76
+ * 2. .upstream/ (GitHub auto-download)
77
+ * 3. User custom (~/.adhdev/providers/ excluding _upstream)
78
+ * Later loads override earlier ones, so user custom always wins.
79
+ */
80
+ loadAll(): void {
81
+ this.providers.clear();
82
+
83
+ // 1. Load builtin (npm package bundle — lowest priority)
84
+ let builtinCount = 0;
85
+ for (const dir of this.builtinDirs) {
86
+ if (fs.existsSync(dir)) {
87
+ builtinCount += this.loadDir(dir);
88
+ }
89
+ }
90
+ this.log(`Loaded ${builtinCount} builtin providers`);
91
+
92
+ // 2. Load upstream (GitHub auto-download — overrides builtin)
93
+ let upstreamCount = 0;
94
+ if (fs.existsSync(this.upstreamDir)) {
95
+ upstreamCount = this.loadDir(this.upstreamDir);
96
+ if (upstreamCount > 0) {
97
+ this.log(`Loaded ${upstreamCount} upstream providers (auto-updated)`);
98
+ }
99
+ }
100
+
101
+ // 3. Load user custom (excluding _upstream — highest priority, never auto-updated)
102
+ if (fs.existsSync(this.userDir)) {
103
+ const userCount = this.loadDir(this.userDir, ['.upstream']);
104
+ if (userCount > 0) {
105
+ this.log(`Loaded ${userCount} user custom providers (never auto-updated)`);
106
+ }
107
+ }
108
+
109
+ this.log(`Total: ${this.providers.size} providers [${[...this.providers.keys()].join(', ')}]`);
110
+
111
+ // ⚠️ Warning: using builtin fallback only, upstream not available
112
+ if (upstreamCount === 0 && builtinCount > 0) {
113
+ this.log(`⚠ Using bundled providers only (upstream not available). Run 'adhdev daemon' with internet to auto-update.`);
114
+ }
115
+
116
+ // ❌ Error: no providers found anywhere
117
+ if (this.providers.size === 0) {
118
+ this.log(`❌ No providers loaded! Check builtinDirs.`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Get raw provider metadata by type (NO scripts loaded).
124
+ * Use resolve() when you need scripts (readChat, listModels, etc).
125
+ * @deprecated Use getMeta() for metadata or resolve() for scripts.
126
+ */
127
+ get(type: string): ProviderModule | undefined {
128
+ return this.providers.get(type);
129
+ }
130
+
131
+ /**
132
+ * Get raw provider metadata by type (NO scripts loaded).
133
+ * Safe for: category checks, icon, displayName, targetFilter, cdpPorts.
134
+ * NOT safe for: script execution (readChat, listModels, sendMessage).
135
+ * Use resolve() when scripts are needed.
136
+ */
137
+ getMeta(type: string): ProviderModule | undefined {
138
+ return this.providers.get(type);
139
+ }
140
+
141
+ /**
142
+ * Resolve provider type by alias
143
+ * 'claude' → 'claude-cli', 'codex' → 'codex-cli' etc
144
+ * Returns input as-is if no match found.
145
+ */
146
+ resolveAlias(input: string): string {
147
+ // 1. directly match
148
+ if (this.providers.has(input)) return input;
149
+ // 2. alias match
150
+ for (const p of this.providers.values()) {
151
+ if (p.aliases?.includes(input)) return p.type;
152
+ }
153
+ return input;
154
+ }
155
+
156
+ /**
157
+ * Get provider with alias resolution (get + alias fallback)
158
+ */
159
+ getByAlias(input: string): ProviderModule | undefined {
160
+ return this.providers.get(this.resolveAlias(input));
161
+ }
162
+
163
+ /**
164
+ * Build CLI/ACP detection list (replaces cli-detector)
165
+ * Dynamically generated from provider.js spawn.command.
166
+ */
167
+ getCliDetectionList(): { id: string; displayName: string; icon: string; command: string; category: string }[] {
168
+ const result: { id: string; displayName: string; icon: string; command: string; category: string }[] = [];
169
+ for (const p of this.providers.values()) {
170
+ if ((p.category === 'cli' || p.category === 'acp') && p.spawn?.command) {
171
+ result.push({
172
+ id: p.type,
173
+ displayName: p.displayName || p.name,
174
+ icon: p.icon || '🔧',
175
+ command: p.spawn.command,
176
+ category: p.category,
177
+ });
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * List providers by category
185
+ */
186
+ getByCategory(cat: ProviderCategory): ProviderModule[] {
187
+ return [...this.providers.values()].filter(p => p.category === cat);
188
+ }
189
+
190
+ /**
191
+ * Extension Extension providers with extensionIdPattern only
192
+ * (used by discoverAgentWebviews in daemon-cdp.ts)
193
+ */
194
+ getExtensionProviders(): ProviderModule[] {
195
+ return [...this.providers.values()].filter(
196
+ p => p.category === 'extension' && p.extensionIdPattern
197
+ );
198
+ }
199
+
200
+ /**
201
+ * All loaded providers
202
+ */
203
+ getAll(): ProviderModule[] {
204
+ return [...this.providers.values()];
205
+ }
206
+
207
+ /**
208
+ * Check if a provider is enabled (per-IDE)
209
+ * Checks ideSettings[ideType].extensions[type].enabled.
210
+ * Default false (disabled) — user must explicitly enable.
211
+ * Always returns true when called without ideType.
212
+ */
213
+ isEnabled(type: string, ideType?: string): boolean {
214
+ if (!ideType) return true;
215
+ try {
216
+ const { loadConfig } = require('../config/config.js');
217
+ const config = loadConfig();
218
+ const val = config.ideSettings?.[ideType]?.extensions?.[type]?.enabled;
219
+ return val === true; // undefined → false (default inactive)
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Save IDE extension enabled setting
227
+ */
228
+ setIdeExtensionEnabled(ideType: string, extensionType: string, enabled: boolean): boolean {
229
+ try {
230
+ const { loadConfig, saveConfig } = require('../config/config.js');
231
+ const config = loadConfig();
232
+ if (!config.ideSettings) config.ideSettings = {};
233
+ if (!config.ideSettings[ideType]) config.ideSettings[ideType] = {};
234
+ if (!config.ideSettings[ideType].extensions) config.ideSettings[ideType].extensions = {};
235
+ config.ideSettings[ideType].extensions[extensionType] = { enabled };
236
+ saveConfig(config);
237
+ this.log(`IDE extension setting: ${ideType}.${extensionType}.enabled = ${enabled}`);
238
+ return true;
239
+ } catch (e) {
240
+ this.log(`Failed to save IDE extension setting: ${(e as Error).message}`);
241
+ return false;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Return only enabled providers by category (per-IDE)
247
+ */
248
+ getEnabledByCategory(cat: ProviderCategory, ideType?: string): ProviderModule[] {
249
+ return this.getByCategory(cat).filter(p => this.isEnabled(p.type, ideType));
250
+ }
251
+
252
+ /**
253
+ * Extension Enabled extension providers with extensionIdPattern only (per-IDE)
254
+ */
255
+ getEnabledExtensionProviders(ideType?: string): ProviderModule[] {
256
+ return this.getExtensionProviders().filter(p => this.isEnabled(p.type, ideType));
257
+ }
258
+
259
+ /**
260
+ * Return CDP port map for IDE providers
261
+ * Used by launch.ts, adhdev-daemon.ts
262
+ */
263
+ getCdpPortMap(): Record<string, [number, number]> {
264
+ const map: Record<string, [number, number]> = {};
265
+ for (const p of this.providers.values()) {
266
+ if (p.category === 'ide' && p.cdpPorts) {
267
+ map[p.type] = p.cdpPorts as [number, number];
268
+ }
269
+ }
270
+ return map;
271
+ }
272
+
273
+ /**
274
+ * Return IDE process name map (macOS)
275
+ */
276
+ getMacAppIdentifiers(): Record<string, string> {
277
+ const map: Record<string, string> = {};
278
+ for (const p of this.providers.values()) {
279
+ if (p.category === 'ide' && p.processNames?.darwin) {
280
+ map[p.type] = p.processNames.darwin as string;
281
+ }
282
+ }
283
+ return map;
284
+ }
285
+
286
+ /**
287
+ * Return IDE process name map (Windows)
288
+ */
289
+ getWinProcessNames(): Record<string, string[]> {
290
+ const map: Record<string, string[]> = {};
291
+ for (const p of this.providers.values()) {
292
+ if (p.category === 'ide' && p.processNames?.win32) {
293
+ map[p.type] = p.processNames.win32 as string[];
294
+ }
295
+ }
296
+ return map;
297
+ }
298
+
299
+ /**
300
+ * Available IDE types (only those with cdpPorts)
301
+ */
302
+ getAvailableIdeTypes(): string[] {
303
+ return [...this.providers.values()]
304
+ .filter(p => p.category === 'ide' && p.cdpPorts)
305
+ .map(p => p.type);
306
+ }
307
+
308
+ /**
309
+ * Register IDE providers to core/detector registry
310
+ * → Enables detectIDEs() to detect provider.js-based IDEs
311
+ */
312
+ registerToDetector(): number {
313
+ let count = 0;
314
+ for (const p of this.providers.values()) {
315
+ if (p.category === 'ide' && p.cli && p.paths) {
316
+ registerIDEDefinition({
317
+ id: p.type,
318
+ name: p.name,
319
+ displayName: p.displayName || p.name,
320
+ icon: p.icon || '💻',
321
+ extensionSupport: 'full',
322
+ cli: p.cli,
323
+ paths: p.paths as { darwin?: string[]; win32?: string[]; linux?: string[] },
324
+ });
325
+ count++;
326
+ }
327
+ }
328
+ this.log(`Registered ${count} IDE providers to detector`);
329
+ return count;
330
+ }
331
+
332
+ /**
333
+ * Return final provider with OS/version overrides applied.
334
+ *
335
+ * Script resolution order:
336
+ * 1. compatibility array (new format — preferred)
337
+ * Provider.json defines: "compatibility": [{ "ideVersion": ">=1.107.0", "scriptDir": "scripts/1.107" }]
338
+ * First matching range wins. Fallback: defaultScriptDir.
339
+ * 2. versions field (legacy format — backward compat)
340
+ * "versions": { "< 1.107.0": { "__dir": "scripts/legacy" } }
341
+ * 3. Root scripts.js (original format — no versioning)
342
+ *
343
+ * Version source: context.version → VersionArchive → undefined
344
+ */
345
+ resolve(type: string, context?: { os?: string; version?: string }): ResolvedProvider | undefined {
346
+ const base = this.providers.get(type);
347
+ if (!base) return undefined;
348
+
349
+ const currentOs = context?.os || process.platform;
350
+ const currentVersion = context?.version ??
351
+ this.versionArchive?.getLatest(type) ??
352
+ undefined;
353
+
354
+ // Deep clone to avoid mutating the original
355
+ const resolved: ResolvedProvider = JSON.parse(JSON.stringify(base));
356
+ // Restore RegExp from original (lost during JSON.parse)
357
+ if (base.extensionIdPattern) {
358
+ resolved.extensionIdPattern = base.extensionIdPattern;
359
+ }
360
+ // Restore script functions (lost during JSON.parse)
361
+ if (base.scripts) {
362
+ resolved.scripts = { ...base.scripts };
363
+ }
364
+
365
+ // 1. Apply OS override
366
+ if (base.os?.[currentOs]) {
367
+ const osOverride = base.os[currentOs];
368
+ if (osOverride.scripts) {
369
+ resolved.scripts = { ...resolved.scripts, ...osOverride.scripts };
370
+ }
371
+ if (osOverride.inputMethod) resolved.inputMethod = osOverride.inputMethod;
372
+ if (osOverride.inputSelector) resolved.inputSelector = osOverride.inputSelector;
373
+ resolved._resolvedOs = currentOs;
374
+ }
375
+
376
+ // 2. Apply version-based script selection
377
+ if (currentVersion) {
378
+ resolved._resolvedVersion = currentVersion;
379
+
380
+ // --- New format: compatibility array ---
381
+ if (Array.isArray((base as any).compatibility)) {
382
+ const compat = (base as any).compatibility as { ideVersion: string; scriptDir: string }[];
383
+ let matched = false;
384
+
385
+ for (const entry of compat) {
386
+ if (this.matchesVersion(currentVersion, entry.ideVersion)) {
387
+ const loaded = this.loadScriptsFromDir(type, entry.scriptDir);
388
+ if (loaded) {
389
+ resolved.scripts = loaded;
390
+ this.log(` [compatibility] ${type} v${currentVersion} → ${entry.scriptDir}`);
391
+ matched = true;
392
+ }
393
+ break; // first match wins
394
+ }
395
+ }
396
+
397
+ // No compatibility match → defaultScriptDir
398
+ if (!matched && (base as any).defaultScriptDir) {
399
+ const loaded = this.loadScriptsFromDir(type, (base as any).defaultScriptDir);
400
+ if (loaded) {
401
+ resolved.scripts = loaded;
402
+ this.log(` [compatibility] ${type} v${currentVersion} → default: ${(base as any).defaultScriptDir}`);
403
+ }
404
+ resolved._versionWarning = `Version ${currentVersion} not in compatibility matrix. Using default scripts.`;
405
+ }
406
+
407
+ // --- Legacy format: versions field ---
408
+ } else if (base.versions) {
409
+ for (const [range, override] of Object.entries(base.versions)) {
410
+ if (!this.matchesVersion(currentVersion, range)) continue;
411
+
412
+ const dirOverride = (override as any).__dir as string | undefined;
413
+ if (dirOverride) {
414
+ const loaded = this.loadScriptsFromDir(type, dirOverride);
415
+ if (loaded) {
416
+ resolved.scripts = loaded;
417
+ this.log(` [version override] ${type} ${range} → ${dirOverride}`);
418
+ }
419
+ } else if (override.scripts) {
420
+ resolved.scripts = { ...resolved.scripts, ...override.scripts };
421
+ }
422
+ }
423
+ }
424
+ } else if (Array.isArray((base as any).compatibility) && (base as any).defaultScriptDir) {
425
+ // No version detected but compatibility format → use defaultScriptDir
426
+ const loaded = this.loadScriptsFromDir(type, (base as any).defaultScriptDir);
427
+ if (loaded) {
428
+ resolved.scripts = loaded;
429
+ this.log(` [compatibility] ${type} no version detected → default: ${(base as any).defaultScriptDir}`);
430
+ }
431
+ }
432
+
433
+ // 3. Composite override (OS + version)
434
+ if (base.overrides) {
435
+ for (const override of base.overrides) {
436
+ const osMatch = !override.when.os || override.when.os === currentOs;
437
+ const verMatch = !override.when.version || (currentVersion && this.matchesVersion(currentVersion, override.when.version));
438
+ if (osMatch && verMatch && override.scripts) {
439
+ resolved.scripts = { ...resolved.scripts, ...override.scripts };
440
+ }
441
+ }
442
+ }
443
+
444
+ return resolved;
445
+ }
446
+
447
+ /**
448
+ * Load scripts from a scriptDir within a provider directory.
449
+ * Tries scripts.js first, then individual .js files.
450
+ */
451
+ private loadScriptsFromDir(type: string, scriptDir: string): Record<string, any> | null {
452
+ const providerDir = this.findProviderDir(type);
453
+ if (!providerDir) {
454
+ this.log(` [loadScriptsFromDir] ${type}: providerDir not found`);
455
+ return null;
456
+ }
457
+
458
+ const dir = path.join(providerDir, scriptDir);
459
+ if (!fs.existsSync(dir)) {
460
+ this.log(` [loadScriptsFromDir] ${type}: dir not found: ${dir}`);
461
+ return null;
462
+ }
463
+
464
+ // Return cached scripts if available (cleared on reload/watch)
465
+ const cached = this.scriptsCache.get(dir);
466
+ if (cached) return cached;
467
+
468
+ // Try scripts.js first
469
+ const scriptsJs = path.join(dir, 'scripts.js');
470
+ if (fs.existsSync(scriptsJs)) {
471
+ try {
472
+ delete require.cache[require.resolve(scriptsJs)];
473
+ const loaded = require(scriptsJs);
474
+ this.log(` [loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
475
+ this.scriptsCache.set(dir, loaded);
476
+ return loaded;
477
+ } catch (e) {
478
+ this.log(` ⚠ scripts.js load failed: ${scriptsJs}: ${(e as Error).message}`);
479
+ }
480
+ }
481
+
482
+ // Fallback: build from individual .js files
483
+ const result = this.buildScriptWrappersFromDir(dir) as Record<string, any>;
484
+ this.log(` [loadScriptsFromDir] ${type}: built wrappers from ${dir} (${Object.keys(result).length} scripts)`);
485
+ this.scriptsCache.set(dir, result);
486
+ return result;
487
+ }
488
+
489
+ /**
490
+ * Hot-reload: start watching for file changes
491
+ */
492
+ watch(): void {
493
+ this.stopWatch();
494
+ const watchDir = (dir: string) => {
495
+ if (!fs.existsSync(dir)) {
496
+ // Create directory if missing (so user can drop files)
497
+ try { fs.mkdirSync(dir, { recursive: true }); } catch { return; }
498
+ }
499
+ try {
500
+ const watcher = fs.watch(dir, { recursive: true }, (event, filename) => {
501
+ if (filename?.endsWith('.js') || filename?.endsWith('.json')) {
502
+ this.log(`File changed: ${filename}, reloading...`);
503
+ this.loadAll();
504
+ }
505
+ });
506
+ this.watchers.push(watcher);
507
+ } catch (e) {
508
+ this.log(`Watch failed for ${dir}: ${(e as Error).message}`);
509
+ }
510
+ };
511
+ this.builtinDirs.forEach(dir => watchDir(dir));
512
+ watchDir(this.userDir);
513
+ }
514
+
515
+ /**
516
+ * Stop hot-reload
517
+ */
518
+ stopWatch(): void {
519
+ for (const w of this.watchers) {
520
+ try { w.close(); } catch { }
521
+ }
522
+ this.watchers = [];
523
+ }
524
+
525
+ /**
526
+ * Full reload
527
+ */
528
+ reload(): void {
529
+ this.log('Reloading all providers...');
530
+ // Clear caches
531
+ this.scriptsCache.clear();
532
+ // Clear require cache (hot-reload)
533
+ for (const key of Object.keys(require.cache)) {
534
+ if (key.includes('providers') && (key.endsWith('.js') || key.endsWith('.json'))) {
535
+ delete require.cache[key];
536
+ }
537
+ }
538
+ this.loadAll();
539
+ }
540
+
541
+ // ─── Upstream Auto-Update ─────────────────────────
542
+
543
+ /**
544
+ * Download latest providers tarball from GitHub → extract to .upstream/
545
+ * - ETag-based change detection (skip if unchanged)
546
+ * - Never touches user custom files in ~/.adhdev/providers/
547
+ * - Runs in background; existing providers are kept on failure
548
+ *
549
+ * @returns Whether an update occurred
550
+ */
551
+ async fetchLatest(): Promise<{ updated: boolean; error?: string }> {
552
+ const https = require('https') as typeof import('https');
553
+ const { execSync } = require('child_process') as typeof import('child_process');
554
+
555
+ const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
556
+ let prevEtag = '';
557
+ let prevTimestamp = 0;
558
+
559
+ // Read previous metadata
560
+ try {
561
+ if (fs.existsSync(metaPath)) {
562
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
563
+ prevEtag = meta.etag || '';
564
+ prevTimestamp = meta.timestamp || 0;
565
+ }
566
+ } catch { }
567
+
568
+ // Minimum 30-minute interval (prevent excessive checks)
569
+ const MIN_INTERVAL_MS = 30 * 60 * 1000;
570
+ if (prevTimestamp && (Date.now() - prevTimestamp) < MIN_INTERVAL_MS) {
571
+ this.log('Upstream check skipped (last check < 30min ago)');
572
+ return { updated: false };
573
+ }
574
+
575
+ try {
576
+ // Step 1: HEAD request to check ETag
577
+ const etag = await new Promise<string>((resolve, reject) => {
578
+ const options = {
579
+ method: 'HEAD',
580
+ hostname: 'github.com',
581
+ path: '/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz',
582
+ headers: { 'User-Agent': 'adhdev-launcher' },
583
+ timeout: 10000,
584
+ };
585
+
586
+ const req = https.request(options, (res) => {
587
+ // GitHub 302 redirect → follow
588
+ if (res.statusCode === 302 && res.headers.location) {
589
+ const url = new URL(res.headers.location);
590
+ const req2 = https.request({
591
+ method: 'HEAD',
592
+ hostname: url.hostname,
593
+ path: url.pathname + (url.search || ''),
594
+ headers: { 'User-Agent': 'adhdev-launcher' },
595
+ timeout: 10000,
596
+ }, (res2) => {
597
+ resolve(res2.headers.etag || res2.headers['last-modified'] || '');
598
+ });
599
+ req2.on('error', reject);
600
+ req2.on('timeout', () => { req2.destroy(); reject(new Error('timeout')); });
601
+ req2.end();
602
+ } else {
603
+ resolve(res.headers.etag || res.headers['last-modified'] || '');
604
+ }
605
+ });
606
+ req.on('error', reject);
607
+ req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
608
+ req.end();
609
+ });
610
+
611
+ // Compare ETag — skip if unchanged
612
+ if (etag && etag === prevEtag) {
613
+ // Update timestamp only
614
+ this.writeMeta(metaPath, prevEtag, Date.now());
615
+ this.log('Upstream unchanged (ETag match)');
616
+ return { updated: false };
617
+ }
618
+
619
+ // Step 2: Download + extract
620
+ this.log('Downloading latest providers from GitHub...');
621
+
622
+ const tmpTar = path.join(os.tmpdir(), `adhdev-providers-${Date.now()}.tar.gz`);
623
+ const tmpExtract = path.join(os.tmpdir(), `adhdev-providers-extract-${Date.now()}`);
624
+
625
+ // Download tarball
626
+ await this.downloadFile(ProviderLoader.GITHUB_TARBALL_URL, tmpTar);
627
+
628
+ // Extract
629
+ fs.mkdirSync(tmpExtract, { recursive: true });
630
+ execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
631
+
632
+ // Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
633
+ const extracted = fs.readdirSync(tmpExtract);
634
+ const rootDir = extracted.find(d =>
635
+ fs.statSync(path.join(tmpExtract, d)).isDirectory() && d.startsWith('adhdev-providers')
636
+ );
637
+ if (!rootDir) throw new Error('Unexpected tarball structure');
638
+
639
+ const sourceDir = path.join(tmpExtract, rootDir);
640
+
641
+ // .upstream replacement (atomic-ish: rename old → copy new → delete old)
642
+ const backupDir = this.upstreamDir + '.bak';
643
+ if (fs.existsSync(this.upstreamDir)) {
644
+ // Backup
645
+ if (fs.existsSync(backupDir)) fs.rmSync(backupDir, { recursive: true, force: true });
646
+ fs.renameSync(this.upstreamDir, backupDir);
647
+ }
648
+
649
+ try {
650
+ // Copy new upstream
651
+ this.copyDirRecursive(sourceDir, this.upstreamDir);
652
+ // Save metadata
653
+ this.writeMeta(metaPath, etag || `ts-${Date.now()}`, Date.now());
654
+ // Backup remove
655
+ if (fs.existsSync(backupDir)) fs.rmSync(backupDir, { recursive: true, force: true });
656
+ } catch (e) {
657
+ // Restore backup on copy failure
658
+ if (fs.existsSync(backupDir)) {
659
+ if (fs.existsSync(this.upstreamDir)) fs.rmSync(this.upstreamDir, { recursive: true, force: true });
660
+ fs.renameSync(backupDir, this.upstreamDir);
661
+ }
662
+ throw e;
663
+ }
664
+
665
+ // Cleanup temp
666
+ try { fs.rmSync(tmpTar, { force: true }); } catch { }
667
+ try { fs.rmSync(tmpExtract, { recursive: true, force: true }); } catch { }
668
+
669
+ const upstreamCount = this.countProviders(this.upstreamDir);
670
+ this.log(`✅ Upstream updated: ${upstreamCount} providers`);
671
+
672
+ return { updated: true };
673
+ } catch (e: any) {
674
+ this.log(`⚠ Upstream fetch failed (using existing): ${e?.message}`);
675
+ // Update timestamp even on failure (prevent continuous retries)
676
+ this.writeMeta(metaPath, prevEtag, Date.now());
677
+ return { updated: false, error: e?.message };
678
+ }
679
+ }
680
+
681
+ /** HTTP(S) file download (follows redirects) */
682
+ private downloadFile(url: string, destPath: string): Promise<void> {
683
+ const https = require('https') as typeof import('https');
684
+ const http = require('http') as typeof import('http');
685
+
686
+ return new Promise((resolve, reject) => {
687
+ const doRequest = (reqUrl: string, redirectCount = 0) => {
688
+ if (redirectCount > 5) { reject(new Error('Too many redirects')); return; }
689
+ const mod = reqUrl.startsWith('https') ? https : http;
690
+ const req = mod.get(reqUrl, { headers: { 'User-Agent': 'adhdev-launcher' }, timeout: 60000 }, (res) => {
691
+ if (res.statusCode === 301 || res.statusCode === 302) {
692
+ doRequest(res.headers.location!, redirectCount + 1);
693
+ return;
694
+ }
695
+ if (res.statusCode !== 200) {
696
+ reject(new Error(`HTTP ${res.statusCode}`));
697
+ return;
698
+ }
699
+ const ws = fs.createWriteStream(destPath);
700
+ res.pipe(ws);
701
+ ws.on('finish', () => { ws.close(); resolve(); });
702
+ ws.on('error', reject);
703
+ });
704
+ req.on('error', reject);
705
+ req.on('timeout', () => { req.destroy(); reject(new Error('Download timeout')); });
706
+ };
707
+ doRequest(url);
708
+ });
709
+ }
710
+
711
+ /** Recursive directory copy */
712
+ private copyDirRecursive(src: string, dest: string): void {
713
+ fs.mkdirSync(dest, { recursive: true });
714
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
715
+ const srcPath = path.join(src, entry.name);
716
+ const destPath = path.join(dest, entry.name);
717
+ if (entry.isDirectory()) {
718
+ this.copyDirRecursive(srcPath, destPath);
719
+ } else {
720
+ fs.copyFileSync(srcPath, destPath);
721
+ }
722
+ }
723
+ }
724
+
725
+ /** .meta.json save */
726
+ private writeMeta(metaPath: string, etag: string, timestamp: number): void {
727
+ try {
728
+ fs.mkdirSync(path.dirname(metaPath), { recursive: true });
729
+ fs.writeFileSync(metaPath, JSON.stringify({
730
+ etag,
731
+ timestamp,
732
+ lastCheck: new Date(timestamp).toISOString(),
733
+ source: ProviderLoader.GITHUB_TARBALL_URL,
734
+ }, null, 2));
735
+ } catch { }
736
+ }
737
+
738
+ /** Count provider files (provider.js or provider.json) */
739
+ private countProviders(dir: string): number {
740
+ if (!fs.existsSync(dir)) return 0;
741
+ let count = 0;
742
+ const scan = (d: string) => {
743
+ try {
744
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
745
+ if (entry.isDirectory()) scan(path.join(d, entry.name));
746
+ else if (entry.name === 'provider.json') count++;
747
+ }
748
+ } catch { }
749
+ };
750
+ scan(dir);
751
+ return count;
752
+ }
753
+
754
+ // ─── Provider Settings API ─────────────────────────
755
+
756
+ /**
757
+ * Get public settings schema for a provider (for dashboard UI rendering)
758
+ */
759
+ getPublicSettings(type: string): ProviderSettingSchema[] {
760
+ const provider = this.providers.get(type);
761
+ if (!provider?.settings) return [];
762
+ return Object.entries(provider.settings)
763
+ .filter(([, def]) => (def as any).public === true)
764
+ .map(([key, def]) => ({ key, ...(def as any) }));
765
+ }
766
+
767
+ /**
768
+ * Get public settings schema for all providers
769
+ */
770
+ getAllPublicSettings(): Record<string, ProviderSettingSchema[]> {
771
+ const result: Record<string, ProviderSettingSchema[]> = {};
772
+ for (const [type] of this.providers) {
773
+ const settings = this.getPublicSettings(type);
774
+ if (settings.length > 0) result[type] = settings;
775
+ }
776
+ return result;
777
+ }
778
+
779
+ /**
780
+ * Resolved setting value for a provider (default + user override)
781
+ */
782
+ getSettingValue(type: string, key: string): any {
783
+ const provider = this.providers.get(type);
784
+ const schemaDef = provider?.settings?.[key];
785
+ const defaultVal = schemaDef ? (schemaDef as any).default : undefined;
786
+
787
+ // Load user-saved value
788
+ try {
789
+ const { loadConfig } = require('../config/config.js');
790
+ const config = loadConfig();
791
+ const userVal = config.providerSettings?.[type]?.[key];
792
+ return userVal !== undefined ? userVal : defaultVal;
793
+ } catch {
794
+ return defaultVal;
795
+ }
796
+ }
797
+
798
+ /**
799
+ * All resolved settings for a provider (default + user override)
800
+ */
801
+ getSettings(type: string): Record<string, any> {
802
+ const provider = this.providers.get(type);
803
+ if (!provider?.settings) return {};
804
+ const result: Record<string, any> = {};
805
+ for (const [key, def] of Object.entries(provider.settings)) {
806
+ result[key] = this.getSettingValue(type, key);
807
+ }
808
+ return result;
809
+ }
810
+
811
+ /**
812
+ * Save provider setting value (writes to config.json)
813
+ */
814
+ setSetting(type: string, key: string, value: any): boolean {
815
+ const provider = this.providers.get(type);
816
+ const schemaDef = provider?.settings?.[key] as any;
817
+ if (!schemaDef) return false;
818
+
819
+ // Non-public settings cannot be modified externally
820
+ if (!schemaDef.public) return false;
821
+
822
+ // Type validation
823
+ if (schemaDef.type === 'boolean' && typeof value !== 'boolean') return false;
824
+ if (schemaDef.type === 'number') {
825
+ if (typeof value !== 'number') return false;
826
+ if (schemaDef.min !== undefined && value < schemaDef.min) return false;
827
+ if (schemaDef.max !== undefined && value > schemaDef.max) return false;
828
+ }
829
+ if (schemaDef.type === 'select' && schemaDef.options && !schemaDef.options.includes(value)) return false;
830
+
831
+ try {
832
+ const { loadConfig, saveConfig } = require('../config/config.js');
833
+ const config = loadConfig();
834
+ if (!config.providerSettings) config.providerSettings = {};
835
+ if (!config.providerSettings[type]) config.providerSettings[type] = {};
836
+ config.providerSettings[type][key] = value;
837
+ saveConfig(config);
838
+ this.log(`Setting updated: ${type}.${key} = ${JSON.stringify(value)}`);
839
+ return true;
840
+ } catch (e) {
841
+ this.log(`Failed to save setting: ${(e as Error).message}`);
842
+ return false;
843
+ }
844
+ }
845
+
846
+ // ─── Private ───────────────────────────────────
847
+
848
+ /**
849
+ * Find the on-disk directory for a provider by type.
850
+ * Checks builtinDir, upstreamDir, userDir in order.
851
+ */
852
+ private findProviderDir(type: string): string | null {
853
+ const provider = this.providers.get(type);
854
+ if (!provider) return null;
855
+ const cat = provider.category;
856
+
857
+ const searchRoots = [this.userDir, this.upstreamDir, ...this.builtinDirs];
858
+ for (const root of searchRoots) {
859
+ if (!fs.existsSync(root)) continue;
860
+ // Direct: root/type or root/cat/type
861
+ for (const candidate of [path.join(root, type), path.join(root, cat, type)]) {
862
+ if (fs.existsSync(path.join(candidate, 'provider.json'))) return candidate;
863
+ }
864
+ // Scan category dir for type match
865
+ const catDir = path.join(root, cat);
866
+ if (fs.existsSync(catDir)) {
867
+ try {
868
+ for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
869
+ if (!entry.isDirectory()) continue;
870
+ const jsonPath = path.join(catDir, entry.name, 'provider.json');
871
+ if (fs.existsSync(jsonPath)) {
872
+ try {
873
+ const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
874
+ if (data.type === type) return path.join(catDir, entry.name);
875
+ } catch { /* skip */ }
876
+ }
877
+ }
878
+ } catch { /* skip */ }
879
+ }
880
+ }
881
+ return null;
882
+ }
883
+
884
+ /**
885
+ * Build a scripts function map from individual .js files in a directory.
886
+ * Each file is wrapped as: (params?) => fs.readFileSync(filePath, 'utf-8')
887
+ * (template substitution is NOT applied here — scripts.js handles that)
888
+ */
889
+ private buildScriptWrappersFromDir(dir: string): Partial<ProviderScripts> {
890
+ // Use a dedicated scripts.js in the alt dir if present
891
+ const scriptsJs = path.join(dir, 'scripts.js');
892
+ if (fs.existsSync(scriptsJs)) {
893
+ try {
894
+ delete require.cache[require.resolve(scriptsJs)];
895
+ return require(scriptsJs);
896
+ } catch { /* fall through to individual file loading */ }
897
+ }
898
+
899
+ // Individual files: list_models.js → scripts.listModels, etc.
900
+ const toCamel = (name: string) =>
901
+ name.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
902
+
903
+ const result: Partial<ProviderScripts> = {};
904
+ try {
905
+ for (const file of fs.readdirSync(dir)) {
906
+ if (!file.endsWith('.js')) continue;
907
+ const scriptName = toCamel(file.replace('.js', ''));
908
+ const filePath = path.join(dir, file);
909
+ (result as any)[scriptName] = (..._args: any[]): string => {
910
+ try { return fs.readFileSync(filePath, 'utf-8'); } catch { return ''; }
911
+ };
912
+ }
913
+ } catch { /* ignore */ }
914
+ return result;
915
+ }
916
+
917
+ /**
918
+ * Recursively scan directory to load provider files
919
+ * Supports two formats:
920
+ * 1. provider.json (metadata) + scripts.js (optional CDP scripts)
921
+ * 2. provider.js (legacy — everything in one file)
922
+ * Structure: dir/category/agent-name/provider.{json,js}
923
+ */
924
+ private loadDir(dir: string, excludeDirs?: string[]): number {
925
+ if (!fs.existsSync(dir)) return 0;
926
+ let count = 0;
927
+
928
+ const scan = (d: string) => {
929
+ let entries: fs.Dirent[];
930
+ try {
931
+ entries = fs.readdirSync(d, { withFileTypes: true });
932
+ } catch {
933
+ return;
934
+ }
935
+
936
+ // Check if this directory has provider.json
937
+ const hasJson = entries.some(e => e.name === 'provider.json');
938
+
939
+ if (hasJson) {
940
+ const jsonPath = path.join(d, 'provider.json');
941
+ try {
942
+ const raw = fs.readFileSync(jsonPath, 'utf-8');
943
+ const mod = JSON.parse(raw) as ProviderModule;
944
+
945
+ if (!mod.type || !mod.name || !mod.category) {
946
+ this.log(`⚠ Invalid provider at ${jsonPath}: missing type/name/category`);
947
+ } else {
948
+ // Restore RegExp fields from JSON (extensionIdPattern)
949
+ if ((mod as any).extensionIdPattern && typeof (mod as any).extensionIdPattern === 'string') {
950
+ const flags = (mod as any).extensionIdPattern_flags || '';
951
+ (mod as any).extensionIdPattern = new RegExp((mod as any).extensionIdPattern, flags);
952
+ delete (mod as any).extensionIdPattern_flags;
953
+ }
954
+
955
+ // Load scripts.js if exists (IDE/Extension)
956
+ // Skip for compatibility-format providers — scripts loaded lazily in resolve()
957
+ const hasCompatibility = Array.isArray((mod as any).compatibility);
958
+ const scriptsPath = path.join(d, 'scripts.js');
959
+ if (!hasCompatibility && fs.existsSync(scriptsPath)) {
960
+ try {
961
+ delete require.cache[require.resolve(scriptsPath)];
962
+ const scripts = require(scriptsPath);
963
+ mod.scripts = scripts;
964
+ } catch (e) {
965
+ this.log(`⚠ Failed to load scripts: ${scriptsPath}: ${(e as Error).message}`);
966
+ }
967
+ }
968
+
969
+ const existed = this.providers.has(mod.type);
970
+ this.providers.set(mod.type, mod);
971
+ count++;
972
+ // Identify source tier for debugging
973
+ const source = d.startsWith(this.userDir) && !d.includes('.upstream')
974
+ ? 'user' : d.startsWith(this.upstreamDir) ? 'upstream' : 'builtin';
975
+ const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES builtin/upstream' : '';
976
+ this.log(` ${existed ? '🔄' : '✅'} ${mod.type} (${mod.category}) — ${mod.name} [${source}]${overrideWarning}`);
977
+ }
978
+ } catch (e) {
979
+ this.log(`⚠ Failed to load ${jsonPath}: ${(e as Error).message}`);
980
+ }
981
+ }
982
+
983
+ // Continue scanning subdirectories (only for dirs without provider.json)
984
+ if (!hasJson) {
985
+ for (const entry of entries) {
986
+ if (!entry.isDirectory()) continue;
987
+ if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
988
+ if (excludeDirs && d === dir && excludeDirs.includes(entry.name)) continue;
989
+ scan(path.join(d, entry.name));
990
+ }
991
+ }
992
+ };
993
+
994
+ scan(dir);
995
+ return count;
996
+ }
997
+
998
+ /**
999
+ * Simple semver range matching
1000
+ * Supported formats: '>=4.0.0', '<3.0.0', '>=2.1.0'
1001
+ */
1002
+ private matchesVersion(current: string, range: string): boolean {
1003
+ const match = range.match(/^([><=!]+)\s*(\d+\.\d+\.\d+)$/);
1004
+ if (!match) return false;
1005
+
1006
+ const [, op, target] = match;
1007
+ const cmp = this.compareVersions(current, target);
1008
+
1009
+ switch (op) {
1010
+ case '>=': return cmp >= 0;
1011
+ case '>': return cmp > 0;
1012
+ case '<=': return cmp <= 0;
1013
+ case '<': return cmp < 0;
1014
+ case '=':
1015
+ case '==': return cmp === 0;
1016
+ case '!=': return cmp !== 0;
1017
+ default: return false;
1018
+ }
1019
+ }
1020
+
1021
+ private compareVersions(a: string, b: string): number {
1022
+ const pa = a.split('.').map(Number);
1023
+ const pb = b.split('.').map(Number);
1024
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
1025
+ const va = pa[i] || 0;
1026
+ const vb = pb[i] || 0;
1027
+ if (va !== vb) return va - vb;
1028
+ }
1029
+ return 0;
1030
+ }
1031
+ }