@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
package/src/calls/call-store.ts
CHANGED
|
@@ -41,6 +41,10 @@ const parseCallSession = createRowMapper<
|
|
|
41
41
|
inviteGuardianName: "inviteGuardianName",
|
|
42
42
|
callerIdentityMode: "callerIdentityMode",
|
|
43
43
|
callerIdentitySource: "callerIdentitySource",
|
|
44
|
+
skipDisclosure: {
|
|
45
|
+
from: "skipDisclosure",
|
|
46
|
+
transform: (v: unknown) => v === 1,
|
|
47
|
+
},
|
|
44
48
|
initiatedFromConversationId: "initiatedFromConversationId",
|
|
45
49
|
startedAt: "startedAt",
|
|
46
50
|
endedAt: "endedAt",
|
|
@@ -87,11 +91,13 @@ export function createCallSession(opts: {
|
|
|
87
91
|
inviteGuardianName?: string;
|
|
88
92
|
callerIdentityMode?: string;
|
|
89
93
|
callerIdentitySource?: string;
|
|
94
|
+
skipDisclosure?: boolean;
|
|
90
95
|
initiatedFromConversationId?: string;
|
|
91
96
|
}): CallSession {
|
|
92
97
|
const db = getDb();
|
|
93
98
|
const now = Date.now();
|
|
94
|
-
const
|
|
99
|
+
const skipDisclosure = opts.skipDisclosure ?? false;
|
|
100
|
+
const row = {
|
|
95
101
|
id: uuid(),
|
|
96
102
|
conversationId: opts.conversationId,
|
|
97
103
|
provider: opts.provider,
|
|
@@ -106,6 +112,7 @@ export function createCallSession(opts: {
|
|
|
106
112
|
inviteGuardianName: opts.inviteGuardianName ?? null,
|
|
107
113
|
callerIdentityMode: opts.callerIdentityMode ?? null,
|
|
108
114
|
callerIdentitySource: opts.callerIdentitySource ?? null,
|
|
115
|
+
skipDisclosure: skipDisclosure ? 1 : 0,
|
|
109
116
|
initiatedFromConversationId: opts.initiatedFromConversationId ?? null,
|
|
110
117
|
startedAt: null,
|
|
111
118
|
endedAt: null,
|
|
@@ -113,8 +120,8 @@ export function createCallSession(opts: {
|
|
|
113
120
|
createdAt: now,
|
|
114
121
|
updatedAt: now,
|
|
115
122
|
};
|
|
116
|
-
db.insert(callSessions).values(
|
|
117
|
-
return
|
|
123
|
+
db.insert(callSessions).values(row).run();
|
|
124
|
+
return { ...row, skipDisclosure };
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
export function getCallSession(id: string): CallSession | null {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { FishAudioConfig } from "../config/schemas/fish-audio.js";
|
|
2
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
3
|
+
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
4
|
+
import { getLogger } from "../util/logger.js";
|
|
5
|
+
|
|
6
|
+
const log = getLogger("fish-audio-client");
|
|
7
|
+
|
|
8
|
+
/** Timeout waiting for the first chunk from Fish Audio (ms). */
|
|
9
|
+
const FIRST_CHUNK_TIMEOUT_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
/** Timeout waiting between consecutive chunks (ms). */
|
|
12
|
+
const IDLE_TIMEOUT_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Fish Audio REST API (POST /v1/tts)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
interface SynthesizeOptions {
|
|
19
|
+
onChunk?: (chunk: Uint8Array) => void;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Synthesize text to audio using the Fish Audio REST API with the s2-pro
|
|
25
|
+
* model. Streams audio chunks via the optional `onChunk` callback as they
|
|
26
|
+
* arrive from the server's chunked transfer-encoded response. Returns the
|
|
27
|
+
* complete audio buffer when the response finishes.
|
|
28
|
+
*
|
|
29
|
+
* Pass an `AbortSignal` to cancel in-flight synthesis (e.g. on barge-in).
|
|
30
|
+
*/
|
|
31
|
+
export async function synthesizeWithFishAudio(
|
|
32
|
+
text: string,
|
|
33
|
+
config: FishAudioConfig,
|
|
34
|
+
options?: SynthesizeOptions,
|
|
35
|
+
): Promise<Buffer> {
|
|
36
|
+
const apiKey = await getSecureKeyAsync(
|
|
37
|
+
credentialKey("fish-audio", "api_key"),
|
|
38
|
+
);
|
|
39
|
+
if (!apiKey) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Fish Audio API key not configured. Store it via: assistant credentials set --service fish-audio --field api_key <key>",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const body = {
|
|
46
|
+
text,
|
|
47
|
+
reference_id: config.referenceId || undefined,
|
|
48
|
+
model: "s2-pro",
|
|
49
|
+
format: config.format,
|
|
50
|
+
mp3_bitrate: 192,
|
|
51
|
+
chunk_length: config.chunkLength,
|
|
52
|
+
normalize: true,
|
|
53
|
+
latency: config.latency,
|
|
54
|
+
temperature: 1.0,
|
|
55
|
+
prosody: config.speed !== 1.0 ? { speed: config.speed } : undefined,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
log.info(
|
|
59
|
+
{
|
|
60
|
+
referenceId: config.referenceId,
|
|
61
|
+
format: config.format,
|
|
62
|
+
textLength: text.length,
|
|
63
|
+
},
|
|
64
|
+
"Starting Fish Audio synthesis",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const response = await fetch("https://api.fish.audio/v1/tts", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: {
|
|
70
|
+
Authorization: `Bearer ${apiKey}`,
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
},
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
signal: options?.signal,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorText = await response.text();
|
|
79
|
+
throw new Error(`Fish Audio API error (${response.status}): ${errorText}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!response.body) {
|
|
83
|
+
throw new Error("Fish Audio API returned no body");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const chunks: Uint8Array[] = [];
|
|
87
|
+
const reader = response.body.getReader();
|
|
88
|
+
let isFirstChunk = true;
|
|
89
|
+
|
|
90
|
+
while (true) {
|
|
91
|
+
const timeoutMs = isFirstChunk ? FIRST_CHUNK_TIMEOUT_MS : IDLE_TIMEOUT_MS;
|
|
92
|
+
const timeout = new Promise<never>((_, reject) =>
|
|
93
|
+
setTimeout(
|
|
94
|
+
() => reject(new Error(`Fish Audio read timed out after ${timeoutMs}ms`)),
|
|
95
|
+
timeoutMs,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
const { done, value } = await Promise.race([reader.read(), timeout]);
|
|
99
|
+
if (done) break;
|
|
100
|
+
if (value) {
|
|
101
|
+
isFirstChunk = false;
|
|
102
|
+
chunks.push(value);
|
|
103
|
+
options?.onChunk?.(value);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0);
|
|
108
|
+
const merged = new Uint8Array(totalLength);
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
merged.set(chunk, offset);
|
|
112
|
+
offset += chunk.byteLength;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
log.debug({ bytes: totalLength }, "Fish Audio synthesis complete");
|
|
116
|
+
return Buffer.from(merged);
|
|
117
|
+
}
|
|
@@ -139,6 +139,12 @@ export interface RelayEndMessage {
|
|
|
139
139
|
handoffData?: string;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
export interface RelayPlayMessage {
|
|
143
|
+
type: "play";
|
|
144
|
+
source: string;
|
|
145
|
+
interruptible: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
// ── WebSocket data type ──────────────────────────────────────────────
|
|
143
149
|
|
|
144
150
|
export interface RelayWebSocketData {
|
|
@@ -323,6 +329,27 @@ export class RelayConnection {
|
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Send a play-audio URL to the caller. Used when the assistant handles
|
|
334
|
+
* TTS synthesis itself (e.g. Fish Audio) instead of relying on
|
|
335
|
+
* ConversationRelay's built-in TTS.
|
|
336
|
+
*/
|
|
337
|
+
sendPlayUrl(url: string): void {
|
|
338
|
+
const message: RelayPlayMessage = {
|
|
339
|
+
type: "play",
|
|
340
|
+
source: url,
|
|
341
|
+
interruptible: true,
|
|
342
|
+
};
|
|
343
|
+
try {
|
|
344
|
+
this.ws.send(JSON.stringify(message));
|
|
345
|
+
} catch (err) {
|
|
346
|
+
log.error(
|
|
347
|
+
{ err, callSessionId: this.callSessionId },
|
|
348
|
+
"Failed to send play URL",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
326
353
|
/**
|
|
327
354
|
* End the ConversationRelay session.
|
|
328
355
|
*/
|
|
@@ -49,6 +49,7 @@ export function generateTwiML(
|
|
|
49
49
|
profile: {
|
|
50
50
|
language: string;
|
|
51
51
|
transcriptionProvider: string;
|
|
52
|
+
speechModel?: string;
|
|
52
53
|
ttsProvider: string;
|
|
53
54
|
voice: string;
|
|
54
55
|
},
|
|
@@ -91,7 +92,7 @@ export function generateTwiML(
|
|
|
91
92
|
${greetingAttr}
|
|
92
93
|
voice="${escapeXml(profile.voice)}"
|
|
93
94
|
language="${escapeXml(profile.language)}"
|
|
94
|
-
transcriptionProvider="${escapeXml(profile.transcriptionProvider)}"
|
|
95
|
+
transcriptionProvider="${escapeXml(profile.transcriptionProvider)}"${profile.speechModel ? `\n speechModel="${escapeXml(profile.speechModel)}"` : ""}
|
|
95
96
|
ttsProvider="${escapeXml(profile.ttsProvider)}"
|
|
96
97
|
interruptible="true"
|
|
97
98
|
dtmfDetection="true"
|
package/src/calls/types.ts
CHANGED
|
@@ -75,6 +75,7 @@ export interface CallSession {
|
|
|
75
75
|
inviteGuardianName: string | null;
|
|
76
76
|
callerIdentityMode: string | null;
|
|
77
77
|
callerIdentitySource: string | null;
|
|
78
|
+
skipDisclosure: boolean;
|
|
78
79
|
initiatedFromConversationId?: string | null;
|
|
79
80
|
startedAt: number | null;
|
|
80
81
|
endedAt: number | null;
|
|
@@ -29,18 +29,6 @@ function fail(error: string): VoiceIngressPreflightFailure {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function buildGatewayUnhealthyMessage(
|
|
33
|
-
target: string,
|
|
34
|
-
error: string | undefined,
|
|
35
|
-
afterRecoveryAttempt: boolean,
|
|
36
|
-
): string {
|
|
37
|
-
const detail = error ?? "Unknown gateway health check failure";
|
|
38
|
-
if (afterRecoveryAttempt) {
|
|
39
|
-
return `Voice callback gateway is still unhealthy at ${target} after a local recovery attempt: ${detail}`;
|
|
40
|
-
}
|
|
41
|
-
return `Voice callback gateway is unhealthy at ${target}: ${detail}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
32
|
export async function preflightVoiceIngress(): Promise<VoiceIngressPreflightResult> {
|
|
45
33
|
const ingressConfig = loadConfig();
|
|
46
34
|
|
|
@@ -65,36 +53,6 @@ export async function preflightVoiceIngress(): Promise<VoiceIngressPreflightResu
|
|
|
65
53
|
);
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
const { ensureLocalGatewayReady, probeLocalGatewayHealth } =
|
|
69
|
-
await import("../runtime/local-gateway-health.js");
|
|
70
|
-
|
|
71
|
-
const initialHealth = await probeLocalGatewayHealth();
|
|
72
|
-
if (!initialHealth.healthy && !initialHealth.localDeployment) {
|
|
73
|
-
return fail(
|
|
74
|
-
buildGatewayUnhealthyMessage(
|
|
75
|
-
initialHealth.target,
|
|
76
|
-
initialHealth.error,
|
|
77
|
-
false,
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (initialHealth.localDeployment) {
|
|
83
|
-
const recovery = await ensureLocalGatewayReady();
|
|
84
|
-
// Re-probe after the wake flow so the dial path only continues when the
|
|
85
|
-
// current gateway process is demonstrably serving the callback stack.
|
|
86
|
-
const confirmedHealth = await probeLocalGatewayHealth();
|
|
87
|
-
if (!confirmedHealth.healthy) {
|
|
88
|
-
return fail(
|
|
89
|
-
buildGatewayUnhealthyMessage(
|
|
90
|
-
confirmedHealth.target,
|
|
91
|
-
confirmedHealth.error ?? recovery.error,
|
|
92
|
-
recovery.recoveryAttempted,
|
|
93
|
-
),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
56
|
return {
|
|
99
57
|
ok: true,
|
|
100
58
|
ingressConfig: {
|
|
@@ -3,6 +3,7 @@ import { loadConfig } from "../config/loader.js";
|
|
|
3
3
|
export interface VoiceQualityProfile {
|
|
4
4
|
language: string;
|
|
5
5
|
transcriptionProvider: string;
|
|
6
|
+
speechModel?: string;
|
|
6
7
|
ttsProvider: string;
|
|
7
8
|
voice: string;
|
|
8
9
|
}
|
|
@@ -42,19 +43,39 @@ export function buildElevenLabsVoiceSpec(config: {
|
|
|
42
43
|
/**
|
|
43
44
|
* Resolve the effective voice quality profile from config.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Supports ElevenLabs (default) and Fish Audio TTS providers.
|
|
47
|
+
* When Fish Audio is selected, `ttsProvider` is set to `"Google"` as a
|
|
48
|
+
* placeholder — ConversationRelay requires a valid provider in TwiML, but
|
|
49
|
+
* actual audio is delivered via `play` messages from the call-controller.
|
|
50
|
+
* The voice string is left empty since it is unused in that mode.
|
|
51
|
+
*
|
|
52
|
+
* For ElevenLabs, the voice ID comes from the shared `elevenlabs.voiceId`
|
|
53
|
+
* config (defaults to Amelia — ZF6FPAbjXT4488VcRRnw).
|
|
48
54
|
*/
|
|
49
55
|
export function resolveVoiceQualityProfile(
|
|
50
56
|
config?: ReturnType<typeof loadConfig>,
|
|
51
57
|
): VoiceQualityProfile {
|
|
52
58
|
const cfg = config ?? loadConfig();
|
|
53
59
|
const voice = cfg.calls.voice;
|
|
60
|
+
const configuredTts = voice.ttsProvider ?? "elevenlabs";
|
|
61
|
+
const fishAudio = configuredTts === "fish-audio";
|
|
54
62
|
return {
|
|
55
63
|
language: voice.language,
|
|
56
64
|
transcriptionProvider: voice.transcriptionProvider,
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
speechModel:
|
|
66
|
+
voice.speechModel ??
|
|
67
|
+
(voice.transcriptionProvider === "Google" ? undefined : "nova-3"),
|
|
68
|
+
ttsProvider: fishAudio ? "Google" : "ElevenLabs",
|
|
69
|
+
voice: fishAudio ? "" : buildElevenLabsVoiceSpec(cfg.elevenlabs),
|
|
59
70
|
};
|
|
60
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether Fish Audio TTS is configured for phone calls.
|
|
75
|
+
*/
|
|
76
|
+
export function isFishAudioTts(
|
|
77
|
+
config?: ReturnType<typeof loadConfig>,
|
|
78
|
+
): boolean {
|
|
79
|
+
const cfg = config ?? loadConfig();
|
|
80
|
+
return cfg.calls.voice?.ttsProvider === "fish-audio";
|
|
81
|
+
}
|
|
@@ -20,9 +20,7 @@ import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
|
20
20
|
import { buildAssistantEvent } from "../runtime/assistant-event.js";
|
|
21
21
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
22
22
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
23
|
-
import { checkIngressForSecrets } from "../security/secret-ingress.js";
|
|
24
23
|
import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
|
|
25
|
-
import { IngressBlockedError } from "../util/errors.js";
|
|
26
24
|
import { getLogger } from "../util/logger.js";
|
|
27
25
|
import {
|
|
28
26
|
CALL_OPENING_MARKER,
|
|
@@ -95,6 +93,8 @@ export interface VoiceTurnOptions {
|
|
|
95
93
|
isInbound: boolean;
|
|
96
94
|
/** The outbound call task, if any. */
|
|
97
95
|
task?: string | null;
|
|
96
|
+
/** When true, skip the disclosure announcement for this call. */
|
|
97
|
+
skipDisclosure?: boolean;
|
|
98
98
|
/** Called for each streaming text token from the agent loop. */
|
|
99
99
|
onTextDelta: (text: string) => void;
|
|
100
100
|
/** Called when the agent loop completes a full response. */
|
|
@@ -128,9 +128,11 @@ function buildVoiceCallControlPrompt(opts: {
|
|
|
128
128
|
isInbound: boolean;
|
|
129
129
|
task?: string | null;
|
|
130
130
|
isCallerGuardian?: boolean;
|
|
131
|
+
skipDisclosure?: boolean;
|
|
131
132
|
}): string {
|
|
132
133
|
const config = getConfig();
|
|
133
|
-
const disclosureEnabled =
|
|
134
|
+
const disclosureEnabled =
|
|
135
|
+
config.calls?.disclosure?.enabled === true && !opts.skipDisclosure;
|
|
134
136
|
const disclosureText = config.calls?.disclosure?.text?.trim();
|
|
135
137
|
const disclosureRule =
|
|
136
138
|
disclosureEnabled && disclosureText
|
|
@@ -240,15 +242,6 @@ export async function startVoiceTurn(
|
|
|
240
242
|
);
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
// Block inbound content that contains secrets
|
|
244
|
-
const ingressCheck = checkIngressForSecrets(opts.content);
|
|
245
|
-
if (ingressCheck.blocked) {
|
|
246
|
-
throw new IngressBlockedError(
|
|
247
|
-
ingressCheck.userNotice!,
|
|
248
|
-
ingressCheck.detectedTypes,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
245
|
const eventSink: VoiceRunEventSink = {
|
|
253
246
|
onTextDelta: opts.onTextDelta,
|
|
254
247
|
onMessageComplete: opts.onComplete,
|
|
@@ -286,6 +279,7 @@ export async function startVoiceTurn(
|
|
|
286
279
|
isInbound: opts.isInbound,
|
|
287
280
|
task: opts.task,
|
|
288
281
|
isCallerGuardian,
|
|
282
|
+
skipDisclosure: opts.skipDisclosure,
|
|
289
283
|
});
|
|
290
284
|
|
|
291
285
|
// Get or create the conversation
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
loadRawConfig,
|
|
7
7
|
saveRawConfig,
|
|
8
8
|
setNestedValue,
|
|
9
|
-
syncConfigToLockfile,
|
|
10
9
|
} from "../../config/loader.js";
|
|
11
10
|
import { AssistantConfigSchema } from "../../config/schema.js";
|
|
12
11
|
import { getSchemaAtPath } from "../../config/schema-utils.js";
|
|
@@ -73,8 +72,7 @@ Arguments:
|
|
|
73
72
|
true, "42" becomes number 42). Falls back to plain string if JSON
|
|
74
73
|
parsing fails.
|
|
75
74
|
|
|
76
|
-
After writing the value to config.json, the
|
|
77
|
-
to reflect the updated configuration.
|
|
75
|
+
After writing the value to config.json, the change takes effect immediately.
|
|
78
76
|
|
|
79
77
|
To manage API keys, use "assistant keys set <provider> <key>" instead.
|
|
80
78
|
|
|
@@ -93,7 +91,6 @@ Examples:
|
|
|
93
91
|
}
|
|
94
92
|
setNestedValue(raw, key, parsed);
|
|
95
93
|
saveRawConfig(raw);
|
|
96
|
-
syncConfigToLockfile();
|
|
97
94
|
log.info(`Set ${key} = ${JSON.stringify(parsed)}`);
|
|
98
95
|
});
|
|
99
96
|
|
|
@@ -15,6 +15,7 @@ import { credentialKey } from "../../security/credential-key.js";
|
|
|
15
15
|
import {
|
|
16
16
|
deleteSecureKeyAsync,
|
|
17
17
|
getSecureKeyAsync,
|
|
18
|
+
getSecureKeyResultAsync,
|
|
18
19
|
setSecureKeyAsync,
|
|
19
20
|
} from "../../security/secure-keys.js";
|
|
20
21
|
import {
|
|
@@ -608,10 +609,19 @@ Examples:
|
|
|
608
609
|
return;
|
|
609
610
|
}
|
|
610
611
|
|
|
611
|
-
const secret
|
|
612
|
+
const { value: secret, unreachable } =
|
|
613
|
+
await getSecureKeyResultAsync(storageKey);
|
|
612
614
|
|
|
613
615
|
if (!metadata && (secret == null || secret.length === 0)) {
|
|
614
|
-
|
|
616
|
+
if (unreachable) {
|
|
617
|
+
writeOutput(cmd, {
|
|
618
|
+
ok: false,
|
|
619
|
+
error:
|
|
620
|
+
"Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
|
|
621
|
+
});
|
|
622
|
+
} else {
|
|
623
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
624
|
+
}
|
|
615
625
|
process.exitCode = 1;
|
|
616
626
|
return;
|
|
617
627
|
}
|
|
@@ -646,10 +656,21 @@ Examples:
|
|
|
646
656
|
|
|
647
657
|
const connection = safeGetConnectionByProvider(metadata.service);
|
|
648
658
|
const output = buildCredentialOutput(metadata, secret, connection);
|
|
659
|
+
|
|
660
|
+
if (unreachable && (secret == null || secret.length === 0)) {
|
|
661
|
+
output.scrubbedValue = "(broker unreachable)";
|
|
662
|
+
output.brokerUnreachable = true;
|
|
663
|
+
}
|
|
664
|
+
|
|
649
665
|
writeOutput(cmd, output);
|
|
650
666
|
|
|
651
667
|
if (!shouldOutputJson(cmd)) {
|
|
652
668
|
printCredentialHuman(output);
|
|
669
|
+
if (unreachable && (secret == null || secret.length === 0)) {
|
|
670
|
+
log.info(
|
|
671
|
+
" \u26A0 Keychain broker unreachable — restart the Vellum app and accept the macOS Keychain prompt to access credentials",
|
|
672
|
+
);
|
|
673
|
+
}
|
|
653
674
|
}
|
|
654
675
|
} catch (err) {
|
|
655
676
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -725,10 +746,19 @@ Examples:
|
|
|
725
746
|
return;
|
|
726
747
|
}
|
|
727
748
|
|
|
728
|
-
const secret
|
|
749
|
+
const { value: secret, unreachable } =
|
|
750
|
+
await getSecureKeyResultAsync(storageKey);
|
|
729
751
|
|
|
730
752
|
if (secret == null || secret.length === 0) {
|
|
731
|
-
|
|
753
|
+
if (unreachable) {
|
|
754
|
+
writeOutput(cmd, {
|
|
755
|
+
ok: false,
|
|
756
|
+
error:
|
|
757
|
+
"Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
|
|
758
|
+
});
|
|
759
|
+
} else {
|
|
760
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
761
|
+
}
|
|
732
762
|
process.exitCode = 1;
|
|
733
763
|
return;
|
|
734
764
|
}
|
|
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|
|
2
2
|
|
|
3
3
|
import { registerAppCommands } from "./apps.js";
|
|
4
4
|
import { registerConnectionCommands } from "./connections.js";
|
|
5
|
+
import { registerPlatformCommands } from "./platform.js";
|
|
5
6
|
import { registerProviderCommands } from "./providers.js";
|
|
6
7
|
|
|
7
8
|
export function registerOAuthCommand(program: Command): void {
|
|
@@ -49,4 +50,10 @@ Examples:
|
|
|
49
50
|
// ---------------------------------------------------------------------------
|
|
50
51
|
|
|
51
52
|
registerConnectionCommands(oauth);
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// platform — subcommand group
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
registerPlatformCommands(oauth);
|
|
52
59
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
import { getConfig } from "../../../config/loader.js";
|
|
4
|
+
import {
|
|
5
|
+
type Services,
|
|
6
|
+
ServicesSchema,
|
|
7
|
+
} from "../../../config/schemas/services.js";
|
|
8
|
+
import { getProvider } from "../../../oauth/oauth-store.js";
|
|
9
|
+
import { VellumPlatformClient } from "../../../platform/client.js";
|
|
10
|
+
import { getCliLogger } from "../../logger.js";
|
|
11
|
+
import { shouldOutputJson, writeOutput } from "../../output.js";
|
|
12
|
+
|
|
13
|
+
const log = getCliLogger("cli");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a bare provider name (e.g. "google") into the canonical provider
|
|
17
|
+
* key used internally (e.g. "integration:google").
|
|
18
|
+
*/
|
|
19
|
+
function toProviderKey(provider: string): string {
|
|
20
|
+
return provider.startsWith("integration:")
|
|
21
|
+
? provider
|
|
22
|
+
: `integration:${provider}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerPlatformCommands(oauth: Command): void {
|
|
26
|
+
const platform = oauth
|
|
27
|
+
.command("platform")
|
|
28
|
+
.description(
|
|
29
|
+
"Query platform-managed OAuth provider status and connections",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// platform status <provider>
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
platform
|
|
37
|
+
.command("status <provider>")
|
|
38
|
+
.description(
|
|
39
|
+
"Check whether a provider supports managed OAuth and list the user's active connections",
|
|
40
|
+
)
|
|
41
|
+
.addHelpText(
|
|
42
|
+
"after",
|
|
43
|
+
`
|
|
44
|
+
Arguments:
|
|
45
|
+
provider Provider name (e.g. google, slack, twitter)
|
|
46
|
+
|
|
47
|
+
Checks whether the platform offers managed OAuth for the given provider,
|
|
48
|
+
whether managed mode is currently enabled, and lists any active connections
|
|
49
|
+
the user has set up on the platform.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
$ assistant oauth platform status google
|
|
53
|
+
$ assistant oauth platform status slack --json`,
|
|
54
|
+
)
|
|
55
|
+
.action(
|
|
56
|
+
async (
|
|
57
|
+
provider: string,
|
|
58
|
+
_opts: Record<string, unknown>,
|
|
59
|
+
cmd: Command,
|
|
60
|
+
) => {
|
|
61
|
+
try {
|
|
62
|
+
const providerKey = toProviderKey(provider);
|
|
63
|
+
const providerRow = getProvider(providerKey);
|
|
64
|
+
|
|
65
|
+
// 1. Check if the provider even supports managed mode
|
|
66
|
+
const managedKey = providerRow?.managedServiceConfigKey;
|
|
67
|
+
if (!managedKey || !(managedKey in ServicesSchema.shape)) {
|
|
68
|
+
writeOutput(cmd, {
|
|
69
|
+
ok: true,
|
|
70
|
+
provider,
|
|
71
|
+
managedAvailable: false,
|
|
72
|
+
managedEnabled: false,
|
|
73
|
+
connections: [],
|
|
74
|
+
});
|
|
75
|
+
if (!shouldOutputJson(cmd)) {
|
|
76
|
+
log.info(
|
|
77
|
+
`Provider "${provider}" does not support platform-managed OAuth`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Check if managed mode is enabled in the services config
|
|
84
|
+
const services: Services = getConfig().services;
|
|
85
|
+
const managedEnabled =
|
|
86
|
+
services[managedKey as keyof Services].mode === "managed";
|
|
87
|
+
|
|
88
|
+
if (!managedEnabled) {
|
|
89
|
+
writeOutput(cmd, {
|
|
90
|
+
ok: true,
|
|
91
|
+
provider,
|
|
92
|
+
managedAvailable: true,
|
|
93
|
+
managedEnabled: false,
|
|
94
|
+
connections: [],
|
|
95
|
+
});
|
|
96
|
+
if (!shouldOutputJson(cmd)) {
|
|
97
|
+
log.info(
|
|
98
|
+
`Provider "${provider}" supports managed OAuth but is set to "your-own" mode`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 3. Fetch active connections from the platform
|
|
105
|
+
const client = await VellumPlatformClient.create();
|
|
106
|
+
if (!client || !client.platformAssistantId) {
|
|
107
|
+
writeOutput(cmd, {
|
|
108
|
+
ok: false,
|
|
109
|
+
error:
|
|
110
|
+
"Platform prerequisites not met (not logged in or missing assistant ID)",
|
|
111
|
+
});
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const params = new URLSearchParams();
|
|
117
|
+
params.set("provider", provider);
|
|
118
|
+
params.set("status", "ACTIVE");
|
|
119
|
+
|
|
120
|
+
const path = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/connections/?${params.toString()}`;
|
|
121
|
+
const response = await client.fetch(path);
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
writeOutput(cmd, {
|
|
125
|
+
ok: false,
|
|
126
|
+
error: `Platform returned HTTP ${response.status}`,
|
|
127
|
+
});
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const body = (await response.json()) as unknown;
|
|
133
|
+
|
|
134
|
+
// The platform returns either a flat array or a {results: [...]} wrapper.
|
|
135
|
+
const rawEntries = (
|
|
136
|
+
Array.isArray(body)
|
|
137
|
+
? body
|
|
138
|
+
: ((body as Record<string, unknown>).results ?? [])
|
|
139
|
+
) as Array<{
|
|
140
|
+
id: string;
|
|
141
|
+
account_label?: string;
|
|
142
|
+
scopes_granted?: string[];
|
|
143
|
+
status?: string;
|
|
144
|
+
}>;
|
|
145
|
+
|
|
146
|
+
const connections = rawEntries.map((c) => ({
|
|
147
|
+
id: c.id,
|
|
148
|
+
accountLabel: c.account_label ?? null,
|
|
149
|
+
scopesGranted: c.scopes_granted ?? [],
|
|
150
|
+
status: c.status ?? "ACTIVE",
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
writeOutput(cmd, {
|
|
154
|
+
ok: true,
|
|
155
|
+
provider,
|
|
156
|
+
managedAvailable: true,
|
|
157
|
+
managedEnabled: true,
|
|
158
|
+
connections,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!shouldOutputJson(cmd)) {
|
|
162
|
+
if (connections.length === 0) {
|
|
163
|
+
log.info(
|
|
164
|
+
`Provider "${provider}" is managed but has no active connections`,
|
|
165
|
+
);
|
|
166
|
+
} else {
|
|
167
|
+
log.info(
|
|
168
|
+
`Provider "${provider}": ${connections.length} active connection(s)`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
174
|
+
writeOutput(cmd, { ok: false, error: message });
|
|
175
|
+
process.exitCode = 1;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
}
|