@vellumai/assistant 0.5.9 → 0.5.11

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 (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. package/src/security/keychain-broker-client.ts +0 -446
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,7 @@
14
14
  "daemon:restart:http": "RUNTIME_HTTP_PORT=7821 bun run src/index.ts daemon restart",
15
15
  "db:generate": "drizzle-kit generate",
16
16
  "db:push": "drizzle-kit push",
17
+ "generate:openapi": "bun run scripts/generate-openapi.ts",
17
18
  "format": "prettier --write .",
18
19
  "format:check": "prettier --check .",
19
20
  "lint": "eslint",
@@ -0,0 +1,562 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Generate a minimal OpenAPI 3.0 YAML specification from the assistant's
4
+ * HTTP route definitions.
5
+ *
6
+ * Pipeline:
7
+ * 1. Programmatically import and invoke all *RouteDefinitions() exports
8
+ * from src/runtime/routes/ — no regex, no source-text parsing.
9
+ * 2. Combine with inline routes (defined in buildRouteTable()) and
10
+ * pre-auth / non-v1 routes.
11
+ * 3. Convert to OpenAPI path items.
12
+ * 4. Write to openapi.yaml.
13
+ *
14
+ * Usage:
15
+ * cd assistant && bun run scripts/generate-openapi.ts
16
+ * cd assistant && bun run generate:openapi # via npm script
17
+ * cd assistant && bun run generate:openapi -- --check # CI: fail if stale
18
+ */
19
+
20
+ import { readFileSync } from "node:fs";
21
+ import { readdir, readFile, writeFile } from "node:fs/promises";
22
+ import { join, resolve } from "node:path";
23
+
24
+ import { stringify } from "yaml";
25
+ import { z } from "zod";
26
+
27
+ const ROOT = resolve(import.meta.dir, "..");
28
+ const ROUTES_DIR = join(ROOT, "src/runtime/routes");
29
+ const OUTPUT_PATH = join(ROOT, "openapi.yaml");
30
+ const PKG_PATH = join(ROOT, "package.json");
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Schemas
34
+ // ---------------------------------------------------------------------------
35
+
36
+ const RouteQueryParamSchema = z.object({
37
+ name: z.string(),
38
+ type: z.string().optional(),
39
+ required: z.boolean().optional(),
40
+ description: z.string().optional(),
41
+ schema: z.record(z.string(), z.unknown()).optional(),
42
+ });
43
+
44
+ /**
45
+ * Accepts either a Zod schema instance (has _zod property) or a plain
46
+ * JSON-Schema-style object for backward compatibility with inline routes.
47
+ */
48
+ const RouteBodySchemaSchema = z.any().refine(
49
+ (v) =>
50
+ v != null &&
51
+ typeof v === "object" &&
52
+ // Zod schema instance (Zod 4 uses _zod branded property)
53
+ ("_zod" in v ||
54
+ // Plain JSON Schema fallback
55
+ typeof (v as Record<string, unknown>).type === "string"),
56
+ { message: "Expected a Zod schema or a plain JSON Schema object" },
57
+ );
58
+
59
+ const RouteEntrySchema = z.object({
60
+ method: z.string(),
61
+ /** Endpoint path relative to /v1/ (e.g. "conversations/:id"). */
62
+ endpoint: z.string(),
63
+ /** Short summary for OpenAPI operation. */
64
+ summary: z.string().optional(),
65
+ /** Longer description for OpenAPI operation. */
66
+ description: z.string().optional(),
67
+ /** Grouping tags. */
68
+ tags: z.array(z.string()).optional(),
69
+ /** Query parameter definitions. */
70
+ queryParams: z.array(RouteQueryParamSchema).optional(),
71
+ /** JSON Schema for the request body. */
72
+ requestBody: RouteBodySchemaSchema.optional(),
73
+ /** JSON Schema for the 200 response body. */
74
+ responseBody: RouteBodySchemaSchema.optional(),
75
+ /** Source module filename, used for auto-deriving tags. */
76
+ sourceModule: z.string().optional(),
77
+ });
78
+
79
+ type RouteEntry = z.infer<typeof RouteEntrySchema>;
80
+
81
+ /** JSON Schema representation of a body (for the OpenAPI spec output). */
82
+ interface JSONSchemaObject {
83
+ type?: string;
84
+ properties?: Record<string, unknown>;
85
+ required?: string[];
86
+ description?: string;
87
+ additionalProperties?: boolean;
88
+ [key: string]: unknown;
89
+ }
90
+
91
+ /** Convert a Zod schema or plain JSON Schema object to a JSON Schema object. */
92
+ function toJSONSchemaObject(schema: unknown): JSONSchemaObject {
93
+ if (schema == null || typeof schema !== "object") return {};
94
+ // Zod schema: has _zod branded property
95
+ if ("_zod" in (schema as Record<string, unknown>)) {
96
+ const converted = z.toJSONSchema(schema as z.ZodType, {
97
+ unrepresentable: "any",
98
+ });
99
+ // z.toJSONSchema may add $schema — strip it for inline embedding
100
+ const { $schema: _, ...rest } = converted as Record<string, unknown>;
101
+ return rest as JSONSchemaObject;
102
+ }
103
+ // Plain JSON Schema object (backward compat for inline/pre-auth routes)
104
+ return schema as JSONSchemaObject;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Programmatic route extraction
109
+ // ---------------------------------------------------------------------------
110
+
111
+ /**
112
+ * Create a recursive proxy that stands in for any dependency object.
113
+ *
114
+ * Route definition functions capture deps in handler closures but never
115
+ * access them during array construction, so this stub is never actually
116
+ * invoked at runtime — it just needs to be truthy and not throw when
117
+ * properties are read or the value is called as a function.
118
+ */
119
+ function createDeepStub(): unknown {
120
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
+ const stub: any = new Proxy(function () {}, {
122
+ get(_target, prop) {
123
+ // Prevent the stub from being treated as a Promise (await-able).
124
+ if (prop === "then") return undefined;
125
+ // Prevent infinite iteration.
126
+ if (prop === Symbol.iterator) return undefined;
127
+ // String coercion.
128
+ if (prop === Symbol.toPrimitive) return () => "";
129
+ return createDeepStub();
130
+ },
131
+ apply() {
132
+ return createDeepStub();
133
+ },
134
+ });
135
+ return stub;
136
+ }
137
+
138
+ /**
139
+ * Dynamically import every route module under `src/runtime/routes/`,
140
+ * find all exported functions whose names end with `RouteDefinitions`,
141
+ * invoke each with a deep stub as its first argument, and collect the
142
+ * `{ endpoint, method }` pairs from the returned arrays.
143
+ *
144
+ * This replaces the previous regex + balanced-brace scanning approach
145
+ * and automatically picks up new route modules without manual updates.
146
+ */
147
+ async function collectRoutesFromModules(): Promise<RouteEntry[]> {
148
+ const routes: RouteEntry[] = [];
149
+
150
+ const files = (await readdir(ROUTES_DIR, { recursive: true })).filter(
151
+ (f) =>
152
+ typeof f === "string" &&
153
+ f.endsWith(".ts") &&
154
+ !f.endsWith(".test.ts") &&
155
+ !f.endsWith(".benchmark.test.ts") &&
156
+ !f.includes("node_modules"),
157
+ );
158
+
159
+ for (const file of files) {
160
+ const filePath = join(ROUTES_DIR, file);
161
+ let mod: Record<string, unknown>;
162
+ try {
163
+ mod = (await import(filePath)) as Record<string, unknown>;
164
+ } catch (err) {
165
+ console.warn(
166
+ `Warning: could not import ${file}: ${err instanceof Error ? err.message : err}`,
167
+ );
168
+ continue;
169
+ }
170
+
171
+ for (const [exportName, exportValue] of Object.entries(mod)) {
172
+ if (
173
+ !exportName.endsWith("RouteDefinitions") ||
174
+ typeof exportValue !== "function"
175
+ ) {
176
+ continue;
177
+ }
178
+
179
+ try {
180
+ const rawDefs = exportValue(createDeepStub());
181
+ if (!Array.isArray(rawDefs)) continue;
182
+ for (const raw of rawDefs) {
183
+ const result = RouteEntrySchema.safeParse({
184
+ ...(typeof raw === "object" && raw !== null ? raw : {}),
185
+ sourceModule: file,
186
+ });
187
+ if (result.success) {
188
+ routes.push(result.data);
189
+ }
190
+ }
191
+ } catch (err) {
192
+ console.warn(
193
+ `Warning: ${exportName}() in ${file} threw: ${err instanceof Error ? err.message : err}`,
194
+ );
195
+ }
196
+ }
197
+ }
198
+
199
+ return routes;
200
+ }
201
+
202
+ /**
203
+ * Routes defined inline in RuntimeHttpServer.buildRouteTable() that are
204
+ * not exported from any route module. These are kept here because they
205
+ * depend on cross-cutting concerns specific to the RuntimeHttpServer
206
+ * instance (see B2 in the improvement plan for the recommendation to
207
+ * extract these into modules).
208
+ *
209
+ * Whenever buildRouteTable() gains or loses an inline route, this list
210
+ * must be updated manually. Note: `--check` only compares the generated
211
+ * YAML against the committed YAML, so it will NOT catch a missing entry
212
+ * here if openapi.yaml is also stale. Plan items B2/C2 address this gap.
213
+ */
214
+ const INLINE_ROUTES: RouteEntry[] = [
215
+ { endpoint: "browser-relay/status", method: "GET" },
216
+ { endpoint: "browser-relay/command", method: "POST" },
217
+ { endpoint: "conversations", method: "GET" },
218
+ { endpoint: "conversations/seen", method: "POST" },
219
+ { endpoint: "conversations/unread", method: "POST" },
220
+ { endpoint: "conversations/:id", method: "GET" },
221
+ { endpoint: "interfaces/:path*", method: "GET" },
222
+ { endpoint: "internal/twilio/voice-webhook", method: "POST" },
223
+ { endpoint: "internal/twilio/status", method: "POST" },
224
+ { endpoint: "internal/twilio/connect-action", method: "POST" },
225
+ { endpoint: "internal/oauth/callback", method: "POST" },
226
+ ];
227
+
228
+ /**
229
+ * Pre-auth routes handled directly in routeRequest() before the router.
230
+ * These are a small, stable set that bypass JWT authentication and are
231
+ * not part of the declarative route table.
232
+ */
233
+ const PRE_AUTH_ROUTES: RouteEntry[] = [
234
+ { method: "GET", endpoint: "audio/:id" },
235
+ { method: "POST", endpoint: "guardian/init" },
236
+ { method: "POST", endpoint: "guardian/refresh" },
237
+ { method: "POST", endpoint: "pairing/request" },
238
+ { method: "GET", endpoint: "pairing/status" },
239
+ ];
240
+
241
+ /**
242
+ * Top-level routes outside the /v1/ namespace.
243
+ * These are added to the spec separately.
244
+ */
245
+ const NON_V1_ROUTES: Array<{ method: string; path: string }> = [
246
+ { method: "GET", path: "/healthz" },
247
+ { method: "GET", path: "/readyz" },
248
+ { method: "GET", path: "/pages/{id}" },
249
+ ];
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // OpenAPI helpers
253
+ // ---------------------------------------------------------------------------
254
+
255
+ /** Convert route endpoint `:param` / `:param*` syntax to OpenAPI `{param}`. */
256
+ function toOpenApiPath(endpoint: string): string {
257
+ return (
258
+ "/v1/" + endpoint.replace(/:(\w+)\*/g, "{$1}").replace(/:(\w+)/g, "{$1}")
259
+ );
260
+ }
261
+
262
+ /** Derive a unique operationId from the endpoint and HTTP method. */
263
+ function toOperationId(endpoint: string, method: string): string {
264
+ const slug = endpoint
265
+ .replace(/:(\w+)\*/g, "by_$1")
266
+ .replace(/:(\w+)/g, "by_$1")
267
+ .replace(/[/]/g, "_")
268
+ .replace(/[^a-zA-Z0-9_]/g, "");
269
+ return `${slug}_${method.toLowerCase()}`;
270
+ }
271
+
272
+ /** Extract path parameter names from an OpenAPI-style path. */
273
+ function extractPathParams(openApiPath: string): string[] {
274
+ const params: string[] = [];
275
+ const re = /\{(\w+)\}/g;
276
+ let m: RegExpExecArray | null;
277
+ while ((m = re.exec(openApiPath)) !== null) {
278
+ params.push(m[1]);
279
+ }
280
+ return params;
281
+ }
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Spec builder
285
+ // ---------------------------------------------------------------------------
286
+
287
+ interface OpenApiParameter {
288
+ name: string;
289
+ in: string;
290
+ required: boolean;
291
+ schema: { type: string };
292
+ description?: string;
293
+ }
294
+
295
+ interface OpenApiOperation {
296
+ operationId: string;
297
+ summary?: string;
298
+ description?: string;
299
+ tags?: string[];
300
+ parameters?: OpenApiParameter[];
301
+ requestBody?: {
302
+ required: boolean;
303
+ content: {
304
+ "application/json": {
305
+ schema: JSONSchemaObject;
306
+ };
307
+ };
308
+ };
309
+ responses: Record<
310
+ string,
311
+ {
312
+ description: string;
313
+ content?: {
314
+ "application/json": {
315
+ schema: JSONSchemaObject;
316
+ };
317
+ };
318
+ }
319
+ >;
320
+ }
321
+
322
+ interface OpenApiPathItem {
323
+ [method: string]: OpenApiOperation;
324
+ }
325
+
326
+ /** Derive a tag name from a route module filename (e.g. "secret-routes.ts" → "secrets"). */
327
+ function deriveTagFromModule(filename: string): string {
328
+ // Strip directory prefix and extension
329
+ const base = filename.replace(/^.*[\/]/, "").replace(/\.ts$/, "");
330
+ // Remove trailing "-routes" suffix
331
+ return base.replace(/-routes$/, "");
332
+ }
333
+
334
+ function buildSpec(
335
+ routes: RouteEntry[],
336
+ version: string,
337
+ ): Record<string, unknown> {
338
+ // Deduplicate by path+method
339
+ const seen = new Set<string>();
340
+ const uniqueRoutes: Array<{
341
+ path: string;
342
+ method: string;
343
+ endpoint: string;
344
+ entry: RouteEntry;
345
+ }> = [];
346
+
347
+ // Non-v1 routes first
348
+ for (const r of NON_V1_ROUTES) {
349
+ const key = `${r.method}:${r.path}`;
350
+ if (!seen.has(key)) {
351
+ seen.add(key);
352
+ uniqueRoutes.push({
353
+ path: r.path,
354
+ method: r.method,
355
+ endpoint: r.path,
356
+ entry: { method: r.method, endpoint: r.path },
357
+ });
358
+ }
359
+ }
360
+
361
+ // v1 routes
362
+ for (const r of routes) {
363
+ const openApiPath = toOpenApiPath(r.endpoint);
364
+ const key = `${r.method}:${openApiPath}`;
365
+ if (!seen.has(key)) {
366
+ seen.add(key);
367
+ uniqueRoutes.push({
368
+ path: openApiPath,
369
+ method: r.method,
370
+ endpoint: r.endpoint,
371
+ entry: r,
372
+ });
373
+ }
374
+ }
375
+
376
+ // Sort by path, then by method for deterministic output
377
+ uniqueRoutes.sort((a, b) => {
378
+ const pathCmp = a.path.localeCompare(b.path);
379
+ if (pathCmp !== 0) return pathCmp;
380
+ return a.method.localeCompare(b.method);
381
+ });
382
+
383
+ // Build paths object
384
+ const paths: Record<string, OpenApiPathItem> = {};
385
+ for (const route of uniqueRoutes) {
386
+ if (!paths[route.path]) {
387
+ paths[route.path] = {};
388
+ }
389
+
390
+ const methodLower = route.method.toLowerCase();
391
+ const operationId = route.path.startsWith("/v1/")
392
+ ? toOperationId(route.endpoint, route.method)
393
+ : route.path.replace(/^\//, "").replace(/[/{}\-]/g, "_") +
394
+ `_${methodLower}`;
395
+
396
+ const { entry } = route;
397
+
398
+ // Build parameters: path params + query params from metadata
399
+ const pathParams = extractPathParams(route.path);
400
+ const parameters: OpenApiParameter[] = pathParams.map((name) => ({
401
+ name,
402
+ in: "path" as const,
403
+ required: true,
404
+ schema: { type: "string" },
405
+ }));
406
+
407
+ if (entry.queryParams) {
408
+ for (const qp of entry.queryParams) {
409
+ parameters.push({
410
+ name: qp.name,
411
+ in: "query",
412
+ required: qp.required ?? false,
413
+ schema: qp.schema ?? { type: qp.type ?? "string" },
414
+ ...(qp.description ? { description: qp.description } : {}),
415
+ });
416
+ }
417
+ }
418
+
419
+ // Determine tags: explicit tags > auto-derived from source module
420
+ const tags: string[] | undefined =
421
+ entry.tags && entry.tags.length > 0
422
+ ? entry.tags
423
+ : entry.sourceModule
424
+ ? [deriveTagFromModule(entry.sourceModule)]
425
+ : undefined;
426
+
427
+ // Build the operation
428
+ const operation: OpenApiOperation = {
429
+ operationId,
430
+ ...(entry.summary ? { summary: entry.summary } : {}),
431
+ ...(entry.description ? { description: entry.description } : {}),
432
+ ...(tags ? { tags } : {}),
433
+ responses: {
434
+ "200": entry.responseBody
435
+ ? {
436
+ description: "Successful response",
437
+ content: {
438
+ "application/json": {
439
+ schema: toJSONSchemaObject(entry.responseBody),
440
+ },
441
+ },
442
+ }
443
+ : { description: "Successful response" },
444
+ },
445
+ };
446
+
447
+ if (parameters.length > 0) {
448
+ operation.parameters = parameters;
449
+ }
450
+
451
+ if (entry.requestBody) {
452
+ operation.requestBody = {
453
+ required: true,
454
+ content: {
455
+ "application/json": {
456
+ schema: toJSONSchemaObject(entry.requestBody),
457
+ },
458
+ },
459
+ };
460
+ }
461
+
462
+ paths[route.path][methodLower] = operation;
463
+ }
464
+
465
+ return {
466
+ openapi: "3.0.0",
467
+ info: {
468
+ title: "Vellum Assistant API",
469
+ version,
470
+ description:
471
+ "Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.",
472
+ },
473
+ servers: [
474
+ {
475
+ url: "http://127.0.0.1:7821",
476
+ description: "Local assistant (default port)",
477
+ },
478
+ ],
479
+ paths,
480
+ };
481
+ }
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // Main
485
+ // ---------------------------------------------------------------------------
486
+
487
+ async function main() {
488
+ const isCheck = process.argv.includes("--check");
489
+
490
+ // Read package version
491
+ const pkg = JSON.parse(readFileSync(PKG_PATH, "utf-8")) as {
492
+ version: string;
493
+ };
494
+ const version = pkg.version;
495
+
496
+ // Collect routes programmatically from route modules
497
+ const moduleRoutes = await collectRoutesFromModules();
498
+
499
+ // Combine all route sources
500
+ const allRoutes: RouteEntry[] = [
501
+ ...PRE_AUTH_ROUTES,
502
+ ...INLINE_ROUTES,
503
+ ...moduleRoutes,
504
+ ];
505
+
506
+ // Build the spec
507
+ const spec = buildSpec(allRoutes, version);
508
+ const rawYaml =
509
+ "# Auto-generated by scripts/generate-openapi.ts — DO NOT EDIT\n" +
510
+ "# Regenerate: cd assistant && bun run generate:openapi\n" +
511
+ stringify(spec, { lineWidth: 120 });
512
+
513
+ // Format with prettier so the output matches what the pre-commit hook produces.
514
+ const prettierProc = Bun.spawn(["bunx", "prettier", "--parser", "yaml"], {
515
+ stdin: new Blob([rawYaml]),
516
+ stdout: "pipe",
517
+ stderr: "pipe",
518
+ });
519
+ const [yamlOutput, prettierExitCode] = await Promise.all([
520
+ new Response(prettierProc.stdout).text(),
521
+ prettierProc.exited,
522
+ ]);
523
+ if (prettierExitCode !== 0) {
524
+ const stderr = await new Response(prettierProc.stderr).text();
525
+ console.error(`prettier exited with code ${prettierExitCode}: ${stderr}`);
526
+ process.exit(1);
527
+ }
528
+
529
+ if (isCheck) {
530
+ let existing: string;
531
+ try {
532
+ existing = await readFile(OUTPUT_PATH, "utf-8");
533
+ } catch {
534
+ console.error(
535
+ "openapi.yaml does not exist. Run: bun run generate:openapi",
536
+ );
537
+ process.exit(1);
538
+ }
539
+ if (existing !== yamlOutput) {
540
+ console.error("openapi.yaml is stale. Run: bun run generate:openapi");
541
+ process.exit(1);
542
+ }
543
+ console.log("openapi.yaml is up to date.");
544
+ return;
545
+ }
546
+
547
+ await writeFile(OUTPUT_PATH, yamlOutput);
548
+
549
+ // Count stats
550
+ const pathCount = Object.keys(spec.paths as Record<string, unknown>).length;
551
+ const operationCount = Object.values(
552
+ spec.paths as Record<string, Record<string, unknown>>,
553
+ ).reduce((n, methods) => n + Object.keys(methods).length, 0);
554
+
555
+ console.log(`Generated ${OUTPUT_PATH}`);
556
+ console.log(` ${pathCount} paths, ${operationCount} operations`);
557
+ }
558
+
559
+ main().catch((err) => {
560
+ console.error(err);
561
+ process.exit(1);
562
+ });