@vellumai/assistant 0.5.7 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
@@ -26,6 +26,7 @@ import {
26
26
  releaseCallbackClaim,
27
27
  updateCallSession,
28
28
  } from "./call-store.js";
29
+ import { resolveCallHints } from "./stt-hints.js";
29
30
  import type { CallStatus } from "./types.js";
30
31
  import { resolveVoiceQualityProfile } from "./voice-quality.js";
31
32
 
@@ -52,9 +53,11 @@ export function generateTwiML(
52
53
  speechModel?: string;
53
54
  ttsProvider: string;
54
55
  voice: string;
56
+ interruptSensitivity: string;
55
57
  },
56
58
  relayToken?: string,
57
59
  customParameters?: Record<string, string>,
60
+ hints?: string,
58
61
  ): string {
59
62
  const greetingAttr =
60
63
  welcomeGreeting && welcomeGreeting.trim().length > 0
@@ -96,6 +99,7 @@ ${greetingAttr}
96
99
  ttsProvider="${escapeXml(profile.ttsProvider)}"
97
100
  interruptible="true"
98
101
  dtmfDetection="true"
102
+ interruptSensitivity="${escapeXml(profile.interruptSensitivity)}"${hints ? `\n hints="${escapeXml(hints)}"` : ""}
99
103
  ${relayClose}
100
104
  </Connect>
101
105
  </Response>`;
@@ -180,7 +184,14 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
180
184
 
181
185
  return buildVoiceWebhookTwiml(
182
186
  session.id,
183
- session.task,
187
+ {
188
+ task: session.task,
189
+ toNumber: callerTo,
190
+ fromNumber: callerFrom,
191
+ direction: "inbound",
192
+ inviteFriendName: null,
193
+ inviteGuardianName: null,
194
+ },
184
195
  session.verificationSessionId,
185
196
  );
186
197
  }
@@ -208,7 +219,14 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
208
219
 
209
220
  return buildVoiceWebhookTwiml(
210
221
  callSessionId,
211
- session.task,
222
+ {
223
+ task: session.task,
224
+ toNumber: session.toNumber,
225
+ fromNumber: session.fromNumber,
226
+ direction: "outbound",
227
+ inviteFriendName: session.inviteFriendName,
228
+ inviteGuardianName: session.inviteGuardianName,
229
+ },
212
230
  session.verificationSessionId,
213
231
  );
214
232
  }
@@ -225,18 +243,27 @@ export async function handleVoiceWebhook(req: Request): Promise<Response> {
225
243
  */
226
244
  function buildVoiceWebhookTwiml(
227
245
  callSessionId: string,
228
- task: string | null,
246
+ sessionContext: {
247
+ task: string | null;
248
+ toNumber: string;
249
+ fromNumber: string;
250
+ direction: "inbound" | "outbound";
251
+ inviteFriendName: string | null;
252
+ inviteGuardianName: string | null;
253
+ } | null,
229
254
  verificationSessionId?: string | null,
230
255
  ): Response {
231
256
  const profile = resolveVoiceQualityProfile(loadConfig());
232
257
 
258
+ const hints = resolveCallHints(sessionContext, profile.hints);
259
+
233
260
  log.info(
234
261
  { callSessionId, ttsProvider: profile.ttsProvider, voice: profile.voice },
235
262
  "Voice quality profile resolved",
236
263
  );
237
264
 
238
265
  const relayUrl = getTwilioRelayUrl(loadConfig());
239
- const welcomeGreeting = buildWelcomeGreeting(task);
266
+ const welcomeGreeting = buildWelcomeGreeting(sessionContext?.task ?? null);
240
267
 
241
268
  const relayToken = mintEdgeRelayToken();
242
269
 
@@ -253,6 +280,7 @@ function buildVoiceWebhookTwiml(
253
280
  profile,
254
281
  relayToken,
255
282
  customParameters,
283
+ hints || undefined,
256
284
  );
257
285
 
258
286
  log.info({ callSessionId }, "Returning ConversationRelay TwiML");
@@ -6,6 +6,8 @@ export interface VoiceQualityProfile {
6
6
  speechModel?: string;
7
7
  ttsProvider: string;
8
8
  voice: string;
9
+ interruptSensitivity: string;
10
+ hints: string[];
9
11
  }
10
12
 
11
13
  /**
@@ -59,14 +61,24 @@ export function resolveVoiceQualityProfile(
59
61
  const voice = cfg.calls.voice;
60
62
  const configuredTts = voice.ttsProvider ?? "elevenlabs";
61
63
  const fishAudio = configuredTts === "fish-audio";
64
+ const isGoogle = voice.transcriptionProvider === "Google";
65
+ // Treat the legacy Deepgram default ("nova-3") as unset when provider is
66
+ // Google — upgraded workspaces may still have it persisted from prior defaults.
67
+ const effectiveSpeechModel =
68
+ voice.speechModel == null ||
69
+ (voice.speechModel === "nova-3" && isGoogle)
70
+ ? isGoogle
71
+ ? undefined
72
+ : "nova-3"
73
+ : voice.speechModel;
62
74
  return {
63
75
  language: voice.language,
64
76
  transcriptionProvider: voice.transcriptionProvider,
65
- speechModel:
66
- voice.speechModel ??
67
- (voice.transcriptionProvider === "Google" ? undefined : "nova-3"),
77
+ speechModel: effectiveSpeechModel,
68
78
  ttsProvider: fishAudio ? "Google" : "ElevenLabs",
69
79
  voice: fishAudio ? "" : buildElevenLabsVoiceSpec(cfg.elevenlabs),
80
+ interruptSensitivity: voice.interruptSensitivity ?? "low",
81
+ hints: voice.hints ?? [],
70
82
  };
71
83
  }
72
84
 
@@ -212,6 +212,7 @@ function buildVoiceCallControlPrompt(opts: {
212
212
  lines.push(
213
213
  "9. After the opening greeting turn, treat the Task field as background context only — do not re-execute its instructions on subsequent turns.",
214
214
  '10. Do not make up information. If you are unsure, use [ASK_GUARDIAN: your question] to consult your guardian. For tool permission requests, use [ASK_GUARDIAN_APPROVAL: {"question":"...","toolName":"...","input":{...}}].',
215
+ `11. Your text is sent directly to a text-to-speech engine. Never use markdown formatting (asterisks, headers, backticks, links) or emojis in your spoken responses. Write plain conversational text only. Protocol markers like ${opts.isCallerGuardian ? "[END_CALL]" : "[ASK_GUARDIAN: ...] and [END_CALL]"} are not spoken text and should still be used normally.`,
215
216
  "</voice_call_control>",
216
217
  );
217
218
 
@@ -11,9 +11,9 @@ import {
11
11
  } from "../../avatar/traits-png-sync.js";
12
12
  import { setPlatformBaseUrl } from "../../config/env.js";
13
13
  import { credentialKey } from "../../security/credential-key.js";
14
- import { getSecureKeyAsync } from "../../security/secure-keys.js";
15
14
  import { generateAndSaveAvatar } from "../../tools/system/avatar-generator.js";
16
15
  import { getWorkspaceDir } from "../../util/platform.js";
16
+ import { getSecureKeyViaDaemon } from "../lib/daemon-credential-client.js";
17
17
  import { log } from "../logger.js";
18
18
  import { writeOutput } from "../output.js";
19
19
 
@@ -74,7 +74,7 @@ Examples:
74
74
  // without the daemon's in-memory state.
75
75
  try {
76
76
  const key = credentialKey("vellum", "platform_base_url");
77
- const persisted = await getSecureKeyAsync(key);
77
+ const persisted = await getSecureKeyViaDaemon(key);
78
78
  if (persisted) {
79
79
  setPlatformBaseUrl(persisted);
80
80
  }
@@ -12,12 +12,6 @@ import {
12
12
  type OAuthConnectionRow,
13
13
  } from "../../oauth/oauth-store.js";
14
14
  import { credentialKey } from "../../security/credential-key.js";
15
- import {
16
- deleteSecureKeyAsync,
17
- getSecureKeyAsync,
18
- getSecureKeyResultAsync,
19
- setSecureKeyAsync,
20
- } from "../../security/secure-keys.js";
21
15
  import {
22
16
  assertMetadataWritable,
23
17
  type CredentialMetadata,
@@ -27,9 +21,33 @@ import {
27
21
  listCredentialMetadata,
28
22
  upsertCredentialMetadata,
29
23
  } from "../../tools/credentials/metadata-store.js";
24
+ import {
25
+ deleteSecureKeyViaDaemon,
26
+ getSecureKeyResultViaDaemon,
27
+ getSecureKeyViaDaemon,
28
+ setSecureKeyViaDaemon,
29
+ } from "../lib/daemon-credential-client.js";
30
30
  import { log } from "../logger.js";
31
31
  import { shouldOutputJson, writeOutput } from "../output.js";
32
32
 
33
+ // ---------------------------------------------------------------------------
34
+ // Format-aware error output
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Write an error message respecting the output format. In JSON mode, emit a
39
+ * structured `{ ok: false, error }` object to stdout. In human mode, write
40
+ * plain text to stderr so the assistant (LLM) doesn't receive JSON that it
41
+ * might misinterpret as data.
42
+ */
43
+ function writeError(cmd: Command, message: string): void {
44
+ if (shouldOutputJson(cmd)) {
45
+ writeOutput(cmd, { ok: false, error: message });
46
+ } else {
47
+ process.stderr.write(`Error: ${message}\n`);
48
+ }
49
+ }
50
+
33
51
  // ---------------------------------------------------------------------------
34
52
  // CES shell lockdown guard
35
53
  // ---------------------------------------------------------------------------
@@ -310,7 +328,7 @@ Examples:
310
328
 
311
329
  const credentials = await Promise.all(
312
330
  allMetadata.map(async (m) => {
313
- const secret = await getSecureKeyAsync(
331
+ const secret = await getSecureKeyViaDaemon(
314
332
  credentialKey(m.service, m.field),
315
333
  );
316
334
  const connection = connectionsByProvider.get(m.service);
@@ -336,13 +354,13 @@ Examples:
336
354
  managedOutputs = descriptors.map(buildManagedCredentialOutput);
337
355
  }
338
356
 
339
- writeOutput(cmd, {
340
- ok: true,
341
- credentials,
342
- managedCredentials: managedOutputs,
343
- });
344
-
345
- if (!shouldOutputJson(cmd)) {
357
+ if (shouldOutputJson(cmd)) {
358
+ writeOutput(cmd, {
359
+ ok: true,
360
+ credentials,
361
+ managedCredentials: managedOutputs,
362
+ });
363
+ } else {
346
364
  const totalCount = credentials.length + managedOutputs.length;
347
365
  if (totalCount === 0) {
348
366
  log.info("No credentials found");
@@ -367,7 +385,7 @@ Examples:
367
385
  }
368
386
  } catch (err) {
369
387
  const message = err instanceof Error ? err.message : String(err);
370
- writeOutput(cmd, { ok: false, error: message });
388
+ writeError(cmd, message);
371
389
  process.exitCode = 1;
372
390
  }
373
391
  });
@@ -418,16 +436,16 @@ Examples:
418
436
  ) => {
419
437
  try {
420
438
  const { service, field } = opts;
421
- const storageKey = credentialKey(service, field);
422
439
 
423
440
  assertMetadataWritable();
424
441
 
425
- const stored = await setSecureKeyAsync(storageKey, value);
442
+ const stored = await setSecureKeyViaDaemon(
443
+ "credential",
444
+ `${service}:${field}`,
445
+ value,
446
+ );
426
447
  if (!stored) {
427
- writeOutput(cmd, {
428
- ok: false,
429
- error: `Failed to store secret for ${service}:${field}`,
430
- });
448
+ writeError(cmd, `Failed to store secret for ${service}:${field}`);
431
449
  process.exitCode = 1;
432
450
  return;
433
451
  }
@@ -443,21 +461,21 @@ Examples:
443
461
  });
444
462
  await syncManualTokenConnection(service);
445
463
 
446
- writeOutput(cmd, {
447
- ok: true,
448
- credentialId: metadata.credentialId,
449
- service,
450
- field,
451
- });
452
-
453
- if (!shouldOutputJson(cmd)) {
464
+ if (shouldOutputJson(cmd)) {
465
+ writeOutput(cmd, {
466
+ ok: true,
467
+ credentialId: metadata.credentialId,
468
+ service,
469
+ field,
470
+ });
471
+ } else {
454
472
  log.info(
455
473
  `Stored credential ${service}:${field} (${metadata.credentialId})`,
456
474
  );
457
475
  }
458
476
  } catch (err) {
459
477
  const message = err instanceof Error ? err.message : String(err);
460
- writeOutput(cmd, { ok: false, error: message });
478
+ writeError(cmd, message);
461
479
  process.exitCode = 1;
462
480
  }
463
481
  },
@@ -485,16 +503,18 @@ Examples:
485
503
  .action(async (opts: { service: string; field: string }, cmd: Command) => {
486
504
  try {
487
505
  const { service, field } = opts;
488
- const storageKey = credentialKey(service, field);
489
506
 
490
507
  assertMetadataWritable();
491
508
 
492
- const secretResult = await deleteSecureKeyAsync(storageKey);
509
+ const secretResult = await deleteSecureKeyViaDaemon(
510
+ "credential",
511
+ `${service}:${field}`,
512
+ );
493
513
  if (secretResult === "error") {
494
- writeOutput(cmd, {
495
- ok: false,
496
- error: "Failed to delete credential from secure storage",
497
- });
514
+ writeError(
515
+ cmd,
516
+ "Failed to delete credential from secure storage",
517
+ );
498
518
  process.exitCode = 1;
499
519
  return;
500
520
  }
@@ -511,10 +531,10 @@ Examples:
511
531
  }
512
532
 
513
533
  if (oauthResult === "error") {
514
- writeOutput(cmd, {
515
- ok: false,
516
- error: "Failed to disconnect OAuth provider — please try again",
517
- });
534
+ writeError(
535
+ cmd,
536
+ "Failed to disconnect OAuth provider — please try again",
537
+ );
518
538
  process.exitCode = 1;
519
539
  return;
520
540
  }
@@ -524,19 +544,19 @@ Examples:
524
544
  !metadataDeleted &&
525
545
  oauthResult !== "disconnected"
526
546
  ) {
527
- writeOutput(cmd, { ok: false, error: "Credential not found" });
547
+ writeError(cmd, "Credential not found");
528
548
  process.exitCode = 1;
529
549
  return;
530
550
  }
531
551
 
532
- writeOutput(cmd, { ok: true, service, field });
533
-
534
- if (!shouldOutputJson(cmd)) {
552
+ if (shouldOutputJson(cmd)) {
553
+ writeOutput(cmd, { ok: true, service, field });
554
+ } else {
535
555
  log.info(`Deleted credential ${service}:${field}`);
536
556
  }
537
557
  } catch (err) {
538
558
  const message = err instanceof Error ? err.message : String(err);
539
- writeOutput(cmd, { ok: false, error: message });
559
+ writeError(cmd, message);
540
560
  process.exitCode = 1;
541
561
  }
542
562
  });
@@ -595,32 +615,30 @@ Examples:
595
615
  field = metadata.field;
596
616
  } else {
597
617
  // No metadata found by UUID, and we can't determine the storage key
598
- writeOutput(cmd, { ok: false, error: "Credential not found" });
618
+ writeError(cmd, "Credential not found");
599
619
  process.exitCode = 1;
600
620
  return;
601
621
  }
602
622
  } else {
603
- writeOutput(cmd, {
604
- ok: false,
605
- error:
606
- "Either --service and --field flags or a credential UUID is required",
607
- });
623
+ writeError(
624
+ cmd,
625
+ "Either --service and --field flags or a credential UUID is required",
626
+ );
608
627
  process.exitCode = 1;
609
628
  return;
610
629
  }
611
630
 
612
631
  const { value: secret, unreachable } =
613
- await getSecureKeyResultAsync(storageKey);
632
+ await getSecureKeyResultViaDaemon(storageKey);
614
633
 
615
634
  if (!metadata && (secret == null || secret.length === 0)) {
616
635
  if (unreachable) {
617
- writeOutput(cmd, {
618
- ok: false,
619
- error:
620
- "Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
621
- });
636
+ writeError(
637
+ cmd,
638
+ "Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
639
+ );
622
640
  } else {
623
- writeOutput(cmd, { ok: false, error: "Credential not found" });
641
+ writeError(cmd, "Credential not found");
624
642
  }
625
643
  process.exitCode = 1;
626
644
  return;
@@ -630,23 +648,23 @@ Examples:
630
648
  // This can happen if someone stored a key directly without going through the
631
649
  // credential set command. Build a minimal output in that case.
632
650
  if (!metadata) {
633
- writeOutput(cmd, {
634
- ok: true,
635
- service: service,
636
- field: field,
637
- credentialId: null,
638
- scrubbedValue: scrubSecret(secret),
639
- hasSecret: secret != null && secret.length > 0,
640
- alias: null,
641
- usageDescription: null,
642
- allowedTools: [],
643
- allowedDomains: [],
644
- createdAt: null,
645
- updatedAt: null,
646
- injectionTemplateCount: 0,
647
- });
648
-
649
- if (!shouldOutputJson(cmd)) {
651
+ if (shouldOutputJson(cmd)) {
652
+ writeOutput(cmd, {
653
+ ok: true,
654
+ service: service,
655
+ field: field,
656
+ credentialId: null,
657
+ scrubbedValue: scrubSecret(secret),
658
+ hasSecret: secret != null && secret.length > 0,
659
+ alias: null,
660
+ usageDescription: null,
661
+ allowedTools: [],
662
+ allowedDomains: [],
663
+ createdAt: null,
664
+ updatedAt: null,
665
+ injectionTemplateCount: 0,
666
+ });
667
+ } else {
650
668
  log.info(` ${service}:${field}`);
651
669
  log.info(` Value: ${scrubSecret(secret)}`);
652
670
  log.info(" (no metadata record)");
@@ -662,9 +680,9 @@ Examples:
662
680
  output.brokerUnreachable = true;
663
681
  }
664
682
 
665
- writeOutput(cmd, output);
666
-
667
- if (!shouldOutputJson(cmd)) {
683
+ if (shouldOutputJson(cmd)) {
684
+ writeOutput(cmd, output);
685
+ } else {
668
686
  printCredentialHuman(output);
669
687
  if (unreachable && (secret == null || secret.length === 0)) {
670
688
  log.info(
@@ -674,7 +692,7 @@ Examples:
674
692
  }
675
693
  } catch (err) {
676
694
  const message = err instanceof Error ? err.message : String(err);
677
- writeOutput(cmd, { ok: false, error: message });
695
+ writeError(cmd, message);
678
696
  process.exitCode = 1;
679
697
  }
680
698
  },
@@ -718,7 +736,7 @@ Examples:
718
736
  try {
719
737
  // CES shell lockdown: deny raw secret reveal in untrusted shells.
720
738
  if (isUntrustedShell()) {
721
- writeOutput(cmd, { ok: false, error: UNTRUSTED_SHELL_ERROR });
739
+ writeError(cmd, UNTRUSTED_SHELL_ERROR);
722
740
  process.exitCode = 1;
723
741
  return;
724
742
  }
@@ -732,32 +750,30 @@ Examples:
732
750
  if (metadata) {
733
751
  storageKey = credentialKey(metadata.service, metadata.field);
734
752
  } else {
735
- writeOutput(cmd, { ok: false, error: "Credential not found" });
753
+ writeError(cmd, "Credential not found");
736
754
  process.exitCode = 1;
737
755
  return;
738
756
  }
739
757
  } else {
740
- writeOutput(cmd, {
741
- ok: false,
742
- error:
743
- "Either --service and --field flags or a credential UUID is required",
744
- });
758
+ writeError(
759
+ cmd,
760
+ "Either --service and --field flags or a credential UUID is required",
761
+ );
745
762
  process.exitCode = 1;
746
763
  return;
747
764
  }
748
765
 
749
766
  const { value: secret, unreachable } =
750
- await getSecureKeyResultAsync(storageKey);
767
+ await getSecureKeyResultViaDaemon(storageKey);
751
768
 
752
769
  if (secret == null || secret.length === 0) {
753
770
  if (unreachable) {
754
- writeOutput(cmd, {
755
- ok: false,
756
- error:
757
- "Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
758
- });
771
+ writeError(
772
+ cmd,
773
+ "Keychain broker is unreachable — restart the Vellum app and accept the macOS Keychain prompt",
774
+ );
759
775
  } else {
760
- writeOutput(cmd, { ok: false, error: "Credential not found" });
776
+ writeError(cmd, "Credential not found");
761
777
  }
762
778
  process.exitCode = 1;
763
779
  return;
@@ -770,7 +786,7 @@ Examples:
770
786
  }
771
787
  } catch (err) {
772
788
  const message = err instanceof Error ? err.message : String(err);
773
- writeOutput(cmd, { ok: false, error: message });
789
+ writeError(cmd, message);
774
790
  process.exitCode = 1;
775
791
  }
776
792
  },
@@ -7,7 +7,6 @@ import { getRuntimeHttpPort } from "../../config/env.js";
7
7
  import { loadRawConfig } from "../../config/loader.js";
8
8
  import { shouldAutoStartDaemon } from "../../daemon/connection-policy.js";
9
9
  import { isHttpHealthy } from "../../daemon/daemon-control.js";
10
- import { getProviderKeyAsync } from "../../security/secure-keys.js";
11
10
  import {
12
11
  getDbPath,
13
12
  getHooksDir,
@@ -16,6 +15,7 @@ import {
16
15
  getWorkspaceDir,
17
16
  getWorkspaceSkillsDir,
18
17
  } from "../../util/platform.js";
18
+ import { getProviderKeyViaDaemon } from "../lib/daemon-credential-client.js";
19
19
  import { log } from "../logger.js";
20
20
 
21
21
  export function registerDoctorCommand(program: Command): void {
@@ -81,7 +81,7 @@ Examples:
81
81
  typeof rawInferenceProvider === "string"
82
82
  ? rawInferenceProvider
83
83
  : "anthropic";
84
- const configKey = await getProviderKeyAsync(provider);
84
+ const configKey = await getProviderKeyViaDaemon(provider);
85
85
 
86
86
  if (provider === "ollama") {
87
87
  pass("Provider configured (Ollama; API key optional)");
@@ -2,10 +2,10 @@ import type { Command } from "commander";
2
2
 
3
3
  import { API_KEY_PROVIDERS } from "../../config/loader.js";
4
4
  import {
5
- deleteSecureKeyAsync,
6
- getSecureKeyAsync,
7
- setSecureKeyAsync,
8
- } from "../../security/secure-keys.js";
5
+ deleteSecureKeyViaDaemon,
6
+ getSecureKeyViaDaemon,
7
+ setSecureKeyViaDaemon,
8
+ } from "../lib/daemon-credential-client.js";
9
9
  import { log } from "../logger.js";
10
10
 
11
11
  // ---------------------------------------------------------------------------
@@ -61,7 +61,7 @@ Examples:
61
61
  .action(async () => {
62
62
  const stored: string[] = [];
63
63
  for (const provider of API_KEY_PROVIDERS) {
64
- const value = await getSecureKeyAsync(provider);
64
+ const value = await getSecureKeyViaDaemon(provider);
65
65
  if (value) stored.push(provider);
66
66
  }
67
67
  if (stored.length === 0) {
@@ -99,7 +99,7 @@ Examples:
99
99
  process.exit(1);
100
100
  }
101
101
 
102
- if (await setSecureKeyAsync(provider, key)) {
102
+ if (await setSecureKeyViaDaemon("api_key", provider, key)) {
103
103
  log.info(`Stored API key for "${provider}"`);
104
104
  } else {
105
105
  log.error(`Failed to store API key for "${provider}"`);
@@ -130,7 +130,7 @@ Examples:
130
130
  process.exit(1);
131
131
  }
132
132
 
133
- const result = await deleteSecureKeyAsync(provider);
133
+ const result = await deleteSecureKeyViaDaemon("api_key", provider);
134
134
  if (result === "deleted") {
135
135
  log.info(`Deleted API key for "${provider}"`);
136
136
  } else if (result === "error") {
@@ -183,7 +183,7 @@ Examples:
183
183
  log.info(`Memory degraded: ${result.reason ?? "unknown reason"}`);
184
184
  }
185
185
  log.info(`Semantic hits: ${result.semanticHits}`);
186
- log.info(`Recency hits: ${result.recencyHits}`);
186
+ log.info("Recency hits: 0");
187
187
  log.info(`Injected tokens: ${result.injectedTokens}`);
188
188
  log.info(`Latency: ${result.latencyMs}ms`);
189
189
  if (result.injectedText.length > 0) {