@vellumai/assistant 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
@@ -0,0 +1,251 @@
1
+ import { mkdtempSync, realpathSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = realpathSync(
7
+ mkdtempSync(join(tmpdir(), "http-conversation-lineage-test-")),
8
+ );
9
+
10
+ mock.module("../util/platform.js", () => ({
11
+ getRootDir: () => join(testDir, ".vellum"),
12
+ getDataDir: () => join(testDir, ".vellum", "workspace", "data"),
13
+ getWorkspaceDir: () => join(testDir, ".vellum", "workspace"),
14
+ getConversationsDir: () =>
15
+ join(testDir, ".vellum", "workspace", "conversations"),
16
+ isMacOS: () => process.platform === "darwin",
17
+ isLinux: () => process.platform === "linux",
18
+ isWindows: () => process.platform === "win32",
19
+ getPidPath: () => join(testDir, "test.pid"),
20
+ getDbPath: () => join(testDir, "test.db"),
21
+ getLogPath: () => join(testDir, "test.log"),
22
+ ensureDataDir: () => {},
23
+ }));
24
+
25
+ mock.module("../util/logger.js", () => ({
26
+ getLogger: () =>
27
+ new Proxy({} as Record<string, unknown>, {
28
+ get: () => () => {},
29
+ }),
30
+ }));
31
+
32
+ mock.module("../config/env.js", () => ({
33
+ isHttpAuthDisabled: () => true,
34
+ hasUngatedHttpAuthDisabled: () => false,
35
+ getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
36
+ getGatewayPort: () => 7830,
37
+ getRuntimeHttpPort: () => 7821,
38
+ getRuntimeHttpHost: () => "127.0.0.1",
39
+ getRuntimeGatewayOriginSecret: () => undefined,
40
+ getIngressPublicBaseUrl: () => undefined,
41
+ setIngressPublicBaseUrl: () => {},
42
+ }));
43
+
44
+ mock.module("../config/loader.js", () => ({
45
+ getConfig: () => ({
46
+ ui: {},
47
+ model: "test",
48
+ provider: "test",
49
+ memory: { enabled: false },
50
+ rateLimit: { maxRequestsPerMinute: 0 },
51
+ secretDetection: { enabled: false },
52
+ }),
53
+ }));
54
+
55
+ import {
56
+ batchSetDisplayOrders,
57
+ createConversation,
58
+ updateConversationTitle,
59
+ } from "../memory/conversation-crud.js";
60
+ import { getDb, initializeDb, rawRun, resetDb } from "../memory/db.js";
61
+ import { RuntimeHttpServer } from "../runtime/http-server.js";
62
+
63
+ initializeDb();
64
+
65
+ type ConversationSummary = {
66
+ id: string;
67
+ title: string;
68
+ displayOrder?: number | null;
69
+ isPinned?: boolean;
70
+ forkParent?: {
71
+ conversationId: string;
72
+ messageId: string;
73
+ title: string;
74
+ };
75
+ };
76
+
77
+ describe("conversation lineage in HTTP reads", () => {
78
+ let server: RuntimeHttpServer | null = null;
79
+
80
+ beforeEach(async () => {
81
+ await server?.stop();
82
+ server = null;
83
+ clearTables();
84
+ });
85
+
86
+ afterAll(async () => {
87
+ await server?.stop();
88
+ resetDb();
89
+ try {
90
+ rmSync(testDir, { recursive: true, force: true });
91
+ } catch {
92
+ /* best effort */
93
+ }
94
+ });
95
+
96
+ test("GET /v1/conversations returns forkParent for surviving parents", async () => {
97
+ const { child, parent } = seedForkedConversation();
98
+ await startServer();
99
+
100
+ const response = await fetch(url("/conversations"));
101
+ expect(response.status).toBe(200);
102
+
103
+ const body = (await response.json()) as {
104
+ conversations: ConversationSummary[];
105
+ hasMore: boolean;
106
+ };
107
+ const listedChild = body.conversations.find((item) => item.id === child.id);
108
+
109
+ expect(listedChild).toMatchObject({
110
+ id: child.id,
111
+ title: child.title ?? "Untitled",
112
+ forkParent: {
113
+ conversationId: parent.id,
114
+ messageId: "parent-msg-1",
115
+ title: parent.title ?? "Untitled",
116
+ },
117
+ });
118
+ expect(body.hasMore).toBe(false);
119
+ });
120
+
121
+ test("GET /v1/conversations/:id returns forkParent for surviving parents", async () => {
122
+ const { child, parent } = seedForkedConversation();
123
+ await startServer();
124
+
125
+ const response = await fetch(url(`/conversations/${child.id}`));
126
+ expect(response.status).toBe(200);
127
+
128
+ const body = (await response.json()) as {
129
+ conversation: ConversationSummary;
130
+ };
131
+
132
+ expect(body.conversation).toMatchObject({
133
+ id: child.id,
134
+ title: child.title ?? "Untitled",
135
+ forkParent: {
136
+ conversationId: parent.id,
137
+ messageId: "parent-msg-1",
138
+ title: parent.title ?? "Untitled",
139
+ },
140
+ });
141
+ });
142
+
143
+ test("GET /v1/conversations/:id includes pin metadata when present", async () => {
144
+ const conversation = createConversation("Pinned conversation");
145
+ batchSetDisplayOrders([
146
+ { id: conversation.id, displayOrder: 7, isPinned: true },
147
+ ]);
148
+ await startServer();
149
+
150
+ const response = await fetch(url(`/conversations/${conversation.id}`));
151
+ expect(response.status).toBe(200);
152
+
153
+ const body = (await response.json()) as {
154
+ conversation: ConversationSummary;
155
+ };
156
+
157
+ expect(body.conversation).toMatchObject({
158
+ id: conversation.id,
159
+ title: conversation.title ?? "Untitled",
160
+ displayOrder: 7,
161
+ isPinned: true,
162
+ });
163
+ });
164
+
165
+ test("GET /v1/conversations/:id resolves the parent's current title at read time", async () => {
166
+ const { child, parent } = seedForkedConversation({
167
+ parentTitle: "Original parent title",
168
+ });
169
+ updateConversationTitle(parent.id, "Renamed parent title", 0);
170
+ await startServer();
171
+
172
+ const response = await fetch(url(`/conversations/${child.id}`));
173
+ expect(response.status).toBe(200);
174
+
175
+ const body = (await response.json()) as {
176
+ conversation: ConversationSummary;
177
+ };
178
+
179
+ expect(body.conversation.forkParent).toEqual({
180
+ conversationId: parent.id,
181
+ messageId: "parent-msg-1",
182
+ title: "Renamed parent title",
183
+ });
184
+ });
185
+
186
+ test("deleted parents are omitted from list and detail responses", async () => {
187
+ const { child, parent } = seedForkedConversation();
188
+ rawRun("DELETE FROM conversations WHERE id = ?", parent.id);
189
+ await startServer();
190
+
191
+ const listResponse = await fetch(url("/conversations"));
192
+ expect(listResponse.status).toBe(200);
193
+ const listBody = (await listResponse.json()) as {
194
+ conversations: ConversationSummary[];
195
+ };
196
+ const listedChild = listBody.conversations.find(
197
+ (item) => item.id === child.id,
198
+ );
199
+ expect(listedChild).toBeDefined();
200
+ expect(listedChild?.forkParent).toBeUndefined();
201
+
202
+ const detailResponse = await fetch(url(`/conversations/${child.id}`));
203
+ expect(detailResponse.status).toBe(200);
204
+ const detailBody = (await detailResponse.json()) as {
205
+ conversation: ConversationSummary;
206
+ };
207
+ expect(detailBody.conversation.forkParent).toBeUndefined();
208
+ });
209
+
210
+ function clearTables(): void {
211
+ const db = getDb();
212
+ db.run("DELETE FROM conversation_assistant_attention_state");
213
+ db.run("DELETE FROM external_conversation_bindings");
214
+ db.run("DELETE FROM conversation_keys");
215
+ db.run("DELETE FROM messages");
216
+ db.run("DELETE FROM conversations");
217
+ }
218
+
219
+ function seedForkedConversation(opts?: { parentTitle?: string }) {
220
+ const parent = createConversation(
221
+ opts?.parentTitle ?? "Parent conversation",
222
+ );
223
+ const child = createConversation("Forked conversation");
224
+
225
+ rawRun(
226
+ `
227
+ UPDATE conversations
228
+ SET fork_parent_conversation_id = ?, fork_parent_message_id = ?
229
+ WHERE id = ?
230
+ `,
231
+ parent.id,
232
+ "parent-msg-1",
233
+ child.id,
234
+ );
235
+
236
+ return { parent, child };
237
+ }
238
+
239
+ async function startServer(): Promise<void> {
240
+ server = new RuntimeHttpServer({
241
+ port: 0,
242
+ bearerToken: "test-bearer-token",
243
+ });
244
+ await server.start();
245
+ }
246
+
247
+ function url(pathname: string): string {
248
+ if (!server) throw new Error("server not started");
249
+ return `http://127.0.0.1:${server.actualPort}/v1${pathname}`;
250
+ }
251
+ });
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { reinjectImageSourcePaths } from "../daemon/conversation-lifecycle.js";
4
+ import type { ContentBlock } from "../providers/types.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // reinjectImageSourcePaths — re-inject [Attached image source: /path]
8
+ // annotations when loading conversation history from DB
9
+ // ---------------------------------------------------------------------------
10
+
11
+ describe("reinjectImageSourcePaths", () => {
12
+ const baseContent: ContentBlock[] = [
13
+ { type: "text", text: "what is this?" },
14
+ {
15
+ type: "image",
16
+ source: { type: "base64", media_type: "image/jpeg", data: "base64img" },
17
+ },
18
+ ];
19
+
20
+ test("adds annotation when user message has imageSourcePaths in metadata", () => {
21
+ const metadata = JSON.stringify({
22
+ imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
23
+ });
24
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
25
+
26
+ expect(result).toHaveLength(3);
27
+ const annotation = result[2] as { type: "text"; text: string };
28
+ expect(annotation.type).toBe("text");
29
+ expect(annotation.text).toBe(
30
+ "[Attached image source: /Users/me/Desktop/photo.jpg]",
31
+ );
32
+ });
33
+
34
+ test("does NOT annotate assistant messages even if metadata has imageSourcePaths", () => {
35
+ const metadata = JSON.stringify({
36
+ imageSourcePaths: { "photo.jpg": "/Users/me/Desktop/photo.jpg" },
37
+ });
38
+ const result = reinjectImageSourcePaths(baseContent, "assistant", metadata);
39
+
40
+ // Should return the original content unchanged
41
+ expect(result).toBe(baseContent);
42
+ expect(result).toHaveLength(2);
43
+ });
44
+
45
+ test("returns content unchanged when metadata is null", () => {
46
+ const result = reinjectImageSourcePaths(baseContent, "user", null);
47
+ expect(result).toBe(baseContent);
48
+ expect(result).toHaveLength(2);
49
+ });
50
+
51
+ test("returns content unchanged when metadata has no imageSourcePaths", () => {
52
+ const metadata = JSON.stringify({
53
+ userMessageChannel: "desktop",
54
+ });
55
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
56
+ expect(result).toBe(baseContent);
57
+ expect(result).toHaveLength(2);
58
+ });
59
+
60
+ test("returns content unchanged when imageSourcePaths is empty object", () => {
61
+ const metadata = JSON.stringify({
62
+ imageSourcePaths: {},
63
+ });
64
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
65
+ expect(result).toBe(baseContent);
66
+ expect(result).toHaveLength(2);
67
+ });
68
+
69
+ test("handles multiple image source paths", () => {
70
+ const metadata = JSON.stringify({
71
+ imageSourcePaths: {
72
+ "a.jpg": "/path/to/a.jpg",
73
+ "b.png": "/path/to/b.png",
74
+ },
75
+ });
76
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
77
+
78
+ expect(result).toHaveLength(3);
79
+ const annotation = result[2] as { type: "text"; text: string };
80
+ expect(annotation.type).toBe("text");
81
+ expect(annotation.text).toBe(
82
+ "[Attached image source: /path/to/a.jpg]\n[Attached image source: /path/to/b.png]",
83
+ );
84
+ });
85
+
86
+ test("gracefully handles malformed metadata JSON", () => {
87
+ const result = reinjectImageSourcePaths(
88
+ baseContent,
89
+ "user",
90
+ "not-valid-json{{{",
91
+ );
92
+ // Should return original content, not throw
93
+ expect(result).toBe(baseContent);
94
+ expect(result).toHaveLength(2);
95
+ });
96
+
97
+ test("filters out non-string values in imageSourcePaths", () => {
98
+ const metadata = JSON.stringify({
99
+ imageSourcePaths: {
100
+ "photo.jpg": "/Users/me/Desktop/photo.jpg",
101
+ "bad.jpg": 42,
102
+ "also_bad.jpg": null,
103
+ },
104
+ });
105
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
106
+
107
+ expect(result).toHaveLength(3);
108
+ const annotation = result[2] as { type: "text"; text: string };
109
+ expect(annotation.text).toBe(
110
+ "[Attached image source: /Users/me/Desktop/photo.jpg]",
111
+ );
112
+ });
113
+
114
+ test("returns content unchanged when imageSourcePaths has only non-string values", () => {
115
+ const metadata = JSON.stringify({
116
+ imageSourcePaths: {
117
+ "bad.jpg": 42,
118
+ "also_bad.jpg": null,
119
+ },
120
+ });
121
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
122
+ expect(result).toBe(baseContent);
123
+ expect(result).toHaveLength(2);
124
+ });
125
+
126
+ test("preserves original content blocks in returned array", () => {
127
+ const metadata = JSON.stringify({
128
+ imageSourcePaths: { "photo.jpg": "/path/photo.jpg" },
129
+ });
130
+ const result = reinjectImageSourcePaths(baseContent, "user", metadata);
131
+
132
+ // First two blocks should be identical to the originals
133
+ expect(result[0]).toEqual(baseContent[0]);
134
+ expect(result[1]).toEqual(baseContent[1]);
135
+ });
136
+ });
@@ -0,0 +1,311 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ import type { InlineCommandResult } from "../skills/inline-command-runner.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks — must be declared before the module under test is imported
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const mockConfig = {
10
+ provider: "anthropic",
11
+ model: "test",
12
+ maxTokens: 4096,
13
+ dataDir: "/tmp",
14
+ sandbox: { enabled: true },
15
+ timeouts: {
16
+ shellDefaultTimeoutSec: 120,
17
+ shellMaxTimeoutSec: 600,
18
+ permissionTimeoutSec: 300,
19
+ },
20
+ rateLimit: { maxRequestsPerMinute: 0 },
21
+ secretDetection: {
22
+ enabled: true,
23
+ action: "warn" as const,
24
+ entropyThreshold: 4.0,
25
+ },
26
+ auditLog: { retentionDays: 0 },
27
+ };
28
+
29
+ mock.module("../config/loader.js", () => ({
30
+ getConfig: () => mockConfig,
31
+ loadConfig: () => mockConfig,
32
+ invalidateConfigCache: () => {},
33
+ saveConfig: () => {},
34
+ loadRawConfig: () => ({}),
35
+ saveRawConfig: () => {},
36
+ getNestedValue: () => undefined,
37
+ setNestedValue: () => {},
38
+ }));
39
+
40
+ mock.module("../util/logger.js", () => ({
41
+ getLogger: () =>
42
+ new Proxy({} as Record<string, unknown>, {
43
+ get: () => () => {},
44
+ }),
45
+ }));
46
+
47
+ // Track wrapCommand calls to verify sandbox-only execution
48
+ let lastWrapCall: {
49
+ command: string;
50
+ workingDir: string;
51
+ config: { enabled: boolean };
52
+ options?: { networkMode?: string };
53
+ } | null = null;
54
+
55
+ mock.module("../tools/terminal/sandbox.js", () => ({
56
+ wrapCommand: (
57
+ command: string,
58
+ workingDir: string,
59
+ config: { enabled: boolean },
60
+ options?: { networkMode?: string },
61
+ ) => {
62
+ lastWrapCall = { command, workingDir, config, options };
63
+ return {
64
+ command: "bash",
65
+ args: ["-c", "--", command],
66
+ sandboxed: false,
67
+ };
68
+ },
69
+ }));
70
+
71
+ mock.module("../tools/terminal/safe-env.js", () => ({
72
+ buildSanitizedEnv: () => ({
73
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
74
+ HOME: process.env.HOME ?? "/tmp",
75
+ }),
76
+ }));
77
+
78
+ import { runInlineCommand } from "../skills/inline-command-runner.js";
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Helpers
82
+ // ---------------------------------------------------------------------------
83
+
84
+ const CWD = process.cwd();
85
+
86
+ function expectOk(result: InlineCommandResult): void {
87
+ expect(result.ok).toBe(true);
88
+ expect(result.failureReason).toBeUndefined();
89
+ }
90
+
91
+ function expectFailure(
92
+ result: InlineCommandResult,
93
+ reason: InlineCommandResult["failureReason"],
94
+ ): void {
95
+ expect(result.ok).toBe(false);
96
+ expect(result.failureReason).toBe(reason);
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Tests
101
+ // ---------------------------------------------------------------------------
102
+
103
+ describe("runInlineCommand", () => {
104
+ // ── Sandbox enforcement ──────────────────────────────────────────────────
105
+
106
+ describe("sandbox enforcement", () => {
107
+ test("always passes sandbox config with enabled=true", async () => {
108
+ lastWrapCall = null;
109
+ await runInlineCommand("echo sandbox-check", CWD);
110
+
111
+ expect(lastWrapCall).not.toBeNull();
112
+ expect(lastWrapCall!.config.enabled).toBe(true);
113
+ });
114
+
115
+ test("always passes networkMode=off", async () => {
116
+ lastWrapCall = null;
117
+ await runInlineCommand("echo network-check", CWD);
118
+
119
+ expect(lastWrapCall).not.toBeNull();
120
+ expect(lastWrapCall!.options?.networkMode).toBe("off");
121
+ });
122
+
123
+ test("uses the provided workingDir as cwd", async () => {
124
+ lastWrapCall = null;
125
+ const customDir = "/tmp/my-project";
126
+ await runInlineCommand("echo cwd-check", customDir);
127
+
128
+ expect(lastWrapCall).not.toBeNull();
129
+ expect(lastWrapCall!.workingDir).toBe(customDir);
130
+ });
131
+
132
+ test("passes the literal command string to the sandbox", async () => {
133
+ lastWrapCall = null;
134
+ await runInlineCommand("git log --oneline -n 5", CWD);
135
+
136
+ expect(lastWrapCall).not.toBeNull();
137
+ expect(lastWrapCall!.command).toBe("git log --oneline -n 5");
138
+ });
139
+ });
140
+
141
+ // ── Successful execution ─────────────────────────────────────────────────
142
+
143
+ describe("successful execution", () => {
144
+ test("captures stdout from a simple echo", async () => {
145
+ const result = await runInlineCommand("echo hello-world", CWD);
146
+
147
+ expectOk(result);
148
+ expect(result.output).toBe("hello-world");
149
+ });
150
+
151
+ test("captures multi-line stdout", async () => {
152
+ const result = await runInlineCommand(
153
+ "printf 'line1\\nline2\\nline3'",
154
+ CWD,
155
+ );
156
+
157
+ expectOk(result);
158
+ expect(result.output).toContain("line1");
159
+ expect(result.output).toContain("line2");
160
+ expect(result.output).toContain("line3");
161
+ });
162
+
163
+ test("returns empty string for command with no output", async () => {
164
+ const result = await runInlineCommand("true", CWD);
165
+
166
+ expectOk(result);
167
+ expect(result.output).toBe("");
168
+ });
169
+ });
170
+
171
+ // ── ANSI stripping ──────────────────────────────────────────────────────
172
+
173
+ describe("ANSI stripping", () => {
174
+ test("strips SGR color codes from output", async () => {
175
+ const result = await runInlineCommand(
176
+ "printf '\\033[31mred\\033[0m normal'",
177
+ CWD,
178
+ );
179
+
180
+ expectOk(result);
181
+ expect(result.output).toBe("red normal");
182
+ expect(result.output).not.toContain("\x1b");
183
+ });
184
+
185
+ test("strips cursor movement sequences", async () => {
186
+ const result = await runInlineCommand("printf '\\033[2Ahello'", CWD);
187
+
188
+ expectOk(result);
189
+ expect(result.output).toBe("hello");
190
+ });
191
+ });
192
+
193
+ // ── Binary output rejection ──────────────────────────────────────────────
194
+
195
+ describe("binary output rejection", () => {
196
+ test("rejects binary-ish output", async () => {
197
+ // Generate output with >10% control characters
198
+ const result = await runInlineCommand(
199
+ "printf '\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07abc'",
200
+ CWD,
201
+ );
202
+
203
+ expectFailure(result, "binary_output");
204
+ expect(result.output).toBe("Inline command produced binary output.");
205
+ });
206
+ });
207
+
208
+ // ── Output clamping ──────────────────────────────────────────────────────
209
+
210
+ describe("output clamping", () => {
211
+ test("truncates output exceeding the cap", async () => {
212
+ // Generate output larger than a small cap
213
+ const result = await runInlineCommand("printf '%0.s-' {1..200}", CWD, {
214
+ maxOutputChars: 50,
215
+ });
216
+
217
+ expectOk(result);
218
+ expect(result.output.length).toBeLessThanOrEqual(
219
+ 50 + "\n[output truncated]".length,
220
+ );
221
+ expect(result.output).toContain("[output truncated]");
222
+ });
223
+
224
+ test("does not truncate output under the cap", async () => {
225
+ const result = await runInlineCommand("echo short", CWD, {
226
+ maxOutputChars: 1000,
227
+ });
228
+
229
+ expectOk(result);
230
+ expect(result.output).toBe("short");
231
+ expect(result.output).not.toContain("[output truncated]");
232
+ });
233
+ });
234
+
235
+ // ── Timeout handling ─────────────────────────────────────────────────────
236
+
237
+ describe("timeout handling", () => {
238
+ test("produces deterministic timeout result", async () => {
239
+ const result = await runInlineCommand("sleep 60", CWD, {
240
+ timeoutMs: 200,
241
+ });
242
+
243
+ expectFailure(result, "timeout");
244
+ expect(result.output).toBe("Inline command timed out after 200ms.");
245
+ });
246
+ });
247
+
248
+ // ── Non-zero exit ────────────────────────────────────────────────────────
249
+
250
+ describe("non-zero exit", () => {
251
+ test("produces deterministic failure for exit code 1", async () => {
252
+ const result = await runInlineCommand("exit 1", CWD);
253
+
254
+ expectFailure(result, "non_zero_exit");
255
+ expect(result.output).toBe("Inline command failed (exit code 1).");
256
+ });
257
+
258
+ test("produces deterministic failure for exit code 127", async () => {
259
+ const result = await runInlineCommand(
260
+ "nonexistent_command_that_does_not_exist_xyz",
261
+ CWD,
262
+ );
263
+
264
+ expectFailure(result, "non_zero_exit");
265
+ expect(result.output).toMatch(
266
+ /Inline command failed \(exit code \d+\)\./,
267
+ );
268
+ });
269
+
270
+ test("does not expose stderr in the error result", async () => {
271
+ const result = await runInlineCommand("echo err-msg >&2 && exit 1", CWD);
272
+
273
+ expectFailure(result, "non_zero_exit");
274
+ expect(result.output).not.toContain("err-msg");
275
+ expect(result.output).toBe("Inline command failed (exit code 1).");
276
+ });
277
+ });
278
+
279
+ // ── Spawn failures ───────────────────────────────────────────────────────
280
+
281
+ describe("spawn failures", () => {
282
+ test("returns spawn_failure when cwd does not exist", async () => {
283
+ // When the working directory doesn't exist, the child process fails to
284
+ // start (ENOENT from posix_spawn). The runner should catch this and
285
+ // return a deterministic spawn_failure result.
286
+ const result = await runInlineCommand(
287
+ "echo hello",
288
+ "/nonexistent/path/that/does/not/exist",
289
+ );
290
+
291
+ expectFailure(result, "spawn_failure");
292
+ expect(result.output).toBe("Inline command could not be started.");
293
+ });
294
+ });
295
+
296
+ // ── stderr suppression ─────────────────────────────────────────────────
297
+
298
+ describe("stderr suppression", () => {
299
+ test("does not include stderr in successful output", async () => {
300
+ const result = await runInlineCommand(
301
+ "echo stdout-only && echo stderr-msg >&2",
302
+ CWD,
303
+ );
304
+
305
+ // Command may succeed (exit 0) — stderr should not leak into output
306
+ expectOk(result);
307
+ expect(result.output).toBe("stdout-only");
308
+ expect(result.output).not.toContain("stderr-msg");
309
+ });
310
+ });
311
+ });