@ouro.bot/cli 0.1.0-alpha.888 → 0.1.0-alpha.889

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/changelog.json CHANGED
@@ -1,6 +1,12 @@
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.889",
6
+ "changes": [
7
+ "Agents can join a call on this Mac (FaceTime, Zoom, Meet, a podcast recorder) through BlackHole virtual audio: ouro voice join/leave/status and a family-only voice_join_local_audio tool, with a spoken AI disclosure, room-level trust, a two-tool allowlist, a single-join lock and fail-loud device checks."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.888",
6
12
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.888",
2
+ "runtimeVersion": "0.1.0-alpha.889",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-09-03T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>ouro-butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.888</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.889</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -8220,6 +8220,24 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
8220
8220
  return executeLegacyConfigModel(command, deps);
8221
8221
  }
8222
8222
  /* v8 ignore stop */
8223
+ // ── voice join / leave / status (local audio lane; no daemon socket, control is a local unix socket) ──
8224
+ if (command.kind === "voice.join" || command.kind === "voice.leave" || command.kind === "voice.status") {
8225
+ const voice = await Promise.resolve().then(() => __importStar(require("../../senses/voice/local-audio-join")));
8226
+ const voiceDeps = voice.defaultLocalAudioJoinDeps();
8227
+ let message;
8228
+ if (command.kind === "voice.join") {
8229
+ const summary = await voice.runLocalAudioJoin(command.request, voiceDeps);
8230
+ message = `Local audio session ${summary.callSid} ended (${summary.reason}) after ${Math.round(summary.durationMs / 1000)}s.`;
8231
+ }
8232
+ else if (command.kind === "voice.leave") {
8233
+ message = await voice.runLocalAudioLeave(command.agent, voiceDeps);
8234
+ }
8235
+ else {
8236
+ message = await voice.runLocalAudioStatus(command.agent, voiceDeps);
8237
+ }
8238
+ deps.writeStdout(message);
8239
+ return message;
8240
+ }
8223
8241
  // ── whoami (local, no daemon socket needed) ──
8224
8242
  if (command.kind === "whoami") {
8225
8243
  const formatWhoami = (agentName, homePath, bonesVersion) => {
@@ -99,6 +99,13 @@ exports.COMMAND_REGISTRY = {
99
99
  usage: "ouro whoami [--agent <name>]",
100
100
  example: "ouro whoami",
101
101
  },
102
+ voice: {
103
+ category: "Agents",
104
+ description: "Join a call on this Mac through the BlackHole audio devices, or leave it",
105
+ usage: "ouro voice join --agent <name> [--friend <id>] [--participants <text>] [--occasion <text>] [--mode conversation] | ouro voice leave --agent <name> | ouro voice status --agent <name>",
106
+ example: "ouro voice join --agent slugger --participants \"Ari and Sam\" --occasion \"podcast prep\"",
107
+ subcommands: ["join", "leave", "status"],
108
+ },
102
109
  config: {
103
110
  category: "Agents",
104
111
  description: "Legacy model compatibility helpers; prefer `ouro use` and `ouro check`",
@@ -5,6 +5,39 @@
5
5
  * Pure functions: no side effects, no daemon communication.
6
6
  * Each command group has its own parser; parseOuroCommand dispatches.
7
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  Object.defineProperty(exports, "__esModule", { value: true });
9
42
  exports.extractAgentFlag = extractAgentFlag;
10
43
  exports.extractFacingFlag = extractFacingFlag;
@@ -15,8 +48,11 @@ exports.inferAgentNameFromRemote = inferAgentNameFromRemote;
15
48
  exports.parseMcpServeCommand = parseMcpServeCommand;
16
49
  exports.parseAcpServeCommand = parseAcpServeCommand;
17
50
  exports.parseOuroCommand = parseOuroCommand;
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
18
53
  const friends_1 = require("@ouro.bot/friends");
19
54
  const cli_help_1 = require("./cli-help");
55
+ const local_audio_control_1 = require("../../senses/voice/local-audio-control");
20
56
  const cli_desk_1 = require("./cli-desk");
21
57
  const flight_recorder_1 = require("../../arc/flight-recorder");
22
58
  const core_1 = require("../../nerves/review/core");
@@ -31,6 +67,119 @@ function extractAgentFlag(args) {
31
67
  const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
32
68
  return { agent, rest };
33
69
  }
70
+ const VOICE_USAGE = "Usage: ouro voice join --agent <name> [--friend <id>] [--participants <text>] [--occasion <text>] [--mode conversation] | ouro voice leave --agent <name> | ouro voice status --agent <name>";
71
+ const VOICE_TEXT_FLAGS = {
72
+ "--friend": "friendId",
73
+ "--participants": "participants",
74
+ "--occasion": "occasion",
75
+ "--owner-name": "ownerName",
76
+ "--silent-consent": "silentConsent",
77
+ };
78
+ function parseVoicePositiveInt(value) {
79
+ if (!value || !/^[1-9]\d*$/.test(value))
80
+ throw new Error(VOICE_USAGE);
81
+ return Number(value);
82
+ }
83
+ /**
84
+ * File mode hands these paths to sox, which treats a bare `-` as standard input and `-d` or `-t`
85
+ * style words as devices and options. Only a real file path is allowed: absolute after resolving, a
86
+ * regular file for input, and a regular file (or new file) in an existing directory for output.
87
+ */
88
+ function validateVoiceFile(flag, value, kind) {
89
+ const fail = (why) => { throw new Error(`${flag} ${why}. ${VOICE_USAGE}`); };
90
+ if (value.startsWith("-"))
91
+ return fail("must be a regular file path, not a dash or option");
92
+ const resolved = path.resolve(value);
93
+ const describe = (file) => {
94
+ try {
95
+ return fs.statSync(file);
96
+ }
97
+ catch {
98
+ return undefined;
99
+ }
100
+ };
101
+ const stat = describe(resolved);
102
+ if (kind === "input") {
103
+ if (!stat)
104
+ return fail(`file ${resolved} does not exist`);
105
+ if (!stat.isFile())
106
+ return fail(`must be a regular file, and ${resolved} is not`);
107
+ return resolved;
108
+ }
109
+ if (stat) {
110
+ if (!stat.isFile())
111
+ return fail(`must be a regular file, and ${resolved} is not (no devices or directories)`);
112
+ return resolved;
113
+ }
114
+ const parent = describe(path.dirname(resolved));
115
+ if (!parent?.isDirectory())
116
+ return fail(`needs an existing directory, and ${path.dirname(resolved)} is not one`);
117
+ return resolved;
118
+ }
119
+ /** `ouro voice join|leave|status`: the local audio lane. Device names are never arguments. */
120
+ function parseVoiceCommand(args) {
121
+ const [sub, ...flags] = args;
122
+ if (sub !== "join" && sub !== "leave" && sub !== "status")
123
+ throw new Error(VOICE_USAGE);
124
+ const request = { agentName: "" };
125
+ let inputPath;
126
+ let outputPath;
127
+ for (let i = 0; i < flags.length; i += 1) {
128
+ const flag = flags[i];
129
+ if (flag === "--owner-alone" && sub === "join") {
130
+ request.ownerAlone = true;
131
+ continue;
132
+ }
133
+ const value = flags[i + 1];
134
+ if (value === undefined || value.startsWith("--"))
135
+ throw new Error(VOICE_USAGE);
136
+ i += 1;
137
+ if (flag === "--agent") {
138
+ request.agentName = value;
139
+ }
140
+ else if (sub !== "join") {
141
+ throw new Error(VOICE_USAGE);
142
+ }
143
+ else if (flag in VOICE_TEXT_FLAGS) {
144
+ request[VOICE_TEXT_FLAGS[flag]] = value;
145
+ }
146
+ else if (flag === "--mode") {
147
+ if (value !== "conversation")
148
+ throw new Error(VOICE_USAGE);
149
+ request.mode = "conversation";
150
+ }
151
+ else if (flag === "--notify-session") {
152
+ const notify = (0, local_audio_control_1.parseNotifySession)(value);
153
+ if (!notify)
154
+ throw new Error(VOICE_USAGE);
155
+ request.notify = notify;
156
+ }
157
+ else if (flag === "--input-file") {
158
+ inputPath = validateVoiceFile(flag, value, "input");
159
+ }
160
+ else if (flag === "--output-file") {
161
+ outputPath = validateVoiceFile(flag, value, "output");
162
+ }
163
+ else if (flag === "--idle-silence-ms") {
164
+ request.idleSilenceMs = parseVoicePositiveInt(value);
165
+ }
166
+ else if (flag === "--max-duration-ms") {
167
+ request.maxDurationMs = parseVoicePositiveInt(value);
168
+ }
169
+ else {
170
+ throw new Error(VOICE_USAGE);
171
+ }
172
+ }
173
+ if (!request.agentName)
174
+ throw new Error(VOICE_USAGE);
175
+ if (sub !== "join")
176
+ return { kind: sub === "leave" ? "voice.leave" : "voice.status", agent: request.agentName };
177
+ if ((inputPath === undefined) !== (outputPath === undefined))
178
+ throw new Error(VOICE_USAGE);
179
+ if (inputPath !== undefined && outputPath !== undefined)
180
+ request.files = { inputPath, outputPath };
181
+ return { kind: "voice.join", request };
182
+ }
34
183
  function parseLogsCommand(args) {
35
184
  if (args.length === 0)
36
185
  return { kind: "daemon.logs" };
@@ -164,6 +313,9 @@ function usage() {
164
313
  " ouro a2a delegated-commands revoke --friend <id> [--agent <name>] (run as root)",
165
314
  " ouro a2a delegated-commands list [--json] [--agent <name>]",
166
315
  " ouro whoami [--agent <name>]",
316
+ " ouro voice join --agent <name> [--friend <id>] [--participants <text>] [--occasion <text>] [--mode conversation] [--owner-alone] [--owner-name <name>] [--silent-consent <statement>]",
317
+ " ouro voice leave --agent <name>",
318
+ " ouro voice status --agent <name>",
167
319
  " ouro session list [--agent <name>]",
168
320
  " ouro mcp list",
169
321
  " ouro mcp call <server> <tool> [--args '{...}']",
@@ -2892,6 +3044,8 @@ function parseOuroCommand(args) {
2892
3044
  return parseConfigCommand(args.slice(1));
2893
3045
  if (head === "mcp")
2894
3046
  return parseMcpCommand(args.slice(1));
3047
+ if (head === "voice")
3048
+ return parseVoiceCommand(args.slice(1));
2895
3049
  if (head === "whoami") {
2896
3050
  const { agent } = extractAgentFlag(args.slice(1));
2897
3051
  return { kind: "whoami", ...(agent ? { agent } : {}) };
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.voiceLocalAudioToolDefinition = void 0;
37
+ exports.localAudioToolGate = localAudioToolGate;
38
+ exports.ownerConsentsToSilentJoin = ownerConsentsToSilentJoin;
39
+ exports.createVoiceLocalAudioToolDefinition = createVoiceLocalAudioToolDefinition;
40
+ exports.defaultLocalAudioToolDeps = defaultLocalAudioToolDeps;
41
+ const path = __importStar(require("path"));
42
+ const identity_1 = require("../heart/identity");
43
+ const runtime_1 = require("../nerves/runtime");
44
+ /**
45
+ * The tool that lets a family-trusted owner ask the agent to join a call on this Mac through the
46
+ * BlackHole virtual devices. It has no device arguments: the lane always captures BlackHole 16ch
47
+ * and plays into BlackHole 2ch, so it can never listen to a physical microphone.
48
+ */
49
+ const TOOL_NAME = "voice_join_local_audio";
50
+ /** Direct (one person to one agent) text channels. Voice, inner, mail, a2a and mcp never get this tool. */
51
+ const DIRECT_TEXT_CHANNELS = new Set(["cli", "bluebubbles", "telegram"]);
52
+ const DEVICE_ARGUMENTS = new Set(["device", "input_device", "output_device", "microphone", "input", "output", "capture_device", "playback_device"]);
53
+ const ACTIONS = new Set(["join", "leave", "status"]);
54
+ /** One predicate for both the offer (tool selection) and the execution-time re-check. */
55
+ function localAudioToolGate(input) {
56
+ const context = input.context;
57
+ if (!context?.friend || !context.channel)
58
+ return { ok: false, reason: "no resolved friend for this session" };
59
+ if (input.habitSession || input.autonomousTurnKind || input.currentExternalEvent || input.delegatedCommand || input.relationshipAuthorization || input.voiceCall) {
60
+ return { ok: false, reason: "this turn is automatic, delegated, relationship-scoped or a live call, not a direct conversation" };
61
+ }
62
+ if (!DIRECT_TEXT_CHANNELS.has(context.channel.channel)) {
63
+ return { ok: false, reason: "it works only in a direct text session (cli, bluebubbles, telegram); Teams cannot confirm a one-to-one chat yet" };
64
+ }
65
+ if (context.isGroupChat)
66
+ return { ok: false, reason: "it is not available in a group chat" };
67
+ if (context.friend.trustLevel !== "family")
68
+ return { ok: false, reason: "only a family-trust friend can start it" };
69
+ return { ok: true };
70
+ }
71
+ const normalizeSentence = (text) => text.replace(/\s+/g, " ").trim().replace(/[.!?;:,\s]+$/u, "").toLowerCase();
72
+ const CONSENT_WORDING = /\b(consent(s|ed)?|agree[sd]?|okay with|ok with|fine with|permission)\b/iu;
73
+ const SILENT_WORDING = /\b(silent(ly)?|quiet(ly)?|no announcement|without (an |the )?announc\w*|don'?t announce|do not announce|skip the announcement)\b/iu;
74
+ const MIN_CONSENT_WORDS = 5;
75
+ /**
76
+ * True only when the statement is one whole sentence of the owner's current message, word for word,
77
+ * and that sentence both states consent and asks for a silent join. A fragment ("the call") or a
78
+ * sentence about something else never counts, so text the model was steered into quoting cannot
79
+ * turn an ordinary request into a covert recording.
80
+ */
81
+ function ownerConsentsToSilentJoin(message, statement) {
82
+ const wanted = normalizeSentence(statement);
83
+ if (wanted.split(" ").length < MIN_CONSENT_WORDS || !CONSENT_WORDING.test(wanted) || !SILENT_WORDING.test(wanted))
84
+ return false;
85
+ const sentences = (message ?? "").split(/(?<=[.!?])\s+|\n+/u).map(normalizeSentence);
86
+ return sentences.includes(wanted);
87
+ }
88
+ function readString(args, key) {
89
+ const value = args[key];
90
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
91
+ }
92
+ function createVoiceLocalAudioToolDefinition(deps) {
93
+ return {
94
+ tool: {
95
+ type: "function",
96
+ function: {
97
+ name: TOOL_NAME,
98
+ description: [
99
+ "join, leave, or check a call on this Mac through the BlackHole virtual audio devices (any call app: FaceTime, Zoom, Meet, a recorder).",
100
+ "The call app must use BlackHole 2ch as its microphone and BlackHole 16ch as its speaker. This tool never listens to a physical microphone and takes no device names.",
101
+ "Joining announces you aloud as an AI assistant that is transcribing. Skip the announcement only when the owner's current message has a sentence saying everyone consented to a silent join; pass that whole sentence in silent_consent exactly as they wrote it. A paraphrase or a fragment is refused.",
102
+ "Several people may share this audio, and you cannot tell voices apart. Use action=leave to hang up.",
103
+ ].join(" "),
104
+ parameters: {
105
+ type: "object",
106
+ properties: {
107
+ action: { type: "string", enum: ["join", "leave", "status"], description: "join (default), leave, or status" },
108
+ participants: { type: "string", description: "who is on the call, as the owner described them" },
109
+ occasion: { type: "string", description: "what the call is for" },
110
+ owner_alone: { type: "boolean", description: "true only when the owner said they are alone on the call" },
111
+ silent_consent: { type: "string", description: "the owner's own words consenting to a silent join (no spoken announcement), copied verbatim from their current message; omit otherwise" },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ handler: async (args, ctx) => {
117
+ const gate = ctx ? localAudioToolGate(ctx) : { ok: false, reason: "no session context" };
118
+ if (!gate.ok)
119
+ return `${TOOL_NAME} is not available: ${gate.reason}.`;
120
+ const provided = Object.keys(args).find((key) => DEVICE_ARGUMENTS.has(key));
121
+ if (provided) {
122
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "tools", event: "tool.voice_local_audio_device_refused", message: "local audio tool refused a device argument", meta: { argument: provided } });
123
+ return "This tool only uses the BlackHole virtual devices and never listens to a physical microphone or any other device. Remove the device argument.";
124
+ }
125
+ const action = readString(args, "action") ?? "join";
126
+ if (!ACTIONS.has(action))
127
+ return "action must be join, leave, or status.";
128
+ const agentName = ctx?.agentName;
129
+ if (!agentName)
130
+ return `${TOOL_NAME} is not available: no agent name for this session.`;
131
+ if (action === "leave")
132
+ return deps.leave(agentName);
133
+ if (action === "status")
134
+ return deps.status(agentName);
135
+ const silentConsent = readString(args, "silent_consent");
136
+ if (silentConsent && !ownerConsentsToSilentJoin(ctx.currentUserMessage, silentConsent)) {
137
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "tools", event: "tool.voice_local_audio_silent_refused", message: "silent join refused: consent not in the owner's current message", meta: { agentName } });
138
+ return "I can't do a silent join from here: the consent has to be one whole sentence of the owner's current message, copied exactly, that says everyone consented to a silent join, and I can't confirm that. Join with the spoken announcement instead, or the owner can run `ouro voice join --silent-consent \"...\"` themselves.";
139
+ }
140
+ const friend = ctx.context.friend;
141
+ const channel = ctx.context.channel.channel;
142
+ const session = ctx.currentSession;
143
+ const notify = session
144
+ ? { friendId: session.friendId, channel: session.channel, key: session.key }
145
+ : channel === "cli" ? { friendId: friend.id, channel: "cli", key: "session" } : undefined;
146
+ const request = {
147
+ agentName,
148
+ friendId: friend.id,
149
+ mode: "conversation",
150
+ ownerName: friend.name,
151
+ ...(readString(args, "participants") ? { participants: readString(args, "participants") } : {}),
152
+ ...(readString(args, "occasion") ? { occasion: readString(args, "occasion") } : {}),
153
+ ...(String(args.owner_alone) === "true" ? { ownerAlone: true } : {}),
154
+ ...(silentConsent ? { silentConsent } : {}),
155
+ ...(notify ? { notify } : {}),
156
+ };
157
+ (0, runtime_1.emitNervesEvent)({
158
+ component: "tools",
159
+ event: "tool.voice_local_audio_join_requested",
160
+ message: "local audio join requested by the owner",
161
+ meta: { agentName, silent: String(Boolean(request.silentConsent)), ownerAlone: String(Boolean(request.ownerAlone)), notify: String(Boolean(notify)) },
162
+ });
163
+ const result = await deps.launch(request);
164
+ if (!result.ok)
165
+ return `I did not join the call: ${result.message}`;
166
+ return [
167
+ result.message,
168
+ request.silentConsent
169
+ ? "I joined silently on the owner's stated consent."
170
+ : "I am announcing myself aloud as an AI assistant that is transcribing; if I cannot confirm that was spoken I leave and tell the owner.",
171
+ "The call app must use BlackHole 2ch as its microphone and BlackHole 16ch as its speaker for me to hear and be heard.",
172
+ notify ? "The owner is notified when I join and when I leave." : "I could not notify the owner's session (no session to notify), so tell them yourself.",
173
+ ].join(" ");
174
+ },
175
+ };
176
+ }
177
+ /** Loaded lazily: the join runner pulls in the whole Realtime session, which itself imports the tool registry. */
178
+ function defaultLocalAudioToolDeps(options = {}) {
179
+ const agentRoot = options.agentRoot ?? ((agentName) => (0, identity_1.getAgentRoot)(agentName));
180
+ return {
181
+ launch: async (request) => {
182
+ const join = await Promise.resolve().then(() => __importStar(require("../senses/voice/local-audio-join")));
183
+ const control = await Promise.resolve().then(() => __importStar(require("../senses/voice/local-audio-control")));
184
+ const launch = options.launch ?? {
185
+ execPath: process.execPath,
186
+ cliEntry: path.resolve(__dirname, "../heart/daemon/ouro-entry.js"),
187
+ spawn: join.defaultLaunchSpawn(control.localAudioPaths(agentRoot(request.agentName))),
188
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
189
+ now: Date.now,
190
+ timeoutMs: 300_000,
191
+ };
192
+ return join.launchLocalAudioJoin(request, { agentRoot, ...launch });
193
+ },
194
+ leave: async (agentName) => (await Promise.resolve().then(() => __importStar(require("../senses/voice/local-audio-join")))).runLocalAudioLeave(agentName, { agentRoot }),
195
+ status: async (agentName) => (await Promise.resolve().then(() => __importStar(require("../senses/voice/local-audio-join")))).runLocalAudioStatus(agentName, { agentRoot }),
196
+ };
197
+ }
198
+ exports.voiceLocalAudioToolDefinition = createVoiceLocalAudioToolDefinition(defaultLocalAudioToolDeps());
@@ -40,6 +40,7 @@ const tools_house_care_1 = require("./tools-house-care");
40
40
  const steward_policy_1 = require("../heart/steward-policy");
41
41
  const relationship_authorization_1 = require("./relationship-authorization");
42
42
  const tool_approval_1 = require("../heart/tool-approval");
43
+ const tools_voice_local_1 = require("./tools-voice-local");
43
44
  const tools_sanctuary_host_1 = require("./tools-sanctuary-host");
44
45
  function safeGetAgentRoot() {
45
46
  try {
@@ -172,6 +173,7 @@ function selectToolsForChannel(capabilities, toolPreferences, _context, provider
172
173
  ...tools_base_1.baseToolDefinitions,
173
174
  ...(capabilities?.channel === "bluebubbles" ? tools_bluebubbles_1.bluebubblesToolDefinitions : []),
174
175
  ...(capabilities?.channel === "voice" ? tools_voice_1.voiceToolDefinitions : []),
176
+ ...((0, tools_voice_local_1.localAudioToolGate)({ ...context, context: _context }).ok ? [tools_voice_local_1.voiceLocalAudioToolDefinition] : []),
175
177
  ...integrations, ...mcp,
176
178
  ];
177
179
  }
@@ -139,7 +139,9 @@ class PendingVoiceCalls {
139
139
  this.maxEntries = options.maxEntries ?? exports.DEFAULT_PENDING_VOICE_CALL_MAX_ENTRIES;
140
140
  this.now = options.now ?? Date.now;
141
141
  }
142
- record(identity, nonce) {
142
+ record(candidate, nonce) {
143
+ // The network registry never carries local-audio identity; only the local transport sets it.
144
+ const { local: _local, ...identity } = candidate;
143
145
  const existing = this.live(identity.callSid);
144
146
  const nonces = existing?.nonces ?? new Set();
145
147
  nonces.add(nonce);