@adhdev/daemon-core 0.9.82-rc.33 → 0.9.82-rc.331

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 (316) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +46 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +115 -64
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
  13. package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
  14. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  15. package/dist/cli-adapters/resolve-executable.d.ts +14 -0
  16. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  17. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  18. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  19. package/dist/commands/cli-manager.d.ts +2 -1
  20. package/dist/commands/handler.d.ts +110 -0
  21. package/dist/commands/mesh-coordinator.d.ts +85 -1
  22. package/dist/commands/router.d.ts +279 -1
  23. package/dist/commands/upgrade-helper.d.ts +12 -0
  24. package/dist/config/chat-history.d.ts +9 -0
  25. package/dist/config/config.d.ts +5 -0
  26. package/dist/config/mesh-config.d.ts +77 -1
  27. package/dist/detection/ide-detector.d.ts +13 -0
  28. package/dist/detection/win32-ide-version.d.ts +37 -0
  29. package/dist/git/change-impact-config.d.ts +159 -0
  30. package/dist/git/git-commands.d.ts +11 -1
  31. package/dist/git/git-diff.d.ts +6 -0
  32. package/dist/git/git-executor.d.ts +11 -0
  33. package/dist/git/git-status.d.ts +23 -0
  34. package/dist/git/git-types.d.ts +2 -50
  35. package/dist/git/index.d.ts +2 -0
  36. package/dist/index.d.ts +51 -12
  37. package/dist/index.js +38419 -13805
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +41329 -16835
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/installer.d.ts +1 -4
  42. package/dist/ipc/local-ipc-server.d.ts +91 -0
  43. package/dist/launch.d.ts +1 -1
  44. package/dist/logging/async-batch-writer.d.ts +10 -0
  45. package/dist/logging/log-redactor.d.ts +24 -0
  46. package/dist/logging/log-tail-reader.d.ts +46 -0
  47. package/dist/mesh/contracts.d.ts +164 -0
  48. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  49. package/dist/mesh/coordinator-registry.d.ts +59 -0
  50. package/dist/mesh/mesh-active-work.d.ts +174 -0
  51. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  52. package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
  53. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  54. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  55. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  56. package/dist/mesh/mesh-events.d.ts +6 -49
  57. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  58. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  59. package/dist/mesh/mesh-init.d.ts +86 -0
  60. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  61. package/dist/mesh/mesh-ledger.d.ts +77 -1
  62. package/dist/mesh/mesh-missions.d.ts +116 -0
  63. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  64. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  65. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  66. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  67. package/dist/mesh/mesh-routing.d.ts +70 -0
  68. package/dist/mesh/mesh-runtime-store.d.ts +401 -0
  69. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  70. package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
  71. package/dist/mesh/mesh-work-queue.d.ts +207 -5
  72. package/dist/mesh/preview-freshness.d.ts +18 -0
  73. package/dist/mesh/refine-config.d.ts +215 -0
  74. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  75. package/dist/providers/acp-provider-instance.d.ts +2 -0
  76. package/dist/providers/approval-utils.d.ts +13 -0
  77. package/dist/providers/chat-message-normalization.d.ts +12 -0
  78. package/dist/providers/cli-provider-instance.d.ts +61 -3
  79. package/dist/providers/contracts.d.ts +130 -6
  80. package/dist/providers/external-sources.d.ts +71 -0
  81. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  82. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  83. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  84. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  85. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  86. package/dist/providers/native-history/index.d.ts +13 -0
  87. package/dist/providers/provider-instance-manager.d.ts +13 -0
  88. package/dist/providers/provider-instance.d.ts +13 -1
  89. package/dist/providers/provider-loader.d.ts +26 -4
  90. package/dist/providers/provider-trust.d.ts +31 -0
  91. package/dist/providers/read-chat-contract.d.ts +29 -0
  92. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  93. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  94. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  95. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  96. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  97. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  98. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  99. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  100. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  101. package/dist/providers/sdk/v1/index.d.ts +30 -0
  102. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  103. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  104. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  105. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  106. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  107. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  108. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  109. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  110. package/dist/providers/spec/adapter.d.ts +69 -0
  111. package/dist/providers/spec/cli-adapter.d.ts +217 -0
  112. package/dist/providers/spec/evaluator.d.ts +23 -0
  113. package/dist/providers/spec/fsm-driver.d.ts +306 -0
  114. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  115. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  116. package/dist/providers/spec/fsm-types.d.ts +144 -0
  117. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  118. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  119. package/dist/providers/spec/route.d.ts +4 -0
  120. package/dist/providers/spec/types.d.ts +180 -0
  121. package/dist/providers/status-monitor.d.ts +7 -7
  122. package/dist/providers/transcript-v2.d.ts +176 -0
  123. package/dist/providers/types/interactive-prompt.d.ts +84 -0
  124. package/dist/providers/working-dir.d.ts +17 -0
  125. package/dist/repo-mesh-types.d.ts +295 -13
  126. package/dist/sessions/registry.d.ts +6 -0
  127. package/dist/shared-types-extra.d.ts +2 -4
  128. package/dist/shared-types.d.ts +52 -2
  129. package/dist/status/normalize.d.ts +1 -1
  130. package/dist/status/normalize.js +1 -0
  131. package/dist/status/normalize.js.map +1 -1
  132. package/dist/status/normalize.mjs +1 -0
  133. package/dist/status/normalize.mjs.map +1 -1
  134. package/dist/status/reporter.d.ts +2 -0
  135. package/dist/status/snapshot.d.ts +1 -0
  136. package/dist/types.d.ts +5 -0
  137. package/package.json +7 -3
  138. package/src/agent-stream/poller.ts +2 -3
  139. package/src/agent-stream/provider-adapter.ts +1 -1
  140. package/src/boot/daemon-lifecycle.ts +55 -12
  141. package/src/boot/process-hardening.ts +89 -0
  142. package/src/build-info.ts +73 -0
  143. package/src/chat/source-machine.ts +534 -0
  144. package/src/chat/source-resolver.ts +0 -0
  145. package/src/chat/subscription-updates.ts +20 -1
  146. package/src/cli-adapter-types.d.ts +1 -0
  147. package/src/cli-adapter-types.ts +47 -2
  148. package/src/cli-adapters/cli-script-runner.ts +421 -0
  149. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  150. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  151. package/src/cli-adapters/provider-cli-adapter.ts +713 -1141
  152. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  153. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  154. package/src/cli-adapters/provider-cli-runtime.ts +56 -7
  155. package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
  156. package/src/cli-adapters/provider-cli-shared.ts +100 -11
  157. package/src/cli-adapters/pty-transport.ts +2 -1
  158. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  159. package/src/cli-adapters/resolve-executable.ts +90 -0
  160. package/src/cli-adapters/session-host-transport.ts +2 -1
  161. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  162. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  163. package/src/cli-adapters/terminal-screen.ts +16 -81
  164. package/src/commands/chat-commands.ts +1823 -62
  165. package/src/commands/cli-manager.ts +286 -14
  166. package/src/commands/handler.ts +809 -2
  167. package/src/commands/mesh-coordinator.ts +332 -122
  168. package/src/commands/router.ts +7270 -1306
  169. package/src/commands/stream-commands.ts +8 -0
  170. package/src/commands/upgrade-helper.ts +81 -30
  171. package/src/config/chat-history.ts +483 -24
  172. package/src/config/config.ts +12 -0
  173. package/src/config/mesh-config.ts +388 -6
  174. package/src/config/recent-activity.ts +8 -2
  175. package/src/daemon/dev-auto-implement.ts +3 -2
  176. package/src/daemon/dev-cli-debug.ts +10 -1
  177. package/src/detection/cli-detector.ts +28 -9
  178. package/src/detection/ide-detector.ts +55 -16
  179. package/src/detection/win32-ide-version.ts +106 -0
  180. package/src/git/change-impact-config.ts +354 -0
  181. package/src/git/git-commands.ts +55 -14
  182. package/src/git/git-diff.ts +53 -0
  183. package/src/git/git-executor.ts +12 -0
  184. package/src/git/git-status.ts +574 -43
  185. package/src/git/git-types.ts +14 -62
  186. package/src/git/git-worktree.ts +8 -1
  187. package/src/git/index.ts +16 -0
  188. package/src/index.ts +145 -11
  189. package/src/installer.d.ts +1 -1
  190. package/src/installer.ts +8 -6
  191. package/src/ipc/local-ipc-server.ts +278 -0
  192. package/src/launch.d.ts +1 -1
  193. package/src/launch.ts +37 -28
  194. package/src/logging/async-batch-writer.ts +55 -0
  195. package/src/logging/log-redactor.ts +100 -0
  196. package/src/logging/log-tail-reader.ts +220 -0
  197. package/src/logging/logger.ts +2 -1
  198. package/src/mesh/contracts.ts +329 -0
  199. package/src/mesh/coordinator-prompt.ts +234 -31
  200. package/src/mesh/coordinator-registry.ts +121 -0
  201. package/src/mesh/mesh-active-work.ts +623 -0
  202. package/src/mesh/mesh-delivery-policy.ts +315 -0
  203. package/src/mesh/mesh-events-coordinator.ts +2110 -0
  204. package/src/mesh/mesh-events-pending.ts +508 -0
  205. package/src/mesh/mesh-events-stale.ts +290 -0
  206. package/src/mesh/mesh-events-utils.ts +254 -0
  207. package/src/mesh/mesh-events.ts +30 -1035
  208. package/src/mesh/mesh-fast-forward.ts +839 -0
  209. package/src/mesh/mesh-host-ownership.ts +73 -0
  210. package/src/mesh/mesh-init.ts +260 -0
  211. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  212. package/src/mesh/mesh-ledger.ts +589 -102
  213. package/src/mesh/mesh-missions.ts +248 -0
  214. package/src/mesh/mesh-reconcile-loop.ts +786 -0
  215. package/src/mesh/mesh-refine-batch.ts +197 -0
  216. package/src/mesh/mesh-refine-status.ts +231 -0
  217. package/src/mesh/mesh-review-inbox.ts +307 -0
  218. package/src/mesh/mesh-routing.ts +291 -0
  219. package/src/mesh/mesh-runtime-store.ts +1578 -0
  220. package/src/mesh/mesh-task-stats.ts +154 -0
  221. package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
  222. package/src/mesh/mesh-work-queue.ts +874 -141
  223. package/src/mesh/preview-freshness.ts +118 -0
  224. package/src/mesh/refine-config.ts +403 -0
  225. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  226. package/src/providers/acp-provider-instance.ts +25 -9
  227. package/src/providers/approval-utils.d.ts +4 -0
  228. package/src/providers/approval-utils.ts +47 -5
  229. package/src/providers/chat-message-normalization.ts +45 -5
  230. package/src/providers/cli-provider-instance.ts +986 -99
  231. package/src/providers/contracts.d.ts +55 -0
  232. package/src/providers/contracts.ts +141 -6
  233. package/src/providers/extension-provider-instance.ts +12 -7
  234. package/src/providers/external-sources.ts +218 -0
  235. package/src/providers/ide-provider-instance.ts +35 -12
  236. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  237. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  238. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  239. package/src/providers/native-history/dispatcher.ts +340 -0
  240. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  241. package/src/providers/native-history/index.ts +30 -0
  242. package/src/providers/provider-instance-manager.ts +30 -0
  243. package/src/providers/provider-instance.ts +10 -1
  244. package/src/providers/provider-loader.ts +662 -50
  245. package/src/providers/provider-schema.ts +87 -14
  246. package/src/providers/provider-trust.ts +114 -0
  247. package/src/providers/read-chat-contract.ts +76 -16
  248. package/src/providers/sdk/README.md +49 -0
  249. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  250. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  251. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  252. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  253. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  254. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  255. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  256. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  257. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  258. package/src/providers/sdk/v1/index.ts +152 -0
  259. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  260. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  261. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  262. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  263. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  264. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  265. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  266. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  267. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  268. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  269. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  270. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  271. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  272. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  273. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  274. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  275. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  276. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  277. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  278. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  279. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  280. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  281. package/src/providers/sdk/v1/validators/index.ts +19 -0
  282. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  283. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  284. package/src/providers/spec/adapter.ts +168 -0
  285. package/src/providers/spec/cli-adapter.ts +986 -0
  286. package/src/providers/spec/evaluator.ts +345 -0
  287. package/src/providers/spec/fsm-driver.ts +993 -0
  288. package/src/providers/spec/fsm-evaluator.ts +255 -0
  289. package/src/providers/spec/fsm-loader.ts +120 -0
  290. package/src/providers/spec/fsm-types.ts +238 -0
  291. package/src/providers/spec/native-history-executor.ts +1136 -0
  292. package/src/providers/spec/pre-launch-trust.ts +104 -0
  293. package/src/providers/spec/route.ts +51 -0
  294. package/src/providers/spec/types.ts +226 -0
  295. package/src/providers/status-monitor.d.ts +7 -7
  296. package/src/providers/status-monitor.ts +37 -22
  297. package/src/providers/transcript-v2.ts +567 -0
  298. package/src/providers/types/interactive-prompt.ts +536 -0
  299. package/src/providers/version-archive.ts +64 -24
  300. package/src/providers/working-dir.ts +23 -0
  301. package/src/repo-mesh-types.ts +378 -14
  302. package/src/sessions/registry.ts +6 -0
  303. package/src/shared-types-extra.ts +2 -4
  304. package/src/shared-types.d.ts +8 -0
  305. package/src/shared-types.ts +57 -1
  306. package/src/status/builders.ts +26 -6
  307. package/src/status/normalize.ts +2 -0
  308. package/src/status/reporter.ts +19 -1
  309. package/src/status/snapshot.ts +84 -25
  310. package/src/system/host-memory.ts +29 -12
  311. package/src/types.ts +5 -0
  312. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  313. package/dist/mesh/mesh-sync.d.ts +0 -53
  314. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  315. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  316. package/src/mesh/mesh-sync.ts +0 -111
@@ -22,6 +22,7 @@ import { LOG } from '../logging/logger.js';
22
22
  import { VersionArchive } from './version-archive.js';
23
23
  import type {
24
24
  ProviderCompatibilityEntry,
25
+ ProviderControlDef,
25
26
  ProviderModule,
26
27
  ProviderCategory,
27
28
  ProviderScripts,
@@ -30,8 +31,34 @@ import type {
30
31
  ResolvedProvider,
31
32
  } from './contracts.js';
32
33
  import { validateProviderDefinition } from './provider-schema.js';
34
+ import {
35
+ loadProvidersActive,
36
+ resolveActiveSource,
37
+ } from './external-sources.js';
33
38
  import type { ProviderSourceMode } from '../config/config.js';
34
39
  import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
40
+ import { executeNativeHistory } from './spec/native-history-executor.js';
41
+ import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
42
+
43
+ /**
44
+ * Adds a provider-script root to the require whitelist. Wrapped in a
45
+ * try/catch + null check so a loader hot-path can't crash on a path
46
+ * that doesn't exist yet or one the whitelist hook rejects.
47
+ *
48
+ * The require-whitelist module is loaded lazily on first call. Eagerly
49
+ * top-level importing it pulls `node:fs.realpathSync.native` into
50
+ * module evaluation, which breaks unit tests that partially mock `fs`
51
+ * (e.g. test/commands/get-logs-incremental.test.ts mocks only
52
+ * existsSync + readFileSync). Lazy load keeps that mock surface valid.
53
+ */
54
+ function registerProviderScriptRootSafely(root: string | null | undefined): void {
55
+ if (!root || typeof root !== 'string') return;
56
+ try {
57
+ const { registerProviderScriptRoot } =
58
+ require('./sdk/v1/sandbox/require-whitelist.js') as typeof import('./sdk/v1/sandbox/require-whitelist.js');
59
+ registerProviderScriptRoot(root);
60
+ } catch { /* boot-time only — swallow */ }
61
+ }
35
62
 
36
63
  interface ProviderAvailabilityState {
37
64
  installed: boolean;
@@ -61,6 +88,70 @@ export interface MachineProviderConfig {
61
88
  lastVerification?: MachineProviderCheckResult;
62
89
  }
63
90
 
91
+ /**
92
+ * Translate a spec `control_bar` array into the web-facing
93
+ * `ProviderControlDef[]` shape the dashboard renders.
94
+ *
95
+ * The two shapes are distinct: `control_bar` entries are daemon-side
96
+ * `{ id, label, visible_when_state, action }` records driving
97
+ * SpecCliAdapter.invokeScript, while the dashboard's chat bar reads
98
+ * `ProviderControlDef` (`{ id, type, label, placement, ... }`). Spec
99
+ * providers (claude-cli / codex-cli) historically declared *only*
100
+ * `control_bar`, so the dashboard saw no controls at all — the Model / Mode
101
+ * pickers never rendered. This bridges that gap without changing how the
102
+ * controls actually dispatch.
103
+ *
104
+ * Script-name contract: the dashboard sends the control's
105
+ * `listScript` / `setScript` / `invokeScript` name through
106
+ * `invoke_provider_script`, which gates on `provider.scripts[<name>]` and then
107
+ * routes to `SpecCliAdapter.invokeScript(<name>)` — which matches the name
108
+ * against `control_bar[].id`. So every synthesized script name MUST equal the
109
+ * control id (the loader stubs `provider.scripts[id]` from the same source).
110
+ *
111
+ * Mapping:
112
+ * open_picker → select (dynamic): list + set both keyed on the control id;
113
+ * the adapter distinguishes LIST vs SELECT by the presence of
114
+ * a choice arg, so one id serves both roles.
115
+ * send_keys → action: one-shot keystroke (stop, cycle_mode).
116
+ * attach_image → skipped: it needs an image blob from a file picker, not a
117
+ * bare bar button; surfacing it as an `action` would only
118
+ * produce a button that errors with "requires args.blob".
119
+ */
120
+ function synthesizeControlsFromControlBar(specControls: any[]): ProviderControlDef[] {
121
+ const out: ProviderControlDef[] = [];
122
+ specControls.forEach((ctl, index) => {
123
+ const id = typeof ctl?.id === 'string' ? ctl.id.trim() : '';
124
+ const actionType = ctl?.action?.type;
125
+ if (!id || !actionType) return;
126
+ const label = typeof ctl?.label === 'string' && ctl.label.trim() ? ctl.label : id;
127
+ if (actionType === 'open_picker') {
128
+ out.push({
129
+ id,
130
+ type: 'select',
131
+ label,
132
+ placement: 'bar',
133
+ dynamic: true,
134
+ listScript: id,
135
+ setScript: id,
136
+ readFrom: id,
137
+ order: index,
138
+ });
139
+ } else if (actionType === 'send_keys') {
140
+ out.push({
141
+ id,
142
+ type: 'action',
143
+ label,
144
+ placement: 'bar',
145
+ invokeScript: id,
146
+ resultDisplay: 'none',
147
+ order: index,
148
+ });
149
+ }
150
+ // attach_image intentionally skipped — see fn doc.
151
+ });
152
+ return out;
153
+ }
154
+
64
155
  type CliDetectionEntry = {
65
156
  id: string;
66
157
  displayName: string;
@@ -92,7 +183,9 @@ export class ProviderLoader {
92
183
  }
93
184
 
94
185
  private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
186
+ private static readonly REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
95
187
  private static readonly META_FILE = '.meta.json';
188
+ private static readonly REGISTRY_META_FILE = '.registry-meta.json';
96
189
  private static readonly REPO_PROVIDER_DIRNAME = 'adhdev-providers';
97
190
  private static readonly SIBLING_MARKER_FILE = '.adhdev-provider-root';
98
191
  private static readonly SIBLING_ENV_VAR = 'ADHDEV_USE_SIBLING_PROVIDERS';
@@ -193,6 +286,33 @@ export class ProviderLoader {
193
286
  sourceMode: options?.sourceMode,
194
287
  disableUpstream: options?.disableUpstream,
195
288
  });
289
+
290
+ // One-time migration: ~/.adhdev/marketplace → ~/.adhdev/external.
291
+ // The directory was renamed when the "marketplace" install model was
292
+ // dropped in favour of explicit external git sources. Best-effort:
293
+ // if the rename fails we leave both dirs in place and log so the user
294
+ // can investigate.
295
+ this.migrateMarketplaceDirToExternal();
296
+ }
297
+
298
+ private migrateMarketplaceDirToExternal(): void {
299
+ try {
300
+ const home = os.homedir();
301
+ const oldDir = path.join(home, '.adhdev', 'marketplace');
302
+ const newDir = path.join(home, '.adhdev', 'external');
303
+ if (!fs.existsSync(oldDir)) return;
304
+ if (fs.existsSync(newDir)) {
305
+ // Both exist — don't merge. Leave old in place; surface in logs so
306
+ // the user can decide what to keep. Loader still loads from
307
+ // external/ only, so old marketplace/ becomes inert.
308
+ this.log(`Migration skipped: both ~/.adhdev/marketplace and ~/.adhdev/external exist (marketplace dir is now inert and can be removed manually).`);
309
+ return;
310
+ }
311
+ fs.renameSync(oldDir, newDir);
312
+ this.log(`Migrated ~/.adhdev/marketplace → ~/.adhdev/external (one-time rename after provider source-layer cleanup).`);
313
+ } catch (e: any) {
314
+ this.log(`Marketplace→external migration failed: ${e?.message || e}`);
315
+ }
196
316
  }
197
317
 
198
318
  private log(msg: string): void {
@@ -224,7 +344,12 @@ export class ProviderLoader {
224
344
  * Highest-priority editable overrides come first.
225
345
  */
226
346
  getProviderRoots(): string[] {
227
- return [this.userDir, this.upstreamDir];
347
+ // Order matters: user customs > external (3rd-party sources) > upstream
348
+ // (official auto-sync). findProviderDirInternal walks this list in order
349
+ // to locate the provider dir containing the scripts/, so external must
350
+ // be included here even though loadAll() also reads it directly.
351
+ const externalDir = path.join(os.homedir(), '.adhdev', 'external');
352
+ return [this.userDir, externalDir, this.upstreamDir];
228
353
  }
229
354
 
230
355
  getSourceConfig(): ProviderSourceConfigSnapshot {
@@ -316,16 +441,19 @@ export class ProviderLoader {
316
441
 
317
442
  /**
318
443
  * Load all providers (3-tier priority)
319
- * 1. .upstream/ (GitHub auto-download — primary source)
320
- * 2. User custom (~/.adhdev/providers/ excluding .upstream)
321
- * User custom always wins (highest priority).
444
+ * 1. ~/.adhdev/providers/.upstream/ official git, auto-synced
445
+ * 2. ~/.adhdev/external/ 3rd-party git sources, user-added,
446
+ * bundled providers may include arbitrary JS (untrusted by default)
447
+ * 3. ~/.adhdev/providers/ (excluding .upstream) — user-authored customs,
448
+ * always wins
449
+ * Highest priority listed last (overwrites earlier loads).
322
450
  * If .upstream/ is empty, call fetchLatest() before loadAll().
323
451
  */
324
452
  loadAll(): void {
325
453
  this.providers.clear();
326
454
  this.providerAvailability.clear();
327
455
 
328
- // 1. Load upstream (GitHub auto-download — primary source)
456
+ // 1. Load upstream (GitHub auto-download — primary official source)
329
457
  let upstreamCount = 0;
330
458
  if (!this.disableUpstream && fs.existsSync(this.upstreamDir)) {
331
459
  upstreamCount = this.loadDir(this.upstreamDir);
@@ -336,7 +464,90 @@ export class ProviderLoader {
336
464
  this.log('Upstream loading disabled (sourceMode=no-upstream)');
337
465
  }
338
466
 
339
- // 2. Load user custom (excluding .upstream — highest priority, never auto-updated)
467
+ // 2. Load external providers from ~/.adhdev/external/<source-name>/
468
+ // (3rd-party git sources). Overrides upstream but is itself overridden
469
+ // by user customs in step 3.
470
+ //
471
+ // Each registered source is a separate subdirectory so two sources can
472
+ // both expose the same provider type without overwriting each other.
473
+ // When more than one source provides the same type, providers-active.json
474
+ // chooses the active one; without an explicit choice we deterministically
475
+ // pick the first in disk-walk order and log the ambiguity so the user
476
+ // can resolve it from the dashboard.
477
+ //
478
+ // Any non-spec manifest (tui block / overrides / scriptDir) coming from
479
+ // an external source runs JavaScript the daemon hasn't audited, so
480
+ // dashboards must surface an "untrusted source" badge before letting
481
+ // the user enable them.
482
+ const externalDir = path.join(os.homedir(), '.adhdev', 'external');
483
+ if (fs.existsSync(externalDir)) {
484
+ // Legacy layout (pre-source-namespace): manifests sit directly at
485
+ // external/<category>/<type>/. Detect by presence of category dirs at
486
+ // the root and migrate inline by treating the whole tree as a single
487
+ // implicit source. Loader behavior unchanged for legacy callers.
488
+ const rootEntries = (() => {
489
+ try { return fs.readdirSync(externalDir, { withFileTypes: true }); }
490
+ catch { return [] as fs.Dirent[]; }
491
+ })();
492
+ const KNOWN_CATEGORIES = new Set(['cli', 'ide', 'extension', 'acp']);
493
+ const looksLegacy = rootEntries.some(e => e.isDirectory() && KNOWN_CATEGORIES.has(e.name));
494
+ if (looksLegacy) {
495
+ // Tree shape predates per-source dirs — treat the whole thing as a
496
+ // single anonymous source so existing installs keep working until
497
+ // they're migrated to a real source registration.
498
+ const externalCount = this.loadDir(externalDir);
499
+ if (externalCount > 0) {
500
+ this.log(`Loaded ${externalCount} external providers (legacy unnamed source)`);
501
+ }
502
+ } else {
503
+ // New layout: external/<source-name>/<category>/<type>/…
504
+ const activeFile = loadProvidersActive();
505
+ let totalLoaded = 0;
506
+ const ambiguousTypes: { type: string; chosen: string; candidates: string[] }[] = [];
507
+ // Per-source load, then filter by active-selection: for each type
508
+ // present in more than one source, only the active source's copy
509
+ // is left in this.providers.
510
+ for (const sourceEntry of rootEntries) {
511
+ if (!sourceEntry.isDirectory()) continue;
512
+ const sourceDir = path.join(externalDir, sourceEntry.name);
513
+ const sourceLoaded = this.loadDir(sourceDir);
514
+ if (sourceLoaded > 0) {
515
+ totalLoaded += sourceLoaded;
516
+ this.log(`Loaded ${sourceLoaded} providers from external source "${sourceEntry.name}"`);
517
+ }
518
+ }
519
+ // Resolve ambiguities — when the same type came from multiple
520
+ // sources, the last load wins by default. Replay with the active
521
+ // selection so the user-chosen source ends up winning.
522
+ for (const [type] of this.providers) {
523
+ const prov = this.providers.get(type);
524
+ if (!prov) continue;
525
+ const resolved = resolveActiveSource(prov.category, type, activeFile);
526
+ if (resolved.candidates.length <= 1) continue;
527
+ if (resolved.ambiguous) {
528
+ ambiguousTypes.push({ type, chosen: resolved.source ?? '?', candidates: resolved.candidates });
529
+ }
530
+ if (resolved.source && resolved.source !== '?') {
531
+ const sourceDir = path.join(externalDir, resolved.source);
532
+ // Reload only this source's copy of the conflicting type so it
533
+ // overwrites whatever else won the initial pass.
534
+ const reloadCount = this.loadDir(sourceDir);
535
+ // reloadCount is a sanity check — we expect ≥1
536
+ if (reloadCount === 0) {
537
+ this.log(`Active source "${resolved.source}" no longer provides ${type}`);
538
+ }
539
+ }
540
+ }
541
+ if (totalLoaded > 0) {
542
+ this.log(`Loaded ${totalLoaded} external providers (3rd-party sources)`);
543
+ }
544
+ for (const a of ambiguousTypes) {
545
+ this.log(`Ambiguous provider "${a.type}" — provided by [${a.candidates.join(', ')}], defaulted to "${a.chosen}". Set the active source from the dashboard to silence this warning.`);
546
+ }
547
+ }
548
+ }
549
+
550
+ // 3. Load user custom (excluding .upstream — highest priority, never auto-updated)
340
551
  if (fs.existsSync(this.userDir)) {
341
552
  const userCount = this.loadDir(this.userDir, ['.upstream']);
342
553
  if (userCount > 0) {
@@ -909,18 +1120,28 @@ export class ProviderLoader {
909
1120
 
910
1121
  for (const entry of compat) {
911
1122
  if (this.matchesVersion(currentVersion, entry.ideVersion)) {
912
- const loaded = this.loadScriptsFromDir(type, entry.scriptDir);
913
- if (loaded) {
914
- resolved.scripts = loaded;
915
- this.debugLog(` [compatibility] ${type} v${currentVersion} ${entry.scriptDir}`);
916
- resolved._resolvedScriptDir = entry.scriptDir;
917
- resolved._resolvedScriptsSource = `compatibility:${entry.ideVersion}`;
918
- if (providerDir) {
919
- const fullDir = path.join(providerDir, entry.scriptDir);
920
- resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
921
- ? path.join(fullDir, 'scripts.js')
922
- : fullDir;
1123
+ // entry.scriptDir is optional now — spec-driven providers (agy,
1124
+ // codex on >=0.137, claude on >=2.1) only ship `spec` here, so
1125
+ // there's nothing to load from the filesystem. SpecCliAdapter
1126
+ // takes over via the `spec` path later in this method.
1127
+ if (entry.scriptDir) {
1128
+ const loaded = this.loadScriptsFromDir(type, entry.scriptDir);
1129
+ if (loaded) {
1130
+ resolved.scripts = loaded;
1131
+ this.debugLog(` [compatibility] ${type} v${currentVersion} → ${entry.scriptDir}`);
1132
+ resolved._resolvedScriptDir = entry.scriptDir;
1133
+ resolved._resolvedScriptsSource = `compatibility:${entry.ideVersion}`;
1134
+ if (providerDir) {
1135
+ const fullDir = path.join(providerDir, entry.scriptDir);
1136
+ resolved._resolvedScriptsPath = fs.existsSync(path.join(fullDir, 'scripts.js'))
1137
+ ? path.join(fullDir, 'scripts.js')
1138
+ : fullDir;
1139
+ }
1140
+ matched = true;
923
1141
  }
1142
+ } else {
1143
+ // Spec-only entry — still counts as a match so the
1144
+ // defaultScriptDir fallback below doesn't kick in.
924
1145
  matched = true;
925
1146
  }
926
1147
  break; // first match wins
@@ -988,7 +1209,12 @@ export class ProviderLoader {
988
1209
  }
989
1210
 
990
1211
  // 3. Composite override (OS + version)
991
- if (base.overrides) {
1212
+ // Legacy shape: base.overrides is an Array<{ when: {os,version}, scripts }>.
1213
+ // v1 manifests (Phase 3-4) repurposed `overrides` as an object map of
1214
+ // capability overrides (e.g. { detectStatus: { path, schema } }), which is
1215
+ // consumed by the SDK builders, not by this resolver. Only iterate when
1216
+ // the field is in the legacy array shape.
1217
+ if (Array.isArray(base.overrides)) {
992
1218
  for (const override of base.overrides) {
993
1219
  const osMatch = !override.when.os || override.when.os === currentOs;
994
1220
  const verMatch = !override.when.version || (currentVersion && this.matchesVersion(currentVersion, override.when.version));
@@ -996,6 +1222,40 @@ export class ProviderLoader {
996
1222
  resolved.scripts = { ...resolved.scripts, ...override.scripts };
997
1223
  }
998
1224
  }
1225
+ } else if (base.overrides && typeof base.overrides === 'object') {
1226
+ // v1 manifest shape: { detectStatus: { path }, parseSession: { path }, ... }
1227
+ // Each script name maps to a path inside the provider directory. We load
1228
+ // the file and merge its export(s) into resolved.scripts. Lets a
1229
+ // provider override a single primitive (e.g. just detectStatus) while
1230
+ // letting the SDK synthesize the rest from the tui block.
1231
+ const providerDir = this.findProviderDirInternal(base.type);
1232
+ if (providerDir) {
1233
+ for (const [scriptName, override] of Object.entries(base.overrides as Record<string, any>)) {
1234
+ if (!override || typeof override.path !== 'string') continue;
1235
+ const fullPath = path.join(providerDir, override.path);
1236
+ if (!fs.existsSync(fullPath)) {
1237
+ this.log(` [overrides] ${base.type}: ${scriptName} path not found: ${fullPath}`);
1238
+ continue;
1239
+ }
1240
+ try {
1241
+ // Override scripts go through the same whitelist gate as the
1242
+ // main scripts dir. Use the provider parent root so a v1
1243
+ // override can still require ../_shared helpers.
1244
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1245
+ delete require.cache[require.resolve(fullPath)];
1246
+ const fn = require(fullPath);
1247
+ const target = typeof fn === 'function' ? fn : (fn && fn[scriptName]);
1248
+ if (typeof target === 'function') {
1249
+ resolved.scripts = { ...resolved.scripts, [scriptName]: target } as any;
1250
+ this.log(` [overrides] ${base.type}: ${scriptName} loaded from ${override.path}`);
1251
+ } else {
1252
+ this.log(` [overrides] ${base.type}: ${scriptName} export missing in ${override.path}`);
1253
+ }
1254
+ } catch (e: any) {
1255
+ this.log(` [overrides] ${base.type}: ${scriptName} require failed: ${e?.message || e}`);
1256
+ }
1257
+ }
1258
+ }
999
1259
  }
1000
1260
 
1001
1261
  if ((resolved.category === 'cli' || resolved.category === 'acp') && resolved.spawn?.command) {
@@ -1006,6 +1266,135 @@ export class ProviderLoader {
1006
1266
  };
1007
1267
  }
1008
1268
 
1269
+ // (spec migration) Late-binding spec.json native-history hook. Runs
1270
+ // *after* every script-loading path (compatibility / defaultScriptDir /
1271
+ // overrides) so it deterministically wins over a legacy v1 scripts.js
1272
+ // export. Three modes, picked by spec.json's native_history block:
1273
+ // 1. source — declarative jsonl/sqlite executor (new-provider path,
1274
+ // no daemon change needed for new on-disk formats)
1275
+ // 2. override_path — provider-supplied reader file (escape hatch for
1276
+ // exotic formats); module default-exports a reader fn
1277
+ // 3. reader — built-in reader id (claude-cli / codex-cli /
1278
+ // antigravity-cli / hermes-cli), kept for backwards
1279
+ // compatibility with the four shipped providers
1280
+ if (providerDir) {
1281
+ try {
1282
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1283
+ const fs = require('node:fs');
1284
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1285
+ const path = require('node:path');
1286
+ // Pick the right spec file for the detected CLI version. Resolution
1287
+ // order:
1288
+ // 1. compatibility[i].spec where ideVersion matches currentVersion
1289
+ // (lets a provider ship specs/2.0.json, specs/2.1.json, etc.
1290
+ // alongside the matching scriptDir)
1291
+ // 2. specs/default.json — explicit fallback
1292
+ // 3. spec.json — legacy single-spec layout
1293
+ // Missing files fall through silently to the next candidate.
1294
+ const candidates: string[] = [];
1295
+ if (Array.isArray((base as any).compatibility)) {
1296
+ for (const entry of (base as any).compatibility) {
1297
+ if (typeof entry?.spec !== 'string') continue;
1298
+ // If currentVersion is unknown (cli-manager hasn't probed yet)
1299
+ // we still let compatibility entries that don't pin a version
1300
+ // through, plus any entry whose pin matches.
1301
+ const matches = !entry.ideVersion
1302
+ || (currentVersion && this.matchesVersion(currentVersion, entry.ideVersion))
1303
+ || !currentVersion;
1304
+ if (matches) candidates.push(path.join(providerDir, entry.spec));
1305
+ }
1306
+ }
1307
+ candidates.push(path.join(providerDir, 'specs', 'default.json'));
1308
+ candidates.push(path.join(providerDir, 'spec.json'));
1309
+ const specPath = candidates.find((p: string) => fs.existsSync(p));
1310
+ if (specPath) {
1311
+ // Hand the resolved spec path off to route.ts via a hidden field
1312
+ // so the routing layer doesn't have to repeat the candidate walk.
1313
+ (resolved as any)._resolvedSpecPath = specPath;
1314
+ // Extract control_bar + native_history directly from the JSON header.
1315
+ let specControls: any[] | undefined;
1316
+ let nh: any | undefined;
1317
+ try {
1318
+ const rawSpec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
1319
+ specControls = rawSpec.control_bar;
1320
+ nh = rawSpec.native_history;
1321
+ } catch { /* unreadable spec — leave controls/native unavailable */ }
1322
+ // Stub each control_bar entry as a provider.scripts.<id>. The
1323
+ // upstream invoke_provider_script gate checks that the script
1324
+ // name exists on provider.scripts before calling adapter.invokeScript;
1325
+ // for spec providers the *actual* dispatch happens inside
1326
+ // SpecCliAdapter.invokeScript which maps the name to control_bar.
1327
+ // The stub is just a presence marker so the gate doesn't reject.
1328
+ if (specControls && specControls.length > 0) {
1329
+ resolved.scripts = { ...(resolved.scripts || {}) };
1330
+ for (const ctl of specControls) {
1331
+ if (!(resolved.scripts as any)[ctl.id]) {
1332
+ (resolved.scripts as any)[ctl.id] = (..._args: unknown[]) => ({
1333
+ __spec_control: true,
1334
+ controlId: ctl.id,
1335
+ actionType: ctl.action.type,
1336
+ });
1337
+ }
1338
+ }
1339
+ // Bridge the spec control_bar into the web-facing controls schema so
1340
+ // the dashboard chat bar actually renders Model/Mode pickers. Only
1341
+ // synthesize when the provider hasn't already declared its own
1342
+ // `controls` in provider.v1.json (e.g. hermes-cli) — an explicit
1343
+ // declaration wins and must not be clobbered.
1344
+ const hasDeclaredControls = Array.isArray((resolved as any).controls)
1345
+ && (resolved as any).controls.length > 0;
1346
+ if (!hasDeclaredControls) {
1347
+ const synthesized = synthesizeControlsFromControlBar(specControls);
1348
+ if (synthesized.length > 0) {
1349
+ resolved.controls = synthesized;
1350
+ }
1351
+ }
1352
+ }
1353
+ if (nh) {
1354
+ let reader: ((input: any) => any) | null = null;
1355
+ let format = 'spec';
1356
+
1357
+ if (nh.source) {
1358
+ format = `spec-${nh.source.kind}`;
1359
+ reader = (input: any) => executeNativeHistory(nh, input);
1360
+ } else if (nh.override_path) {
1361
+ const overrideFile = path.resolve(providerDir, nh.override_path);
1362
+ if (fs.existsSync(overrideFile)) {
1363
+ try {
1364
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1365
+ delete require.cache[require.resolve(overrideFile)];
1366
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
1367
+ const mod = require(overrideFile);
1368
+ const fn = typeof mod === 'function' ? mod : (mod && typeof mod.default === 'function' ? mod.default : null);
1369
+ if (fn) {
1370
+ format = 'spec-override';
1371
+ reader = (input: any) => fn(input);
1372
+ }
1373
+ } catch { /* fall through — leave native unavailable */ }
1374
+ }
1375
+ } else if (nh.reader) {
1376
+ const dispatch = createNativeHistoryDispatcher(nh.reader as ReaderId);
1377
+ format = nh.reader;
1378
+ reader = (input: any) => dispatch(input);
1379
+ }
1380
+
1381
+ if (reader) {
1382
+ resolved.scripts = { ...(resolved.scripts || {}) };
1383
+ (resolved.scripts as any).readNativeHistory = reader;
1384
+ (resolved as any).nativeHistory = {
1385
+ format,
1386
+ watchPath: undefined,
1387
+ scripts: { readSession: 'readNativeHistory' },
1388
+ mode: 'native-source',
1389
+ };
1390
+ }
1391
+ }
1392
+ }
1393
+ } catch {
1394
+ // Best-effort — spec wiring failure must not break legacy providers.
1395
+ }
1396
+ }
1397
+
1009
1398
  return resolved;
1010
1399
  }
1011
1400
 
@@ -1016,16 +1405,27 @@ export class ProviderLoader {
1016
1405
  private loadScriptsFromDir(type: string, scriptDir: string): Partial<ProviderScripts> | null {
1017
1406
  const providerDir = this.findProviderDirInternal(type);
1018
1407
  if (!providerDir) {
1019
- this.log(` [loadScriptsFromDir] ${type}: providerDir not found`);
1408
+ // No provider dir for this type a spec-only provider with no
1409
+ // legacy scripts/v1 layout is a normal configuration, not a
1410
+ // problem to surface at INFO. resolve() calls this on every
1411
+ // request; INFO spam every 200ms drowns out the real signal.
1412
+ this.debugLog(`[loadScriptsFromDir] ${type}: providerDir not found`);
1020
1413
  return null;
1021
1414
  }
1022
1415
 
1023
1416
  const dir = path.join(providerDir, scriptDir);
1024
1417
  if (!fs.existsSync(dir)) {
1025
- this.log(` [loadScriptsFromDir] ${type}: dir not found: ${dir}`);
1418
+ this.debugLog(`[loadScriptsFromDir] ${type}: dir not found: ${dir}`);
1026
1419
  return null;
1027
1420
  }
1028
1421
 
1422
+ // Register the provider's *parent root* (e.g. .../adhdev-providers/) so
1423
+ // the require whitelist gates every script + every _shared helper this
1424
+ // provider may reach. Picking the grandparent (one above the category
1425
+ // dir `cli/`) lets sibling helpers in `_shared` resolve while still
1426
+ // blocking `../../etc/...` escapes. Idempotent.
1427
+ registerProviderScriptRootSafely(path.dirname(path.dirname(providerDir)));
1428
+
1029
1429
  // Return cached scripts if available (cleared on reload/watch)
1030
1430
  const cached = this.scriptsCache.get(dir);
1031
1431
  if (cached) return cached;
@@ -1036,7 +1436,7 @@ export class ProviderLoader {
1036
1436
  try {
1037
1437
  delete require.cache[require.resolve(scriptsJs)];
1038
1438
  const loaded = require(scriptsJs);
1039
- this.log(` [loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
1439
+ this.debugLog(`[loadScriptsFromDir] ${type}: loaded scripts.js from ${dir} (${Object.keys(loaded).length} exports)`);
1040
1440
  this.scriptsCache.set(dir, loaded);
1041
1441
  return loaded;
1042
1442
  } catch (e) {
@@ -1067,13 +1467,17 @@ export class ProviderLoader {
1067
1467
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
1068
1468
  });
1069
1469
 
1470
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
1070
1471
  const handleChange = (filePath: string) => {
1071
1472
  if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
1072
1473
  return;
1073
1474
  }
1074
1475
  if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
1075
- this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1076
- this.reload();
1476
+ if (reloadTimer) clearTimeout(reloadTimer);
1477
+ reloadTimer = setTimeout(() => {
1478
+ this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1479
+ this.reload();
1480
+ }, 300);
1077
1481
  }
1078
1482
  };
1079
1483
 
@@ -1124,13 +1528,118 @@ export class ProviderLoader {
1124
1528
  *
1125
1529
  * @returns Whether an update occurred
1126
1530
  */
1531
+ /**
1532
+ * Sync providers from the ADHDev registry (registry.adhf.dev).
1533
+ *
1534
+ * Downloads only providers whose server checksum differs from the locally
1535
+ * cached checksum. Falls back gracefully to the GitHub tarball path if the
1536
+ * registry is unreachable or returns an unexpected response.
1537
+ *
1538
+ * Returns `{ updated: true }` when at least one provider file changed on disk,
1539
+ * `{ updated: false }` when everything is already current, or
1540
+ * `{ updated: false, error }` when the registry couldn't be reached and we
1541
+ * should proceed to the GitHub tarball fallback.
1542
+ */
1543
+ async fetchFromRegistry(): Promise<{ updated: boolean; error?: string }> {
1544
+ if (this.disableUpstream) {
1545
+ this.log('Registry sync skipped (sourceMode=no-upstream)');
1546
+ return { updated: false };
1547
+ }
1548
+ this.log(`Registry sync starting (${ProviderLoader.REGISTRY_BASE_URL})...`);
1549
+
1550
+ const https = require('https') as typeof import('https');
1551
+ const regMetaPath = path.join(this.upstreamDir, ProviderLoader.REGISTRY_META_FILE);
1552
+
1553
+ // Load cached checksums
1554
+ let cachedChecksums: Record<string, string> = {};
1555
+ try {
1556
+ if (fs.existsSync(regMetaPath)) {
1557
+ cachedChecksums = JSON.parse(fs.readFileSync(regMetaPath, 'utf-8')).checksums ?? {};
1558
+ }
1559
+ } catch { }
1560
+
1561
+ try {
1562
+ // 1. Fetch provider list
1563
+ const listUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers`;
1564
+ const listBody = await new Promise<string>((resolve, reject) => {
1565
+ const req = https.get(listUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
1566
+ if (res.statusCode !== 200) { reject(new Error(`registry list HTTP ${res.statusCode}`)); return; }
1567
+ const chunks: Buffer[] = [];
1568
+ res.on('data', (c: Buffer) => chunks.push(c));
1569
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1570
+ });
1571
+ req.on('error', reject);
1572
+ req.on('timeout', () => { req.destroy(); reject(new Error('registry list timeout')); });
1573
+ });
1574
+
1575
+ const list = JSON.parse(listBody) as { providers: Array<{ type: string; category: string; checksum: string; version: string }> };
1576
+ if (!Array.isArray(list.providers)) throw new Error('unexpected registry response shape');
1577
+
1578
+ let updatedCount = 0;
1579
+
1580
+ for (const entry of list.providers) {
1581
+ const { type, category, checksum, version } = entry;
1582
+ const cacheKey = `${category}/${type}`;
1583
+ if (cachedChecksums[cacheKey] === checksum) continue; // already current
1584
+
1585
+ // Download this provider's manifest
1586
+ const dlUrl = `${ProviderLoader.REGISTRY_BASE_URL}/providers/${type}/${version}/download`;
1587
+ const manifestBody = await new Promise<string>((resolve, reject) => {
1588
+ const req = https.get(dlUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 30000 }, (res) => {
1589
+ if (res.statusCode !== 200) { reject(new Error(`registry download HTTP ${res.statusCode} for ${type}@${version}`)); return; }
1590
+ const chunks: Buffer[] = [];
1591
+ res.on('data', (c: Buffer) => chunks.push(c));
1592
+ res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
1593
+ });
1594
+ req.on('error', reject);
1595
+ req.on('timeout', () => { req.destroy(); reject(new Error(`download timeout for ${type}`)); });
1596
+ });
1597
+
1598
+ // Verify checksum
1599
+ const actualChecksum = await new Promise<string>((resolve) => {
1600
+ const crypto = require('crypto') as typeof import('crypto');
1601
+ resolve(crypto.createHash('sha256').update(manifestBody, 'utf-8').digest('hex'));
1602
+ });
1603
+ if (actualChecksum !== checksum) {
1604
+ this.log(`⚠ Registry checksum mismatch for ${type}@${version} — skipping`);
1605
+ continue;
1606
+ }
1607
+
1608
+ // Write to upstream dir
1609
+ const providerDir = path.join(this.upstreamDir, category, type);
1610
+ fs.mkdirSync(providerDir, { recursive: true });
1611
+ fs.writeFileSync(path.join(providerDir, 'provider.json'), manifestBody, 'utf-8');
1612
+
1613
+ cachedChecksums[cacheKey] = checksum;
1614
+ updatedCount++;
1615
+ this.log(`✓ Registry updated: ${category}/${type}@${version}`);
1616
+ }
1617
+
1618
+ // Persist updated checksums
1619
+ fs.mkdirSync(this.upstreamDir, { recursive: true });
1620
+ fs.writeFileSync(regMetaPath, JSON.stringify({
1621
+ checksums: cachedChecksums,
1622
+ syncedAt: new Date().toISOString(),
1623
+ providerCount: list.providers.length,
1624
+ }, null, 2));
1625
+
1626
+ this.log(`Registry sync complete: ${list.providers.length} providers, ${updatedCount} updated`);
1627
+ return { updated: updatedCount > 0 };
1628
+ } catch (e: any) {
1629
+ this.log(`⚠ Registry sync failed (falling back to GitHub tarball): ${e?.message}`);
1630
+ return { updated: false, error: e?.message };
1631
+ }
1632
+ }
1633
+
1127
1634
  async fetchLatest(): Promise<{ updated: boolean; error?: string }> {
1128
1635
  if (this.disableUpstream) {
1129
1636
  this.log('Upstream fetch skipped (sourceMode=no-upstream)');
1130
1637
  return { updated: false };
1131
1638
  }
1132
1639
  const https = require('https') as typeof import('https');
1133
- const { execSync } = require('child_process') as typeof import('child_process');
1640
+ const { exec } = require('child_process') as typeof import('child_process');
1641
+ const { promisify } = require('util');
1642
+ const execAsync = promisify(exec);
1134
1643
 
1135
1644
  const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
1136
1645
  let prevEtag = '';
@@ -1207,7 +1716,7 @@ export class ProviderLoader {
1207
1716
 
1208
1717
  // Extract
1209
1718
  fs.mkdirSync(tmpExtract, { recursive: true });
1210
- execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1719
+ await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1211
1720
 
1212
1721
  // Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
1213
1722
  const extracted = fs.readdirSync(tmpExtract);
@@ -1315,15 +1824,17 @@ export class ProviderLoader {
1315
1824
  } catch { }
1316
1825
  }
1317
1826
 
1318
- /** Count provider files (provider.js or provider.json) */
1827
+ /** Count provider files (provider.v1.json or provider.json — at most one per dir). */
1319
1828
  private countProviders(dir: string): number {
1320
1829
  if (!fs.existsSync(dir)) return 0;
1321
1830
  let count = 0;
1322
1831
  const scan = (d: string) => {
1323
1832
  try {
1324
- for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
1833
+ const entries = fs.readdirSync(d, { withFileTypes: true });
1834
+ const hasManifest = entries.some(e => e.name === 'provider.v1.json' || e.name === 'provider.json');
1835
+ if (hasManifest) count++;
1836
+ for (const entry of entries) {
1325
1837
  if (entry.isDirectory()) scan(path.join(d, entry.name));
1326
- else if (entry.name === 'provider.json') count++;
1327
1838
  }
1328
1839
  } catch { }
1329
1840
  };
@@ -1372,11 +1883,15 @@ export class ProviderLoader {
1372
1883
  return args ? args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(' ') : '';
1373
1884
  }
1374
1885
  const schemaDef = this.getSettingsSchema(providerType)[key];
1375
- const defaultVal = schemaDef
1376
- ? (key === 'autoApprove' && schemaDef.type === 'boolean'
1377
- ? true
1378
- : schemaDef.default)
1379
- : undefined;
1886
+ // (fix) Previously this hard-coded `autoApprove` boolean default to `true`,
1887
+ // overriding whatever schemaDef.default the provider.json declared. That
1888
+ // surfaced as soon as a provider added an `autoApprove` schema entry with
1889
+ // default=false: the user had never opted in but the daemon treated the
1890
+ // session as auto-approve, which then triggered recordAutoApproval every
1891
+ // time the CLI showed an approval modal — producing a flood of system
1892
+ // "Auto-approved: ..." messages and keeping the session pinned to
1893
+ // generating while modals cycled in and out. Trust the schemaDef.default.
1894
+ const defaultVal = schemaDef ? schemaDef.default : undefined;
1380
1895
 
1381
1896
  const config = this.readConfig();
1382
1897
  const userVal = config?.providerSettings?.[providerType]?.[key];
@@ -1490,10 +2005,17 @@ export class ProviderLoader {
1490
2005
  ...this.getSyntheticSettings(type, provider),
1491
2006
  ...(provider.settings || {}),
1492
2007
  };
2008
+ // (fix) Previously this clause forced `autoApprove.default = true` for any
2009
+ // boolean autoApprove schema, even when the provider.json explicitly set
2010
+ // `default: false`. Combined with the synthetic-settings fallback at
2011
+ // getSyntheticSettings (which also defaults autoApprove to true when the
2012
+ // provider doesn't supply one), that meant CLI providers silently turned on
2013
+ // auto-approval, producing a flood of "Auto-approved: ..." system messages
2014
+ // every time an approval modal appeared and pinning the session to
2015
+ // generating while modals cycled. Trust the provider's declared default.
1493
2016
  if (result.autoApprove?.type === 'boolean') {
1494
2017
  result.autoApprove = {
1495
2018
  ...result.autoApprove,
1496
- default: true,
1497
2019
  public: true,
1498
2020
  label: result.autoApprove.label || 'Auto Approve',
1499
2021
  description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1518,7 +2040,10 @@ export class ProviderLoader {
1518
2040
  if (!provider.settings?.autoApprove) {
1519
2041
  result.autoApprove = {
1520
2042
  type: 'boolean',
1521
- default: true,
2043
+ // (fix) Safe default is *off*. Auto-approving every modal without the
2044
+ // user opting in produced silent-bash-execution surprises and the
2045
+ // "Auto-approved: ..." system-message flood seen on AGY/Codex.
2046
+ default: false,
1522
2047
  public: true,
1523
2048
  label: 'Auto Approve',
1524
2049
  description: 'Automatically approve actionable prompts without sending approval alerts.',
@@ -1581,23 +2106,32 @@ export class ProviderLoader {
1581
2106
  const cat = provider.category;
1582
2107
 
1583
2108
  const searchRoots = this.getProviderRoots();
2109
+ const hasManifest = (dir: string) =>
2110
+ fs.existsSync(path.join(dir, 'provider.v1.json')) || fs.existsSync(path.join(dir, 'provider.json'));
2111
+ const readManifestType = (dir: string): string | null => {
2112
+ for (const file of ['provider.v1.json', 'provider.json']) {
2113
+ const p = path.join(dir, file);
2114
+ if (!fs.existsSync(p)) continue;
2115
+ try {
2116
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
2117
+ if (typeof data?.type === 'string') return data.type;
2118
+ } catch { /* skip */ }
2119
+ }
2120
+ return null;
2121
+ };
1584
2122
  for (const root of searchRoots) {
1585
2123
  if (!fs.existsSync(root)) continue;
1586
2124
  const candidate = this.getProviderDir(root, cat, type);
1587
- if (fs.existsSync(path.join(candidate, 'provider.json'))) return candidate;
2125
+ if (hasManifest(candidate)) return candidate;
1588
2126
  // Scan category dir for type match
1589
2127
  const catDir = path.join(root, cat);
1590
2128
  if (fs.existsSync(catDir)) {
1591
2129
  try {
1592
2130
  for (const entry of fs.readdirSync(catDir, { withFileTypes: true })) {
1593
2131
  if (!entry.isDirectory()) continue;
1594
- const jsonPath = path.join(catDir, entry.name, 'provider.json');
1595
- if (fs.existsSync(jsonPath)) {
1596
- try {
1597
- const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
1598
- if (data.type === type) return path.join(catDir, entry.name);
1599
- } catch { /* skip */ }
1600
- }
2132
+ const entryDir = path.join(catDir, entry.name);
2133
+ const manifestType = readManifestType(entryDir);
2134
+ if (manifestType === type) return entryDir;
1601
2135
  }
1602
2136
  } catch { /* skip */ }
1603
2137
  }
@@ -1690,17 +2224,44 @@ export class ProviderLoader {
1690
2224
  return;
1691
2225
  }
1692
2226
 
1693
- // Check if this directory has provider.json
2227
+ // v1-first manifest selection. provider.v1.json (the SDK-shape
2228
+ // manifest with `overrides`, `tui`, `source`, `canonicalHistory`)
2229
+ // wins over provider.json (legacy). Without this branch the v1
2230
+ // file is silently ignored — that's how the codex-cli `overrides`
2231
+ // path and the tui-block builders went un-honored for the first
2232
+ // pass of SDK rollout.
2233
+ const hasV1 = entries.some(e => e.name === 'provider.v1.json');
1694
2234
  const hasJson = entries.some(e => e.name === 'provider.json');
1695
2235
 
1696
- if (hasJson) {
1697
- const jsonPath = path.join(d, 'provider.json');
2236
+ if (hasV1 || hasJson) {
2237
+ const manifestFile = hasV1 ? 'provider.v1.json' : 'provider.json';
2238
+ const jsonPath = path.join(d, manifestFile);
1698
2239
  try {
1699
2240
  const raw = fs.readFileSync(jsonPath, 'utf-8');
1700
2241
  const mod = JSON.parse(raw) as Omit<ProviderModule, 'extensionIdPattern'> & {
1701
2242
  extensionIdPattern?: RegExp | string;
1702
2243
  };
1703
2244
 
2245
+ // Validate v1 manifests against the SDK schema. Failures are
2246
+ // surfaced as a single warning line with all issues attached
2247
+ // so manifest authors don't need to guess which field is wrong.
2248
+ // Loading still proceeds — bricking the daemon on a single
2249
+ // bad field would be worse than running with a known warning.
2250
+ if (hasV1 && mod?.category === 'cli') {
2251
+ try {
2252
+ const { validateCliProviderManifest, formatManifestValidationIssues } =
2253
+ require('./sdk/v1/validators/manifest.js') as typeof import('./sdk/v1/validators/manifest.js');
2254
+ const validation = validateCliProviderManifest(mod);
2255
+ if (!validation.ok) {
2256
+ this.log(`⚠ ${jsonPath}: schema validation failed:\n${formatManifestValidationIssues(validation.issues)}`);
2257
+ }
2258
+ } catch (e: any) {
2259
+ // Validator load failed — log once and continue so a
2260
+ // broken validator can't take down provider loading.
2261
+ this.log(`⚠ ${jsonPath}: validator unavailable: ${e?.message || e}`);
2262
+ }
2263
+ }
2264
+
1704
2265
  // Restore RegExp fields from JSON (extensionIdPattern)
1705
2266
  if (typeof mod.extensionIdPattern === 'string') {
1706
2267
  const flags = mod.extensionIdPattern_flags || '';
@@ -1712,6 +2273,22 @@ export class ProviderLoader {
1712
2273
  ...(extensionIdPattern instanceof RegExp ? { extensionIdPattern } : {}),
1713
2274
  };
1714
2275
 
2276
+ // v1 manifests use `nativeHistory` as the canonical field name.
2277
+ // Legacy v0 manifests use `canonicalHistory`. The daemon's
2278
+ // runtime + downstream code reads `provider.nativeHistory`, so
2279
+ // for legacy manifests we copy `canonicalHistory` into
2280
+ // `nativeHistory` here. We also keep `canonicalHistory`
2281
+ // populated in both directions (deprecated alias) so any
2282
+ // external consumers still reading the old name keep working
2283
+ // during the one-release deprecation window.
2284
+ const nh = (normalizedProvider as any).nativeHistory;
2285
+ const ch = (normalizedProvider as any).canonicalHistory;
2286
+ if (nh && !ch) {
2287
+ (normalizedProvider as any).canonicalHistory = nh;
2288
+ } else if (ch && !nh) {
2289
+ (normalizedProvider as any).nativeHistory = ch;
2290
+ }
2291
+
1715
2292
  const validation = validateProviderDefinition(normalizedProvider);
1716
2293
  for (const warning of validation.warnings) {
1717
2294
  this.log(`⚠ ${jsonPath}: ${warning}`);
@@ -1725,6 +2302,10 @@ export class ProviderLoader {
1725
2302
  const scriptsPath = path.join(d, 'scripts.js');
1726
2303
  if (!hasCompatibility && fs.existsSync(scriptsPath)) {
1727
2304
  try {
2305
+ // Gate the IDE/extension scripts.js (legacy single-file
2306
+ // format) under the same whitelist. `d` here is the
2307
+ // provider dir; its grandparent contains _shared.
2308
+ registerProviderScriptRootSafely(path.dirname(path.dirname(d)));
1728
2309
  delete require.cache[require.resolve(scriptsPath)];
1729
2310
  const scripts = require(scriptsPath) as Partial<ProviderScripts>;
1730
2311
  normalizedProvider.scripts = scripts;
@@ -1733,14 +2314,40 @@ export class ProviderLoader {
1733
2314
  }
1734
2315
  }
1735
2316
 
2317
+ // Classify trust based on which on-disk layer this manifest
2318
+ // came from + whether it ships JavaScript hooks. The dashboard
2319
+ // uses this to render trust badges; non-spec external manifests
2320
+ // need an explicit user confirm before activation.
2321
+ const externalDirAbs = path.join(os.homedir(), '.adhdev', 'external');
2322
+ const layer: 'user' | 'upstream' | 'external' = d.startsWith(externalDirAbs)
2323
+ ? 'external'
2324
+ : (d.startsWith(this.userDir) && !d.includes('.upstream') ? 'user' : 'upstream');
2325
+ try {
2326
+ const { inspectManifestShape, classifyTrust } =
2327
+ require('./provider-trust.js') as typeof import('./provider-trust.js');
2328
+ const shape = inspectManifestShape(mod as Record<string, unknown>);
2329
+ const trust = classifyTrust(layer, shape);
2330
+ (normalizedProvider as any)._sourceLayer = layer;
2331
+ (normalizedProvider as any)._sourceTrust = trust;
2332
+ (normalizedProvider as any)._manifestShape = shape;
2333
+ // For external-namespaced layouts (external/<source>/…) record
2334
+ // which source the manifest came from so dashboards can name
2335
+ // it in the trust badge.
2336
+ if (layer === 'external') {
2337
+ const rel = path.relative(externalDirAbs, d);
2338
+ const firstSeg = rel.split(path.sep)[0];
2339
+ if (firstSeg && firstSeg !== '..') (normalizedProvider as any)._sourceName = firstSeg;
2340
+ }
2341
+ } catch { /* best-effort — trust is enrichment, not gating */ }
2342
+
1736
2343
  const existed = this.providers.has(normalizedProvider.type);
1737
2344
  this.providers.set(normalizedProvider.type, normalizedProvider);
1738
2345
  count++;
1739
- // Identify source tier for debugging
1740
- const source = d.startsWith(this.userDir) && !d.includes('.upstream')
1741
- ? 'user' : 'upstream';
2346
+ const source = (normalizedProvider as any)._sourceLayer ?? 'upstream';
1742
2347
  const overrideWarning = existed && source === 'user' ? ' ⚠ OVERRIDES upstream' : '';
1743
- this.log(` ${existed ? '🔄' : '✅'} ${normalizedProvider.type} (${normalizedProvider.category}) ${normalizedProvider.name} [${source}]${overrideWarning}`);
2348
+ const sourceName = (normalizedProvider as any)._sourceName;
2349
+ const sourceLabel = sourceName ? `${source}/${sourceName}` : source;
2350
+ this.log(` ${existed ? '🔄' : '✅'} ${normalizedProvider.type} (${normalizedProvider.category}) — ${normalizedProvider.name} [${sourceLabel}]${overrideWarning}`);
1744
2351
  }
1745
2352
  } catch (e) {
1746
2353
  this.log(`⚠ Failed to load ${jsonPath}: ${(e as Error).message}`);
@@ -1752,6 +2359,11 @@ export class ProviderLoader {
1752
2359
  for (const entry of entries) {
1753
2360
  if (!entry.isDirectory()) continue;
1754
2361
  if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
2362
+ // `examples/` is a documentation / scaffold tree (e.g. stub-cli),
2363
+ // not a real provider source. SDK authors copy from here when
2364
+ // writing a new provider; daemon-core tests reference the
2365
+ // manifest by path. Keep it off the dashboard's provider list.
2366
+ if (d === dir && entry.name === 'examples') continue;
1755
2367
  if (excludeDirs && d === dir && excludeDirs.includes(entry.name)) continue;
1756
2368
  scan(path.join(d, entry.name));
1757
2369
  }