@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.
- package/ARCHITECTURE.md +13 -14
- package/README.md +11 -12
- package/docs/architecture/integrations.md +75 -93
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -2
- package/src/__tests__/bundled-asset.test.ts +1 -1
- package/src/__tests__/checker.test.ts +31 -28
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +6 -6
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/error-handler-friendly-messages.test.ts +46 -0
- package/src/__tests__/managed-twitter-guardrails.test.ts +5 -1
- package/src/__tests__/onboarding-template-contract.test.ts +0 -10
- package/src/__tests__/provider-fail-open-selection.test.ts +12 -2
- package/src/__tests__/send-endpoint-busy.test.ts +0 -3
- package/src/__tests__/session-confirmation-signals.test.ts +7 -45
- package/src/__tests__/starter-task-flow.test.ts +9 -19
- package/src/__tests__/system-prompt.test.ts +3 -4
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/twitter-platform-proxy-client.test.ts +43 -18
- package/src/cli/commands/amazon/index.ts +4 -39
- package/src/cli/commands/amazon/session.ts +18 -26
- package/src/cli/commands/twitter/__tests__/cli-read-routing.test.ts +58 -196
- package/src/cli/commands/twitter/__tests__/cli-routing.test.ts +26 -186
- package/src/cli/commands/twitter/__tests__/oauth-client.test.ts +1 -47
- package/src/cli/commands/twitter/index.ts +95 -835
- package/src/cli/commands/twitter/oauth-client.ts +1 -35
- package/src/cli/commands/twitter/router.ts +70 -115
- package/src/cli/commands/twitter/types.ts +30 -0
- package/src/cli/reference.ts +2 -2
- package/src/config/bundled-skills/amazon/SKILL.md +0 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -6
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -4
- package/src/config/bundled-skills/doordash/SKILL.md +0 -1
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +1 -82
- package/src/config/bundled-skills/doordash/doordash-cli.ts +17 -28
- package/src/config/bundled-skills/doordash/lib/session.ts +21 -17
- package/src/config/bundled-skills/twitter/SKILL.md +53 -166
- package/src/config/feature-flag-registry.json +8 -0
- package/src/daemon/handlers/session-history.ts +41 -9
- package/src/daemon/lifecycle.ts +4 -17
- package/src/daemon/message-types/apps.ts +0 -25
- package/src/daemon/message-types/integrations.ts +1 -7
- package/src/daemon/message-types/sessions.ts +6 -1
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/ride-shotgun-handler.ts +33 -1
- package/src/daemon/seed-files.ts +3 -27
- package/src/daemon/server.ts +2 -18
- package/src/daemon/session-agent-loop-handlers.ts +24 -2
- package/src/daemon/session-runtime-assembly.ts +0 -7
- package/src/daemon/session-surfaces.ts +185 -33
- package/src/daemon/session.ts +2 -28
- package/src/memory/app-store.ts +0 -18
- package/src/memory/schema/infrastructure.ts +0 -8
- package/src/permissions/defaults.ts +3 -3
- package/src/prompts/system-prompt.ts +4 -5
- package/src/prompts/templates/BOOTSTRAP.md +0 -3
- package/src/providers/registry.ts +2 -4
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/scopes.test.ts +2 -1
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/auth/scopes.ts +1 -0
- package/src/runtime/auth/token-service.ts +1 -1
- package/src/runtime/http-types.ts +10 -0
- package/src/runtime/middleware/error-handler.ts +14 -1
- package/src/runtime/routes/app-management-routes.ts +61 -64
- package/src/runtime/routes/brain-graph/brain-graph.html +1845 -0
- package/src/runtime/routes/brain-graph-routes.ts +4 -42
- package/src/runtime/routes/conversation-routes.ts +9 -6
- package/src/runtime/routes/diagnostics-routes.ts +91 -14
- package/src/runtime/routes/settings-routes.ts +3 -93
- package/src/tools/AGENTS.md +38 -0
- package/src/tools/apps/executors.ts +0 -6
- package/src/tools/document/editor-template.ts +10 -8
- package/src/twitter/platform-proxy-client.ts +6 -3
- package/src/util/errors.ts +12 -0
- package/src/__tests__/home-base-bootstrap.test.ts +0 -84
- package/src/__tests__/prebuilt-home-base-seed.test.ts +0 -79
- package/src/cli/commands/twitter/__tests__/cli-error-shaping.test.ts +0 -265
- package/src/cli/commands/twitter/client.ts +0 -989
- package/src/cli/commands/twitter/session.ts +0 -121
- package/src/home-base/app-link-store.ts +0 -78
- package/src/home-base/bootstrap.ts +0 -74
- package/src/home-base/prebuilt/brain-graph.html +0 -1483
- package/src/home-base/prebuilt/index.html +0 -702
- package/src/home-base/prebuilt/seed-metadata.json +0 -21
- package/src/home-base/prebuilt/seed.ts +0 -122
- package/src/home-base/prebuilt-home-base-updater.ts +0 -36
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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) =>
|
|
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
|
|
142
|
+
test("throws ProviderNotConfiguredError when no providers are available", () => {
|
|
142
143
|
setupNoProviders();
|
|
143
144
|
expect(() => getFailoverProvider("gemini", ["fireworks"])).toThrow(
|
|
144
|
-
|
|
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
|
-
* -
|
|
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
|
|
525
|
-
test("
|
|
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("
|
|
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("
|
|
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 =
|
|
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 =
|
|
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 !== "
|
|
7
|
+
if (id !== "test-app") return null;
|
|
8
8
|
return {
|
|
9
9
|
id,
|
|
10
|
-
name: "
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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: "
|
|
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("
|
|
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
|
|
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
|
-
|
|
328
|
-
|
|
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
|
|
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:
|
|
881
|
-
expect(match!.decision).toBe("
|
|
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:
|
|
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 () => ({
|
|
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 () => ({
|
|
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
|
-
|
|
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. "
|
|
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"
|
|
126
|
+
"refresh" or "refresh-headless".
|
|
162
127
|
|
|
163
128
|
Examples:
|
|
164
129
|
$ assistant amazon logout`,
|