@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.
@@ -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
- return Object.freeze({ ordinary: selected.ordinary, engine: Object.freeze([]) });
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 voiceContext = await resolveVoiceFriendContext(this.options, {
1775
- friendId: this.direction === "outbound" ? this.identity.friendId : undefined,
1776
- remotePhone: this.from || undefined,
1777
- callSid: this.callSid,
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 = twilioPhoneVoiceSessionKey({
1783
- defaultFriendId: this.friendId,
1784
- from: this.from,
1785
- to: this.to,
1786
- callSid: this.callSid,
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.sendInitialGreeting();
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: (request) => this.playPreparedAudio(request),
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
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.888",
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.888",
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.888",
3
+ "version": "0.1.0-alpha.889",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },