@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.
- package/AGENTS.md +9 -1
- package/ARCHITECTURE.md +48 -48
- package/Dockerfile +2 -0
- package/README.md +1 -1
- package/docs/architecture/integrations.md +6 -13
- package/docs/architecture/memory.md +7 -12
- package/docs/architecture/security.md +5 -5
- package/docs/credential-execution-service.md +9 -9
- package/docs/skills.md +1 -1
- package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
- package/openapi.yaml +7130 -0
- package/package.json +2 -1
- package/scripts/generate-openapi.ts +562 -0
- package/src/__tests__/acp-session.test.ts +239 -44
- package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
- package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
- package/src/__tests__/browser-skill-endstate.test.ts +1 -1
- package/src/__tests__/btw-routes.test.ts +8 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
- package/src/__tests__/channel-approvals.test.ts +7 -7
- package/src/__tests__/channel-readiness-service.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +10 -2
- package/src/__tests__/context-memory-e2e.test.ts +2 -6
- package/src/__tests__/conversation-skill-tools.test.ts +1 -3
- package/src/__tests__/conversation-title-service.test.ts +2 -15
- package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
- package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
- package/src/__tests__/credential-security-e2e.test.ts +4 -4
- package/src/__tests__/credential-security-invariants.test.ts +3 -3
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/heartbeat-service.test.ts +35 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
- package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
- package/src/__tests__/log-export-workspace.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-recall-log-store.test.ts +182 -0
- package/src/__tests__/memory-recall-quality.test.ts +6 -8
- package/src/__tests__/memory-regressions.test.ts +53 -42
- package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
- package/src/__tests__/messaging-skill-split.test.ts +2 -17
- package/src/__tests__/oauth-cli.test.ts +98 -551
- package/src/__tests__/platform-callback-registration.test.ts +119 -0
- package/src/__tests__/secret-ingress-channel.test.ts +261 -0
- package/src/__tests__/secret-ingress-cli.test.ts +201 -0
- package/src/__tests__/secret-ingress-http.test.ts +312 -0
- package/src/__tests__/secret-ingress.test.ts +283 -0
- package/src/__tests__/secret-onetime-send.test.ts +4 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
- package/src/__tests__/skill-feature-flags.test.ts +11 -19
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
- package/src/__tests__/skill-load-inline-command.test.ts +3 -3
- package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
- package/src/__tests__/skill-memory.test.ts +2 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
- package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
- package/src/__tests__/skills.test.ts +16 -2
- package/src/__tests__/slack-channel-config.test.ts +1 -1
- package/src/__tests__/slack-skill.test.ts +5 -69
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
- package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
- package/src/acp/client-handler.ts +113 -31
- package/src/acp/session-manager.ts +29 -27
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/cli/AGENTS.md +73 -0
- package/src/cli/commands/autonomy.ts +3 -5
- package/src/cli/commands/credential-execution.ts +1 -2
- package/src/cli/commands/credentials.ts +4 -4
- package/src/cli/commands/memory.ts +2 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
- package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
- package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
- package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
- package/src/cli/commands/oauth/apps.ts +29 -11
- package/src/cli/commands/oauth/connect.ts +373 -0
- package/src/cli/commands/oauth/connections.ts +14 -493
- package/src/cli/commands/oauth/disconnect.ts +333 -0
- package/src/cli/commands/oauth/index.ts +62 -10
- package/src/cli/commands/oauth/mode.ts +263 -0
- package/src/cli/commands/oauth/ping.ts +222 -0
- package/src/cli/commands/oauth/providers.ts +30 -3
- package/src/cli/commands/oauth/request.ts +576 -0
- package/src/cli/commands/oauth/shared.ts +132 -0
- package/src/cli/commands/oauth/status.ts +202 -0
- package/src/cli/commands/oauth/token.ts +159 -0
- package/src/cli/commands/platform.ts +20 -14
- package/src/cli.ts +82 -17
- package/src/config/assistant-feature-flags.ts +74 -11
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +13 -36
- package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/SKILL.md +2 -2
- package/src/config/bundled-skills/settings/SKILL.md +5 -3
- package/src/config/bundled-skills/settings/TOOLS.json +17 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
- package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
- package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
- package/src/config/bundled-skills/slack/SKILL.md +58 -44
- package/src/config/bundled-tool-registry.ts +2 -19
- package/src/config/env.ts +5 -1
- package/src/config/feature-flag-registry.json +57 -41
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/platform.ts +0 -8
- package/src/config/schemas/security.ts +9 -1
- package/src/config/schemas/services.ts +1 -1
- package/src/config/skill-state.ts +1 -3
- package/src/config/skills.ts +2 -4
- package/src/credential-execution/feature-gates.ts +9 -16
- package/src/credential-execution/process-manager.ts +12 -0
- package/src/daemon/config-watcher.ts +4 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
- package/src/daemon/conversation-agent-loop.ts +49 -2
- package/src/daemon/conversation-memory.ts +0 -1
- package/src/daemon/handlers/config-slack-channel.ts +43 -1
- package/src/daemon/handlers/conversations.ts +41 -33
- package/src/daemon/lifecycle.ts +28 -5
- package/src/daemon/message-types/acp.ts +0 -15
- package/src/daemon/message-types/memory.ts +0 -1
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +9 -0
- package/src/daemon/server.ts +19 -7
- package/src/email/feature-gate.ts +3 -3
- package/src/heartbeat/heartbeat-service.ts +48 -0
- package/src/inbound/platform-callback-registration.ts +61 -7
- package/src/mcp/mcp-oauth-provider.ts +3 -3
- package/src/memory/app-store.ts +3 -3
- package/src/memory/conversation-crud.ts +124 -0
- package/src/memory/conversation-title-service.ts +7 -17
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-local.ts +47 -2
- package/src/memory/indexer.ts +13 -10
- package/src/memory/items-extractor.ts +12 -4
- package/src/memory/job-utils.ts +5 -0
- package/src/memory/jobs-store.ts +10 -2
- package/src/memory/journal-memory.ts +6 -2
- package/src/memory/llm-request-log-store.ts +88 -21
- package/src/memory/memory-recall-log-store.ts +128 -0
- package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
- package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/validate-migration-state.ts +14 -1
- package/src/memory/retriever.test.ts +4 -5
- package/src/memory/schema/infrastructure.ts +31 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +54 -0
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +26 -5
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -2
- package/src/permissions/trust-client.ts +2 -2
- package/src/platform/client.ts +2 -2
- package/src/prompts/journal-context.ts +6 -1
- package/src/providers/anthropic/client.ts +143 -1
- package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
- package/src/runtime/auth/route-policy.ts +0 -1
- package/src/runtime/btw-sidechain.ts +7 -1
- package/src/runtime/channel-approvals.ts +2 -2
- package/src/runtime/channel-readiness-service.ts +30 -7
- package/src/runtime/http-router.ts +31 -0
- package/src/runtime/http-server.ts +21 -4
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/pending-interactions.ts +21 -3
- package/src/runtime/routes/acp-routes.ts +46 -28
- package/src/runtime/routes/app-management-routes.ts +123 -0
- package/src/runtime/routes/app-routes.ts +31 -0
- package/src/runtime/routes/approval-routes.ts +108 -3
- package/src/runtime/routes/attachment-routes.ts +45 -0
- package/src/runtime/routes/avatar-routes.ts +16 -0
- package/src/runtime/routes/brain-graph-routes.ts +18 -0
- package/src/runtime/routes/btw-routes.ts +20 -0
- package/src/runtime/routes/call-routes.ts +81 -0
- package/src/runtime/routes/channel-readiness-routes.ts +48 -7
- package/src/runtime/routes/channel-routes.ts +18 -0
- package/src/runtime/routes/channel-verification-routes.ts +49 -1
- package/src/runtime/routes/contact-routes.ts +77 -0
- package/src/runtime/routes/conversation-attention-routes.ts +37 -0
- package/src/runtime/routes/conversation-management-routes.ts +94 -0
- package/src/runtime/routes/conversation-query-routes.ts +78 -0
- package/src/runtime/routes/conversation-routes.ts +115 -38
- package/src/runtime/routes/conversation-starter-routes.ts +29 -0
- package/src/runtime/routes/debug-routes.ts +23 -0
- package/src/runtime/routes/diagnostics-routes.ts +30 -0
- package/src/runtime/routes/documents-routes.ts +42 -0
- package/src/runtime/routes/events-routes.ts +10 -0
- package/src/runtime/routes/global-search-routes.ts +35 -0
- package/src/runtime/routes/guardian-action-routes.ts +47 -2
- package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
- package/src/runtime/routes/heartbeat-routes.ts +278 -0
- package/src/runtime/routes/host-bash-routes.ts +16 -1
- package/src/runtime/routes/host-cu-routes.ts +23 -1
- package/src/runtime/routes/host-file-routes.ts +18 -1
- package/src/runtime/routes/identity-routes.ts +35 -0
- package/src/runtime/routes/inbound-message-handler.ts +46 -25
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
- package/src/runtime/routes/integrations/twilio.ts +32 -22
- package/src/runtime/routes/invite-routes.ts +83 -0
- package/src/runtime/routes/log-export-routes.ts +14 -0
- package/src/runtime/routes/memory-item-routes.ts +99 -1
- package/src/runtime/routes/migration-rollback-routes.ts +25 -0
- package/src/runtime/routes/migration-routes.ts +40 -0
- package/src/runtime/routes/notification-routes.ts +20 -0
- package/src/runtime/routes/oauth-apps.ts +11 -3
- package/src/runtime/routes/pairing-routes.ts +15 -0
- package/src/runtime/routes/recording-routes.ts +72 -0
- package/src/runtime/routes/schedule-routes.ts +77 -5
- package/src/runtime/routes/secret-routes.ts +63 -1
- package/src/runtime/routes/settings-routes.ts +91 -1
- package/src/runtime/routes/skills-routes.ts +98 -16
- package/src/runtime/routes/subagents-routes.ts +38 -3
- package/src/runtime/routes/surface-action-routes.ts +66 -24
- package/src/runtime/routes/surface-content-routes.ts +20 -0
- package/src/runtime/routes/telemetry-routes.ts +12 -0
- package/src/runtime/routes/trace-event-routes.ts +25 -0
- package/src/runtime/routes/trust-rules-routes.ts +46 -0
- package/src/runtime/routes/tts-routes.ts +15 -4
- package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
- package/src/runtime/routes/usage-routes.ts +59 -0
- package/src/runtime/routes/watch-routes.ts +28 -0
- package/src/runtime/routes/work-items-routes.ts +59 -0
- package/src/runtime/routes/workspace-commit-routes.ts +12 -0
- package/src/runtime/routes/workspace-routes.ts +102 -0
- package/src/schedule/scheduler.ts +7 -1
- package/src/security/AGENTS.md +7 -0
- package/src/security/credential-backend.ts +1 -1
- package/src/security/encrypted-store.ts +3 -3
- package/src/security/oauth2.ts +55 -0
- package/src/security/secret-ingress.ts +174 -0
- package/src/security/secret-patterns.ts +133 -0
- package/src/security/secret-scanner.ts +28 -117
- package/src/signals/confirm.ts +12 -8
- package/src/signals/user-message.ts +18 -3
- package/src/skills/skill-memory.ts +1 -2
- package/src/tasks/task-runner.ts +7 -1
- package/src/tools/credentials/broker.ts +1 -1
- package/src/tools/credentials/metadata-store.ts +1 -1
- package/src/tools/credentials/vault.ts +2 -3
- package/src/tools/memory/definitions.ts +1 -1
- package/src/tools/memory/handlers.test.ts +2 -4
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/tool-manifest.ts +1 -1
- package/src/util/log-redact.ts +9 -34
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
- package/src/workspace/migrations/AGENTS.md +11 -0
- package/src/workspace/migrations/runner.ts +16 -6
- package/src/workspace/migrations/types.ts +7 -0
- package/docs/architecture/keychain-broker.md +0 -69
- package/src/__tests__/keychain-broker-client.test.ts +0 -800
- package/src/cli/commands/oauth/platform.ts +0 -525
- package/src/config/bundled-skills/slack/TOOLS.json +0 -272
- package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
- package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
- 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.
|
|
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
|
+
});
|