@vellumai/assistant 0.6.0 → 0.6.2

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 (358) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -3,6 +3,9 @@ import { join } from "node:path";
3
3
 
4
4
  import type { Command } from "commander";
5
5
 
6
+ import { getConfig } from "../../config/loader.js";
7
+ import { resolveSkillStates } from "../../config/skill-state.js";
8
+ import { loadSkillCatalog } from "../../config/skills.js";
6
9
  import type { CatalogSkill } from "../../skills/catalog-install.js";
7
10
  import {
8
11
  fetchCatalog,
@@ -56,13 +59,13 @@ Examples:
56
59
 
57
60
  skills
58
61
  .command("list")
59
- .description("List available catalog skills")
62
+ .description("List all skills (bundled, installed, and catalog)")
60
63
  .option("--json", "Machine-readable JSON output")
61
64
  .addHelpText(
62
65
  "after",
63
66
  `
64
- Lists all skills available in the Vellum catalog with their ID, name,
65
- description, and dependency information.
67
+ Lists all skills: bundled (compiled-in), installed (user-added), and
68
+ available catalog skills with their ID, name, and description.
66
69
 
67
70
  Examples:
68
71
  $ assistant skills list
@@ -70,34 +73,103 @@ Examples:
70
73
  )
71
74
  .action(async (opts: { json?: boolean }) => {
72
75
  try {
73
- // In dev mode, use the local catalog as the source of truth
74
- // and skip the remote Platform API entirely.
76
+ // ── Bundled + installed skills (from loadSkillCatalog) ────────
77
+ const localCatalog = loadSkillCatalog();
78
+ const config = getConfig();
79
+ const resolved = resolveSkillStates(localCatalog, config);
80
+ const bundled = resolved.filter(
81
+ (r) => r.summary.source === "bundled",
82
+ );
83
+ const installed = resolved.filter(
84
+ (r) =>
85
+ r.summary.source === "managed" ||
86
+ r.summary.source === "workspace" ||
87
+ r.summary.source === "extra",
88
+ );
89
+
90
+ // ── Remote catalog skills ────────────────────────────────────
75
91
  const repoSkillsDir = getRepoSkillsDir();
76
- let catalog: CatalogSkill[];
92
+ let remoteCatalog: CatalogSkill[];
77
93
  if (repoSkillsDir) {
78
- catalog = readLocalCatalog(repoSkillsDir);
94
+ remoteCatalog = readLocalCatalog(repoSkillsDir);
79
95
  } else {
80
- catalog = await fetchCatalog();
96
+ remoteCatalog = await fetchCatalog();
81
97
  }
98
+ // Exclude catalog skills that are already installed/bundled
99
+ const localIds = new Set(localCatalog.map((s) => s.id));
100
+ const availableCatalog = remoteCatalog.filter(
101
+ (s) => !localIds.has(s.id),
102
+ );
103
+
104
+ const totalCount =
105
+ bundled.length + installed.length + availableCatalog.length;
82
106
 
83
107
  if (opts.json) {
84
- console.log(JSON.stringify({ ok: true, skills: catalog }));
108
+ const bundledJson = bundled.map((r) => ({
109
+ id: r.summary.id,
110
+ name: r.summary.displayName,
111
+ description: r.summary.description,
112
+ emoji: r.summary.emoji,
113
+ state: r.state,
114
+ }));
115
+ const installedJson = installed.map((r) => ({
116
+ id: r.summary.id,
117
+ name: r.summary.displayName,
118
+ description: r.summary.description,
119
+ emoji: r.summary.emoji,
120
+ state: r.state,
121
+ }));
122
+ console.log(
123
+ JSON.stringify({
124
+ ok: true,
125
+ skills: [...bundledJson, ...installedJson, ...availableCatalog],
126
+ bundled: bundledJson,
127
+ installed: installedJson,
128
+ catalog: availableCatalog,
129
+ }),
130
+ );
85
131
  return;
86
132
  }
87
133
 
88
- if (catalog.length === 0) {
89
- log.info("No skills available in the catalog.");
134
+ if (totalCount === 0) {
135
+ log.info("No skills available.");
90
136
  return;
91
137
  }
92
138
 
93
- log.info(`Available skills (${catalog.length}):\n`);
94
- for (const s of catalog) {
95
- const emoji = s.emoji ? `${s.emoji} ` : "";
96
- const deps = s.includes?.length
97
- ? ` (requires: ${s.includes.join(", ")})`
98
- : "";
99
- log.info(` ${emoji}${s.id}`);
100
- log.info(` ${s.name} — ${s.description}${deps}`);
139
+ if (bundled.length > 0) {
140
+ log.info(`Bundled skills (${bundled.length}):\n`);
141
+ for (const r of bundled) {
142
+ const s = r.summary;
143
+ const emoji = s.emoji ? `${s.emoji} ` : "";
144
+ const state = r.state === "disabled" ? " [disabled]" : "";
145
+ log.info(` ${emoji}${s.id}${state}`);
146
+ log.info(` ${s.displayName} — ${s.description}`);
147
+ }
148
+ log.info("");
149
+ }
150
+
151
+ if (installed.length > 0) {
152
+ log.info(`Installed skills (${installed.length}):\n`);
153
+ for (const r of installed) {
154
+ const s = r.summary;
155
+ const emoji = s.emoji ? `${s.emoji} ` : "";
156
+ const state = r.state === "disabled" ? " [disabled]" : "";
157
+ log.info(` ${emoji}${s.id}${state}`);
158
+ log.info(` ${s.displayName} — ${s.description}`);
159
+ }
160
+ log.info("");
161
+ }
162
+
163
+ if (availableCatalog.length > 0) {
164
+ log.info(`Available catalog skills (${availableCatalog.length}):\n`);
165
+ for (const s of availableCatalog) {
166
+ const emoji = s.emoji ? `${s.emoji} ` : "";
167
+ const deps = s.includes?.length
168
+ ? ` (requires: ${s.includes.join(", ")})`
169
+ : "";
170
+ log.info(` ${emoji}${s.id}`);
171
+ log.info(` ${s.name} — ${s.description}${deps}`);
172
+ }
101
173
  }
102
174
  } catch (err) {
103
175
  const msg = err instanceof Error ? err.message : String(err);
@@ -139,6 +211,14 @@ Examples:
139
211
  const limit = parseInt(opts.limit, 10) || 10;
140
212
 
141
213
  try {
214
+ // ── Bundled + installed skill search ─────────────────────────
215
+ const localCatalog = loadSkillCatalog();
216
+ const bundledMatches = filterByQuery(localCatalog, query, [
217
+ (s) => s.id,
218
+ (s) => s.displayName,
219
+ (s) => s.description,
220
+ ]);
221
+
142
222
  // ── Vellum catalog search ────────────────────────────────────
143
223
  const repoSkillsDir = getRepoSkillsDir();
144
224
  let catalog: CatalogSkill[];
@@ -151,8 +231,11 @@ Examples:
151
231
  catalog = [];
152
232
  }
153
233
  }
234
+ // Exclude catalog entries that match a bundled/installed skill
235
+ const localIds = new Set(localCatalog.map((s) => s.id));
236
+ const filteredCatalog = catalog.filter((s) => !localIds.has(s.id));
154
237
 
155
- const catalogMatches = filterByQuery(catalog, query, [
238
+ const catalogMatches = filterByQuery(filteredCatalog, query, [
156
239
  (s) => s.id,
157
240
  (s) => s.name,
158
241
  (s) => s.description,
@@ -202,6 +285,7 @@ Examples:
202
285
  ]);
203
286
 
204
287
  if (
288
+ bundledMatches.length === 0 &&
205
289
  catalogMatches.length === 0 &&
206
290
  registryResults.length === 0 &&
207
291
  clawhubResults.length === 0
@@ -210,6 +294,7 @@ Examples:
210
294
  console.log(
211
295
  JSON.stringify({
212
296
  ok: true,
297
+ bundled: [],
213
298
  catalog: [],
214
299
  community: [],
215
300
  clawhub: [],
@@ -255,6 +340,13 @@ Examples:
255
340
  console.log(
256
341
  JSON.stringify({
257
342
  ok: true,
343
+ bundled: bundledMatches.map((s) => ({
344
+ id: s.id,
345
+ name: s.displayName,
346
+ description: s.description,
347
+ emoji: s.emoji,
348
+ source: s.source,
349
+ })),
258
350
  catalog: catalogMatches,
259
351
  community: registryResults,
260
352
  clawhub: clawhubResults,
@@ -271,6 +363,24 @@ Examples:
271
363
  const isInstalled = (id: string) =>
272
364
  existsSync(join(skillsDir, id, "SKILL.md"));
273
365
 
366
+ // ── Display bundled/installed results ─────────────────────────
367
+ if (bundledMatches.length > 0) {
368
+ log.info(
369
+ `Bundled & installed skills (${bundledMatches.length}):\n`,
370
+ );
371
+ for (const s of bundledMatches) {
372
+ const emoji = s.emoji ? `${s.emoji} ` : "";
373
+ const tag = s.source === "bundled" ? " [bundled]" : " [installed]";
374
+ log.info(` ${emoji}${s.displayName}${tag}`);
375
+ if (s.displayName !== s.id) {
376
+ log.info(` ID: ${s.id}`);
377
+ }
378
+ log.info(` ${s.description}`);
379
+ log.info(` Load: skill_load skill=${s.id}`);
380
+ log.info("");
381
+ }
382
+ }
383
+
274
384
  // ── Display catalog results ──────────────────────────────────
275
385
  if (catalogMatches.length > 0) {
276
386
  log.info(`Vellum catalog (${catalogMatches.length}):\n`);
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
 
3
+ import { initFeatureFlagOverrides } from "../config/assistant-feature-flags.js";
3
4
  import { getConfig } from "../config/loader.js";
4
5
  import { isEmailEnabled } from "../email/feature-gate.js";
5
6
  import { registerHooksCommand } from "../hooks/cli.js";
@@ -26,19 +27,26 @@ import { registerMemoryCommand } from "./commands/memory.js";
26
27
  import { registerNotificationsCommand } from "./commands/notifications.js";
27
28
  import { registerOAuthCommand } from "./commands/oauth/index.js";
28
29
  import { registerPlatformCommand } from "./commands/platform/index.js";
30
+ import { registerRoutesCommand } from "./commands/routes.js";
29
31
  import { registerSequenceCommand } from "./commands/sequence.js";
30
32
  import { registerShotgunCommand } from "./commands/shotgun.js";
31
33
  import { registerSkillsCommand } from "./commands/skills.js";
32
34
  import { registerTrustCommand } from "./commands/trust.js";
33
35
  import { registerUsageCommand } from "./commands/usage.js";
34
36
 
35
- export function buildCliProgram(): Command {
37
+ /**
38
+ * Build the CLI program tree. Pre-populates the feature flag cache from
39
+ * the gateway so flag-gated commands are registered correctly.
40
+ */
41
+ export async function buildCliProgram(): Promise<Command> {
42
+ await initFeatureFlagOverrides();
36
43
  const program = new Command();
37
44
 
38
45
  program
39
46
  .name("assistant")
40
47
  .description("Local AI assistant")
41
- .version(APP_VERSION);
48
+ .version(APP_VERSION)
49
+ .allowExcessArguments(true);
42
50
 
43
51
  program.addHelpText(
44
52
  "after",
@@ -75,6 +83,7 @@ Examples:
75
83
  registerNotificationsCommand(program);
76
84
  registerPlatformCommand(program);
77
85
  registerOAuthCommand(program);
86
+ registerRoutesCommand(program);
78
87
  registerSkillsCommand(program);
79
88
  registerBrowserRelayCommand(program);
80
89
  registerUsageCommand(program);
package/src/cli.ts CHANGED
@@ -33,11 +33,6 @@ import {
33
33
  type EventStreamWatcher,
34
34
  watchEventStream,
35
35
  } from "./signals/event-stream.js";
36
- import {
37
- copyToClipboard,
38
- extractLastCodeBlock,
39
- formatConversationForExport,
40
- } from "./util/clipboard.js";
41
36
  import { formatDiff, formatNewFileDiff } from "./util/diff.js";
42
37
  import { getHistoryPath, getSignalsDir } from "./util/platform.js";
43
38
  import { Spinner } from "./util/spinner.js";
@@ -143,7 +138,6 @@ export async function startCli(): Promise<void> {
143
138
  let conversationId = "";
144
139
  let pendingUserContent: string | null = null;
145
140
  let generating = false;
146
- let lastResponse = "";
147
141
  let lastUsage: {
148
142
  inputTokens: number;
149
143
  outputTokens: number;
@@ -153,7 +147,6 @@ export async function startCli(): Promise<void> {
153
147
  model: string;
154
148
  } | null = null;
155
149
  let pendingSessionPick = false;
156
- let pendingCopySession = false;
157
150
  let toolStreaming = false;
158
151
  let lastDisplayedError: string | null = null;
159
152
  let eventSubscription: EventStreamWatcher | null = null;
@@ -398,7 +391,6 @@ export async function startCli(): Promise<void> {
398
391
  if (pendingUserContent) {
399
392
  const content = pendingUserContent;
400
393
  pendingUserContent = null;
401
- lastResponse = "";
402
394
  sendUserMessage(content).then((result) => {
403
395
  if (result.ok) {
404
396
  generating = true;
@@ -408,7 +400,6 @@ export async function startCli(): Promise<void> {
408
400
  process.stdout.write(`${result.message}\n`);
409
401
  rl.question("Send anyway? (y/N): ", (answer) => {
410
402
  if (answer.trim().toLowerCase() === "y") {
411
- lastResponse = "";
412
403
  sendUserMessage(content, {
413
404
  bypassSecretCheck: true,
414
405
  }).then((retryResult) => {
@@ -439,7 +430,6 @@ export async function startCli(): Promise<void> {
439
430
 
440
431
  case "assistant_text_delta":
441
432
  spinner.stop();
442
- lastResponse += msg.text;
443
433
  process.stdout.write(msg.text);
444
434
  break;
445
435
 
@@ -595,9 +585,8 @@ export async function startCli(): Promise<void> {
595
585
  case "error":
596
586
  spinner.stop();
597
587
  generating = false;
598
- if (pendingSessionPick || pendingCopySession) {
588
+ if (pendingSessionPick) {
599
589
  pendingSessionPick = false;
600
- pendingCopySession = false;
601
590
  rl.removeAllListeners("line");
602
591
  rl.on("line", handleLine);
603
592
  }
@@ -646,26 +635,6 @@ export async function startCli(): Promise<void> {
646
635
  break;
647
636
 
648
637
  case "history_response":
649
- if (pendingCopySession) {
650
- pendingCopySession = false;
651
- if (msg.messages.length === 0) {
652
- process.stdout.write("\n No messages to copy.\n\n");
653
- } else {
654
- try {
655
- const formatted = formatConversationForExport(msg.messages);
656
- copyToClipboard(formatted);
657
- process.stdout.write(
658
- `\n Copied conversation (${msg.messages.length} messages) to clipboard.\n\n`,
659
- );
660
- } catch (err) {
661
- process.stdout.write(
662
- `\n Clipboard error: ${(err as Error).message}\n\n`,
663
- );
664
- }
665
- }
666
- prompt();
667
- break;
668
- }
669
638
  process.stdout.write("\n");
670
639
  if (msg.messages.length === 0) {
671
640
  process.stdout.write(" No messages in this conversation.\n");
@@ -686,7 +655,6 @@ export async function startCli(): Promise<void> {
686
655
  if (msg.removedCount === 0) {
687
656
  process.stdout.write("\n Nothing to undo.\n\n");
688
657
  } else {
689
- lastResponse = "";
690
658
  process.stdout.write(
691
659
  `\n Removed last exchange (${msg.removedCount} messages).\n\n`,
692
660
  );
@@ -754,21 +722,6 @@ export async function startCli(): Promise<void> {
754
722
  /* ignore */
755
723
  }
756
724
 
757
- if (content === "/copy") {
758
- if (!lastResponse) {
759
- process.stdout.write("No response to copy.\n");
760
- } else {
761
- try {
762
- copyToClipboard(lastResponse);
763
- process.stdout.write("Copied to clipboard.\n");
764
- } catch (err) {
765
- process.stdout.write(`Clipboard error: ${(err as Error).message}\n`);
766
- }
767
- }
768
- prompt();
769
- return;
770
- }
771
-
772
725
  if (content === "/conversations") {
773
726
  pendingSessionPick = true;
774
727
  try {
@@ -787,65 +740,6 @@ export async function startCli(): Promise<void> {
787
740
  return;
788
741
  }
789
742
 
790
- if (content === "/copy-code") {
791
- const code = extractLastCodeBlock(lastResponse);
792
- if (code == null) {
793
- process.stdout.write("No code block found.\n");
794
- } else {
795
- try {
796
- copyToClipboard(code);
797
- process.stdout.write("Copied code block to clipboard.\n");
798
- } catch (err) {
799
- process.stdout.write(`Clipboard error: ${(err as Error).message}\n`);
800
- }
801
- }
802
- prompt();
803
- return;
804
- }
805
-
806
- if (content === "/copy-conversation") {
807
- try {
808
- const mapping = getConversationByKey(conversationKey);
809
- if (!mapping) {
810
- process.stdout.write("\n No messages to copy.\n\n");
811
- prompt();
812
- return;
813
- }
814
- const rawMessages = getMessages(mapping.conversationId);
815
- if (rawMessages.length === 0) {
816
- process.stdout.write("\n No messages to copy.\n\n");
817
- } else {
818
- const rendered = rawMessages.map((msg) => {
819
- let parsedContent: unknown;
820
- try {
821
- parsedContent = JSON.parse(msg.content);
822
- } catch {
823
- parsedContent = msg.content;
824
- }
825
- return {
826
- role: msg.role as "user" | "assistant",
827
- text: renderHistoryContent(parsedContent).text,
828
- };
829
- });
830
- try {
831
- const formatted = formatConversationForExport(rendered);
832
- copyToClipboard(formatted);
833
- process.stdout.write(
834
- `\n Copied conversation (${rawMessages.length} messages) to clipboard.\n\n`,
835
- );
836
- } catch (err) {
837
- process.stdout.write(
838
- `\n Clipboard error: ${(err as Error).message}\n\n`,
839
- );
840
- }
841
- }
842
- } catch {
843
- process.stdout.write("[Failed to fetch history]\n");
844
- }
845
- prompt();
846
- return;
847
- }
848
-
849
743
  if (content === "/new") {
850
744
  // Create a new conversation by using a unique key
851
745
  conversationKey = `builtin-cli:${randomUUID()}`;
@@ -859,7 +753,6 @@ export async function startCli(): Promise<void> {
859
753
  }
860
754
 
861
755
  if (content === "/clear") {
862
- lastResponse = "";
863
756
  process.stdout.write("\x1b[r");
864
757
  process.stdout.write("\x1b[2J\x1b[H");
865
758
  mainScreenLayout = renderMainScreen();
@@ -955,7 +848,6 @@ export async function startCli(): Promise<void> {
955
848
  if (result.removedCount === 0) {
956
849
  process.stdout.write("\n Nothing to undo.\n\n");
957
850
  } else {
958
- lastResponse = "";
959
851
  process.stdout.write(
960
852
  `\n Removed last exchange (${result.removedCount} messages).\n\n`,
961
853
  );
@@ -1016,15 +908,6 @@ export async function startCli(): Promise<void> {
1016
908
  " /undo Remove last message exchange\n",
1017
909
  );
1018
910
  process.stdout.write(" /usage Show token usage and cost\n");
1019
- process.stdout.write(
1020
- " /copy Copy last response to clipboard\n",
1021
- );
1022
- process.stdout.write(
1023
- " /copy-code Copy last code block to clipboard\n",
1024
- );
1025
- process.stdout.write(
1026
- " /copy-conversation Copy entire conversation to clipboard\n",
1027
- );
1028
911
  process.stdout.write(" /help Show this help\n");
1029
912
  process.stdout.write("\n");
1030
913
  prompt();
@@ -1032,14 +915,12 @@ export async function startCli(): Promise<void> {
1032
915
  }
1033
916
 
1034
917
  // Regular user message
1035
- lastResponse = "";
1036
918
  sendUserMessage(content).then((result) => {
1037
919
  if (!result.ok) {
1038
920
  if (result.error === "secret_blocked" && result.message) {
1039
921
  process.stdout.write(`${result.message}\n`);
1040
922
  rl.question("Send anyway? (y/N): ", (answer) => {
1041
923
  if (answer.trim().toLowerCase() === "y") {
1042
- lastResponse = "";
1043
924
  sendUserMessage(content, { bypassSecretCheck: true }).then(
1044
925
  (retryResult) => {
1045
926
  if (retryResult.ok) {
@@ -20,7 +20,6 @@ import { existsSync, readFileSync } from "node:fs";
20
20
  import { homedir } from "node:os";
21
21
  import { dirname, join } from "node:path";
22
22
 
23
- import { getIsContainerized } from "./env-registry.js";
24
23
  import type { AssistantConfig } from "./schema.js";
25
24
 
26
25
  // ---------------------------------------------------------------------------
@@ -173,61 +172,49 @@ function loadOverridesFromFile(): Record<string, boolean> {
173
172
  }
174
173
 
175
174
  /**
176
- * Load override values from the gateway via synchronous HTTP call.
175
+ * Fetch override values from the gateway via async HTTP.
177
176
  *
178
- * Follows the trust-client pattern: uses `Bun.spawnSync` + `curl` to make
179
- * a blocking GET request to the gateway's feature-flags endpoint. The
180
- * gateway returns `{ flags: Array<{ key, enabled, ... }> }` and we extract
181
- * just the key → enabled map.
177
+ * Returns the gateway's merged feature flag map (persisted > remote >
178
+ * registry), or an empty record on any failure (network, auth, parse).
182
179
  */
183
- function loadOverridesFromGateway(): Record<string, boolean> {
180
+ async function fetchOverridesFromGateway(): Promise<Record<string, boolean>> {
184
181
  try {
185
182
  // Lazy-import to avoid circular dependency and keep this module
186
183
  // importable from bootstrap code when not in containerized mode.
187
184
  const { getGatewayInternalBaseUrl } =
188
185
  // eslint-disable-next-line @typescript-eslint/no-require-imports
189
186
  require("./env.js") as typeof import("./env.js");
190
- const { mintEdgeRelayToken } =
187
+ const {
188
+ mintEdgeRelayToken,
189
+ isSigningKeyInitialized,
190
+ initAuthSigningKey,
191
+ resolveSigningKey,
192
+ } =
191
193
  // eslint-disable-next-line @typescript-eslint/no-require-imports
192
194
  require("../runtime/auth/token-service.js") as typeof import("../runtime/auth/token-service.js");
193
195
 
196
+ // CLI subprocesses don't run daemon startup, so the signing key
197
+ // may not be initialized yet. Initialize it now so mintEdgeRelayToken
198
+ // can produce a valid JWT for the gateway request.
199
+ if (!isSigningKeyInitialized()) {
200
+ initAuthSigningKey(resolveSigningKey());
201
+ }
202
+
194
203
  const url = `${getGatewayInternalBaseUrl()}/v1/feature-flags`;
195
204
  const token = mintEdgeRelayToken();
196
205
 
197
- const proc = Bun.spawnSync(
198
- [
199
- "curl",
200
- "-s",
201
- "-S",
202
- "-X",
203
- "GET",
204
- "--max-time",
205
- "10",
206
- "-H",
207
- `Authorization: Bearer ${token}`,
208
- "-H",
209
- "Accept: application/json",
210
- "-w",
211
- "\n%{http_code}",
212
- url,
213
- ],
214
- { stdout: "pipe", stderr: "pipe" },
215
- );
216
-
217
- if (proc.exitCode !== 0) return {};
218
-
219
- const output = proc.stdout.toString().trim();
220
- const lastNewline = output.lastIndexOf("\n");
221
- const responseBody = lastNewline >= 0 ? output.slice(0, lastNewline) : "";
222
- const statusCode = parseInt(
223
- lastNewline >= 0 ? output.slice(lastNewline + 1) : output,
224
- 10,
225
- );
226
-
227
- if (statusCode < 200 || statusCode >= 300) return {};
228
- if (!responseBody) return {};
229
-
230
- const parsed = JSON.parse(responseBody) as {
206
+ const response = await fetch(url, {
207
+ method: "GET",
208
+ headers: {
209
+ Authorization: `Bearer ${token}`,
210
+ Accept: "application/json",
211
+ },
212
+ signal: AbortSignal.timeout(10_000),
213
+ });
214
+
215
+ if (!response.ok) return {};
216
+
217
+ const parsed = (await response.json()) as {
231
218
  flags?: Array<{ key: string; enabled: boolean }>;
232
219
  };
233
220
  if (!Array.isArray(parsed.flags)) return {};
@@ -245,25 +232,42 @@ function loadOverridesFromGateway(): Record<string, boolean> {
245
232
  }
246
233
 
247
234
  /**
248
- * Load overrides, preferring the gateway HTTP API.
235
+ * Pre-populate the override cache from the gateway (async).
249
236
  *
250
- * In containerized mode, always uses the gateway. In local mode, tries
251
- * the gateway first and falls back to `loadOverridesFromFile()` when
252
- * the gateway is not yet available (startup race).
237
+ * Call this once during startup (daemon or CLI entry) before any sync
238
+ * `isAssistantFeatureFlagEnabled` calls. In containerized mode, always
239
+ * uses the gateway. In local mode, falls back to the local file when
240
+ * the gateway is unreachable.
253
241
  *
254
- * Results are cached at module level.
242
+ * On failure, the cache is left unset so subsequent sync calls fall
243
+ * through to the file-based fallback rather than caching an empty map
244
+ * that masks all overrides for the process lifetime.
255
245
  */
256
- function loadOverrides(): Record<string, boolean> {
257
- if (cachedOverrides != null) return cachedOverrides;
258
-
259
- const gatewayOverrides = loadOverridesFromGateway();
260
- if (Object.keys(gatewayOverrides).length > 0 || getIsContainerized()) {
246
+ export async function initFeatureFlagOverrides(): Promise<void> {
247
+ const gatewayOverrides = await fetchOverridesFromGateway();
248
+ if (Object.keys(gatewayOverrides).length > 0) {
261
249
  cachedOverrides = gatewayOverrides;
262
- return cachedOverrides;
250
+ return;
263
251
  }
264
252
 
265
- // Graceful fallback: in local mode, if the gateway hasn't started yet
266
- // (empty response), read overrides from file as a temporary measure.
253
+ // Gateway returned empty or failed. Leave the cache unset so
254
+ // loadOverrides() falls through to file on the next sync read,
255
+ // regardless of containerized vs local mode.
256
+ }
257
+
258
+ /**
259
+ * Read cached overrides synchronously.
260
+ *
261
+ * If `initFeatureFlagOverrides()` was called at startup, this returns the
262
+ * pre-populated cache. Otherwise falls back to the local file — this
263
+ * ensures the resolver never blocks on a network call.
264
+ */
265
+ function loadOverrides(): Record<string, boolean> {
266
+ if (cachedOverrides != null) return cachedOverrides;
267
+
268
+ // Cache not yet populated (initFeatureFlagOverrides wasn't called or
269
+ // hasn't finished). Fall back to the local file so the resolver still
270
+ // works, just without gateway data.
267
271
  cachedOverrides = loadOverridesFromFile();
268
272
  return cachedOverrides;
269
273
  }