@ouro.bot/cli 0.1.0-alpha.559 → 0.1.0-alpha.560

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/README.md CHANGED
@@ -23,6 +23,7 @@ Current first-class senses:
23
23
  - `teams`
24
24
  - `bluebubbles`
25
25
  - `mail`
26
+ - `voice`
26
27
 
27
28
  (MCP is a bridge for developer tools — a separate channel, not a sense. See `src/heart/mcp/` for the implementation.)
28
29
 
@@ -47,7 +48,7 @@ The shared harness lives in `src/`:
47
48
  - `src/repertoire/`
48
49
  Tool registry (split into category modules: files, shell, notes, bridge, session, continuity, flow, surface, config, and sense-specific tools), coding orchestration, task tools, shared API client, and integration clients (Graph, ADO, GitHub).
49
50
  - `src/senses/`
50
- CLI (with TUI in senses/cli/), Teams, BlueBubbles (in senses/bluebubbles/), Mail (in senses/mail.ts), activity transport, inner-dialog orchestration, and contextual heartbeat. The MCP bridge is at `src/heart/mcp/`, not here.
51
+ CLI (with TUI in senses/cli/), Teams, BlueBubbles (in senses/bluebubbles/), Mail (in senses/mail.ts), Voice (in senses/voice/), activity transport, inner-dialog orchestration, and contextual heartbeat. The MCP bridge is at `src/heart/mcp/`, not here.
51
52
  - `src/nerves/`
52
53
  Structured runtime logging and coverage-audit infrastructure.
53
54
  - `src/__tests__/`
@@ -95,7 +96,7 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
95
96
 
96
97
  ## Runtime Truths
97
98
 
98
- - `agent.json` is the source of truth for identity, phrase pools, context settings, enabled senses, vault coordinates, and provider+model selection. It has two provider lanes: `outward` for CLI, Teams, and BlueBubbles turns, and `inner` for inner dialogue.
99
+ - `agent.json` is the source of truth for identity, phrase pools, context settings, enabled senses, vault coordinates, and provider+model selection. It has two provider lanes: `outward` for CLI, Teams, BlueBubbles, Mail, and Voice turns, and `inner` for inner dialogue.
99
100
  - Legacy `humanFacing`/`agentFacing` provider fields are read only as compatibility aliases for `outward`/`inner`; they are not a second config surface.
100
101
  - Each agent has one credential vault for provider, runtime, sense, integration, travel, and tool credentials. There is no machine-wide credential pool.
101
102
  - Vault unlock material is local machine state. Prefer macOS Keychain, Windows DPAPI, or Linux Secret Service; plaintext fallback is allowed only by explicit human choice.
@@ -104,6 +105,7 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
104
105
  - Human TTY commands share one CLI surface family: bare `ouro` opens the home deck, `ouro up` uses the boot checklist, `ouro connect`/`ouro auth verify`/`ouro repair` agree on provider and vault truth, and `ouro help`/`ouro whoami`/`ouro versions`/`ouro hatch` render through the same Ouro-branded wizard/guide language instead of raw transcript walls. Orientation commands such as root `ouro connect` may use shorter live probes, while startup and verification commands own durable readiness updates.
105
106
  - Human-facing CLI commands that can wait on browser auth, vault IO, daemon startup, daemon restart, provider checks, or connector setup use a shared progress checklist. If a cursor may blink for more than a few seconds, the command should print or animate the current step instead of going quiet.
106
107
  - CLI commands that mutate bundle config, such as vault setup or `ouro connect bluebubbles`, run bundle sync after the change when `sync.enabled` is true and report a compact `bundle sync:` line.
108
+ - Voice is transcript-first: voice sessions use the ordinary `state/sessions/<friend>/voice/<key>.json` session path and appear in Ouro Mailbox as text transcripts. ElevenLabs API credentials live in portable `runtime/config` at `integrations.elevenLabsApiKey`; Whisper.cpp CLI/model paths live in the machine runtime item at `voice.whisperCliPath` and `voice.whisperModelPath`.
107
109
  - The daemon discovers bundles dynamically from `~/AgentBundles`.
108
110
  - `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
109
111
  - `bundle-meta.json` tracks the runtime version that last touched a bundle.
@@ -191,6 +193,7 @@ ouro connect perplexity --agent <name>
191
193
  ouro connect embeddings --agent <name>
192
194
  ouro connect teams --agent <name>
193
195
  ouro connect bluebubbles --agent <name>
196
+ ouro connect voice --agent <name>
194
197
  ouro auth --agent <name>
195
198
  ouro auth --agent <name> --provider <provider>
196
199
  ouro auth verify --agent <name> [--provider <provider>]
package/changelog.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.560",
6
+ "changes": [
7
+ "Voice is now a first-class disabled-by-default sense and channel, with daemon inventory, connect-menu, prompt status, session transcript, and Mailbox UI surfaces.",
8
+ "Voice foundation modules now define Whisper.cpp STT, ElevenLabs streaming TTS, canonical transcript metadata, and loopback voice turns through ordinary `voice` sessions.",
9
+ "The daemon can launch a managed voice entrypoint, and docs now spell out voice runtime credential ownership plus the next meeting/Riverside joining milestone."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.559",
6
14
  "changes": [
@@ -182,6 +182,24 @@ const registryData = [
182
182
  topics: ["senses", "bluebubbles", "imessage", "channels", "interface"],
183
183
  validate: validateObject({ enabled: validateBoolean }),
184
184
  },
185
+ {
186
+ path: "senses.mail",
187
+ tier: "self",
188
+ description: "Mail sense configuration. Controls whether the agent-owned Mailroom mailbox is enabled.",
189
+ default: { enabled: false },
190
+ effects: "Enables or disables the agent mail sense. Runtime credentials still live in the agent vault.",
191
+ topics: ["senses", "mail", "mailroom", "channels", "interface"],
192
+ validate: validateObject({ enabled: validateBoolean }),
193
+ },
194
+ {
195
+ path: "senses.voice",
196
+ tier: "self",
197
+ description: "Voice sense configuration. Controls whether conversational audio sessions are enabled.",
198
+ default: { enabled: false },
199
+ effects: "Enables or disables voice sessions. STT/TTS credentials and local audio attachments are configured separately.",
200
+ topics: ["senses", "voice", "audio", "speech", "channels", "interface"],
201
+ validate: validateObject({ enabled: validateBoolean }),
202
+ },
185
203
  {
186
204
  path: "sync.enabled",
187
205
  tier: "self",
@@ -207,7 +207,7 @@ function hasFreshPendingWork(options) {
207
207
  * where mid-turn delivery is meaningful. Inner dialog has `ponder`. MCP returns
208
208
  * synchronously. Mail is batch. Anything else (unknown channel) treats as non-chat. */
209
209
  function isChatStyleChannel(channel) {
210
- return channel === "cli" || channel === "teams" || channel === "bluebubbles";
210
+ return channel === "cli" || channel === "teams" || channel === "bluebubbles" || channel === "voice";
211
211
  }
212
212
  // Sole-call tools must be the only tool call in a turn. When they appear
213
213
  // alongside other tools, the sole-call tool is rejected with this message.
@@ -2484,11 +2484,12 @@ function enableAgentSense(agent, sense, deps) {
2484
2484
  teams: senses.teams ?? { enabled: false },
2485
2485
  bluebubbles: senses.bluebubbles ?? { enabled: false },
2486
2486
  mail: senses.mail ?? { enabled: false },
2487
+ voice: senses.voice ?? { enabled: false },
2487
2488
  [sense]: { ...existing, enabled: true },
2488
2489
  };
2489
2490
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
2490
2491
  }
2491
- const CONNECT_MENU_PROMPT = "Choose [1-6] or type a name: ";
2492
+ const CONNECT_MENU_PROMPT = "Choose [1-7] or type a name: ";
2492
2493
  function connectMenuIsTTY(deps) {
2493
2494
  return deps.isTTY ?? process.stdout.isTTY === true;
2494
2495
  }
@@ -2499,6 +2500,7 @@ function readConnectBaySenseFlags(agent, deps) {
2499
2500
  teamsEnabled: parsed.senses?.teams?.enabled === true,
2500
2501
  blueBubblesEnabled: parsed.senses?.bluebubbles?.enabled === true,
2501
2502
  mailEnabled: parsed.senses?.mail?.enabled === true,
2503
+ voiceEnabled: parsed.senses?.voice?.enabled === true,
2502
2504
  };
2503
2505
  }
2504
2506
  async function buildConnectMenu(agent, deps, onProgress) {
@@ -2528,13 +2530,16 @@ async function buildConnectMenu(agent, deps, onProgress) {
2528
2530
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2529
2531
  onProgress?.("loading this machine's settings");
2530
2532
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2531
- const { teamsEnabled, blueBubblesEnabled, mailEnabled } = readConnectBaySenseFlags(agent, deps);
2533
+ const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled } = readConnectBaySenseFlags(agent, deps);
2532
2534
  const perplexityApiKey = runtimeConfig.ok
2533
2535
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2534
2536
  : null;
2535
2537
  const embeddingsApiKey = runtimeConfig.ok
2536
2538
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.openaiEmbeddingsApiKey")
2537
2539
  : null;
2540
+ const elevenLabsApiKey = runtimeConfig.ok
2541
+ ? readRuntimeConfigString(runtimeConfig.config, "integrations.elevenLabsApiKey")
2542
+ : null;
2538
2543
  const shouldVerifyPerplexity = runtimeConfig.ok && !!perplexityApiKey;
2539
2544
  const shouldVerifyEmbeddings = runtimeConfig.ok && !!embeddingsApiKey;
2540
2545
  let perplexityVerification;
@@ -2605,6 +2610,16 @@ async function buildConnectMenu(agent, deps, onProgress) {
2605
2610
  ? "attached"
2606
2611
  : "not attached"
2607
2612
  : machineRuntimeReadStatus(machineRuntime);
2613
+ const voiceStatus = runtimeConfig.ok
2614
+ ? machineRuntime.ok
2615
+ ? elevenLabsApiKey
2616
+ && hasRuntimeConfigValue(machineRuntime.config, "voice.whisperCliPath")
2617
+ && hasRuntimeConfigValue(machineRuntime.config, "voice.whisperModelPath")
2618
+ && voiceEnabled
2619
+ ? "attached"
2620
+ : "not attached"
2621
+ : machineRuntimeReadStatus(machineRuntime)
2622
+ : runtimeConfigReadStatus(runtimeConfig);
2608
2623
  const mailroomConfig = runtimeConfig.ok && runtimeConfig.config.mailroom && typeof runtimeConfig.config.mailroom === "object" && !Array.isArray(runtimeConfig.config.mailroom)
2609
2624
  ? runtimeConfig.config.mailroom
2610
2625
  : {};
@@ -2695,6 +2710,26 @@ async function buildConnectMenu(agent, deps, onProgress) {
2695
2710
  status: mailStatus,
2696
2711
  }) ? `ouro connect mail --agent ${agent}` : undefined,
2697
2712
  },
2713
+ {
2714
+ option: "7",
2715
+ name: "Voice",
2716
+ section: "This machine",
2717
+ status: voiceStatus,
2718
+ description: "Conversational audio via local Whisper.cpp STT and ElevenLabs TTS.",
2719
+ detailLines: runtimeConfig.ok && machineRuntime.ok
2720
+ ? [
2721
+ elevenLabsApiKey ? "ElevenLabs API key saved in portable runtime config" : "missing integrations.elevenLabsApiKey",
2722
+ hasRuntimeConfigValue(machineRuntime.config, "voice.whisperCliPath") ? "Whisper.cpp CLI path saved for this machine" : "missing voice.whisperCliPath",
2723
+ hasRuntimeConfigValue(machineRuntime.config, "voice.whisperModelPath") ? "Whisper.cpp model path saved for this machine" : "missing voice.whisperModelPath",
2724
+ ]
2725
+ : [],
2726
+ nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
2727
+ option: "7",
2728
+ name: "Voice",
2729
+ section: "This machine",
2730
+ status: voiceStatus,
2731
+ }) ? `ouro connect voice --agent ${agent}` : undefined,
2732
+ },
2698
2733
  ];
2699
2734
  const isTTY = connectMenuIsTTY(deps);
2700
2735
  return (0, connect_bay_1.renderConnectBay)(entries, {
@@ -4228,8 +4263,24 @@ function connectMenuTarget(answer) {
4228
4263
  return "bluebubbles";
4229
4264
  if (normalized === "6" || normalized === "mail" || normalized === "email" || normalized === "mailroom")
4230
4265
  return "mail";
4266
+ if (normalized === "7" || normalized === "voice" || normalized === "audio" || normalized === "speech")
4267
+ return "voice";
4231
4268
  return "cancel";
4232
4269
  }
4270
+ async function executeConnectVoice(agent, deps) {
4271
+ const message = [
4272
+ `Voice foundation for ${agent}`,
4273
+ "Configure the portable ElevenLabs API key with:",
4274
+ ` ouro vault config set --agent ${agent} --key integrations.elevenLabsApiKey`,
4275
+ "Configure this machine's Whisper.cpp attachment with:",
4276
+ ` ouro vault config set --agent ${agent} --scope machine --key voice.whisperCliPath`,
4277
+ ` ouro vault config set --agent ${agent} --scope machine --key voice.whisperModelPath`,
4278
+ "Then enable agent.json: senses.voice.enabled = true and restart with `ouro up`.",
4279
+ "Meeting-link joining and browser/system audio routing are tracked as the next milestone.",
4280
+ ].join("\n");
4281
+ deps.writeStdout(message);
4282
+ return message;
4283
+ }
4233
4284
  async function executeConnect(command, deps) {
4234
4285
  if (command.target === "providers")
4235
4286
  return executeConnectProviders(command.agent, deps);
@@ -4243,6 +4294,8 @@ async function executeConnect(command, deps) {
4243
4294
  return executeConnectBlueBubbles(command.agent, deps);
4244
4295
  if (command.target === "mail")
4245
4296
  return executeConnectMail(command.agent, deps, command);
4297
+ if (command.target === "voice")
4298
+ return executeConnectVoice(command.agent, deps);
4246
4299
  const progress = createHumanCommandProgress(deps, "connect");
4247
4300
  let menu;
4248
4301
  try {
@@ -4254,7 +4307,7 @@ async function executeConnect(command, deps) {
4254
4307
  const promptInput = deps.promptInput;
4255
4308
  if (!promptInput) {
4256
4309
  const message = [
4257
- menu.replace(/\nChoose \[1-6\] or type a name: $/, ""),
4310
+ menu.replace(/\nChoose \[1-7\] or type a name: $/, ""),
4258
4311
  "",
4259
4312
  `Run: ouro connect providers --agent ${command.agent}`,
4260
4313
  `Run: ouro connect perplexity --agent ${command.agent}`,
@@ -4262,6 +4315,7 @@ async function executeConnect(command, deps) {
4262
4315
  `Run: ouro connect teams --agent ${command.agent}`,
4263
4316
  `Run: ouro connect bluebubbles --agent ${command.agent}`,
4264
4317
  `Run: ouro connect mail --agent ${command.agent}`,
4318
+ `Run: ouro connect voice --agent ${command.agent}`,
4265
4319
  ].join("\n");
4266
4320
  deps.writeStdout(message);
4267
4321
  return message;
@@ -4279,6 +4333,8 @@ async function executeConnect(command, deps) {
4279
4333
  return executeConnectBlueBubbles(command.agent, deps);
4280
4334
  if (answer === "mail")
4281
4335
  return executeConnectMail(command.agent, deps);
4336
+ if (answer === "voice")
4337
+ return executeConnectVoice(command.agent, deps);
4282
4338
  const message = "connect cancelled.";
4283
4339
  deps.writeStdout(message);
4284
4340
  return message;
@@ -85,7 +85,7 @@ function usage() {
85
85
  " ouro config models [--agent <name>]",
86
86
  " ouro auth [--agent <name>] [--provider <provider>]",
87
87
  " ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
88
- " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
88
+ " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
89
89
  " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
90
90
  " ouro mail backfill-indexes [--agent <name>] [--foreground]",
91
91
  " ouro auth verify [--agent <name>] [--provider <provider>]",
@@ -811,7 +811,9 @@ function normalizeConnectTarget(value) {
811
811
  return "bluebubbles";
812
812
  if (value === "mail" || value === "email" || value === "mailroom")
813
813
  return "mail";
814
- throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>]");
814
+ if (value === "voice" || value === "audio" || value === "speech")
815
+ return "voice";
816
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice] [--agent <name>]");
815
817
  }
816
818
  function extractMailSourceFlags(args, usageText) {
817
819
  const rest = [];
@@ -864,7 +866,7 @@ function extractMailSourceFlags(args, usageText) {
864
866
  };
865
867
  }
866
868
  function parseConnectCommand(args) {
867
- const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
869
+ const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
868
870
  const { agent, rest: afterAgent } = extractAgentFlag(args);
869
871
  const mailFlags = extractMailSourceFlags(afterAgent, usageText);
870
872
  if (mailFlags.rest.length > 1)
@@ -50,7 +50,7 @@ function defaultLoggingForProcess(processName) {
50
50
  sinks: ["ndjson"],
51
51
  };
52
52
  }
53
- if (processName === "bluebubbles" || processName === "mail") {
53
+ if (processName === "bluebubbles" || processName === "mail" || processName === "voice") {
54
54
  return {
55
55
  level: "warn",
56
56
  sinks: ["terminal", "ndjson"],
@@ -55,6 +55,7 @@ function defaultSenses() {
55
55
  teams: { ...identity_1.DEFAULT_AGENT_SENSES.teams },
56
56
  bluebubbles: { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
57
57
  mail: { ...identity_1.DEFAULT_AGENT_SENSES.mail },
58
+ voice: { ...identity_1.DEFAULT_AGENT_SENSES.voice },
58
59
  };
59
60
  }
60
61
  function readAgentSenses(agentJsonPath) {
@@ -80,7 +81,7 @@ function readAgentSenses(agentJsonPath) {
80
81
  if (!rawSenses || typeof rawSenses !== "object" || Array.isArray(rawSenses)) {
81
82
  return defaults;
82
83
  }
83
- for (const sense of ["cli", "teams", "bluebubbles", "mail"]) {
84
+ for (const sense of ["cli", "teams", "bluebubbles", "mail", "voice"]) {
84
85
  const rawSense = rawSenses[sense];
85
86
  if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
86
87
  continue;
@@ -121,6 +122,7 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
121
122
  teams: { configured: false, detail: "not enabled in agent.json" },
122
123
  bluebubbles: { configured: false, detail: "not enabled in agent.json" },
123
124
  mail: { configured: false, detail: "not enabled in agent.json" },
125
+ voice: { configured: false, detail: "not enabled in agent.json" },
124
126
  };
125
127
  const payload = runtimeConfig.ok ? runtimeConfig.config : {};
126
128
  const unavailableDetail = runtimeConfigUnavailableDetail(agent, runtimeConfig);
@@ -130,6 +132,8 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
130
132
  const bluebubbles = machinePayload.bluebubbles;
131
133
  const bluebubblesChannel = machinePayload.bluebubblesChannel;
132
134
  const mailroom = payload.mailroom;
135
+ const integrations = payload.integrations;
136
+ const voice = machinePayload.voice;
133
137
  if (senses.teams.enabled) {
134
138
  const missing = [];
135
139
  if (!textField(teams, "clientId"))
@@ -189,6 +193,28 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
189
193
  : unavailableDetail,
190
194
  };
191
195
  }
196
+ if (senses.voice.enabled) {
197
+ const missing = [];
198
+ if (!textField(integrations, "elevenLabsApiKey"))
199
+ missing.push("integrations.elevenLabsApiKey");
200
+ if (!textField(voice, "whisperCliPath"))
201
+ missing.push("voice.whisperCliPath");
202
+ if (!textField(voice, "whisperModelPath"))
203
+ missing.push("voice.whisperModelPath");
204
+ base.voice = missing.length === 0
205
+ ? { configured: true, detail: "local Whisper.cpp STT + ElevenLabs TTS" }
206
+ : {
207
+ configured: false,
208
+ optional: !machineRuntimeConfig.ok && machineRuntimeConfig.reason === "missing",
209
+ detail: !machineRuntimeConfig.ok && machineRuntimeConfig.reason === "missing"
210
+ ? "not attached on this machine"
211
+ : runtimeConfig.ok && machineRuntimeConfig.ok
212
+ ? `missing ${missing.join("/")}`
213
+ : !runtimeConfig.ok
214
+ ? unavailableDetail
215
+ : runtimeConfigUnavailableDetail(agent, machineRuntimeConfig),
216
+ };
217
+ }
192
218
  return base;
193
219
  }
194
220
  function senseRepairHint(agent, sense) {
@@ -198,6 +224,9 @@ function senseRepairHint(agent, sense) {
198
224
  if (sense === "mail") {
199
225
  return `Agent-runnable: provision Mailroom access with 'ouro connect mail --agent ${agent}', then restart with 'ouro up'.`;
200
226
  }
227
+ if (sense === "voice") {
228
+ return `Agent-runnable: run 'ouro connect voice --agent ${agent}' for config guidance, save ElevenLabs and local Whisper.cpp settings, then run 'ouro up' again.`;
229
+ }
201
230
  return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
202
231
  }
203
232
  function currentMachineId() {
@@ -208,7 +237,7 @@ function parseSenseSnapshotName(name) {
208
237
  if (parts.length !== 2)
209
238
  return null;
210
239
  const [agent, sense] = parts;
211
- if (sense !== "teams" && sense !== "bluebubbles" && sense !== "mail")
240
+ if (sense !== "teams" && sense !== "bluebubbles" && sense !== "mail" && sense !== "voice")
212
241
  return null;
213
242
  return { agent, sense };
214
243
  }
@@ -222,12 +251,14 @@ function managedSenseEntry(sense) {
222
251
  return "senses/teams-entry.js";
223
252
  if (sense === "bluebubbles")
224
253
  return "senses/bluebubbles/entry.js";
254
+ if (sense === "voice")
255
+ return "senses/voice-entry.js";
225
256
  return "senses/mail-entry.js";
226
257
  }
227
258
  function runtimeCredentialBootstrapFor(agent, sense) {
228
259
  const runtime = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent);
229
- const machineId = sense === "bluebubbles" ? currentMachineId() : undefined;
230
- const machine = sense === "bluebubbles" ? (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent) : null;
260
+ const machineId = sense === "bluebubbles" || sense === "voice" ? currentMachineId() : undefined;
261
+ const machine = sense === "bluebubbles" || sense === "voice" ? (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent) : null;
231
262
  const providerPool = (0, provider_credentials_1.readProviderCredentialPool)(agent);
232
263
  const providerCredentialRecords = providerPool.ok
233
264
  ? Object.values(providerPool.pool.providers).filter((record) => !!record)
@@ -394,7 +425,7 @@ class DaemonSenseManager {
394
425
  return [agent, { senses, facts }];
395
426
  }));
396
427
  const managedSenseAgents = [...this.contexts.entries()].flatMap(([agent, context]) => {
397
- return ["teams", "bluebubbles", "mail"]
428
+ return ["teams", "bluebubbles", "mail", "voice"]
398
429
  .filter((sense) => context.senses[sense].enabled)
399
430
  .map((sense) => ({
400
431
  name: `${agent}:${sense}`,
@@ -453,7 +484,7 @@ class DaemonSenseManager {
453
484
  async refreshSenseConfigAndRetry(name, parsed) {
454
485
  try {
455
486
  const refreshed = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(parsed.agent, { preserveCachedOnFailure: true });
456
- const machineRefreshed = parsed.sense === "bluebubbles"
487
+ const machineRefreshed = parsed.sense === "bluebubbles" || parsed.sense === "voice"
457
488
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
458
489
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
459
490
  const context = this.contexts.get(parsed.agent);
@@ -572,6 +603,11 @@ class DaemonSenseManager {
572
603
  configured: context.facts.mail.configured,
573
604
  ...(runtime.get(agent)?.mail ?? {}),
574
605
  },
606
+ voice: {
607
+ configured: context.facts.voice.configured,
608
+ optional: context.facts.voice.optional,
609
+ ...(runtime.get(agent)?.voice ?? {}),
610
+ },
575
611
  };
576
612
  const inventory = (0, sense_truth_1.getSenseInventory)({ senses: context.senses }, runtimeInfo);
577
613
  return inventory.map((entry) => ({
@@ -136,6 +136,7 @@ exports.DEFAULT_AGENT_SENSES = {
136
136
  teams: { enabled: false },
137
137
  bluebubbles: { enabled: false },
138
138
  mail: { enabled: false },
139
+ voice: { enabled: false },
139
140
  };
140
141
  function normalizeSenses(value, configFile) {
141
142
  const defaults = {
@@ -143,6 +144,7 @@ function normalizeSenses(value, configFile) {
143
144
  teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
144
145
  bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
145
146
  mail: { ...exports.DEFAULT_AGENT_SENSES.mail },
147
+ voice: { ...exports.DEFAULT_AGENT_SENSES.voice },
146
148
  };
147
149
  if (value === undefined) {
148
150
  return defaults;
@@ -158,7 +160,7 @@ function normalizeSenses(value, configFile) {
158
160
  throw new Error(`agent.json at ${configFile} must include senses as an object when present.`);
159
161
  }
160
162
  const raw = value;
161
- const senseNames = ["cli", "teams", "bluebubbles", "mail"];
163
+ const senseNames = ["cli", "teams", "bluebubbles", "mail", "voice"];
162
164
  for (const senseName of senseNames) {
163
165
  const rawSense = raw[senseName];
164
166
  if (rawSense === undefined) {
@@ -201,6 +203,7 @@ function buildDefaultAgentTemplate(_agentName) {
201
203
  teams: { ...exports.DEFAULT_AGENT_SENSES.teams },
202
204
  bluebubbles: { ...exports.DEFAULT_AGENT_SENSES.bluebubbles },
203
205
  mail: { ...exports.DEFAULT_AGENT_SENSES.mail },
206
+ voice: { ...exports.DEFAULT_AGENT_SENSES.voice },
204
207
  },
205
208
  phrases: {
206
209
  thinking: [...exports.DEFAULT_AGENT_PHRASES.thinking],
@@ -8,6 +8,7 @@ const SENSES = [
8
8
  { sense: "teams", label: "Teams", daemonManaged: true },
9
9
  { sense: "bluebubbles", label: "BlueBubbles", daemonManaged: true },
10
10
  { sense: "mail", label: "Mail", daemonManaged: true },
11
+ { sense: "voice", label: "Voice", daemonManaged: true },
11
12
  ];
12
13
  function configuredSenses(senses) {
13
14
  const configured = senses ?? {};
@@ -17,6 +18,7 @@ function configuredSenses(senses) {
17
18
  teams: configured.teams ?? { ...identity_1.DEFAULT_AGENT_SENSES.teams },
18
19
  bluebubbles: configured.bluebubbles ?? { ...identity_1.DEFAULT_AGENT_SENSES.bluebubbles },
19
20
  mail: configured.mail ?? { ...identity_1.DEFAULT_AGENT_SENSES.mail },
21
+ voice: configured.voice ?? { ...identity_1.DEFAULT_AGENT_SENSES.voice },
20
22
  };
21
23
  }
22
24
  function resolveStatus(enabled, daemonManaged, runtimeInfo) {
@@ -153,6 +153,7 @@ function readSenseStatusLines() {
153
153
  teams: configuredSenses.teams ?? { enabled: false },
154
154
  bluebubbles: configuredSenses.bluebubbles ?? { enabled: false },
155
155
  mail: configuredSenses.mail ?? { enabled: false },
156
+ voice: configuredSenses.voice ?? { enabled: false },
156
157
  };
157
158
  const payload = (0, config_1.loadConfig)();
158
159
  const agentName = (0, identity_1.getAgentName)();
@@ -163,12 +164,15 @@ function readSenseStatusLines() {
163
164
  const teams = recordOrUndefined(runtimePayload.teams) ?? recordOrUndefined(payload.teams);
164
165
  const bluebubbles = recordOrUndefined(machinePayload.bluebubbles) ?? recordOrUndefined(payload.bluebubbles);
165
166
  const mailroom = recordOrUndefined(runtimePayload.mailroom) ?? recordOrUndefined(payload.mailroom);
167
+ const voice = recordOrUndefined(machinePayload.voice) ?? recordOrUndefined(payload.voice);
168
+ const integrations = recordOrUndefined(runtimePayload.integrations) ?? recordOrUndefined(payload.integrations);
166
169
  const privateKeys = mailroom?.privateKeys;
167
170
  const configured = {
168
171
  cli: true,
169
172
  teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
170
173
  bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
171
174
  mail: hasTextField(mailroom, "mailboxAddress") && !!privateKeys && typeof privateKeys === "object" && !Array.isArray(privateKeys),
175
+ voice: hasTextField(integrations, "elevenLabsApiKey") && hasTextField(voice, "whisperCliPath") && hasTextField(voice, "whisperModelPath"),
172
176
  };
173
177
  const rows = [
174
178
  { label: "CLI", status: "interactive" },
@@ -184,6 +188,10 @@ function readSenseStatusLines() {
184
188
  label: "Mail",
185
189
  status: !senses.mail.enabled ? "disabled" : configured.mail ? "ready" : "needs_config",
186
190
  },
191
+ {
192
+ label: "Voice",
193
+ status: !senses.voice.enabled ? "disabled" : configured.voice ? "ready" : "needs_config",
194
+ },
187
195
  ];
188
196
  return rows.map((row) => `- ${row.label}: ${row.status}`);
189
197
  }