@ouro.bot/cli 0.1.0-alpha.560 → 0.1.0-alpha.562
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 +1 -1
- package/changelog.json +16 -0
- package/dist/heart/daemon/cli-exec.js +17 -2
- package/dist/heart/daemon/sense-manager.js +3 -0
- package/dist/heart/turn-context.js +5 -1
- package/dist/mind/prompt.js +6 -2
- package/dist/repertoire/tools-session.js +6 -0
- package/dist/repertoire/tools-surface.js +17 -0
- package/dist/senses/bluebubbles/index.js +50 -0
- package/dist/senses/bluebubbles-meta-guard.js +40 -0
- package/dist/senses/shared-turn.js +4 -1
- package/dist/senses/voice/audio-routing.js +119 -0
- package/dist/senses/voice/elevenlabs.js +54 -1
- package/dist/senses/voice/golden-path.js +116 -0
- package/dist/senses/voice/index.js +5 -0
- package/dist/senses/voice/meeting.js +113 -0
- package/dist/senses/voice/playback.js +139 -0
- package/dist/senses/voice/twilio-phone.js +462 -0
- package/dist/senses/voice/whisper.js +29 -1
- package/dist/senses/voice-twilio-entry.js +216 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,7 +105,7 @@ Task docs do not live in this repo anymore. Planning and doing docs live in the
|
|
|
105
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.
|
|
106
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.
|
|
107
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`.
|
|
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` and `integrations.elevenLabsVoiceId`; Whisper.cpp CLI/model paths live in the machine runtime item at `voice.whisperCliPath` and `voice.whisperModelPath`. Phone calls, browser meetings, and local microphone capture are transports under the single `voice` sense, not separate senses; the Twilio phone transport uses Twilio Record -> Whisper.cpp -> voice session -> ElevenLabs -> Twilio Play.
|
|
109
109
|
- The daemon discovers bundles dynamically from `~/AgentBundles`.
|
|
110
110
|
- `ouro status` reports version, last-updated time, discovered agents, senses, and workers.
|
|
111
111
|
- `bundle-meta.json` tracks the runtime version that last touched a bundle.
|
package/changelog.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
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.562",
|
|
6
|
+
"changes": [
|
|
7
|
+
"BlueBubbles outbound delivery now blocks narrow internal/meta markers such as `[surfaced from inner dialog]`, `[pending from ...]:`, routing-control prompt sections, and `<think>` tags before they can reach iMessage.",
|
|
8
|
+
"The guard fails closed across surface, proactive BlueBubbles sends, pending-drain retries, normal flush, and speak/flushNow paths so blocked internal text is logged instead of queued for later delivery.",
|
|
9
|
+
"Regression coverage now keeps ordinary user-facing prose about inner-dialog concepts deliverable while preventing the reported internal surfaced-thought leakage path."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.561",
|
|
14
|
+
"changes": [
|
|
15
|
+
"Voice golden-path orchestration now accepts meeting URLs, checks local BlackHole/Multi-Output readiness, runs Whisper.cpp STT, routes text through ordinary `voice` sessions, streams ElevenLabs TTS, and writes playback-ready audio artifacts.",
|
|
16
|
+
"Voice runtime edges now include default Node Whisper.cpp process execution, default Node WebSocket adaptation for ElevenLabs, and tested playback artifact handling while keeping credentials injected at runtime.",
|
|
17
|
+
"Shared sense turns now store sessions under the explicit agent's bundle instead of depending on process argv for the session path, and voice setup guidance now reflects meeting URL intake plus live-browser handoff limits truthfully."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
4
20
|
{
|
|
5
21
|
"version": "0.1.0-alpha.560",
|
|
6
22
|
"changes": [
|
|
@@ -2540,6 +2540,10 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
2540
2540
|
const elevenLabsApiKey = runtimeConfig.ok
|
|
2541
2541
|
? readRuntimeConfigString(runtimeConfig.config, "integrations.elevenLabsApiKey")
|
|
2542
2542
|
: null;
|
|
2543
|
+
const elevenLabsVoiceId = runtimeConfig.ok
|
|
2544
|
+
? readRuntimeConfigString(runtimeConfig.config, "integrations.elevenLabsVoiceId")
|
|
2545
|
+
?? readRuntimeConfigString(runtimeConfig.config, "voice.elevenLabsVoiceId")
|
|
2546
|
+
: null;
|
|
2543
2547
|
const shouldVerifyPerplexity = runtimeConfig.ok && !!perplexityApiKey;
|
|
2544
2548
|
const shouldVerifyEmbeddings = runtimeConfig.ok && !!embeddingsApiKey;
|
|
2545
2549
|
let perplexityVerification;
|
|
@@ -2613,6 +2617,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
2613
2617
|
const voiceStatus = runtimeConfig.ok
|
|
2614
2618
|
? machineRuntime.ok
|
|
2615
2619
|
? elevenLabsApiKey
|
|
2620
|
+
&& elevenLabsVoiceId
|
|
2616
2621
|
&& hasRuntimeConfigValue(machineRuntime.config, "voice.whisperCliPath")
|
|
2617
2622
|
&& hasRuntimeConfigValue(machineRuntime.config, "voice.whisperModelPath")
|
|
2618
2623
|
&& voiceEnabled
|
|
@@ -2719,6 +2724,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
|
|
|
2719
2724
|
detailLines: runtimeConfig.ok && machineRuntime.ok
|
|
2720
2725
|
? [
|
|
2721
2726
|
elevenLabsApiKey ? "ElevenLabs API key saved in portable runtime config" : "missing integrations.elevenLabsApiKey",
|
|
2727
|
+
elevenLabsVoiceId ? "ElevenLabs voice ID saved in portable runtime config" : "missing integrations.elevenLabsVoiceId",
|
|
2722
2728
|
hasRuntimeConfigValue(machineRuntime.config, "voice.whisperCliPath") ? "Whisper.cpp CLI path saved for this machine" : "missing voice.whisperCliPath",
|
|
2723
2729
|
hasRuntimeConfigValue(machineRuntime.config, "voice.whisperModelPath") ? "Whisper.cpp model path saved for this machine" : "missing voice.whisperModelPath",
|
|
2724
2730
|
]
|
|
@@ -4270,13 +4276,22 @@ function connectMenuTarget(answer) {
|
|
|
4270
4276
|
async function executeConnectVoice(agent, deps) {
|
|
4271
4277
|
const message = [
|
|
4272
4278
|
`Voice foundation for ${agent}`,
|
|
4273
|
-
"Configure
|
|
4279
|
+
"Configure portable ElevenLabs settings with:",
|
|
4274
4280
|
` ouro vault config set --agent ${agent} --key integrations.elevenLabsApiKey`,
|
|
4281
|
+
` ouro vault config set --agent ${agent} --key integrations.elevenLabsVoiceId`,
|
|
4275
4282
|
"Configure this machine's Whisper.cpp attachment with:",
|
|
4276
4283
|
` ouro vault config set --agent ${agent} --scope machine --key voice.whisperCliPath`,
|
|
4277
4284
|
` ouro vault config set --agent ${agent} --scope machine --key voice.whisperModelPath`,
|
|
4285
|
+
"Optional Twilio phone testing setup:",
|
|
4286
|
+
` ouro vault config set --agent ${agent} --key voice.twilioAccountSid`,
|
|
4287
|
+
` ouro vault config set --agent ${agent} --key voice.twilioAuthToken`,
|
|
4288
|
+
` ouro vault config set --agent ${agent} --scope machine --key voice.twilioPublicUrl`,
|
|
4289
|
+
` ouro vault config set --agent ${agent} --scope machine --key voice.twilioPort --value 18910`,
|
|
4290
|
+
` ouro vault config set --agent ${agent} --scope machine --key voice.twilioDefaultFriendId --value ari`,
|
|
4291
|
+
` node dist/senses/voice-twilio-entry.js --agent ${agent} --port 18910 --public-url https://<cloudflare-tunnel>`,
|
|
4292
|
+
`Set the Twilio number's Voice webhook to POST https://<cloudflare-tunnel>/voice/twilio/incoming.`,
|
|
4278
4293
|
"Then enable agent.json: senses.voice.enabled = true and restart with `ouro up`.",
|
|
4279
|
-
"Meeting
|
|
4294
|
+
"Meeting links use URL intake plus BlackHole/Multi-Output readiness checks. Phone testing uses Twilio Record -> Whisper.cpp -> voice session -> ElevenLabs -> Twilio Play.",
|
|
4280
4295
|
].join("\n");
|
|
4281
4296
|
deps.writeStdout(message);
|
|
4282
4297
|
return message;
|
|
@@ -197,6 +197,9 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
|
|
|
197
197
|
const missing = [];
|
|
198
198
|
if (!textField(integrations, "elevenLabsApiKey"))
|
|
199
199
|
missing.push("integrations.elevenLabsApiKey");
|
|
200
|
+
if (!textField(integrations, "elevenLabsVoiceId") && !textField(payload.voice, "elevenLabsVoiceId")) {
|
|
201
|
+
missing.push("integrations.elevenLabsVoiceId");
|
|
202
|
+
}
|
|
200
203
|
if (!textField(voice, "whisperCliPath"))
|
|
201
204
|
missing.push("voice.whisperCliPath");
|
|
202
205
|
if (!textField(voice, "whisperModelPath"))
|
|
@@ -165,6 +165,7 @@ function readSenseStatusLines() {
|
|
|
165
165
|
const bluebubbles = recordOrUndefined(machinePayload.bluebubbles) ?? recordOrUndefined(payload.bluebubbles);
|
|
166
166
|
const mailroom = recordOrUndefined(runtimePayload.mailroom) ?? recordOrUndefined(payload.mailroom);
|
|
167
167
|
const voice = recordOrUndefined(machinePayload.voice) ?? recordOrUndefined(payload.voice);
|
|
168
|
+
const portableVoice = recordOrUndefined(runtimePayload.voice) ?? recordOrUndefined(payload.voice);
|
|
168
169
|
const integrations = recordOrUndefined(runtimePayload.integrations) ?? recordOrUndefined(payload.integrations);
|
|
169
170
|
const privateKeys = mailroom?.privateKeys;
|
|
170
171
|
const configured = {
|
|
@@ -172,7 +173,10 @@ function readSenseStatusLines() {
|
|
|
172
173
|
teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
|
|
173
174
|
bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
|
|
174
175
|
mail: hasTextField(mailroom, "mailboxAddress") && !!privateKeys && typeof privateKeys === "object" && !Array.isArray(privateKeys),
|
|
175
|
-
voice: hasTextField(integrations, "elevenLabsApiKey")
|
|
176
|
+
voice: hasTextField(integrations, "elevenLabsApiKey")
|
|
177
|
+
&& (hasTextField(integrations, "elevenLabsVoiceId") || hasTextField(portableVoice, "elevenLabsVoiceId"))
|
|
178
|
+
&& hasTextField(voice, "whisperCliPath")
|
|
179
|
+
&& hasTextField(voice, "whisperModelPath"),
|
|
176
180
|
};
|
|
177
181
|
const rows = [
|
|
178
182
|
{ label: "CLI", status: "interactive" },
|
package/dist/mind/prompt.js
CHANGED
|
@@ -441,6 +441,7 @@ function localSenseStatusLines() {
|
|
|
441
441
|
const bluebubbles = recordOrUndefined(machinePayload.bluebubbles) ?? recordOrUndefined(payload.bluebubbles);
|
|
442
442
|
const mailroom = recordOrUndefined(runtimePayload.mailroom) ?? recordOrUndefined(payload.mailroom);
|
|
443
443
|
const voice = recordOrUndefined(machinePayload.voice) ?? recordOrUndefined(payload.voice);
|
|
444
|
+
const portableVoice = recordOrUndefined(runtimePayload.voice) ?? recordOrUndefined(payload.voice);
|
|
444
445
|
const integrations = recordOrUndefined(runtimePayload.integrations) ?? recordOrUndefined(payload.integrations);
|
|
445
446
|
const privateKeys = mailroom?.privateKeys;
|
|
446
447
|
const configured = {
|
|
@@ -448,7 +449,10 @@ function localSenseStatusLines() {
|
|
|
448
449
|
teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
|
|
449
450
|
bluebubbles: hasTextField(bluebubbles, "serverUrl") && hasTextField(bluebubbles, "password"),
|
|
450
451
|
mail: hasTextField(mailroom, "mailboxAddress") && !!privateKeys && typeof privateKeys === "object" && !Array.isArray(privateKeys),
|
|
451
|
-
voice: hasTextField(integrations, "elevenLabsApiKey")
|
|
452
|
+
voice: hasTextField(integrations, "elevenLabsApiKey")
|
|
453
|
+
&& (hasTextField(integrations, "elevenLabsVoiceId") || hasTextField(portableVoice, "elevenLabsVoiceId"))
|
|
454
|
+
&& hasTextField(voice, "whisperCliPath")
|
|
455
|
+
&& hasTextField(voice, "whisperModelPath"),
|
|
452
456
|
};
|
|
453
457
|
const rows = [
|
|
454
458
|
{ label: "CLI", status: "interactive" },
|
|
@@ -502,7 +506,7 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
|
|
|
502
506
|
lines.push("mail validation diagnostics: health checks, bounded mail tools, access logs, and UI inspection can support validation, but they are evidence inside those paths, not additional paths. If asked to name golden paths, do not include diagnostic commands, tool names, or status checks in the answer.");
|
|
503
507
|
lines.push("mail diagnostic naming: `ouro doctor` is installation-wide; do not invent `ouro doctor --agent <agent>`.");
|
|
504
508
|
lines.push("mail setup boundaries: do not invent `ouro auth verify --provider mail`, HEY OAuth, HEY IMAP, `ouro mcp call mail ...`, policy flags, autonomous sending, destructive mail actions, or production MX/DNS/forwarding changes. HEY export, HEY forwarding, DNS, MX cutover, sending, and destructive actions require explicit human confirmation.");
|
|
505
|
-
lines.push("voice setup truth: voice sessions are transcript-first local sessions. ElevenLabs credentials belong in portable runtime/config at `integrations.elevenLabsApiKey`; Whisper.cpp CLI/model paths belong in the machine runtime item under `voice.whisperCliPath` and `voice.whisperModelPath`. Meeting
|
|
509
|
+
lines.push("voice setup truth: voice sessions are transcript-first local sessions. ElevenLabs credentials belong in portable runtime/config at `integrations.elevenLabsApiKey` and `integrations.elevenLabsVoiceId`; Whisper.cpp CLI/model paths belong in the machine runtime item under `voice.whisperCliPath` and `voice.whisperModelPath`. Meeting links have URL intake and local BlackHole/Multi-Output readiness checks; phone testing uses Twilio Record -> Whisper.cpp -> voice session -> ElevenLabs -> Twilio Play. Live browser join/injection remains an explicit handoff edge until provider automation lands.");
|
|
506
510
|
if (channel === "cli") {
|
|
507
511
|
lines.push("cli is interactive: it is available when the user opens it, not something `ouro up` daemonizes.");
|
|
508
512
|
}
|
|
@@ -656,6 +656,12 @@ exports.sessionToolDefinitions = [
|
|
|
656
656
|
detail: "bluebubbles could not resolve a routable target for that session",
|
|
657
657
|
};
|
|
658
658
|
}
|
|
659
|
+
if (result.reason === "blocked_meta_content") {
|
|
660
|
+
return {
|
|
661
|
+
status: "blocked",
|
|
662
|
+
detail: "blocked: contains internal meta markers",
|
|
663
|
+
};
|
|
664
|
+
}
|
|
659
665
|
if (result.reason === "send_error") {
|
|
660
666
|
return {
|
|
661
667
|
status: "failed",
|
|
@@ -39,6 +39,8 @@ const identity_1 = require("../heart/identity");
|
|
|
39
39
|
const surface_tool_1 = require("../senses/surface-tool");
|
|
40
40
|
const obligations_1 = require("../arc/obligations");
|
|
41
41
|
const session_activity_1 = require("../heart/session-activity");
|
|
42
|
+
const bluebubbles_meta_guard_1 = require("../senses/bluebubbles-meta-guard");
|
|
43
|
+
const runtime_1 = require("../nerves/runtime");
|
|
42
44
|
const path = __importStar(require("path"));
|
|
43
45
|
// Surface tool schema — canonical home. Handler lives in senses/surface-tool.ts.
|
|
44
46
|
exports.surfaceToolDef = {
|
|
@@ -71,6 +73,21 @@ exports.surfaceToolDef = {
|
|
|
71
73
|
exports.surfaceToolDefinition = {
|
|
72
74
|
tool: exports.surfaceToolDef,
|
|
73
75
|
handler: async (args, ctx) => {
|
|
76
|
+
const rawContent = args.content ?? "";
|
|
77
|
+
if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(rawContent)) {
|
|
78
|
+
(0, runtime_1.emitNervesEvent)({
|
|
79
|
+
level: "warn",
|
|
80
|
+
component: "repertoire",
|
|
81
|
+
event: "tools.surface_meta_blocked",
|
|
82
|
+
message: "surface tool blocked: internal meta markers in content",
|
|
83
|
+
meta: {
|
|
84
|
+
hasDelegationId: Boolean(args.delegationId),
|
|
85
|
+
hasFriendId: Boolean(args.friendId),
|
|
86
|
+
contentLength: rawContent.length,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
return "failed — blocked: contains internal meta markers";
|
|
90
|
+
}
|
|
74
91
|
const queue = ctx?.delegatedOrigins ?? [];
|
|
75
92
|
const agentName = (() => { try {
|
|
76
93
|
return (0, identity_1.getAgentName)();
|
|
@@ -69,6 +69,7 @@ const prompt_1 = require("../../mind/prompt");
|
|
|
69
69
|
const mcp_manager_1 = require("../../repertoire/mcp-manager");
|
|
70
70
|
const runtime_1 = require("../../nerves/runtime");
|
|
71
71
|
const proactive_content_guard_1 = require("../proactive-content-guard");
|
|
72
|
+
const bluebubbles_meta_guard_1 = require("../bluebubbles-meta-guard");
|
|
72
73
|
const model_1 = require("./model");
|
|
73
74
|
const client_1 = require("./client");
|
|
74
75
|
const inbound_log_1 = require("./inbound-log");
|
|
@@ -616,6 +617,17 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
616
617
|
if (!trimmed)
|
|
617
618
|
return;
|
|
618
619
|
textBuffer = "";
|
|
620
|
+
if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(trimmed)) {
|
|
621
|
+
(0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
|
|
622
|
+
site: "flushNow",
|
|
623
|
+
message: "bluebubbles speak text blocked: internal meta markers",
|
|
624
|
+
meta: {
|
|
625
|
+
chatGuid: chat.chatGuid ?? null,
|
|
626
|
+
messageLength: trimmed.length,
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
619
631
|
await client.sendText({
|
|
620
632
|
chat,
|
|
621
633
|
text: trimmed,
|
|
@@ -650,6 +662,17 @@ function createBlueBubblesCallbacks(client, chat, replyTarget, isGroupChat, onVi
|
|
|
650
662
|
enqueue("typing_stop", async () => { await client.setTyping(chat, false); });
|
|
651
663
|
await queue;
|
|
652
664
|
}
|
|
665
|
+
if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(trimmed)) {
|
|
666
|
+
(0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
|
|
667
|
+
site: "flush",
|
|
668
|
+
message: "bluebubbles outbound text blocked: internal meta markers",
|
|
669
|
+
meta: {
|
|
670
|
+
chatGuid: chat.chatGuid ?? null,
|
|
671
|
+
messageLength: trimmed.length,
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
653
676
|
await client.sendText({
|
|
654
677
|
chat,
|
|
655
678
|
text: trimmed,
|
|
@@ -1925,6 +1948,17 @@ function buildChatRefForSessionKey(friend, sessionKey) {
|
|
|
1925
1948
|
};
|
|
1926
1949
|
}
|
|
1927
1950
|
async function sendProactiveBlueBubblesMessageToSession(params, deps = {}) {
|
|
1951
|
+
if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(params.text)) {
|
|
1952
|
+
(0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
|
|
1953
|
+
site: "proactive",
|
|
1954
|
+
message: "bluebubbles proactive send blocked: internal meta markers",
|
|
1955
|
+
meta: {
|
|
1956
|
+
friendId: params.friendId,
|
|
1957
|
+
sessionKey: params.sessionKey,
|
|
1958
|
+
},
|
|
1959
|
+
});
|
|
1960
|
+
return { delivered: false, reason: "blocked_meta_content" };
|
|
1961
|
+
}
|
|
1928
1962
|
const resolvedDeps = { ...defaultDeps, ...deps };
|
|
1929
1963
|
const client = resolvedDeps.createClient();
|
|
1930
1964
|
const store = resolvedDeps.createFriendStore();
|
|
@@ -2131,6 +2165,22 @@ async function drainAndSendPendingBlueBubbles(deps = {}, pendingRoot) {
|
|
|
2131
2165
|
catch { /* ignore */ }
|
|
2132
2166
|
continue;
|
|
2133
2167
|
}
|
|
2168
|
+
if ((0, bluebubbles_meta_guard_1.containsInternalMetaMarkers)(messageText)) {
|
|
2169
|
+
result.skipped++;
|
|
2170
|
+
try {
|
|
2171
|
+
fs.unlinkSync(filePath);
|
|
2172
|
+
}
|
|
2173
|
+
catch { /* ignore */ }
|
|
2174
|
+
(0, bluebubbles_meta_guard_1.emitBluebubblesMetaBlocked)({
|
|
2175
|
+
site: "drain",
|
|
2176
|
+
message: "bluebubbles drain blocked: internal meta markers",
|
|
2177
|
+
meta: {
|
|
2178
|
+
friendId,
|
|
2179
|
+
filePath,
|
|
2180
|
+
},
|
|
2181
|
+
});
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2134
2184
|
const internalBlockReason = (0, proactive_content_guard_1.getProactiveInternalContentBlockReason)(messageText);
|
|
2135
2185
|
if (internalBlockReason) {
|
|
2136
2186
|
result.skipped++;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Outbound BlueBubbles meta-content guard.
|
|
3
|
+
//
|
|
4
|
+
// Blocks accidental delivery of internal/meta text — pipeline section markers,
|
|
5
|
+
// surfacing-mechanics prefixes, reasoning tags — to the live iMessage channel.
|
|
6
|
+
// Failure mode is "drop and log", never queue for later delivery.
|
|
7
|
+
//
|
|
8
|
+
// Patterns are deliberately narrow: bracketed system markers and angle-bracket
|
|
9
|
+
// reasoning tags. Plain prose mentioning "inner dialog" or "attention queue"
|
|
10
|
+
// is NOT blocked, so user-facing replies that legitimately discuss those
|
|
11
|
+
// concepts still pass.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.containsInternalMetaMarkers = containsInternalMetaMarkers;
|
|
14
|
+
exports.emitBluebubblesMetaBlocked = emitBluebubblesMetaBlocked;
|
|
15
|
+
const runtime_1 = require("../nerves/runtime");
|
|
16
|
+
const META_CONTENT_PATTERNS = [
|
|
17
|
+
/\[surfaced from inner dialog\]/i,
|
|
18
|
+
/\[pending from [^\]]+\]:/i,
|
|
19
|
+
/\[conversation scope:/i,
|
|
20
|
+
/\[recent active lanes\]/i,
|
|
21
|
+
/\[routing control:/i,
|
|
22
|
+
/<\/?think>/i,
|
|
23
|
+
];
|
|
24
|
+
function containsInternalMetaMarkers(text) {
|
|
25
|
+
if (!text)
|
|
26
|
+
return false;
|
|
27
|
+
return META_CONTENT_PATTERNS.some((pattern) => pattern.test(text));
|
|
28
|
+
}
|
|
29
|
+
function emitBluebubblesMetaBlocked(options) {
|
|
30
|
+
(0, runtime_1.emitNervesEvent)({
|
|
31
|
+
level: "warn",
|
|
32
|
+
component: "senses",
|
|
33
|
+
event: "senses.bluebubbles_meta_blocked",
|
|
34
|
+
message: options.message,
|
|
35
|
+
meta: {
|
|
36
|
+
site: options.site,
|
|
37
|
+
...options.meta,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -43,6 +43,7 @@ exports.stripThinkBlocks = stripThinkBlocks;
|
|
|
43
43
|
exports.runSenseTurn = runSenseTurn;
|
|
44
44
|
const os = __importStar(require("os"));
|
|
45
45
|
const path = __importStar(require("path"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
46
47
|
const core_1 = require("../heart/core");
|
|
47
48
|
const identity_1 = require("../heart/identity");
|
|
48
49
|
const config_1 = require("../heart/config");
|
|
@@ -130,7 +131,9 @@ async function runSenseTurn(options) {
|
|
|
130
131
|
// Initialize MCP manager so MCP tools appear as first-class tools in the agent's tool list
|
|
131
132
|
const mcpManager = await (0, mcp_manager_1.getSharedMcpManager)() ?? undefined;
|
|
132
133
|
// Session path and loading
|
|
133
|
-
const
|
|
134
|
+
const sessionDir = path.join(agentRoot, "state", "sessions", friendId, channel);
|
|
135
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
136
|
+
const sessPath = path.join(sessionDir, `${(0, config_1.sanitizeKey)(sessionKey)}.json`);
|
|
134
137
|
const existing = (0, context_1.loadSession)(sessPath);
|
|
135
138
|
let sessionState = existing?.state;
|
|
136
139
|
let persistPromise;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNodeVoiceCommandRunner = createNodeVoiceCommandRunner;
|
|
4
|
+
exports.inspectVoiceAudioRouting = inspectVoiceAudioRouting;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
7
|
+
function createNodeVoiceCommandRunner() {
|
|
8
|
+
return (command, args, options) => new Promise((resolve, reject) => {
|
|
9
|
+
const child = (0, child_process_1.spawn)(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
10
|
+
const stdout = [];
|
|
11
|
+
const stderr = [];
|
|
12
|
+
const timer = setTimeout(() => {
|
|
13
|
+
child.kill("SIGTERM");
|
|
14
|
+
reject(new Error(`command timed out after ${options.timeoutMs}ms`));
|
|
15
|
+
}, options.timeoutMs);
|
|
16
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
17
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
18
|
+
child.on("error", (error) => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
reject(error);
|
|
21
|
+
});
|
|
22
|
+
child.on("close", (exitCode) => {
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
resolve({
|
|
25
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
26
|
+
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
27
|
+
exitCode: exitCode ?? 0,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function parseDeviceLines(output) {
|
|
33
|
+
return output
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.map((line) => line.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
}
|
|
38
|
+
function commandFailureMessage(exitCode, result) {
|
|
39
|
+
const stderr = result.stderr?.trim();
|
|
40
|
+
if (stderr)
|
|
41
|
+
return stderr;
|
|
42
|
+
const stdout = result.stdout?.trim();
|
|
43
|
+
if (stdout)
|
|
44
|
+
return stdout;
|
|
45
|
+
return `exit ${exitCode}`;
|
|
46
|
+
}
|
|
47
|
+
function setupGuidance(missing, currentOutput, outputDeviceName) {
|
|
48
|
+
const guidance = missing.map((device) => `Install or configure the local audio device: ${device}.`);
|
|
49
|
+
if (currentOutput && currentOutput !== outputDeviceName) {
|
|
50
|
+
guidance.push(`Browser meeting audio should be routed through ${outputDeviceName}; current output is ${currentOutput}.`);
|
|
51
|
+
}
|
|
52
|
+
return guidance;
|
|
53
|
+
}
|
|
54
|
+
async function inspectVoiceAudioRouting(options = {}) {
|
|
55
|
+
const commandRunner = options.commandRunner ?? createNodeVoiceCommandRunner();
|
|
56
|
+
const switchAudioSourcePath = options.switchAudioSourcePath ?? "SwitchAudioSource";
|
|
57
|
+
const captureDeviceName = options.captureDeviceName ?? "BlackHole 2ch";
|
|
58
|
+
const outputDeviceName = options.outputDeviceName ?? "Multi-Output Device";
|
|
59
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
60
|
+
try {
|
|
61
|
+
const devicesResult = await commandRunner(switchAudioSourcePath, ["-a"], { timeoutMs });
|
|
62
|
+
if (typeof devicesResult.exitCode === "number" && devicesResult.exitCode !== 0) {
|
|
63
|
+
throw new Error(commandFailureMessage(devicesResult.exitCode, devicesResult));
|
|
64
|
+
}
|
|
65
|
+
const currentResult = await commandRunner(switchAudioSourcePath, ["-c"], { timeoutMs });
|
|
66
|
+
if (typeof currentResult.exitCode === "number" && currentResult.exitCode !== 0) {
|
|
67
|
+
throw new Error(commandFailureMessage(currentResult.exitCode, currentResult));
|
|
68
|
+
}
|
|
69
|
+
const devices = parseDeviceLines(devicesResult.stdout ?? "");
|
|
70
|
+
const currentOutput = parseDeviceLines(currentResult.stdout ?? "")[0] ?? null;
|
|
71
|
+
const hasCaptureDevice = devices.includes(captureDeviceName);
|
|
72
|
+
const hasOutputDevice = devices.includes(outputDeviceName);
|
|
73
|
+
const missing = [
|
|
74
|
+
...(hasCaptureDevice ? [] : [captureDeviceName]),
|
|
75
|
+
...(hasOutputDevice ? [] : [outputDeviceName]),
|
|
76
|
+
];
|
|
77
|
+
const result = {
|
|
78
|
+
status: missing.length === 0 ? "ready" : "needs_setup",
|
|
79
|
+
hasCaptureDevice,
|
|
80
|
+
hasOutputDevice,
|
|
81
|
+
currentOutput,
|
|
82
|
+
missing,
|
|
83
|
+
guidance: setupGuidance(missing, currentOutput, outputDeviceName),
|
|
84
|
+
};
|
|
85
|
+
(0, runtime_1.emitNervesEvent)({
|
|
86
|
+
component: "senses",
|
|
87
|
+
event: "senses.voice_audio_routing_checked",
|
|
88
|
+
message: "voice audio routing readiness checked",
|
|
89
|
+
meta: {
|
|
90
|
+
status: result.status,
|
|
91
|
+
hasCaptureDevice,
|
|
92
|
+
hasOutputDevice,
|
|
93
|
+
currentOutput,
|
|
94
|
+
missing,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
const result = {
|
|
102
|
+
status: "unknown",
|
|
103
|
+
hasCaptureDevice: false,
|
|
104
|
+
hasOutputDevice: false,
|
|
105
|
+
currentOutput: null,
|
|
106
|
+
missing: [captureDeviceName, outputDeviceName],
|
|
107
|
+
guidance: setupGuidance([captureDeviceName, outputDeviceName], null, outputDeviceName),
|
|
108
|
+
error: message,
|
|
109
|
+
};
|
|
110
|
+
(0, runtime_1.emitNervesEvent)({
|
|
111
|
+
level: "error",
|
|
112
|
+
component: "senses",
|
|
113
|
+
event: "senses.voice_audio_routing_error",
|
|
114
|
+
message: "voice audio routing readiness check failed",
|
|
115
|
+
meta: { error: message, missing: result.missing },
|
|
116
|
+
});
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEFAULT_ELEVENLABS_MIME_TYPE = exports.DEFAULT_ELEVENLABS_OUTPUT_FORMAT = exports.DEFAULT_ELEVENLABS_MODEL_ID = void 0;
|
|
4
|
+
exports.createNodeElevenLabsSocketFactory = createNodeElevenLabsSocketFactory;
|
|
4
5
|
exports.createElevenLabsTtsClient = createElevenLabsTtsClient;
|
|
5
6
|
const runtime_1 = require("../../nerves/runtime");
|
|
6
7
|
exports.DEFAULT_ELEVENLABS_MODEL_ID = "eleven_flash_v2_5";
|
|
@@ -14,15 +15,67 @@ function elevenLabsStreamUrl(voiceId, modelId, outputFormat) {
|
|
|
14
15
|
return `wss://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}/stream-input?${params.toString()}`;
|
|
15
16
|
}
|
|
16
17
|
function payloadText(payload) {
|
|
18
|
+
if (payload && typeof payload === "object" && "data" in payload) {
|
|
19
|
+
return payloadText(payload.data);
|
|
20
|
+
}
|
|
17
21
|
if (typeof payload === "string")
|
|
18
22
|
return payload;
|
|
19
23
|
if (Buffer.isBuffer(payload))
|
|
20
24
|
return payload.toString("utf8");
|
|
21
25
|
return String(payload ?? "");
|
|
22
26
|
}
|
|
27
|
+
function createNodeElevenLabsSocketFactory(webSocketConstructor) {
|
|
28
|
+
const WebSocketConstructor = webSocketConstructor
|
|
29
|
+
?? globalThis.WebSocket;
|
|
30
|
+
if (!WebSocketConstructor) {
|
|
31
|
+
throw new Error("global WebSocket is unavailable; inject an ElevenLabs socketFactory");
|
|
32
|
+
}
|
|
33
|
+
return (url) => {
|
|
34
|
+
const socket = new WebSocketConstructor(url);
|
|
35
|
+
const handlers = {
|
|
36
|
+
open: [],
|
|
37
|
+
message: [],
|
|
38
|
+
error: [],
|
|
39
|
+
close: [],
|
|
40
|
+
};
|
|
41
|
+
const emit = (event, payload) => {
|
|
42
|
+
for (const handler of handlers[event]) {
|
|
43
|
+
handler(payload);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const attach = (event) => {
|
|
47
|
+
const listener = (payload) => emit(event, payload);
|
|
48
|
+
if (typeof socket.addEventListener === "function") {
|
|
49
|
+
socket.addEventListener(event, listener);
|
|
50
|
+
}
|
|
51
|
+
else if (typeof socket.on === "function") {
|
|
52
|
+
socket.on(event, listener);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
socket[`on${event}`] = listener;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
attach("open");
|
|
59
|
+
attach("message");
|
|
60
|
+
attach("error");
|
|
61
|
+
attach("close");
|
|
62
|
+
return {
|
|
63
|
+
on(event, handler) {
|
|
64
|
+
handlers[event].push(handler);
|
|
65
|
+
},
|
|
66
|
+
send(payload) {
|
|
67
|
+
socket.send(payload);
|
|
68
|
+
},
|
|
69
|
+
close() {
|
|
70
|
+
socket.close();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
}
|
|
23
75
|
function createElevenLabsTtsClient(options) {
|
|
24
76
|
const modelId = options.modelId ?? exports.DEFAULT_ELEVENLABS_MODEL_ID;
|
|
25
77
|
const outputFormat = options.outputFormat ?? exports.DEFAULT_ELEVENLABS_OUTPUT_FORMAT;
|
|
78
|
+
const socketFactory = options.socketFactory ?? createNodeElevenLabsSocketFactory();
|
|
26
79
|
const mimeType = outputFormat === exports.DEFAULT_ELEVENLABS_OUTPUT_FORMAT
|
|
27
80
|
? exports.DEFAULT_ELEVENLABS_MIME_TYPE
|
|
28
81
|
: "audio/mpeg";
|
|
@@ -40,7 +93,7 @@ function createElevenLabsTtsClient(options) {
|
|
|
40
93
|
throw new Error("voice TTS text is empty");
|
|
41
94
|
}
|
|
42
95
|
const url = elevenLabsStreamUrl(options.voiceId, modelId, outputFormat);
|
|
43
|
-
const socket =
|
|
96
|
+
const socket = socketFactory(url);
|
|
44
97
|
const chunks = [];
|
|
45
98
|
(0, runtime_1.emitNervesEvent)({
|
|
46
99
|
component: "senses",
|