open-agents-ai 0.93.1 → 0.95.0

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.
Files changed (2) hide show
  1. package/dist/index.js +327 -12
  2. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -32523,13 +32523,66 @@ var init_voice = __esm({
32523
32523
  id: "glados",
32524
32524
  label: "GLaDOS",
32525
32525
  onnxUrl: "https://raw.githubusercontent.com/robit-man/EGG/main/voice/glados_piper_medium.onnx",
32526
- configUrl: "https://raw.githubusercontent.com/robit-man/EGG/main/voice/glados_piper_medium.onnx.json"
32526
+ configUrl: "https://raw.githubusercontent.com/robit-man/EGG/main/voice/glados_piper_medium.onnx.json",
32527
+ backend: "onnx"
32527
32528
  },
32528
32529
  overwatch: {
32529
32530
  id: "overwatch",
32530
32531
  label: "Overwatch",
32531
32532
  onnxUrl: "https://raw.githubusercontent.com/robit-man/combine_overwatch_onnx/main/overwatch.onnx",
32532
- configUrl: "https://raw.githubusercontent.com/robit-man/combine_overwatch_onnx/main/overwatch.onnx.json"
32533
+ configUrl: "https://raw.githubusercontent.com/robit-man/combine_overwatch_onnx/main/overwatch.onnx.json",
32534
+ backend: "onnx"
32535
+ },
32536
+ // MLX Audio models — macOS Apple Silicon only
32537
+ kokoro: {
32538
+ id: "kokoro",
32539
+ label: "Kokoro (MLX)",
32540
+ onnxUrl: "",
32541
+ configUrl: "",
32542
+ backend: "mlx",
32543
+ mlxModelId: "mlx-community/Kokoro-82M-bf16",
32544
+ mlxVoice: "af_heart",
32545
+ mlxLangCode: "a"
32546
+ },
32547
+ "kokoro:af_heart": {
32548
+ id: "kokoro:af_heart",
32549
+ label: "Kokoro Heart (MLX)",
32550
+ onnxUrl: "",
32551
+ configUrl: "",
32552
+ backend: "mlx",
32553
+ mlxModelId: "mlx-community/Kokoro-82M-bf16",
32554
+ mlxVoice: "af_heart",
32555
+ mlxLangCode: "a"
32556
+ },
32557
+ "kokoro:af_bella": {
32558
+ id: "kokoro:af_bella",
32559
+ label: "Kokoro Bella (MLX)",
32560
+ onnxUrl: "",
32561
+ configUrl: "",
32562
+ backend: "mlx",
32563
+ mlxModelId: "mlx-community/Kokoro-82M-bf16",
32564
+ mlxVoice: "af_bella",
32565
+ mlxLangCode: "a"
32566
+ },
32567
+ "kokoro:am_adam": {
32568
+ id: "kokoro:am_adam",
32569
+ label: "Kokoro Adam (MLX)",
32570
+ onnxUrl: "",
32571
+ configUrl: "",
32572
+ backend: "mlx",
32573
+ mlxModelId: "mlx-community/Kokoro-82M-bf16",
32574
+ mlxVoice: "am_adam",
32575
+ mlxLangCode: "a"
32576
+ },
32577
+ "kokoro:bf_emma": {
32578
+ id: "kokoro:bf_emma",
32579
+ label: "Kokoro Emma (MLX)",
32580
+ onnxUrl: "",
32581
+ configUrl: "",
32582
+ backend: "mlx",
32583
+ mlxModelId: "mlx-community/Kokoro-82M-bf16",
32584
+ mlxVoice: "bf_emma",
32585
+ mlxLangCode: "b"
32533
32586
  }
32534
32587
  };
32535
32588
  VoiceEngine = class {
@@ -32551,6 +32604,12 @@ var init_voice = __esm({
32551
32604
  speakQueue = [];
32552
32605
  speaking = false;
32553
32606
  phonemizeFn = null;
32607
+ /** True when current model uses MLX Audio backend */
32608
+ mlxActive = false;
32609
+ /** Cached: is mlx-audio pip package installed? */
32610
+ mlxInstalled = null;
32611
+ /** Path to python3 binary (resolved once) */
32612
+ python3Path = null;
32554
32613
  // -------------------------------------------------------------------------
32555
32614
  // Public API
32556
32615
  // -------------------------------------------------------------------------
@@ -32560,13 +32619,21 @@ var init_voice = __esm({
32560
32619
  this.killPlayback();
32561
32620
  return "Voice feedback disabled.";
32562
32621
  }
32622
+ const model = VOICE_MODELS[this.modelId];
32623
+ const isMlx = model?.backend === "mlx";
32563
32624
  try {
32564
- await this.ensureRuntime();
32565
- await this.ensureModel(this.modelId);
32566
- await this.loadSession();
32625
+ if (isMlx) {
32626
+ await this.ensureMlxAudio();
32627
+ this.mlxActive = true;
32628
+ } else {
32629
+ await this.ensureRuntime();
32630
+ await this.ensureModel(this.modelId);
32631
+ await this.loadSession();
32632
+ this.mlxActive = false;
32633
+ }
32567
32634
  this.enabled = true;
32568
32635
  this.ready = true;
32569
- return `Voice feedback enabled (${VOICE_MODELS[this.modelId]?.label ?? this.modelId}).`;
32636
+ return `Voice feedback enabled (${model?.label ?? this.modelId}).`;
32570
32637
  } catch (err) {
32571
32638
  return `Failed to enable voice: ${err instanceof Error ? err.message : String(err)}`;
32572
32639
  }
@@ -32574,24 +32641,36 @@ var init_voice = __esm({
32574
32641
  async setModel(id) {
32575
32642
  const key = id.toLowerCase();
32576
32643
  if (!VOICE_MODELS[key]) {
32577
- return `Unknown voice model: "${id}". Available: ${Object.keys(VOICE_MODELS).join(", ")}`;
32578
- }
32644
+ const onnxModels = Object.keys(VOICE_MODELS).filter((k) => VOICE_MODELS[k].backend !== "mlx");
32645
+ const mlxModels = Object.keys(VOICE_MODELS).filter((k) => VOICE_MODELS[k].backend === "mlx");
32646
+ return `Unknown voice model: "${id}". Available:
32647
+ ONNX: ${onnxModels.join(", ")}
32648
+ MLX (macOS): ${mlxModels.join(", ")}`;
32649
+ }
32650
+ const model = VOICE_MODELS[key];
32651
+ const isMlx = model.backend === "mlx";
32579
32652
  this.modelId = key;
32580
32653
  this.session = null;
32581
32654
  this.config = null;
32582
32655
  this.ready = false;
32583
32656
  if (this.enabled) {
32584
32657
  try {
32585
- await this.ensureModel(key);
32586
- await this.loadSession();
32658
+ if (isMlx) {
32659
+ await this.ensureMlxAudio();
32660
+ this.mlxActive = true;
32661
+ } else {
32662
+ await this.ensureModel(key);
32663
+ await this.loadSession();
32664
+ this.mlxActive = false;
32665
+ }
32587
32666
  this.ready = true;
32588
- return `Switched to ${VOICE_MODELS[key].label} voice.`;
32667
+ return `Switched to ${model.label} voice.`;
32589
32668
  } catch (err) {
32590
32669
  this.enabled = false;
32591
32670
  return `Failed to load ${key}: ${err instanceof Error ? err.message : String(err)}`;
32592
32671
  }
32593
32672
  }
32594
- return `Voice model set to ${VOICE_MODELS[key].label}. Enable with /voice.`;
32673
+ return `Voice model set to ${model.label}. Enable with /voice.`;
32595
32674
  }
32596
32675
  /**
32597
32676
  * Speak text asynchronously (non-blocking) at full volume.
@@ -32636,6 +32715,9 @@ var init_voice = __esm({
32636
32715
  * Used for sending TTS audio to Telegram and WebSocket clients.
32637
32716
  */
32638
32717
  async synthesizeToBuffer(text) {
32718
+ if (this.mlxActive) {
32719
+ return this.synthesizeMlxToBuffer(text);
32720
+ }
32639
32721
  if (!this.session || !this.config || !this.ort)
32640
32722
  return null;
32641
32723
  const cleaned = text.replace(/\*/g, "");
@@ -32668,6 +32750,14 @@ var init_voice = __esm({
32668
32750
  * Used for streaming to WebSocket clients.
32669
32751
  */
32670
32752
  async synthesizeToPCM(text) {
32753
+ if (this.mlxActive) {
32754
+ const wav = await this.synthesizeMlxToBuffer(text);
32755
+ if (!wav || wav.length <= 44)
32756
+ return null;
32757
+ const sampleRate = wav.readUInt32LE(24);
32758
+ const pcm = Buffer.from(wav.buffer, wav.byteOffset + 44, wav.length - 44);
32759
+ return { pcm, sampleRate };
32760
+ }
32671
32761
  if (!this.session || !this.config || !this.ort)
32672
32762
  return null;
32673
32763
  const cleaned = text.replace(/\*/g, "");
@@ -32705,6 +32795,8 @@ var init_voice = __esm({
32705
32795
  }
32706
32796
  /** Get the current model's sample rate */
32707
32797
  get sampleRate() {
32798
+ if (this.mlxActive)
32799
+ return 24e3;
32708
32800
  return this.config?.audio?.sample_rate ?? 22050;
32709
32801
  }
32710
32802
  dispose() {
@@ -32713,6 +32805,7 @@ var init_voice = __esm({
32713
32805
  this.session = null;
32714
32806
  this.ort = null;
32715
32807
  this.config = null;
32808
+ this.mlxActive = false;
32716
32809
  }
32717
32810
  // -------------------------------------------------------------------------
32718
32811
  // Text chunking for long TTS input
@@ -32775,6 +32868,10 @@ var init_voice = __esm({
32775
32868
  // Synthesis pipeline
32776
32869
  // -------------------------------------------------------------------------
32777
32870
  async synthesizeAndPlay(text, volume = 1, pitchFactor = 1) {
32871
+ if (this.mlxActive) {
32872
+ await this.synthesizeWithMlx(text, volume, pitchFactor);
32873
+ return;
32874
+ }
32778
32875
  if (!this.session || !this.config || !this.ort)
32779
32876
  return;
32780
32877
  const cleaned = text.replace(/\*/g, "");
@@ -33021,6 +33118,196 @@ var init_voice = __esm({
33021
33118
  }
33022
33119
  }
33023
33120
  // -------------------------------------------------------------------------
33121
+ // MLX Audio backend (macOS Apple Silicon)
33122
+ // -------------------------------------------------------------------------
33123
+ /** Check if we're on macOS Apple Silicon */
33124
+ isMlxCapable() {
33125
+ return platform3() === "darwin" && process.arch === "arm64";
33126
+ }
33127
+ /** Resolve python3 binary path */
33128
+ findPython3() {
33129
+ if (this.python3Path)
33130
+ return this.python3Path;
33131
+ for (const bin of ["python3", "python"]) {
33132
+ try {
33133
+ const path = execSync27(`which ${bin}`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
33134
+ if (path) {
33135
+ this.python3Path = path;
33136
+ return path;
33137
+ }
33138
+ } catch {
33139
+ }
33140
+ }
33141
+ return null;
33142
+ }
33143
+ /** Check if mlx-audio is installed */
33144
+ checkMlxInstalled() {
33145
+ if (this.mlxInstalled !== null)
33146
+ return this.mlxInstalled;
33147
+ const py = this.findPython3();
33148
+ if (!py) {
33149
+ this.mlxInstalled = false;
33150
+ return false;
33151
+ }
33152
+ try {
33153
+ execSync27(`${py} -c "import mlx_audio"`, { stdio: "pipe", timeout: 1e4 });
33154
+ this.mlxInstalled = true;
33155
+ return true;
33156
+ } catch {
33157
+ this.mlxInstalled = false;
33158
+ return false;
33159
+ }
33160
+ }
33161
+ /**
33162
+ * Ensure mlx-audio is installed. Auto-installs via pip on first use.
33163
+ * Only works on macOS Apple Silicon.
33164
+ */
33165
+ async ensureMlxAudio() {
33166
+ if (!this.isMlxCapable()) {
33167
+ throw new Error("MLX Audio requires macOS on Apple Silicon (M1/M2/M3/M4). Use an ONNX voice model instead: glados, overwatch");
33168
+ }
33169
+ const py = this.findPython3();
33170
+ if (!py) {
33171
+ throw new Error("python3 not found. Install Python 3.10+ to use MLX Audio voices. Try: brew install python@3.12");
33172
+ }
33173
+ if (this.checkMlxInstalled())
33174
+ return;
33175
+ renderInfo("Installing MLX Audio for voice synthesis (first time setup)...");
33176
+ try {
33177
+ execSync27(`${py} -m pip install mlx-audio --quiet`, {
33178
+ stdio: "pipe",
33179
+ timeout: 3e5
33180
+ // 5 min — may need to compile
33181
+ });
33182
+ this.mlxInstalled = true;
33183
+ renderInfo("MLX Audio installed successfully.");
33184
+ } catch (err) {
33185
+ try {
33186
+ execSync27(`${py} -m pip install mlx-audio --user --quiet`, {
33187
+ stdio: "pipe",
33188
+ timeout: 3e5
33189
+ });
33190
+ this.mlxInstalled = true;
33191
+ renderInfo("MLX Audio installed successfully (user site-packages).");
33192
+ } catch (err2) {
33193
+ throw new Error(`Failed to install mlx-audio. Try manually: pip install mlx-audio
33194
+ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
33195
+ }
33196
+ }
33197
+ }
33198
+ /**
33199
+ * Synthesize and play audio using MLX Audio CLI.
33200
+ * Generates a WAV file via `python3 -m mlx_audio.tts.generate`, then plays it.
33201
+ */
33202
+ async synthesizeWithMlx(text, volume = 1, _pitchFactor = 1) {
33203
+ const py = this.findPython3();
33204
+ if (!py)
33205
+ return;
33206
+ const cleaned = text.replace(/\*/g, "").trim();
33207
+ if (!cleaned)
33208
+ return;
33209
+ const model = VOICE_MODELS[this.modelId];
33210
+ if (!model)
33211
+ return;
33212
+ const mlxModelId = model.mlxModelId ?? "mlx-community/Kokoro-82M-bf16";
33213
+ const mlxVoice = model.mlxVoice ?? "af_heart";
33214
+ const mlxLangCode = model.mlxLangCode ?? "a";
33215
+ const wavPath = join41(tmpdir6(), `oa-mlx-${Date.now()}.wav`);
33216
+ const pyScript = [
33217
+ "import sys, json",
33218
+ "from mlx_audio.tts import generate as tts_gen",
33219
+ "text = json.loads(sys.argv[1])",
33220
+ `tts_gen.main(["--model", ${JSON.stringify(mlxModelId)}, "--text", text, "--voice", ${JSON.stringify(mlxVoice)}, "--lang_code", ${JSON.stringify(mlxLangCode)}, "--audio_path", ${JSON.stringify(wavPath)}])`
33221
+ ].join("; ");
33222
+ try {
33223
+ execSync27(`${py} -c ${JSON.stringify(pyScript)} ${JSON.stringify(JSON.stringify(cleaned))}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
33224
+ } catch (err) {
33225
+ try {
33226
+ const safeText = cleaned.replace(/'/g, "'\\''");
33227
+ execSync27(`${py} -m mlx_audio.tts.generate --model ${mlxModelId} --text '${safeText}' --voice ${mlxVoice} --lang_code ${mlxLangCode} --audio_path ${JSON.stringify(wavPath)}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
33228
+ } catch (err2) {
33229
+ return;
33230
+ }
33231
+ }
33232
+ if (!existsSync31(wavPath))
33233
+ return;
33234
+ if (volume !== 1) {
33235
+ try {
33236
+ const wavData = readFileSync22(wavPath);
33237
+ if (wavData.length > 44) {
33238
+ const header = wavData.subarray(0, 44);
33239
+ const samples = new Int16Array(wavData.buffer, wavData.byteOffset + 44, (wavData.length - 44) / 2);
33240
+ for (let i = 0; i < samples.length; i++) {
33241
+ samples[i] = Math.round(samples[i] * volume);
33242
+ }
33243
+ const scaled = Buffer.concat([header, Buffer.from(samples.buffer, samples.byteOffset, samples.byteLength)]);
33244
+ writeFileSync11(wavPath, scaled);
33245
+ }
33246
+ } catch {
33247
+ }
33248
+ }
33249
+ if (this.onPCMOutput) {
33250
+ try {
33251
+ const wavData = readFileSync22(wavPath);
33252
+ if (wavData.length > 44) {
33253
+ const pcm = Buffer.from(wavData.buffer, wavData.byteOffset + 44, wavData.length - 44);
33254
+ const sampleRate = wavData.readUInt32LE(24);
33255
+ this.onPCMOutput(pcm, sampleRate);
33256
+ }
33257
+ } catch {
33258
+ }
33259
+ }
33260
+ await this.playWav(wavPath);
33261
+ try {
33262
+ unlinkSync4(wavPath);
33263
+ } catch {
33264
+ }
33265
+ }
33266
+ /**
33267
+ * Synthesize text to a WAV buffer using MLX Audio (no playback).
33268
+ * Used by synthesizeToBuffer / synthesizeToPCM for MLX models.
33269
+ */
33270
+ async synthesizeMlxToBuffer(text) {
33271
+ const py = this.findPython3();
33272
+ if (!py)
33273
+ return null;
33274
+ const cleaned = text.replace(/\*/g, "").trim();
33275
+ if (!cleaned)
33276
+ return null;
33277
+ const model = VOICE_MODELS[this.modelId];
33278
+ if (!model)
33279
+ return null;
33280
+ const mlxModelId = model.mlxModelId ?? "mlx-community/Kokoro-82M-bf16";
33281
+ const mlxVoice = model.mlxVoice ?? "af_heart";
33282
+ const mlxLangCode = model.mlxLangCode ?? "a";
33283
+ const wavPath = join41(tmpdir6(), `oa-mlx-buf-${Date.now()}.wav`);
33284
+ const pyScript = [
33285
+ "import sys, json",
33286
+ "from mlx_audio.tts import generate as tts_gen",
33287
+ "text = json.loads(sys.argv[1])",
33288
+ `tts_gen.main(["--model", ${JSON.stringify(mlxModelId)}, "--text", text, "--voice", ${JSON.stringify(mlxVoice)}, "--lang_code", ${JSON.stringify(mlxLangCode)}, "--audio_path", ${JSON.stringify(wavPath)}])`
33289
+ ].join("; ");
33290
+ try {
33291
+ execSync27(`${py} -c ${JSON.stringify(pyScript)} ${JSON.stringify(JSON.stringify(cleaned))}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
33292
+ } catch {
33293
+ try {
33294
+ const safeText = cleaned.replace(/'/g, "'\\''");
33295
+ execSync27(`${py} -m mlx_audio.tts.generate --model ${mlxModelId} --text '${safeText}' --voice ${mlxVoice} --lang_code ${mlxLangCode} --audio_path ${JSON.stringify(wavPath)}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
33296
+ } catch {
33297
+ return null;
33298
+ }
33299
+ }
33300
+ if (!existsSync31(wavPath))
33301
+ return null;
33302
+ try {
33303
+ const data = readFileSync22(wavPath);
33304
+ unlinkSync4(wavPath);
33305
+ return data;
33306
+ } catch {
33307
+ return null;
33308
+ }
33309
+ }
33310
+ // -------------------------------------------------------------------------
33024
33311
  // Setup: ONNX runtime installation
33025
33312
  // -------------------------------------------------------------------------
33026
33313
  async ensureRuntime() {
@@ -38983,6 +39270,8 @@ var init_status_bar = __esm({
38983
39270
  _currentFooterHeight = 5;
38984
39271
  /** Timestamp when streaming started (for token rate calculation) */
38985
39272
  _streamStartTime = 0;
39273
+ /** Current package version (shown in metrics row, rightmost) */
39274
+ _version = "";
38986
39275
  /**
38987
39276
  * Provide a callback that returns readline's current input state.
38988
39277
  * StatusBar uses this to render typed text and position the cursor
@@ -39084,6 +39373,12 @@ var init_status_bar = __esm({
39084
39373
  setContextWindowSize(size) {
39085
39374
  this.metrics.contextWindowSize = size;
39086
39375
  }
39376
+ /** Set the current package version for display in the metrics row */
39377
+ setVersion(version) {
39378
+ this._version = version;
39379
+ if (this.active)
39380
+ this.renderFooterPreserveCursor();
39381
+ }
39087
39382
  /** Human expert speed ratio tracker */
39088
39383
  _speedTracker = new HumanSpeedTracker();
39089
39384
  /** Record a tool call for speed ratio tracking */
@@ -39530,6 +39825,12 @@ var init_status_bar = __esm({
39530
39825
  const recW = 1 + 4 + (this._countdown > 0 ? 1 + `${this._countdown}s`.length : 0);
39531
39826
  sections.push({ expanded: recStr, compact: recStr, expandedW: recW, compactW: recW, empty: false });
39532
39827
  }
39828
+ if (this._version) {
39829
+ const vExpanded = pastel2(245, "v" + this._version);
39830
+ const vCompact = pastel2(245, "v" + this._version);
39831
+ const vW = 1 + this._version.length;
39832
+ sections.push({ expanded: vExpanded, compact: vCompact, expandedW: vW, compactW: vW, empty: false });
39833
+ }
39533
39834
  const activeIndices = sections.map((s, i) => !s.empty ? i : -1).filter((i) => i >= 0);
39534
39835
  const isCompact = new Array(sections.length).fill(false);
39535
39836
  const calcTotalWidth = () => {
@@ -40648,6 +40949,7 @@ async function startInteractive(config, repoPath) {
40648
40949
  });
40649
40950
  }
40650
40951
  const statusBar = new StatusBar();
40952
+ statusBar.setVersion(version);
40651
40953
  if (process.stdout.isTTY) {
40652
40954
  const scrollTop = carouselLines > 0 ? carouselLines + 1 : 1;
40653
40955
  statusBar.activate(scrollTop);
@@ -40738,6 +41040,19 @@ async function startInteractive(config, repoPath) {
40738
41040
  }).catch(() => {
40739
41041
  });
40740
41042
  }
41043
+ const AUTO_UPDATE_INTERVAL_MS = 30 * 60 * 1e3;
41044
+ const autoUpdateTimer = setInterval(() => {
41045
+ const updateMode = savedSettings.updateMode ?? "auto";
41046
+ if (updateMode === "manual")
41047
+ return;
41048
+ checkForUpdate(version).then((updateInfo) => {
41049
+ if (updateInfo) {
41050
+ performSilentUpdate();
41051
+ }
41052
+ }).catch(() => {
41053
+ });
41054
+ }, AUTO_UPDATE_INTERVAL_MS);
41055
+ autoUpdateTimer.unref();
40741
41056
  const voiceEngine = new VoiceEngine();
40742
41057
  let voiceSession = null;
40743
41058
  let exposeGateway = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.93.1",
3
+ "version": "0.95.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,10 @@
56
56
  "micropayments",
57
57
  "factory-ai",
58
58
  "droid",
59
- "code-delegation"
59
+ "code-delegation",
60
+ "mlx-audio",
61
+ "apple-silicon",
62
+ "kokoro-tts"
60
63
  ],
61
64
  "author": "robit-man",
62
65
  "license": "MIT",