@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
@@ -5,8 +5,15 @@
5
5
  * Requires the conversation to already exist (does not create new conversations).
6
6
  */
7
7
  import { getLogger } from "../../util/logger.js";
8
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
9
+ import type { AuthContext } from "../auth/types.js";
10
+ import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
8
11
  import { httpError } from "../http-errors.js";
9
12
  import type { RouteDefinition } from "../http-router.js";
13
+ import {
14
+ resolveTrustContext,
15
+ withSourceChannel,
16
+ } from "../trust-context-resolver.js";
10
17
 
11
18
  const log = getLogger("surface-action-routes");
12
19
 
@@ -18,6 +25,11 @@ interface SurfaceActionTarget {
18
25
  data?: Record<string, unknown>,
19
26
  ): void;
20
27
  handleSurfaceUndo?(surfaceId: string): void;
28
+ setTrustContext?(ctx: {
29
+ trustClass: "guardian" | "trusted_contact" | "unknown";
30
+ sourceChannel: string;
31
+ }): void;
32
+ trustContext?: { trustClass: string } | null;
21
33
  }
22
34
 
23
35
  export type ConversationLookup = (
@@ -28,6 +40,53 @@ export type ConversationLookupBySurfaceId = (
28
40
  surfaceId: string,
29
41
  ) => SurfaceActionTarget | undefined;
30
42
 
43
+ /**
44
+ * Resolve trust context from the request's auth context and set it on the
45
+ * conversation, following the same pattern as POST /v1/messages. This ensures
46
+ * surface actions inherit the correct trust class (guardian vs trusted_contact)
47
+ * rather than defaulting to unknown.
48
+ */
49
+ function applyTrustContext(
50
+ conversation: SurfaceActionTarget,
51
+ authContext: AuthContext,
52
+ ): void {
53
+ if (!conversation.setTrustContext) return;
54
+
55
+ const sourceChannel = "vellum";
56
+
57
+ if (authContext.actorPrincipalId) {
58
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
59
+ let trustCtx = resolveTrustContext({
60
+ assistantId,
61
+ sourceChannel,
62
+ conversationExternalId: "local",
63
+ actorExternalId: authContext.actorPrincipalId,
64
+ });
65
+ if (trustCtx.trustClass === "unknown") {
66
+ const healed = healGuardianBindingDrift(authContext.actorPrincipalId);
67
+ if (healed) {
68
+ trustCtx = resolveTrustContext({
69
+ assistantId,
70
+ sourceChannel,
71
+ conversationExternalId: "local",
72
+ actorExternalId: authContext.actorPrincipalId,
73
+ });
74
+ log.info(
75
+ {
76
+ actorPrincipalId: authContext.actorPrincipalId,
77
+ trustClass: trustCtx.trustClass,
78
+ },
79
+ "Trust re-resolved after guardian binding drift heal (surface action)",
80
+ );
81
+ }
82
+ }
83
+ conversation.setTrustContext(withSourceChannel(sourceChannel, trustCtx));
84
+ } else {
85
+ // Service principals or tokens without an actor ID get guardian context.
86
+ conversation.setTrustContext({ trustClass: "guardian", sourceChannel });
87
+ }
88
+ }
89
+
31
90
  /**
32
91
  * POST /v1/surface-actions — handle a UI surface action.
33
92
  *
@@ -37,6 +96,7 @@ export async function handleSurfaceAction(
37
96
  req: Request,
38
97
  findConversation: ConversationLookup,
39
98
  findConversationBySurfaceId?: ConversationLookupBySurfaceId,
99
+ authContext?: AuthContext,
40
100
  ): Promise<Response> {
41
101
  const body = (await req.json()) as {
42
102
  conversationId?: string | null;
@@ -65,6 +125,12 @@ export async function handleSurfaceAction(
65
125
  return httpError("NOT_FOUND", "No active conversation found", 404);
66
126
  }
67
127
 
128
+ // Resolve trust context from the request's auth headers so the conversation
129
+ // has the correct trust class for tool approval decisions.
130
+ if (authContext) {
131
+ applyTrustContext(conversation, authContext);
132
+ }
133
+
68
134
  try {
69
135
  conversation.handleSurfaceAction(surfaceId, actionId, data);
70
136
  log.info(
@@ -143,7 +209,7 @@ export function surfaceActionRouteDefinitions(deps: {
143
209
  {
144
210
  endpoint: "surface-actions",
145
211
  method: "POST",
146
- handler: async ({ req }) => {
212
+ handler: async ({ req, authContext }) => {
147
213
  if (!deps.findConversation) {
148
214
  return httpError(
149
215
  "NOT_IMPLEMENTED",
@@ -155,6 +221,7 @@ export function surfaceActionRouteDefinitions(deps: {
155
221
  req,
156
222
  deps.findConversation,
157
223
  deps.findConversationBySurfaceId,
224
+ authContext,
158
225
  );
159
226
  },
160
227
  },
@@ -42,7 +42,10 @@ export function traceEventRouteDefinitions(): RouteDefinition[] {
42
42
  const afterSequence = afterSequenceParam
43
43
  ? parseInt(afterSequenceParam, 10)
44
44
  : undefined;
45
- if (afterSequenceParam && (isNaN(afterSequence!) || afterSequence! < 0)) {
45
+ if (
46
+ afterSequenceParam &&
47
+ (isNaN(afterSequence!) || afterSequence! < 0)
48
+ ) {
46
49
  return httpError(
47
50
  "BAD_REQUEST",
48
51
  "afterSequence must be a non-negative integer",
@@ -15,10 +15,7 @@ import type { ScheduleSyntax } from "./recurrence-types.js";
15
15
  const logger = getLogger("schedule-store");
16
16
 
17
17
  export type ScheduleMode = "notify" | "execute";
18
- export type RoutingIntent =
19
- | "single_channel"
20
- | "multi_channel"
21
- | "all_channels";
18
+ export type RoutingIntent = "single_channel" | "multi_channel" | "all_channels";
22
19
  export type ScheduleStatus = "active" | "firing" | "fired" | "cancelled";
23
20
 
24
21
  export interface ScheduleJob {
@@ -193,9 +190,7 @@ export function listSchedules(options?: {
193
190
  conditions.push(isNull(scheduleJobs.cronExpression));
194
191
  }
195
192
  if (options?.recurringOnly) {
196
- conditions.push(
197
- sql`${scheduleJobs.cronExpression} IS NOT NULL`,
198
- );
193
+ conditions.push(sql`${scheduleJobs.cronExpression} IS NOT NULL`);
199
194
  }
200
195
  const where = conditions.length > 0 ? and(...conditions) : undefined;
201
196
  const rows = db
@@ -207,6 +202,27 @@ export function listSchedules(options?: {
207
202
  return rows.map(parseJobRow);
208
203
  }
209
204
 
205
+ /**
206
+ * Return enabled schedules whose next run falls within a time window.
207
+ * Used by the memory brief compiler to surface due-soon schedule entries.
208
+ */
209
+ export function getDueSoonSchedules(
210
+ now: number,
211
+ horizonMs: number,
212
+ ): ScheduleJob[] {
213
+ const db = getDb();
214
+ const cutoff = now + horizonMs;
215
+ const rows = db
216
+ .select()
217
+ .from(scheduleJobs)
218
+ .where(
219
+ and(eq(scheduleJobs.enabled, true), lte(scheduleJobs.nextRunAt, cutoff)),
220
+ )
221
+ .orderBy(asc(scheduleJobs.nextRunAt))
222
+ .all();
223
+ return rows.map(parseJobRow);
224
+ }
225
+
210
226
  export function updateSchedule(
211
227
  id: string,
212
228
  updates: {
@@ -415,10 +431,7 @@ export function claimDueSchedules(now: number): ScheduleJob[] {
415
431
  updatedAt: now,
416
432
  })
417
433
  .where(
418
- and(
419
- eq(scheduleJobs.id, row.id),
420
- eq(scheduleJobs.status, "active"),
421
- ),
434
+ and(eq(scheduleJobs.id, row.id), eq(scheduleJobs.status, "active")),
422
435
  )
423
436
  .run();
424
437
 
@@ -450,9 +463,7 @@ export function completeOneShot(id: string): void {
450
463
  enabled: false,
451
464
  updatedAt: now,
452
465
  })
453
- .where(
454
- and(eq(scheduleJobs.id, id), eq(scheduleJobs.status, "firing")),
455
- )
466
+ .where(and(eq(scheduleJobs.id, id), eq(scheduleJobs.status, "firing")))
456
467
  .run();
457
468
  }
458
469
 
@@ -468,9 +479,7 @@ export function failOneShot(id: string): void {
468
479
  status: "active",
469
480
  updatedAt: now,
470
481
  })
471
- .where(
472
- and(eq(scheduleJobs.id, id), eq(scheduleJobs.status, "firing")),
473
- )
482
+ .where(and(eq(scheduleJobs.id, id), eq(scheduleJobs.status, "firing")))
474
483
  .run();
475
484
  }
476
485
 
@@ -487,9 +496,7 @@ export function cancelSchedule(id: string): boolean {
487
496
  enabled: false,
488
497
  updatedAt: now,
489
498
  })
490
- .where(
491
- and(eq(scheduleJobs.id, id), eq(scheduleJobs.status, "active")),
492
- )
499
+ .where(and(eq(scheduleJobs.id, id), eq(scheduleJobs.status, "active")))
493
500
  .run();
494
501
  return rawChanges() > 0;
495
502
  }
@@ -770,7 +777,9 @@ function parseJobRow(row: typeof scheduleJobs.$inferSelect): ScheduleJob {
770
777
  };
771
778
  }
772
779
 
773
- function safeParseJson(json: string | null | undefined): Record<string, unknown> {
780
+ function safeParseJson(
781
+ json: string | null | undefined,
782
+ ): Record<string, unknown> {
774
783
  if (!json) return {};
775
784
  try {
776
785
  return JSON.parse(json) as Record<string, unknown>;
@@ -193,6 +193,27 @@ export async function getProviderKeyAsync(
193
193
  return envVar ? process.env[envVar] : undefined;
194
194
  }
195
195
 
196
+ // ---------------------------------------------------------------------------
197
+ // Masked provider key — for safe display in client UIs
198
+ // ---------------------------------------------------------------------------
199
+
200
+ /**
201
+ * Retrieve a provider API key and return a masked version suitable for
202
+ * display. Shows the first 10 characters and last 4, with `...` in between,
203
+ * always hiding at least 3 characters. Returns `null` if no key is stored.
204
+ */
205
+ export async function getMaskedProviderKey(
206
+ provider: string,
207
+ ): Promise<string | null> {
208
+ const key = await getProviderKeyAsync(provider);
209
+ if (!key || key.length === 0) return null;
210
+ const minHidden = 3;
211
+ const maxVisible = Math.max(1, key.length - minHidden);
212
+ const prefixLen = Math.min(10, maxVisible);
213
+ const suffixLen = Math.min(4, Math.max(0, maxVisible - prefixLen));
214
+ return `${key.slice(0, prefixLen)}...${suffixLen > 0 ? key.slice(-suffixLen) : ""}`;
215
+ }
216
+
196
217
  // ---------------------------------------------------------------------------
197
218
  // Test helpers
198
219
  // ---------------------------------------------------------------------------
@@ -80,7 +80,7 @@ export function handleBashSignal(filename: string): void {
80
80
  exitCode: null,
81
81
  timedOut: false,
82
82
  error:
83
- "Bash signals are disabled. The running assistant process must have been started with VELLUM_DEBUG=1 (setting it on the CLI command alone is not enough). Restart the assistant with: VELLUM_DEBUG=1 assistant start",
83
+ "Bash signals are disabled. The running assistant process must have been started with VELLUM_DEBUG=1 (setting it on the CLI command alone is not enough). Restart the assistant with: vellum sleep && VELLUM_DEBUG=1 vellum wake",
84
84
  });
85
85
  }
86
86
  return;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Canonical parser for inline command expansion tokens in skill bodies.
3
+ *
4
+ * Syntax: !\`command\`
5
+ *
6
+ * These tokens are parsed from the markdown body of a SKILL.md file (after
7
+ * frontmatter extraction). Tokens inside fenced code blocks are ignored so
8
+ * that documentation examples or literal snippets do not accidentally execute.
9
+ *
10
+ * The parser fails closed on malformed tokens: unmatched backticks, empty
11
+ * commands, or nested backticks that make the command text ambiguous are
12
+ * rejected rather than best-effort expanded.
13
+ */
14
+
15
+ import { getLogger } from "../util/logger.js";
16
+
17
+ const log = getLogger("inline-command-expansions");
18
+
19
+ // ─── Types ────────────────────────────────────────────────────────────────────
20
+
21
+ /** A single parsed inline command expansion descriptor. */
22
+ export interface InlineCommandExpansion {
23
+ /** The raw command text between the backticks (trimmed). */
24
+ command: string;
25
+ /** Byte offset of the `!` character in the original body string. */
26
+ startOffset: number;
27
+ /** Byte offset one past the closing backtick in the original body string. */
28
+ endOffset: number;
29
+ /** Stable placeholder ID derived from encounter order (0-indexed). */
30
+ placeholderId: number;
31
+ }
32
+
33
+ /** Result of parsing a skill body for inline command expansions. */
34
+ export interface InlineCommandExpansionResult {
35
+ /** Successfully parsed expansion descriptors, in encounter order. */
36
+ expansions: InlineCommandExpansion[];
37
+ /** Malformed tokens that were rejected (fail-closed). */
38
+ errors: InlineCommandExpansionError[];
39
+ }
40
+
41
+ /** A malformed inline command expansion token. */
42
+ export interface InlineCommandExpansionError {
43
+ /** The raw matched text that was rejected. */
44
+ raw: string;
45
+ /** Byte offset in the original body. */
46
+ offset: number;
47
+ /** Human-readable reason for rejection. */
48
+ reason: string;
49
+ }
50
+
51
+ // ─── Fenced code block stripping ──────────────────────────────────────────────
52
+
53
+ /**
54
+ * Build a set of character ranges that fall inside fenced code blocks.
55
+ * A fenced code block starts with a line matching ``` (with optional info
56
+ * string) and ends with a line matching ``` (or end of string).
57
+ */
58
+ function buildFencedCodeRanges(body: string): Array<[number, number]> {
59
+ const ranges: Array<[number, number]> = [];
60
+ // Match fenced code block delimiters: ``` optionally followed by info string
61
+ const fenceRe = /^(`{3,}|~{3,})(.*)?$/gm;
62
+ let openFence: { index: number; delimiter: string } | undefined;
63
+
64
+ let match: RegExpExecArray | undefined;
65
+ while ((match = fenceRe.exec(body) ?? undefined) !== undefined) {
66
+ const delimiter = match[1];
67
+ if (openFence === undefined) {
68
+ // Opening fence
69
+ openFence = {
70
+ index: match.index,
71
+ delimiter: delimiter[0].repeat(delimiter.length),
72
+ };
73
+ } else if (
74
+ delimiter[0] === openFence.delimiter[0] &&
75
+ delimiter.length >= openFence.delimiter.length &&
76
+ // Closing fence must be bare (no info string after it)
77
+ (!match[2] || match[2].trim() === "")
78
+ ) {
79
+ // Closing fence — range covers from opening fence to end of closing fence line
80
+ ranges.push([openFence.index, match.index + match[0].length]);
81
+ openFence = undefined;
82
+ }
83
+ // Otherwise ignore (nested fence-like lines inside a code block)
84
+ }
85
+
86
+ // If a fence was opened but never closed, treat everything from the opening
87
+ // fence to EOF as inside a code block.
88
+ if (openFence !== undefined) {
89
+ ranges.push([openFence.index, body.length]);
90
+ }
91
+
92
+ return ranges;
93
+ }
94
+
95
+ function isInsideFencedCode(
96
+ offset: number,
97
+ ranges: Array<[number, number]>,
98
+ ): boolean {
99
+ for (const [start, end] of ranges) {
100
+ if (offset >= start && offset < end) return true;
101
+ }
102
+ return false;
103
+ }
104
+
105
+ // ─── Parser ───────────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Parse inline command expansion tokens (`!\`...\``) from a skill body.
109
+ *
110
+ * The body must be the markdown content _after_ frontmatter has been stripped.
111
+ * Tokens inside fenced code blocks are skipped.
112
+ *
113
+ * Returns both the successfully parsed expansions and any malformed tokens
114
+ * that were rejected (fail-closed).
115
+ */
116
+ export function parseInlineCommandExpansions(
117
+ body: string,
118
+ ): InlineCommandExpansionResult {
119
+ const expansions: InlineCommandExpansion[] = [];
120
+ const errors: InlineCommandExpansionError[] = [];
121
+
122
+ const fencedRanges = buildFencedCodeRanges(body);
123
+
124
+ // Match !\`...\` tokens. The regex captures the content between the backticks.
125
+ // We use a non-greedy match to find the first closing backtick.
126
+ const tokenRe = /!\`([^`]*)\`/g;
127
+
128
+ let match: RegExpExecArray | undefined;
129
+ let placeholderCounter = 0;
130
+
131
+ while ((match = tokenRe.exec(body) ?? undefined) !== undefined) {
132
+ const startOffset = match.index;
133
+ const endOffset = startOffset + match[0].length;
134
+ const rawCommand = match[1];
135
+
136
+ // Skip tokens inside fenced code blocks
137
+ if (isInsideFencedCode(startOffset, fencedRanges)) {
138
+ continue;
139
+ }
140
+
141
+ // Fail closed: empty command
142
+ if (rawCommand.trim().length === 0) {
143
+ errors.push({
144
+ raw: match[0],
145
+ offset: startOffset,
146
+ reason: "Empty command text",
147
+ });
148
+ continue;
149
+ }
150
+
151
+ // Fail closed: nested backticks (would make command text ambiguous)
152
+ if (rawCommand.includes("`")) {
153
+ errors.push({
154
+ raw: match[0],
155
+ offset: startOffset,
156
+ reason: "Nested backticks in command text",
157
+ });
158
+ continue;
159
+ }
160
+
161
+ expansions.push({
162
+ command: rawCommand.trim(),
163
+ startOffset,
164
+ endOffset,
165
+ placeholderId: placeholderCounter++,
166
+ });
167
+ }
168
+
169
+ // Also detect malformed tokens: !\` without a closing backtick.
170
+ // These are unmatched opening tokens that didn't match the regex above.
171
+ const unmatchedRe = /!\`/g;
172
+ const matchedStarts = new Set<number>();
173
+ // Re-run the token regex to collect all matched positions
174
+ tokenRe.lastIndex = 0;
175
+ while ((match = tokenRe.exec(body) ?? undefined) !== undefined) {
176
+ matchedStarts.add(match.index);
177
+ }
178
+
179
+ let unmatchedMatch: RegExpExecArray | undefined;
180
+ while ((unmatchedMatch = unmatchedRe.exec(body) ?? undefined) !== undefined) {
181
+ const offset = unmatchedMatch.index;
182
+
183
+ // Skip if this was already matched as a complete token
184
+ if (matchedStarts.has(offset)) continue;
185
+
186
+ // Skip if inside a fenced code block
187
+ if (isInsideFencedCode(offset, fencedRanges)) continue;
188
+
189
+ errors.push({
190
+ raw: body.slice(offset, Math.min(offset + 40, body.length)),
191
+ offset,
192
+ reason: "Unmatched opening backtick (no closing backtick found)",
193
+ });
194
+ }
195
+
196
+ if (errors.length > 0) {
197
+ log.warn(
198
+ { errorCount: errors.length, errors },
199
+ "Malformed inline command expansion tokens detected",
200
+ );
201
+ }
202
+
203
+ return { expansions, errors };
204
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Renderer for inline command expansion tokens in skill bodies.
3
+ *
4
+ * Given a skill body and its parsed `InlineCommandExpansion` descriptors,
5
+ * replaces each `!\`command\`` token by executing the command through the
6
+ * sandbox-only runner and wrapping the result in XML tags:
7
+ *
8
+ * <inline_skill_command index="0">...output...</inline_skill_command>
9
+ *
10
+ * Render failures produce stable inline stubs rather than dumping raw
11
+ * shell stderr into the prompt:
12
+ *
13
+ * <inline_skill_command index="0">[inline command unavailable: <reason>]</inline_skill_command>
14
+ */
15
+
16
+ import { getLogger } from "../util/logger.js";
17
+ import type { InlineCommandExpansion } from "./inline-command-expansions.js";
18
+ import type { InlineCommandResult } from "./inline-command-runner.js";
19
+ import { runInlineCommand } from "./inline-command-runner.js";
20
+
21
+ const log = getLogger("inline-command-render");
22
+
23
+ // ─── Types ────────────────────────────────────────────────────────────────────
24
+
25
+ /** Result of rendering all inline command expansions in a skill body. */
26
+ export interface InlineCommandRenderResult {
27
+ /** The body with all inline command tokens replaced. */
28
+ renderedBody: string;
29
+ /** Count of successfully expanded tokens. */
30
+ expandedCount: number;
31
+ /** Count of tokens that failed to expand (rendered as stubs). */
32
+ failedCount: number;
33
+ }
34
+
35
+ // ─── Failure reason mapping ───────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Map a machine-readable failure reason to a human-readable stub message
39
+ * suitable for inclusion in the prompt. These messages are intentionally
40
+ * terse and deterministic so they don't leak raw stderr or confuse the LLM.
41
+ */
42
+ function failureReasonToStub(result: InlineCommandResult): string {
43
+ switch (result.failureReason) {
44
+ case "timeout":
45
+ return "command timed out";
46
+ case "non_zero_exit":
47
+ return "command failed";
48
+ case "binary_output":
49
+ return "command produced binary output";
50
+ case "spawn_failure":
51
+ return "command could not be started";
52
+ default:
53
+ return "unknown error";
54
+ }
55
+ }
56
+
57
+ // ─── Public API ───────────────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Render all inline command expansion tokens in a skill body.
61
+ *
62
+ * Each `!\`command\`` token is executed through the sandbox-only runner and
63
+ * replaced with its output wrapped in XML tags. Expansions are processed
64
+ * sequentially (not in parallel) to keep execution order deterministic and
65
+ * avoid overwhelming the sandbox.
66
+ *
67
+ * @param body The skill body containing `!\`command\`` tokens.
68
+ * @param expansions Parsed expansion descriptors from `parseInlineCommandExpansions`.
69
+ * @param workingDir The conversation's working directory (repo root).
70
+ */
71
+ export async function renderInlineCommands(
72
+ body: string,
73
+ expansions: InlineCommandExpansion[],
74
+ workingDir: string,
75
+ ): Promise<InlineCommandRenderResult> {
76
+ if (expansions.length === 0) {
77
+ return { renderedBody: body, expandedCount: 0, failedCount: 0 };
78
+ }
79
+
80
+ let expandedCount = 0;
81
+ let failedCount = 0;
82
+
83
+ // Process replacements in reverse offset order so that earlier offsets
84
+ // remain valid after splicing in replacement text.
85
+ const sorted = [...expansions].sort((a, b) => b.startOffset - a.startOffset);
86
+
87
+ let result = body;
88
+
89
+ for (const expansion of sorted) {
90
+ const commandResult = await runInlineCommand(expansion.command, workingDir);
91
+
92
+ let replacement: string;
93
+ if (commandResult.ok) {
94
+ replacement = wrapInXml(expansion.placeholderId, commandResult.output);
95
+ expandedCount++;
96
+ } else {
97
+ const stub = failureReasonToStub(commandResult);
98
+ replacement = wrapInXml(
99
+ expansion.placeholderId,
100
+ `[inline command unavailable: ${stub}]`,
101
+ );
102
+ failedCount++;
103
+ log.warn(
104
+ {
105
+ command: expansion.command,
106
+ placeholderId: expansion.placeholderId,
107
+ failureReason: commandResult.failureReason,
108
+ },
109
+ "Inline command expansion failed, rendering stub",
110
+ );
111
+ }
112
+
113
+ // Replace the original token with the rendered output
114
+ result =
115
+ result.slice(0, expansion.startOffset) +
116
+ replacement +
117
+ result.slice(expansion.endOffset);
118
+ }
119
+
120
+ return { renderedBody: result, expandedCount, failedCount };
121
+ }
122
+
123
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
124
+
125
+ function wrapInXml(index: number, content: string): string {
126
+ return `<inline_skill_command index="${index}">${content}</inline_skill_command>`;
127
+ }