@vellumai/assistant 0.5.7 → 0.5.8

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 (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
@@ -19,17 +19,16 @@ import {
19
19
  getProviderBehavior,
20
20
  resolveService,
21
21
  } from "../../../oauth/provider-behaviors.js";
22
- import { credentialKey } from "../../../security/credential-key.js";
23
- import {
24
- deleteSecureKeyAsync,
25
- getSecureKeyAsync,
26
- } from "../../../security/secure-keys.js";
27
22
  import { withValidToken } from "../../../security/token-manager.js";
28
23
  import {
29
24
  assertMetadataWritable,
30
25
  deleteCredentialMetadata,
31
26
  } from "../../../tools/credentials/metadata-store.js";
32
- import { isLinux, isMacOS } from "../../../util/platform.js";
27
+ import { openInBrowser } from "../../../util/browser.js";
28
+ import {
29
+ deleteSecureKeyViaDaemon,
30
+ getSecureKeyViaDaemon,
31
+ } from "../../lib/daemon-credential-client.js";
33
32
  import { getCliLogger } from "../../logger.js";
34
33
  import { shouldOutputJson, writeOutput } from "../../output.js";
35
34
 
@@ -538,8 +537,10 @@ Examples:
538
537
  "client_secret",
539
538
  ];
540
539
  for (const field of legacyFields) {
541
- const key = credentialKey(providerKey, field);
542
- const result = await deleteSecureKeyAsync(key);
540
+ const result = await deleteSecureKeyViaDaemon(
541
+ "credential",
542
+ `${providerKey}:${field}`,
543
+ );
543
544
  if (result === "deleted") cleanedUp = true;
544
545
 
545
546
  const metaDeleted = deleteCredentialMetadata(providerKey, field);
@@ -633,7 +634,7 @@ Examples:
633
634
 
634
635
  if (dbApp) {
635
636
  if (!clientId) clientId = dbApp.clientId;
636
- const storedSecret = await getSecureKeyAsync(
637
+ const storedSecret = await getSecureKeyViaDaemon(
637
638
  dbApp.clientSecretCredentialPath,
638
639
  );
639
640
  if (storedSecret) clientSecret = storedSecret;
@@ -685,26 +686,7 @@ Examples:
685
686
  clientId,
686
687
  clientSecret,
687
688
  isInteractive: !!opts.openBrowser,
688
- openUrl: opts.openBrowser
689
- ? (url) => {
690
- if (isMacOS()) {
691
- Bun.spawn(["open", url], {
692
- stdout: "ignore",
693
- stderr: "ignore",
694
- });
695
- } else if (isLinux()) {
696
- Bun.spawn(["xdg-open", url], {
697
- stdout: "ignore",
698
- stderr: "ignore",
699
- });
700
- } else {
701
- // Fallback: print URL for manual opening (stderr to keep --json stdout clean)
702
- process.stderr.write(
703
- `Open this URL to authorize:\n\n${url}\n`,
704
- );
705
- }
706
- }
707
- : undefined,
689
+ openUrl: opts.openBrowser ? openInBrowser : undefined,
708
690
  ...(opts.scopes ? { requestedScopes: opts.scopes } : {}),
709
691
  });
710
692
 
@@ -7,11 +7,27 @@ import {
7
7
  } from "../../../config/schemas/services.js";
8
8
  import { getProvider } from "../../../oauth/oauth-store.js";
9
9
  import { VellumPlatformClient } from "../../../platform/client.js";
10
+ import { openInBrowser } from "../../../util/browser.js";
10
11
  import { getCliLogger } from "../../logger.js";
11
12
  import { shouldOutputJson, writeOutput } from "../../output.js";
12
13
 
13
14
  const log = getCliLogger("cli");
14
15
 
16
+ // ---------------------------------------------------------------------------
17
+ // Shared types
18
+ // ---------------------------------------------------------------------------
19
+
20
+ interface PlatformConnectionEntry {
21
+ id: string;
22
+ account_label?: string;
23
+ scopes_granted?: string[];
24
+ status?: string;
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Shared helpers
29
+ // ---------------------------------------------------------------------------
30
+
15
31
  /**
16
32
  * Normalize a bare provider name (e.g. "google") into the canonical provider
17
33
  * key used internally (e.g. "integration:google").
@@ -22,17 +38,144 @@ function toProviderKey(provider: string): string {
22
38
  : `integration:${provider}`;
23
39
  }
24
40
 
41
+ /**
42
+ * Extract the bare provider slug (e.g. "google") from either a raw CLI
43
+ * argument or a canonical provider key (e.g. "integration:google").
44
+ * Platform API paths expect the bare slug, not the internal key.
45
+ */
46
+ function toBareProvider(provider: string): string {
47
+ return provider.startsWith("integration:")
48
+ ? provider.slice("integration:".length)
49
+ : provider;
50
+ }
51
+
52
+ /**
53
+ * Validate that a provider supports managed OAuth (has a managedServiceConfigKey).
54
+ * Does NOT check whether managed mode is currently enabled — use this for
55
+ * operations that should work regardless of the current mode (e.g. disconnect).
56
+ * Returns the managed config key on success, or writes an error and returns null.
57
+ */
58
+ function requireManagedCapableProvider(
59
+ provider: string,
60
+ cmd: Command,
61
+ ): string | null {
62
+ const providerKey = toProviderKey(provider);
63
+ const providerRow = getProvider(providerKey);
64
+
65
+ const managedKey = providerRow?.managedServiceConfigKey;
66
+ if (!managedKey || !(managedKey in ServicesSchema.shape)) {
67
+ writeOutput(cmd, {
68
+ ok: false,
69
+ error: `Provider "${provider}" does not support platform-managed OAuth`,
70
+ });
71
+ process.exitCode = 1;
72
+ return null;
73
+ }
74
+
75
+ return managedKey;
76
+ }
77
+
78
+ /**
79
+ * Validate that a provider supports managed OAuth AND that managed mode is
80
+ * currently enabled. Use this for operations that require active managed
81
+ * mode (e.g. connect). Returns the managed config key on success, or writes
82
+ * an error and returns null.
83
+ */
84
+ function requireManagedProvider(provider: string, cmd: Command): string | null {
85
+ const managedKey = requireManagedCapableProvider(provider, cmd);
86
+ if (!managedKey) return null;
87
+
88
+ const services: Services = getConfig().services;
89
+ const managedEnabled =
90
+ services[managedKey as keyof Services].mode === "managed";
91
+
92
+ if (!managedEnabled) {
93
+ writeOutput(cmd, {
94
+ ok: false,
95
+ error: `Provider "${provider}" supports managed OAuth but is set to "your-own" mode`,
96
+ });
97
+ process.exitCode = 1;
98
+ return null;
99
+ }
100
+
101
+ return managedKey;
102
+ }
103
+
104
+ /**
105
+ * Create an authenticated platform client, or write an error and return null.
106
+ */
107
+ async function requirePlatformClient(
108
+ cmd: Command,
109
+ ): Promise<VellumPlatformClient | null> {
110
+ const client = await VellumPlatformClient.create();
111
+ if (!client || !client.platformAssistantId) {
112
+ writeOutput(cmd, {
113
+ ok: false,
114
+ error:
115
+ "Platform prerequisites not met (not logged in or missing assistant ID)",
116
+ });
117
+ process.exitCode = 1;
118
+ return null;
119
+ }
120
+ return client;
121
+ }
122
+
123
+ /**
124
+ * Fetch active platform connections for a provider. Returns the parsed entries
125
+ * or writes an error and returns null.
126
+ */
127
+ async function fetchActiveConnections(
128
+ client: VellumPlatformClient,
129
+ provider: string,
130
+ cmd: Command,
131
+ ): Promise<PlatformConnectionEntry[] | null> {
132
+ const params = new URLSearchParams();
133
+ params.set("provider", toBareProvider(provider));
134
+ params.set("status", "ACTIVE");
135
+
136
+ const path = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/connections/?${params.toString()}`;
137
+ const response = await client.fetch(path);
138
+
139
+ if (!response.ok) {
140
+ writeOutput(cmd, {
141
+ ok: false,
142
+ error: `Platform returned HTTP ${response.status}`,
143
+ });
144
+ process.exitCode = 1;
145
+ return null;
146
+ }
147
+
148
+ const body = (await response.json()) as unknown;
149
+
150
+ // The platform returns either a flat array or a {results: [...]} wrapper.
151
+ return (
152
+ Array.isArray(body)
153
+ ? body
154
+ : ((body as Record<string, unknown>).results ?? [])
155
+ ) as PlatformConnectionEntry[];
156
+ }
157
+
158
+ // ---------------------------------------------------------------------------
159
+ // Command registration
160
+ // ---------------------------------------------------------------------------
161
+
25
162
  export function registerPlatformCommands(oauth: Command): void {
26
163
  const platform = oauth
27
164
  .command("platform")
28
165
  .description(
29
- "Query platform-managed OAuth provider status and connections",
166
+ "Manage platform-managed OAuth provider status and connections",
30
167
  );
31
168
 
32
- // ---------------------------------------------------------------------------
33
- // platform status <provider>
34
- // ---------------------------------------------------------------------------
169
+ registerStatusCommand(platform);
170
+ registerConnectCommand(platform);
171
+ registerDisconnectCommand(platform);
172
+ }
35
173
 
174
+ // ---------------------------------------------------------------------------
175
+ // platform status <provider>
176
+ // ---------------------------------------------------------------------------
177
+
178
+ function registerStatusCommand(platform: Command): void {
36
179
  platform
37
180
  .command("status <provider>")
38
181
  .description(
@@ -102,46 +245,15 @@ Examples:
102
245
  }
103
246
 
104
247
  // 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
- }
248
+ const client = await requirePlatformClient(cmd);
249
+ if (!client) return;
131
250
 
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
- }>;
251
+ const rawEntries = await fetchActiveConnections(
252
+ client,
253
+ provider,
254
+ cmd,
255
+ );
256
+ if (!rawEntries) return;
145
257
 
146
258
  const connections = rawEntries.map((c) => ({
147
259
  id: c.id,
@@ -177,3 +289,237 @@ Examples:
177
289
  },
178
290
  );
179
291
  }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // platform connect <provider>
295
+ // ---------------------------------------------------------------------------
296
+
297
+ function registerConnectCommand(platform: Command): void {
298
+ platform
299
+ .command("connect <provider>")
300
+ .description(
301
+ "Initiate a platform-managed OAuth flow for a provider and open the browser",
302
+ )
303
+ .option(
304
+ "--scopes <scopes...>",
305
+ "Exact OAuth scopes to request (must be a subset of the provider's allowed scopes)",
306
+ )
307
+ .addHelpText(
308
+ "after",
309
+ `
310
+ Arguments:
311
+ provider Provider name (e.g. google, slack, twitter)
312
+
313
+ Starts a platform-managed OAuth authorization flow by calling the platform's
314
+ /start/ endpoint, then opens the returned authorization URL in the user's
315
+ browser. The user completes consent in the browser; the platform handles the
316
+ callback, token exchange, and credential storage.
317
+
318
+ The provider must support managed OAuth and managed mode must be enabled in
319
+ the services config.
320
+
321
+ Scope behavior:
322
+ Without --scopes, the platform requests ALL of the provider's allowed scopes.
323
+ With --scopes, only the specified scopes are requested (no merging with
324
+ defaults). Each scope must be in the provider's allowed set or the platform
325
+ will reject it. Use full scope URLs where required (e.g. Google scopes use
326
+ https://www.googleapis.com/auth/... format).
327
+
328
+ Examples:
329
+ $ assistant oauth platform connect google
330
+ $ assistant oauth platform connect google --scopes https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events
331
+ $ assistant oauth platform connect google --json`,
332
+ )
333
+ .action(
334
+ async (provider: string, opts: { scopes?: string[] }, cmd: Command) => {
335
+ try {
336
+ if (!requireManagedProvider(provider, cmd)) return;
337
+
338
+ const client = await requirePlatformClient(cmd);
339
+ if (!client) return;
340
+
341
+ // Call the platform's OAuth start endpoint
342
+ const startPath = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/${encodeURIComponent(toBareProvider(provider))}/start/`;
343
+
344
+ const body: Record<string, unknown> = {};
345
+ if (opts.scopes && opts.scopes.length > 0) {
346
+ body.requested_scopes = opts.scopes;
347
+ }
348
+
349
+ const response = await client.fetch(startPath, {
350
+ method: "POST",
351
+ headers: { "Content-Type": "application/json" },
352
+ body: JSON.stringify(body),
353
+ });
354
+
355
+ if (!response.ok) {
356
+ const errorText = await response.text().catch(() => "");
357
+ writeOutput(cmd, {
358
+ ok: false,
359
+ error: `Platform returned HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`,
360
+ });
361
+ process.exitCode = 1;
362
+ return;
363
+ }
364
+
365
+ const result = (await response.json()) as {
366
+ connect_url?: string;
367
+ };
368
+
369
+ if (!result.connect_url) {
370
+ writeOutput(cmd, {
371
+ ok: false,
372
+ error:
373
+ "Platform did not return a connect URL — the OAuth flow could not be started",
374
+ });
375
+ process.exitCode = 1;
376
+ return;
377
+ }
378
+
379
+ openInBrowser(result.connect_url);
380
+
381
+ writeOutput(cmd, {
382
+ ok: true,
383
+ deferred: true,
384
+ provider,
385
+ connectUrl: result.connect_url,
386
+ });
387
+
388
+ if (!shouldOutputJson(cmd)) {
389
+ log.info(
390
+ `Opening browser to connect ${provider}. Complete the authorization in your browser.`,
391
+ );
392
+ }
393
+ } catch (err) {
394
+ const message = err instanceof Error ? err.message : String(err);
395
+ writeOutput(cmd, { ok: false, error: message });
396
+ process.exitCode = 1;
397
+ }
398
+ },
399
+ );
400
+ }
401
+
402
+ // ---------------------------------------------------------------------------
403
+ // platform disconnect <provider>
404
+ // ---------------------------------------------------------------------------
405
+
406
+ function registerDisconnectCommand(platform: Command): void {
407
+ platform
408
+ .command("disconnect <provider>")
409
+ .description(
410
+ "Disconnect a platform-managed OAuth connection for a provider",
411
+ )
412
+ .option(
413
+ "--connection-id <id>",
414
+ "Specific connection ID to disconnect (required when multiple active connections exist)",
415
+ )
416
+ .addHelpText(
417
+ "after",
418
+ `
419
+ Arguments:
420
+ provider Provider name (e.g. google, slack, twitter)
421
+
422
+ Disconnects a platform-managed OAuth connection by calling the platform's
423
+ /disconnect/ endpoint, which revokes the connection.
424
+
425
+ If the provider has multiple active connections, use --connection-id to specify
426
+ which one to disconnect. Without --connection-id, the command disconnects the
427
+ sole active connection or fails with a list of active connections if there are
428
+ multiple.
429
+
430
+ Examples:
431
+ $ assistant oauth platform disconnect google
432
+ $ assistant oauth platform disconnect google --connection-id conn_abc123
433
+ $ assistant oauth platform disconnect google --json`,
434
+ )
435
+ .action(
436
+ async (
437
+ provider: string,
438
+ opts: { connectionId?: string },
439
+ cmd: Command,
440
+ ) => {
441
+ try {
442
+ if (!requireManagedCapableProvider(provider, cmd)) return;
443
+
444
+ const client = await requirePlatformClient(cmd);
445
+ if (!client) return;
446
+
447
+ // Always fetch active connections for the provider so we can
448
+ // verify --connection-id belongs to it (prevents cross-provider
449
+ // disconnects from typos or stale IDs).
450
+ const entries = await fetchActiveConnections(client, provider, cmd);
451
+ if (!entries) return;
452
+
453
+ let connectionId = opts.connectionId;
454
+
455
+ if (connectionId) {
456
+ // Verify the supplied ID belongs to this provider
457
+ if (!entries.some((c) => c.id === connectionId)) {
458
+ writeOutput(cmd, {
459
+ ok: false,
460
+ error: `Connection "${connectionId}" is not an active ${provider} connection`,
461
+ });
462
+ process.exitCode = 1;
463
+ return;
464
+ }
465
+ } else {
466
+ if (entries.length === 0) {
467
+ writeOutput(cmd, {
468
+ ok: false,
469
+ error: `No active connections found for provider "${provider}"`,
470
+ });
471
+ process.exitCode = 1;
472
+ return;
473
+ }
474
+
475
+ if (entries.length > 1) {
476
+ const connectionList = entries.map((c) => ({
477
+ id: c.id,
478
+ accountLabel: c.account_label ?? null,
479
+ }));
480
+ writeOutput(cmd, {
481
+ ok: false,
482
+ error: `Multiple active connections for "${provider}". Use --connection-id to specify which one to disconnect.`,
483
+ connections: connectionList,
484
+ });
485
+ process.exitCode = 1;
486
+ return;
487
+ }
488
+
489
+ connectionId = entries[0].id;
490
+ }
491
+
492
+ // Disconnect the connection
493
+ const disconnectPath = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/connections/${encodeURIComponent(connectionId)}/disconnect/`;
494
+ const disconnectResponse = await client.fetch(disconnectPath, {
495
+ method: "POST",
496
+ headers: { "Content-Type": "application/json" },
497
+ });
498
+
499
+ if (!disconnectResponse.ok) {
500
+ const errorText = await disconnectResponse.text().catch(() => "");
501
+ writeOutput(cmd, {
502
+ ok: false,
503
+ error: `Platform returned HTTP ${disconnectResponse.status}${errorText ? `: ${errorText}` : ""}`,
504
+ });
505
+ process.exitCode = 1;
506
+ return;
507
+ }
508
+
509
+ writeOutput(cmd, {
510
+ ok: true,
511
+ provider,
512
+ connectionId,
513
+ });
514
+
515
+ if (!shouldOutputJson(cmd)) {
516
+ log.info(`Disconnected ${provider} connection ${connectionId}`);
517
+ }
518
+ } catch (err) {
519
+ const message = err instanceof Error ? err.message : String(err);
520
+ writeOutput(cmd, { ok: false, error: message });
521
+ process.exitCode = 1;
522
+ }
523
+ },
524
+ );
525
+ }