@vellumai/assistant 0.4.43 → 0.4.44

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 (88) hide show
  1. package/ARCHITECTURE.md +13 -14
  2. package/README.md +11 -12
  3. package/docs/architecture/integrations.md +75 -93
  4. package/package.json +1 -1
  5. package/src/__tests__/approval-routes-http.test.ts +0 -2
  6. package/src/__tests__/bundled-asset.test.ts +1 -1
  7. package/src/__tests__/checker.test.ts +31 -28
  8. package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -1
  10. package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
  11. package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
  12. package/src/__tests__/onboarding-template-contract.test.ts +0 -10
  13. package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
  14. package/src/__tests__/send-endpoint-busy.test.ts +0 -3
  15. package/src/__tests__/session-confirmation-signals.test.ts +7 -45
  16. package/src/__tests__/starter-task-flow.test.ts +9 -19
  17. package/src/__tests__/system-prompt.test.ts +3 -4
  18. package/src/__tests__/trust-store.test.ts +4 -4
  19. package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
  20. package/src/cli/commands/amazon/index.ts +4 -39
  21. package/src/cli/commands/amazon/session.ts +18 -26
  22. package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
  23. package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
  24. package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
  25. package/src/cli/commands/twitter/index.ts +95 -835
  26. package/src/cli/commands/twitter/oauth-client.ts +1 -35
  27. package/src/cli/commands/twitter/router.ts +70 -115
  28. package/src/cli/commands/twitter/types.ts +30 -0
  29. package/src/cli/reference.ts +2 -2
  30. package/src/config/bundled-skills/amazon/SKILL.md +0 -1
  31. package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
  32. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
  33. package/src/config/bundled-skills/doordash/SKILL.md +0 -1
  34. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
  35. package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
  36. package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
  37. package/src/config/bundled-skills/twitter/SKILL.md +53 -166
  38. package/src/config/feature-flag-registry.json +8 -0
  39. package/src/daemon/handlers/session-history.ts +41 -9
  40. package/src/daemon/lifecycle.ts +4 -17
  41. package/src/daemon/message-types/apps.ts +0 -25
  42. package/src/daemon/message-types/integrations.ts +1 -7
  43. package/src/daemon/message-types/sessions.ts +6 -1
  44. package/src/daemon/message-types/surfaces.ts +2 -0
  45. package/src/daemon/ride-shotgun-handler.ts +33 -1
  46. package/src/daemon/seed-files.ts +3 -27
  47. package/src/daemon/server.ts +2 -18
  48. package/src/daemon/session-agent-loop-handlers.ts +24 -2
  49. package/src/daemon/session-runtime-assembly.ts +0 -7
  50. package/src/daemon/session-surfaces.ts +185 -33
  51. package/src/daemon/session.ts +2 -28
  52. package/src/memory/app-store.ts +0 -18
  53. package/src/memory/schema/infrastructure.ts +0 -8
  54. package/src/permissions/defaults.ts +3 -3
  55. package/src/prompts/system-prompt.ts +4 -5
  56. package/src/prompts/templates/BOOTSTRAP.md +0 -3
  57. package/src/providers/registry.ts +2 -4
  58. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  59. package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
  60. package/src/runtime/auth/route-policy.ts +0 -4
  61. package/src/runtime/auth/scopes.ts +1 -0
  62. package/src/runtime/auth/token-service.ts +1 -1
  63. package/src/runtime/http-types.ts +10 -0
  64. package/src/runtime/middleware/error-handler.ts +14 -1
  65. package/src/runtime/routes/app-management-routes.ts +61 -64
  66. package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
  67. package/src/runtime/routes/brain-graph-routes.ts +4 -42
  68. package/src/runtime/routes/conversation-routes.ts +9 -6
  69. package/src/runtime/routes/diagnostics-routes.ts +91 -14
  70. package/src/runtime/routes/settings-routes.ts +3 -93
  71. package/src/tools/AGENTS.md +38 -0
  72. package/src/tools/apps/executors.ts +0 -6
  73. package/src/tools/document/editor-template.ts +10 -8
  74. package/src/twitter/platform-proxy-client.ts +6 -3
  75. package/src/util/errors.ts +12 -0
  76. package/src/__tests__/home-base-bootstrap.test.ts +0 -84
  77. package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
  78. package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
  79. package/src/cli/commands/twitter/client.ts +0 -989
  80. package/src/cli/commands/twitter/session.ts +0 -121
  81. package/src/home-base/app-link-store.ts +0 -78
  82. package/src/home-base/bootstrap.ts +0 -74
  83. package/src/home-base/prebuilt/brain-graph.html +0 -1483
  84. package/src/home-base/prebuilt/index.html +0 -702
  85. package/src/home-base/prebuilt/seed-metadata.json +0 -21
  86. package/src/home-base/prebuilt/seed.ts +0 -122
  87. package/src/home-base/prebuilt-home-base-updater.ts +0 -36
  88. package/src/util/cookie-session.ts +0 -98
@@ -2,42 +2,17 @@
2
2
  * OAuth-backed Twitter API client.
3
3
  *
4
4
  * Accepts an OAuth2 Bearer token as a parameter and uses it to execute
5
- * Twitter API v2 operations directly, without requiring a browser session.
6
- * Currently supports post and reply; all other operations fall back to the
7
- * browser-based CDP client.
5
+ * Twitter API v2 operations directly (post and reply).
8
6
  */
9
7
 
10
8
  const TWITTER_API_BASE = "https://api.x.com/2";
11
9
 
12
- /** Operations that the OAuth client can handle natively. */
13
- const SUPPORTED_OPERATIONS = new Set(["post", "reply"]);
14
-
15
10
  export interface OAuthPostResult {
16
11
  tweetId: string;
17
12
  text: string;
18
13
  url?: string;
19
14
  }
20
15
 
21
- export interface OAuthOperationError {
22
- message: string;
23
- suggestFallback: boolean;
24
- fallbackPath: "browser";
25
- operation: string;
26
- }
27
-
28
- export class UnsupportedOAuthOperationError extends Error {
29
- public readonly suggestFallback = true;
30
- public readonly fallbackPath = "browser" as const;
31
- public readonly operation: string;
32
- constructor(operation: string) {
33
- super(
34
- `The "${operation}" operation is not available via the OAuth API. Use the browser path instead.`,
35
- );
36
- this.name = "UnsupportedOAuthOperationError";
37
- this.operation = operation;
38
- }
39
- }
40
-
41
16
  /**
42
17
  * Post a tweet (or reply) using OAuth2 Bearer token authentication.
43
18
  *
@@ -83,12 +58,3 @@ export async function oauthPostTweet(
83
58
  export function oauthIsAvailable(oauthToken: string | undefined): boolean {
84
59
  return oauthToken != null && oauthToken.length > 0;
85
60
  }
86
-
87
- /**
88
- * Check whether a given operation is supported via the OAuth API path.
89
- * Only `post` and `reply` are currently supported; everything else
90
- * (timeline, search, bookmarks, etc.) requires the browser path.
91
- */
92
- export function oauthSupportsOperation(operation: string): boolean {
93
- return SUPPORTED_OPERATIONS.has(operation);
94
- }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Strategy router for Twitter operations.
3
- * Selects managed proxy, OAuth, or browser path based on the caller-provided strategy.
2
+ * Mode router for Twitter operations.
3
+ * Selects managed proxy or OAuth path based on the caller-provided integration mode.
4
4
  */
5
5
 
6
6
  import {
@@ -11,47 +11,27 @@ import {
11
11
  searchRecentTweets as managedSearchRecentTweets,
12
12
  TwitterProxyError,
13
13
  } from "../../../twitter/platform-proxy-client.js";
14
- import type { PostTweetResult, TweetEntry, UserInfo } from "./client.js";
15
- import {
16
- getTweetDetail as browserGetTweetDetail,
17
- getUserByScreenName as browserGetUserByScreenName,
18
- getUserTweets as browserGetUserTweets,
19
- postTweet as browserPostTweet,
20
- searchTweets as browserSearchTweets,
21
- SessionExpiredError,
22
- } from "./client.js";
23
- import {
24
- oauthIsAvailable,
25
- oauthPostTweet,
26
- oauthSupportsOperation,
27
- } from "./oauth-client.js";
14
+ import { oauthIsAvailable, oauthPostTweet } from "./oauth-client.js";
15
+ import type { PostTweetResult, TweetEntry, UserInfo } from "./types.js";
28
16
 
29
- export type TwitterStrategy = "oauth" | "browser" | "auto" | "managed";
17
+ export type TwitterMode = "oauth" | "managed";
30
18
 
31
19
  export interface RoutedResult<T> {
32
20
  result: T;
33
- pathUsed: TwitterStrategy;
34
- }
35
-
36
- export interface RoutedError {
37
- message: string;
38
- pathUsed: TwitterStrategy;
39
- suggestAlternative?: TwitterStrategy;
40
- alternativeSetupHint?: string;
21
+ pathUsed: TwitterMode;
41
22
  }
42
23
 
43
24
  export async function routedPostTweet(
44
25
  text: string,
45
26
  opts: {
46
27
  inReplyToTweetId?: string;
47
- strategy: TwitterStrategy;
28
+ mode: TwitterMode;
48
29
  oauthToken?: string;
49
30
  },
50
31
  ): Promise<RoutedResult<PostTweetResult>> {
51
- const strategy = opts.strategy;
52
- const operation = opts.inReplyToTweetId ? "reply" : "post";
32
+ const mode = opts.mode;
53
33
 
54
- if (strategy === "managed") {
34
+ if (mode === "managed") {
55
35
  // Route through platform proxy — the platform holds the OAuth credentials
56
36
  try {
57
37
  const response = await managedPostTweet(text, {
@@ -91,16 +71,15 @@ export async function routedPostTweet(
91
71
  }
92
72
  }
93
73
 
94
- if (strategy === "oauth") {
74
+ if (mode === "oauth") {
95
75
  // User explicitly wants OAuth
96
76
  if (!oauthIsAvailable(opts.oauthToken)) {
97
77
  throw Object.assign(
98
78
  new Error(
99
- "OAuth is not configured. Provide your X developer credentials here in the chat to set up OAuth, or switch to browser strategy: `assistant config set twitter.operationStrategy browser`.",
79
+ "OAuth is not configured. Connect your X developer credentials to set up OAuth.",
100
80
  ),
101
81
  {
102
82
  pathUsed: "oauth" as const,
103
- suggestAlternative: "browser" as const,
104
83
  },
105
84
  );
106
85
  }
@@ -118,68 +97,9 @@ export async function routedPostTweet(
118
97
  };
119
98
  }
120
99
 
121
- if (strategy === "browser") {
122
- // User explicitly wants browser
123
- try {
124
- const result = await browserPostTweet(text, {
125
- inReplyToTweetId: opts.inReplyToTweetId,
126
- });
127
- return { result, pathUsed: "browser" };
128
- } catch (err) {
129
- if (err instanceof SessionExpiredError) {
130
- throw Object.assign(err, {
131
- pathUsed: "browser" as const,
132
- suggestAlternative: "oauth" as const,
133
- });
134
- }
135
- throw err;
136
- }
137
- }
138
-
139
- // auto strategy: try OAuth first if available and supported, fallback to browser
140
- let oauthError: Error | undefined;
141
- if (oauthIsAvailable(opts.oauthToken) && oauthSupportsOperation(operation)) {
142
- try {
143
- const result = await oauthPostTweet(text, {
144
- inReplyToTweetId: opts.inReplyToTweetId,
145
- oauthToken: opts.oauthToken!,
146
- });
147
- return {
148
- result: {
149
- tweetId: result.tweetId,
150
- text: result.text,
151
- url: result.url ?? `https://x.com/i/status/${result.tweetId}`,
152
- },
153
- pathUsed: "oauth",
154
- };
155
- } catch (err) {
156
- oauthError = err instanceof Error ? err : new Error(String(err));
157
- // Fall through to browser
158
- }
159
- }
160
-
161
- // Fallback to browser
162
- try {
163
- const result = await browserPostTweet(text, {
164
- inReplyToTweetId: opts.inReplyToTweetId,
165
- });
166
- return { result, pathUsed: "browser" };
167
- } catch (err) {
168
- if (err instanceof SessionExpiredError) {
169
- throw Object.assign(err, {
170
- pathUsed: "auto" as const,
171
- oauthError: oauthError?.message,
172
- });
173
- }
174
- if (oauthError) {
175
- const browserError = err instanceof Error ? err : new Error(String(err));
176
- throw Object.assign(browserError, {
177
- pathUsed: "auto" as const,
178
- oauthError: oauthError.message,
179
- });
180
- }
181
- throw err;
182
- }
100
+ // Exhaustive check should never reach here
101
+ const _exhaustive: never = mode;
102
+ throw new Error(`Unknown mode: ${_exhaustive}`);
183
103
  }
184
104
 
185
105
  // ---------------------------------------------------------------------------
@@ -188,13 +108,13 @@ export async function routedPostTweet(
188
108
 
189
109
  /**
190
110
  * Look up a user by screen name.
191
- * Managed mode uses GET /2/users/by/username/:username; browser mode uses GraphQL.
111
+ * Managed mode uses GET /2/users/by/username/:username.
192
112
  */
193
113
  export async function routedGetUserByScreenName(
194
114
  screenName: string,
195
- opts: { strategy: TwitterStrategy },
115
+ opts: { mode: TwitterMode },
196
116
  ): Promise<RoutedResult<UserInfo>> {
197
- if (opts.strategy === "managed") {
117
+ if (opts.mode === "managed") {
198
118
  try {
199
119
  const response = await managedGetUserByUsername(screenName, {
200
120
  "user.fields": "id,name,username",
@@ -228,21 +148,29 @@ export async function routedGetUserByScreenName(
228
148
  }
229
149
  }
230
150
 
231
- // Browser path (used for browser, oauth, auto — read operations always go through browser)
232
- const result = await browserGetUserByScreenName(screenName);
233
- return { result, pathUsed: "browser" };
151
+ if (opts.mode === "oauth") {
152
+ throw Object.assign(
153
+ new Error(
154
+ "Read operations are not supported via OAuth. Use managed mode for read access.",
155
+ ),
156
+ { pathUsed: "oauth" as const },
157
+ );
158
+ }
159
+
160
+ const _exhaustive: never = opts.mode;
161
+ throw new Error(`Unknown mode: ${_exhaustive}`);
234
162
  }
235
163
 
236
164
  /**
237
165
  * Fetch a user's recent tweets.
238
- * Managed mode uses GET /2/users/:id/tweets; browser mode uses GraphQL.
166
+ * Managed mode uses GET /2/users/:id/tweets.
239
167
  */
240
168
  export async function routedGetUserTweets(
241
169
  userId: string,
242
170
  count: number,
243
- opts: { strategy: TwitterStrategy },
171
+ opts: { mode: TwitterMode },
244
172
  ): Promise<RoutedResult<TweetEntry[]>> {
245
- if (opts.strategy === "managed") {
173
+ if (opts.mode === "managed") {
246
174
  try {
247
175
  const response = await managedGetUserTweets(userId, {
248
176
  max_results: String(Math.min(count, 100)),
@@ -269,19 +197,28 @@ export async function routedGetUserTweets(
269
197
  }
270
198
  }
271
199
 
272
- const result = await browserGetUserTweets(userId, count);
273
- return { result, pathUsed: "browser" };
200
+ if (opts.mode === "oauth") {
201
+ throw Object.assign(
202
+ new Error(
203
+ "Read operations are not supported via OAuth. Use managed mode for read access.",
204
+ ),
205
+ { pathUsed: "oauth" as const },
206
+ );
207
+ }
208
+
209
+ const _exhaustive: never = opts.mode;
210
+ throw new Error(`Unknown mode: ${_exhaustive}`);
274
211
  }
275
212
 
276
213
  /**
277
214
  * Fetch a single tweet by ID.
278
- * Managed mode uses GET /2/tweets/:id; browser mode uses GraphQL TweetDetail.
215
+ * Managed mode uses GET /2/tweets/:id.
279
216
  */
280
217
  export async function routedGetTweetDetail(
281
218
  tweetId: string,
282
- opts: { strategy: TwitterStrategy },
219
+ opts: { mode: TwitterMode },
283
220
  ): Promise<RoutedResult<TweetEntry[]>> {
284
- if (opts.strategy === "managed") {
221
+ if (opts.mode === "managed") {
285
222
  try {
286
223
  const response = await managedGetTweet(tweetId, {
287
224
  "tweet.fields": "id,text,created_at,author_id,conversation_id",
@@ -340,20 +277,29 @@ export async function routedGetTweetDetail(
340
277
  }
341
278
  }
342
279
 
343
- const result = await browserGetTweetDetail(tweetId);
344
- return { result, pathUsed: "browser" };
280
+ if (opts.mode === "oauth") {
281
+ throw Object.assign(
282
+ new Error(
283
+ "Read operations are not supported via OAuth. Use managed mode for read access.",
284
+ ),
285
+ { pathUsed: "oauth" as const },
286
+ );
287
+ }
288
+
289
+ const _exhaustive: never = opts.mode;
290
+ throw new Error(`Unknown mode: ${_exhaustive}`);
345
291
  }
346
292
 
347
293
  /**
348
294
  * Search tweets.
349
- * Managed mode uses GET /2/tweets/search/recent; browser mode uses GraphQL SearchTimeline.
295
+ * Managed mode uses GET /2/tweets/search/recent.
350
296
  */
351
297
  export async function routedSearchTweets(
352
298
  query: string,
353
299
  product: "Top" | "Latest" | "People" | "Media",
354
- opts: { strategy: TwitterStrategy },
300
+ opts: { mode: TwitterMode },
355
301
  ): Promise<RoutedResult<TweetEntry[]>> {
356
- if (opts.strategy === "managed") {
302
+ if (opts.mode === "managed") {
357
303
  if (product === "People" || product === "Media") {
358
304
  throw Object.assign(
359
305
  new Error(
@@ -391,6 +337,15 @@ export async function routedSearchTweets(
391
337
  }
392
338
  }
393
339
 
394
- const result = await browserSearchTweets(query, product);
395
- return { result, pathUsed: "browser" };
340
+ if (opts.mode === "oauth") {
341
+ throw Object.assign(
342
+ new Error(
343
+ "Read operations are not supported via OAuth. Use managed mode for read access.",
344
+ ),
345
+ { pathUsed: "oauth" as const },
346
+ );
347
+ }
348
+
349
+ const _exhaustive: never = opts.mode;
350
+ throw new Error(`Unknown mode: ${_exhaustive}`);
396
351
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Public types shared across the Twitter CLI module.
3
+ * Extracted from the former browser CDP client.
4
+ */
5
+
6
+ export interface PostTweetResult {
7
+ tweetId: string;
8
+ text: string;
9
+ url: string;
10
+ }
11
+
12
+ export interface UserInfo {
13
+ userId: string;
14
+ screenName: string;
15
+ name: string;
16
+ }
17
+
18
+ export interface TweetEntry {
19
+ tweetId: string;
20
+ text: string;
21
+ url: string;
22
+ createdAt: string;
23
+ }
24
+
25
+ export interface NotificationEntry {
26
+ id: string;
27
+ message: string;
28
+ timestamp: string;
29
+ url?: string;
30
+ }
@@ -24,7 +24,7 @@ Commands:
24
24
  email [options] Email operations (provider-agnostic)
25
25
  contacts [options] Manage and query the contact graph
26
26
  channel-verification-sessions [options] Manage channel verification sessions
27
- amazon [options] Shop on Amazon and Amazon Fresh. Requires a session imported from a Ride Shotgun recording.
27
+ amazon [options] Shop on Amazon and Amazon Fresh. Requires an active session (use "refresh" to authenticate).
28
28
  autonomy [options] View and configure autonomy tiers
29
29
  completions <shell> Generate shell completion script (e.g. assistant completions bash >> ~/.bashrc)
30
30
  notifications [options] Send and inspect notifications through the unified notification router
@@ -32,7 +32,7 @@ Commands:
32
32
  oauth [options] Manage OAuth tokens for connected integrations
33
33
  skills Browse and install skills from the Vellum catalog
34
34
  browser Browser automation, extension relay, and Chrome CDP management
35
- x|twitter [options] Post on X and manage connections. Supports OAuth (official API) and browser session paths.
35
+ x|twitter [options] Post on X and manage connections. Supports managed (platform proxy) and OAuth (official API) paths.
36
36
  map [options] <domain> Auto-navigate a domain and produce a deduplicated API map. Launches Chrome with CDP, starts a Ride Shotgun learn session, then analyzes captured network traffic.
37
37
  sequence [options] Manage email sequences
38
38
  `;
@@ -86,7 +86,6 @@ Session capture (`assistant amazon refresh`) and session checks (`assistant amaz
86
86
  ```
87
87
  assistant amazon status --json # Check if logged in
88
88
  assistant amazon refresh --json # Capture fresh session via Ride Shotgun
89
- assistant amazon login --recording <path> # Import session from a recording file
90
89
  assistant amazon logout # Clear session
91
90
 
92
91
  assistant amazon search "<query>" [--fresh] [--limit <n>] --json
@@ -533,12 +533,6 @@ When the user wants to triage or bulk-act on items, generate an interactive UI w
533
533
  4. Execute tools, update UI with `ui_update`, show feedback via `widgets.toast()`
534
534
  5. Use `window.vellum.confirm()` for destructive actions
535
535
 
536
- ## Home Base
537
-
538
- Home Base starts from a prebuilt scaffold. When updating, preserve required task-lane anchors and apply changes through `app_file_edit` or `app_file_write`.
539
-
540
- Home Base buttons send prefilled natural-language prompts through `vellum.sendAction`. Treat these as normal user messages.
541
-
542
536
  ## External Links
543
537
 
544
538
  Use `vellum.openLink(url, metadata)` to make items clickable. Construct deep-link URLs when possible. Include `metadata.provider` and `metadata.type` for context.
@@ -36,10 +36,6 @@
36
36
  "type": "boolean",
37
37
  "description": "When true (default), an inline preview card is shown in chat after creation. The app is NOT automatically opened in a workspace panel \u2014 users can open it explicitly via the 'Open App' button on the inline card."
38
38
  },
39
- "set_as_home_base": {
40
- "type": "boolean",
41
- "description": "Link this app as the user's Home Base dashboard. Defaults to false. When true, this app becomes the default Home Base shown on launch."
42
- },
43
39
  "preview": {
44
40
  "type": "object",
45
41
  "description": "Inline preview card shown in chat after creation. Always include this so users see a compact summary of what was built. Required fields: title.",
@@ -125,7 +125,6 @@ doordash cart add --store-id <id> --menu-id <id> --item-id <id> --item-name "Lat
125
125
  ```
126
126
  doordash status --json # Check if logged in
127
127
  doordash refresh --json # Capture fresh session via Ride Shotgun (auto-stops after login)
128
- doordash login --recording <path> # Import session from a recording file manually
129
128
  doordash logout --json # Clear session
130
129
  doordash search "<query>" --json # Search restaurants
131
130
  doordash menu <storeId> --json # Get store menu (auto-detects retail stores)
@@ -1,20 +1,11 @@
1
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
- import { afterEach, beforeEach, describe, expect, it } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
5
2
 
6
3
  import {
7
4
  type DoorDashSession,
8
5
  getCookieHeader,
9
6
  getCsrfToken,
10
- importFromRecording,
11
7
  } from "../lib/session.js";
12
8
 
13
- // Override VELLUM_DATA_DIR to use a temp directory during tests.
14
- // session.ts reads process.env.VELLUM_DATA_DIR directly.
15
- const TEST_DIR = join(tmpdir(), `vellum-dd-test-${process.pid}`);
16
- let originalDataDir: string | undefined;
17
-
18
9
  function makeCookie(
19
10
  name: string,
20
11
  value: string,
@@ -81,75 +72,3 @@ describe("DoorDash session helpers", () => {
81
72
  });
82
73
  });
83
74
  });
84
-
85
- describe("DoorDash session persistence", () => {
86
- // These tests exercise the real loadSession/saveSession/clearSession
87
- // by pointing VELLUM_DATA_DIR at a temp directory and testing via
88
- // importFromRecording which exercises save+load.
89
-
90
- beforeEach(() => {
91
- originalDataDir = process.env.VELLUM_DATA_DIR;
92
- process.env.VELLUM_DATA_DIR = TEST_DIR;
93
- mkdirSync(TEST_DIR, { recursive: true });
94
- });
95
-
96
- afterEach(() => {
97
- if (originalDataDir === undefined) {
98
- delete process.env.VELLUM_DATA_DIR;
99
- } else {
100
- process.env.VELLUM_DATA_DIR = originalDataDir;
101
- }
102
- if (existsSync(TEST_DIR)) {
103
- rmSync(TEST_DIR, { recursive: true, force: true });
104
- }
105
- });
106
-
107
- describe("importFromRecording", () => {
108
- it("throws when the recording file does not exist", () => {
109
- expect(() => importFromRecording("/nonexistent/recording.json")).toThrow(
110
- "Recording not found",
111
- );
112
- });
113
-
114
- it("throws when the recording contains no cookies", () => {
115
- const recordingPath = join(TEST_DIR, "empty-recording.json");
116
- writeFileSync(
117
- recordingPath,
118
- JSON.stringify({
119
- id: "rec-empty",
120
- startedAt: 0,
121
- endedAt: 1,
122
- targetDomain: "doordash.com",
123
- networkEntries: [],
124
- cookies: [],
125
- observations: [],
126
- }),
127
- );
128
- expect(() => importFromRecording(recordingPath)).toThrow(
129
- "Recording contains no cookies",
130
- );
131
- });
132
-
133
- it("successfully imports a recording with cookies", () => {
134
- const recordingPath = join(TEST_DIR, "valid-recording.json");
135
- writeFileSync(
136
- recordingPath,
137
- JSON.stringify({
138
- id: "rec-valid",
139
- startedAt: 0,
140
- endedAt: 1,
141
- targetDomain: "doordash.com",
142
- networkEntries: [],
143
- cookies: [makeCookie("session_id", "xyz")],
144
- observations: [],
145
- }),
146
- );
147
- const session = importFromRecording(recordingPath);
148
- expect(session.cookies).toHaveLength(1);
149
- expect(session.cookies[0].name).toBe("session_id");
150
- expect(session.cookies[0].value).toBe("xyz");
151
- expect(session.recordingId).toBe("rec-valid");
152
- expect(session.importedAt).toBeTruthy();
153
- });
154
- });
155
- });
@@ -33,11 +33,16 @@ import {
33
33
  import { extractQueries, saveQueries } from "./lib/query-extractor.js";
34
34
  import {
35
35
  clearSession,
36
- importFromRecording,
36
+ importFromCredentialStore,
37
37
  loadSession,
38
38
  } from "./lib/session.js";
39
39
  import { NetworkRecorder } from "./lib/shared/network-recorder.js";
40
- import { buildDaemonUrl, getDataDir, getHttpPort, readHttpToken } from "./lib/shared/platform.js";
40
+ import {
41
+ buildDaemonUrl,
42
+ getDataDir,
43
+ getHttpPort,
44
+ readHttpToken,
45
+ } from "./lib/shared/platform.js";
41
46
  import { loadRecording, saveRecording } from "./lib/shared/recording-store.js";
42
47
  import type { SessionRecording } from "./lib/shared/recording-types.js";
43
48
 
@@ -94,27 +99,10 @@ export function registerDoordashCommand(program: Command): void {
94
99
  const dd = program
95
100
  .command("doordash")
96
101
  .description(
97
- "Order food from DoorDash. Requires a session imported from a Ride Shotgun recording.",
102
+ 'Order food from DoorDash. Requires an active session (use "refresh" to authenticate).',
98
103
  )
99
104
  .option("--json", "Machine-readable JSON output");
100
105
 
101
- // =========================================================================
102
- // login — import session from a recording
103
- // =========================================================================
104
- dd.command("login")
105
- .description("Import a DoorDash session from a Ride Shotgun recording")
106
- .requiredOption("--recording <path>", "Path to the recording JSON file")
107
- .action(async (opts: { recording: string }, cmd: Command) => {
108
- await run(cmd, async () => {
109
- const session = importFromRecording(opts.recording);
110
- return {
111
- message: "Session imported successfully",
112
- cookieCount: session.cookies.length,
113
- recordingId: session.recordingId,
114
- };
115
- });
116
- });
117
-
118
106
  // =========================================================================
119
107
  // logout — clear saved session
120
108
  // =========================================================================
@@ -148,13 +136,13 @@ export function registerDoordashCommand(program: Command): void {
148
136
  }
149
137
 
150
138
  const result = await startLearnSession(duration);
151
- if (result.recordingPath) {
152
- const session = importFromRecording(result.recordingPath);
139
+ if (result.recordingId) {
140
+ const session = await importFromCredentialStore("doordash.com");
153
141
 
154
142
  // Also extract and save captured queries for self-healing
155
143
  let queriesCaptured = 0;
156
144
  try {
157
- const recording = loadRecording(result.recordingId ?? "");
145
+ const recording = loadRecording(result.recordingId);
158
146
  if (recording) {
159
147
  const queries = extractQueries(recording);
160
148
  if (queries.length > 0) {
@@ -188,7 +176,7 @@ export function registerDoordashCommand(program: Command): void {
188
176
  output(
189
177
  {
190
178
  ok: false,
191
- error: "Recording completed but no recording path returned",
179
+ error: "Recording completed but no recording ID returned",
192
180
  recordingId: result.recordingId,
193
181
  },
194
182
  json,
@@ -1020,7 +1008,10 @@ async function startLearnSession(
1020
1008
  // Poll session status to detect failures early
1021
1009
  try {
1022
1010
  const fetchAbort = AbortSignal.timeout(10_000);
1023
- const statusRes = await fetch(statusUrl, { headers, signal: fetchAbort });
1011
+ const statusRes = await fetch(statusUrl, {
1012
+ headers,
1013
+ signal: fetchAbort,
1014
+ });
1024
1015
  if (statusRes.ok) {
1025
1016
  const status = (await statusRes.json()) as {
1026
1017
  status: string;
@@ -1073,9 +1064,7 @@ async function startLearnSession(
1073
1064
 
1074
1065
  // Completed but no recordingId — cannot correlate
1075
1066
  reject(
1076
- new Error(
1077
- "Learn session completed but no recording was saved.",
1078
- ),
1067
+ new Error("Learn session completed but no recording was saved."),
1079
1068
  );
1080
1069
  return;
1081
1070
  }