@vellumai/assistant 0.5.11 → 0.5.13

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 (209) hide show
  1. package/Dockerfile +42 -9
  2. package/docs/architecture/integrations.md +34 -32
  3. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  4. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  5. package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
  6. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
  7. package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
  8. package/openapi.yaml +87 -9
  9. package/package.json +1 -1
  10. package/src/__tests__/catalog-cache.test.ts +164 -0
  11. package/src/__tests__/catalog-search.test.ts +61 -0
  12. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  13. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  14. package/src/__tests__/conversation-error.test.ts +3 -2
  15. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  16. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  17. package/src/__tests__/credential-vault.test.ts +25 -33
  18. package/src/__tests__/credentials-cli.test.ts +3 -3
  19. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  20. package/src/__tests__/first-greeting.test.ts +7 -0
  21. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  22. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  23. package/src/__tests__/host-file-proxy.test.ts +89 -0
  24. package/src/__tests__/integration-status.test.ts +5 -5
  25. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  26. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  27. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  28. package/src/__tests__/navigate-settings-tab.test.ts +6 -2
  29. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  30. package/src/__tests__/oauth-cli.test.ts +126 -119
  31. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  32. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  33. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  34. package/src/__tests__/platform.test.ts +3 -168
  35. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  36. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  37. package/src/__tests__/skill-feature-flags.test.ts +8 -0
  38. package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  41. package/src/__tests__/slack-share-routes.test.ts +5 -5
  42. package/src/__tests__/system-prompt.test.ts +39 -0
  43. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
  44. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  45. package/src/cli/AGENTS.md +47 -7
  46. package/src/cli/commands/browser-relay.ts +2 -17
  47. package/src/cli/commands/contacts.ts +6 -4
  48. package/src/cli/commands/conversations.ts +13 -1
  49. package/src/cli/commands/credential-execution.ts +16 -1
  50. package/src/cli/commands/credentials.ts +2 -8
  51. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  52. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  53. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  54. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  55. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  56. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  57. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  58. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  59. package/src/cli/commands/oauth/apps.ts +63 -44
  60. package/src/cli/commands/oauth/connect.ts +187 -155
  61. package/src/cli/commands/oauth/disconnect.ts +27 -75
  62. package/src/cli/commands/oauth/index.ts +36 -46
  63. package/src/cli/commands/oauth/mode.ts +22 -34
  64. package/src/cli/commands/oauth/ping.ts +19 -45
  65. package/src/cli/commands/oauth/providers.ts +569 -62
  66. package/src/cli/commands/oauth/request.ts +36 -48
  67. package/src/cli/commands/oauth/shared.ts +1 -19
  68. package/src/cli/commands/oauth/status.ts +14 -25
  69. package/src/cli/commands/oauth/token.ts +25 -34
  70. package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
  71. package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
  72. package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
  73. package/src/cli/commands/platform/connect.ts +104 -0
  74. package/src/cli/commands/platform/disconnect.ts +118 -0
  75. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  76. package/src/cli/commands/sequence.ts +5 -4
  77. package/src/cli/commands/shotgun.ts +16 -0
  78. package/src/cli/commands/skills.ts +173 -41
  79. package/src/cli/commands/usage.ts +5 -11
  80. package/src/cli/lib/daemon-credential-client.ts +22 -38
  81. package/src/cli/program.ts +1 -1
  82. package/src/config/assistant-feature-flags.ts +3 -7
  83. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  84. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  85. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  86. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  87. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  88. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  89. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  90. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  91. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  92. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  93. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  94. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  95. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  96. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  97. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  98. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  99. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  100. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  101. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  102. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  103. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  104. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  105. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  106. package/src/config/bundled-skills/settings/TOOLS.json +5 -3
  107. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
  108. package/src/config/bundled-tool-registry.ts +5 -0
  109. package/src/config/feature-flag-registry.json +2 -2
  110. package/src/credential-execution/client.ts +15 -3
  111. package/src/daemon/conversation-agent-loop.ts +2 -0
  112. package/src/daemon/conversation-error.ts +36 -6
  113. package/src/daemon/conversation-messaging.ts +9 -0
  114. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  115. package/src/daemon/conversation-surfaces.ts +120 -14
  116. package/src/daemon/conversation.ts +5 -0
  117. package/src/daemon/first-greeting.ts +6 -1
  118. package/src/daemon/handlers/skills.ts +148 -3
  119. package/src/daemon/host-bash-proxy.ts +16 -0
  120. package/src/daemon/host-cu-proxy.ts +16 -0
  121. package/src/daemon/host-file-proxy.ts +16 -0
  122. package/src/daemon/lifecycle.ts +56 -5
  123. package/src/daemon/message-types/conversations.ts +1 -0
  124. package/src/daemon/message-types/guardian-actions.ts +2 -0
  125. package/src/daemon/message-types/host-bash.ts +6 -1
  126. package/src/daemon/message-types/host-cu.ts +6 -1
  127. package/src/daemon/message-types/host-file.ts +6 -1
  128. package/src/daemon/message-types/integrations.ts +0 -1
  129. package/src/daemon/server.ts +29 -2
  130. package/src/hooks/cli.ts +74 -0
  131. package/src/inbound/platform-callback-registration.ts +7 -12
  132. package/src/index.ts +0 -12
  133. package/src/mcp/client.ts +6 -1
  134. package/src/mcp/manager.ts +2 -1
  135. package/src/memory/conversation-crud.ts +92 -3
  136. package/src/memory/conversation-key-store.ts +26 -0
  137. package/src/memory/conversation-queries.ts +6 -6
  138. package/src/memory/db-init.ts +16 -0
  139. package/src/memory/journal-memory.ts +8 -2
  140. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  141. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  142. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  143. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  144. package/src/memory/migrations/index.ts +4 -0
  145. package/src/memory/migrations/registry.ts +8 -0
  146. package/src/memory/schema/oauth.ts +11 -0
  147. package/src/messaging/provider.ts +13 -12
  148. package/src/messaging/providers/gmail/adapter.ts +44 -35
  149. package/src/messaging/providers/slack/adapter.ts +63 -33
  150. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  151. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  152. package/src/notifications/adapters/telegram.ts +78 -2
  153. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  154. package/src/oauth/byo-connection.test.ts +22 -24
  155. package/src/oauth/connect-orchestrator.ts +37 -76
  156. package/src/oauth/connect-types.ts +7 -65
  157. package/src/oauth/connection-resolver.test.ts +13 -13
  158. package/src/oauth/connection-resolver.ts +3 -4
  159. package/src/oauth/identity-verifier.ts +177 -0
  160. package/src/oauth/oauth-store.ts +228 -3
  161. package/src/oauth/platform-connection.test.ts +56 -6
  162. package/src/oauth/platform-connection.ts +8 -1
  163. package/src/oauth/seed-providers.ts +247 -34
  164. package/src/permissions/checker.ts +127 -1
  165. package/src/prompts/journal-context.ts +4 -1
  166. package/src/prompts/system-prompt.ts +54 -9
  167. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  168. package/src/providers/anthropic/client.ts +2 -33
  169. package/src/runtime/guardian-action-service.ts +7 -2
  170. package/src/runtime/http-server.ts +12 -18
  171. package/src/runtime/http-types.ts +8 -1
  172. package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
  173. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  174. package/src/runtime/routes/conversation-routes.ts +79 -4
  175. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  176. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  177. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  178. package/src/runtime/routes/oauth-apps.ts +2 -1
  179. package/src/runtime/routes/secret-routes.ts +45 -15
  180. package/src/runtime/routes/settings-routes.ts +12 -19
  181. package/src/runtime/routes/skills-routes.ts +45 -4
  182. package/src/schedule/integration-status.ts +2 -2
  183. package/src/security/ces-rpc-credential-backend.ts +19 -16
  184. package/src/security/oauth-completion-page.ts +153 -0
  185. package/src/security/oauth2.ts +3 -17
  186. package/src/security/secure-keys.ts +207 -7
  187. package/src/security/token-manager.ts +3 -6
  188. package/src/signals/bash.ts +6 -1
  189. package/src/skills/catalog-cache.ts +44 -0
  190. package/src/skills/catalog-search.ts +18 -0
  191. package/src/tools/browser/browser-manager.ts +2 -2
  192. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  193. package/src/tools/credentials/vault.ts +34 -45
  194. package/src/tools/host-terminal/host-shell.ts +16 -3
  195. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  196. package/src/tools/skills/sandbox-runner.ts +16 -3
  197. package/src/tools/terminal/shell.ts +16 -3
  198. package/src/util/logger.ts +11 -1
  199. package/src/util/platform.ts +1 -91
  200. package/src/util/sentry-log-stream.ts +51 -0
  201. package/src/watcher/providers/github.ts +2 -2
  202. package/src/watcher/providers/gmail.ts +1 -1
  203. package/src/watcher/providers/google-calendar.ts +1 -1
  204. package/src/watcher/providers/linear.ts +2 -2
  205. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  206. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  207. package/src/workspace/migrations/registry.ts +2 -0
  208. package/src/cli/commands/oauth/connections.ts +0 -255
  209. package/src/oauth/provider-behaviors.ts +0 -634
@@ -1,3 +1,5 @@
1
+ import { createServer, type Server } from "node:http";
2
+
1
3
  import type { Command } from "commander";
2
4
 
3
5
  import { orchestrateOAuthConnect } from "../../../oauth/connect-orchestrator.js";
@@ -6,7 +8,7 @@ import {
6
8
  getMostRecentAppByProvider,
7
9
  getProvider,
8
10
  } from "../../../oauth/oauth-store.js";
9
- import { getProviderBehavior } from "../../../oauth/provider-behaviors.js";
11
+ import { renderOAuthCompletionPage } from "../../../security/oauth-completion-page.js";
10
12
  import { openInBrowser } from "../../../util/browser.js";
11
13
  import { getSecureKeyViaDaemon } from "../../lib/daemon-credential-client.js";
12
14
  import { getCliLogger } from "../../logger.js";
@@ -15,12 +17,57 @@ import {
15
17
  fetchActiveConnections,
16
18
  isManagedMode,
17
19
  requirePlatformClient,
18
- resolveService,
19
- toBareProvider,
20
20
  } from "./shared.js";
21
21
 
22
22
  const log = getCliLogger("cli");
23
23
 
24
+ /**
25
+ * Start a temporary loopback server to serve a nice completion page after the
26
+ * platform redirects the user's browser following a managed OAuth flow.
27
+ * Returns the base URL and a cleanup function.
28
+ */
29
+ function startManagedRedirectServer(provider: string): Promise<{
30
+ redirectUrl: string;
31
+ cleanup: () => void;
32
+ }> {
33
+ return new Promise((resolve, reject) => {
34
+ const server: Server = createServer((req, res) => {
35
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
36
+ const error = url.searchParams.get("error");
37
+ const errorDesc = url.searchParams.get("error_description");
38
+ const providerHint = url.searchParams.get("provider") ?? provider;
39
+
40
+ if (error) {
41
+ const message = errorDesc ?? error;
42
+ res.writeHead(200, { "Content-Type": "text/html" });
43
+ res.end(renderOAuthCompletionPage(message, false, providerHint));
44
+ } else {
45
+ res.writeHead(200, { "Content-Type": "text/html" });
46
+ res.end(
47
+ renderOAuthCompletionPage(
48
+ "You can close this tab and return to your assistant.",
49
+ true,
50
+ providerHint,
51
+ ),
52
+ );
53
+ }
54
+ });
55
+
56
+ server.listen(0, "localhost", () => {
57
+ const addr = server.address() as { port: number };
58
+ const redirectUrl = `http://localhost:${addr.port}/oauth/complete`;
59
+ resolve({
60
+ redirectUrl,
61
+ cleanup: () => server.close(),
62
+ });
63
+ });
64
+
65
+ server.on("error", (err) => {
66
+ reject(new Error(`Failed to start redirect server: ${err.message}`));
67
+ });
68
+ });
69
+ }
70
+
24
71
  // ---------------------------------------------------------------------------
25
72
  // Command registration
26
73
  // ---------------------------------------------------------------------------
@@ -29,7 +76,7 @@ export function registerConnectCommand(oauth: Command): void {
29
76
  oauth
30
77
  .command("connect <provider>")
31
78
  .description(
32
- "Initiate an OAuth authorization flow for a provider (auto-detects managed vs BYO mode)",
79
+ "Initiate an OAuth authorization flow for a specified provider",
33
80
  )
34
81
  .option("--scopes <scopes...>", "Scopes to request for the authorization")
35
82
  .option(
@@ -41,36 +88,18 @@ export function registerConnectCommand(oauth: Command): void {
41
88
  "after",
42
89
  `
43
90
  Arguments:
44
- provider Provider key, alias, or ID from 'assistant oauth providers list'.
45
- Accepts canonical keys (e.g. integration:google), aliases (e.g.
46
- gmail), or bare provider names (e.g. google).
47
-
48
- Options:
49
- --scopes <scopes...> Scopes to request for the authorization. In managed
50
- mode, each scope must be in the provider's allowed set
51
- (use full scope URLs where required). In BYO mode,
52
- scopes are appended to the provider's defaults.
53
- --open-browser Open the authorization URL in your browser and wait
54
- for completion. In managed mode, polls for a new
55
- platform connection. In BYO mode, starts a local
56
- callback server and blocks until the OAuth redirect.
57
- --client-id <id> BYO-only: select a specific OAuth app when multiple
58
- apps exist for the same provider. Ignored for
59
- platform-managed providers.
60
-
61
- Mode detection:
62
- The command checks the services config to determine whether the provider
63
- runs in platform-managed or BYO (bring-your-own credentials) mode.
64
-
65
- Managed mode: Calls the platform /start/ endpoint, returns a connect URL.
66
- With --open-browser, opens the URL and polls for a new connection.
67
- BYO mode: Resolves local client credentials from the OAuth app store and
68
- runs the OAuth2 authorization code flow via the local orchestrator.
91
+ provider Provider name (e.g. google, slack, notion).
92
+ Run 'assistant oauth providers list' to see available providers.
93
+
94
+ In managed mode, --scopes must be in the provider's allowed set (use full
95
+ scope URLs). In BYO mode, --scopes are appended to the provider's defaults.
96
+ The --open-browser flag polls for a platform connection (managed) or starts
97
+ a local callback server (BYO).
69
98
 
70
99
  Examples:
71
100
  $ assistant oauth connect google
72
- $ assistant oauth connect gmail --open-browser
73
- $ assistant oauth connect integration:google --scopes https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events
101
+ $ assistant oauth connect google --open-browser
102
+ $ assistant oauth connect google --scopes https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events
74
103
  $ assistant oauth connect google --client-id abc123 --open-browser`,
75
104
  )
76
105
  .action(
@@ -93,17 +122,12 @@ Examples:
93
122
 
94
123
  try {
95
124
  // ---------------------------------------------------------------
96
- // 1. Resolve provider key
97
- // ---------------------------------------------------------------
98
- const providerKey = resolveService(provider);
99
-
100
- // ---------------------------------------------------------------
101
- // 2. Validate provider exists
125
+ // 1. Validate provider exists
102
126
  // ---------------------------------------------------------------
103
- const providerRow = getProvider(providerKey);
127
+ const providerRow = getProvider(provider);
104
128
  if (!providerRow) {
105
129
  writeError(
106
- `Unknown provider "${providerKey}". ` +
130
+ `Unknown provider "${provider}". ` +
107
131
  `Run 'assistant oauth providers list' to see available providers.`,
108
132
  );
109
133
  return;
@@ -112,7 +136,7 @@ Examples:
112
136
  // ---------------------------------------------------------------
113
137
  // 3. Detect mode
114
138
  // ---------------------------------------------------------------
115
- const managed = isManagedMode(providerKey);
139
+ const managed = isManagedMode(provider);
116
140
 
117
141
  if (managed) {
118
142
  // =============================================================
@@ -122,7 +146,7 @@ Examples:
122
146
  // Warn about --client-id being ignored in managed mode
123
147
  if (opts.clientId) {
124
148
  log.info(
125
- `Warning: --client-id is ignored for platform-managed providers. The platform manages OAuth apps for "${providerKey}".`,
149
+ `Warning: --client-id is ignored for platform-managed providers. The platform manages OAuth apps for "${provider}".`,
126
150
  );
127
151
  }
128
152
 
@@ -130,148 +154,163 @@ Examples:
130
154
  if (!client) return;
131
155
 
132
156
  // Call the platform's OAuth start endpoint
133
- const startPath = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/${encodeURIComponent(toBareProvider(providerKey))}/start/`;
157
+ const startPath = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/${encodeURIComponent(provider)}/start/`;
134
158
 
135
159
  const body: Record<string, unknown> = {};
136
160
  if (opts.scopes && opts.scopes.length > 0) {
137
161
  body.requested_scopes = opts.scopes;
138
162
  }
139
163
 
140
- const response = await client.fetch(startPath, {
141
- method: "POST",
142
- headers: { "Content-Type": "application/json" },
143
- body: JSON.stringify(body),
144
- });
145
-
146
- if (!response.ok) {
147
- const errorText = await response.text().catch(() => "");
148
- writeError(
149
- `Platform returned HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`,
150
- );
151
- return;
164
+ // When opening the browser, start a local server to show a nice
165
+ // completion page instead of redirecting to the platform website.
166
+ let redirectServer:
167
+ | { redirectUrl: string; cleanup: () => void }
168
+ | undefined;
169
+ if (opts.openBrowser) {
170
+ try {
171
+ redirectServer = await startManagedRedirectServer(provider);
172
+ body.redirect_after_connect = redirectServer.redirectUrl;
173
+ } catch {
174
+ // Non-fatal — fall back to platform default redirect
175
+ }
152
176
  }
153
177
 
154
- const result = (await response.json()) as {
155
- connect_url?: string;
156
- };
157
-
158
- if (!result.connect_url) {
159
- writeError(
160
- "Platform did not return a connect URL — the OAuth flow could not be started",
161
- );
162
- return;
163
- }
178
+ try {
179
+ const response = await client.fetch(startPath, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify(body),
183
+ });
164
184
 
165
- if (opts.openBrowser) {
166
- // Snapshot existing connection IDs before opening browser
167
- const snapshotEntries = await fetchActiveConnections(
168
- client,
169
- providerKey,
170
- cmd,
171
- );
172
- if (!snapshotEntries) {
173
- // fetchActiveConnections already wrote the error output
185
+ if (!response.ok) {
186
+ const errorText = await response.text().catch(() => "");
187
+ writeError(
188
+ `Platform returned HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`,
189
+ );
174
190
  return;
175
191
  }
176
- const snapshotIds = new Set(snapshotEntries.map((e) => e.id));
177
192
 
178
- openInBrowser(result.connect_url);
193
+ const result = (await response.json()) as {
194
+ connect_url?: string;
195
+ };
179
196
 
180
- if (!jsonMode) {
181
- log.info(
182
- `Opening browser to connect ${providerKey}. Waiting for authorization...`,
197
+ if (!result.connect_url) {
198
+ writeError(
199
+ "Platform did not return a connect URL the OAuth flow could not be started",
183
200
  );
201
+ return;
184
202
  }
185
203
 
186
- // Poll for a new connection every 2s for up to 5 minutes
187
- const pollIntervalMs = 2000;
188
- const timeoutMs = 5 * 60 * 1000;
189
- const deadline = Date.now() + timeoutMs;
190
- let newConnection: {
191
- id: string;
192
- account_label?: string;
193
- scopes_granted?: string[];
194
- } | null = null;
195
-
196
- while (Date.now() < deadline) {
197
- await new Promise((r) => setTimeout(r, pollIntervalMs));
198
-
199
- const currentEntries = await fetchActiveConnections(
204
+ if (opts.openBrowser) {
205
+ // Snapshot existing connection IDs before opening browser
206
+ const snapshotEntries = await fetchActiveConnections(
200
207
  client,
201
- providerKey,
208
+ provider,
202
209
  cmd,
203
- { silent: true },
204
210
  );
205
- if (!currentEntries) continue;
211
+ if (!snapshotEntries) {
212
+ // fetchActiveConnections already wrote the error output
213
+ return;
214
+ }
215
+ const snapshotIds = new Set(snapshotEntries.map((e) => e.id));
206
216
 
207
- const found = currentEntries.find(
208
- (e) => !snapshotIds.has(e.id),
209
- );
210
- if (found) {
211
- newConnection = found;
212
- break;
217
+ openInBrowser(result.connect_url);
218
+
219
+ if (!jsonMode) {
220
+ log.info(
221
+ `Opening browser to connect ${provider}. Waiting for authorization...`,
222
+ );
213
223
  }
214
- }
215
224
 
216
- if (newConnection) {
217
- // Success new connection found
218
- if (jsonMode) {
219
- writeOutput(cmd, {
220
- ok: true,
221
- provider: providerKey,
222
- connectionId: newConnection.id,
223
- accountLabel: newConnection.account_label ?? null,
224
- scopesGranted: newConnection.scopes_granted ?? [],
225
- });
225
+ // Poll for a new connection every 2s for up to 5 minutes
226
+ const pollIntervalMs = 2000;
227
+ const timeoutMs = 5 * 60 * 1000;
228
+ const deadline = Date.now() + timeoutMs;
229
+ let newConnection: {
230
+ id: string;
231
+ account_label?: string;
232
+ scopes_granted?: string[];
233
+ } | null = null;
234
+
235
+ while (Date.now() < deadline) {
236
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
237
+
238
+ const currentEntries = await fetchActiveConnections(
239
+ client,
240
+ provider,
241
+ cmd,
242
+ { silent: true },
243
+ );
244
+ if (!currentEntries) continue;
245
+
246
+ const found = currentEntries.find(
247
+ (e) => !snapshotIds.has(e.id),
248
+ );
249
+ if (found) {
250
+ newConnection = found;
251
+ break;
252
+ }
253
+ }
254
+
255
+ if (newConnection) {
256
+ // Success — new connection found
257
+ if (jsonMode) {
258
+ writeOutput(cmd, {
259
+ ok: true,
260
+ provider: provider,
261
+ connectionId: newConnection.id,
262
+ accountLabel: newConnection.account_label ?? null,
263
+ scopesGranted: newConnection.scopes_granted ?? [],
264
+ });
265
+ } else {
266
+ const label = newConnection.account_label
267
+ ? ` as ${newConnection.account_label}`
268
+ : "";
269
+ process.stdout.write(`Connected to ${provider}${label}\n`);
270
+ }
226
271
  } else {
227
- const label = newConnection.account_label
228
- ? ` as ${newConnection.account_label}`
229
- : "";
230
- process.stdout.write(`Connected to ${providerKey}${label}\n`);
272
+ // Timeout authorization may still be in progress
273
+ if (jsonMode) {
274
+ writeOutput(cmd, {
275
+ ok: true,
276
+ deferred: true,
277
+ provider: provider,
278
+ connectUrl: result.connect_url,
279
+ message:
280
+ "Authorization may still be in progress. Check with 'assistant oauth status <provider>'.",
281
+ });
282
+ } else {
283
+ process.stdout.write(
284
+ `Timed out waiting for authorization. It may still be in progress.\n` +
285
+ `Check with: assistant oauth status ${provider}\n`,
286
+ );
287
+ }
231
288
  }
232
289
  } else {
233
- // Timeout authorization may still be in progress
290
+ // No --open-browser: output the connect URL
234
291
  if (jsonMode) {
235
292
  writeOutput(cmd, {
236
293
  ok: true,
237
294
  deferred: true,
238
- provider: providerKey,
239
295
  connectUrl: result.connect_url,
240
- message:
241
- "Authorization may still be in progress. Check with 'assistant oauth status <provider>'.",
296
+ provider: provider,
242
297
  });
243
298
  } else {
244
- process.stdout.write(
245
- `Timed out waiting for authorization. It may still be in progress.\n` +
246
- `Check with: assistant oauth status ${providerKey}\n`,
247
- );
299
+ process.stdout.write(result.connect_url + "\n");
248
300
  }
249
301
  }
250
- } else {
251
- // No --open-browser: output the connect URL
252
- if (jsonMode) {
253
- writeOutput(cmd, {
254
- ok: true,
255
- deferred: true,
256
- connectUrl: result.connect_url,
257
- provider: providerKey,
258
- });
259
- } else {
260
- process.stdout.write(result.connect_url + "\n");
261
- }
302
+ } finally {
303
+ redirectServer?.cleanup();
262
304
  }
263
305
  } else {
264
306
  // =============================================================
265
307
  // BYO PATH
266
308
  // =============================================================
267
309
 
268
- // a. Resolve service alias (already done above via resolveService)
269
- const resolvedServiceKey = providerKey;
270
-
271
- // b. Resolve client credentials from the DB
310
+ // a. Resolve client credentials from the DB
272
311
  const dbApp = opts.clientId
273
- ? getAppByProviderAndClientId(resolvedServiceKey, opts.clientId)
274
- : getMostRecentAppByProvider(resolvedServiceKey);
312
+ ? getAppByProviderAndClientId(provider, opts.clientId)
313
+ : getMostRecentAppByProvider(provider);
275
314
 
276
315
  let clientId = opts.clientId;
277
316
  let clientSecret: string | undefined;
@@ -285,7 +324,7 @@ Examples:
285
324
  } else if (opts.clientId) {
286
325
  // --client-id was explicitly provided but no matching app exists
287
326
  writeError(
288
- `No registered app found for "${resolvedServiceKey}" with client ID "${opts.clientId}". ` +
327
+ `No registered app found for "${provider}" with client ID "${opts.clientId}". ` +
289
328
  `Register one with 'assistant oauth apps upsert'.`,
290
329
  );
291
330
  return;
@@ -294,7 +333,7 @@ Examples:
294
333
  // c. Validate client_id exists
295
334
  if (!clientId) {
296
335
  writeError(
297
- `No client_id found for "${resolvedServiceKey}". ` +
336
+ `No client_id found for "${provider}". ` +
298
337
  `Register one with 'assistant oauth apps upsert'.`,
299
338
  );
300
339
  return;
@@ -302,18 +341,11 @@ Examples:
302
341
 
303
342
  // d. Check if client_secret is required but missing
304
343
  if (clientSecret === undefined) {
305
- const behavior = getProviderBehavior(resolvedServiceKey);
306
-
307
- const requiresSecret =
308
- behavior?.setup?.requiresClientSecret ??
309
- !!(
310
- providerRow?.tokenEndpointAuthMethod ||
311
- providerRow?.extraParams
312
- );
344
+ const requiresSecret = !!providerRow?.requiresClientSecret;
313
345
 
314
346
  if (requiresSecret) {
315
347
  writeError(
316
- `client_secret is required for ${resolvedServiceKey} but not found. ` +
348
+ `client_secret is required for ${provider} but not found. ` +
317
349
  `Store it with 'assistant oauth apps upsert --client-secret'.`,
318
350
  );
319
351
  return;
@@ -346,7 +378,7 @@ Examples:
346
378
  });
347
379
  } else {
348
380
  process.stdout.write(
349
- `\nAuthorize with ${resolvedServiceKey}:\n\n${result.authUrl}\n\nThe connection will complete automatically once you authorize.\n`,
381
+ `\nAuthorize with ${provider}:\n\n${result.authUrl}\n\nThe connection will complete automatically once you authorize.\n`,
350
382
  );
351
383
  }
352
384
  return;
@@ -360,7 +392,7 @@ Examples:
360
392
  accountInfo: result.accountInfo,
361
393
  });
362
394
  } else {
363
- const msg = `Connected to ${resolvedServiceKey}${result.accountInfo ? ` as ${result.accountInfo}` : ""}`;
395
+ const msg = `Connected to ${provider}${result.accountInfo ? ` as ${result.accountInfo}` : ""}`;
364
396
  process.stdout.write(msg + "\n");
365
397
  }
366
398
  }