@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
@@ -152,7 +152,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
152
152
  const runAgentLoop = mock(async () => undefined);
153
153
  const session = {
154
154
  setTrustContext: () => {},
155
- setStateSignalListener: () => {},
155
+ updateClient: () => {},
156
156
  emitConfirmationStateChanged: () => {},
157
157
  emitActivityState: () => {},
158
158
  setTurnChannelContext: () => {},
@@ -223,7 +223,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
223
223
  const runAgentLoop = mock(async () => undefined);
224
224
  const session = {
225
225
  setTrustContext: () => {},
226
- setStateSignalListener: () => {},
226
+ updateClient: () => {},
227
227
  emitConfirmationStateChanged: () => {},
228
228
  emitActivityState: () => {},
229
229
  setTurnChannelContext: () => {},
@@ -289,7 +289,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
289
289
  const runAgentLoop = mock(async () => undefined);
290
290
  const session = {
291
291
  setTrustContext: () => {},
292
- setStateSignalListener: () => {},
292
+ updateClient: () => {},
293
293
  emitConfirmationStateChanged: () => {},
294
294
  emitActivityState: () => {},
295
295
  setTurnChannelContext: () => {},
@@ -361,7 +361,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
361
361
  const runAgentLoop = mock(async () => undefined);
362
362
  const session = {
363
363
  setTrustContext: () => {},
364
- setStateSignalListener: () => {},
364
+ updateClient: () => {},
365
365
  emitConfirmationStateChanged: () => {},
366
366
  emitActivityState: () => {},
367
367
  setTurnChannelContext: () => {},
@@ -428,7 +428,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
428
428
  const runAgentLoop = mock(async () => undefined);
429
429
  const session = {
430
430
  setTrustContext: () => {},
431
- setStateSignalListener: () => {},
431
+ updateClient: () => {},
432
432
  emitConfirmationStateChanged: () => {},
433
433
  emitActivityState: () => {},
434
434
  setTurnChannelContext: () => {},
@@ -489,7 +489,7 @@ describe("handleSendMessage canonical guardian reply interception", () => {
489
489
  const runAgentLoop = mock(async () => undefined);
490
490
  const session = {
491
491
  setTrustContext: () => {},
492
- setStateSignalListener: () => {},
492
+ updateClient: () => {},
493
493
  emitConfirmationStateChanged: () => {},
494
494
  emitActivityState: () => {},
495
495
  setTurnChannelContext: () => {},
@@ -228,9 +228,10 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
228
228
  "mcp/client.ts", // MCP client cached-token lookup
229
229
  "oauth/token-persistence.ts", // OAuth token persistence (set/delete tokens)
230
230
  "runtime/routes/secret-routes.ts", // HTTP secret management routes (set/delete secrets)
231
+ "daemon/ride-shotgun-handler.ts", // learn session cookie persistence
231
232
  "daemon/session-messaging.ts", // credential storage during session messaging
232
233
  "runtime/routes/settings-routes.ts", // settings routes OAuth credential lookup (client_id/client_secret/access tokens)
233
- "util/cookie-session.ts", // shared cookie-session persistence (session CRUD via credential store)
234
+ "twitter/platform-proxy-client.ts", // managed Twitter proxy auth token lookup
234
235
  ]);
235
236
 
236
237
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { withErrorHandling } from "../runtime/middleware/error-handler.js";
4
+ import { ConfigError, ProviderNotConfiguredError } from "../util/errors.js";
5
+
6
+ describe("withErrorHandling – friendly error messages", () => {
7
+ test("ProviderNotConfiguredError returns actionable message for anthropic", async () => {
8
+ const response = await withErrorHandling("test", async () => {
9
+ throw new ProviderNotConfiguredError("anthropic", []);
10
+ });
11
+
12
+ expect(response.status).toBe(422);
13
+ const body = (await response.json()) as {
14
+ error: { code: string; message: string };
15
+ };
16
+ expect(body.error.code).toBe("UNPROCESSABLE_ENTITY");
17
+ expect(body.error.message).toContain("No API key configured");
18
+ expect(body.error.message).toContain("ANTHROPIC_API_KEY");
19
+ expect(body.error.message).toContain("vellum hatch");
20
+ });
21
+
22
+ test("ProviderNotConfiguredError tailors env var to requested provider", async () => {
23
+ const response = await withErrorHandling("test", async () => {
24
+ throw new ProviderNotConfiguredError("openai", []);
25
+ });
26
+
27
+ expect(response.status).toBe(422);
28
+ const body = (await response.json()) as {
29
+ error: { code: string; message: string };
30
+ };
31
+ expect(body.error.message).toContain("OPENAI_API_KEY");
32
+ expect(body.error.message).not.toContain("ANTHROPIC_API_KEY");
33
+ });
34
+
35
+ test("generic ConfigError still returns its own message", async () => {
36
+ const response = await withErrorHandling("test", async () => {
37
+ throw new ConfigError("Twilio phone number not configured.");
38
+ });
39
+
40
+ expect(response.status).toBe(422);
41
+ const body = (await response.json()) as {
42
+ error: { code: string; message: string };
43
+ };
44
+ expect(body.error.message).toBe("Twilio phone number not configured.");
45
+ });
46
+ });
@@ -106,7 +106,11 @@ mock.module("../util/logger.js", () => ({
106
106
  }));
107
107
 
108
108
  mock.module("../security/secure-keys.js", () => ({
109
- getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
109
+ getSecureKey: (account: string) => {
110
+ if (account === "credential:vellum:platform_assistant_id")
111
+ return "ast_test123";
112
+ return secureKeyStore[account] ?? undefined;
113
+ },
110
114
  setSecureKey: (account: string, value: string) => {
111
115
  secureKeyStore[account] = value;
112
116
  return true;
@@ -28,15 +28,6 @@ describe("onboarding template contracts", () => {
28
28
  expect(lower).toContain("change it later");
29
29
  });
30
30
 
31
- test("creates Home Base silently in the background", () => {
32
- const lower = bootstrap.toLowerCase();
33
- expect(lower).toContain("app_create");
34
- expect(lower).toContain("set_as_home_base");
35
- // Must NOT open or announce it
36
- expect(lower).toContain("do not open it with `app_open`");
37
- expect(lower).toContain("do not announce it");
38
- });
39
-
40
31
  test("contains naming intent markers so the first reply includes naming cues", () => {
41
32
  const lower = bootstrap.toLowerCase();
42
33
  // The template must prompt the assistant to ask about names.
@@ -84,7 +75,6 @@ describe("onboarding template contracts", () => {
84
75
  expect(lower).toContain("work role");
85
76
  expect(lower).toContain("2 suggestions shown");
86
77
  expect(lower).toContain("selected one, deferred both");
87
- expect(lower).toContain("home base");
88
78
  });
89
79
 
90
80
  test("contains refusal policy", () => {
@@ -32,6 +32,7 @@ import {
32
32
  listProviders,
33
33
  resolveProviderSelection,
34
34
  } from "../providers/registry.js";
35
+ import { ProviderNotConfiguredError } from "../util/errors.js";
35
36
 
36
37
  /**
37
38
  * Tests for fail-open provider selection: when the configured primary provider
@@ -138,11 +139,20 @@ describe("getFailoverProvider (fail-open)", () => {
138
139
  expect(provider).toBeDefined();
139
140
  });
140
141
 
141
- test("throws ConfigError when no providers are available", () => {
142
+ test("throws ProviderNotConfiguredError when no providers are available", () => {
142
143
  setupNoProviders();
143
144
  expect(() => getFailoverProvider("gemini", ["fireworks"])).toThrow(
144
- /No providers available/,
145
+ ProviderNotConfiguredError,
145
146
  );
147
+ try {
148
+ getFailoverProvider("gemini", ["fireworks"]);
149
+ } catch (err) {
150
+ expect(err).toBeInstanceOf(ProviderNotConfiguredError);
151
+ const typed = err as ProviderNotConfiguredError;
152
+ expect(typed.requestedProvider).toBe("gemini");
153
+ expect(typed.registeredProviders).toEqual([]);
154
+ expect(typed.message).toMatch(/No providers available/);
155
+ }
146
156
  });
147
157
 
148
158
  test("single available provider returns it directly (no failover wrapper)", () => {
@@ -112,7 +112,6 @@ function makeCompletingSession(): Session {
112
112
  setCommandIntent: () => {},
113
113
  setTurnChannelContext: () => {},
114
114
  setTurnInterfaceContext: () => {},
115
- setStateSignalListener: () => {},
116
115
  updateClient: () => {},
117
116
  hasAnyPendingConfirmation: () => false,
118
117
  hasPendingConfirmation: () => false,
@@ -165,7 +164,6 @@ function makeHangingSession(): Session {
165
164
  setCommandIntent: () => {},
166
165
  setTurnChannelContext: () => {},
167
166
  setTurnInterfaceContext: () => {},
168
- setStateSignalListener: () => {},
169
167
  updateClient: () => {},
170
168
  hasAnyPendingConfirmation: () => false,
171
169
  hasPendingConfirmation: () => false,
@@ -246,7 +244,6 @@ function makePendingApprovalSession(
246
244
  setCommandIntent: () => {},
247
245
  setTurnChannelContext: () => {},
248
246
  setTurnInterfaceContext: () => {},
249
- setStateSignalListener: () => {},
250
247
  updateClient: () => {},
251
248
  hasAnyPendingConfirmation: () => pending.size > 0,
252
249
  hasPendingConfirmation: (candidateRequestId: string) =>
@@ -6,7 +6,7 @@
6
6
  * - handleConfirmationResponse emits both confirmation_state_changed and
7
7
  * assistant_activity_state events centrally
8
8
  * - emitActivityState produces monotonically increasing activityVersion
9
- * - setStateSignalListener routes signals to an external callback (HTTP/SSE)
9
+ * - sendToClient receives state signals (confirmation_state_changed, assistant_activity_state)
10
10
  * - "deny" decisions produce 'denied' state, "allow" produces 'approved'
11
11
  */
12
12
  import { mkdtempSync, rmSync } from "node:fs";
@@ -521,40 +521,21 @@ describe("activity version ordering", () => {
521
521
  });
522
522
  });
523
523
 
524
- describe("state signal listener", () => {
525
- test("setStateSignalListener routes emitActivityState to external callback", () => {
524
+ describe("sendToClient receives state signals", () => {
525
+ test("emitActivityState delivers to sendToClient", () => {
526
526
  const clientMsgs: ServerMessage[] = [];
527
- const signalMsgs: ServerMessage[] = [];
528
-
529
527
  const session = makeSession((msg) => clientMsgs.push(msg));
530
- session.setStateSignalListener((msg) => signalMsgs.push(msg));
531
528
 
532
529
  session.emitActivityState("thinking", "message_dequeued", "assistant_turn");
533
530
 
534
- // Both sendToClient and signal listener should receive the message
535
531
  expect(
536
532
  clientMsgs.filter((m) => m.type === "assistant_activity_state"),
537
533
  ).toHaveLength(1);
538
- expect(
539
- signalMsgs.filter((m) => m.type === "assistant_activity_state"),
540
- ).toHaveLength(1);
541
-
542
- // Messages should be identical
543
- const clientMsg = clientMsgs.find(
544
- (m) => m.type === "assistant_activity_state",
545
- );
546
- const signalMsg = signalMsgs.find(
547
- (m) => m.type === "assistant_activity_state",
548
- );
549
- expect(clientMsg).toEqual(signalMsg);
550
534
  });
551
535
 
552
- test("setStateSignalListener routes emitConfirmationStateChanged to external callback", () => {
536
+ test("emitConfirmationStateChanged delivers to sendToClient", () => {
553
537
  const clientMsgs: ServerMessage[] = [];
554
- const signalMsgs: ServerMessage[] = [];
555
-
556
538
  const session = makeSession((msg) => clientMsgs.push(msg));
557
- session.setStateSignalListener((msg) => signalMsgs.push(msg));
558
539
 
559
540
  session.emitConfirmationStateChanged({
560
541
  sessionId: "conv-signals-test",
@@ -566,41 +547,22 @@ describe("state signal listener", () => {
566
547
  expect(
567
548
  clientMsgs.filter((m) => m.type === "confirmation_state_changed"),
568
549
  ).toHaveLength(1);
569
- expect(
570
- signalMsgs.filter((m) => m.type === "confirmation_state_changed"),
571
- ).toHaveLength(1);
572
550
  });
573
551
 
574
- test("without state signal listener, only sendToClient receives messages", () => {
552
+ test("handleConfirmationResponse delivers state signals to sendToClient", () => {
575
553
  const clientMsgs: ServerMessage[] = [];
576
-
577
554
  const session = makeSession((msg) => clientMsgs.push(msg));
578
- // No setStateSignalListener call
579
-
580
- session.emitActivityState("idle", "message_complete", "global");
581
-
582
- expect(
583
- clientMsgs.filter((m) => m.type === "assistant_activity_state"),
584
- ).toHaveLength(1);
585
- });
586
-
587
- test("state signal listener receives handleConfirmationResponse emissions", () => {
588
- const signalMsgs: ServerMessage[] = [];
589
-
590
- // Use no-op sendToClient (simulates HTTP session with no socket)
591
- const session = makeSession(() => {});
592
- session.setStateSignalListener((msg) => signalMsgs.push(msg));
593
555
 
594
556
  seedPendingConfirmation(session, "req-signal-confirm");
595
557
  session.handleConfirmationResponse("req-signal-confirm", "allow");
596
558
 
597
- const confirmSignal = signalMsgs.find(
559
+ const confirmSignal = clientMsgs.find(
598
560
  (m) =>
599
561
  m.type === "confirmation_state_changed" &&
600
562
  "requestId" in m &&
601
563
  (m as { requestId: string }).requestId === "req-signal-confirm",
602
564
  );
603
- const activitySignal = signalMsgs.find(
565
+ const activitySignal = clientMsgs.find(
604
566
  (m) =>
605
567
  m.type === "assistant_activity_state" &&
606
568
  "reason" in m &&
@@ -4,30 +4,20 @@ import type { ServerMessage, SurfaceType } from "../daemon/message-protocol.js";
4
4
 
5
5
  mock.module("../memory/app-store.js", () => ({
6
6
  getApp: (id: string) => {
7
- if (id !== "home-base-app") return null;
7
+ if (id !== "test-app") return null;
8
8
  return {
9
9
  id,
10
- name: "Home Base",
11
- htmlDefinition:
12
- '<main id="home-base-root" data-vellum-home-base="v1"></main>',
10
+ name: "Test App",
11
+ description: "A test app",
12
+ htmlDefinition: "<main>Test App</main>",
13
13
  };
14
14
  },
15
+ getAppPreview: () => null,
15
16
  updateApp: () => {
16
17
  throw new Error("updateApp should not be called in this test");
17
18
  },
18
19
  }));
19
20
 
20
- mock.module("../home-base/prebuilt/seed.js", () => ({
21
- findSeededHomeBaseApp: () => ({ id: "home-base-app" }),
22
- getPrebuiltHomeBasePreview: () => ({
23
- title: "Home Base",
24
- subtitle: "Dashboard",
25
- description: "Preview",
26
- icon: "🏠",
27
- metrics: [{ label: "Starter tasks", value: "3" }],
28
- }),
29
- }));
30
-
31
21
  import {
32
22
  createSurfaceMutex,
33
23
  handleSurfaceAction,
@@ -70,12 +60,12 @@ describe("starter task surface actions", () => {
70
60
  ctx.pendingSurfaceActions.set("surf-1", { surfaceType: "dynamic_page" });
71
61
 
72
62
  handleSurfaceAction(ctx, "surf-1", "relay_prompt", {
73
- prompt: "Help me customize Home Base with a calmer palette.",
63
+ prompt: "Help me customize the app with a calmer palette.",
74
64
  task: "change_look_and_feel",
75
65
  });
76
66
 
77
67
  expect(forwarded).toEqual([
78
- "Help me customize Home Base with a calmer palette.",
68
+ "Help me customize the app with a calmer palette.",
79
69
  ]);
80
70
  expect(ctx.pendingSurfaceActions.has("surf-1")).toBe(true);
81
71
  });
@@ -133,7 +123,7 @@ describe("starter task surface actions", () => {
133
123
  ctx.sendToClient = (msg) => sent.push(msg);
134
124
 
135
125
  const result = await surfaceProxyResolver(ctx, "app_open", {
136
- app_id: "home-base-app",
126
+ app_id: "test-app",
137
127
  });
138
128
 
139
129
  expect(result.isError).toBe(false);
@@ -141,7 +131,7 @@ describe("starter task surface actions", () => {
141
131
  surfaceId: string;
142
132
  appId: string;
143
133
  };
144
- expect(parsed.appId).toBe("home-base-app");
134
+ expect(parsed.appId).toBe("test-app");
145
135
  expect(ctx.pendingSurfaceActions.get(parsed.surfaceId)?.surfaceType).toBe(
146
136
  "dynamic_page",
147
137
  );
@@ -320,13 +320,12 @@ describe("buildSystemPrompt", () => {
320
320
  expect(result).not.toContain("use `app_update` to change the HTML");
321
321
  });
322
322
 
323
- test("onboarding playbook uses app_file_edit for accent color, not app_update", () => {
323
+ test("onboarding playbook does not reference Home Base for accent color", () => {
324
324
  // Starter task playbooks only included during onboarding (BOOTSTRAP.md exists)
325
325
  writeFileSync(join(TEST_DIR, "BOOTSTRAP.md"), "# First run");
326
326
  const result = buildSystemPrompt();
327
- expect(result).toContain(
328
- "using `app_file_edit` to update the theme styles",
329
- );
327
+ // The make_it_yours playbook should not reference Home Base anymore
328
+ expect(result).not.toContain("Home Base dashboard");
330
329
  expect(result).not.toContain(
331
330
  "using `app_update` to regenerate the Home Base HTML",
332
331
  );
@@ -874,13 +874,13 @@ describe("Trust Store", () => {
874
874
  );
875
875
  });
876
876
 
877
- test("findHighestPriorityRule matches default allow for host_bash", () => {
877
+ test("findHighestPriorityRule matches default ask for host_bash", () => {
878
878
  const match = findHighestPriorityRule("host_bash", ["ls"], "/tmp");
879
879
  expect(match).not.toBeNull();
880
- expect(match!.id).toBe("default:allow-host_bash-global");
881
- expect(match!.decision).toBe("allow");
880
+ expect(match!.id).toBe("default:ask-host_bash-global");
881
+ expect(match!.decision).toBe("ask");
882
882
  expect(match!.priority).toBe(
883
- DEFAULT_PRIORITY_BY_ID.get("default:allow-host_bash-global")!,
883
+ DEFAULT_PRIORITY_BY_ID.get("default:ask-host_bash-global")!,
884
884
  );
885
885
  });
886
886
 
@@ -7,11 +7,14 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
7
7
  let mockApiKey: string | undefined = "test-api-key-123";
8
8
  let mockPlatformEnvUrl = "https://platform.vellum.ai";
9
9
  let mockPlatformAssistantId = "ast_abc123";
10
+ let mockPlatformAssistantIdFromStore: string | undefined = undefined;
10
11
  let mockConfigBaseUrl = "";
11
12
 
12
13
  mock.module("../security/secure-keys.js", () => ({
13
14
  getSecureKey: (account: string) => {
14
15
  if (account === "credential:vellum:assistant_api_key") return mockApiKey;
16
+ if (account === "credential:vellum:platform_assistant_id")
17
+ return mockPlatformAssistantIdFromStore;
15
18
  return undefined;
16
19
  },
17
20
  }));
@@ -73,12 +76,17 @@ beforeEach(() => {
73
76
  mockApiKey = "test-api-key-123";
74
77
  mockPlatformEnvUrl = "https://platform.vellum.ai";
75
78
  mockPlatformAssistantId = "ast_abc123";
79
+ mockPlatformAssistantIdFromStore = undefined;
76
80
  mockConfigBaseUrl = "";
77
81
  lastFetchArgs = null;
78
82
  fetchResponse = {
79
83
  ok: true,
80
84
  status: 200,
81
- json: async () => ({ data: { id: "12345", text: "Hello world" } }),
85
+ json: async () => ({
86
+ status: 200,
87
+ headers: {},
88
+ body: { data: { id: "12345", text: "Hello world" } },
89
+ }),
82
90
  };
83
91
  });
84
92
 
@@ -114,9 +122,22 @@ describe("prerequisite resolution", () => {
114
122
  });
115
123
 
116
124
  test("resolvePlatformAssistantId returns the env value", () => {
125
+ mockPlatformAssistantIdFromStore = undefined;
117
126
  expect(resolvePlatformAssistantId()).toBe("ast_abc123");
118
127
  });
119
128
 
129
+ test("resolvePlatformAssistantId prefers secure key store over env", () => {
130
+ mockPlatformAssistantIdFromStore = "ast_from_store";
131
+ mockPlatformAssistantId = "ast_from_env";
132
+ expect(resolvePlatformAssistantId()).toBe("ast_from_store");
133
+ });
134
+
135
+ test("resolvePlatformAssistantId falls back to env when store is empty", () => {
136
+ mockPlatformAssistantIdFromStore = undefined;
137
+ mockPlatformAssistantId = "ast_from_env";
138
+ expect(resolvePlatformAssistantId()).toBe("ast_from_env");
139
+ });
140
+
120
141
  test("resolvePrerequisites returns all values when present", () => {
121
142
  const prereqs = resolvePrerequisites();
122
143
  expect(prereqs.platformBaseUrl).toBe("https://platform.vellum.ai");
@@ -205,9 +226,9 @@ describe("proxyTwitterCall", () => {
205
226
  });
206
227
 
207
228
  const parsed = JSON.parse(lastFetchArgs![1].body as string);
208
- expect(parsed.method).toBe("POST");
209
- expect(parsed.path).toBe("/2/tweets");
210
- expect(parsed.body).toEqual({ text: "Hello world" });
229
+ expect(parsed.request.method).toBe("POST");
230
+ expect(parsed.request.path).toBe("/2/tweets");
231
+ expect(parsed.request.body).toEqual({ text: "Hello world" });
211
232
  });
212
233
 
213
234
  test("GET-style request includes query parameters", async () => {
@@ -218,16 +239,20 @@ describe("proxyTwitterCall", () => {
218
239
  });
219
240
 
220
241
  const parsed = JSON.parse(lastFetchArgs![1].body as string);
221
- expect(parsed.method).toBe("GET");
222
- expect(parsed.path).toBe("/2/users/me");
223
- expect(parsed.query).toEqual({ "user.fields": "name,username" });
242
+ expect(parsed.request.method).toBe("GET");
243
+ expect(parsed.request.path).toBe("/2/users/me");
244
+ expect(parsed.request.query).toEqual({ "user.fields": "name,username" });
224
245
  });
225
246
 
226
247
  test("returns parsed response data on success", async () => {
227
248
  fetchResponse = {
228
249
  ok: true,
229
250
  status: 200,
230
- json: async () => ({ data: { id: "99", text: "ok" } }),
251
+ json: async () => ({
252
+ status: 200,
253
+ headers: {},
254
+ body: { data: { id: "99", text: "ok" } },
255
+ }),
231
256
  };
232
257
 
233
258
  const result = await proxyTwitterCall({
@@ -412,16 +437,16 @@ describe("postTweet", () => {
412
437
  await postTweet("Hello from proxy");
413
438
 
414
439
  const parsed = JSON.parse(lastFetchArgs![1].body as string);
415
- expect(parsed.method).toBe("POST");
416
- expect(parsed.path).toBe("/2/tweets");
417
- expect(parsed.body.text).toBe("Hello from proxy");
440
+ expect(parsed.request.method).toBe("POST");
441
+ expect(parsed.request.path).toBe("/2/tweets");
442
+ expect(parsed.request.body.text).toBe("Hello from proxy");
418
443
  });
419
444
 
420
445
  test("includes reply metadata when replyToId is provided", async () => {
421
446
  await postTweet("This is a reply", { replyToId: "tweet_789" });
422
447
 
423
448
  const parsed = JSON.parse(lastFetchArgs![1].body as string);
424
- expect(parsed.body.reply).toEqual({
449
+ expect(parsed.request.body.reply).toEqual({
425
450
  in_reply_to_tweet_id: "tweet_789",
426
451
  });
427
452
  });
@@ -432,9 +457,9 @@ describe("getMe", () => {
432
457
  await getMe({ "user.fields": "name,username,profile_image_url" });
433
458
 
434
459
  const parsed = JSON.parse(lastFetchArgs![1].body as string);
435
- expect(parsed.method).toBe("GET");
436
- expect(parsed.path).toBe("/2/users/me");
437
- expect(parsed.query).toEqual({
460
+ expect(parsed.request.method).toBe("GET");
461
+ expect(parsed.request.path).toBe("/2/users/me");
462
+ expect(parsed.request.query).toEqual({
438
463
  "user.fields": "name,username,profile_image_url",
439
464
  });
440
465
  });
@@ -443,8 +468,8 @@ describe("getMe", () => {
443
468
  await getMe();
444
469
 
445
470
  const parsed = JSON.parse(lastFetchArgs![1].body as string);
446
- expect(parsed.method).toBe("GET");
447
- expect(parsed.path).toBe("/2/users/me");
448
- expect(parsed.query).toBeUndefined();
471
+ expect(parsed.request.method).toBe("GET");
472
+ expect(parsed.request.path).toBe("/2/users/me");
473
+ expect(parsed.request.query).toBeUndefined();
449
474
  });
450
475
  });
@@ -22,12 +22,7 @@ import {
22
22
  SessionExpiredError,
23
23
  viewCart,
24
24
  } from "./client.js";
25
- import {
26
- clearSession,
27
- importFromRecording,
28
- loadSession,
29
- saveSession,
30
- } from "./session.js";
25
+ import { clearSession, loadSession, saveSession } from "./session.js";
31
26
 
32
27
  // ---------------------------------------------------------------------------
33
28
  // Helpers
@@ -82,7 +77,7 @@ export function registerAmazonCommand(program: Command): void {
82
77
  const amz = program
83
78
  .command("amazon")
84
79
  .description(
85
- "Shop on Amazon and Amazon Fresh. Requires a session imported from a Ride Shotgun recording.",
80
+ 'Shop on Amazon and Amazon Fresh. Requires an active session (use "refresh" to authenticate).',
86
81
  )
87
82
  .option("--json", "Machine-readable JSON output");
88
83
 
@@ -98,8 +93,7 @@ Session lifecycle:
98
93
  2. "refresh-headless" reads cookies directly from Chrome's local SQLite database.
99
94
  No visible browser window is needed, but Chrome must already be signed into Amazon.
100
95
  3. "status" checks whether a valid session exists.
101
- 4. "login" imports a session from a previously saved Ride Shotgun recording file.
102
- 5. "logout" clears the saved session.
96
+ 4. "logout" clears the saved session.
103
97
 
104
98
  Product workflow: search for products, view details/variations by ASIN, then add
105
99
  to cart. Use --fresh flag for Amazon Fresh grocery items throughout the workflow.
@@ -118,35 +112,6 @@ Examples:
118
112
  $ assistant amazon order place --payment-method-id pm_abc123`,
119
113
  );
120
114
 
121
- // =========================================================================
122
- // login — import session from a recording
123
- // =========================================================================
124
- amz
125
- .command("login")
126
- .description("Import an Amazon session from a Ride Shotgun recording")
127
- .requiredOption("--recording <path>", "Path to the recording JSON file")
128
- .addHelpText(
129
- "after",
130
- `
131
- Imports Amazon session cookies from a previously saved Ride Shotgun recording
132
- file. The recording must contain captured cookies from an authenticated Amazon
133
- session. Typically used to restore a session from a saved recording rather than
134
- re-authenticating via "refresh".
135
-
136
- Examples:
137
- $ assistant amazon login --recording /path/to/recording.json
138
- $ assistant amazon login --recording ~/recordings/amazon-2024-01-15.json`,
139
- )
140
- .action(async (opts: { recording: string }, cmd: Command) => {
141
- await run(cmd, async () => {
142
- const session = await importFromRecording(opts.recording);
143
- return {
144
- message: "Session imported successfully",
145
- cookieCount: session.cookies.length,
146
- };
147
- });
148
- });
149
-
150
115
  // =========================================================================
151
116
  // logout — clear saved session
152
117
  // =========================================================================
@@ -158,7 +123,7 @@ Examples:
158
123
  `
159
124
  Removes all saved Amazon session cookies from local storage. After logout,
160
125
  all shopping commands will fail until a new session is established via
161
- "refresh", "refresh-headless", or "login".
126
+ "refresh" or "refresh-headless".
162
127
 
163
128
  Examples:
164
129
  $ assistant amazon logout`,