@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
@@ -3,11 +3,18 @@ import { type Command } from "commander";
3
3
  import { loadConfig } from "../../../config/loader.js";
4
4
  import { getOAuthCallbackUrl } from "../../../inbound/public-ingress-urls.js";
5
5
  import {
6
+ deleteApp,
7
+ deleteConnection,
8
+ deleteProvider,
9
+ disconnectOAuthProvider,
6
10
  getProvider,
11
+ listApps,
12
+ listConnections,
7
13
  listProviders,
8
14
  registerProvider,
15
+ updateProvider,
9
16
  } from "../../../oauth/oauth-store.js";
10
- import { getProviderBehavior } from "../../../oauth/provider-behaviors.js";
17
+ import { SEEDED_PROVIDER_KEYS } from "../../../oauth/seed-providers.js";
11
18
  import { getCliLogger } from "../../logger.js";
12
19
  import { shouldOutputJson, writeOutput } from "../../output.js";
13
20
 
@@ -17,21 +24,19 @@ const LOOPBACK_CALLBACK_PATH = "/oauth/callback";
17
24
 
18
25
  /** Resolve the redirect URI for a provider based on its callback transport. */
19
26
  function resolveRedirectUri(
20
- providerKey: string,
21
27
  callbackTransport: string | null,
28
+ loopbackPort: number | null,
22
29
  ): string | null {
23
30
  const transport = callbackTransport ?? "loopback";
24
31
  if (transport === "loopback") {
25
- const behavior = getProviderBehavior(providerKey);
26
- const port = behavior?.loopbackPort;
27
- if (!port) {
32
+ if (!loopbackPort) {
28
33
  // No fixed port — loopback still works at runtime with an OS-assigned
29
34
  // port, but we can't predict the redirect URI ahead of time. Return
30
35
  // a sentinel so callers know the transport is loopback-dynamic rather
31
36
  // than unsupported.
32
37
  return "http://localhost:<dynamic>/oauth/callback";
33
38
  }
34
- return `http://localhost:${port}${LOOPBACK_CALLBACK_PATH}`;
39
+ return `http://localhost:${loopbackPort}${LOOPBACK_CALLBACK_PATH}`;
35
40
  }
36
41
  // Gateway transport — resolve from public ingress config.
37
42
  // Try the explicit publicBaseUrl first, then fall back to platform
@@ -55,13 +60,31 @@ function parseProviderRow(row: ReturnType<typeof getProvider>) {
55
60
  description: row.description ?? null,
56
61
  dashboardUrl: row.dashboardUrl ?? null,
57
62
  clientIdPlaceholder: row.clientIdPlaceholder ?? null,
58
- requiresClientSecret: row.requiresClientSecret ?? 1,
63
+ requiresClientSecret: !!(row.requiresClientSecret ?? 1),
64
+ supportsManagedMode: !!row.managedServiceConfigKey,
59
65
  defaultScopes: row.defaultScopes ? JSON.parse(row.defaultScopes) : [],
60
66
  scopePolicy: row.scopePolicy ? JSON.parse(row.scopePolicy) : {},
61
67
  extraParams: row.extraParams ? JSON.parse(row.extraParams) : null,
62
68
  pingHeaders: row.pingHeaders ? JSON.parse(row.pingHeaders) : null,
63
69
  pingBody: row.pingBody ? JSON.parse(row.pingBody) : null,
64
- redirectUri: resolveRedirectUri(row.providerKey, row.callbackTransport),
70
+ loopbackPort: row.loopbackPort ?? null,
71
+ injectionTemplates: row.injectionTemplates
72
+ ? JSON.parse(row.injectionTemplates)
73
+ : null,
74
+ appType: row.appType ?? null,
75
+ setupNotes: row.setupNotes ? JSON.parse(row.setupNotes) : null,
76
+ identityUrl: row.identityUrl ?? null,
77
+ identityMethod: row.identityMethod ?? null,
78
+ identityHeaders: row.identityHeaders
79
+ ? JSON.parse(row.identityHeaders)
80
+ : null,
81
+ identityBody: row.identityBody ? JSON.parse(row.identityBody) : null,
82
+ identityResponsePaths: row.identityResponsePaths
83
+ ? JSON.parse(row.identityResponsePaths)
84
+ : null,
85
+ identityFormat: row.identityFormat ?? null,
86
+ identityOkField: row.identityOkField ?? null,
87
+ redirectUri: resolveRedirectUri(row.callbackTransport, row.loopbackPort),
65
88
  createdAt: new Date(row.createdAt).toISOString(),
66
89
  updatedAt: new Date(row.updatedAt).toISOString(),
67
90
  };
@@ -71,7 +94,7 @@ export function registerProviderCommands(oauth: Command): void {
71
94
  const providers = oauth
72
95
  .command("providers")
73
96
  .description(
74
- "Manage OAuth provider configurations (auth URLs, scopes, endpoints)",
97
+ "Fetch configured OAuth providers and register custom providers of your own",
75
98
  );
76
99
 
77
100
  providers.addHelpText(
@@ -83,7 +106,7 @@ authorization URL, token URL, default scopes, and other endpoint details.
83
106
  They are seeded on startup for built-in integrations (e.g. Google, Slack,
84
107
  GitHub) but can also be registered dynamically via the "register" subcommand.
85
108
 
86
- Each provider is identified by a provider key (e.g. "integration:google").`,
109
+ Each provider is identified by a provider key (e.g. "google").`,
87
110
  );
88
111
 
89
112
  // ---------------------------------------------------------------------------
@@ -159,7 +182,7 @@ Examples:
159
182
  "after",
160
183
  `
161
184
  Arguments:
162
- provider-key The full provider key (e.g. "integration:google").
185
+ provider-key Provider key (e.g. "google").
163
186
  Must match the key used during registration or seeding.
164
187
 
165
188
  Returns the full provider configuration including auth URL, token URL,
@@ -167,8 +190,8 @@ default scopes, scope policy, and extra parameters. Exits with code 1
167
190
  if the provider key is not found.
168
191
 
169
192
  Examples:
170
- $ assistant oauth providers get integration:google
171
- $ assistant oauth providers get integration:twitter --json`,
193
+ $ assistant oauth providers get google
194
+ $ assistant oauth providers get twitter --json`,
172
195
  )
173
196
  .action((providerKey: string, _opts: unknown, cmd: Command) => {
174
197
  try {
@@ -200,84 +223,148 @@ Examples:
200
223
  .description("Register a new OAuth provider configuration")
201
224
  .requiredOption(
202
225
  "--provider-key <key>",
203
- "Unique provider key (e.g. \"integration:custom-service\"). Must not collide with an existing key from 'assistant oauth providers list'.",
226
+ "Unique provider key (e.g. \"custom-service\"). Must not collide with an existing key from 'assistant oauth providers list'.",
227
+ )
228
+ .requiredOption(
229
+ "--auth-url <url>",
230
+ "OAuth authorization endpoint URL (e.g. https://accounts.example.com/o/oauth2/auth)",
231
+ )
232
+ .requiredOption(
233
+ "--token-url <url>",
234
+ "OAuth token endpoint URL (e.g. https://oauth2.example.com/token)",
235
+ )
236
+ .option("--base-url <url>", "API base URL for the service")
237
+ .option("--userinfo-url <url>", "OpenID Connect userinfo endpoint URL")
238
+ .option(
239
+ "--scopes <scopes>",
240
+ 'Comma-separated default scopes (e.g. "read,write,profile")',
241
+ )
242
+ .option(
243
+ "--token-auth-method <method>",
244
+ 'How the client authenticates at the token endpoint: "client_secret_post" or "client_secret_basic"',
245
+ )
246
+ .option(
247
+ "--callback-transport <transport>",
248
+ 'OAuth callback transport: "loopback" (local HTTP server, default) or "gateway" (public ingress)',
204
249
  )
205
- .requiredOption("--auth-url <url>", "Authorization endpoint URL")
206
- .requiredOption("--token-url <url>", "Token endpoint URL")
207
- .option("--base-url <url>", "API base URL")
208
- .option("--userinfo-url <url>", "Userinfo endpoint URL")
209
- .option("--scopes <scopes>", "Comma-separated default scopes")
210
- .option("--token-auth-method <method>", "Token endpoint auth method")
211
- .option("--callback-transport <transport>", "Callback transport")
212
250
  .option(
213
251
  "--ping-url <url>",
214
- "Health-check endpoint URL for token validation",
252
+ 'Health-check endpoint URL for token validation (e.g. "https://api.example.com/user"). Used by "assistant oauth ping" to verify a stored token.',
215
253
  )
216
254
  .option(
217
255
  "--ping-method <method>",
218
- "HTTP method for the ping endpoint (default: GET)",
256
+ "HTTP method for the ping endpoint: GET (default) or POST",
219
257
  )
220
258
  .option(
221
259
  "--ping-headers <json>",
222
- "JSON object of extra headers for the ping request",
260
+ 'JSON object of extra headers for the ping request (e.g. \'{"Notion-Version":"2022-06-28"}\')',
261
+ )
262
+ .option(
263
+ "--ping-body <json>",
264
+ 'JSON body to send with the ping request (e.g. \'{"query":"{ viewer { id } }"}\')',
265
+ )
266
+ .option(
267
+ "--display-name <name>",
268
+ "Human-readable display name for the provider",
269
+ )
270
+ .option("--description <text>", "Short description of the provider")
271
+ .option(
272
+ "--dashboard-url <url>",
273
+ "URL to the provider's developer console / dashboard",
274
+ )
275
+ .option(
276
+ "--client-id-placeholder <text>",
277
+ "Placeholder text shown in the client ID input field",
223
278
  )
224
- .option("--ping-body <json>", "JSON body to send with the ping request")
225
- .option("--display-name <name>", "Display name for the provider")
226
- .option("--description <text>", "Short description")
227
- .option("--dashboard-url <url>", "Developer console URL")
228
- .option("--client-id-placeholder <text>", "Placeholder for client ID field")
229
279
  .option(
230
280
  "--no-client-secret",
231
- "Set requires_client_secret to false (default is true)",
281
+ "Mark this provider as not requiring a client secret (default: required)",
282
+ )
283
+ .option(
284
+ "--loopback-port <port>",
285
+ "Fixed port for the local OAuth callback server (e.g. 17322). When set, the redirect URI is http://localhost:<port>/oauth/callback",
286
+ )
287
+ .option(
288
+ "--injection-templates <json>",
289
+ 'JSON array of token injection templates — each with hostPattern, injectionType, headerName, valuePrefix (e.g. \'[{"hostPattern":"api.example.com","injectionType":"header","headerName":"Authorization","valuePrefix":"Bearer "}]\')',
290
+ )
291
+ .option(
292
+ "--app-type <type>",
293
+ 'What the provider calls its OAuth apps (e.g. "OAuth App", "Desktop app", "Public integration")',
294
+ )
295
+ .option(
296
+ "--identity-url <url>",
297
+ "Identity verification endpoint URL — called after OAuth to identify the connected account",
298
+ )
299
+ .option(
300
+ "--identity-method <method>",
301
+ "HTTP method for the identity endpoint: GET (default) or POST",
302
+ )
303
+ .option(
304
+ "--identity-headers <json>",
305
+ 'JSON object of extra headers for the identity request (e.g. \'{"Notion-Version":"2022-06-28"}\')',
306
+ )
307
+ .option(
308
+ "--identity-body <body>",
309
+ 'JSON body to send with the identity request (e.g. \'{"query":"{ viewer { email } }"}\')',
310
+ )
311
+ .option(
312
+ "--identity-response-paths <paths>",
313
+ 'Comma-separated dot-notation paths to extract identity from the response (e.g. "email,name,person.email")',
314
+ )
315
+ .option(
316
+ "--identity-format <template>",
317
+ 'Format template for the extracted identity using ${pathName} tokens from --identity-response-paths (e.g. "@${login}" or "@${user} (${team})")',
318
+ )
319
+ .option(
320
+ "--identity-ok-field <field>",
321
+ 'Dot-notation path to a boolean field that must be truthy for the response to be considered valid (e.g. "ok")',
322
+ )
323
+ .option(
324
+ "--setup-notes <json>",
325
+ 'JSON array of setup instruction notes shown during guided setup (e.g. \'["Enable the Gmail API","Add test users"]\')',
232
326
  )
233
327
  .addHelpText(
234
328
  "after",
235
329
  `
236
- Arguments (via options):
237
- --provider-key Unique identifier for this provider (e.g. "integration:custom-service").
238
- Must not collide with an existing provider key.
239
- --auth-url The OAuth authorization endpoint URL.
240
- --token-url The OAuth token endpoint URL.
241
- --base-url Optional API base URL for the service.
242
- --userinfo-url Optional OpenID Connect userinfo endpoint.
243
- --scopes Comma-separated list of default scopes (e.g. "read,write,profile").
244
- --token-auth-method How the client authenticates at the token endpoint
245
- (e.g. "client_secret_post", "client_secret_basic").
246
- --callback-transport Transport method for the OAuth callback.
247
- --ping-url Optional URL for a lightweight health-check endpoint.
248
- Used by "assistant oauth ping" to validate that a stored token
249
- is still functional (e.g. "https://api.example.com/user").
250
- --ping-method HTTP method for the ping health-check (GET, POST). Defaults to GET.
251
- --ping-headers JSON object of extra HTTP headers for the ping request
252
- (e.g. '{"Notion-Version":"2022-06-28"}').
253
- --ping-body JSON body to send with the ping request
254
- (e.g. '{"query":"{ viewer { id } }"}').
255
- --display-name Optional human-readable display name for the provider.
256
- --description Optional short description of the provider.
257
- --dashboard-url Optional URL to the provider's developer console / dashboard.
258
- --client-id-placeholder Optional placeholder text for the client ID input field.
259
- --no-client-secret If set, marks the provider as not requiring a client secret
260
- (sets requires_client_secret to 0). Default is true (1).
261
-
262
- Registers a new OAuth provider configuration in the local store. This is
263
- used for custom integrations not covered by the built-in provider seeds.
330
+ Registers a new OAuth provider configuration in the local store for custom
331
+ integrations not covered by the built-in provider seeds. The provider key
332
+ must be unique — if it collides with an existing key, the command fails.
333
+ Run 'assistant oauth providers list' to see existing keys.
334
+
264
335
  On success, returns the full provider row including generated timestamps.
336
+ After registering, create an OAuth app with 'assistant oauth apps create'
337
+ and then connect with 'assistant oauth connect <provider-key>'.
338
+
339
+ Token injection templates control how the OAuth access token is injected
340
+ into outgoing HTTP requests matched by host pattern. Identity config
341
+ defines how the assistant verifies the connected account after OAuth.
265
342
 
266
343
  Examples:
267
344
  $ assistant oauth providers register \\
268
- --provider-key integration:custom-api \\
345
+ --provider-key custom-api \\
269
346
  --auth-url https://custom-api.example.com/oauth/authorize \\
270
347
  --token-url https://custom-api.example.com/oauth/token
271
348
  $ assistant oauth providers register \\
272
- --provider-key integration:my-service \\
349
+ --provider-key my-service \\
273
350
  --auth-url https://my-service.com/auth \\
274
351
  --token-url https://my-service.com/token \\
275
352
  --scopes read,write --json
276
353
  $ assistant oauth providers register \\
277
- --provider-key integration:custom-api \\
354
+ --provider-key my-graphql-api \\
278
355
  --auth-url https://example.com/auth \\
279
356
  --token-url https://example.com/token \\
280
- --ping-url https://example.com/user`,
357
+ --ping-url https://example.com/graphql \\
358
+ --ping-method POST \\
359
+ --ping-body '{"query":"{ viewer { id } }"}'
360
+ $ assistant oauth providers register \\
361
+ --provider-key my-api \\
362
+ --auth-url https://example.com/auth \\
363
+ --token-url https://example.com/token \\
364
+ --loopback-port 17400 \\
365
+ --injection-templates '[{"hostPattern":"api.example.com","injectionType":"header","headerName":"Authorization","valuePrefix":"Bearer "}]' \\
366
+ --identity-url https://api.example.com/me \\
367
+ --identity-response-paths email,name`,
281
368
  )
282
369
  .action(
283
370
  (
@@ -299,6 +386,17 @@ Examples:
299
386
  dashboardUrl?: string;
300
387
  clientIdPlaceholder?: string;
301
388
  clientSecret: boolean;
389
+ loopbackPort?: string;
390
+ injectionTemplates?: string;
391
+ appType?: string;
392
+ identityUrl?: string;
393
+ identityMethod?: string;
394
+ identityHeaders?: string;
395
+ identityBody?: string;
396
+ identityResponsePaths?: string;
397
+ identityFormat?: string;
398
+ identityOkField?: string;
399
+ setupNotes?: string;
302
400
  },
303
401
  cmd: Command,
304
402
  ) => {
@@ -324,9 +422,418 @@ Examples:
324
422
  dashboardUrl: opts.dashboardUrl,
325
423
  clientIdPlaceholder: opts.clientIdPlaceholder,
326
424
  requiresClientSecret: opts.clientSecret ? 1 : 0,
425
+ loopbackPort: opts.loopbackPort
426
+ ? parseInt(opts.loopbackPort, 10)
427
+ : undefined,
428
+ injectionTemplates: opts.injectionTemplates
429
+ ? JSON.parse(opts.injectionTemplates)
430
+ : undefined,
431
+ appType: opts.appType,
432
+ identityUrl: opts.identityUrl,
433
+ identityMethod: opts.identityMethod,
434
+ identityHeaders: opts.identityHeaders
435
+ ? JSON.parse(opts.identityHeaders)
436
+ : undefined,
437
+ identityBody: opts.identityBody
438
+ ? JSON.parse(opts.identityBody)
439
+ : undefined,
440
+ identityResponsePaths: opts.identityResponsePaths
441
+ ? opts.identityResponsePaths.split(",")
442
+ : undefined,
443
+ identityFormat: opts.identityFormat,
444
+ identityOkField: opts.identityOkField,
445
+ setupNotes: opts.setupNotes
446
+ ? JSON.parse(opts.setupNotes)
447
+ : undefined,
327
448
  });
328
449
 
329
450
  writeOutput(cmd, parseProviderRow(row));
451
+ } catch (err) {
452
+ let message = err instanceof Error ? err.message : String(err);
453
+ if (message.includes("already exists")) {
454
+ message += ` Run 'assistant oauth providers list' to see existing providers, or choose a different --provider-key.`;
455
+ }
456
+ writeOutput(cmd, { ok: false, error: message });
457
+ process.exitCode = 1;
458
+ }
459
+ },
460
+ );
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // providers update <provider-key>
464
+ // ---------------------------------------------------------------------------
465
+
466
+ providers
467
+ .command("update <provider-key>")
468
+ .description("Update an existing custom OAuth provider configuration")
469
+ .option(
470
+ "--auth-url <url>",
471
+ "OAuth authorization endpoint URL (e.g. https://accounts.example.com/o/oauth2/auth)",
472
+ )
473
+ .option(
474
+ "--token-url <url>",
475
+ "OAuth token endpoint URL (e.g. https://oauth2.example.com/token)",
476
+ )
477
+ .option("--base-url <url>", "API base URL for the service")
478
+ .option("--userinfo-url <url>", "OpenID Connect userinfo endpoint URL")
479
+ .option(
480
+ "--scopes <scopes>",
481
+ 'Comma-separated default scopes (e.g. "read,write,profile")',
482
+ )
483
+ .option(
484
+ "--token-auth-method <method>",
485
+ 'How the client authenticates at the token endpoint: "client_secret_post" or "client_secret_basic"',
486
+ )
487
+ .option(
488
+ "--callback-transport <transport>",
489
+ 'OAuth callback transport: "loopback" (local HTTP server, default) or "gateway" (public ingress)',
490
+ )
491
+ .option(
492
+ "--ping-url <url>",
493
+ 'Health-check endpoint URL for token validation (e.g. "https://api.example.com/user"). Used by "assistant oauth ping" to verify a stored token.',
494
+ )
495
+ .option(
496
+ "--ping-method <method>",
497
+ "HTTP method for the ping endpoint: GET (default) or POST",
498
+ )
499
+ .option(
500
+ "--ping-headers <json>",
501
+ 'JSON object of extra headers for the ping request (e.g. \'{"Notion-Version":"2022-06-28"}\')',
502
+ )
503
+ .option(
504
+ "--ping-body <json>",
505
+ 'JSON body to send with the ping request (e.g. \'{"query":"{ viewer { id } }"}\')',
506
+ )
507
+ .option(
508
+ "--display-name <name>",
509
+ "Human-readable display name for the provider",
510
+ )
511
+ .option("--description <text>", "Short description of the provider")
512
+ .option(
513
+ "--dashboard-url <url>",
514
+ "URL to the provider's developer console / dashboard",
515
+ )
516
+ .option(
517
+ "--client-id-placeholder <text>",
518
+ "Placeholder text shown in the client ID input field",
519
+ )
520
+ .option(
521
+ "--no-client-secret",
522
+ "Mark this provider as not requiring a client secret (default: required)",
523
+ )
524
+ .option(
525
+ "--loopback-port <port>",
526
+ "Fixed port for the local OAuth callback server (e.g. 17322). When set, the redirect URI is http://localhost:<port>/oauth/callback",
527
+ )
528
+ .option(
529
+ "--injection-templates <json>",
530
+ 'JSON array of token injection templates — each with hostPattern, injectionType, headerName, valuePrefix (e.g. \'[{"hostPattern":"api.example.com","injectionType":"header","headerName":"Authorization","valuePrefix":"Bearer "}]\')',
531
+ )
532
+ .option(
533
+ "--app-type <type>",
534
+ 'What the provider calls its OAuth apps (e.g. "OAuth App", "Desktop app", "Public integration")',
535
+ )
536
+ .option(
537
+ "--identity-url <url>",
538
+ "Identity verification endpoint URL — called after OAuth to identify the connected account",
539
+ )
540
+ .option(
541
+ "--identity-method <method>",
542
+ "HTTP method for the identity endpoint: GET (default) or POST",
543
+ )
544
+ .option(
545
+ "--identity-headers <json>",
546
+ 'JSON object of extra headers for the identity request (e.g. \'{"Notion-Version":"2022-06-28"}\')',
547
+ )
548
+ .option(
549
+ "--identity-body <body>",
550
+ 'JSON body to send with the identity request (e.g. \'{"query":"{ viewer { email } }"}\')',
551
+ )
552
+ .option(
553
+ "--identity-response-paths <paths>",
554
+ 'Comma-separated dot-notation paths to extract identity from the response (e.g. "email,name,person.email")',
555
+ )
556
+ .option(
557
+ "--identity-format <template>",
558
+ 'Format template for the extracted identity using ${pathName} tokens from --identity-response-paths (e.g. "@${login}" or "@${user} (${team})")',
559
+ )
560
+ .option(
561
+ "--identity-ok-field <field>",
562
+ 'Dot-notation path to a boolean field that must be truthy for the response to be considered valid (e.g. "ok")',
563
+ )
564
+ .option(
565
+ "--setup-notes <json>",
566
+ 'JSON array of setup instruction notes shown during guided setup (e.g. \'["Enable the Gmail API","Add test users"]\')',
567
+ )
568
+ .addHelpText(
569
+ "after",
570
+ `
571
+ Arguments:
572
+ provider-key Provider key to update (e.g. "custom-api").
573
+ Run 'assistant oauth providers list' to see all registered providers.
574
+
575
+ Only the fields you specify are updated — all other fields remain unchanged.
576
+ Built-in providers (e.g. "google", "slack") cannot be updated; they are
577
+ managed by the system and reset on startup. To create a custom provider with
578
+ different settings, use 'assistant oauth providers register'.
579
+
580
+ Token injection templates control how the OAuth access token is injected
581
+ into outgoing HTTP requests matched by host pattern. Identity config
582
+ defines how the assistant verifies the connected account after OAuth.
583
+
584
+ Examples:
585
+ $ assistant oauth providers update custom-api --display-name "My Custom API"
586
+ $ assistant oauth providers update custom-api --scopes read,write --auth-url https://new.example.com/auth
587
+ $ assistant oauth providers update custom-api --ping-url https://api.example.com/me --json
588
+ $ assistant oauth providers update custom-api \\
589
+ --identity-url https://api.example.com/me \\
590
+ --identity-response-paths email,name
591
+ $ assistant oauth providers update custom-api \\
592
+ --injection-templates '[{"hostPattern":"api.example.com","injectionType":"header","headerName":"Authorization","valuePrefix":"Bearer "}]'`,
593
+ )
594
+ .action(
595
+ (
596
+ providerKey: string,
597
+ opts: {
598
+ authUrl?: string;
599
+ tokenUrl?: string;
600
+ baseUrl?: string;
601
+ userinfoUrl?: string;
602
+ scopes?: string;
603
+ tokenAuthMethod?: string;
604
+ callbackTransport?: string;
605
+ pingUrl?: string;
606
+ pingMethod?: string;
607
+ pingHeaders?: string;
608
+ pingBody?: string;
609
+ displayName?: string;
610
+ description?: string;
611
+ dashboardUrl?: string;
612
+ clientIdPlaceholder?: string;
613
+ clientSecret: boolean;
614
+ loopbackPort?: string;
615
+ injectionTemplates?: string;
616
+ appType?: string;
617
+ identityUrl?: string;
618
+ identityMethod?: string;
619
+ identityHeaders?: string;
620
+ identityBody?: string;
621
+ identityResponsePaths?: string;
622
+ identityFormat?: string;
623
+ identityOkField?: string;
624
+ setupNotes?: string;
625
+ },
626
+ cmd: Command,
627
+ ) => {
628
+ try {
629
+ // Verify provider exists
630
+ const existing = getProvider(providerKey);
631
+ if (!existing) {
632
+ writeOutput(cmd, {
633
+ ok: false,
634
+ error: `Provider "${providerKey}" not found. Run 'assistant oauth providers list' to see all registered providers.`,
635
+ });
636
+ process.exitCode = 1;
637
+ return;
638
+ }
639
+
640
+ // Block updates to built-in providers
641
+ if (SEEDED_PROVIDER_KEYS.has(providerKey)) {
642
+ writeOutput(cmd, {
643
+ ok: false,
644
+ error: `Cannot update built-in provider "${providerKey}". Built-in providers are managed by the system and reset on startup. To create a custom provider with different settings, use 'assistant oauth providers register --provider-key <your-custom-key> ...'`,
645
+ });
646
+ process.exitCode = 1;
647
+ return;
648
+ }
649
+
650
+ // Build params object from provided options, omitting undefined values
651
+ const params: Record<string, unknown> = {};
652
+
653
+ if (opts.authUrl !== undefined) params.authUrl = opts.authUrl;
654
+ if (opts.tokenUrl !== undefined) params.tokenUrl = opts.tokenUrl;
655
+ if (opts.baseUrl !== undefined) params.baseUrl = opts.baseUrl;
656
+ if (opts.userinfoUrl !== undefined)
657
+ params.userinfoUrl = opts.userinfoUrl;
658
+ if (opts.scopes !== undefined)
659
+ params.defaultScopes = opts.scopes.split(",");
660
+ if (opts.tokenAuthMethod !== undefined)
661
+ params.tokenEndpointAuthMethod = opts.tokenAuthMethod;
662
+ if (opts.callbackTransport !== undefined)
663
+ params.callbackTransport = opts.callbackTransport;
664
+ if (opts.pingUrl !== undefined) params.pingUrl = opts.pingUrl;
665
+ if (opts.pingMethod !== undefined)
666
+ params.pingMethod = opts.pingMethod;
667
+ if (opts.pingHeaders !== undefined)
668
+ params.pingHeaders = JSON.parse(opts.pingHeaders);
669
+ if (opts.pingBody !== undefined)
670
+ params.pingBody = JSON.parse(opts.pingBody);
671
+ if (opts.displayName !== undefined)
672
+ params.displayName = opts.displayName;
673
+ if (opts.description !== undefined)
674
+ params.description = opts.description;
675
+ if (opts.dashboardUrl !== undefined)
676
+ params.dashboardUrl = opts.dashboardUrl;
677
+ if (opts.clientIdPlaceholder !== undefined)
678
+ params.clientIdPlaceholder = opts.clientIdPlaceholder;
679
+
680
+ // Handle the negated --no-client-* flag: Commander defaults
681
+ // opts.clientSecret to true; the negated form sets it to false.
682
+ // Use getOptionValueSource to detect explicit user intent.
683
+ if (cmd.getOptionValueSource("clientSecret") === "cli") {
684
+ params.requiresClientSecret = opts.clientSecret ? 1 : 0;
685
+ }
686
+
687
+ if (opts.loopbackPort !== undefined)
688
+ params.loopbackPort = parseInt(opts.loopbackPort, 10);
689
+ if (opts.injectionTemplates !== undefined)
690
+ params.injectionTemplates = JSON.parse(opts.injectionTemplates);
691
+ if (opts.appType !== undefined) params.appType = opts.appType;
692
+ if (opts.identityUrl !== undefined)
693
+ params.identityUrl = opts.identityUrl;
694
+ if (opts.identityMethod !== undefined)
695
+ params.identityMethod = opts.identityMethod;
696
+ if (opts.identityHeaders !== undefined)
697
+ params.identityHeaders = JSON.parse(opts.identityHeaders);
698
+ if (opts.identityBody !== undefined)
699
+ params.identityBody = JSON.parse(opts.identityBody);
700
+ if (opts.identityResponsePaths !== undefined)
701
+ params.identityResponsePaths =
702
+ opts.identityResponsePaths.split(",");
703
+ if (opts.identityFormat !== undefined)
704
+ params.identityFormat = opts.identityFormat;
705
+ if (opts.identityOkField !== undefined)
706
+ params.identityOkField = opts.identityOkField;
707
+ if (opts.setupNotes !== undefined)
708
+ params.setupNotes = JSON.parse(opts.setupNotes);
709
+
710
+ // Check if any fields were actually provided
711
+ if (Object.keys(params).length === 0) {
712
+ writeOutput(cmd, {
713
+ ok: false,
714
+ error:
715
+ "Nothing to update. Provide at least one option to change (e.g. --auth-url, --scopes, --display-name). Run 'assistant oauth providers update --help' for all options.",
716
+ });
717
+ process.exitCode = 1;
718
+ return;
719
+ }
720
+
721
+ const row = updateProvider(providerKey, params);
722
+
723
+ writeOutput(cmd, parseProviderRow(row));
724
+ } catch (err) {
725
+ const message = err instanceof Error ? err.message : String(err);
726
+ writeOutput(cmd, { ok: false, error: message });
727
+ process.exitCode = 1;
728
+ }
729
+ },
730
+ );
731
+
732
+ // ---------------------------------------------------------------------------
733
+ // providers delete <provider-key>
734
+ // ---------------------------------------------------------------------------
735
+
736
+ providers
737
+ .command("delete <provider-key>")
738
+ .description(
739
+ "Delete a custom OAuth provider and optionally its associated apps and connections",
740
+ )
741
+ .option(
742
+ "--force",
743
+ "Cascade-delete all associated apps and connections before removing the provider",
744
+ )
745
+ .addHelpText(
746
+ "after",
747
+ `
748
+ Arguments:
749
+ provider-key Provider key to delete (e.g. "custom-api").
750
+ Run 'assistant oauth providers list' to see registered providers.
751
+
752
+ When --force is specified, all OAuth connections and apps that depend on
753
+ this provider are deleted before the provider itself is removed. Without
754
+ --force, the command refuses to delete a provider that has dependents and
755
+ exits with an error listing the counts.
756
+
757
+ Built-in providers (e.g. "google", "slack") can be deleted, but a warning
758
+ is emitted because they will be re-created automatically on the next
759
+ assistant startup.
760
+
761
+ Examples:
762
+ $ assistant oauth providers delete custom-api
763
+ $ assistant oauth providers delete custom-api --force
764
+ $ assistant oauth providers delete custom-api --force --json`,
765
+ )
766
+ .action(
767
+ async (providerKey: string, opts: { force?: boolean }, cmd: Command) => {
768
+ try {
769
+ const provider = getProvider(providerKey);
770
+ if (!provider) {
771
+ writeOutput(cmd, {
772
+ ok: false,
773
+ error: `Provider not found: "${providerKey}". Run 'assistant oauth providers list' to see all registered providers.`,
774
+ });
775
+ process.exitCode = 1;
776
+ return;
777
+ }
778
+
779
+ if (SEEDED_PROVIDER_KEYS.has(providerKey) && !opts.force) {
780
+ log.info(
781
+ `Note: "${providerKey}" is a built-in provider and will be re-created on next startup.`,
782
+ );
783
+ }
784
+
785
+ const dependentApps = listApps().filter(
786
+ (a) => a.providerKey === providerKey,
787
+ );
788
+ const dependentConnections = listConnections(providerKey);
789
+ const appCount = dependentApps.length;
790
+ const connCount = dependentConnections.length;
791
+
792
+ if ((appCount > 0 || connCount > 0) && !opts.force) {
793
+ writeOutput(cmd, {
794
+ ok: false,
795
+ error: `Cannot delete provider "${providerKey}": ${appCount} app(s) and ${connCount} connection(s) depend on it. Use --force to cascade-delete all dependent apps and connections, or remove them manually first with 'assistant oauth apps delete' and 'assistant oauth disconnect'.`,
796
+ });
797
+ process.exitCode = 1;
798
+ return;
799
+ }
800
+
801
+ // Warn about built-in providers when --force is used
802
+ if (SEEDED_PROVIDER_KEYS.has(providerKey) && opts.force) {
803
+ log.info(
804
+ `Note: "${providerKey}" is a built-in provider and will be re-created on next startup.`,
805
+ );
806
+ }
807
+
808
+ // Cascade-delete connections first, then apps, then the provider.
809
+ // Use disconnectOAuthProvider to clean up OAuth tokens from secure
810
+ // storage in addition to deleting the connection DB row.
811
+ for (const conn of dependentConnections) {
812
+ const result = await disconnectOAuthProvider(
813
+ providerKey,
814
+ undefined,
815
+ conn.id as string,
816
+ );
817
+ if (result === "error") {
818
+ log.info(
819
+ `Warning: failed to clean up tokens for connection ${conn.id} — deleting connection row to continue cascade.`,
820
+ );
821
+ deleteConnection(conn.id);
822
+ }
823
+ }
824
+ for (const app of dependentApps) {
825
+ await deleteApp(app.id);
826
+ }
827
+ deleteProvider(providerKey);
828
+
829
+ if (!shouldOutputJson(cmd)) {
830
+ log.info(`Deleted provider: ${providerKey}`);
831
+ }
832
+
833
+ writeOutput(cmd, {
834
+ ok: true,
835
+ deleted: { provider: 1, apps: appCount, connections: connCount },
836
+ });
330
837
  } catch (err) {
331
838
  const message = err instanceof Error ? err.message : String(err);
332
839
  writeOutput(cmd, { ok: false, error: message });