@ouro.bot/cli 0.1.0-alpha.887 → 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 +12 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/core.js +51 -28
- package/dist/heart/daemon/cli-exec.js +18 -0
- package/dist/heart/daemon/cli-help.js +7 -0
- package/dist/heart/daemon/cli-parse.js +154 -0
- package/dist/repertoire/tools-voice-local.js +198 -0
- package/dist/repertoire/tools.js +2 -0
- package/dist/senses/voice/call-auth.js +3 -1
- package/dist/senses/voice/local-audio-control.js +344 -0
- package/dist/senses/voice/local-audio-devices.js +471 -0
- package/dist/senses/voice/local-audio-disclosure.js +174 -0
- package/dist/senses/voice/local-audio-identity.js +103 -0
- package/dist/senses/voice/local-audio-join.js +325 -0
- package/dist/senses/voice/local-audio-mute.js +199 -0
- package/dist/senses/voice/local-audio-notify.js +121 -0
- package/dist/senses/voice/local-audio-prompts.js +116 -0
- package/dist/senses/voice/local-audio-transport.js +406 -0
- package/dist/senses/voice/local-audio.js +281 -0
- package/dist/senses/voice/local-media-stream.js +145 -0
- package/dist/senses/voice/twilio-phone-runtime.js +16 -0
- package/dist/senses/voice/twilio-phone.js +116 -17
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.TwilioAudioStreamJobStore = exports.TwilioOpenAIRealtimeMediaStreamSession = exports.TwilioMediaStreamSession = exports.DEFAULT_TWILIO_MEDIA_MAX_UTTERANCE_MS = exports.DEFAULT_TWILIO_MEDIA_MIN_SPEECH_MS = exports.DEFAULT_TWILIO_MEDIA_SILENCE_END_MS = exports.DEFAULT_TWILIO_MEDIA_SPEECH_RMS_THRESHOLD = exports.DEFAULT_TWILIO_PHONE_TRANSPORT_MODE = exports.DEFAULT_TWILIO_PHONE_PLAYBACK_MODE = exports.TWILIO_PHONE_WEBHOOK_BASE_PATH = exports.DEFAULT_TWILIO_GREETING_PREBUFFER_MS = exports.DEFAULT_TWILIO_RECORD_MAX_LENGTH_SECONDS = exports.DEFAULT_TWILIO_RECORD_TIMEOUT_SECONDS = exports.DEFAULT_TWILIO_PHONE_PORT = exports.normalizeTwilioE164PhoneNumber = exports.PendingVoiceCalls = void 0;
|
|
36
|
+
exports.TwilioAudioStreamJobStore = exports.TwilioOpenAIRealtimeMediaStreamSession = exports.LOCAL_AUDIO_TOOL_ALLOWLIST = exports.TwilioMediaStreamSession = exports.DEFAULT_TWILIO_MEDIA_MAX_UTTERANCE_MS = exports.DEFAULT_TWILIO_MEDIA_MIN_SPEECH_MS = exports.DEFAULT_TWILIO_MEDIA_SILENCE_END_MS = exports.DEFAULT_TWILIO_MEDIA_SPEECH_RMS_THRESHOLD = exports.DEFAULT_TWILIO_PHONE_TRANSPORT_MODE = exports.DEFAULT_TWILIO_PHONE_PLAYBACK_MODE = exports.TWILIO_PHONE_WEBHOOK_BASE_PATH = exports.DEFAULT_TWILIO_GREETING_PREBUFFER_MS = exports.DEFAULT_TWILIO_RECORD_MAX_LENGTH_SECONDS = exports.DEFAULT_TWILIO_RECORD_TIMEOUT_SECONDS = exports.DEFAULT_TWILIO_PHONE_PORT = exports.normalizeTwilioE164PhoneNumber = exports.PendingVoiceCalls = void 0;
|
|
37
37
|
exports.resolveTwilioPhoneAgentRoot = resolveTwilioPhoneAgentRoot;
|
|
38
38
|
exports.normalizeTwilioPhoneBasePath = normalizeTwilioPhoneBasePath;
|
|
39
39
|
exports.normalizeTwilioPhonePlaybackMode = normalizeTwilioPhonePlaybackMode;
|
|
@@ -50,6 +50,7 @@ exports.twilioPhoneVoiceSessionKey = twilioPhoneVoiceSessionKey;
|
|
|
50
50
|
exports.outboundCallAnsweredPrompt = outboundCallAnsweredPrompt;
|
|
51
51
|
exports.computeOpenAIWebhookSignature = computeOpenAIWebhookSignature;
|
|
52
52
|
exports.validateOpenAIWebhookSignature = validateOpenAIWebhookSignature;
|
|
53
|
+
exports.tonesOnly = tonesOnly;
|
|
53
54
|
exports.parseToolArguments = parseToolArguments;
|
|
54
55
|
exports.computeTwilioSignature = computeTwilioSignature;
|
|
55
56
|
exports.validateTwilioSignature = validateTwilioSignature;
|
|
@@ -83,6 +84,9 @@ const phone_1 = require("./phone");
|
|
|
83
84
|
const audio_playback_1 = require("./audio-playback");
|
|
84
85
|
const floor_controller_1 = require("./floor-controller");
|
|
85
86
|
const call_auth_1 = require("./call-auth");
|
|
87
|
+
const local_audio_identity_1 = require("./local-audio-identity");
|
|
88
|
+
const local_audio_disclosure_1 = require("./local-audio-disclosure");
|
|
89
|
+
const local_audio_prompts_1 = require("./local-audio-prompts");
|
|
86
90
|
var call_auth_2 = require("./call-auth");
|
|
87
91
|
Object.defineProperty(exports, "PendingVoiceCalls", { enumerable: true, get: function () { return call_auth_2.PendingVoiceCalls; } });
|
|
88
92
|
var phone_2 = require("./phone");
|
|
@@ -1500,10 +1504,29 @@ function realtimeOutputAudioConfig(realtime, format) {
|
|
|
1500
1504
|
...(speed === undefined ? {} : { speed }),
|
|
1501
1505
|
};
|
|
1502
1506
|
}
|
|
1507
|
+
/**
|
|
1508
|
+
* A local audio room is several unidentified people on the owner's Mac, so no trust level (not even
|
|
1509
|
+
* family through an owner-alone join) unlocks files, shell, credentials or MCP: the whole session is
|
|
1510
|
+
* limited to these two tools, in what it advertises and in what it will run.
|
|
1511
|
+
*/
|
|
1512
|
+
exports.LOCAL_AUDIO_TOOL_ALLOWLIST = new Set(["voice_end_call", "voice_play_audio"]);
|
|
1513
|
+
/**
|
|
1514
|
+
* In a local audio room anyone in the room can ask for a clip, so only generated tones play: a URL
|
|
1515
|
+
* clip would make the Mac fetch any address (including the LAN), and a file clip reads local files.
|
|
1516
|
+
*/
|
|
1517
|
+
function tonesOnly(play) {
|
|
1518
|
+
return async (request) => {
|
|
1519
|
+
if ((request.source ?? "tone") !== "tone")
|
|
1520
|
+
throw new Error("only tones can play in a local audio room; clips from a URL or file are not allowed here");
|
|
1521
|
+
return play(request);
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1503
1524
|
function selectRealtimeToolContext(context, mcpManager) {
|
|
1525
|
+
const allowlist = context.toolAllowlist;
|
|
1504
1526
|
const selectCurrentTools = () => {
|
|
1505
1527
|
const selected = (0, tools_1.selectToolsForChannel)((0, friends_1.getChannelCapabilities)("voice"), context.context?.friend.toolPreferences, context.context, undefined, mcpManager, undefined, context);
|
|
1506
|
-
|
|
1528
|
+
const ordinary = allowlist ? selected.ordinary.filter(({ tool }) => allowlist.has(tool.function.name)) : selected.ordinary;
|
|
1529
|
+
return Object.freeze({ ordinary, engine: Object.freeze([]) });
|
|
1507
1530
|
};
|
|
1508
1531
|
return { ...context, toolSelection: selectCurrentTools(), selectCurrentTools };
|
|
1509
1532
|
}
|
|
@@ -1669,6 +1692,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1669
1692
|
started = false;
|
|
1670
1693
|
openaiReady = false;
|
|
1671
1694
|
greetingSent = false;
|
|
1695
|
+
disclosure;
|
|
1672
1696
|
hangupRequested = false;
|
|
1673
1697
|
pendingAudioPayloads = [];
|
|
1674
1698
|
openaiWs = null;
|
|
@@ -1771,20 +1795,29 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1771
1795
|
this.from = this.identity.from;
|
|
1772
1796
|
this.to = this.identity.to;
|
|
1773
1797
|
try {
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1798
|
+
const local = this.identity.local;
|
|
1799
|
+
const voiceContext = local
|
|
1800
|
+
? await (0, local_audio_identity_1.resolveLocalAudioFriend)({
|
|
1801
|
+
agentRoot: resolveTwilioPhoneAgentRoot(this.options),
|
|
1802
|
+
friendId: this.identity.friendId,
|
|
1803
|
+
ownerAlone: local.ownerAlone,
|
|
1804
|
+
})
|
|
1805
|
+
: await resolveVoiceFriendContext(this.options, {
|
|
1806
|
+
friendId: this.direction === "outbound" ? this.identity.friendId : undefined,
|
|
1807
|
+
remotePhone: this.from || undefined,
|
|
1808
|
+
callSid: this.callSid,
|
|
1809
|
+
});
|
|
1779
1810
|
this.friendId = voiceContext.friendId;
|
|
1780
1811
|
this.friendStore = voiceContext.friendStore;
|
|
1781
1812
|
this.resolvedContext = voiceContext.resolved;
|
|
1782
|
-
this.sessionKey =
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1813
|
+
this.sessionKey = local
|
|
1814
|
+
? `local-audio-${safeSegment(this.callSid)}`
|
|
1815
|
+
: twilioPhoneVoiceSessionKey({
|
|
1816
|
+
defaultFriendId: this.friendId,
|
|
1817
|
+
from: this.from,
|
|
1818
|
+
to: this.to,
|
|
1819
|
+
callSid: this.callSid,
|
|
1820
|
+
});
|
|
1788
1821
|
this.lifecycle?.onIdentityChange?.(this, { callSid: this.callSid, outboundId: this.outboundId });
|
|
1789
1822
|
this.floor = new floor_controller_1.VoiceFloorController({
|
|
1790
1823
|
transport: "twilio-media-stream",
|
|
@@ -1884,9 +1917,9 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1884
1917
|
];
|
|
1885
1918
|
if (!toolContext || this.closed)
|
|
1886
1919
|
return;
|
|
1887
|
-
this.sendOpenAIRealtimeSessionUpdate(realtime, instructions, toolContext);
|
|
1920
|
+
this.sendOpenAIRealtimeSessionUpdate(realtime, this.localizeInstructions(instructions), toolContext);
|
|
1888
1921
|
this.flushPendingAudio();
|
|
1889
|
-
this.
|
|
1922
|
+
this.sendGreeting();
|
|
1890
1923
|
if (!usedBootstrap)
|
|
1891
1924
|
return;
|
|
1892
1925
|
Promise.all([instructionsPromise, toolsPromise])
|
|
@@ -1898,7 +1931,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1898
1931
|
type: "session.update",
|
|
1899
1932
|
session: {
|
|
1900
1933
|
type: "realtime",
|
|
1901
|
-
instructions: fullInstructions,
|
|
1934
|
+
instructions: this.localizeInstructions(fullInstructions),
|
|
1902
1935
|
tools: realtimeToolsFromSelection(fullContext.toolSelection, this.toolAdvertiser),
|
|
1903
1936
|
tool_choice: "auto",
|
|
1904
1937
|
},
|
|
@@ -1913,6 +1946,64 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1913
1946
|
this.end();
|
|
1914
1947
|
});
|
|
1915
1948
|
}
|
|
1949
|
+
/** Local audio rooms get transport-aware wording and the room facts; phone calls are unchanged. */
|
|
1950
|
+
localizeInstructions(instructions) {
|
|
1951
|
+
const local = this.identity.local;
|
|
1952
|
+
return local
|
|
1953
|
+
? (0, local_audio_prompts_1.localizeRealtimeInstructions)(instructions, this.options.agentName, local, { disclosureSpoken: this.disclosure?.isSpoken() === true })
|
|
1954
|
+
: instructions;
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* A spoken local join opens with the disclosure notice and tracks it until it has really been
|
|
1958
|
+
* spoken (retrying a lost, cancelled or cut attempt); a consented silent join has no first turn.
|
|
1959
|
+
*/
|
|
1960
|
+
sendGreeting() {
|
|
1961
|
+
const local = this.identity.local;
|
|
1962
|
+
if (!local) {
|
|
1963
|
+
this.sendInitialGreeting();
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
// Configured once per Realtime connection, so the opening turn is requested exactly once here.
|
|
1967
|
+
this.greetingSent = true;
|
|
1968
|
+
const prompt = (0, local_audio_prompts_1.localAudioGreetingPrompt)(this.options.agentName, local);
|
|
1969
|
+
if (!prompt)
|
|
1970
|
+
return;
|
|
1971
|
+
const body = { instructions: prompt };
|
|
1972
|
+
this.disclosure = new local_audio_disclosure_1.DisclosureTracker({
|
|
1973
|
+
body,
|
|
1974
|
+
host: {
|
|
1975
|
+
request: (response) => this.requestRealtimeResponse(response),
|
|
1976
|
+
isQueued: (response) => this.holdsRealtimeResponse(response),
|
|
1977
|
+
sendMark: (name) => this.sendDisclosureMark(name),
|
|
1978
|
+
onSpoken: (atMs) => this.noteDisclosureSpoken(atMs),
|
|
1979
|
+
onFailed: () => this.lifecycle?.onDisclosureFailed?.(),
|
|
1980
|
+
},
|
|
1981
|
+
});
|
|
1982
|
+
this.disclosure.start();
|
|
1983
|
+
}
|
|
1984
|
+
/** True while a response.create for this body is held by the floor gate, the hold timer or the wire. */
|
|
1985
|
+
holdsRealtimeResponse(response) {
|
|
1986
|
+
return this.queuedGatedRealtimeRequest?.response === response
|
|
1987
|
+
|| this.pendingRealtimeResponse?.response === response
|
|
1988
|
+
|| this.realtimeResponseCreateInFlight?.response === response;
|
|
1989
|
+
}
|
|
1990
|
+
sendDisclosureMark(name) {
|
|
1991
|
+
if (this.closed || !this.streamSid || !this.socket.isOpen())
|
|
1992
|
+
return;
|
|
1993
|
+
this.socket.send(JSON.stringify({ event: "mark", streamSid: this.streamSid, mark: { name } }));
|
|
1994
|
+
}
|
|
1995
|
+
noteDisclosureSpoken(atMs) {
|
|
1996
|
+
// The instructions said "do not claim you announced yourself until a system note confirms it".
|
|
1997
|
+
this.sendOpenAI({
|
|
1998
|
+
type: "conversation.item.create",
|
|
1999
|
+
item: {
|
|
2000
|
+
type: "message",
|
|
2001
|
+
role: "system",
|
|
2002
|
+
content: [{ type: "input_text", text: "System note: your disclosure notice has now been spoken aloud and heard. You have announced yourself." }],
|
|
2003
|
+
},
|
|
2004
|
+
});
|
|
2005
|
+
this.lifecycle?.onDisclosureSpoken?.(atMs);
|
|
2006
|
+
}
|
|
1916
2007
|
sendOpenAIRealtimeSessionUpdate(realtime, instructions, toolContext) {
|
|
1917
2008
|
this.toolContext = toolContext;
|
|
1918
2009
|
this.sendOpenAI({
|
|
@@ -1974,6 +2065,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1974
2065
|
ensureVoiceToolContext() {
|
|
1975
2066
|
if (this.toolContext)
|
|
1976
2067
|
return selectRealtimeToolContext(this.toolContext);
|
|
2068
|
+
const play = (request) => this.playPreparedAudio(request);
|
|
1977
2069
|
const context = selectRealtimeToolContext({
|
|
1978
2070
|
signin: async () => undefined,
|
|
1979
2071
|
agentName: this.options.agentName,
|
|
@@ -1982,8 +2074,9 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
1982
2074
|
friendStore: this.friendStore,
|
|
1983
2075
|
voiceCall: {
|
|
1984
2076
|
requestEnd: () => this.requestHangupFromTool(),
|
|
1985
|
-
playAudio: (
|
|
2077
|
+
playAudio: this.identity.local ? tonesOnly(play) : play,
|
|
1986
2078
|
},
|
|
2079
|
+
...(this.identity.local ? { toolAllowlist: exports.LOCAL_AUDIO_TOOL_ALLOWLIST } : {}),
|
|
1987
2080
|
});
|
|
1988
2081
|
this.toolContext = { ...context, toolSelection: Object.freeze({ ordinary: Object.freeze([]), engine: Object.freeze([]) }) };
|
|
1989
2082
|
return context;
|
|
@@ -2041,6 +2134,8 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
2041
2134
|
const name = stringField(mark?.name);
|
|
2042
2135
|
if (!name)
|
|
2043
2136
|
return;
|
|
2137
|
+
if (this.disclosure?.noteMark(name))
|
|
2138
|
+
return;
|
|
2044
2139
|
const playback = this.playbackMarks.get(name);
|
|
2045
2140
|
if (!playback)
|
|
2046
2141
|
return;
|
|
@@ -2060,6 +2155,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
2060
2155
|
return;
|
|
2061
2156
|
}
|
|
2062
2157
|
const type = typeof event.type === "string" ? event.type : "";
|
|
2158
|
+
this.disclosure?.handleRealtimeEvent(event);
|
|
2063
2159
|
if (type === "response.created") {
|
|
2064
2160
|
this.noteRealtimeResponseCreated(event);
|
|
2065
2161
|
return;
|
|
@@ -2518,6 +2614,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
2518
2614
|
}
|
|
2519
2615
|
sendRealtimeResponseCreate(request) {
|
|
2520
2616
|
this.realtimeResponseCreateInFlight = request;
|
|
2617
|
+
this.disclosure?.noteSent(request.response);
|
|
2521
2618
|
this.sendOpenAI({
|
|
2522
2619
|
type: "response.create",
|
|
2523
2620
|
...(request.response ? { response: request.response } : {}),
|
|
@@ -2638,6 +2735,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
2638
2735
|
sendTwilioClear() {
|
|
2639
2736
|
if (this.closed || !this.streamSid || !this.socket.isOpen())
|
|
2640
2737
|
return;
|
|
2738
|
+
this.disclosure?.noteCleared();
|
|
2641
2739
|
this.socket.send(JSON.stringify({ event: "clear", streamSid: this.streamSid }));
|
|
2642
2740
|
}
|
|
2643
2741
|
appendTranscript(role, text) {
|
|
@@ -2674,6 +2772,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
|
|
|
2674
2772
|
if (this.closed)
|
|
2675
2773
|
return;
|
|
2676
2774
|
this.closed = true;
|
|
2775
|
+
this.disclosure?.stop();
|
|
2677
2776
|
if (this.openaiWs && (this.openaiWs.readyState === ws_1.WebSocket.OPEN || this.openaiWs.readyState === ws_1.WebSocket.CONNECTING)) {
|
|
2678
2777
|
this.openaiWs.close();
|
|
2679
2778
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.889",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.889",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|