@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
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
deleteSecureKey,
|
|
5
|
+
getSecureKey,
|
|
6
|
+
setSecureKey,
|
|
7
|
+
} from "../security/secure-keys.js";
|
|
8
|
+
import {
|
|
9
|
+
assertMetadataWritable,
|
|
10
|
+
type CredentialMetadata,
|
|
11
|
+
deleteCredentialMetadata,
|
|
12
|
+
getCredentialMetadata,
|
|
13
|
+
getCredentialMetadataById,
|
|
14
|
+
listCredentialMetadata,
|
|
15
|
+
upsertCredentialMetadata,
|
|
16
|
+
} from "../tools/credentials/metadata-store.js";
|
|
17
|
+
import { getCliLogger } from "../util/logger.js";
|
|
18
|
+
import { shouldOutputJson, writeOutput } from "./integrations.js";
|
|
19
|
+
|
|
20
|
+
const log = getCliLogger("cli");
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Shared helpers
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse a `service:field` name string. Returns the parsed pair or undefined
|
|
28
|
+
* if the format is invalid (no colon or empty segments).
|
|
29
|
+
*/
|
|
30
|
+
function parseCredentialName(
|
|
31
|
+
name: string,
|
|
32
|
+
): { service: string; field: string } | undefined {
|
|
33
|
+
const colonIndex = name.indexOf(":");
|
|
34
|
+
if (colonIndex <= 0 || colonIndex >= name.length - 1) return undefined;
|
|
35
|
+
return {
|
|
36
|
+
service: name.slice(0, colonIndex),
|
|
37
|
+
field: name.slice(colonIndex + 1),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Scrub a secret value for display. Shows `****` + last 4 characters for
|
|
43
|
+
* secrets longer than 4 chars, `****` for secrets 4 chars or fewer, and
|
|
44
|
+
* `(not set)` when no secret is stored.
|
|
45
|
+
*/
|
|
46
|
+
function scrubSecret(secret: string | undefined): string {
|
|
47
|
+
if (secret == null || secret.length === 0) return "(not set)";
|
|
48
|
+
if (secret.length <= 4) return "****";
|
|
49
|
+
return "****" + secret.slice(-4);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a structured credential output object suitable for both `inspect`
|
|
54
|
+
* and `list` responses. Produces an identical shape for every credential.
|
|
55
|
+
*/
|
|
56
|
+
function buildCredentialOutput(
|
|
57
|
+
metadata: CredentialMetadata,
|
|
58
|
+
secret: string | undefined,
|
|
59
|
+
): Record<string, unknown> {
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
service: metadata.service,
|
|
63
|
+
field: metadata.field,
|
|
64
|
+
credentialId: metadata.credentialId,
|
|
65
|
+
scrubbedValue: scrubSecret(secret),
|
|
66
|
+
hasSecret: secret != null && secret.length > 0,
|
|
67
|
+
alias: metadata.alias ?? null,
|
|
68
|
+
usageDescription: metadata.usageDescription ?? null,
|
|
69
|
+
allowedTools: metadata.allowedTools,
|
|
70
|
+
allowedDomains: metadata.allowedDomains,
|
|
71
|
+
accountInfo: metadata.accountInfo ?? null,
|
|
72
|
+
grantedScopes: metadata.grantedScopes ?? null,
|
|
73
|
+
expiresAt: metadata.expiresAt
|
|
74
|
+
? new Date(metadata.expiresAt).toISOString()
|
|
75
|
+
: null,
|
|
76
|
+
createdAt: new Date(metadata.createdAt).toISOString(),
|
|
77
|
+
updatedAt: new Date(metadata.updatedAt).toISOString(),
|
|
78
|
+
injectionTemplateCount: metadata.injectionTemplates?.length ?? 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Print a human-readable view of a single credential to the logger.
|
|
84
|
+
*/
|
|
85
|
+
function printCredentialHuman(output: Record<string, unknown>): void {
|
|
86
|
+
log.info(` ${output.service}:${output.field}`);
|
|
87
|
+
log.info(` ID: ${output.credentialId}`);
|
|
88
|
+
log.info(` Value: ${output.scrubbedValue}`);
|
|
89
|
+
if (output.alias) log.info(` Label: ${output.alias}`);
|
|
90
|
+
if (output.usageDescription)
|
|
91
|
+
log.info(` Description: ${output.usageDescription}`);
|
|
92
|
+
if (
|
|
93
|
+
Array.isArray(output.allowedTools) &&
|
|
94
|
+
(output.allowedTools as string[]).length > 0
|
|
95
|
+
)
|
|
96
|
+
log.info(
|
|
97
|
+
` Tools: ${(output.allowedTools as string[]).join(", ")}`,
|
|
98
|
+
);
|
|
99
|
+
if (
|
|
100
|
+
Array.isArray(output.allowedDomains) &&
|
|
101
|
+
(output.allowedDomains as string[]).length > 0
|
|
102
|
+
)
|
|
103
|
+
log.info(
|
|
104
|
+
` Domains: ${(output.allowedDomains as string[]).join(", ")}`,
|
|
105
|
+
);
|
|
106
|
+
if (output.accountInfo) log.info(` Account: ${output.accountInfo}`);
|
|
107
|
+
if (output.grantedScopes)
|
|
108
|
+
log.info(
|
|
109
|
+
` Scopes: ${(output.grantedScopes as string[]).join(", ")}`,
|
|
110
|
+
);
|
|
111
|
+
if (output.expiresAt) log.info(` Expires: ${output.expiresAt}`);
|
|
112
|
+
log.info(` Created: ${output.createdAt}`);
|
|
113
|
+
log.info(` Updated: ${output.updatedAt}`);
|
|
114
|
+
if ((output.injectionTemplateCount as number) > 0)
|
|
115
|
+
log.info(` Templates: ${output.injectionTemplateCount}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Command registration
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
export function registerCredentialsCommand(program: Command): void {
|
|
123
|
+
const credential = program
|
|
124
|
+
.command("credentials")
|
|
125
|
+
.description(
|
|
126
|
+
"Manage credentials in the encrypted vault (API keys, tokens, passwords)",
|
|
127
|
+
)
|
|
128
|
+
.option("--json", "Machine-readable compact JSON output");
|
|
129
|
+
|
|
130
|
+
credential.addHelpText(
|
|
131
|
+
"after",
|
|
132
|
+
`
|
|
133
|
+
Credentials are identified by name in service:field format, matching the
|
|
134
|
+
storage convention used internally (credential:{service}:{field}):
|
|
135
|
+
|
|
136
|
+
twilio:account_sid Twilio account SID
|
|
137
|
+
twilio:auth_token Twilio auth token
|
|
138
|
+
telegram:bot_token Telegram bot token
|
|
139
|
+
slack_channel:bot_token Slack channel bot token
|
|
140
|
+
github:token GitHub personal access token
|
|
141
|
+
agentmail:api_key AgentMail API key
|
|
142
|
+
|
|
143
|
+
Secrets are stored in AES-256-GCM encrypted storage. Metadata (policy,
|
|
144
|
+
timestamps, labels) is tracked separately and never contains secret values.
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
$ vellum credentials list
|
|
148
|
+
$ vellum credentials list --search twilio
|
|
149
|
+
$ vellum credentials set twilio:account_sid AC1234567890
|
|
150
|
+
$ vellum credentials inspect twilio:account_sid
|
|
151
|
+
$ vellum credentials delete twilio:auth_token`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// -------------------------------------------------------------------------
|
|
155
|
+
// list
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
credential
|
|
159
|
+
.command("list")
|
|
160
|
+
.description("List all stored credentials with metadata and masked values")
|
|
161
|
+
.option(
|
|
162
|
+
"--search <query>",
|
|
163
|
+
"Filter credentials by substring match on service, field, label, or description",
|
|
164
|
+
)
|
|
165
|
+
.addHelpText(
|
|
166
|
+
"after",
|
|
167
|
+
`
|
|
168
|
+
Lists all credentials in the vault. Each entry includes the same fields as
|
|
169
|
+
"inspect" — scrubbed value, timestamps, policy, and metadata.
|
|
170
|
+
|
|
171
|
+
The --search flag filters results by case-insensitive substring match against
|
|
172
|
+
the credential's service name, field name, label, or description. For example, --search
|
|
173
|
+
twilio matches twilio:account_sid, twilio:auth_token, and twilio:phone_number.
|
|
174
|
+
|
|
175
|
+
Returns an array of credential objects. Empty array if no credentials exist
|
|
176
|
+
or none match the search query.
|
|
177
|
+
|
|
178
|
+
Examples:
|
|
179
|
+
$ vellum credentials list
|
|
180
|
+
$ vellum credentials list --search twilio
|
|
181
|
+
$ vellum credentials list --search bot_token
|
|
182
|
+
$ vellum credentials list --json`,
|
|
183
|
+
)
|
|
184
|
+
.action((opts: { search?: string }, cmd: Command) => {
|
|
185
|
+
try {
|
|
186
|
+
let allMetadata = listCredentialMetadata();
|
|
187
|
+
|
|
188
|
+
if (opts.search) {
|
|
189
|
+
const query = opts.search.toLowerCase();
|
|
190
|
+
allMetadata = allMetadata.filter((m) => {
|
|
191
|
+
const service = m.service.toLowerCase();
|
|
192
|
+
const field = m.field.toLowerCase();
|
|
193
|
+
const alias = (m.alias ?? "").toLowerCase();
|
|
194
|
+
const description = (m.usageDescription ?? "").toLowerCase();
|
|
195
|
+
return (
|
|
196
|
+
service.includes(query) ||
|
|
197
|
+
field.includes(query) ||
|
|
198
|
+
alias.includes(query) ||
|
|
199
|
+
description.includes(query)
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const credentials = allMetadata.map((m) => {
|
|
205
|
+
const secret = getSecureKey(`credential:${m.service}:${m.field}`);
|
|
206
|
+
return buildCredentialOutput(m, secret);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
writeOutput(cmd, { ok: true, credentials });
|
|
210
|
+
|
|
211
|
+
if (!shouldOutputJson(cmd)) {
|
|
212
|
+
if (credentials.length === 0) {
|
|
213
|
+
log.info("No credentials found");
|
|
214
|
+
} else {
|
|
215
|
+
log.info(`${credentials.length} credential(s):\n`);
|
|
216
|
+
for (const cred of credentials) {
|
|
217
|
+
printCredentialHuman(cred);
|
|
218
|
+
log.info("");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
+
writeOutput(cmd, { ok: false, error: message });
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// -------------------------------------------------------------------------
|
|
230
|
+
// set
|
|
231
|
+
// -------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
credential
|
|
234
|
+
.command("set <name> <value>")
|
|
235
|
+
.description("Store a secret and create or update its metadata")
|
|
236
|
+
.option("--label <label>", 'Human-friendly label (e.g. "prod", "work")')
|
|
237
|
+
.option("--description <description>", "What this credential is used for")
|
|
238
|
+
.option(
|
|
239
|
+
"--allowed-tools <tools>",
|
|
240
|
+
"Comma-separated tool names that may use this credential",
|
|
241
|
+
)
|
|
242
|
+
.addHelpText(
|
|
243
|
+
"after",
|
|
244
|
+
`
|
|
245
|
+
Arguments:
|
|
246
|
+
name Credential name in service:field format (e.g. twilio:account_sid)
|
|
247
|
+
value The secret value to store
|
|
248
|
+
|
|
249
|
+
If the credential already exists, the secret is overwritten and metadata is
|
|
250
|
+
updated with any provided flags. Omitted flags leave existing metadata intact.
|
|
251
|
+
|
|
252
|
+
Examples:
|
|
253
|
+
$ vellum credentials set twilio:account_sid AC1234567890
|
|
254
|
+
$ vellum credentials set fal:api_key key_live_abc --label "fal-prod" --description "Image generation"
|
|
255
|
+
$ vellum credentials set github:token ghp_abc --allowed-tools "bash,host_bash"`,
|
|
256
|
+
)
|
|
257
|
+
.action(
|
|
258
|
+
(
|
|
259
|
+
name: string,
|
|
260
|
+
value: string,
|
|
261
|
+
opts: { label?: string; description?: string; allowedTools?: string },
|
|
262
|
+
cmd: Command,
|
|
263
|
+
) => {
|
|
264
|
+
try {
|
|
265
|
+
const parsed = parseCredentialName(name);
|
|
266
|
+
if (!parsed) {
|
|
267
|
+
writeOutput(cmd, {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
|
|
270
|
+
});
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { service, field } = parsed;
|
|
276
|
+
const storageKey = `credential:${service}:${field}`;
|
|
277
|
+
|
|
278
|
+
assertMetadataWritable();
|
|
279
|
+
|
|
280
|
+
const stored = setSecureKey(storageKey, value);
|
|
281
|
+
if (!stored) {
|
|
282
|
+
writeOutput(cmd, {
|
|
283
|
+
ok: false,
|
|
284
|
+
error: `Failed to store secret for ${name}`,
|
|
285
|
+
});
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const allowedTools = opts.allowedTools
|
|
291
|
+
? opts.allowedTools.split(",").map((t) => t.trim())
|
|
292
|
+
: undefined;
|
|
293
|
+
|
|
294
|
+
const metadata = upsertCredentialMetadata(service, field, {
|
|
295
|
+
alias: opts.label,
|
|
296
|
+
usageDescription: opts.description,
|
|
297
|
+
allowedTools,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
writeOutput(cmd, {
|
|
301
|
+
ok: true,
|
|
302
|
+
credentialId: metadata.credentialId,
|
|
303
|
+
service,
|
|
304
|
+
field,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (!shouldOutputJson(cmd)) {
|
|
308
|
+
log.info(
|
|
309
|
+
`Stored credential ${service}:${field} (${metadata.credentialId})`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
} catch (err) {
|
|
313
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
314
|
+
writeOutput(cmd, { ok: false, error: message });
|
|
315
|
+
process.exitCode = 1;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
// delete
|
|
322
|
+
// -------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
credential
|
|
325
|
+
.command("delete <name>")
|
|
326
|
+
.description("Remove a secret and its metadata from the vault")
|
|
327
|
+
.addHelpText(
|
|
328
|
+
"after",
|
|
329
|
+
`
|
|
330
|
+
Arguments:
|
|
331
|
+
name Credential name in service:field format (e.g. twilio:account_sid)
|
|
332
|
+
|
|
333
|
+
Deletes both the encrypted secret and all associated metadata (policy,
|
|
334
|
+
timestamps, injection templates). This action cannot be undone.
|
|
335
|
+
|
|
336
|
+
Examples:
|
|
337
|
+
$ vellum credentials delete twilio:auth_token
|
|
338
|
+
$ vellum credentials delete github:token`,
|
|
339
|
+
)
|
|
340
|
+
.action((name: string, _opts: unknown, cmd: Command) => {
|
|
341
|
+
try {
|
|
342
|
+
const parsed = parseCredentialName(name);
|
|
343
|
+
if (!parsed) {
|
|
344
|
+
writeOutput(cmd, {
|
|
345
|
+
ok: false,
|
|
346
|
+
error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
|
|
347
|
+
});
|
|
348
|
+
process.exitCode = 1;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const { service, field } = parsed;
|
|
353
|
+
const storageKey = `credential:${service}:${field}`;
|
|
354
|
+
|
|
355
|
+
assertMetadataWritable();
|
|
356
|
+
|
|
357
|
+
const secretResult = deleteSecureKey(storageKey);
|
|
358
|
+
if (secretResult === "error") {
|
|
359
|
+
writeOutput(cmd, {
|
|
360
|
+
ok: false,
|
|
361
|
+
error: "Failed to delete credential from secure storage",
|
|
362
|
+
});
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const metadataDeleted = deleteCredentialMetadata(service, field);
|
|
368
|
+
|
|
369
|
+
if (secretResult !== "deleted" && !metadataDeleted) {
|
|
370
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
371
|
+
process.exitCode = 1;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
writeOutput(cmd, { ok: true, service, field });
|
|
376
|
+
|
|
377
|
+
if (!shouldOutputJson(cmd)) {
|
|
378
|
+
log.info(`Deleted credential ${service}:${field}`);
|
|
379
|
+
}
|
|
380
|
+
} catch (err) {
|
|
381
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
382
|
+
writeOutput(cmd, { ok: false, error: message });
|
|
383
|
+
process.exitCode = 1;
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// -------------------------------------------------------------------------
|
|
388
|
+
// inspect
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
|
|
391
|
+
credential
|
|
392
|
+
.command("inspect <name>")
|
|
393
|
+
.description("Show metadata and a masked preview of a stored credential")
|
|
394
|
+
.addHelpText(
|
|
395
|
+
"after",
|
|
396
|
+
`
|
|
397
|
+
Arguments:
|
|
398
|
+
name Credential name in service:field format, or a credential UUID
|
|
399
|
+
|
|
400
|
+
Shows everything known about a credential without revealing the secret value.
|
|
401
|
+
The secret is masked to show only the last 4 characters (e.g. ****c123).
|
|
402
|
+
|
|
403
|
+
Displayed fields include: label, creation/update timestamps, allowed tools,
|
|
404
|
+
allowed domains, OAuth2 scopes, account info, and injection template count.
|
|
405
|
+
|
|
406
|
+
Examples:
|
|
407
|
+
$ vellum credentials inspect twilio:account_sid
|
|
408
|
+
$ vellum credentials inspect 7a3b1c2d-4e5f-6789-abcd-ef0123456789
|
|
409
|
+
$ vellum credentials inspect --json slack_channel:bot_token`,
|
|
410
|
+
)
|
|
411
|
+
.action((name: string, _opts: unknown, cmd: Command) => {
|
|
412
|
+
try {
|
|
413
|
+
let metadata: CredentialMetadata | undefined;
|
|
414
|
+
let storageKey: string;
|
|
415
|
+
|
|
416
|
+
if (name.includes(":")) {
|
|
417
|
+
const parsed = parseCredentialName(name);
|
|
418
|
+
if (!parsed) {
|
|
419
|
+
writeOutput(cmd, {
|
|
420
|
+
ok: false,
|
|
421
|
+
error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
|
|
422
|
+
});
|
|
423
|
+
process.exitCode = 1;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
metadata = getCredentialMetadata(parsed.service, parsed.field);
|
|
427
|
+
storageKey = `credential:${parsed.service}:${parsed.field}`;
|
|
428
|
+
} else {
|
|
429
|
+
metadata = getCredentialMetadataById(name);
|
|
430
|
+
if (metadata) {
|
|
431
|
+
storageKey = `credential:${metadata.service}:${metadata.field}`;
|
|
432
|
+
} else {
|
|
433
|
+
// No metadata found by UUID, and we can't determine the storage key
|
|
434
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const secret = getSecureKey(storageKey);
|
|
441
|
+
|
|
442
|
+
if (!metadata && (secret == null || secret.length === 0)) {
|
|
443
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
444
|
+
process.exitCode = 1;
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// If we have a secret but no metadata, we still need metadata for the output.
|
|
449
|
+
// This can happen if someone stored a key directly without going through the
|
|
450
|
+
// credential set command. Build a minimal output in that case.
|
|
451
|
+
if (!metadata) {
|
|
452
|
+
// We only get here for the service:field path where we have storageKey
|
|
453
|
+
// but no metadata record. Output what we can.
|
|
454
|
+
const parsed = parseCredentialName(name)!;
|
|
455
|
+
writeOutput(cmd, {
|
|
456
|
+
ok: true,
|
|
457
|
+
service: parsed.service,
|
|
458
|
+
field: parsed.field,
|
|
459
|
+
credentialId: null,
|
|
460
|
+
scrubbedValue: scrubSecret(secret),
|
|
461
|
+
hasSecret: secret != null && secret.length > 0,
|
|
462
|
+
alias: null,
|
|
463
|
+
usageDescription: null,
|
|
464
|
+
allowedTools: [],
|
|
465
|
+
allowedDomains: [],
|
|
466
|
+
accountInfo: null,
|
|
467
|
+
grantedScopes: null,
|
|
468
|
+
expiresAt: null,
|
|
469
|
+
createdAt: null,
|
|
470
|
+
updatedAt: null,
|
|
471
|
+
injectionTemplateCount: 0,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
if (!shouldOutputJson(cmd)) {
|
|
475
|
+
log.info(` ${parsed.service}:${parsed.field}`);
|
|
476
|
+
log.info(` Value: ${scrubSecret(secret)}`);
|
|
477
|
+
log.info(" (no metadata record)");
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const output = buildCredentialOutput(metadata, secret);
|
|
483
|
+
writeOutput(cmd, output);
|
|
484
|
+
|
|
485
|
+
if (!shouldOutputJson(cmd)) {
|
|
486
|
+
printCredentialHuman(output);
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
490
|
+
writeOutput(cmd, { ok: false, error: message });
|
|
491
|
+
process.exitCode = 1;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// -------------------------------------------------------------------------
|
|
496
|
+
// reveal
|
|
497
|
+
// -------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
credential
|
|
500
|
+
.command("reveal <name>")
|
|
501
|
+
.description("Print the plaintext value of a credential")
|
|
502
|
+
.addHelpText(
|
|
503
|
+
"after",
|
|
504
|
+
`
|
|
505
|
+
Arguments:
|
|
506
|
+
name Credential name in service:field format, or a credential UUID
|
|
507
|
+
|
|
508
|
+
Prints the raw secret value to stdout for piping into other tools. In JSON
|
|
509
|
+
mode the value is returned as {"ok": true, "value": "..."}. In human mode
|
|
510
|
+
only the bare secret is printed (no labels or decoration) so it can be
|
|
511
|
+
captured with shell substitution, e.g. $(vellum credentials reveal twilio:auth_token).
|
|
512
|
+
|
|
513
|
+
Examples:
|
|
514
|
+
$ vellum credentials reveal twilio:auth_token
|
|
515
|
+
$ vellum credentials reveal 7a3b1c2d-4e5f-6789-abcd-ef0123456789
|
|
516
|
+
$ vellum credentials reveal --json twilio:account_sid
|
|
517
|
+
$ export TWILIO_TOKEN=$(vellum credentials reveal twilio:auth_token)`,
|
|
518
|
+
)
|
|
519
|
+
.action((name: string, _opts: unknown, cmd: Command) => {
|
|
520
|
+
try {
|
|
521
|
+
let storageKey: string;
|
|
522
|
+
|
|
523
|
+
if (name.includes(":")) {
|
|
524
|
+
const parsed = parseCredentialName(name);
|
|
525
|
+
if (!parsed) {
|
|
526
|
+
writeOutput(cmd, {
|
|
527
|
+
ok: false,
|
|
528
|
+
error: `Invalid credential name "${name}". Expected service:field format (e.g. twilio:account_sid)`,
|
|
529
|
+
});
|
|
530
|
+
process.exitCode = 1;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
storageKey = `credential:${parsed.service}:${parsed.field}`;
|
|
534
|
+
} else {
|
|
535
|
+
const metadata = getCredentialMetadataById(name);
|
|
536
|
+
if (metadata) {
|
|
537
|
+
storageKey = `credential:${metadata.service}:${metadata.field}`;
|
|
538
|
+
} else {
|
|
539
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
540
|
+
process.exitCode = 1;
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const secret = getSecureKey(storageKey);
|
|
546
|
+
|
|
547
|
+
if (secret == null || secret.length === 0) {
|
|
548
|
+
writeOutput(cmd, { ok: false, error: "Credential not found" });
|
|
549
|
+
process.exitCode = 1;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (shouldOutputJson(cmd)) {
|
|
554
|
+
writeOutput(cmd, { ok: true, value: secret });
|
|
555
|
+
} else {
|
|
556
|
+
process.stdout.write(secret + "\n");
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
560
|
+
writeOutput(cmd, { ok: false, error: message });
|
|
561
|
+
process.exitCode = 1;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
import { startCli } from "../cli.js";
|
|
4
|
+
import { shouldAutoStartDaemon } from "../daemon/connection-policy.js";
|
|
5
|
+
import { ensureDaemonRunning } from "../daemon/lifecycle.js";
|
|
6
|
+
|
|
7
|
+
export function registerDefaultAction(program: Command): void {
|
|
8
|
+
program.action(async () => {
|
|
9
|
+
if (shouldAutoStartDaemon()) {
|
|
10
|
+
await ensureDaemonRunning();
|
|
11
|
+
}
|
|
12
|
+
await startCli();
|
|
13
|
+
});
|
|
14
|
+
}
|
package/src/cli/dev.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
import { getDaemonStatus, stopDaemon } from "../daemon/lifecycle.js";
|
|
7
|
+
import { getCliLogger } from "../util/logger.js";
|
|
8
|
+
|
|
9
|
+
const log = getCliLogger("cli");
|
|
10
|
+
|
|
11
|
+
export function registerDevCommand(program: Command): void {
|
|
12
|
+
program
|
|
13
|
+
.command("dev")
|
|
14
|
+
.description("Run the assistant in dev mode")
|
|
15
|
+
.option(
|
|
16
|
+
"--watch",
|
|
17
|
+
"Auto-restart on source file changes (disruptive during Claude Code sessions)",
|
|
18
|
+
)
|
|
19
|
+
.addHelpText(
|
|
20
|
+
"after",
|
|
21
|
+
`
|
|
22
|
+
Starts the assistant in foreground dev mode for local development. If an
|
|
23
|
+
existing assistant is running, it is stopped first (waits up to 5 seconds
|
|
24
|
+
for an unresponsive assistant before force-killing it).
|
|
25
|
+
|
|
26
|
+
Behavioral notes:
|
|
27
|
+
- Sets VELLUM_DEBUG=1 for DEBUG-level logging
|
|
28
|
+
- Sets VELLUM_LOG_STDERR=1 so logs stream to stderr (visible in terminal)
|
|
29
|
+
- Sets BASE_DATA_DIR to the repository root
|
|
30
|
+
- The assistant runs in the foreground; press Ctrl+C to stop
|
|
31
|
+
|
|
32
|
+
The --watch flag passes bun --watch to the child process, which
|
|
33
|
+
auto-restarts the assistant whenever source files change. This is useful
|
|
34
|
+
during development but disruptive if a Claude Code session is active,
|
|
35
|
+
since the restart kills the running assistant mid-conversation.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
$ vellum dev
|
|
39
|
+
$ vellum dev --watch`,
|
|
40
|
+
)
|
|
41
|
+
.action(async (opts: { watch?: boolean }) => {
|
|
42
|
+
let status = await getDaemonStatus();
|
|
43
|
+
if (status.running) {
|
|
44
|
+
log.info("Stopping existing assistant...");
|
|
45
|
+
const stopResult = await stopDaemon();
|
|
46
|
+
if (!stopResult.stopped && stopResult.reason === "stop_failed") {
|
|
47
|
+
log.error(
|
|
48
|
+
"Failed to stop existing assistant — process survived SIGKILL",
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
} else if (status.pid) {
|
|
53
|
+
// PID file references a live process but the socket is unresponsive.
|
|
54
|
+
// This can happen during the daemon startup window before the socket
|
|
55
|
+
// is bound. Wait briefly for it to come up before replacing.
|
|
56
|
+
log.info(
|
|
57
|
+
"Assistant process alive but socket unresponsive — waiting for startup...",
|
|
58
|
+
);
|
|
59
|
+
const maxWait = 5000;
|
|
60
|
+
const interval = 500;
|
|
61
|
+
let waited = 0;
|
|
62
|
+
let resolved = false;
|
|
63
|
+
while (waited < maxWait) {
|
|
64
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
65
|
+
waited += interval;
|
|
66
|
+
status = await getDaemonStatus();
|
|
67
|
+
if (status.running) {
|
|
68
|
+
// Socket came up — stop the daemon normally.
|
|
69
|
+
log.info("Assistant became responsive, stopping it...");
|
|
70
|
+
const stopResult = await stopDaemon();
|
|
71
|
+
if (!stopResult.stopped && stopResult.reason === "stop_failed") {
|
|
72
|
+
log.error(
|
|
73
|
+
"Failed to stop existing assistant — process survived SIGKILL",
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
resolved = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (!status.pid) {
|
|
81
|
+
// Process exited on its own — PID file already cleaned up.
|
|
82
|
+
resolved = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!resolved) {
|
|
87
|
+
// Still alive but unresponsive after waiting — stop it via stopDaemon()
|
|
88
|
+
// which handles SIGTERM → SIGKILL escalation and PID file cleanup.
|
|
89
|
+
log.info("Assistant still unresponsive after wait — stopping it...");
|
|
90
|
+
const stopResult = await stopDaemon();
|
|
91
|
+
if (!stopResult.stopped && stopResult.reason === "stop_failed") {
|
|
92
|
+
log.error(
|
|
93
|
+
"Failed to stop existing assistant — process survived SIGKILL",
|
|
94
|
+
);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const mainPath = `${import.meta.dirname}/../daemon/main.ts`;
|
|
101
|
+
|
|
102
|
+
const useWatch = opts.watch === true;
|
|
103
|
+
log.info(
|
|
104
|
+
`Starting assistant in dev mode${
|
|
105
|
+
useWatch ? " with file watching" : ""
|
|
106
|
+
} (Ctrl+C to stop)`,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const repoRoot = join(import.meta.dirname, "..", "..", "..");
|
|
110
|
+
const args = useWatch ? ["--watch", "run", mainPath] : ["run", mainPath];
|
|
111
|
+
const child = spawn("bun", args, {
|
|
112
|
+
stdio: "inherit",
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
BASE_DATA_DIR: repoRoot,
|
|
116
|
+
VELLUM_LOG_STDERR: "1",
|
|
117
|
+
VELLUM_DEBUG: "1",
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const forward = (signal: NodeJS.Signals) => {
|
|
122
|
+
child.kill(signal);
|
|
123
|
+
};
|
|
124
|
+
process.on("SIGINT", () => forward("SIGINT"));
|
|
125
|
+
process.on("SIGTERM", () => forward("SIGTERM"));
|
|
126
|
+
|
|
127
|
+
child.on("exit", (code) => {
|
|
128
|
+
process.exit(code ?? 0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|