@vellumai/assistant 0.4.35 → 0.4.37
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +44 -49
- package/README.md +32 -20
- package/docs/architecture/keychain-broker.md +186 -0
- package/docs/architecture/security.md +110 -116
- package/docs/runbook-trusted-contacts.md +2 -2
- package/docs/skills.md +25 -25
- package/package.json +5 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
- package/src/__tests__/actor-token-service.test.ts +1 -0
- package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
- package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/bundle-scanner.test.ts +1 -1
- package/src/__tests__/channel-guardian.test.ts +102 -102
- package/src/__tests__/channel-invite-transport.test.ts +155 -256
- package/src/__tests__/channel-readiness-routes.test.ts +336 -0
- package/src/__tests__/checker.test.ts +6 -6
- package/src/__tests__/chrome-cdp.test.ts +350 -0
- package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
- package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
- package/src/__tests__/config-loader-migration.test.ts +85 -0
- package/src/__tests__/conversation-pairing.test.ts +370 -5
- package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
- package/src/__tests__/credential-broker-server-use.test.ts +1 -10
- package/src/__tests__/credential-security-e2e.test.ts +7 -1
- package/src/__tests__/credential-security-invariants.test.ts +14 -20
- package/src/__tests__/credential-vault-unit.test.ts +1 -11
- package/src/__tests__/credential-vault.test.ts +5 -19
- package/src/__tests__/credentials-cli.test.ts +814 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
- package/src/__tests__/email-invite-adapter.test.ts +78 -0
- package/src/__tests__/email-service-config-fallback.test.ts +102 -0
- package/src/__tests__/encrypted-store.test.ts +6 -6
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
- package/src/__tests__/guardian-outbound-http.test.ts +53 -47
- package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
- package/src/__tests__/handlers-telegram-config.test.ts +8 -2
- package/src/__tests__/handlers-twitter-config.test.ts +2 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
- package/src/__tests__/ingress-reconcile.test.ts +6 -0
- package/src/__tests__/intent-routing.test.ts +23 -4
- package/src/__tests__/invite-routes-http.test.ts +12 -0
- package/src/__tests__/ipc-snapshot.test.ts +8 -2
- package/src/__tests__/keychain-broker-client.test.ts +543 -0
- package/src/__tests__/llm-usage-store.test.ts +344 -0
- package/src/__tests__/mcp-client-auth.test.ts +2 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/migration-transport.test.ts +49 -0
- package/src/__tests__/notification-broadcaster.test.ts +205 -5
- package/src/__tests__/notification-deep-link.test.ts +365 -1
- package/src/__tests__/oauth-connect-handler.test.ts +2 -2
- package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
- package/src/__tests__/proxy-approval-callback.test.ts +1 -1
- package/src/__tests__/recording-handler.test.ts +1 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -1
- package/src/__tests__/recording-state-machine.test.ts +1 -1
- package/src/__tests__/relay-server.test.ts +9 -1
- package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +8 -2
- package/src/__tests__/secure-keys.test.ts +175 -216
- package/src/__tests__/session-confirmation-signals.test.ts +1 -1
- package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/session-queue.test.ts +2 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
- package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
- package/src/__tests__/skill-feature-flags.test.ts +12 -9
- package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skills.test.ts +34 -4
- package/src/__tests__/slack-channel-config.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +26 -4
- package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
- package/src/__tests__/twitter-auth-handler.test.ts +2 -2
- package/src/__tests__/twitter-oauth-client.test.ts +1 -1
- package/src/__tests__/usage-routes.test.ts +339 -0
- package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
- package/src/agent/loop.ts +3 -0
- package/src/amazon/checkout.ts +0 -1
- package/src/approvals/guardian-request-resolvers.ts +9 -1
- package/src/bundler/app-bundler.ts +28 -12
- package/src/bundler/bundle-scanner.ts +1 -1
- package/src/bundler/bundle-signer.ts +3 -3
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/signature-verifier.ts +3 -3
- package/src/channels/config.ts +1 -1
- package/src/cli/AGENTS.md +63 -0
- package/src/cli/__tests__/notifications.test.ts +470 -0
- package/src/cli/amazon.ts +344 -167
- package/src/cli/audit.ts +85 -0
- package/src/cli/autonomy.ts +369 -0
- package/src/cli/channels.ts +51 -0
- package/src/cli/completions.ts +208 -0
- package/src/cli/config.ts +220 -0
- package/src/cli/contacts.ts +471 -0
- package/src/cli/credentials.ts +564 -0
- package/src/cli/default-action.ts +14 -0
- package/src/cli/dev.ts +131 -0
- package/src/cli/doctor.ts +398 -0
- package/src/cli/email.ts +494 -0
- package/src/cli/influencer.ts +72 -0
- package/src/cli/integrations.ts +248 -57
- package/src/cli/keys.ts +114 -0
- package/src/cli/map.ts +46 -54
- package/src/cli/mcp.ts +111 -3
- package/src/cli/{config-commands.ts → memory.ts} +134 -245
- package/src/cli/notifications.ts +407 -0
- package/src/cli/program.ts +65 -0
- package/src/cli/reference.ts +48 -0
- package/src/cli/sequence.ts +154 -0
- package/src/cli/sessions.ts +262 -0
- package/src/cli/trust.ts +175 -0
- package/src/cli/twitter.ts +323 -106
- package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
- package/src/config/bundled-skills/amazon/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
- package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
- package/src/config/bundled-skills/contacts/SKILL.md +178 -10
- package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
- package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
- package/src/config/bundled-tool-registry.ts +2 -0
- package/src/config/core-schema.ts +7 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +26 -0
- package/src/config/schema.ts +4 -0
- package/src/config/skill-state.ts +0 -13
- package/src/config/system-prompt.ts +27 -0
- package/src/contacts/contact-store.ts +25 -0
- package/src/daemon/computer-use-session.ts +1 -1
- package/src/daemon/handlers/apps.ts +1 -0
- package/src/daemon/handlers/config-channels.ts +3 -3
- package/src/daemon/handlers/config-dispatch.ts +29 -0
- package/src/daemon/handlers/config-inbox.ts +4 -3
- package/src/daemon/handlers/config.ts +3 -43
- package/src/daemon/handlers/contacts.ts +34 -0
- package/src/daemon/handlers/index.ts +17 -3
- package/src/daemon/handlers/session-user-message.ts +7 -0
- package/src/daemon/handlers/sessions.ts +21 -2
- package/src/daemon/handlers/shared.ts +17 -0
- package/src/daemon/ipc-contract/apps.ts +2 -0
- package/src/daemon/ipc-contract/computer-use.ts +9 -0
- package/src/daemon/ipc-contract/contacts.ts +3 -3
- package/src/daemon/ipc-contract/inbox.ts +2 -0
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +0 -5
- package/src/daemon/ride-shotgun-handler.ts +139 -25
- package/src/daemon/session-agent-loop-handlers.ts +100 -0
- package/src/daemon/session-agent-loop.ts +72 -0
- package/src/daemon/session-tool-setup.ts +7 -0
- package/src/daemon/session.ts +23 -1
- package/src/daemon/tool-side-effects.ts +39 -1
- package/src/email/service.ts +59 -2
- package/src/index.ts +2 -60
- package/src/mcp/mcp-oauth-provider.ts +90 -8
- package/src/media/app-icon-generator.ts +86 -0
- package/src/memory/db-init.ts +11 -0
- package/src/memory/llm-usage-store.ts +186 -0
- package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
- package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/schema-migration.ts +1 -0
- package/src/memory/shared-app-links-store.ts +1 -1
- package/src/messaging/registry.ts +27 -0
- package/src/notifications/README.md +79 -70
- package/src/notifications/broadcaster.ts +2 -1
- package/src/notifications/conversation-pairing.ts +147 -13
- package/src/notifications/copy-composer.ts +7 -3
- package/src/notifications/destination-resolver.ts +14 -1
- package/src/notifications/emit-signal.ts +3 -2
- package/src/notifications/signal.ts +105 -1
- package/src/notifications/types.ts +16 -0
- package/src/permissions/checker.ts +29 -3
- package/src/permissions/prompter.ts +11 -3
- package/src/runtime/access-request-helper.ts +2 -1
- package/src/runtime/auth/route-policy.ts +7 -1
- package/src/runtime/channel-invite-transport.ts +40 -63
- package/src/runtime/channel-invite-transports/email.ts +13 -39
- package/src/runtime/channel-invite-transports/slack.ts +5 -34
- package/src/runtime/channel-invite-transports/sms.ts +8 -29
- package/src/runtime/channel-invite-transports/telegram.ts +69 -28
- package/src/runtime/channel-invite-transports/voice.ts +0 -7
- package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
- package/src/runtime/channel-readiness-service.ts +202 -45
- package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
- package/src/runtime/guardian-outbound-actions.ts +8 -5
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-instruction-generator.ts +178 -0
- package/src/runtime/invite-service.ts +22 -25
- package/src/runtime/migrations/migration-transport.ts +13 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
- package/src/runtime/routes/channel-readiness-routes.ts +30 -11
- package/src/runtime/routes/contact-routes.ts +54 -26
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +1 -1
- package/src/runtime/routes/invite-routes.ts +1 -1
- package/src/runtime/routes/secret-routes.ts +31 -7
- package/src/runtime/routes/twilio-routes.ts +32 -1
- package/src/runtime/routes/usage-routes.ts +114 -0
- package/src/runtime/tool-grant-request-helper.ts +2 -1
- package/src/security/encrypted-store.ts +9 -5
- package/src/security/keychain-broker-client.ts +393 -0
- package/src/security/secure-keys.ts +106 -321
- package/src/tools/apps/executors.ts +73 -0
- package/src/tools/browser/auto-navigate.ts +15 -6
- package/src/tools/browser/chrome-cdp.ts +211 -0
- package/src/tools/browser/network-recorder.test.ts +83 -0
- package/src/tools/browser/network-recorder.ts +8 -7
- package/src/tools/browser/x-auto-navigate.ts +12 -6
- package/src/tools/credentials/policy-types.ts +24 -0
- package/src/tools/credentials/vault.ts +22 -27
- package/src/tools/network/script-proxy/session-manager.ts +47 -3
- package/src/tools/permission-checker.ts +1 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +1 -2
- package/src/tools/watch/watch-state.ts +2 -0
- package/src/__tests__/key-migration.test.ts +0 -240
- package/src/__tests__/keychain.test.ts +0 -286
- package/src/cli/core-commands.ts +0 -899
- package/src/security/keychain-to-encrypted-migration.ts +0 -66
- package/src/security/keychain.ts +0 -490
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Signature verification for .
|
|
2
|
+
* Signature verification for .vellum archives.
|
|
3
3
|
*
|
|
4
4
|
* Checks bundle integrity and Ed25519 signature validity.
|
|
5
5
|
*/
|
|
@@ -39,9 +39,9 @@ function sortKeysDeep(obj: unknown): unknown {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Verify the signature and integrity of a .
|
|
42
|
+
* Verify the signature and integrity of a .vellum bundle.
|
|
43
43
|
*
|
|
44
|
-
* @param zipPath - Path to the .
|
|
44
|
+
* @param zipPath - Path to the .vellum zip archive.
|
|
45
45
|
* @param trustedPublicKeys - Optional map of keyId -> base64-encoded public key for verification.
|
|
46
46
|
* If not provided, signature is checked structurally but returns 'signed' at best.
|
|
47
47
|
* @returns The verification result with trust tier and signer info.
|
package/src/channels/config.ts
CHANGED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Assistant CLI — Agent Instructions
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Commands in `assistant/src/cli/` are scoped to a **single running assistant instance**. They operate on the assistant's local state — config, memory, contacts, trust rules, sessions, autonomy, etc. — and run within the context of the assistant's workspace.
|
|
6
|
+
|
|
7
|
+
This contrasts with `cli/`, which manages the **lifecycle of assistant instances** (create, start, stop, delete) and operates across instances. See `cli/AGENTS.md`.
|
|
8
|
+
|
|
9
|
+
## When a command belongs here vs `cli/`
|
|
10
|
+
|
|
11
|
+
| `assistant/src/cli/` (this directory) | `cli/` |
|
|
12
|
+
| --------------------------------------------------- | ----------------------------------------------- |
|
|
13
|
+
| Operates within a single assistant's workspace | Operates on or across assistant instances |
|
|
14
|
+
| Manages instance-local state (config, memory, etc.) | Manages lifecycle (create, start, stop, delete) |
|
|
15
|
+
| Implicitly scoped to the running assistant | Requires specifying which assistant to target |
|
|
16
|
+
| May require or start the daemon | Works without an assistant process running |
|
|
17
|
+
|
|
18
|
+
Examples: `config`, `contacts`, `memory`, `autonomy`, `sessions`, `doctor` belong here. `hatch`, `wake`, `sleep`, `retire`, `ps`, `ssh` belong in `cli/`.
|
|
19
|
+
|
|
20
|
+
## Conventions
|
|
21
|
+
|
|
22
|
+
- Commands use [Commander.js](https://github.com/tj/commander.js) and follow the `registerXCommand(program: Command)` pattern.
|
|
23
|
+
- Each command module exports a registration function that attaches subcommands to the program.
|
|
24
|
+
- Register new commands in `assistant/src/cli/program.ts` inside the `buildCliProgram()` function by importing and calling the registration function.
|
|
25
|
+
- Use `getCliLogger("cli")` for output (not raw `console.log`).
|
|
26
|
+
|
|
27
|
+
## Service calls — no gateway proxying
|
|
28
|
+
|
|
29
|
+
CLI commands must call the service/store layer directly — the same functions that the HTTP route handlers in `runtime/routes/` call. Do not proxy through the gateway HTTP API.
|
|
30
|
+
|
|
31
|
+
Both the gateway routes and the CLI are thin wrappers around the same shared business logic. For example, `runtime/routes/invite-routes.ts` delegates to `runtime/invite-service.ts`, and `runtime/routes/contact-routes.ts` delegates to `contacts/contact-store.ts`. CLI commands should import and call those same service modules directly.
|
|
32
|
+
|
|
33
|
+
This avoids a dependency on the gateway process being running and removes an unnecessary network hop.
|
|
34
|
+
|
|
35
|
+
## Help Text Standards
|
|
36
|
+
|
|
37
|
+
Every command at every level (namespace, subcommand, nested subcommand) must have
|
|
38
|
+
high-quality `--help` output optimized for AI/LLM consumption. Help text is a
|
|
39
|
+
primary interface — both humans and AI agents read it to understand what a command
|
|
40
|
+
does and how to use it.
|
|
41
|
+
|
|
42
|
+
### Requirements
|
|
43
|
+
|
|
44
|
+
1. **Top-level namespace**: Use `.description()` with a concise one-liner, then
|
|
45
|
+
`.addHelpText("after", ...)` with:
|
|
46
|
+
- A brief explanation of the domain and key concepts (e.g. naming conventions,
|
|
47
|
+
storage model)
|
|
48
|
+
- 3-4 representative examples covering the most common workflows
|
|
49
|
+
|
|
50
|
+
2. **Each subcommand**: Use `.description()` with a one-liner, then
|
|
51
|
+
`.addHelpText("after", ...)` with:
|
|
52
|
+
- An `Arguments:` block explaining each positional argument with its format
|
|
53
|
+
and constraints
|
|
54
|
+
- Behavioral notes (what happens on update vs create, what gets deleted, etc.)
|
|
55
|
+
- 2-3 concrete `Examples:` showing exact invocations with realistic values
|
|
56
|
+
|
|
57
|
+
3. **Write for machines**: Help text is frequently parsed by AI agents to decide
|
|
58
|
+
which command to run and how. Be precise about formats (`service:field`),
|
|
59
|
+
constraints (required vs optional), and side effects. Avoid vague language
|
|
60
|
+
like "configure settings" — say exactly what is configured and where it's stored.
|
|
61
|
+
|
|
62
|
+
4. **Use Commander's `.addHelpText("after", ...)`** for extended help. Don't
|
|
63
|
+
cram everything into `.description()`.
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
afterAll,
|
|
6
|
+
beforeAll,
|
|
7
|
+
beforeEach,
|
|
8
|
+
describe,
|
|
9
|
+
expect,
|
|
10
|
+
mock,
|
|
11
|
+
test,
|
|
12
|
+
} from "bun:test";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Test isolation: in-memory SQLite via temp directory
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const testDir = mkdtempSync(join(tmpdir(), "cli-notifications-test-"));
|
|
19
|
+
|
|
20
|
+
mock.module("../../util/platform.js", () => ({
|
|
21
|
+
getRootDir: () => testDir,
|
|
22
|
+
getDataDir: () => testDir,
|
|
23
|
+
isMacOS: () => process.platform === "darwin",
|
|
24
|
+
isLinux: () => process.platform === "linux",
|
|
25
|
+
isWindows: () => process.platform === "win32",
|
|
26
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
27
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
28
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
29
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
30
|
+
ensureDataDir: () => {},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module("../../util/logger.js", () => ({
|
|
34
|
+
getLogger: () =>
|
|
35
|
+
new Proxy({} as Record<string, unknown>, {
|
|
36
|
+
get: () => () => {},
|
|
37
|
+
}),
|
|
38
|
+
getCliLogger: () =>
|
|
39
|
+
new Proxy({} as Record<string, unknown>, {
|
|
40
|
+
get: () => () => {},
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// Track emitNotificationSignal calls
|
|
45
|
+
const emitSignalCalls: Array<Record<string, unknown>> = [];
|
|
46
|
+
mock.module("../../notifications/emit-signal.js", () => ({
|
|
47
|
+
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
48
|
+
emitSignalCalls.push(params);
|
|
49
|
+
return {
|
|
50
|
+
signalId: "mock-id",
|
|
51
|
+
deduplicated: false,
|
|
52
|
+
dispatched: true,
|
|
53
|
+
reason: "ok",
|
|
54
|
+
deliveryResults: [],
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
mock.module("../../channels/config.js", () => ({
|
|
60
|
+
getDeliverableChannels: () => ["vellum", "telegram", "slack"],
|
|
61
|
+
getChannelPolicy: () => ({
|
|
62
|
+
notification: {
|
|
63
|
+
deliveryEnabled: true,
|
|
64
|
+
conversationStrategy: "start_new_conversation",
|
|
65
|
+
},
|
|
66
|
+
invite: { codeRedemptionEnabled: false },
|
|
67
|
+
}),
|
|
68
|
+
isNotificationDeliverable: () => true,
|
|
69
|
+
getConversationStrategy: () => "start_new_conversation",
|
|
70
|
+
getChannelInvitePolicy: () => ({ codeRedemptionEnabled: false }),
|
|
71
|
+
isInviteCodeRedemptionEnabled: () => false,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
import { Command } from "commander";
|
|
75
|
+
|
|
76
|
+
import { getDb, initializeDb, resetDb } from "../../memory/db.js";
|
|
77
|
+
import { createEvent } from "../../notifications/events-store.js";
|
|
78
|
+
import { registerNotificationsCommand } from "../notifications.js";
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
interface CommandResult {
|
|
85
|
+
parsed: Record<string, unknown>;
|
|
86
|
+
exitCode: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run a notifications subcommand and capture the JSON output.
|
|
91
|
+
* Always passes --json to get compact, single-line JSON output and suppress log messages.
|
|
92
|
+
*
|
|
93
|
+
* Follows the same process.exitCode pattern as credential-cli.test.ts:
|
|
94
|
+
* reset to 0, capture, then reset back to 0 so bun test exits cleanly.
|
|
95
|
+
*/
|
|
96
|
+
async function runCommand(args: string[]): Promise<CommandResult> {
|
|
97
|
+
const chunks: string[] = [];
|
|
98
|
+
const originalWrite = process.stdout.write;
|
|
99
|
+
|
|
100
|
+
process.exitCode = 0;
|
|
101
|
+
|
|
102
|
+
process.stdout.write = ((chunk: string | Buffer) => {
|
|
103
|
+
chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
|
|
104
|
+
return true;
|
|
105
|
+
}) as typeof process.stdout.write;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const program = new Command();
|
|
109
|
+
program.exitOverride();
|
|
110
|
+
registerNotificationsCommand(program);
|
|
111
|
+
await program.parseAsync([
|
|
112
|
+
"node",
|
|
113
|
+
"test",
|
|
114
|
+
"notifications",
|
|
115
|
+
"--json",
|
|
116
|
+
...args,
|
|
117
|
+
]);
|
|
118
|
+
} catch {
|
|
119
|
+
// Commander throws on .exitOverride() for --help/errors; ignore
|
|
120
|
+
} finally {
|
|
121
|
+
process.stdout.write = originalWrite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const exitCode = process.exitCode ?? 0;
|
|
125
|
+
process.exitCode = 0;
|
|
126
|
+
|
|
127
|
+
const output = chunks.join("");
|
|
128
|
+
const firstLine = output.trim().split("\n")[0];
|
|
129
|
+
const parsed = firstLine
|
|
130
|
+
? (JSON.parse(firstLine) as Record<string, unknown>)
|
|
131
|
+
: {};
|
|
132
|
+
|
|
133
|
+
return { parsed, exitCode };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Setup / teardown
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
beforeAll(() => {
|
|
141
|
+
initializeDb();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
emitSignalCalls.length = 0;
|
|
146
|
+
process.exitCode = 0;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
afterAll(() => {
|
|
150
|
+
resetDb();
|
|
151
|
+
try {
|
|
152
|
+
rmSync(testDir, { recursive: true });
|
|
153
|
+
} catch {
|
|
154
|
+
/* best effort */
|
|
155
|
+
}
|
|
156
|
+
process.exitCode = 0;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// send subcommand
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
describe("notifications send", () => {
|
|
164
|
+
test("send with valid args emits signal", async () => {
|
|
165
|
+
const { parsed, exitCode } = await runCommand([
|
|
166
|
+
"send",
|
|
167
|
+
"--source-channel",
|
|
168
|
+
"assistant_tool",
|
|
169
|
+
"--source-event-name",
|
|
170
|
+
"user.send_notification",
|
|
171
|
+
"--message",
|
|
172
|
+
"Hello",
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
expect(exitCode).toBe(0);
|
|
176
|
+
expect(parsed.ok).toBe(true);
|
|
177
|
+
expect(parsed.signalId).toBe("mock-id");
|
|
178
|
+
|
|
179
|
+
expect(emitSignalCalls).toHaveLength(1);
|
|
180
|
+
const call = emitSignalCalls[0];
|
|
181
|
+
expect(call.sourceChannel).toBe("assistant_tool");
|
|
182
|
+
expect(call.sourceEventName).toBe("user.send_notification");
|
|
183
|
+
const payload = call.contextPayload as Record<string, unknown>;
|
|
184
|
+
expect(payload.requestedMessage).toBe("Hello");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("send passes urgency and attention hints", async () => {
|
|
188
|
+
const { parsed, exitCode } = await runCommand([
|
|
189
|
+
"send",
|
|
190
|
+
"--source-channel",
|
|
191
|
+
"scheduler",
|
|
192
|
+
"--source-event-name",
|
|
193
|
+
"reminder.fired",
|
|
194
|
+
"--message",
|
|
195
|
+
"Test",
|
|
196
|
+
"--urgency",
|
|
197
|
+
"high",
|
|
198
|
+
"--requires-action",
|
|
199
|
+
"--is-async-background",
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
expect(exitCode).toBe(0);
|
|
203
|
+
expect(parsed.ok).toBe(true);
|
|
204
|
+
|
|
205
|
+
expect(emitSignalCalls).toHaveLength(1);
|
|
206
|
+
const hints = emitSignalCalls[0].attentionHints as Record<string, unknown>;
|
|
207
|
+
expect(hints.urgency).toBe("high");
|
|
208
|
+
expect(hints.requiresAction).toBe(true);
|
|
209
|
+
expect(hints.isAsyncBackground).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("send passes preferred channels", async () => {
|
|
213
|
+
const { parsed, exitCode } = await runCommand([
|
|
214
|
+
"send",
|
|
215
|
+
"--source-channel",
|
|
216
|
+
"assistant_tool",
|
|
217
|
+
"--source-event-name",
|
|
218
|
+
"user.send_notification",
|
|
219
|
+
"--message",
|
|
220
|
+
"Hello",
|
|
221
|
+
"--preferred-channels",
|
|
222
|
+
"telegram,slack",
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
expect(exitCode).toBe(0);
|
|
226
|
+
expect(parsed.ok).toBe(true);
|
|
227
|
+
|
|
228
|
+
expect(emitSignalCalls).toHaveLength(1);
|
|
229
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
230
|
+
string,
|
|
231
|
+
unknown
|
|
232
|
+
>;
|
|
233
|
+
expect(payload.preferredChannels).toEqual(["telegram", "slack"]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("send rejects invalid source channel", async () => {
|
|
237
|
+
const { parsed, exitCode } = await runCommand([
|
|
238
|
+
"send",
|
|
239
|
+
"--source-channel",
|
|
240
|
+
"bogus",
|
|
241
|
+
"--source-event-name",
|
|
242
|
+
"user.send_notification",
|
|
243
|
+
"--message",
|
|
244
|
+
"Hello",
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
expect(exitCode).toBe(1);
|
|
248
|
+
expect(parsed.ok).toBe(false);
|
|
249
|
+
expect(parsed.error).toContain("bogus");
|
|
250
|
+
// Should list valid channels from the registry
|
|
251
|
+
expect(parsed.error).toContain("assistant_tool");
|
|
252
|
+
expect(parsed.error).toContain("scheduler");
|
|
253
|
+
expect(parsed.error).toContain("watcher");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("send rejects invalid source event name", async () => {
|
|
257
|
+
const { parsed, exitCode } = await runCommand([
|
|
258
|
+
"send",
|
|
259
|
+
"--source-channel",
|
|
260
|
+
"assistant_tool",
|
|
261
|
+
"--source-event-name",
|
|
262
|
+
"bogus.event",
|
|
263
|
+
"--message",
|
|
264
|
+
"Hello",
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
expect(exitCode).toBe(1);
|
|
268
|
+
expect(parsed.ok).toBe(false);
|
|
269
|
+
expect(parsed.error).toContain("bogus.event");
|
|
270
|
+
// Should list valid event names from the registry
|
|
271
|
+
expect(parsed.error).toContain("user.send_notification");
|
|
272
|
+
expect(parsed.error).toContain("reminder.fired");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("send rejects invalid urgency", async () => {
|
|
276
|
+
const { parsed, exitCode } = await runCommand([
|
|
277
|
+
"send",
|
|
278
|
+
"--source-channel",
|
|
279
|
+
"assistant_tool",
|
|
280
|
+
"--source-event-name",
|
|
281
|
+
"user.send_notification",
|
|
282
|
+
"--message",
|
|
283
|
+
"Hello",
|
|
284
|
+
"--urgency",
|
|
285
|
+
"invalid",
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
expect(exitCode).toBe(1);
|
|
289
|
+
expect(parsed.ok).toBe(false);
|
|
290
|
+
expect(parsed.error).toContain("invalid");
|
|
291
|
+
expect(parsed.error).toContain("low");
|
|
292
|
+
expect(parsed.error).toContain("medium");
|
|
293
|
+
expect(parsed.error).toContain("high");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("send rejects invalid preferred channel", async () => {
|
|
297
|
+
const { parsed, exitCode } = await runCommand([
|
|
298
|
+
"send",
|
|
299
|
+
"--source-channel",
|
|
300
|
+
"assistant_tool",
|
|
301
|
+
"--source-event-name",
|
|
302
|
+
"user.send_notification",
|
|
303
|
+
"--message",
|
|
304
|
+
"Hello",
|
|
305
|
+
"--preferred-channels",
|
|
306
|
+
"badchannel",
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
expect(exitCode).toBe(1);
|
|
310
|
+
expect(parsed.ok).toBe(false);
|
|
311
|
+
expect(parsed.error).toContain("badchannel");
|
|
312
|
+
// Should list valid deliverable channels from the mock
|
|
313
|
+
expect(parsed.error).toContain("vellum");
|
|
314
|
+
expect(parsed.error).toContain("telegram");
|
|
315
|
+
expect(parsed.error).toContain("slack");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// list subcommand
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
describe("notifications list", () => {
|
|
324
|
+
beforeEach(() => {
|
|
325
|
+
getDb().run("DELETE FROM notification_events");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("list returns empty array when no events", async () => {
|
|
329
|
+
const { parsed, exitCode } = await runCommand(["list"]);
|
|
330
|
+
|
|
331
|
+
expect(exitCode).toBe(0);
|
|
332
|
+
expect(parsed.ok).toBe(true);
|
|
333
|
+
expect(parsed.events).toEqual([]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("list returns events", async () => {
|
|
337
|
+
createEvent({
|
|
338
|
+
id: `evt-${Date.now()}-1`,
|
|
339
|
+
sourceEventName: "user.send_notification",
|
|
340
|
+
sourceChannel: "assistant_tool",
|
|
341
|
+
sourceSessionId: "session-1",
|
|
342
|
+
attentionHints: {
|
|
343
|
+
requiresAction: true,
|
|
344
|
+
urgency: "medium",
|
|
345
|
+
isAsyncBackground: false,
|
|
346
|
+
visibleInSourceNow: false,
|
|
347
|
+
},
|
|
348
|
+
payload: { requestedMessage: "Test event" },
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const { parsed, exitCode } = await runCommand(["list"]);
|
|
352
|
+
|
|
353
|
+
expect(exitCode).toBe(0);
|
|
354
|
+
expect(parsed.ok).toBe(true);
|
|
355
|
+
const events = parsed.events as Array<Record<string, unknown>>;
|
|
356
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
357
|
+
expect(events[0].sourceEventName).toBe("user.send_notification");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("list respects --limit", async () => {
|
|
361
|
+
for (let i = 0; i < 5; i++) {
|
|
362
|
+
createEvent({
|
|
363
|
+
id: `evt-limit-${Date.now()}-${i}`,
|
|
364
|
+
sourceEventName: "user.send_notification",
|
|
365
|
+
sourceChannel: "assistant_tool",
|
|
366
|
+
sourceSessionId: `session-limit-${i}`,
|
|
367
|
+
attentionHints: {
|
|
368
|
+
requiresAction: true,
|
|
369
|
+
urgency: "medium",
|
|
370
|
+
isAsyncBackground: false,
|
|
371
|
+
visibleInSourceNow: false,
|
|
372
|
+
},
|
|
373
|
+
payload: { requestedMessage: `Limit test ${i}` },
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { parsed, exitCode } = await runCommand(["list", "--limit", "2"]);
|
|
378
|
+
|
|
379
|
+
expect(exitCode).toBe(0);
|
|
380
|
+
expect(parsed.ok).toBe(true);
|
|
381
|
+
const events = parsed.events as Array<Record<string, unknown>>;
|
|
382
|
+
expect(events).toHaveLength(2);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("list filters by --source-event-name", async () => {
|
|
386
|
+
createEvent({
|
|
387
|
+
id: `evt-filter-notif-${Date.now()}`,
|
|
388
|
+
sourceEventName: "user.send_notification",
|
|
389
|
+
sourceChannel: "assistant_tool",
|
|
390
|
+
sourceSessionId: "session-filter-1",
|
|
391
|
+
attentionHints: {
|
|
392
|
+
requiresAction: true,
|
|
393
|
+
urgency: "medium",
|
|
394
|
+
isAsyncBackground: false,
|
|
395
|
+
visibleInSourceNow: false,
|
|
396
|
+
},
|
|
397
|
+
payload: { requestedMessage: "Notification event" },
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
createEvent({
|
|
401
|
+
id: `evt-filter-reminder-${Date.now()}`,
|
|
402
|
+
sourceEventName: "reminder.fired",
|
|
403
|
+
sourceChannel: "scheduler",
|
|
404
|
+
sourceSessionId: "session-filter-2",
|
|
405
|
+
attentionHints: {
|
|
406
|
+
requiresAction: true,
|
|
407
|
+
urgency: "high",
|
|
408
|
+
isAsyncBackground: false,
|
|
409
|
+
visibleInSourceNow: false,
|
|
410
|
+
},
|
|
411
|
+
payload: { requestedMessage: "Reminder event" },
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const { parsed, exitCode } = await runCommand([
|
|
415
|
+
"list",
|
|
416
|
+
"--source-event-name",
|
|
417
|
+
"user.send_notification",
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
expect(exitCode).toBe(0);
|
|
421
|
+
expect(parsed.ok).toBe(true);
|
|
422
|
+
const events = parsed.events as Array<Record<string, unknown>>;
|
|
423
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
424
|
+
for (const event of events) {
|
|
425
|
+
expect(event.sourceEventName).toBe("user.send_notification");
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test("list accepts custom (non-registered) source event names", async () => {
|
|
430
|
+
createEvent({
|
|
431
|
+
id: `evt-custom-${Date.now()}`,
|
|
432
|
+
sourceEventName: "custom.my_event",
|
|
433
|
+
sourceChannel: "assistant_tool",
|
|
434
|
+
sourceSessionId: "session-custom",
|
|
435
|
+
attentionHints: {
|
|
436
|
+
requiresAction: true,
|
|
437
|
+
urgency: "medium",
|
|
438
|
+
isAsyncBackground: false,
|
|
439
|
+
visibleInSourceNow: false,
|
|
440
|
+
},
|
|
441
|
+
payload: { requestedMessage: "Custom event" },
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const { parsed, exitCode } = await runCommand([
|
|
445
|
+
"list",
|
|
446
|
+
"--source-event-name",
|
|
447
|
+
"custom.my_event",
|
|
448
|
+
]);
|
|
449
|
+
|
|
450
|
+
expect(exitCode).toBe(0);
|
|
451
|
+
expect(parsed.ok).toBe(true);
|
|
452
|
+
const events = parsed.events as Array<Record<string, unknown>>;
|
|
453
|
+
expect(events.length).toBeGreaterThanOrEqual(1);
|
|
454
|
+
for (const event of events) {
|
|
455
|
+
expect(event.sourceEventName).toBe("custom.my_event");
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("list returns empty for non-matching custom event name", async () => {
|
|
460
|
+
const { parsed, exitCode } = await runCommand([
|
|
461
|
+
"list",
|
|
462
|
+
"--source-event-name",
|
|
463
|
+
"nonexistent.event",
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
expect(exitCode).toBe(0);
|
|
467
|
+
expect(parsed.ok).toBe(true);
|
|
468
|
+
expect(parsed.events).toEqual([]);
|
|
469
|
+
});
|
|
470
|
+
});
|