@ouro.bot/cli 0.1.0-alpha.891 → 0.1.0-alpha.893

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.
@@ -137,6 +137,23 @@ class LocalAudioDeviceTransport {
137
137
  async leave(reason = "left") {
138
138
  await this.finish(reason);
139
139
  }
140
+ /** Live playback goes through ffmpeg, which names devices by index: find BlackHole 2ch's index now. */
141
+ async resolveLivePlayback() {
142
+ const command = this.deps.ffmpegPath ?? "ffmpeg";
143
+ const list = this.deps.listAudioToolboxDevices ?? (() => (0, local_audio_devices_1.defaultListAudioToolboxDevices)(command));
144
+ let listing;
145
+ try {
146
+ listing = await list();
147
+ }
148
+ catch (error) {
149
+ throw new Error(`Local audio routing is not ready:\n- Live playback needs ffmpeg (${errorText(error)}): brew install ffmpeg`);
150
+ }
151
+ const index = (0, local_audio_devices_1.findAudioToolboxDeviceIndex)(listing, local_audio_devices_1.PLAYBACK_DEVICE_NAME);
152
+ if (index === undefined) {
153
+ throw new Error(`Local audio routing is not ready:\n- ffmpeg does not list the playback device "${local_audio_devices_1.PLAYBACK_DEVICE_NAME}". Install it with brew install --cask blackhole-2ch, then restart macOS audio or reboot.`);
154
+ }
155
+ return { command, args: (0, local_audio_devices_1.ffmpegPlaybackArgs)(index) };
156
+ }
140
157
  async startUnchecked() {
141
158
  const { request, deps } = this;
142
159
  if (!deps.bridgeOptions.openaiRealtime?.apiKey?.trim()) {
@@ -144,10 +161,14 @@ class LocalAudioDeviceTransport {
144
161
  }
145
162
  const files = request.files;
146
163
  const soxPath = deps.soxPath;
164
+ const livePlayback = files ? undefined : await this.resolveLivePlayback();
165
+ if (this.finished)
166
+ return;
147
167
  this.devices = new local_audio_devices_1.LocalAudioDevices({
148
168
  spawner: deps.spawner,
149
169
  captureArgs: files ? (0, local_audio_devices_1.fileCaptureArgs)(files.inputPath) : (0, local_audio_devices_1.soxCaptureArgs)(local_audio_devices_1.CAPTURE_DEVICE_NAME),
150
- playbackArgs: files ? (0, local_audio_devices_1.filePlaybackArgs)(files.outputPath) : (0, local_audio_devices_1.soxPlaybackArgs)(local_audio_devices_1.PLAYBACK_DEVICE_NAME),
170
+ playbackArgs: livePlayback ? livePlayback.args : (0, local_audio_devices_1.filePlaybackArgs)(files.outputPath),
171
+ ...(livePlayback ? { playbackCommand: livePlayback.command } : {}),
151
172
  pidFile: deps.pidFile,
152
173
  soxPath,
153
174
  killGroup: deps.killGroup,
@@ -417,13 +417,19 @@ function isSafeVoiceFriendId(id) {
417
417
  * (the store's display-name fallback is never used for voice). Everything else resolves from the
418
418
  * phone number, or from the call itself when there is none.
419
419
  */
420
+ function friendHasPhone(friend, phone) {
421
+ return friend.externalIds.some((externalId) => (0, phone_1.normalizeTwilioE164PhoneNumber)(externalId.externalId) === phone);
422
+ }
420
423
  async function resolveVoiceFriendContext(options, input) {
421
424
  const agentRoot = resolveTwilioPhoneAgentRoot(options);
422
425
  const friendStore = new friends_1.FileFriendStore(path.join(agentRoot, "friends"));
423
426
  const explicitFriendId = input.friendId?.trim();
427
+ const remotePhone = (0, phone_1.normalizeTwilioE164PhoneNumber)(input.remotePhone);
424
428
  if (explicitFriendId && isSafeVoiceFriendId(explicitFriendId)) {
425
429
  const existing = await friendStore.get(explicitFriendId);
426
- if (existing && existing.id === explicitFriendId) {
430
+ // An outbound job names a friend, but the person who answers is whoever has the dialed number:
431
+ // a number that is not on the friend's record gets resolved like any other caller.
432
+ if (existing && existing.id === explicitFriendId && (!remotePhone || friendHasPhone(existing, remotePhone))) {
427
433
  return {
428
434
  friendId: existing.id,
429
435
  friendStore,
@@ -431,7 +437,6 @@ async function resolveVoiceFriendContext(options, input) {
431
437
  };
432
438
  }
433
439
  }
434
- const remotePhone = (0, phone_1.normalizeTwilioE164PhoneNumber)(input.remotePhone);
435
440
  const provider = remotePhone ? "imessage-handle" : "local";
436
441
  const externalId = remotePhone || friendIdFromCaller("", input.callSid);
437
442
  const resolver = new friends_1.FriendResolver(friendStore, {
@@ -481,6 +486,14 @@ function twilioPhoneVoiceSessionKey(options) {
481
486
  return `twilio-phone-line-${lineSegment}`;
482
487
  return `twilio-phone-${safeSegment(options.callSid ?? "incoming")}`;
483
488
  }
489
+ /**
490
+ * A Realtime `response.create` that carries `instructions` replaces the session instructions for
491
+ * that response, so a greeting sent that way knows nothing about who the agent is. Without this
492
+ * line the model introduced itself as "ChatGPT" in 3 of 6 live calls.
493
+ */
494
+ function realtimeGreetingInstructions(agentName, prompt) {
495
+ return `You are ${agentName}. If you introduce yourself, use that name; never call yourself ChatGPT or an OpenAI assistant.\n${prompt}`;
496
+ }
484
497
  function callConnectedPrompt(params) {
485
498
  const from = params.From?.trim();
486
499
  const to = params.To?.trim();
@@ -2098,7 +2111,7 @@ class TwilioOpenAIRealtimeMediaStreamSession {
2098
2111
  createdAt: new Date().toISOString(),
2099
2112
  }, { From: this.to, To: this.from })
2100
2113
  : callConnectedPrompt({ From: this.from, To: this.to });
2101
- this.requestRealtimeResponse({ instructions: promptText });
2114
+ this.requestRealtimeResponse({ instructions: realtimeGreetingInstructions(this.options.agentName, promptText) });
2102
2115
  }
2103
2116
  handleMedia(media) {
2104
2117
  const payload = stringField(media?.payload);
@@ -3350,7 +3363,7 @@ class OpenAISipPhoneSession {
3350
3363
  return;
3351
3364
  this.initialGreetingSent = true;
3352
3365
  this.requestRealtimeResponse({
3353
- instructions: openAISipCallConnectedPrompt(this.metadata, this.options.openaiRealtime?.voiceStyle),
3366
+ instructions: realtimeGreetingInstructions(this.options.agentName, openAISipCallConnectedPrompt(this.metadata, this.options.openaiRealtime?.voiceStyle)),
3354
3367
  });
3355
3368
  }
3356
3369
  handleOpenAIMessage(raw) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.891",
3
+ "version": "0.1.0-alpha.893",
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.891",
9
+ "version": "0.1.0-alpha.893",
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.891",
3
+ "version": "0.1.0-alpha.893",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },