@vellumai/assistant 0.5.11 → 0.5.12

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 (188) hide show
  1. package/Dockerfile +1 -0
  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/credential-storage/src/index.ts +1 -1
  6. package/openapi.yaml +87 -9
  7. package/package.json +1 -1
  8. package/src/__tests__/catalog-cache.test.ts +164 -0
  9. package/src/__tests__/catalog-search.test.ts +61 -0
  10. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  11. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  12. package/src/__tests__/conversation-error.test.ts +3 -2
  13. package/src/__tests__/credential-security-invariants.test.ts +9 -15
  14. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  15. package/src/__tests__/credential-vault.test.ts +25 -33
  16. package/src/__tests__/credentials-cli.test.ts +3 -3
  17. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  18. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  19. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  20. package/src/__tests__/host-file-proxy.test.ts +89 -0
  21. package/src/__tests__/integration-status.test.ts +5 -5
  22. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  23. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  25. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  26. package/src/__tests__/oauth-cli.test.ts +126 -119
  27. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  28. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  29. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  30. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  31. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  32. package/src/__tests__/skills-uninstall.test.ts +2 -2
  33. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  34. package/src/__tests__/slack-share-routes.test.ts +5 -5
  35. package/src/__tests__/system-prompt.test.ts +39 -0
  36. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  37. package/src/cli/AGENTS.md +47 -7
  38. package/src/cli/commands/browser-relay.ts +2 -17
  39. package/src/cli/commands/contacts.ts +6 -4
  40. package/src/cli/commands/conversations.ts +13 -1
  41. package/src/cli/commands/credential-execution.ts +16 -1
  42. package/src/cli/commands/credentials.ts +2 -8
  43. package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
  44. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
  45. package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
  46. package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
  47. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  48. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  49. package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
  50. package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
  51. package/src/cli/commands/oauth/apps.ts +63 -44
  52. package/src/cli/commands/oauth/connect.ts +187 -155
  53. package/src/cli/commands/oauth/disconnect.ts +27 -75
  54. package/src/cli/commands/oauth/index.ts +36 -46
  55. package/src/cli/commands/oauth/mode.ts +22 -34
  56. package/src/cli/commands/oauth/ping.ts +19 -45
  57. package/src/cli/commands/oauth/providers.ts +569 -62
  58. package/src/cli/commands/oauth/request.ts +36 -48
  59. package/src/cli/commands/oauth/shared.ts +1 -19
  60. package/src/cli/commands/oauth/status.ts +14 -25
  61. package/src/cli/commands/oauth/token.ts +25 -34
  62. package/src/cli/commands/platform/connect.ts +104 -0
  63. package/src/cli/commands/platform/disconnect.ts +118 -0
  64. package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
  65. package/src/cli/commands/sequence.ts +5 -4
  66. package/src/cli/commands/shotgun.ts +16 -0
  67. package/src/cli/commands/skills.ts +173 -41
  68. package/src/cli/commands/usage.ts +5 -11
  69. package/src/cli/lib/daemon-credential-client.ts +22 -38
  70. package/src/cli/program.ts +1 -1
  71. package/src/config/assistant-feature-flags.ts +3 -7
  72. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  73. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  74. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  75. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  76. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  77. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  78. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  79. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  80. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  81. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  82. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  83. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  84. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  85. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  86. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  87. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  88. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  89. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  90. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  91. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  92. package/src/config/bundled-skills/messaging/SKILL.md +7 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  94. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  95. package/src/config/bundled-tool-registry.ts +5 -0
  96. package/src/config/feature-flag-registry.json +1 -1
  97. package/src/credential-execution/client.ts +1 -1
  98. package/src/daemon/conversation-agent-loop.ts +2 -0
  99. package/src/daemon/conversation-error.ts +36 -6
  100. package/src/daemon/conversation-messaging.ts +9 -0
  101. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  102. package/src/daemon/conversation-surfaces.ts +120 -14
  103. package/src/daemon/conversation.ts +5 -0
  104. package/src/daemon/handlers/skills.ts +148 -3
  105. package/src/daemon/host-bash-proxy.ts +16 -0
  106. package/src/daemon/host-cu-proxy.ts +16 -0
  107. package/src/daemon/host-file-proxy.ts +16 -0
  108. package/src/daemon/lifecycle.ts +47 -1
  109. package/src/daemon/message-types/conversations.ts +1 -0
  110. package/src/daemon/message-types/guardian-actions.ts +2 -0
  111. package/src/daemon/message-types/host-bash.ts +6 -1
  112. package/src/daemon/message-types/host-cu.ts +6 -1
  113. package/src/daemon/message-types/host-file.ts +6 -1
  114. package/src/daemon/message-types/integrations.ts +0 -1
  115. package/src/daemon/server.ts +29 -2
  116. package/src/hooks/cli.ts +74 -0
  117. package/src/inbound/platform-callback-registration.ts +7 -12
  118. package/src/mcp/client.ts +6 -1
  119. package/src/mcp/manager.ts +2 -1
  120. package/src/memory/conversation-crud.ts +92 -3
  121. package/src/memory/conversation-key-store.ts +26 -0
  122. package/src/memory/db-init.ts +16 -0
  123. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  124. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  125. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  126. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  127. package/src/memory/migrations/index.ts +4 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/schema/oauth.ts +11 -0
  130. package/src/messaging/provider.ts +13 -12
  131. package/src/messaging/providers/gmail/adapter.ts +44 -35
  132. package/src/messaging/providers/slack/adapter.ts +63 -33
  133. package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
  134. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  135. package/src/notifications/adapters/telegram.ts +78 -2
  136. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  137. package/src/oauth/byo-connection.test.ts +22 -24
  138. package/src/oauth/connect-orchestrator.ts +37 -76
  139. package/src/oauth/connect-types.ts +7 -65
  140. package/src/oauth/connection-resolver.test.ts +13 -13
  141. package/src/oauth/connection-resolver.ts +3 -4
  142. package/src/oauth/identity-verifier.ts +177 -0
  143. package/src/oauth/oauth-store.ts +228 -3
  144. package/src/oauth/platform-connection.test.ts +56 -6
  145. package/src/oauth/platform-connection.ts +8 -1
  146. package/src/oauth/seed-providers.ts +247 -34
  147. package/src/permissions/checker.ts +127 -1
  148. package/src/prompts/system-prompt.ts +43 -9
  149. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  150. package/src/providers/anthropic/client.ts +2 -33
  151. package/src/runtime/guardian-action-service.ts +7 -2
  152. package/src/runtime/http-server.ts +5 -3
  153. package/src/runtime/http-types.ts +8 -1
  154. package/src/runtime/routes/conversation-management-routes.ts +31 -0
  155. package/src/runtime/routes/conversation-routes.ts +79 -4
  156. package/src/runtime/routes/guardian-action-routes.ts +15 -2
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  158. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  159. package/src/runtime/routes/oauth-apps.ts +2 -1
  160. package/src/runtime/routes/secret-routes.ts +36 -13
  161. package/src/runtime/routes/settings-routes.ts +12 -19
  162. package/src/runtime/routes/skills-routes.ts +45 -4
  163. package/src/schedule/integration-status.ts +2 -2
  164. package/src/security/ces-rpc-credential-backend.ts +19 -16
  165. package/src/security/oauth-completion-page.ts +153 -0
  166. package/src/security/oauth2.ts +3 -17
  167. package/src/security/secure-keys.ts +207 -7
  168. package/src/security/token-manager.ts +3 -6
  169. package/src/signals/bash.ts +6 -1
  170. package/src/skills/catalog-cache.ts +44 -0
  171. package/src/skills/catalog-search.ts +18 -0
  172. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  173. package/src/tools/credentials/vault.ts +34 -45
  174. package/src/tools/host-terminal/host-shell.ts +16 -3
  175. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  176. package/src/tools/skills/sandbox-runner.ts +16 -3
  177. package/src/tools/terminal/shell.ts +16 -3
  178. package/src/util/logger.ts +11 -1
  179. package/src/util/sentry-log-stream.ts +51 -0
  180. package/src/watcher/providers/github.ts +2 -2
  181. package/src/watcher/providers/gmail.ts +1 -1
  182. package/src/watcher/providers/google-calendar.ts +1 -1
  183. package/src/watcher/providers/linear.ts +2 -2
  184. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  185. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  186. package/src/workspace/migrations/registry.ts +2 -0
  187. package/src/cli/commands/oauth/connections.ts +0 -255
  188. package/src/oauth/provider-behaviors.ts +0 -634
@@ -106,7 +106,7 @@ async function queueApiKeyPropagation(
106
106
 
107
107
  export async function handleAddSecret(
108
108
  req: Request,
109
- getCesClient?: () => CesClient | undefined,
109
+ deps?: SecretRouteDeps,
110
110
  ): Promise<Response> {
111
111
  let body: { type?: string; name?: string; value?: string };
112
112
  try {
@@ -192,9 +192,7 @@ export async function handleAddSecret(
192
192
  500,
193
193
  );
194
194
  }
195
- clearEmbeddingBackendCache();
196
- invalidateConfigCache();
197
- await initializeProviders(getConfig());
195
+ await refreshProvidersAfterSecretChange(deps);
198
196
  log.info({ provider: name }, "API key updated via HTTP");
199
197
  return Response.json({ success: true, type, name }, { status: 201 });
200
198
  }
@@ -275,12 +273,12 @@ export async function handleAddSecret(
275
273
  }
276
274
  }
277
275
  if (isManagedProxyCredential(service, field)) {
278
- await initializeProviders(getConfig());
276
+ await refreshProvidersAfterSecretChange(deps);
279
277
  if (service === "vellum" && field === "assistant_api_key") {
280
278
  // Push the API key to CES so managed credential materialization
281
279
  // works even though the handshake ran before the key was available.
282
280
  const generation = ++apiKeyGeneration;
283
- const cesClient = getCesClient?.();
281
+ const cesClient = deps?.getCesClient?.();
284
282
  if (cesClient) {
285
283
  if (cesClient.isReady()) {
286
284
  try {
@@ -394,7 +392,10 @@ export async function handleReadSecret(req: Request): Promise<Response> {
394
392
  }
395
393
  }
396
394
 
397
- export async function handleDeleteSecret(req: Request): Promise<Response> {
395
+ export async function handleDeleteSecret(
396
+ req: Request,
397
+ deps?: SecretRouteDeps,
398
+ ): Promise<Response> {
398
399
  let body: { type?: string; name?: string };
399
400
  try {
400
401
  body = (await req.json()) as { type?: string; name?: string };
@@ -438,9 +439,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
438
439
  500,
439
440
  );
440
441
  }
441
- clearEmbeddingBackendCache();
442
- invalidateConfigCache();
443
- await initializeProviders(getConfig());
442
+ await refreshProvidersAfterSecretChange(deps);
444
443
  log.info({ provider: name }, "API key deleted via HTTP");
445
444
  return Response.json({ success: true, type, name });
446
445
  }
@@ -488,7 +487,7 @@ export async function handleDeleteSecret(req: Request): Promise<Response> {
488
487
  setSentryUserId(undefined);
489
488
  }
490
489
  if (isManagedProxyCredential(service, field)) {
491
- await initializeProviders(getConfig());
490
+ await refreshProvidersAfterSecretChange(deps);
492
491
  }
493
492
  log.info({ service, field }, "Credential deleted via HTTP");
494
493
  return Response.json({ success: true, type, name });
@@ -548,6 +547,30 @@ export async function handleListSecrets(): Promise<Response> {
548
547
  export interface SecretRouteDeps {
549
548
  /** Accessor for the CES client, used to push API key updates after hatch. */
550
549
  getCesClient?: () => CesClient | undefined;
550
+ /**
551
+ * Called after provider-affecting credentials change so live conversations
552
+ * can be reloaded with fresh provider instances.
553
+ */
554
+ onProviderCredentialsChanged?: () => void | Promise<void>;
555
+ }
556
+
557
+ async function refreshProvidersAfterSecretChange(
558
+ deps?: SecretRouteDeps,
559
+ ): Promise<void> {
560
+ clearEmbeddingBackendCache();
561
+ invalidateConfigCache();
562
+ await initializeProviders(getConfig());
563
+
564
+ if (!deps?.onProviderCredentialsChanged) return;
565
+
566
+ try {
567
+ await deps.onProviderCredentialsChanged();
568
+ } catch (err) {
569
+ log.warn(
570
+ { error: err instanceof Error ? err.message : String(err) },
571
+ "Failed to refresh live conversations after provider credential change",
572
+ );
573
+ }
551
574
  }
552
575
 
553
576
  export function secretRouteDefinitions(
@@ -557,7 +580,7 @@ export function secretRouteDefinitions(
557
580
  {
558
581
  endpoint: "secrets",
559
582
  method: "POST",
560
- handler: async ({ req }) => handleAddSecret(req, deps?.getCesClient),
583
+ handler: async ({ req }) => handleAddSecret(req, deps),
561
584
  summary: "Add a secret",
562
585
  description:
563
586
  "Store a new secret (API key, OAuth token, etc.) in the credential vault.",
@@ -576,7 +599,7 @@ export function secretRouteDefinitions(
576
599
  {
577
600
  endpoint: "secrets",
578
601
  method: "DELETE",
579
- handler: async ({ req }) => handleDeleteSecret(req),
602
+ handler: async ({ req }) => handleDeleteSecret(req, deps),
580
603
  summary: "Delete a secret",
581
604
  description: "Remove a secret from the credential vault by name.",
582
605
  tags: ["secrets"],
@@ -30,10 +30,6 @@ import {
30
30
  getMostRecentAppByProvider,
31
31
  getProvider,
32
32
  } from "../../oauth/oauth-store.js";
33
- import {
34
- getProviderBehavior,
35
- resolveService,
36
- } from "../../oauth/provider-behaviors.js";
37
33
  import {
38
34
  check,
39
35
  classifyRisk,
@@ -170,14 +166,14 @@ async function handleOAuthConnectStart(body: {
170
166
  return httpError("BAD_REQUEST", "Missing required field: service", 400);
171
167
  }
172
168
 
173
- const resolvedService = resolveService(body.service);
169
+ const service = body.service;
174
170
 
175
171
  // Resolve client_id and client_secret from oauth-store.
176
172
  let clientId: string | undefined;
177
173
  let clientSecret: string | undefined;
178
174
 
179
175
  // Try existing connection first (re-auth flow)
180
- const conn = getConnectionByProvider(resolvedService);
176
+ const conn = getConnectionByProvider(service);
181
177
  if (conn) {
182
178
  const app = getApp(conn.oauthAppId);
183
179
  if (app) {
@@ -188,7 +184,7 @@ async function handleOAuthConnectStart(body: {
188
184
 
189
185
  // Fall back to most recent app for this provider (first-time connect with stored app)
190
186
  if (!clientId) {
191
- const dbApp = getMostRecentAppByProvider(resolvedService);
187
+ const dbApp = getMostRecentAppByProvider(service);
192
188
  if (dbApp) {
193
189
  clientId = dbApp.clientId;
194
190
  if (!clientSecret) {
@@ -202,20 +198,17 @@ async function handleOAuthConnectStart(body: {
202
198
  if (!clientId) {
203
199
  return httpError(
204
200
  "BAD_REQUEST",
205
- `No client_id found for "${body.service}". Store it first via the credential vault.`,
201
+ `No client_id found for "${service}". Store it first via the credential vault.`,
206
202
  400,
207
203
  );
208
204
  }
209
205
 
210
- const behavior = getProviderBehavior(resolvedService);
211
- const providerRow = getProvider(resolvedService);
212
- const requiresSecret =
213
- behavior?.setup?.requiresClientSecret ??
214
- !!(providerRow?.tokenEndpointAuthMethod || providerRow?.extraParams);
206
+ const providerRow = getProvider(service);
207
+ const requiresSecret = !!providerRow?.requiresClientSecret;
215
208
  if (requiresSecret && !clientSecret) {
216
209
  return httpError(
217
210
  "BAD_REQUEST",
218
- `client_secret is required for "${body.service}" but not found in the credential store. Store it first via the credential vault.`,
211
+ `client_secret is required for "${service}" but not found in the credential store. Store it first via the credential vault.`,
219
212
  400,
220
213
  );
221
214
  }
@@ -226,7 +219,7 @@ async function handleOAuthConnectStart(body: {
226
219
  let authUrl: string | undefined;
227
220
 
228
221
  const result = await orchestrateOAuthConnect({
229
- service: body.service,
222
+ service,
230
223
  requestedScopes: body.requestedScopes,
231
224
  clientId,
232
225
  clientSecret,
@@ -238,7 +231,7 @@ async function handleOAuthConnectStart(body: {
238
231
  // Prefer accountInfo from oauth-store when available.
239
232
  let accountInfo = deferredResult.accountInfo;
240
233
  try {
241
- const conn = getConnectionByProvider(resolvedService);
234
+ const conn = getConnectionByProvider(service);
242
235
  if (conn?.accountInfo) accountInfo = conn.accountInfo;
243
236
  } catch {
244
237
  // DB not ready — use orchestrator value
@@ -277,7 +270,7 @@ async function handleOAuthConnectStart(body: {
277
270
 
278
271
  if (!result.success) {
279
272
  log.error(
280
- { err: result.error, service: body.service },
273
+ { err: result.error, service },
281
274
  "OAuth connect orchestrator returned error",
282
275
  );
283
276
  return httpError(
@@ -298,7 +291,7 @@ async function handleOAuthConnectStart(body: {
298
291
  // Prefer accountInfo from oauth-store when available.
299
292
  let responseAccountInfo = result.accountInfo;
300
293
  try {
301
- const conn = getConnectionByProvider(resolvedService);
294
+ const conn = getConnectionByProvider(service);
302
295
  if (conn?.accountInfo) responseAccountInfo = conn.accountInfo;
303
296
  } catch {
304
297
  // DB not ready — use orchestrator value
@@ -312,7 +305,7 @@ async function handleOAuthConnectStart(body: {
312
305
  });
313
306
  } catch (err) {
314
307
  const message = err instanceof Error ? err.message : String(err);
315
- log.error({ err, service: body.service }, "OAuth connect flow failed");
308
+ log.error({ err, service }, "OAuth connect flow failed");
316
309
  return httpError("INTERNAL_ERROR", sanitizeOAuthError(message), 500);
317
310
  }
318
311
  }
@@ -23,6 +23,7 @@ import {
23
23
  inspectSkill,
24
24
  installSkill,
25
25
  listSkills,
26
+ listSkillsWithCatalog,
26
27
  searchSkills,
27
28
  uninstallSkill,
28
29
  updateSkill,
@@ -47,13 +48,53 @@ export function skillRouteDefinitions(deps: SkillRouteDeps): RouteDefinition[] {
47
48
  method: "GET",
48
49
  policyKey: "skills",
49
50
  summary: "List all skills",
50
- description: "Return all installed skills.",
51
+ description:
52
+ "Return all installed skills. Pass ?include=catalog to also include available catalog skills.",
51
53
  tags: ["skills"],
54
+ queryParams: [
55
+ {
56
+ name: "include",
57
+ schema: { type: "string", enum: ["catalog"] },
58
+ description:
59
+ "Optional inclusion flag. Use 'catalog' to merge available Vellum catalog skills into the response.",
60
+ },
61
+ ],
52
62
  responseBody: z.object({
53
- skills: z.array(z.unknown()).describe("Skill objects"),
63
+ skills: z
64
+ .array(
65
+ z.object({
66
+ id: z.string(),
67
+ name: z.string(),
68
+ description: z.string(),
69
+ emoji: z.string().optional(),
70
+ homepage: z.string().optional(),
71
+ source: z.enum([
72
+ "bundled",
73
+ "managed",
74
+ "workspace",
75
+ "clawhub",
76
+ "extra",
77
+ "catalog",
78
+ ]),
79
+ state: z.enum(["enabled", "disabled"]),
80
+ installStatus: z.enum(["bundled", "installed", "available"]),
81
+ updateAvailable: z.boolean(),
82
+ provenance: z.object({
83
+ kind: z.enum(["first-party", "third-party", "local"]),
84
+ provider: z.string().optional(),
85
+ originId: z.string().optional(),
86
+ sourceUrl: z.string().optional(),
87
+ }),
88
+ }),
89
+ )
90
+ .describe("Skill objects"),
54
91
  }),
55
- handler: () => {
56
- const skills = listSkills(ctx());
92
+ handler: async ({ url }) => {
93
+ const include = url.searchParams.get("include");
94
+ const skills =
95
+ include === "catalog"
96
+ ? await listSkillsWithCatalog(ctx())
97
+ : listSkills(ctx());
57
98
  return Response.json({ skills });
58
99
  },
59
100
  },
@@ -15,12 +15,12 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
15
15
  {
16
16
  name: "Gmail",
17
17
  category: "email",
18
- isConnected: () => isProviderConnected("integration:google"),
18
+ isConnected: () => isProviderConnected("google"),
19
19
  },
20
20
  {
21
21
  name: "Slack",
22
22
  category: "messaging",
23
- isConnected: () => isProviderConnected("integration:slack"),
23
+ isConnected: () => isProviderConnected("slack"),
24
24
  },
25
25
  {
26
26
  name: "Twilio",
@@ -30,11 +30,13 @@ export class CesRpcCredentialBackend implements CredentialBackend {
30
30
  }
31
31
 
32
32
  async get(account: string): Promise<CredentialGetResult> {
33
+ if (!this.isAvailable()) {
34
+ return { value: undefined, unreachable: true };
35
+ }
33
36
  try {
34
- const result = await this.client.call(
35
- CesRpcMethod.GetCredential,
36
- { account },
37
- );
37
+ const result = await this.client.call(CesRpcMethod.GetCredential, {
38
+ account,
39
+ });
38
40
  return {
39
41
  value: result.found ? result.value : undefined,
40
42
  unreachable: false,
@@ -46,11 +48,12 @@ export class CesRpcCredentialBackend implements CredentialBackend {
46
48
  }
47
49
 
48
50
  async set(account: string, value: string): Promise<boolean> {
51
+ if (!this.isAvailable()) return false;
49
52
  try {
50
- const result = await this.client.call(
51
- CesRpcMethod.SetCredential,
52
- { account, value },
53
- );
53
+ const result = await this.client.call(CesRpcMethod.SetCredential, {
54
+ account,
55
+ value,
56
+ });
54
57
  return result.ok;
55
58
  } catch (err) {
56
59
  log.warn({ err, account }, "CES RPC credential set failed");
@@ -59,11 +62,11 @@ export class CesRpcCredentialBackend implements CredentialBackend {
59
62
  }
60
63
 
61
64
  async delete(account: string): Promise<DeleteResult> {
65
+ if (!this.isAvailable()) return "error";
62
66
  try {
63
- const result = await this.client.call(
64
- CesRpcMethod.DeleteCredential,
65
- { account },
66
- );
67
+ const result = await this.client.call(CesRpcMethod.DeleteCredential, {
68
+ account,
69
+ });
67
70
  return result.result;
68
71
  } catch (err) {
69
72
  log.warn({ err, account }, "CES RPC credential delete failed");
@@ -72,11 +75,11 @@ export class CesRpcCredentialBackend implements CredentialBackend {
72
75
  }
73
76
 
74
77
  async list(): Promise<CredentialListResult> {
78
+ if (!this.isAvailable()) {
79
+ return { accounts: [], unreachable: true };
80
+ }
75
81
  try {
76
- const result = await this.client.call(
77
- CesRpcMethod.ListCredentials,
78
- {},
79
- );
82
+ const result = await this.client.call(CesRpcMethod.ListCredentials, {});
80
83
  return { accounts: result.accounts, unreachable: false };
81
84
  } catch (err) {
82
85
  log.warn({ err }, "CES RPC credential list failed");
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Shared HTML rendering for OAuth completion pages shown in the browser
3
+ * after a loopback/redirect OAuth flow completes.
4
+ */
5
+
6
+ export function escapeHtml(s: string): string {
7
+ return s
8
+ .replace(/&/g, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/"/g, "&quot;")
12
+ .replace(/'/g, "&#39;");
13
+ }
14
+
15
+ function formatProviderName(provider: string): string {
16
+ // Capitalize first letter of each word, handle common acronyms
17
+ return provider
18
+ .split(/[-_]/)
19
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
20
+ .join(" ");
21
+ }
22
+
23
+ export function renderOAuthCompletionPage(
24
+ message: string,
25
+ success: boolean,
26
+ provider?: string,
27
+ ): string {
28
+ const displayProvider = provider ? formatProviderName(provider) : "";
29
+ const title = success
30
+ ? displayProvider
31
+ ? `Connected to ${escapeHtml(displayProvider)}`
32
+ : "Authorization Successful"
33
+ : "Authorization Failed";
34
+ const subtitle = success
35
+ ? "You can close this tab and return to your assistant."
36
+ : escapeHtml(message);
37
+
38
+ const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
39
+ <circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
40
+ <path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
41
+ </svg>`;
42
+
43
+ const errorSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
44
+ <circle cx="28" cy="28" r="28" fill="var(--negative-bg)"/>
45
+ <path class="cross cross-1" d="M20 20L36 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
46
+ <path class="cross cross-2" d="M36 20L20 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
47
+ </svg>`;
48
+
49
+ return `<!DOCTYPE html>
50
+ <html lang="en">
51
+ <head>
52
+ <meta charset="utf-8">
53
+ <meta name="viewport" content="width=device-width, initial-scale=1">
54
+ <title>${escapeHtml(title)}</title>
55
+ <style>
56
+ :root {
57
+ --surface: #F5F3EB;
58
+ --surface-card: #FFFFFF;
59
+ --card-border: #E8E6DA;
60
+ --text-primary: #2A2A28;
61
+ --text-secondary: #4A4A46;
62
+ --text-tertiary: #A1A096;
63
+ --positive-bg: #D4DFD0;
64
+ --positive-fg: #516748;
65
+ --negative-bg: #F7DAC9;
66
+ --negative-fg: #DA491A;
67
+ --shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
68
+ --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
69
+ }
70
+ @media (prefers-color-scheme: dark) {
71
+ :root {
72
+ --surface: #1A1A18;
73
+ --surface-card: #2A2A28;
74
+ --card-border: #3A3A37;
75
+ --text-primary: #F5F3EB;
76
+ --text-secondary: #BDB9A9;
77
+ --text-tertiary: #6B6B65;
78
+ --positive-bg: #1A2316;
79
+ --positive-fg: #7A8B6F;
80
+ --negative-bg: #4E281D;
81
+ --negative-fg: #E86B40;
82
+ --shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
83
+ }
84
+ }
85
+ * { margin: 0; padding: 0; box-sizing: border-box; }
86
+ body {
87
+ font-family: var(--font);
88
+ background: var(--surface);
89
+ color: var(--text-primary);
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: center;
93
+ min-height: 100vh;
94
+ -webkit-font-smoothing: antialiased;
95
+ }
96
+ .card {
97
+ text-align: center;
98
+ padding: 48px 40px 40px;
99
+ background: var(--surface-card);
100
+ border: 1px solid var(--card-border);
101
+ border-radius: 16px;
102
+ box-shadow: var(--shadow);
103
+ max-width: 380px;
104
+ width: 100%;
105
+ opacity: 0;
106
+ transform: translateY(8px) scale(0.98);
107
+ animation: cardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
108
+ }
109
+ @keyframes cardIn {
110
+ to { opacity: 1; transform: translateY(0) scale(1); }
111
+ }
112
+ .icon {
113
+ width: 56px;
114
+ height: 56px;
115
+ margin-bottom: 20px;
116
+ }
117
+ .check {
118
+ stroke-dasharray: 32;
119
+ stroke-dashoffset: 32;
120
+ animation: draw 0.4s ease-out 0.45s forwards;
121
+ }
122
+ .cross {
123
+ stroke-dasharray: 22;
124
+ stroke-dashoffset: 22;
125
+ }
126
+ .cross-1 { animation: draw 0.3s ease-out 0.45s forwards; }
127
+ .cross-2 { animation: draw 0.3s ease-out 0.55s forwards; }
128
+ @keyframes draw {
129
+ to { stroke-dashoffset: 0; }
130
+ }
131
+ h1 {
132
+ font-size: 18px;
133
+ font-weight: 600;
134
+ letter-spacing: -0.2px;
135
+ color: var(--text-primary);
136
+ margin-bottom: 6px;
137
+ }
138
+ p {
139
+ font-size: 13px;
140
+ line-height: 1.5;
141
+ color: var(--text-secondary);
142
+ }
143
+ </style>
144
+ </head>
145
+ <body>
146
+ <div class="card">
147
+ ${success ? checkmarkSvg : errorSvg}
148
+ <h1>${escapeHtml(title)}</h1>
149
+ <p>${subtitle}</p>
150
+ </div>
151
+ </body>
152
+ </html>`;
153
+ }
@@ -20,6 +20,7 @@ import { createHash, randomBytes } from "node:crypto";
20
20
  import { createServer, type Server } from "node:http";
21
21
 
22
22
  import { getLogger } from "../util/logger.js";
23
+ import { renderOAuthCompletionPage as renderLoopbackPage } from "./oauth-completion-page.js";
23
24
 
24
25
  const log = getLogger("oauth2");
25
26
 
@@ -359,7 +360,7 @@ function startLoopbackServerAndWaitForCode(
359
360
  res.writeHead(200, { "Content-Type": "text/html" });
360
361
  res.end(
361
362
  renderLoopbackPage(
362
- "Authorization successful! You can close this tab.",
363
+ "You can close this tab and return to your assistant.",
363
364
  true,
364
365
  ),
365
366
  );
@@ -433,21 +434,6 @@ function startLoopbackServerAndWaitForCode(
433
434
  });
434
435
  }
435
436
 
436
- function escapeHtml(s: string): string {
437
- return s
438
- .replace(/&/g, "&amp;")
439
- .replace(/</g, "&lt;")
440
- .replace(/>/g, "&gt;")
441
- .replace(/"/g, "&quot;")
442
- .replace(/'/g, "&#39;");
443
- }
444
-
445
- function renderLoopbackPage(message: string, success: boolean): string {
446
- const title = success ? "Authorization Successful" : "Authorization Failed";
447
- const color = success ? "#4CAF50" : "#f44336";
448
- return `<!DOCTYPE html><html><head><title>${escapeHtml(title)}</title><style>body{font-family:system-ui,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#f5f5f5}div{text-align:center;padding:2rem;background:white;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}h1{color:${color}}</style></head><body><div><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></div></body></html>`;
449
- }
450
-
451
437
  // ---------------------------------------------------------------------------
452
438
  // Public API
453
439
  // ---------------------------------------------------------------------------
@@ -640,7 +626,7 @@ function startLoopbackServerForPreparedFlow(
640
626
  res.writeHead(200, { "Content-Type": "text/html" });
641
627
  res.end(
642
628
  renderLoopbackPage(
643
- "Authorization successful! You can close this tab.",
629
+ "You can close this tab and return to your assistant.",
644
630
  true,
645
631
  ),
646
632
  );