@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
@@ -1,19 +1,28 @@
1
1
  /**
2
2
  * Tests for the `assistant changelog` CLI command.
3
3
  *
4
+ * Every test exercises the public CLI surface via `runCli`. Internal helpers
5
+ * (cache plumbing, version compare, rendering) are validated through the
6
+ * commands that exercise them, not via module-level test exports.
7
+ *
4
8
  * Coverage:
5
- * - Pure helpers (compareTags, normalizeTag, stableReleases, parseLimit,
6
- * renderRelease, renderList).
7
- * - Cache plumbing (read returns null when missing/corrupt; write+read
8
- * roundtrip; stale detection).
9
- * - Cache-aware loaders (cache hit short-circuits fetch; --no-cache skips
10
- * read but still writes; stale cache refetches).
11
- * - GitHub error mapping (403/429 → rate-limit message; 404 null tag).
12
- * - End-to-end command actions (default = latest, --since, show, list,
13
- * --json, missing tag, empty release list).
9
+ * - Default action (latest stable, --since, --json).
10
+ * - `show <version>` (renders, --json, 404, persists to cache).
11
+ * - `list` (rows, --json, --limit/--no-cache propagation).
12
+ * - Cache behavior (fresh cache short-circuits fetch; --no-cache bypass;
13
+ * stale TTL refetch; cache capped at CACHE_STABLE_LIMIT stable
14
+ * releases; pagination buffer absorbs drafts/prereleases).
15
+ * - Error mapping (rate-limit friendly stderr; non-zero exit).
14
16
  */
15
17
 
16
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
18
+ import {
19
+ existsSync,
20
+ mkdirSync,
21
+ mkdtempSync,
22
+ readFileSync,
23
+ rmSync,
24
+ writeFileSync,
25
+ } from "node:fs";
17
26
  import { tmpdir } from "node:os";
18
27
  import { join } from "node:path";
19
28
  import {
@@ -98,6 +107,18 @@ const REL_DRAFT: FakeRelease = {
98
107
  prerelease: false,
99
108
  };
100
109
 
110
+ function stableRelease(tag: string, body = `release ${tag}`): FakeRelease {
111
+ return {
112
+ tag_name: tag,
113
+ name: tag,
114
+ body,
115
+ published_at: "2026-04-01T12:00:00Z",
116
+ html_url: `https://github.com/vellum-ai/vellum-assistant/releases/tag/${tag}`,
117
+ draft: false,
118
+ prerelease: false,
119
+ };
120
+ }
121
+
101
122
  // ── fetch mock harness ───────────────────────────────────────────────
102
123
 
103
124
  type FetchHandler = (url: string, init?: RequestInit) => Promise<Response>;
@@ -143,25 +164,29 @@ function jsonResponse(payload: unknown, status = 200): Response {
143
164
  });
144
165
  }
145
166
 
167
+ const cachePath = join(TMP_ROOT, "data", "changelog-cache.json");
168
+
169
+ interface CacheShape {
170
+ fetchedAt: string;
171
+ recent: FakeRelease[];
172
+ byTag: Record<string, FakeRelease>;
173
+ }
174
+
175
+ function readCacheFile(): CacheShape | null {
176
+ if (!existsSync(cachePath)) return null;
177
+ return JSON.parse(readFileSync(cachePath, "utf-8")) as CacheShape;
178
+ }
179
+
180
+ function writeCacheFile(cache: CacheShape): void {
181
+ mkdirSync(join(TMP_ROOT, "data"), { recursive: true });
182
+ writeFileSync(cachePath, JSON.stringify(cache, null, 2));
183
+ }
184
+
146
185
  // ── Import module under test (after mocks) ───────────────────────────
147
186
 
148
- const { registerChangelogCommand, __testing } = await import("../changelog.js");
149
- const {
150
- compareTags,
151
- normalizeTag,
152
- stableReleases,
153
- parseLimit,
154
- renderRelease,
155
- renderList,
156
- readCache,
157
- writeCache,
158
- isStale,
159
- loadReleases,
160
- loadReleaseByTag,
161
- getCachePath,
162
- } = __testing;
163
-
164
- // ── stdout / exit capture ────────────────────────────────────────────
187
+ const { registerChangelogCommand } = await import("../changelog.js");
188
+
189
+ // ── CLI driver ───────────────────────────────────────────────────────
165
190
 
166
191
  interface CapturedRun {
167
192
  stdout: string;
@@ -216,287 +241,32 @@ async function runCli(argv: string[]): Promise<CapturedRun> {
216
241
  };
217
242
  }
218
243
 
219
- // ── Pure helpers ─────────────────────────────────────────────────────
220
-
221
- describe("changelog helpers", () => {
222
- describe("compareTags", () => {
223
- test("equal tags compare as 0", () => {
224
- expect(compareTags("v0.8.0", "v0.8.0")).toBe(0);
225
- });
226
- test("higher major beats lower", () => {
227
- expect(compareTags("v1.0.0", "v0.99.99")).toBeGreaterThan(0);
228
- });
229
- test("higher patch beats lower", () => {
230
- expect(compareTags("v0.8.1", "v0.8.0")).toBeGreaterThan(0);
231
- });
232
- test("accepts inputs without the v prefix", () => {
233
- expect(compareTags("0.8.1", "v0.8.0")).toBeGreaterThan(0);
234
- expect(compareTags("0.7.0", "0.8.0")).toBeLessThan(0);
235
- });
236
- test("missing patch parts default to 0", () => {
237
- expect(compareTags("v0.8", "v0.8.0")).toBe(0);
238
- expect(compareTags("v0.8.1", "v0.8")).toBeGreaterThan(0);
239
- });
240
- });
241
-
242
- describe("normalizeTag", () => {
243
- test("adds v prefix when missing", () => {
244
- expect(normalizeTag("0.8.0")).toBe("v0.8.0");
245
- });
246
- test("leaves v prefix untouched", () => {
247
- expect(normalizeTag("v0.8.0")).toBe("v0.8.0");
248
- });
249
- });
250
-
251
- describe("stableReleases", () => {
252
- test("drops drafts and prereleases", () => {
253
- const filtered = stableReleases([
254
- REL_080,
255
- REL_080_RC,
256
- REL_DRAFT,
257
- REL_079,
258
- ]);
259
- expect(filtered.map((r) => r.tag_name)).toEqual(["v0.8.0", "v0.7.9"]);
260
- });
261
- test("preserves order of the stable subset", () => {
262
- const filtered = stableReleases([REL_079, REL_080]);
263
- expect(filtered.map((r) => r.tag_name)).toEqual(["v0.7.9", "v0.8.0"]);
264
- });
265
- });
266
-
267
- describe("parseLimit", () => {
268
- test("falls back when input is missing", () => {
269
- expect(parseLimit(undefined, 30)).toBe(30);
270
- });
271
- test("falls back on garbage input", () => {
272
- expect(parseLimit("not-a-number", 30)).toBe(30);
273
- });
274
- test("falls back when input is zero or negative", () => {
275
- expect(parseLimit("0", 30)).toBe(30);
276
- expect(parseLimit("-5", 30)).toBe(30);
277
- });
278
- test("clamps to the upper bound of 100", () => {
279
- expect(parseLimit("999", 30)).toBe(100);
280
- });
281
- test("accepts valid values", () => {
282
- expect(parseLimit("42", 30)).toBe(42);
283
- });
284
- });
285
-
286
- describe("renderRelease", () => {
287
- test("includes heading, date, html url, and body", () => {
288
- const rendered = renderRelease(REL_080);
289
- expect(rendered).toContain("# v0.8.0 — Tavily");
290
- expect(rendered).toContain("Published: 2026-05-10");
291
- expect(rendered).toContain(REL_080.html_url);
292
- expect(rendered).toContain("Tavily web search");
293
- });
294
- test("falls back to tag when name is empty", () => {
295
- const rendered = renderRelease({ ...REL_080, name: "" });
296
- expect(rendered).toContain("# v0.8.0");
297
- });
298
- test("placeholder when body is empty", () => {
299
- const rendered = renderRelease({ ...REL_080, body: " " });
300
- expect(rendered).toContain("(no release body)");
301
- });
302
- });
303
-
304
- describe("renderList", () => {
305
- test("formats one row per release", () => {
306
- const rendered = renderList([REL_080, REL_079]);
307
- const lines = rendered.split("\n");
308
- expect(lines).toHaveLength(2);
309
- expect(lines[0]).toContain("v0.8.0");
310
- expect(lines[0]).toContain("2026-05-10");
311
- expect(lines[1]).toContain("v0.7.9");
312
- });
313
- test("handles empty input", () => {
314
- expect(renderList([])).toBe("No releases found.");
315
- });
316
- });
317
- });
318
-
319
- // ── Cache plumbing ───────────────────────────────────────────────────
320
-
321
- describe("changelog cache", () => {
322
- test("cache path lives under the workspace data dir", () => {
323
- expect(getCachePath()).toBe(join(TMP_ROOT, "data", "changelog-cache.json"));
324
- });
325
-
326
- test("readCache returns null when the file is missing", () => {
327
- expect(readCache()).toBeNull();
328
- });
329
-
330
- test("write + read roundtrip", () => {
331
- const store = {
332
- fetchedAt: new Date().toISOString(),
333
- releases: [REL_080, REL_079],
334
- };
335
- writeCache(store);
336
- const loaded = readCache();
337
- expect(loaded).not.toBeNull();
338
- expect(loaded?.releases.map((r) => r.tag_name)).toEqual([
339
- "v0.8.0",
340
- "v0.7.9",
341
- ]);
342
- });
343
-
344
- test("readCache returns null on corrupt JSON", () => {
345
- writeCache({ fetchedAt: new Date().toISOString(), releases: [REL_080] });
346
- // Re-write garbage on top of the cache file.
347
- writeFileSync(getCachePath(), "{not valid json");
348
- expect(readCache()).toBeNull();
349
- });
350
-
351
- test("readCache returns null when shape is wrong", () => {
352
- mkdirSync(join(TMP_ROOT, "data"), { recursive: true });
353
- writeFileSync(getCachePath(), JSON.stringify({ foo: "bar" }));
354
- expect(readCache()).toBeNull();
355
- });
356
-
357
- test("isStale flags caches older than the TTL", () => {
358
- const fresh = { fetchedAt: new Date().toISOString(), releases: [] };
359
- expect(isStale(fresh)).toBe(false);
360
- const old = {
361
- fetchedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
362
- releases: [],
363
- };
364
- expect(isStale(old)).toBe(true);
365
- });
366
-
367
- test("isStale treats unparseable timestamps as stale", () => {
368
- expect(isStale({ fetchedAt: "not-a-date", releases: [] })).toBe(true);
369
- });
370
- });
371
-
372
- // ── Cache-aware loaders ──────────────────────────────────────────────
373
-
374
- describe("loadReleases", () => {
375
- test("returns cached releases when fresh and big enough", async () => {
376
- writeCache({
377
- fetchedAt: new Date().toISOString(),
378
- releases: [REL_080, REL_079],
379
- });
380
- fetchHandler = async () => jsonResponse([]);
381
- const result = await loadReleases({ noCache: false, limit: 2 });
382
- expect(result.map((r) => r.tag_name)).toEqual(["v0.8.0", "v0.7.9"]);
383
- expect(fetchCalls).toHaveLength(0);
384
- });
385
-
386
- test("fetches from GitHub when cache is missing", async () => {
387
- fetchHandler = async () => jsonResponse([REL_080, REL_079]);
388
- const result = await loadReleases({ noCache: false, limit: 30 });
389
- expect(result.map((r) => r.tag_name)).toEqual(["v0.8.0", "v0.7.9"]);
390
- expect(fetchCalls).toHaveLength(1);
391
- expect(fetchCalls[0]).toContain(
392
- "https://api.github.com/repos/vellum-ai/vellum-assistant/releases",
393
- );
394
- // Cache should now be populated.
395
- expect(readCache()?.releases).toHaveLength(2);
396
- });
397
-
398
- test("--no-cache forces a refetch", async () => {
399
- writeCache({
400
- fetchedAt: new Date().toISOString(),
401
- releases: [REL_080, REL_079],
402
- });
403
- fetchHandler = async () => jsonResponse([REL_080]);
404
- const result = await loadReleases({ noCache: true, limit: 30 });
405
- expect(result.map((r) => r.tag_name)).toEqual(["v0.8.0"]);
406
- expect(fetchCalls).toHaveLength(1);
407
- });
408
-
409
- test("stale cache triggers a refetch", async () => {
410
- writeCache({
411
- fetchedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
412
- releases: [REL_079],
413
- });
414
- fetchHandler = async () => jsonResponse([REL_080, REL_079]);
415
- const result = await loadReleases({ noCache: false, limit: 30 });
416
- expect(result.map((r) => r.tag_name)).toEqual(["v0.8.0", "v0.7.9"]);
417
- expect(fetchCalls).toHaveLength(1);
418
- });
419
-
420
- test("cache with fewer entries than the requested limit triggers a refetch", async () => {
421
- writeCache({
422
- fetchedAt: new Date().toISOString(),
423
- releases: [REL_080],
424
- });
425
- fetchHandler = async () => jsonResponse([REL_080, REL_079]);
426
- const result = await loadReleases({ noCache: false, limit: 2 });
427
- expect(result).toHaveLength(2);
428
- expect(fetchCalls).toHaveLength(1);
429
- });
430
-
431
- test("maps 403 to a rate-limit message", async () => {
432
- fetchHandler = async () => new Response("rate limit", { status: 403 });
433
- await expect(loadReleases({ noCache: true, limit: 30 })).rejects.toThrow(
434
- /rate limit/i,
435
- );
436
- });
437
- });
438
-
439
- describe("loadReleaseByTag", () => {
440
- test("returns from cache when the tag is present", async () => {
441
- writeCache({
442
- fetchedAt: new Date().toISOString(),
443
- releases: [REL_080, REL_079],
444
- });
445
- fetchHandler = async () => jsonResponse({}, 500);
446
- const found = await loadReleaseByTag("v0.8.0", { noCache: false });
447
- expect(found?.tag_name).toBe("v0.8.0");
448
- expect(fetchCalls).toHaveLength(0);
449
- });
450
-
451
- test("falls through to fetch when the tag is missing from cache", async () => {
452
- writeCache({
453
- fetchedAt: new Date().toISOString(),
454
- releases: [REL_080],
455
- });
456
- fetchHandler = async () => jsonResponse(REL_079);
457
- const found = await loadReleaseByTag("v0.7.9", { noCache: false });
458
- expect(found?.tag_name).toBe("v0.7.9");
459
- expect(fetchCalls).toHaveLength(1);
460
- expect(fetchCalls[0]).toContain("/tags/v0.7.9");
461
- });
462
-
463
- test("--no-cache always fetches", async () => {
464
- writeCache({
465
- fetchedAt: new Date().toISOString(),
466
- releases: [REL_080],
467
- });
468
- fetchHandler = async () => jsonResponse(REL_080);
469
- await loadReleaseByTag("v0.8.0", { noCache: true });
470
- expect(fetchCalls).toHaveLength(1);
471
- });
472
-
473
- test("returns null on 404", async () => {
474
- fetchHandler = async () => new Response("not found", { status: 404 });
475
- const found = await loadReleaseByTag("v99.99.99", { noCache: true });
476
- expect(found).toBeNull();
477
- });
478
- });
479
-
480
- // ── End-to-end command surface ───────────────────────────────────────
244
+ // ── Default action ───────────────────────────────────────────────────
481
245
 
482
- describe("changelog command", () => {
483
- test("default action shows the latest stable release", async () => {
246
+ describe("assistant changelog (default action)", () => {
247
+ test("shows the latest stable release, filtering drafts and prereleases", async () => {
484
248
  fetchHandler = async () =>
485
249
  jsonResponse([REL_DRAFT, REL_080_RC, REL_080, REL_079]);
486
250
  const result = await runCli(["changelog"]);
487
251
  expect(result.exitCode).toBe(0);
488
252
  expect(result.stdout).toContain("# v0.8.0 — Tavily");
253
+ expect(result.stdout).toContain("Published: 2026-05-10");
254
+ expect(result.stdout).toContain(REL_080.html_url);
255
+ expect(result.stdout).toContain("Tavily web search");
489
256
  expect(result.stdout).not.toContain("draft");
490
257
  expect(result.stdout).not.toContain("rc.1");
491
258
  });
492
259
 
260
+ test("--json emits the latest release as a JSON object", async () => {
261
+ fetchHandler = async () => jsonResponse([REL_080, REL_079]);
262
+ const result = await runCli(["changelog", "--json"]);
263
+ expect(result.exitCode).toBe(0);
264
+ const parsed = JSON.parse(result.stdout) as { tag_name: string };
265
+ expect(parsed.tag_name).toBe("v0.8.0");
266
+ });
267
+
493
268
  test("--since concatenates every newer stable release, newest first", async () => {
494
- const REL_081 = {
495
- ...REL_080,
496
- tag_name: "v0.8.1",
497
- name: "v0.8.1",
498
- body: "patch notes",
499
- };
269
+ const REL_081 = stableRelease("v0.8.1", "patch notes");
500
270
  fetchHandler = async () => jsonResponse([REL_081, REL_080, REL_079]);
501
271
  const result = await runCli(["changelog", "--since", "0.7.9"]);
502
272
  expect(result.exitCode).toBe(0);
@@ -507,43 +277,117 @@ describe("changelog command", () => {
507
277
  expect(result.stdout).not.toContain("v0.7.9");
508
278
  });
509
279
 
510
- test("--since with no newer releases prints an empty-state line", async () => {
280
+ test("--since accepts a tag with or without a v prefix", async () => {
511
281
  fetchHandler = async () => jsonResponse([REL_080, REL_079]);
512
- const result = await runCli(["changelog", "--since", "1.0.0"]);
282
+ const result = await runCli(["changelog", "--since", "v0.7.9"]);
513
283
  expect(result.exitCode).toBe(0);
284
+ expect(result.stdout).toContain("v0.8.0");
514
285
  });
515
286
 
516
- test("--json with default action emits the latest release as JSON", async () => {
287
+ test("--since with no newer releases exits 0 and emits no release bodies", async () => {
517
288
  fetchHandler = async () => jsonResponse([REL_080, REL_079]);
518
- const result = await runCli(["changelog", "--json"]);
289
+ const result = await runCli(["changelog", "--since", "1.0.0"]);
519
290
  expect(result.exitCode).toBe(0);
520
- const parsed = JSON.parse(result.stdout) as { tag_name: string };
521
- expect(parsed.tag_name).toBe("v0.8.0");
291
+ expect(result.stdout).not.toContain("Published:");
292
+ });
293
+
294
+ test("--since --json emits an empty releases array when nothing matches", async () => {
295
+ fetchHandler = async () => jsonResponse([REL_080]);
296
+ const result = await runCli(["changelog", "--since", "1.0.0", "--json"]);
297
+ expect(result.exitCode).toBe(0);
298
+ const parsed = JSON.parse(result.stdout) as { releases: unknown[] };
299
+ expect(parsed.releases).toEqual([]);
522
300
  });
523
301
 
524
- test("show <version> prints the named release", async () => {
302
+ test("empty release list (after stable filter) exits non-zero", async () => {
303
+ fetchHandler = async () => jsonResponse([REL_080_RC, REL_DRAFT]);
304
+ const result = await runCli(["changelog"]);
305
+ expect(result.exitCode).toBe(1);
306
+ expect(result.stderr).toContain("No releases found");
307
+ });
308
+ });
309
+
310
+ // ── show <version> ───────────────────────────────────────────────────
311
+
312
+ describe("assistant changelog show <version>", () => {
313
+ test("prints the named release", async () => {
525
314
  fetchHandler = async () => jsonResponse(REL_080);
526
315
  const result = await runCli(["changelog", "show", "0.8.0"]);
527
316
  expect(result.exitCode).toBe(0);
528
317
  expect(result.stdout).toContain("# v0.8.0 — Tavily");
318
+ expect(fetchCalls[0]).toContain("/releases/tags/v0.8.0");
529
319
  });
530
320
 
531
- test("show <version> with no match exits non-zero", async () => {
321
+ test("accepts a v-prefixed input", async () => {
322
+ fetchHandler = async () => jsonResponse(REL_080);
323
+ const result = await runCli(["changelog", "show", "v0.8.0"]);
324
+ expect(result.exitCode).toBe(0);
325
+ expect(fetchCalls[0]).toContain("/releases/tags/v0.8.0");
326
+ });
327
+
328
+ test("--json forwards through the parent into the show subcommand", async () => {
329
+ fetchHandler = async () => jsonResponse(REL_080);
330
+ const result = await runCli(["changelog", "show", "0.8.0", "--json"]);
331
+ expect(result.exitCode).toBe(0);
332
+ const parsed = JSON.parse(result.stdout) as { tag_name: string };
333
+ expect(parsed.tag_name).toBe("v0.8.0");
334
+ });
335
+
336
+ test("404 surfaces a friendly stderr and exits non-zero", async () => {
532
337
  fetchHandler = async () => new Response("not found", { status: 404 });
533
338
  const result = await runCli(["changelog", "show", "99.99.99"]);
534
339
  expect(result.exitCode).toBe(1);
535
340
  expect(result.stderr).toContain("No release found");
536
341
  });
537
342
 
538
- test("list prints rows of tag/date/name", async () => {
539
- fetchHandler = async () => jsonResponse([REL_080, REL_079]);
343
+ test("persists a fetched tag into the cache so the next call short-circuits", async () => {
344
+ let callCount = 0;
345
+ fetchHandler = async () => {
346
+ callCount += 1;
347
+ return jsonResponse(REL_079);
348
+ };
349
+
350
+ const first = await runCli(["changelog", "show", "0.7.9"]);
351
+ expect(first.exitCode).toBe(0);
352
+ expect(callCount).toBe(1);
353
+
354
+ const cached = readCacheFile();
355
+ expect(cached?.byTag["v0.7.9"]?.tag_name).toBe("v0.7.9");
356
+
357
+ const second = await runCli(["changelog", "show", "0.7.9"]);
358
+ expect(second.exitCode).toBe(0);
359
+ expect(callCount).toBe(1); // no extra fetch
360
+ });
361
+
362
+ test("--no-cache skips the cached entry and refetches", async () => {
363
+ writeCacheFile({
364
+ fetchedAt: new Date().toISOString(),
365
+ recent: [],
366
+ byTag: { "v0.8.0": REL_080 },
367
+ });
368
+ fetchHandler = async () => jsonResponse(REL_080);
369
+ const result = await runCli(["changelog", "show", "0.8.0", "--no-cache"]);
370
+ expect(result.exitCode).toBe(0);
371
+ expect(fetchCalls).toHaveLength(1);
372
+ });
373
+ });
374
+
375
+ // ── list ──────────────────────────────────────────────────────────────
376
+
377
+ describe("assistant changelog list", () => {
378
+ test("prints one row per stable release", async () => {
379
+ fetchHandler = async () =>
380
+ jsonResponse([REL_080_RC, REL_080, REL_DRAFT, REL_079]);
540
381
  const result = await runCli(["changelog", "list"]);
541
382
  expect(result.exitCode).toBe(0);
542
- expect(result.stdout).toContain("v0.8.0");
543
- expect(result.stdout).toContain("v0.7.9");
383
+ const lines = result.stdout.trim().split("\n");
384
+ expect(lines).toHaveLength(2);
385
+ expect(lines[0]).toContain("v0.8.0");
386
+ expect(lines[0]).toContain("2026-05-10");
387
+ expect(lines[1]).toContain("v0.7.9");
544
388
  });
545
389
 
546
- test("list --json emits a releases array", async () => {
390
+ test("--json emits a releases array", async () => {
547
391
  fetchHandler = async () => jsonResponse([REL_080, REL_079]);
548
392
  const result = await runCli(["changelog", "list", "--json"]);
549
393
  expect(result.exitCode).toBe(0);
@@ -556,9 +400,9 @@ describe("changelog command", () => {
556
400
  ]);
557
401
  });
558
402
 
559
- test("list --no-cache --json --limit 5 forwards parent flags into the subcommand", async () => {
403
+ test("--no-cache --json --limit forwards parent flags into the subcommand", async () => {
560
404
  fetchHandler = async () =>
561
- jsonResponse([REL_080, REL_079, { ...REL_079, tag_name: "v0.7.8" }]);
405
+ jsonResponse([REL_080, REL_079, stableRelease("v0.7.8")]);
562
406
  const result = await runCli([
563
407
  "changelog",
564
408
  "list",
@@ -572,22 +416,163 @@ describe("changelog command", () => {
572
416
  releases: Array<{ tag_name: string }>;
573
417
  };
574
418
  expect(parsed.releases.length).toBeGreaterThan(0);
575
- expect(fetchCalls[0]).toContain("per_page=5");
419
+ // Even with --limit 5, the page-size buffer absorbs drafts/prereleases.
420
+ expect(fetchCalls[0]).toMatch(/per_page=\d+/);
576
421
  });
422
+ });
577
423
 
578
- test("show <version> --json forwards --json from parent into the show subcommand", async () => {
579
- fetchHandler = async () => jsonResponse(REL_080);
580
- const result = await runCli(["changelog", "show", "0.8.0", "--json"]);
424
+ // ── Cache behavior ───────────────────────────────────────────────────
425
+
426
+ describe("assistant changelog cache", () => {
427
+ test("second invocation reuses a fresh cache without re-fetching", async () => {
428
+ let callCount = 0;
429
+ fetchHandler = async () => {
430
+ callCount += 1;
431
+ return jsonResponse([REL_080, REL_079]);
432
+ };
433
+ await runCli(["changelog"]);
434
+ expect(callCount).toBe(1);
435
+
436
+ await runCli(["changelog"]);
437
+ // Second call should hit the rolling-recent cache: latest is still
438
+ // populated and not stale, and we only need 1 entry for the default
439
+ // action.
440
+ expect(callCount).toBe(1);
441
+ });
442
+
443
+ test("--no-cache forces a refetch even with a fresh cache", async () => {
444
+ writeCacheFile({
445
+ fetchedAt: new Date().toISOString(),
446
+ recent: [REL_080, REL_079],
447
+ byTag: { "v0.8.0": REL_080, "v0.7.9": REL_079 },
448
+ });
449
+ let callCount = 0;
450
+ fetchHandler = async () => {
451
+ callCount += 1;
452
+ return jsonResponse([REL_080, REL_079]);
453
+ };
454
+ const result = await runCli(["changelog", "--no-cache"]);
581
455
  expect(result.exitCode).toBe(0);
582
- const parsed = JSON.parse(result.stdout) as { tag_name: string };
583
- expect(parsed.tag_name).toBe("v0.8.0");
456
+ expect(callCount).toBe(1);
584
457
  });
585
458
 
586
- test("rate-limit error surfaces a friendly message", async () => {
459
+ test("stale cache (older than TTL) triggers a refetch", async () => {
460
+ writeCacheFile({
461
+ fetchedAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(),
462
+ recent: [REL_079],
463
+ byTag: { "v0.7.9": REL_079 },
464
+ });
465
+ let callCount = 0;
466
+ fetchHandler = async () => {
467
+ callCount += 1;
468
+ return jsonResponse([REL_080, REL_079]);
469
+ };
470
+ await runCli(["changelog"]);
471
+ expect(callCount).toBe(1);
472
+ });
473
+
474
+ test("corrupt cache JSON is treated as a miss", async () => {
475
+ mkdirSync(join(TMP_ROOT, "data"), { recursive: true });
476
+ writeFileSync(cachePath, "{not valid json");
477
+ let callCount = 0;
478
+ fetchHandler = async () => {
479
+ callCount += 1;
480
+ return jsonResponse([REL_080]);
481
+ };
482
+ await runCli(["changelog"]);
483
+ expect(callCount).toBe(1);
484
+ });
485
+
486
+ test("rolling cache is capped at 5 stable releases regardless of fetched count", async () => {
487
+ const stables = [
488
+ stableRelease("v0.9.0"),
489
+ stableRelease("v0.8.9"),
490
+ stableRelease("v0.8.8"),
491
+ stableRelease("v0.8.7"),
492
+ stableRelease("v0.8.6"),
493
+ stableRelease("v0.8.5"),
494
+ stableRelease("v0.8.4"),
495
+ stableRelease("v0.8.3"),
496
+ ];
497
+ fetchHandler = async () => jsonResponse(stables);
498
+ await runCli(["changelog", "list", "--limit", "8"]);
499
+
500
+ const cached = readCacheFile();
501
+ expect(cached?.recent.length).toBe(5);
502
+ expect(cached?.recent.map((r) => r.tag_name)).toEqual([
503
+ "v0.9.0",
504
+ "v0.8.9",
505
+ "v0.8.8",
506
+ "v0.8.7",
507
+ "v0.8.6",
508
+ ]);
509
+ });
510
+
511
+ test("page-size buffer absorbs drafts/prereleases so a small --limit still surfaces stable releases", async () => {
512
+ // First page is mostly noise — drafts and prereleases — followed by
513
+ // stable releases. With a hardcoded per_page=limit the small budget
514
+ // would be eaten by noise. The buffer keeps stable releases visible.
515
+ const noise = Array.from({ length: 10 }, (_, i) => ({
516
+ ...REL_DRAFT,
517
+ tag_name: `v9.9.${i}`,
518
+ }));
519
+ fetchHandler = async () => jsonResponse([...noise, REL_080, REL_079]);
520
+ const result = await runCli([
521
+ "changelog",
522
+ "list",
523
+ "--limit",
524
+ "2",
525
+ "--json",
526
+ ]);
527
+ expect(result.exitCode).toBe(0);
528
+ const parsed = JSON.parse(result.stdout) as {
529
+ releases: Array<{ tag_name: string }>;
530
+ };
531
+ expect(parsed.releases.map((r) => r.tag_name)).toEqual([
532
+ "v0.8.0",
533
+ "v0.7.9",
534
+ ]);
535
+ });
536
+
537
+ test("--limit larger than cached count refetches to satisfy the request", async () => {
538
+ writeCacheFile({
539
+ fetchedAt: new Date().toISOString(),
540
+ recent: [REL_080],
541
+ byTag: { "v0.8.0": REL_080 },
542
+ });
543
+ let callCount = 0;
544
+ fetchHandler = async () => {
545
+ callCount += 1;
546
+ return jsonResponse([REL_080, REL_079]);
547
+ };
548
+ await runCli(["changelog", "list", "--limit", "10"]);
549
+ expect(callCount).toBe(1);
550
+ });
551
+ });
552
+
553
+ // ── Errors ───────────────────────────────────────────────────────────
554
+
555
+ describe("assistant changelog errors", () => {
556
+ test("403 rate-limit response surfaces a friendly stderr", async () => {
587
557
  fetchHandler = async () =>
588
558
  new Response("rate limit exceeded", { status: 403 });
589
559
  const result = await runCli(["changelog", "--no-cache"]);
590
560
  expect(result.exitCode).toBe(1);
591
561
  expect(result.stderr).toMatch(/rate limit/i);
592
562
  });
563
+
564
+ test("429 rate-limit response also surfaces the friendly message", async () => {
565
+ fetchHandler = async () =>
566
+ new Response("too many requests", { status: 429 });
567
+ const result = await runCli(["changelog", "--no-cache"]);
568
+ expect(result.exitCode).toBe(1);
569
+ expect(result.stderr).toMatch(/rate limit/i);
570
+ });
571
+
572
+ test("500 from GitHub surfaces the status code", async () => {
573
+ fetchHandler = async () => new Response("server is sad", { status: 500 });
574
+ const result = await runCli(["changelog", "--no-cache"]);
575
+ expect(result.exitCode).toBe(1);
576
+ expect(result.stderr).toContain("500");
577
+ });
593
578
  });