@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
@@ -21,12 +21,24 @@ import {
21
21
  indexCatalogById,
22
22
  validateIncludes,
23
23
  } from "../../skills/include-graph.js";
24
+ import { renderInlineCommands } from "../../skills/inline-command-render.js";
24
25
  import { parseToolManifestFile } from "../../skills/tool-manifest.js";
25
26
  import { computeSkillVersionHash } from "../../skills/version-hash.js";
26
27
  import { getLogger } from "../../util/logger.js";
28
+ import { getWorkspaceDirDisplay } from "../../util/platform.js";
27
29
  import { registerTool } from "../registry.js";
28
30
  import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
29
31
 
32
+ /** Canonical feature flag key for inline skill command expansion. */
33
+ const INLINE_COMMANDS_FLAG_KEY = "feature_flags.inline-skill-commands.enabled";
34
+
35
+ /** Skill sources eligible for inline command expansion in v1. */
36
+ const INLINE_COMMAND_ELIGIBLE_SOURCES = new Set([
37
+ "bundled",
38
+ "managed",
39
+ "workspace",
40
+ ]);
41
+
30
42
  const log = getLogger("skill-load");
31
43
 
32
44
  /**
@@ -76,7 +88,9 @@ function formatToolSchemas(
76
88
 
77
89
  for (const tool of manifest.tools) {
78
90
  lines.push(`${toolHeadingLevel} ${tool.name}`);
79
- lines.push(tool.description);
91
+ lines.push(
92
+ tool.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay()),
93
+ );
80
94
 
81
95
  const schema = tool.input_schema;
82
96
  const properties = schema.properties as
@@ -96,7 +110,7 @@ function formatToolSchemas(
96
110
  : "optional";
97
111
  const descPart =
98
112
  typeof paramDef.description === "string"
99
- ? `: ${paramDef.description}`
113
+ ? `: ${paramDef.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay())}`
100
114
  : "";
101
115
  lines.push(
102
116
  `- ${paramName} (${paramType}, ${requiredLabel})${descPart}`,
@@ -113,7 +127,7 @@ function formatToolSchemas(
113
127
  export class SkillLoadTool implements Tool {
114
128
  name = "skill_load";
115
129
  description =
116
- "Load full instructions for a skill. Works for both bundled skills (listed in the catalog) and workspace skills in ~/.vellum/workspace/skills.";
130
+ "Load full instructions for a skill. Works for both bundled skills (listed in the catalog) and custom workspace skills.";
117
131
  category = "skills";
118
132
  defaultRiskLevel = RiskLevel.Low;
119
133
 
@@ -136,7 +150,7 @@ export class SkillLoadTool implements Tool {
136
150
 
137
151
  async execute(
138
152
  input: Record<string, unknown>,
139
- _context: ToolContext,
153
+ context: ToolContext,
140
154
  ): Promise<ToolExecutionResult> {
141
155
  const selector = input.skill;
142
156
  if (typeof selector !== "string" || selector.trim().length === 0) {
@@ -279,7 +293,63 @@ export class SkillLoadTool implements Tool {
279
293
  }
280
294
  }
281
295
 
282
- const body = skill.body.length > 0 ? skill.body : "(No body content)";
296
+ let body = skill.body.length > 0 ? skill.body : "(No body content)";
297
+
298
+ // ── Inline command expansion ──────────────────────────────────────────
299
+ const hasInlineCommands =
300
+ skill.inlineCommandExpansions && skill.inlineCommandExpansions.length > 0;
301
+
302
+ if (hasInlineCommands) {
303
+ const inlineFlagEnabled = isAssistantFeatureFlagEnabled(
304
+ INLINE_COMMANDS_FLAG_KEY,
305
+ config,
306
+ );
307
+
308
+ if (!inlineFlagEnabled) {
309
+ // Feature flag is off: fail closed instead of leaving live tokens in
310
+ // the prompt that the LLM might try to interpret.
311
+ return {
312
+ content: `Error: skill "${skill.id}" contains inline command expansions but the inline-skill-commands feature flag is disabled. Enable the flag to use this skill.`,
313
+ isError: true,
314
+ };
315
+ }
316
+
317
+ if (skill.source === "extra") {
318
+ // Third-party extra roots are out of scope for inline command
319
+ // expansion in v1. Reject explicitly so the failure is clear.
320
+ return {
321
+ content: `Error: skill "${skill.id}" contains inline command expansions but inline commands are not supported for third-party (extra) skill sources.`,
322
+ isError: true,
323
+ };
324
+ }
325
+
326
+ if (!INLINE_COMMAND_ELIGIBLE_SOURCES.has(skill.source)) {
327
+ // Defensive: reject any other unknown sources that somehow have
328
+ // inline commands. Should not happen with current SkillSource values,
329
+ // but fail closed if a new source type is added without updating this.
330
+ return {
331
+ content: `Error: skill "${skill.id}" contains inline command expansions but source "${skill.source}" is not eligible for inline command expansion.`,
332
+ isError: true,
333
+ };
334
+ }
335
+
336
+ // Render inline commands by executing each through the sandbox runner
337
+ const renderResult = await renderInlineCommands(
338
+ body,
339
+ skill.inlineCommandExpansions!,
340
+ context.workingDir,
341
+ );
342
+ body = renderResult.renderedBody;
343
+
344
+ log.info(
345
+ {
346
+ skillId: skill.id,
347
+ expandedCount: renderResult.expandedCount,
348
+ failedCount: renderResult.failedCount,
349
+ },
350
+ "Rendered inline command expansions",
351
+ );
352
+ }
283
353
 
284
354
  // Build reference file listing (if any)
285
355
  const referenceListing = listReferenceFiles(skill.directoryPath);
@@ -313,8 +383,72 @@ export class SkillLoadTool implements Tool {
313
383
  // Load the included skill's body content
314
384
  const childLoaded = loadSkillBySelector(childId);
315
385
  if (childLoaded.skill && childLoaded.skill.body.length > 0) {
386
+ let childBody = childLoaded.skill.body;
387
+
388
+ // ── Inline command expansion for included child skill ─────────
389
+ const childHasInlineCommands =
390
+ childLoaded.skill.inlineCommandExpansions &&
391
+ childLoaded.skill.inlineCommandExpansions.length > 0;
392
+
393
+ if (childHasInlineCommands) {
394
+ const childInlineFlagEnabled = isAssistantFeatureFlagEnabled(
395
+ INLINE_COMMANDS_FLAG_KEY,
396
+ config,
397
+ );
398
+
399
+ // Fail closed: if the flag is off, reject the entire skill_load
400
+ // just like we do for root skills. Leaving raw !`...` tokens in
401
+ // the prompt would violate the documented fail-closed contract.
402
+ if (!childInlineFlagEnabled) {
403
+ return {
404
+ content: `Error: included skill "${childId}" contains inline command expansions but the inline-skill-commands feature flag is disabled. Enable the flag to use skill "${skill.id}".`,
405
+ isError: true,
406
+ };
407
+ }
408
+
409
+ if (childLoaded.skill.source === "extra") {
410
+ return {
411
+ content: `Error: included skill "${childId}" contains inline command expansions but inline commands are not supported for third-party (extra) skill sources.`,
412
+ isError: true,
413
+ };
414
+ }
415
+
416
+ if (
417
+ !INLINE_COMMAND_ELIGIBLE_SOURCES.has(childLoaded.skill.source)
418
+ ) {
419
+ return {
420
+ content: `Error: included skill "${childId}" contains inline command expansions but source "${childLoaded.skill.source}" is not eligible for inline command expansion.`,
421
+ isError: true,
422
+ };
423
+ }
424
+
425
+ try {
426
+ const childRenderResult = await renderInlineCommands(
427
+ childBody,
428
+ childLoaded.skill.inlineCommandExpansions!,
429
+ context.workingDir,
430
+ );
431
+ childBody = childRenderResult.renderedBody;
432
+
433
+ log.info(
434
+ {
435
+ skillId: childId,
436
+ parentSkillId: skill.id,
437
+ expandedCount: childRenderResult.expandedCount,
438
+ failedCount: childRenderResult.failedCount,
439
+ },
440
+ "Rendered inline command expansions for included skill",
441
+ );
442
+ } catch (err) {
443
+ log.warn(
444
+ { err, skillId: childId, parentSkillId: skill.id },
445
+ "Failed to render inline commands for included skill, using raw body",
446
+ );
447
+ }
448
+ }
449
+
316
450
  includedBodies.push(
317
- `--- Included Skill: ${childLoaded.skill.displayName} (${childId}) ---\n${childLoaded.skill.body}`,
451
+ `--- Included Skill: ${childLoaded.skill.displayName} (${childId}) ---\n${childBody}`,
318
452
  );
319
453
 
320
454
  // List reference files for the included skill
@@ -11,8 +11,6 @@ import {
11
11
  isCesSecureInstallEnabled,
12
12
  isCesToolsEnabled,
13
13
  } from "../credential-execution/feature-gates.js";
14
- import { assetMaterializeTool } from "./assets/materialize.js";
15
- import { assetSearchTool } from "./assets/search.js";
16
14
  import { makeAuthenticatedRequestTool } from "./credential-execution/make-authenticated-request.js";
17
15
  import { manageSecureCommandTool } from "./credential-execution/manage-secure-command-tool.js";
18
16
  import { runAuthenticatedCommandTool } from "./credential-execution/run-authenticated-command.js";
@@ -63,8 +61,6 @@ export const eagerModuleToolNames: string[] = [
63
61
  "skill_execute",
64
62
  "skill_load",
65
63
  "request_system_permission",
66
- "asset_search",
67
- "asset_materialize",
68
64
  ];
69
65
 
70
66
  // ── Explicit tool instances ─────────────────────────────────────────
@@ -85,8 +81,6 @@ export const explicitTools: Tool[] = [
85
81
  skillExecuteTool,
86
82
  skillLoadTool,
87
83
  requestSystemPermissionTool,
88
- assetSearchTool,
89
- assetMaterializeTool,
90
84
  // Always-explicit tools
91
85
  memoryManageTool,
92
86
  memoryRecallTool,
@@ -30,7 +30,7 @@ export const uiShowTool: Tool = {
30
30
  description:
31
31
  "Show structured data or UI to the user. For long-form writing use the document skill; for interactive apps use the app-builder skill.\n\n" +
32
32
  "Surface types (data shapes):\n" +
33
- "- card: { title, subtitle?, body, metadata?: [{ label, value }], template?, templateData? }. Templates: \"weather_forecast\" (native weather widget), \"task_progress\" (live step tracker - update via ui_update on data.templateData; shape: { title, status: \"in_progress\"|\"completed\"|\"failed\", steps: [{ label, status: \"pending\"|\"in_progress\"|\"completed\"|\"failed\", detail? }] })\n" +
33
+ '- card: { title, subtitle?, body, metadata?: [{ label, value }], template?, templateData? }. Templates: "weather_forecast" (native weather widget), "task_progress" (live step tracker - update via ui_update on data.templateData; shape: { title, status: "in_progress"|"completed"|"failed", steps: [{ label, status: "pending"|"in_progress"|"completed"|"failed", detail? }] })\n' +
34
34
  '- table: { columns: [{ id, label, width? }], rows: [{ id, cells: Record<id, string | { text, icon?, iconColor?: "success"|"warning"|"error"|"muted" }>, selectable?, selected? }], selectionMode?: "none"|"single"|"multiple", caption? }\n' +
35
35
  '- form: { description?, fields: [{ id, type: "text"|"textarea"|"select"|"toggle"|"number"|"password", label, placeholder?, required?, defaultValue?, options?: [{ label, value }] }], submitLabel? }. Multi-page: { pages: [{ id, title, description?, fields }], pageLabels?: { next?, back?, submit? }, submitLabel? }\n' +
36
36
  '- list: { items: [{ id, title, subtitle?, icon?, selected? }], selectionMode: "single"|"multiple"|"none" }\n' +
@@ -91,7 +91,7 @@ export const uiShowTool: Tool = {
91
91
  type: "string",
92
92
  enum: ["inline", "panel"],
93
93
  description:
94
- 'Where to render the surface. "inline" embeds it in the chat message. "panel" shows a floating window. Defaults to "inline".',
94
+ 'Where to render the surface. "inline" embeds it in the chat message. "panel" shows a floating window. Defaults to "inline". Prefer inline — only use panel when the user explicitly asks for a separate window.',
95
95
  },
96
96
  await_action: {
97
97
  type: "boolean",
@@ -1,9 +1,16 @@
1
1
  /**
2
2
  * Device ID resolver.
3
3
  *
4
- * Reads or creates a stable per-device UUID stored in ~/.vellum/device.json.
5
- * The file is a JSON object (`{ "deviceId": "<uuid>" }`) extensible for
6
- * future per-device metadata.
4
+ * Reads or creates a stable per-device UUID stored in device.json under the
5
+ * Vellum config directory. The file is a JSON object (`{ "deviceId": "<uuid>" }`)
6
+ * extensible for future per-device metadata.
7
+ *
8
+ * Path resolution:
9
+ * - Containerized (IS_CONTAINERIZED=true): uses BASE_DATA_DIR, which maps to a
10
+ * persistent volume. Each container is effectively its own "device."
11
+ * - Local (single or multi-instance): uses homedir() so all instances on the
12
+ * same machine share a single device ID, even when BASE_DATA_DIR is set to
13
+ * an instance-scoped directory.
7
14
  *
8
15
  * The value is cached in memory after the first successful read/write.
9
16
  * Falls back to a generated UUID if the file cannot be read or written.
@@ -14,18 +21,34 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
21
  import { homedir } from "node:os";
15
22
  import { join } from "node:path";
16
23
 
24
+ import { getBaseDataDir, getIsContainerized } from "../config/env-registry.js";
17
25
  import { getLogger } from "./logger.js";
18
26
 
19
27
  const log = getLogger("device-id");
20
28
 
21
29
  let cached: string | undefined;
22
30
 
31
+ /**
32
+ * Resolve the base directory for device.json.
33
+ *
34
+ * In containerized environments, BASE_DATA_DIR points to a persistent volume
35
+ * and homedir() is ephemeral, so we must use BASE_DATA_DIR.
36
+ * In local environments (including multi-instance), homedir() is stable and
37
+ * shared across instances, giving a true per-machine device ID.
38
+ */
39
+ export function getDeviceIdBaseDir(): string {
40
+ if (getIsContainerized()) {
41
+ return getBaseDataDir() || homedir();
42
+ }
43
+ return homedir();
44
+ }
45
+
23
46
  /**
24
47
  * Get the stable device ID for this machine.
25
48
  *
26
49
  * Resolution order:
27
50
  * 1. Cached in-memory value (populated on first call)
28
- * 2. `deviceId` field from ~/.vellum/device.json
51
+ * 2. `deviceId` field from device.json
29
52
  * 3. Generate a new UUID, persist it to device.json, and return it
30
53
  *
31
54
  * On any read/write error the generated UUID is still cached so the
@@ -36,7 +59,7 @@ export function getDeviceId(): string {
36
59
  return cached;
37
60
  }
38
61
 
39
- const vellumDir = join(homedir(), ".vellum");
62
+ const vellumDir = join(getDeviceIdBaseDir(), ".vellum");
40
63
  const filePath = join(vellumDir, "device.json");
41
64
  const generated = randomUUID();
42
65
 
@@ -366,6 +366,24 @@ export function getWorkspaceDir(): string {
366
366
  return join(getRootDir(), "workspace");
367
367
  }
368
368
 
369
+ /**
370
+ * Returns a display-friendly workspace path for embedding in agent-facing text
371
+ * (skill bodies, tool descriptions). Replaces the home directory prefix with `~`
372
+ * so paths stay concise and portable across machines.
373
+ *
374
+ * Examples:
375
+ * /Users/sidd/.vellum/workspace → ~/.vellum/workspace
376
+ * /data/.vellum/workspace → /data/.vellum/workspace
377
+ */
378
+ export function getWorkspaceDirDisplay(): string {
379
+ const abs = getWorkspaceDir();
380
+ const home = homedir();
381
+ if (abs.startsWith(home + "/") || abs === home) {
382
+ return "~" + abs.slice(home.length);
383
+ }
384
+ return abs;
385
+ }
386
+
369
387
  /** Returns ~/.vellum/workspace/config.json */
370
388
  export function getWorkspaceConfigPath(): string {
371
389
  return join(getWorkspaceDir(), "config.json");
@@ -381,6 +399,11 @@ export function getWorkspaceHooksDir(): string {
381
399
  return join(getWorkspaceDir(), "hooks");
382
400
  }
383
401
 
402
+ /** Returns ~/.vellum/workspace/conversations */
403
+ export function getConversationsDir(): string {
404
+ return join(getWorkspaceDir(), "conversations");
405
+ }
406
+
384
407
  /** Returns the workspace path for a prompt file (e.g. IDENTITY.md, SOUL.md, USER.md). */
385
408
  export function getWorkspacePromptPath(file: string): string {
386
409
  return join(getWorkspaceDir(), file);
@@ -399,6 +422,7 @@ export function ensureDataDir(): void {
399
422
  join(root, "hooks"),
400
423
  join(workspace, "skills"),
401
424
  join(workspace, "embedding-models"),
425
+ join(workspace, "conversations"),
402
426
  // Data sub-dirs under workspace
403
427
  wsData,
404
428
  join(wsData, "db"),
@@ -25,6 +25,7 @@ const PROVIDER_PRICING: Record<string, Record<string, ModelPricing>> = {
25
25
  },
26
26
  openai: {
27
27
  "gpt-5.4": { inputPer1M: 2.5, outputPer1M: 15 },
28
+ "gpt-5.4-mini": { inputPer1M: 0.5, outputPer1M: 3 },
28
29
  "gpt-5.4-nano": { inputPer1M: 0.2, outputPer1M: 1.25 },
29
30
  "gpt-5.2": { inputPer1M: 1.75, outputPer1M: 14 },
30
31
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10 },
package/src/util/retry.ts CHANGED
@@ -108,9 +108,7 @@ export function isRetryableNetworkError(error: unknown): boolean {
108
108
 
109
109
  // Fall back to message-based detection for errors without errno codes
110
110
  // (e.g. Bun's "The socket connection was closed unexpectedly")
111
- if (
112
- RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))
113
- ) {
111
+ if (RETRYABLE_NETWORK_MESSAGE_PATTERNS.some((p) => p.test(error.message))) {
114
112
  return true;
115
113
  }
116
114
 
@@ -1,16 +1,15 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import { join } from "node:path";
4
3
 
5
- import { getBaseDataDir } from "../../config/env-registry.js";
4
+ import { getDeviceIdBaseDir } from "../../util/device-id.js";
6
5
  import type { WorkspaceMigration } from "./types.js";
7
6
 
8
7
  export const seedDeviceIdMigration: WorkspaceMigration = {
9
8
  id: "003-seed-device-id",
10
9
  description:
11
- "Seed ~/.vellum/device.json deviceId from the most recent lockfile installationId for continuity",
10
+ "Seed device.json deviceId from the most recent lockfile installationId for continuity",
12
11
  run(_workspaceDir: string): void {
13
- const base = getBaseDataDir() || homedir();
12
+ const base = getDeviceIdBaseDir();
14
13
  const vellumDir = join(base, ".vellum");
15
14
  const devicePath = join(vellumDir, "device.json");
16
15
 
@@ -83,6 +83,11 @@ export const servicesConfigMigration: WorkspaceMigration = {
83
83
  ...existingServices,
84
84
  };
85
85
 
86
+ // Legacy top-level fields (provider, model) are the user's actual
87
+ // configuration from before the services structure existed. If they're
88
+ // present as strings they take precedence over any `existingServices`
89
+ // values, which are just defaults written by backfillConfigDefaults().
90
+ // The spread preserves any extra keys that future backfills may add.
86
91
  services.inference = {
87
92
  ...(existingServices.inference ?? {}),
88
93
  mode: inferenceMode,
@@ -0,0 +1,12 @@
1
+ import type { WorkspaceMigration } from "./types.js";
2
+
3
+ export const voiceTimeoutAndMaxStepsMigration: WorkspaceMigration = {
4
+ id: "008-voice-timeout-and-max-steps",
5
+ description:
6
+ "Add elevenlabs.conversationTimeoutSeconds and maxStepsPerSession to config schema (defaults handle new installs; macOS client syncs existing UserDefaults values on startup)",
7
+ run(_workspaceDir: string): void {
8
+ // No-op — schema defaults handle new installs.
9
+ // Existing users: macOS client will sync UserDefaults values
10
+ // to config on next startup via settings sync endpoints.
11
+ },
12
+ };
@@ -0,0 +1,10 @@
1
+ import { rebuildConversationDiskViewFromDb } from "./rebuild-conversation-disk-view.js";
2
+ import type { WorkspaceMigration } from "./types.js";
3
+
4
+ export const backfillConversationDiskViewMigration: WorkspaceMigration = {
5
+ id: "009-backfill-conversation-disk-view",
6
+ description: "Rebuild conversation disk view for existing conversations",
7
+ run(_workspaceDir: string): void {
8
+ rebuildConversationDiskViewFromDb();
9
+ },
10
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Workspace migration 010: Rename UUID-based app directories and files to
3
+ * human-readable slugified names.
4
+ *
5
+ * Inline slugify + dedup logic (not imported from app-store) so the migration
6
+ * remains stable even if runtime code changes in the future.
7
+ *
8
+ * Idempotent: safe to re-run after interruption at any point. Handles
9
+ * partially-renamed states (crash between JSON write and file rename).
10
+ */
11
+
12
+ import { execSync } from "node:child_process";
13
+ import { randomUUID } from "node:crypto";
14
+ import {
15
+ existsSync,
16
+ mkdirSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ renameSync,
20
+ unlinkSync,
21
+ writeFileSync,
22
+ } from "node:fs";
23
+ import { join } from "node:path";
24
+
25
+ import type { WorkspaceMigration } from "./types.js";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Self-contained slug generation (do NOT import from app-store)
29
+ // ---------------------------------------------------------------------------
30
+
31
+ function slugify(name: string): string {
32
+ let slug = name
33
+ .toLowerCase()
34
+ .replace(/[^a-z0-9-]/g, "-")
35
+ .replace(/-{2,}/g, "-")
36
+ .replace(/^-+|-+$/g, "");
37
+
38
+ if (slug.length > 60) {
39
+ slug = slug.slice(0, 60).replace(/-+$/, "");
40
+ }
41
+
42
+ if (!slug) {
43
+ slug = `app-${randomUUID().slice(0, 8)}`;
44
+ }
45
+
46
+ return slug;
47
+ }
48
+
49
+ function generateUniqueDirName(name: string, usedNames: Set<string>): string {
50
+ const base = slugify(name);
51
+ if (!usedNames.has(base)) return base;
52
+ let counter = 2;
53
+ while (usedNames.has(`${base}-${counter}`)) {
54
+ counter++;
55
+ }
56
+ return `${base}-${counter}`;
57
+ }
58
+
59
+ /** Defense-in-depth: reject dirNames that could cause path traversal. */
60
+ function isValidDirName(dirName: string): boolean {
61
+ return (
62
+ !!dirName &&
63
+ !dirName.includes("/") &&
64
+ !dirName.includes("\\") &&
65
+ !dirName.includes("..") &&
66
+ dirName === dirName.trim()
67
+ );
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Migration
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export const appDirRenameMigration: WorkspaceMigration = {
75
+ id: "010-app-dir-rename",
76
+ description:
77
+ "Rename UUID-based app directories and files to human-readable slugified names",
78
+
79
+ run(workspaceDir: string): void {
80
+ const appsDir = join(workspaceDir, "data", "apps");
81
+ if (!existsSync(appsDir)) return;
82
+
83
+ // Read all JSON files (sorted for deterministic ordering)
84
+ const jsonFiles = readdirSync(appsDir)
85
+ .filter((f) => f.endsWith(".json"))
86
+ .sort();
87
+
88
+ if (jsonFiles.length === 0) return;
89
+
90
+ const usedNames = new Set<string>();
91
+
92
+ for (const jsonFile of jsonFiles) {
93
+ const jsonPath = join(appsDir, jsonFile);
94
+ let raw: string;
95
+ try {
96
+ raw = readFileSync(jsonPath, "utf-8");
97
+ } catch {
98
+ continue; // skip unreadable files
99
+ }
100
+
101
+ let parsed: {
102
+ id?: string;
103
+ name?: string;
104
+ dirName?: string;
105
+ };
106
+ try {
107
+ parsed = JSON.parse(raw);
108
+ } catch {
109
+ continue; // skip malformed JSON
110
+ }
111
+
112
+ const appId = parsed.id;
113
+ const appName = parsed.name ?? "untitled";
114
+ if (!appId) continue;
115
+
116
+ // Check if already migrated: has dirName AND filesystem matches
117
+ if (parsed.dirName && isValidDirName(parsed.dirName)) {
118
+ const expectedJsonFile = `${parsed.dirName}.json`;
119
+ if (
120
+ jsonFile === expectedJsonFile &&
121
+ existsSync(join(appsDir, parsed.dirName))
122
+ ) {
123
+ // Already fully migrated -- just track the name
124
+ usedNames.add(parsed.dirName);
125
+ continue;
126
+ }
127
+
128
+ // Partially renamed: JSON has dirName but files may still be at old paths.
129
+ // Use the dirName from JSON but rename from wherever the files actually are.
130
+ const dirName = parsed.dirName;
131
+ usedNames.add(dirName);
132
+ renameAppFiles(appsDir, jsonFile, appId, dirName, parsed, raw);
133
+ continue;
134
+ }
135
+
136
+ // No dirName yet -- generate one
137
+ const dirName = generateUniqueDirName(appName, usedNames);
138
+ if (!isValidDirName(dirName)) continue; // safety check
139
+ usedNames.add(dirName);
140
+ renameAppFiles(appsDir, jsonFile, appId, dirName, parsed, raw);
141
+ }
142
+
143
+ // Best-effort git commit
144
+ try {
145
+ const gitDir = join(appsDir, ".git");
146
+ if (existsSync(gitDir)) {
147
+ execSync(
148
+ "git add -A && git commit -m 'Migration 010: rename app dirs to slugified names' --allow-empty",
149
+ {
150
+ cwd: appsDir,
151
+ stdio: "ignore",
152
+ timeout: 10_000,
153
+ },
154
+ );
155
+ }
156
+ } catch {
157
+ // Git failure is non-fatal -- log nothing since we don't have
158
+ // the logger available in migrations. The next commitAppChange()
159
+ // call will pick up the renamed files naturally.
160
+ }
161
+ },
162
+ };
163
+
164
+ /**
165
+ * Rename app files from their current location to dirName-based paths.
166
+ * Each step checks existence to handle partial completion.
167
+ */
168
+ function renameAppFiles(
169
+ appsDir: string,
170
+ currentJsonFile: string,
171
+ appId: string,
172
+ dirName: string,
173
+ parsed: Record<string, unknown>,
174
+ _rawJson: string,
175
+ ): void {
176
+ const targetJsonFile = `${dirName}.json`;
177
+ const targetPreviewFile = `${dirName}.preview`;
178
+
179
+ // 1. Rename the app directory: {appId}/ -> {dirName}/
180
+ const oldDir = join(appsDir, appId);
181
+ const newDir = join(appsDir, dirName);
182
+ if (existsSync(oldDir) && !existsSync(newDir) && oldDir !== newDir) {
183
+ renameSync(oldDir, newDir);
184
+ } else if (!existsSync(newDir)) {
185
+ // Directory doesn't exist at either location -- create it
186
+ mkdirSync(newDir, { recursive: true });
187
+ }
188
+
189
+ // 2. Rename the preview file: {appId}.preview -> {dirName}.preview
190
+ const oldPreview = join(appsDir, `${appId}.preview`);
191
+ const newPreview = join(appsDir, targetPreviewFile);
192
+ if (
193
+ existsSync(oldPreview) &&
194
+ !existsSync(newPreview) &&
195
+ oldPreview !== newPreview
196
+ ) {
197
+ renameSync(oldPreview, newPreview);
198
+ }
199
+
200
+ // 3. Rename the JSON file: {currentFilename} -> {dirName}.json
201
+ // Also update the dirName field in the JSON content.
202
+ const currentJsonPath = join(appsDir, currentJsonFile);
203
+ const targetJsonPath = join(appsDir, targetJsonFile);
204
+
205
+ // Update the JSON with dirName field
206
+ const updatedParsed = { ...parsed, dirName };
207
+ const updatedJson = JSON.stringify(updatedParsed, null, 2);
208
+
209
+ if (currentJsonFile !== targetJsonFile) {
210
+ // Write to new location, then remove old
211
+ writeFileSync(targetJsonPath, updatedJson, "utf-8");
212
+ if (existsSync(currentJsonPath) && currentJsonPath !== targetJsonPath) {
213
+ try {
214
+ unlinkSync(currentJsonPath);
215
+ } catch {
216
+ // Old file cleanup is best-effort
217
+ }
218
+ }
219
+ } else {
220
+ // Just update the content in place
221
+ writeFileSync(targetJsonPath, updatedJson, "utf-8");
222
+ }
223
+ }