@vellumai/assistant 0.4.49 → 0.4.50

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 (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. package/src/runtime/routes/mcp-routes.ts +0 -20
@@ -1,18 +1,30 @@
1
1
  import type { Command } from "commander";
2
2
 
3
+ import { orchestrateOAuthConnect } from "../../../oauth/connect-orchestrator.js";
3
4
  import {
4
5
  disconnectOAuthProvider,
6
+ getAppByProviderAndClientId,
5
7
  getConnection,
6
8
  getConnectionByProvider,
9
+ getMostRecentAppByProvider,
10
+ getProvider,
7
11
  listConnections,
8
12
  } from "../../../oauth/oauth-store.js";
13
+ import {
14
+ getProviderBehavior,
15
+ resolveService,
16
+ } from "../../../oauth/provider-behaviors.js";
9
17
  import { credentialKey } from "../../../security/credential-key.js";
10
- import { deleteSecureKeyAsync } from "../../../security/secure-keys.js";
18
+ import {
19
+ deleteSecureKeyAsync,
20
+ getSecureKey,
21
+ } from "../../../security/secure-keys.js";
11
22
  import { withValidToken } from "../../../security/token-manager.js";
12
23
  import {
13
24
  assertMetadataWritable,
14
25
  deleteCredentialMetadata,
15
26
  } from "../../../tools/credentials/metadata-store.js";
27
+ import { isLinux, isMacOS } from "../../../util/platform.js";
16
28
  import { getCliLogger } from "../../logger.js";
17
29
  import { shouldOutputJson, writeOutput } from "../../output.js";
18
30
 
@@ -74,9 +86,14 @@ token expiry, refresh token availability, account info, and status.
74
86
  Examples:
75
87
  $ assistant oauth connections list
76
88
  $ assistant oauth connections list --provider integration:gmail
89
+ $ assistant oauth connections list --client-id abc123
77
90
  $ assistant oauth connections get --id <uuid>
78
91
  $ assistant oauth connections get --provider integration:gmail
79
- $ assistant oauth connections token integration:twitter`,
92
+ $ assistant oauth connections get --provider integration:gmail --client-id abc123
93
+ $ assistant oauth connections token integration:twitter
94
+ $ assistant oauth connections ping integration:gmail
95
+ $ assistant oauth connections connect integration:gmail
96
+ $ assistant oauth connections connect integration:gmail --open-browser`,
80
97
  );
81
98
 
82
99
  // ---------------------------------------------------------------------------
@@ -90,21 +107,25 @@ Examples:
90
107
  "--provider <key>",
91
108
  "Filter by provider key (e.g. integration:gmail)",
92
109
  )
110
+ .option("--client-id <id>", "Filter by OAuth client ID")
93
111
  .addHelpText(
94
112
  "after",
95
113
  `
96
- Lists all OAuth connections, optionally filtered by provider key.
114
+ Lists all OAuth connections, optionally filtered by provider key and/or client ID.
97
115
 
98
116
  Each connection shows its ID, provider, account info, granted scopes, token
99
117
  expiry, refresh token availability, and status.
100
118
 
101
119
  Examples:
102
120
  $ assistant oauth connections list
103
- $ assistant oauth connections list --provider integration:gmail`,
121
+ $ assistant oauth connections list --provider integration:gmail
122
+ $ assistant oauth connections list --client-id abc123`,
104
123
  )
105
- .action((opts: { provider?: string }, cmd: Command) => {
124
+ .action((opts: { provider?: string; clientId?: string }, cmd: Command) => {
106
125
  try {
107
- const rows = listConnections(opts.provider).map(formatConnectionRow);
126
+ const rows = listConnections(opts.provider, opts.clientId).map(
127
+ formatConnectionRow,
128
+ );
108
129
 
109
130
  if (!shouldOutputJson(cmd)) {
110
131
  log.info(`Found ${rows.length} connection(s)`);
@@ -130,6 +151,10 @@ Examples:
130
151
  "--provider <key>",
131
152
  "Provider key (returns most recent active connection)",
132
153
  )
154
+ .option(
155
+ "--client-id <id>",
156
+ "Filter by OAuth client ID (used with --provider)",
157
+ )
133
158
  .addHelpText(
134
159
  "after",
135
160
  `
@@ -140,39 +165,45 @@ Two lookup modes are supported:
140
165
 
141
166
  2. By provider (returns the most recent active connection):
142
167
  $ assistant oauth connections get --provider integration:gmail
168
+ $ assistant oauth connections get --provider integration:gmail --client-id abc123
143
169
 
144
170
  At least --id or --provider must be specified.`,
145
171
  )
146
- .action((opts: { id?: string; provider?: string }, cmd: Command) => {
147
- try {
148
- let row;
149
-
150
- if (opts.id) {
151
- row = getConnection(opts.id);
152
- } else if (opts.provider) {
153
- row = getConnectionByProvider(opts.provider);
154
- } else {
155
- writeOutput(cmd, {
156
- ok: false,
157
- error: "Provide --id or --provider",
158
- });
159
- process.exitCode = 1;
160
- return;
161
- }
162
-
163
- if (!row) {
164
- writeOutput(cmd, { ok: false, error: "Connection not found" });
172
+ .action(
173
+ (
174
+ opts: { id?: string; provider?: string; clientId?: string },
175
+ cmd: Command,
176
+ ) => {
177
+ try {
178
+ let row;
179
+
180
+ if (opts.id) {
181
+ row = getConnection(opts.id);
182
+ } else if (opts.provider) {
183
+ row = getConnectionByProvider(opts.provider, opts.clientId);
184
+ } else {
185
+ writeOutput(cmd, {
186
+ ok: false,
187
+ error: "Provide --id or --provider",
188
+ });
189
+ process.exitCode = 1;
190
+ return;
191
+ }
192
+
193
+ if (!row) {
194
+ writeOutput(cmd, { ok: false, error: "Connection not found" });
195
+ process.exitCode = 1;
196
+ return;
197
+ }
198
+
199
+ writeOutput(cmd, formatConnectionRow(row));
200
+ } catch (err) {
201
+ const message = err instanceof Error ? err.message : String(err);
202
+ writeOutput(cmd, { ok: false, error: message });
165
203
  process.exitCode = 1;
166
- return;
167
204
  }
168
-
169
- writeOutput(cmd, formatConnectionRow(row));
170
- } catch (err) {
171
- const message = err instanceof Error ? err.message : String(err);
172
- writeOutput(cmd, { ok: false, error: message });
173
- process.exitCode = 1;
174
- }
175
- });
205
+ },
206
+ );
176
207
 
177
208
  // ---------------------------------------------------------------------------
178
209
  // connections token <provider-key>
@@ -183,6 +214,10 @@ At least --id or --provider must be specified.`,
183
214
  .description(
184
215
  "Print a valid OAuth access token for a provider, refreshing if expired",
185
216
  )
217
+ .option(
218
+ "--client-id <id>",
219
+ "Filter by OAuth client ID when multiple apps exist for the provider",
220
+ )
186
221
  .addHelpText(
187
222
  "after",
188
223
  `
@@ -199,22 +234,135 @@ Exits with code 1 if no access token exists or refresh fails.
199
234
 
200
235
  Examples:
201
236
  $ assistant oauth connections token integration:twitter
202
- $ assistant oauth connections token integration:gmail --json`,
237
+ $ assistant oauth connections token integration:gmail --json
238
+ $ assistant oauth connections token integration:gmail --client-id abc123`,
203
239
  )
204
- .action(async (providerKey: string, _opts: unknown, cmd: Command) => {
205
- try {
206
- const token = await withValidToken(providerKey, async (t) => t);
207
- if (shouldOutputJson(cmd)) {
208
- writeOutput(cmd, { ok: true, token });
209
- } else {
210
- process.stdout.write(token + "\n");
240
+ .action(
241
+ async (
242
+ providerKey: string,
243
+ opts: { clientId?: string },
244
+ cmd: Command,
245
+ ) => {
246
+ try {
247
+ const token = await withValidToken(
248
+ providerKey,
249
+ async (t) => t,
250
+ opts.clientId,
251
+ );
252
+ if (shouldOutputJson(cmd)) {
253
+ writeOutput(cmd, { ok: true, token });
254
+ } else {
255
+ process.stdout.write(token + "\n");
256
+ }
257
+ } catch (err) {
258
+ const message = err instanceof Error ? err.message : String(err);
259
+ writeOutput(cmd, { ok: false, error: message });
260
+ process.exitCode = 1;
211
261
  }
212
- } catch (err) {
213
- const message = err instanceof Error ? err.message : String(err);
214
- writeOutput(cmd, { ok: false, error: message });
215
- process.exitCode = 1;
216
- }
217
- });
262
+ },
263
+ );
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // connections ping <provider-key>
267
+ // ---------------------------------------------------------------------------
268
+
269
+ connections
270
+ .command("ping <provider-key>")
271
+ .description(
272
+ "Verify that a stored OAuth token is still valid by hitting the provider's health-check endpoint",
273
+ )
274
+ .option(
275
+ "--client-id <id>",
276
+ "Filter by OAuth client ID when multiple apps exist for the provider",
277
+ )
278
+ .addHelpText(
279
+ "after",
280
+ `
281
+ Arguments:
282
+ provider-key Provider key (e.g. integration:gmail, integration:twitter)
283
+
284
+ Fetches a valid access token (refreshing if needed) and sends a GET request
285
+ to the provider's configured ping URL. Reports success (HTTP 2xx) or failure.
286
+
287
+ The ping URL is set per-provider in seed data or via "providers register --ping-url".
288
+ If no ping URL is configured for the provider, exits with an error.
289
+
290
+ Examples:
291
+ $ assistant oauth connections ping integration:gmail
292
+ $ assistant oauth connections ping integration:twitter --json
293
+ $ assistant oauth connections ping integration:gmail --client-id abc123`,
294
+ )
295
+ .action(
296
+ async (
297
+ providerKey: string,
298
+ opts: { clientId?: string },
299
+ cmd: Command,
300
+ ) => {
301
+ try {
302
+ const provider = getProvider(providerKey);
303
+ if (!provider) {
304
+ writeOutput(cmd, {
305
+ ok: false,
306
+ error: `Provider not found: ${providerKey}`,
307
+ });
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+
312
+ if (!provider.pingUrl) {
313
+ writeOutput(cmd, {
314
+ ok: false,
315
+ error: `No ping URL configured for "${providerKey}"`,
316
+ });
317
+ process.exitCode = 1;
318
+ return;
319
+ }
320
+
321
+ const pingUrl = provider.pingUrl;
322
+
323
+ const result = await withValidToken(
324
+ providerKey,
325
+ async (token) => {
326
+ const res = await fetch(pingUrl, {
327
+ method: "GET",
328
+ headers: { Authorization: `Bearer ${token}` },
329
+ });
330
+ return { status: res.status, ok: res.ok };
331
+ },
332
+ opts.clientId,
333
+ );
334
+
335
+ if (result.ok) {
336
+ if (shouldOutputJson(cmd)) {
337
+ writeOutput(cmd, {
338
+ ok: true,
339
+ provider: providerKey,
340
+ status: result.status,
341
+ });
342
+ } else {
343
+ log.info(`${providerKey}: OK (HTTP ${result.status})`);
344
+ writeOutput(cmd, {
345
+ ok: true,
346
+ provider: providerKey,
347
+ status: result.status,
348
+ });
349
+ }
350
+ } else {
351
+ writeOutput(cmd, {
352
+ ok: false,
353
+ provider: providerKey,
354
+ status: result.status,
355
+ error: `Ping failed with HTTP ${result.status}`,
356
+ });
357
+ process.exitCode = 1;
358
+ }
359
+ } catch (err) {
360
+ const message = err instanceof Error ? err.message : String(err);
361
+ writeOutput(cmd, { ok: false, error: message });
362
+ process.exitCode = 1;
363
+ }
364
+ },
365
+ );
218
366
 
219
367
  // ---------------------------------------------------------------------------
220
368
  // connections disconnect <provider-key>
@@ -225,6 +373,10 @@ Examples:
225
373
  .description(
226
374
  "Disconnect an OAuth integration and remove all associated credentials",
227
375
  )
376
+ .option(
377
+ "--client-id <id>",
378
+ "Filter by OAuth client ID when multiple apps exist for the provider",
379
+ )
228
380
  .addHelpText(
229
381
  "after",
230
382
  `
@@ -240,60 +392,242 @@ client_id, client_secret) are also cleaned up if present.
240
392
 
241
393
  Examples:
242
394
  $ assistant oauth connections disconnect integration:gmail
243
- $ assistant oauth connections disconnect integration:slack`,
395
+ $ assistant oauth connections disconnect integration:slack
396
+ $ assistant oauth connections disconnect integration:gmail --client-id abc123`,
244
397
  )
245
- .action(async (providerKey: string, _opts: unknown, cmd: Command) => {
246
- try {
247
- assertMetadataWritable();
248
-
249
- let cleanedUp = false;
250
-
251
- // 1. Disconnect the OAuth connection (new-format keys + connection row)
252
- const oauthResult = await disconnectOAuthProvider(providerKey);
253
- if (oauthResult === "error") {
254
- writeOutput(cmd, {
255
- ok: false,
256
- error: `Failed to disconnect OAuth provider "${providerKey}" please try again`,
257
- });
398
+ .action(
399
+ async (
400
+ providerKey: string,
401
+ opts: { clientId?: string },
402
+ cmd: Command,
403
+ ) => {
404
+ try {
405
+ assertMetadataWritable();
406
+
407
+ let cleanedUp = false;
408
+
409
+ // 1. Disconnect the OAuth connection (new-format keys + connection row)
410
+ const oauthResult = await disconnectOAuthProvider(
411
+ providerKey,
412
+ opts.clientId,
413
+ );
414
+ if (oauthResult === "error") {
415
+ writeOutput(cmd, {
416
+ ok: false,
417
+ error: `Failed to disconnect OAuth provider "${providerKey}" — please try again`,
418
+ });
419
+ process.exitCode = 1;
420
+ return;
421
+ }
422
+ if (oauthResult === "disconnected") cleanedUp = true;
423
+
424
+ // 2. Clean up legacy credential keys for common fields
425
+ const legacyFields = [
426
+ "access_token",
427
+ "refresh_token",
428
+ "client_id",
429
+ "client_secret",
430
+ ];
431
+ for (const field of legacyFields) {
432
+ const key = credentialKey(providerKey, field);
433
+ const result = await deleteSecureKeyAsync(key);
434
+ if (result === "deleted") cleanedUp = true;
435
+
436
+ const metaDeleted = deleteCredentialMetadata(providerKey, field);
437
+ if (metaDeleted) cleanedUp = true;
438
+ }
439
+
440
+ if (!cleanedUp) {
441
+ writeOutput(cmd, {
442
+ ok: false,
443
+ error: `No OAuth connection or credentials found for "${providerKey}"`,
444
+ });
445
+ process.exitCode = 1;
446
+ return;
447
+ }
448
+
449
+ writeOutput(cmd, { ok: true, service: providerKey });
450
+
451
+ if (!shouldOutputJson(cmd)) {
452
+ log.info(`Disconnected ${providerKey}`);
453
+ }
454
+ } catch (err) {
455
+ const message = err instanceof Error ? err.message : String(err);
456
+ writeOutput(cmd, { ok: false, error: message });
258
457
  process.exitCode = 1;
259
- return;
260
- }
261
- if (oauthResult === "disconnected") cleanedUp = true;
262
-
263
- // 2. Clean up legacy credential keys for common fields
264
- const legacyFields = [
265
- "access_token",
266
- "refresh_token",
267
- "client_id",
268
- "client_secret",
269
- ];
270
- for (const field of legacyFields) {
271
- const key = credentialKey(providerKey, field);
272
- const result = await deleteSecureKeyAsync(key);
273
- if (result === "deleted") cleanedUp = true;
274
-
275
- const metaDeleted = deleteCredentialMetadata(providerKey, field);
276
- if (metaDeleted) cleanedUp = true;
277
458
  }
459
+ },
460
+ );
278
461
 
279
- if (!cleanedUp) {
280
- writeOutput(cmd, {
281
- ok: false,
282
- error: `No OAuth connection or credentials found for "${providerKey}"`,
283
- });
284
- process.exitCode = 1;
285
- return;
286
- }
462
+ // ---------------------------------------------------------------------------
463
+ // connections connect <provider-key>
464
+ // ---------------------------------------------------------------------------
287
465
 
288
- writeOutput(cmd, { ok: true, service: providerKey });
466
+ connections
467
+ .command("connect <provider-key>")
468
+ .description("Initiate an OAuth2 authorization flow for a provider")
469
+ .option(
470
+ "--client-id <id>",
471
+ "Filter by OAuth client ID when multiple apps exist for the provider",
472
+ )
473
+ .option(
474
+ "--scopes <scopes...>",
475
+ "Additional scopes beyond the provider's defaults",
476
+ )
477
+ .option(
478
+ "--open-browser",
479
+ "Open the auth URL in the browser and wait for completion",
480
+ )
481
+ .addHelpText(
482
+ "after",
483
+ `
484
+ Arguments:
485
+ provider-key Provider key (e.g. integration:gmail) or alias (e.g. gmail)
289
486
 
290
- if (!shouldOutputJson(cmd)) {
291
- log.info(`Disconnected ${providerKey}`);
487
+ Initiates an OAuth2 authorization flow for the given provider. By default,
488
+ prints the authorization URL to stdout — useful for headless/remote sessions.
489
+ The token exchange completes in the background when the user authorizes.
490
+
491
+ With --open-browser, opens the authorization URL in your browser and waits
492
+ for completion.
493
+
494
+ Client credentials are resolved from the OAuth app store. Use --client-id
495
+ to select a specific app when multiple apps exist for the same provider.
496
+
497
+ Examples:
498
+ $ assistant oauth connections connect integration:gmail
499
+ $ assistant oauth connections connect gmail --open-browser
500
+ $ assistant oauth connections connect integration:slack --client-id abc123
501
+ $ assistant oauth connections connect integration:gmail --scopes calendar.readonly --json`,
502
+ )
503
+ .action(
504
+ async (
505
+ providerKey: string,
506
+ opts: {
507
+ clientId?: string;
508
+ scopes?: string[];
509
+ openBrowser?: boolean;
510
+ },
511
+ cmd: Command,
512
+ ) => {
513
+ try {
514
+ // a. Resolve service alias
515
+ const resolvedServiceKey = resolveService(providerKey);
516
+
517
+ // b. Resolve client credentials from the DB
518
+ const dbApp = opts.clientId
519
+ ? getAppByProviderAndClientId(resolvedServiceKey, opts.clientId)
520
+ : getMostRecentAppByProvider(resolvedServiceKey);
521
+
522
+ let clientId = opts.clientId;
523
+ let clientSecret: string | undefined;
524
+
525
+ if (dbApp) {
526
+ if (!clientId) clientId = dbApp.clientId;
527
+ const storedSecret = getSecureKey(dbApp.clientSecretCredentialPath);
528
+ if (storedSecret) clientSecret = storedSecret;
529
+ }
530
+
531
+ // c. Validate client_id
532
+ if (!clientId) {
533
+ writeOutput(cmd, {
534
+ ok: false,
535
+ error:
536
+ "No client_id found. Provide --client-id or register an app first with 'assistant oauth apps upsert'.",
537
+ });
538
+ process.exitCode = 1;
539
+ return;
540
+ }
541
+
542
+ // d. Check if client_secret is required but missing
543
+ if (clientSecret === undefined) {
544
+ const providerRow = getProvider(resolvedServiceKey);
545
+ const behavior = getProviderBehavior(resolvedServiceKey);
546
+
547
+ const requiresSecret =
548
+ behavior?.setup?.requiresClientSecret ??
549
+ !!(
550
+ providerRow?.tokenEndpointAuthMethod || providerRow?.extraParams
551
+ );
552
+
553
+ if (requiresSecret) {
554
+ writeOutput(cmd, {
555
+ ok: false,
556
+ error: `client_secret is required for ${resolvedServiceKey} but not found. Store it first with 'assistant oauth apps upsert --client-secret'.`,
557
+ });
558
+ process.exitCode = 1;
559
+ return;
560
+ }
561
+ }
562
+
563
+ // e. Call the orchestrator
564
+ const result = await orchestrateOAuthConnect({
565
+ service: providerKey,
566
+ clientId,
567
+ clientSecret,
568
+ isInteractive: !!opts.openBrowser,
569
+ openUrl: opts.openBrowser
570
+ ? (url) => {
571
+ if (isMacOS()) {
572
+ Bun.spawn(["open", url], {
573
+ stdout: "ignore",
574
+ stderr: "ignore",
575
+ });
576
+ } else if (isLinux()) {
577
+ Bun.spawn(["xdg-open", url], {
578
+ stdout: "ignore",
579
+ stderr: "ignore",
580
+ });
581
+ } else {
582
+ // Fallback: print URL for manual opening
583
+ process.stdout.write(
584
+ `Open this URL to authorize:\n\n${url}\n`,
585
+ );
586
+ }
587
+ }
588
+ : undefined,
589
+ ...(opts.scopes ? { requestedScopes: opts.scopes } : {}),
590
+ });
591
+
592
+ // f. Handle results
593
+ if (!result.success) {
594
+ writeOutput(cmd, { ok: false, error: result.error });
595
+ process.exitCode = 1;
596
+ return;
597
+ }
598
+
599
+ if (result.deferred) {
600
+ if (shouldOutputJson(cmd)) {
601
+ writeOutput(cmd, {
602
+ ok: true,
603
+ deferred: true,
604
+ authUrl: result.authUrl,
605
+ service: result.service,
606
+ });
607
+ } else {
608
+ process.stdout.write(
609
+ `Open this URL to authorize:\n\n${result.authUrl}\n\nThe connection will complete automatically once you authorize.\n`,
610
+ );
611
+ }
612
+ return;
613
+ }
614
+
615
+ // Interactive mode completed
616
+ if (shouldOutputJson(cmd)) {
617
+ writeOutput(cmd, {
618
+ ok: true,
619
+ grantedScopes: result.grantedScopes,
620
+ accountInfo: result.accountInfo,
621
+ });
622
+ } else {
623
+ const msg = `Connected to ${resolvedServiceKey}${result.accountInfo ? ` as ${result.accountInfo}` : ""}`;
624
+ process.stdout.write(msg + "\n");
625
+ }
626
+ } catch (err) {
627
+ const message = err instanceof Error ? err.message : String(err);
628
+ writeOutput(cmd, { ok: false, error: message });
629
+ process.exitCode = 1;
292
630
  }
293
- } catch (err) {
294
- const message = err instanceof Error ? err.message : String(err);
295
- writeOutput(cmd, { ok: false, error: message });
296
- process.exitCode = 1;
297
- }
298
- });
631
+ },
632
+ );
299
633
  }
@@ -171,6 +171,10 @@ Examples:
171
171
  }
172
172
  return port;
173
173
  })
174
+ .option(
175
+ "--ping-url <url>",
176
+ "Health-check endpoint URL for token validation",
177
+ )
174
178
  .addHelpText(
175
179
  "after",
176
180
  `
@@ -186,6 +190,9 @@ Arguments (via options):
186
190
  (e.g. "client_secret_post", "client_secret_basic").
187
191
  --callback-transport Transport method for the OAuth callback.
188
192
  --loopback-port Port number for the local loopback callback server (1-65535).
193
+ --ping-url Optional URL for a lightweight health-check endpoint.
194
+ Used by "connections ping" to validate that a stored token
195
+ is still functional (e.g. "https://api.example.com/user").
189
196
 
190
197
  Registers a new OAuth provider configuration in the local store. This is
191
198
  used for custom integrations not covered by the built-in provider seeds.
@@ -200,7 +207,12 @@ Examples:
200
207
  --provider-key integration:my-service \\
201
208
  --auth-url https://my-service.com/auth \\
202
209
  --token-url https://my-service.com/token \\
203
- --scopes read,write --json`,
210
+ --scopes read,write --json
211
+ $ assistant oauth providers register \\
212
+ --provider-key integration:custom-api \\
213
+ --auth-url https://example.com/auth \\
214
+ --token-url https://example.com/token \\
215
+ --ping-url https://example.com/user`,
204
216
  )
205
217
  .action(
206
218
  (
@@ -214,6 +226,7 @@ Examples:
214
226
  tokenAuthMethod?: string;
215
227
  callbackTransport?: string;
216
228
  loopbackPort?: number;
229
+ pingUrl?: string;
217
230
  },
218
231
  cmd: Command,
219
232
  ) => {
@@ -229,6 +242,7 @@ Examples:
229
242
  tokenEndpointAuthMethod: opts.tokenAuthMethod,
230
243
  callbackTransport: opts.callbackTransport,
231
244
  loopbackPort: opts.loopbackPort,
245
+ pingUrl: opts.pingUrl,
232
246
  });
233
247
 
234
248
  writeOutput(cmd, parseProviderRow(row));
@@ -12,7 +12,10 @@ import {
12
12
  getMessages,
13
13
  } from "../../memory/conversation-crud.js";
14
14
  import { listConversations } from "../../memory/conversation-queries.js";
15
- import { selectEmbeddingBackend } from "../../memory/embedding-backend.js";
15
+ import {
16
+ selectEmbeddingBackend,
17
+ SPARSE_EMBEDDING_VERSION,
18
+ } from "../../memory/embedding-backend.js";
16
19
  import { initQdrantClient } from "../../memory/qdrant-client.js";
17
20
  import { timeAgo } from "../../util/time.js";
18
21
  import { initializeDb } from "../db.js";
@@ -226,7 +229,7 @@ Examples:
226
229
  const qdrantUrl = getQdrantUrlEnv() || config.memory.qdrant.url;
227
230
  const embeddingSelection = selectEmbeddingBackend(config);
228
231
  const embeddingModel = embeddingSelection.backend
229
- ? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}`
232
+ ? `${embeddingSelection.backend.provider}:${embeddingSelection.backend.model}:sparse-v${SPARSE_EMBEDDING_VERSION}`
230
233
  : undefined;
231
234
  const qdrant = initQdrantClient({
232
235
  url: qdrantUrl,