@vellumai/assistant 0.8.1 → 0.8.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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -151,6 +151,7 @@ describe("PR 11 — inbound Slack message metadata persistence", () => {
151
151
  channelTs: "1700000001.111111",
152
152
  threadTs: "1700000000.000001",
153
153
  displayName: "Alice",
154
+ actorExternalUserId: "U_ALICE",
154
155
  },
155
156
  },
156
157
  undefined,
@@ -164,6 +165,7 @@ describe("PR 11 — inbound Slack message metadata persistence", () => {
164
165
  expect(slackMeta!.channelTs).toBe("1700000001.111111");
165
166
  expect(slackMeta!.threadTs).toBe("1700000000.000001");
166
167
  expect(slackMeta!.displayName).toBe("Alice");
168
+ expect(slackMeta!.actorExternalUserId).toBe("U_ALICE");
167
169
  });
168
170
 
169
171
  test("Slack top-level message: slackMeta has no threadTs", async () => {
@@ -35,7 +35,7 @@ mock.module("../security/secure-keys.js", () => ({
35
35
  }));
36
36
 
37
37
  // Managed proxy context — always unavailable in this test (no platform auth).
38
- mock.module("../providers/managed-proxy/context.js", () => ({
38
+ mock.module("../providers/platform-proxy/context.js", () => ({
39
39
  buildManagedBaseUrl: async () => null,
40
40
  resolveManagedProxyContext: async () => {
41
41
  throw new Error("managed proxy not available in test");
@@ -1,6 +1,7 @@
1
1
  import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import { makeMockLogger } from "./helpers/mock-logger.js";
4
+ import { waitFor } from "./helpers/wait-for.js";
4
5
 
5
6
  mock.module("../util/logger.js", () => ({
6
7
  getLogger: () => makeMockLogger(),
@@ -86,8 +87,9 @@ describe("inference-profile-session-reaper", () => {
86
87
  });
87
88
 
88
89
  tickInferenceProfileReaper();
89
- // Allow microtasks (promise callbacks) to settle
90
- await Promise.resolve();
90
+ await waitFor(() => publishedEvents.length === 2, {
91
+ message: "Timed out waiting for inference profile reaper event",
92
+ });
91
93
 
92
94
  // Expired rows should be cleared
93
95
  expect(getConversation(conv1.id)?.inferenceProfile).toBeNull();
@@ -19,8 +19,10 @@ mock.module("../util/logger.js", () => ({
19
19
  // Stub the event hub so tests don't need a running event bus.
20
20
  // Exposed as a `mock(...)` so individual tests can assert publish calls.
21
21
  const publishMock = mock(async () => {});
22
+ const broadcastMessageMock = mock(() => {});
22
23
  mock.module("../runtime/assistant-event-hub.js", () => ({
23
24
  assistantEventHub: { publish: publishMock },
25
+ broadcastMessage: broadcastMessageMock,
24
26
  }));
25
27
 
26
28
  // Stub buildAssistantEvent to be an identity pass-through for the event object
@@ -51,7 +53,10 @@ mock.module("../config/loader.js", () => ({
51
53
  // Real DB — same pattern as conversation-crud-inference-profile.test.ts
52
54
  // ---------------------------------------------------------------------------
53
55
 
54
- import { createConversation, getConversation } from "../memory/conversation-crud.js";
56
+ import {
57
+ createConversation,
58
+ getConversation,
59
+ } from "../memory/conversation-crud.js";
55
60
  import { getDb } from "../memory/db-connection.js";
56
61
  import { initializeDb } from "../memory/db-init.js";
57
62
 
@@ -83,6 +88,7 @@ describe("setInferenceProfileSession", () => {
83
88
  mockProfiles = { balanced: {}, "cost-optimized": {} };
84
89
  mockMaxTtl = undefined; // reset to default 43200
85
90
  publishMock.mockClear();
91
+ broadcastMessageMock.mockClear();
86
92
  });
87
93
 
88
94
  test("open with ttlSeconds=600 — returns UUID sessionId and expiresAt ≈ now + 600_000", async () => {
@@ -139,7 +145,9 @@ describe("setInferenceProfileSession", () => {
139
145
  expect(result.ttlSeconds).toBe(43200);
140
146
  expect(result.expiresAt).not.toBeNull();
141
147
  expect(result.expiresAt!).toBeGreaterThanOrEqual(before + 43200 * 1000);
142
- expect(result.expiresAt!).toBeLessThanOrEqual(Date.now() + 43200 * 1000 + 1000);
148
+ expect(result.expiresAt!).toBeLessThanOrEqual(
149
+ Date.now() + 43200 * 1000 + 1000,
150
+ );
143
151
  });
144
152
 
145
153
  test("open over active session — replaced carries prior session info", async () => {
@@ -208,6 +216,7 @@ describe("setInferenceProfileSession", () => {
208
216
  const updatedAtBefore = before?.updatedAt;
209
217
 
210
218
  publishMock.mockClear();
219
+ broadcastMessageMock.mockClear();
211
220
  const result = await setInferenceProfileSession({
212
221
  conversationId: conv.id,
213
222
  profile: null,
@@ -221,7 +230,7 @@ describe("setInferenceProfileSession", () => {
221
230
 
222
231
  // No event was published — this is the load-bearing assertion for the
223
232
  // idempotency guard (Codex P2 on PR #29913).
224
- expect(publishMock).not.toHaveBeenCalled();
233
+ expect(broadcastMessageMock).not.toHaveBeenCalled();
225
234
 
226
235
  // No DB write occurred — `updatedAt` is unchanged.
227
236
  const after = getConversation(conv.id);
@@ -240,6 +249,7 @@ describe("setInferenceProfileSession", () => {
240
249
  });
241
250
 
242
251
  publishMock.mockClear();
252
+ broadcastMessageMock.mockClear();
243
253
  const result = await setInferenceProfileSession({
244
254
  conversationId: conv.id,
245
255
  profile: null,
@@ -250,8 +260,8 @@ describe("setInferenceProfileSession", () => {
250
260
  // though the sticky profile was cleared.
251
261
  expect(result.replaced).toBeNull();
252
262
 
253
- // The clear DID happen — DB row reflects it and an event was published.
254
- expect(publishMock).toHaveBeenCalledTimes(1);
263
+ // The clear DID happen — DB row reflects it and legacy+sync events were published.
264
+ expect(broadcastMessageMock).toHaveBeenCalledTimes(2);
255
265
  const row = getConversation(conv.id);
256
266
  expect(row?.inferenceProfile).toBeNull();
257
267
  });
@@ -265,7 +275,9 @@ describe("setInferenceProfileSession", () => {
265
275
  profile: "nonexistent-profile",
266
276
  ttlSeconds: 300,
267
277
  }),
268
- ).rejects.toThrow('Profile "nonexistent-profile" is not defined in llm.profiles');
278
+ ).rejects.toThrow(
279
+ 'Profile "nonexistent-profile" is not defined in llm.profiles',
280
+ );
269
281
  });
270
282
 
271
283
  test("open with ttlSeconds=null — expiresAt=null, sessionId=null, profile kept", async () => {
@@ -15,6 +15,7 @@ mock.module("../util/logger.js", () => ({
15
15
 
16
16
  mock.module("../runtime/assistant-event-hub.js", () => ({
17
17
  assistantEventHub: { publish: async () => {} },
18
+ broadcastMessage: () => {},
18
19
  }));
19
20
 
20
21
  mock.module("../runtime/assistant-event.js", () => ({
@@ -43,9 +44,15 @@ import { ROUTES } from "../runtime/routes/inference-profile-session-routes.js";
43
44
 
44
45
  initializeDb();
45
46
 
46
- const openRoute = ROUTES.find((r) => r.operationId === "inference_profile_open")!;
47
- const closeRoute = ROUTES.find((r) => r.operationId === "inference_profile_close")!;
48
- const listRoute = ROUTES.find((r) => r.operationId === "inference_profile_list")!;
47
+ const openRoute = ROUTES.find(
48
+ (r) => r.operationId === "inference_profile_open",
49
+ )!;
50
+ const closeRoute = ROUTES.find(
51
+ (r) => r.operationId === "inference_profile_close",
52
+ )!;
53
+ const listRoute = ROUTES.find(
54
+ (r) => r.operationId === "inference_profile_list",
55
+ )!;
49
56
 
50
57
  function clearTables(): void {
51
58
  const db = getDb();
@@ -77,7 +84,9 @@ describe("inference_profile_open IPC op", () => {
77
84
  replaced: null,
78
85
  });
79
86
  expect((result as { sessionId: string }).sessionId).not.toBeNull();
80
- expect((result as { expiresAt: number }).expiresAt).toBeGreaterThan(Date.now());
87
+ expect((result as { expiresAt: number }).expiresAt).toBeGreaterThan(
88
+ Date.now(),
89
+ );
81
90
  });
82
91
 
83
92
  test("opens a sticky session (no ttlSeconds) — sessionId=null, expiresAt=null", async () => {
@@ -145,7 +154,10 @@ describe("inference_profile_close IPC op", () => {
145
154
  const result = (await closeRoute.handler({
146
155
  body: { conversationId: conv.id },
147
156
  headers: {},
148
- })) as { noop: boolean; closed: { profile: string; sessionId: string } | null };
157
+ })) as {
158
+ noop: boolean;
159
+ closed: { profile: string; sessionId: string } | null;
160
+ };
149
161
 
150
162
  expect(result.noop).toBe(false);
151
163
  expect(result.closed).not.toBeNull();
@@ -42,12 +42,10 @@ mock.module("../config/loader.js", () => ({
42
42
  },
43
43
  }));
44
44
 
45
- const { applyRuntimeInjections, composeInjectorChain } = await import(
46
- "../daemon/conversation-runtime-assembly.js"
47
- );
48
- const { DEFAULT_INJECTOR_ORDER, defaultInjectorsPlugin } = await import(
49
- "../plugins/defaults/injectors.js"
50
- );
45
+ const { applyRuntimeInjections, composeInjectorChain } =
46
+ await import("../daemon/conversation-runtime-assembly.js");
47
+ const { DEFAULT_INJECTOR_ORDER, defaultInjectorsPlugin } =
48
+ await import("../plugins/defaults/injectors.js");
51
49
  import {
52
50
  getInjectors,
53
51
  registerPlugin,
@@ -80,7 +78,6 @@ function wrapInPlugin(name: string, injectors: Injector[]): Plugin {
80
78
  manifest: {
81
79
  name,
82
80
  version: "0.0.1",
83
- requires: { pluginRuntime: "v1" },
84
81
  },
85
82
  injectors,
86
83
  };
@@ -91,7 +88,7 @@ describe("injector chain", () => {
91
88
  resetPluginRegistryForTests();
92
89
  });
93
90
 
94
- test("defaultInjectorsPlugin registers the ten defaults in the documented order", () => {
91
+ test("defaultInjectorsPlugin registers the defaults in the documented order", () => {
95
92
  registerPlugin(defaultInjectorsPlugin);
96
93
 
97
94
  const names = getInjectors().map((i) => i.name);
@@ -103,6 +100,7 @@ describe("injector chain", () => {
103
100
  "pkb-reminder",
104
101
  "memory-v2-static",
105
102
  "now-md",
103
+ "active-documents",
106
104
  "subagent-status",
107
105
  "slack-messages",
108
106
  "thread-focus",
@@ -128,6 +126,9 @@ describe("injector chain", () => {
128
126
  DEFAULT_INJECTOR_ORDER.memoryV2Static,
129
127
  );
130
128
  expect(byName.get("now-md")).toBe(DEFAULT_INJECTOR_ORDER.nowMd);
129
+ expect(byName.get("active-documents")).toBe(
130
+ DEFAULT_INJECTOR_ORDER.activeDocuments,
131
+ );
131
132
  expect(byName.get("subagent-status")).toBe(
132
133
  DEFAULT_INJECTOR_ORDER.subagentStatus,
133
134
  );
@@ -159,6 +160,7 @@ describe("injector chain", () => {
159
160
  "pkb-reminder", // 35
160
161
  "memory-v2-static", // 38
161
162
  "now-md", // 40
163
+ "active-documents", // 45
162
164
  "subagent-status", // 50
163
165
  "slack-messages", // 60
164
166
  "thread-focus", // 70
@@ -1,3 +1,5 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
4
 
3
5
  // ---------------------------------------------------------------------------
@@ -16,10 +18,13 @@ const mockCatalogSkills = mock(
16
18
  const mockClawhubInstall = mock(
17
19
  async (
18
20
  _slug: string,
19
- _opts?: { version?: string },
20
- ): Promise<{ success: boolean; error?: string; skillName?: string }> => ({
21
- success: true,
22
- }),
21
+ _opts?: { version?: string; projectRoot?: string },
22
+ ): Promise<{
23
+ success: boolean;
24
+ error?: string;
25
+ skillId?: string;
26
+ skillDir?: string;
27
+ }> => ({ success: true }),
23
28
  );
24
29
  const mockInstallExternalSkill = mock(
25
30
  async (
@@ -38,13 +43,27 @@ const mockEnsureSkillEntry = mock(
38
43
  enabled: false,
39
44
  }),
40
45
  );
46
+ const TEST_SKILLS_DIR = "/tmp/test-skills";
47
+ const installedSkillIds = new Set<string>();
48
+ const stagedSkillDirs = new Set<string>();
49
+ let stagingCounter = 0;
41
50
 
42
51
  // ---------------------------------------------------------------------------
43
52
  // Mock modules — before importing module under test
44
53
  // ---------------------------------------------------------------------------
45
54
 
46
55
  mock.module("../config/skills.js", () => ({
47
- loadSkillCatalog: mockCatalogSkills,
56
+ loadSkillCatalog: () => [
57
+ ...mockCatalogSkills(),
58
+ ...[...installedSkillIds].map((id) => ({
59
+ id,
60
+ displayName: id,
61
+ description: `Installed ${id}`,
62
+ source: "managed",
63
+ directoryPath: join(TEST_SKILLS_DIR, id),
64
+ skillFilePath: join(TEST_SKILLS_DIR, id, "SKILL.md"),
65
+ })),
66
+ ],
48
67
  }));
49
68
 
50
69
  mock.module("../skills/clawhub.js", () => ({
@@ -96,15 +115,30 @@ mock.module("../providers/provider-send-message.js", () => ({
96
115
  getConfiguredProvider: async () => null,
97
116
  userMessage: () => ({}),
98
117
  }));
99
- mock.module("../runtime/routes/workspace-utils.js", () => ({
100
- isTextMimeType: () => true,
101
- }));
102
118
  mock.module("../skills/catalog-cache.js", () => ({
103
119
  getCatalog: mockGetCatalog,
104
120
  }));
105
121
  mock.module("../skills/catalog-install.js", () => ({
122
+ commitStagedSkillInstall: (skillId: string, stagedDir: string) => {
123
+ if (!stagedSkillDirs.has(stagedDir)) {
124
+ throw new Error(
125
+ `Installed skill "${skillId}" is missing SKILL.md at the skill root`,
126
+ );
127
+ }
128
+ stagedSkillDirs.delete(stagedDir);
129
+ installedSkillIds.add(skillId);
130
+ },
131
+ createSkillInstallStagingDir: () => {
132
+ const stagingDir = join(
133
+ TEST_SKILLS_DIR,
134
+ ".install-staging",
135
+ `project-${stagingCounter++}`,
136
+ );
137
+ mkdirSync(join(stagingDir, "skills"), { recursive: true });
138
+ return stagingDir;
139
+ },
140
+ installSkillDependenciesIfPresent: () => {},
106
141
  installSkillLocally: mockInstallSkillLocally,
107
- upsertSkillsIndex: () => {},
108
142
  }));
109
143
  mock.module("../skills/catalog-search.js", () => ({
110
144
  filterByQuery: () => [],
@@ -112,7 +146,6 @@ mock.module("../skills/catalog-search.js", () => ({
112
146
  mock.module("../skills/managed-store.js", () => ({
113
147
  createManagedSkill: () => ({ created: true }),
114
148
  deleteManagedSkill: () => ({ deleted: true }),
115
- removeSkillsIndexEntry: () => {},
116
149
  validateManagedSkillId: () => null,
117
150
  }));
118
151
  mock.module("../memory/graph/capability-seed.js", () => ({
@@ -121,7 +154,7 @@ mock.module("../memory/graph/capability-seed.js", () => ({
121
154
  seedUninstalledCatalogSkillMemories: async () => {},
122
155
  }));
123
156
  mock.module("../util/platform.js", () => ({
124
- getWorkspaceSkillsDir: () => "/tmp/test-skills",
157
+ getWorkspaceSkillsDir: () => TEST_SKILLS_DIR,
125
158
  }));
126
159
  mock.module("../daemon/handlers/shared.js", () => ({
127
160
  CONFIG_RELOAD_DEBOUNCE_MS: 100,
@@ -141,13 +174,17 @@ import { installSkill } from "../daemon/handlers/skills.js";
141
174
  // Helpers
142
175
  // ---------------------------------------------------------------------------
143
176
 
144
-
145
177
  // ---------------------------------------------------------------------------
146
178
  // Tests
147
179
  // ---------------------------------------------------------------------------
148
180
 
149
181
  describe("installSkill routing", () => {
150
182
  beforeEach(() => {
183
+ rmSync(TEST_SKILLS_DIR, { recursive: true, force: true });
184
+ mkdirSync(TEST_SKILLS_DIR, { recursive: true });
185
+ installedSkillIds.clear();
186
+ stagedSkillDirs.clear();
187
+ stagingCounter = 0;
151
188
  mockCatalogSkills.mockReset();
152
189
  mockClawhubInstall.mockReset();
153
190
  mockInstallExternalSkill.mockReset();
@@ -158,7 +195,12 @@ describe("installSkill routing", () => {
158
195
 
159
196
  // Defaults
160
197
  mockCatalogSkills.mockReturnValue([]);
161
- mockClawhubInstall.mockResolvedValue({ success: true });
198
+ mockClawhubInstall.mockImplementation(async (slug, opts) => {
199
+ const skillId = slug.includes("/") ? slug.split("/").pop()! : slug;
200
+ const projectRoot = opts?.projectRoot ?? TEST_SKILLS_DIR;
201
+ const skillDir = stageClawhubSkill(projectRoot, skillId);
202
+ return { success: true, skillId, skillDir };
203
+ });
162
204
  mockInstallExternalSkill.mockResolvedValue(undefined);
163
205
  mockGetCatalog.mockResolvedValue([]);
164
206
  mockInstallSkillLocally.mockResolvedValue(undefined);
@@ -166,13 +208,38 @@ describe("installSkill routing", () => {
166
208
  mockEnsureSkillEntry.mockReturnValue({ enabled: false });
167
209
  });
168
210
 
169
- test("install with origin: 'skillssh' and multi-segment slug routes to installExternalSkill", async () => {
170
- const result = await installSkill(
171
- {
172
- slug: "vercel-labs/agent-skills/react-best-practices",
173
- origin: "skillssh",
174
- },
211
+ function writeInstalledSkill(skillId: string): void {
212
+ const skillDir = join(TEST_SKILLS_DIR, skillId);
213
+ mkdirSync(skillDir, { recursive: true });
214
+ writeFileSync(
215
+ join(skillDir, "SKILL.md"),
216
+ `---
217
+ name: "${skillId}"
218
+ description: "Installed test skill."
219
+ ---
220
+
221
+ Body.
222
+ `,
223
+ "utf-8",
175
224
  );
225
+ installedSkillIds.add(skillId);
226
+ }
227
+
228
+ function stageClawhubSkill(projectRoot: string, skillId: string): string {
229
+ const skillDir = join(projectRoot, "skills", skillId);
230
+ stagedSkillDirs.add(skillDir);
231
+ return skillDir;
232
+ }
233
+
234
+ test("install with origin: 'skillssh' and multi-segment slug routes to installExternalSkill", async () => {
235
+ mockInstallExternalSkill.mockImplementation(async () => {
236
+ writeInstalledSkill("react-best-practices");
237
+ });
238
+
239
+ const result = await installSkill({
240
+ slug: "vercel-labs/agent-skills/react-best-practices",
241
+ origin: "skillssh",
242
+ });
176
243
 
177
244
  expect(result.success).toBe(true);
178
245
  expect(mockInstallExternalSkill).toHaveBeenCalledTimes(1);
@@ -189,11 +256,21 @@ describe("installSkill routing", () => {
189
256
  });
190
257
 
191
258
  test("install without origin falls through to clawhub for simple slugs", async () => {
259
+ mockClawhubInstall.mockImplementation(async (slug: string) => {
260
+ const skillDir = stageClawhubSkill(
261
+ join(TEST_SKILLS_DIR, ".install-staging", "project-0"),
262
+ slug,
263
+ );
264
+ return { success: true, skillId: slug, skillDir };
265
+ });
266
+
192
267
  const result = await installSkill({ slug: "some-clawhub-skill" });
193
268
 
194
269
  expect(result.success).toBe(true);
195
270
  expect(mockClawhubInstall).toHaveBeenCalledTimes(1);
196
271
  expect(mockClawhubInstall).toHaveBeenCalledWith("some-clawhub-skill", {
272
+ contactId: undefined,
273
+ projectRoot: join(TEST_SKILLS_DIR, ".install-staging", "project-0"),
197
274
  version: undefined,
198
275
  });
199
276
  // Should not have called installExternalSkill
@@ -201,19 +278,51 @@ describe("installSkill routing", () => {
201
278
  });
202
279
 
203
280
  test("install with origin: 'clawhub' routes directly to clawhub without trying skills.sh", async () => {
204
- const result = await installSkill(
205
- { slug: "my-skill", origin: "clawhub" },
206
- );
281
+ mockClawhubInstall.mockImplementation(async (slug: string) => {
282
+ const skillDir = stageClawhubSkill(
283
+ join(TEST_SKILLS_DIR, ".install-staging", "project-0"),
284
+ slug,
285
+ );
286
+ return { success: true, skillId: slug, skillDir };
287
+ });
288
+
289
+ const result = await installSkill({ slug: "my-skill", origin: "clawhub" });
207
290
 
208
291
  expect(result.success).toBe(true);
209
292
  expect(mockClawhubInstall).toHaveBeenCalledTimes(1);
210
293
  expect(mockInstallExternalSkill).not.toHaveBeenCalled();
211
294
  });
212
295
 
296
+ test("clawhub install fails when the installed skill root is not discoverable", async () => {
297
+ mockClawhubInstall.mockResolvedValue({
298
+ success: true,
299
+ skillId: "missing-root-skill",
300
+ skillDir: join(
301
+ TEST_SKILLS_DIR,
302
+ ".install-staging",
303
+ "project-0",
304
+ "skills",
305
+ "missing-root-skill",
306
+ ),
307
+ });
308
+
309
+ const result = await installSkill({
310
+ slug: "missing-root-skill",
311
+ origin: "clawhub",
312
+ });
313
+
314
+ expect(result.success).toBe(false);
315
+ if (!result.success) {
316
+ expect(result.error).toContain("missing SKILL.md");
317
+ }
318
+ });
319
+
213
320
  test("multi-segment slug without explicit origin auto-routes to skills.sh", async () => {
214
- const result = await installSkill(
215
- { slug: "owner/repo/my-skill" },
216
- );
321
+ mockInstallExternalSkill.mockImplementation(async () => {
322
+ writeInstalledSkill("my-skill");
323
+ });
324
+
325
+ const result = await installSkill({ slug: "owner/repo/my-skill" });
217
326
 
218
327
  expect(result.success).toBe(true);
219
328
  expect(mockInstallExternalSkill).toHaveBeenCalledTimes(1);
@@ -229,11 +338,21 @@ describe("installSkill routing", () => {
229
338
  });
230
339
 
231
340
  test("multi-segment slug with origin: 'clawhub' skips skills.sh and routes to clawhub", async () => {
341
+ mockClawhubInstall.mockImplementation(async (slug: string) => {
342
+ const skillId = slug.split("/").pop()!;
343
+ const skillDir = stageClawhubSkill(
344
+ join(TEST_SKILLS_DIR, ".install-staging", "project-0"),
345
+ skillId,
346
+ );
347
+ return { success: true, skillId, skillDir };
348
+ });
349
+
232
350
  // Even though the slug looks like skills.sh format, explicit origin: "clawhub"
233
351
  // should override the auto-detection and go to clawhub
234
- const result = await installSkill(
235
- { slug: "owner/repo/my-skill", origin: "clawhub" },
236
- );
352
+ const result = await installSkill({
353
+ slug: "owner/repo/my-skill",
354
+ origin: "clawhub",
355
+ });
237
356
 
238
357
  expect(result.success).toBe(true);
239
358
  expect(mockClawhubInstall).toHaveBeenCalledTimes(1);
@@ -251,9 +370,10 @@ describe("installSkill routing", () => {
251
370
  },
252
371
  ]);
253
372
 
254
- const result = await installSkill(
255
- { slug: "bundled-skill", origin: "skillssh" },
256
- );
373
+ const result = await installSkill({
374
+ slug: "bundled-skill",
375
+ origin: "skillssh",
376
+ });
257
377
 
258
378
  expect(result.success).toBe(true);
259
379
  // Should have auto-enabled via ensureSkillEntry, not called external install
@@ -266,12 +386,10 @@ describe("installSkill routing", () => {
266
386
  new Error("Skill not found in repo"),
267
387
  );
268
388
 
269
- const result = await installSkill(
270
- {
271
- slug: "owner/repo/nonexistent-skill",
272
- origin: "skillssh",
273
- },
274
- );
389
+ const result = await installSkill({
390
+ slug: "owner/repo/nonexistent-skill",
391
+ origin: "skillssh",
392
+ });
275
393
 
276
394
  expect(result.success).toBe(false);
277
395
  if (!result.success) {