open-agents-ai 0.4.1 → 0.5.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 (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.js +522 -28
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -160,6 +160,28 @@ The agent follows an iterative fix loop:
160
160
  -V, --version Show version
161
161
  ```
162
162
 
163
+ ### Voice Feedback (TTS)
164
+
165
+ The agent can speak what it's doing using neural TTS voices. Enable it in the interactive REPL:
166
+
167
+ ```bash
168
+ /voice # Toggle voice on/off (default: GLaDOS)
169
+ /voice glados # Switch to GLaDOS voice
170
+ /voice overwatch # Switch to Overwatch voice
171
+ ```
172
+
173
+ On first enable, the agent auto-downloads the ONNX voice model (~50MB) and installs `onnxruntime-node` in `~/.open-agents/voice/`. For best quality, install `espeak-ng`:
174
+
175
+ ```bash
176
+ # Ubuntu/Debian
177
+ sudo apt install espeak-ng
178
+
179
+ # macOS
180
+ brew install espeak-ng
181
+ ```
182
+
183
+ When enabled, the agent speaks brief descriptions of each tool call ("Reading auth.ts", "Running tests", "Editing config.js") through your system speakers.
184
+
163
185
  ### Configuration
164
186
 
165
187
  Config priority: CLI flags > environment variables > `~/.open-agents/config.json` > defaults.
package/dist/index.js CHANGED
@@ -6287,6 +6287,8 @@ function renderSlashHelp() {
6287
6287
  ["/endpoint <url> --auth <t>", "Set endpoint with Bearer auth"],
6288
6288
  ["/config", "Show current configuration"],
6289
6289
  ["/update", "Check for updates and auto-install"],
6290
+ ["/voice", "Toggle TTS voice feedback (GLaDOS)"],
6291
+ ["/voice <model>", "Set voice: glados, overwatch"],
6290
6292
  ["/verbose", "Toggle verbose mode"],
6291
6293
  ["/clear", "Clear the screen"],
6292
6294
  ["/help", "Show this help"],
@@ -6519,6 +6521,16 @@ async function handleSlashCommand(input, ctx) {
6519
6521
  case "upgrade":
6520
6522
  await handleUpdate();
6521
6523
  return "handled";
6524
+ case "voice": {
6525
+ if (arg) {
6526
+ const msg = await ctx.voiceSetModel(arg);
6527
+ renderInfo(msg);
6528
+ } else {
6529
+ const msg = await ctx.voiceToggle();
6530
+ renderInfo(msg);
6531
+ }
6532
+ return "handled";
6533
+ }
6522
6534
  default:
6523
6535
  renderWarning(`Unknown command: /${cmd}. Type /help for available commands.`);
6524
6536
  return "handled";
@@ -6631,11 +6643,11 @@ async function handleEndpoint(arg, ctx) {
6631
6643
  async function handleUpdate() {
6632
6644
  let currentVersion = "0.0.0";
6633
6645
  try {
6634
- const { createRequire: createRequire2 } = await import("node:module");
6646
+ const { createRequire: createRequire3 } = await import("node:module");
6635
6647
  const { fileURLToPath: fileURLToPath2 } = await import("node:url");
6636
- const { dirname: dirname4, join: join17 } = await import("node:path");
6637
- const require2 = createRequire2(import.meta.url);
6638
- const pkgPath = join17(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
6648
+ const { dirname: dirname4, join: join18 } = await import("node:path");
6649
+ const require2 = createRequire3(import.meta.url);
6650
+ const pkgPath = join18(dirname4(fileURLToPath2(import.meta.url)), "..", "package.json");
6639
6651
  const pkg = require2(pkgPath);
6640
6652
  currentVersion = pkg.version ?? "0.0.0";
6641
6653
  } catch {
@@ -6995,9 +7007,9 @@ async function doSetup(config, rl) {
6995
7007
  `PARAMETER num_predict 16384`,
6996
7008
  `PARAMETER stop "<|endoftext|>"`
6997
7009
  ].join("\n");
6998
- const modelDir = join12(homedir3(), ".open-agents", "models");
6999
- mkdirSync3(modelDir, { recursive: true });
7000
- const modelfilePath = join12(modelDir, `Modelfile.${customName}`);
7010
+ const modelDir2 = join12(homedir3(), ".open-agents", "models");
7011
+ mkdirSync3(modelDir2, { recursive: true });
7012
+ const modelfilePath = join12(modelDir2, `Modelfile.${customName}`);
7001
7013
  writeFileSync3(modelfilePath, modelfileContent + "\n", "utf8");
7002
7014
  process.stdout.write(` ${c2.dim("Creating model...")} `);
7003
7015
  execSync7(`ollama create ${customName} -f ${modelfilePath}`, {
@@ -7082,6 +7094,472 @@ var init_setup = __esm({
7082
7094
  }
7083
7095
  });
7084
7096
 
7097
+ // packages/cli/dist/tui/voice.js
7098
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync6, unlinkSync } from "node:fs";
7099
+ import { join as join13 } from "node:path";
7100
+ import { homedir as homedir4, tmpdir, platform } from "node:os";
7101
+ import { execSync as execSync8, spawn as nodeSpawn } from "node:child_process";
7102
+ import { createRequire } from "node:module";
7103
+ function modelDir(id) {
7104
+ return join13(MODELS_DIR, id);
7105
+ }
7106
+ function modelOnnxPath(id) {
7107
+ return join13(modelDir(id), "model.onnx");
7108
+ }
7109
+ function modelConfigPath(id) {
7110
+ return join13(modelDir(id), "config.json");
7111
+ }
7112
+ function describeToolCall(toolName, args) {
7113
+ const path = args["path"];
7114
+ const file = path ? path.split("/").pop() ?? path : "";
7115
+ switch (toolName) {
7116
+ case "file_read":
7117
+ return `Reading ${file}`;
7118
+ case "file_write":
7119
+ return `Writing ${file}`;
7120
+ case "file_edit":
7121
+ return `Editing ${file}`;
7122
+ case "shell": {
7123
+ const cmd = String(args["command"] ?? "");
7124
+ if (/npm\s+test|vitest|jest|mocha/.test(cmd))
7125
+ return "Running tests";
7126
+ if (/npm\s+run\s+build|tsc|esbuild/.test(cmd))
7127
+ return "Building project";
7128
+ if (/npm\s+install|pnpm\s+install/.test(cmd))
7129
+ return "Installing dependencies";
7130
+ if (/git\s+/.test(cmd))
7131
+ return "Running git command";
7132
+ if (/npm\s+run\s+lint|eslint|biome/.test(cmd))
7133
+ return "Running linter";
7134
+ if (cmd.length > 40)
7135
+ return "Running shell command";
7136
+ return `Running ${cmd.slice(0, 30)}`;
7137
+ }
7138
+ case "grep_search":
7139
+ return `Searching for ${args["pattern"] ?? "pattern"}`;
7140
+ case "find_files":
7141
+ return `Finding files matching ${args["pattern"] ?? "pattern"}`;
7142
+ case "list_directory":
7143
+ return `Listing directory ${file || "contents"}`;
7144
+ case "web_search":
7145
+ return `Searching the web`;
7146
+ case "web_fetch":
7147
+ return `Fetching web page`;
7148
+ case "memory_read":
7149
+ return `Reading from memory`;
7150
+ case "memory_write":
7151
+ return `Saving to memory`;
7152
+ case "task_complete":
7153
+ return String(args["summary"] ?? "Task complete");
7154
+ case "batch_edit":
7155
+ return `Editing multiple files`;
7156
+ case "codebase_map":
7157
+ return `Mapping project structure`;
7158
+ case "diagnostic":
7159
+ return `Running diagnostics`;
7160
+ case "git_info":
7161
+ return `Checking git status`;
7162
+ case "aiwg_setup":
7163
+ return `Setting up development framework`;
7164
+ case "aiwg_health":
7165
+ return `Analyzing project health`;
7166
+ case "aiwg_workflow":
7167
+ return `Running workflow command`;
7168
+ default:
7169
+ return `Using ${toolName}`;
7170
+ }
7171
+ }
7172
+ function describeToolResult(toolName, success) {
7173
+ if (toolName === "task_complete")
7174
+ return "";
7175
+ return success ? "Done" : "That failed, trying to fix it";
7176
+ }
7177
+ function formatBytes2(bytes) {
7178
+ if (bytes < 1024)
7179
+ return `${bytes}B`;
7180
+ if (bytes < 1024 * 1024)
7181
+ return `${(bytes / 1024).toFixed(0)}KB`;
7182
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
7183
+ }
7184
+ var VOICE_MODELS, VOICE_DIR, MODELS_DIR, VoiceEngine;
7185
+ var init_voice = __esm({
7186
+ "packages/cli/dist/tui/voice.js"() {
7187
+ "use strict";
7188
+ init_render();
7189
+ VOICE_MODELS = {
7190
+ glados: {
7191
+ id: "glados",
7192
+ label: "GLaDOS",
7193
+ onnxUrl: "https://raw.githubusercontent.com/robit-man/EGG/main/voice/glados_piper_medium.onnx",
7194
+ configUrl: "https://raw.githubusercontent.com/robit-man/EGG/main/voice/glados_piper_medium.onnx.json"
7195
+ },
7196
+ overwatch: {
7197
+ id: "overwatch",
7198
+ label: "Overwatch",
7199
+ onnxUrl: "https://raw.githubusercontent.com/robit-man/combine_overwatch_onnx/main/overwatch.onnx",
7200
+ configUrl: "https://raw.githubusercontent.com/robit-man/combine_overwatch_onnx/main/overwatch.onnx.json"
7201
+ }
7202
+ };
7203
+ VOICE_DIR = join13(homedir4(), ".open-agents", "voice");
7204
+ MODELS_DIR = join13(VOICE_DIR, "models");
7205
+ VoiceEngine = class {
7206
+ enabled = false;
7207
+ modelId = "glados";
7208
+ ready = false;
7209
+ session = null;
7210
+ // ort.InferenceSession
7211
+ ort = null;
7212
+ // onnxruntime-node module
7213
+ config = null;
7214
+ currentPlayback = null;
7215
+ speakQueue = [];
7216
+ speaking = false;
7217
+ hasEspeak = false;
7218
+ // -------------------------------------------------------------------------
7219
+ // Public API
7220
+ // -------------------------------------------------------------------------
7221
+ async toggle() {
7222
+ if (this.enabled) {
7223
+ this.enabled = false;
7224
+ this.killPlayback();
7225
+ return "Voice feedback disabled.";
7226
+ }
7227
+ try {
7228
+ await this.ensureRuntime();
7229
+ await this.ensureModel(this.modelId);
7230
+ await this.loadSession();
7231
+ this.enabled = true;
7232
+ this.ready = true;
7233
+ return `Voice feedback enabled (${VOICE_MODELS[this.modelId]?.label ?? this.modelId}).`;
7234
+ } catch (err) {
7235
+ return `Failed to enable voice: ${err instanceof Error ? err.message : String(err)}`;
7236
+ }
7237
+ }
7238
+ async setModel(id) {
7239
+ const key = id.toLowerCase();
7240
+ if (!VOICE_MODELS[key]) {
7241
+ return `Unknown voice model: "${id}". Available: ${Object.keys(VOICE_MODELS).join(", ")}`;
7242
+ }
7243
+ this.modelId = key;
7244
+ this.session = null;
7245
+ this.config = null;
7246
+ this.ready = false;
7247
+ if (this.enabled) {
7248
+ try {
7249
+ await this.ensureModel(key);
7250
+ await this.loadSession();
7251
+ this.ready = true;
7252
+ return `Switched to ${VOICE_MODELS[key].label} voice.`;
7253
+ } catch (err) {
7254
+ this.enabled = false;
7255
+ return `Failed to load ${key}: ${err instanceof Error ? err.message : String(err)}`;
7256
+ }
7257
+ }
7258
+ return `Voice model set to ${VOICE_MODELS[key].label}. Enable with /voice.`;
7259
+ }
7260
+ /**
7261
+ * Speak text asynchronously (non-blocking).
7262
+ * Queues if already speaking; drops if queue gets too deep.
7263
+ */
7264
+ speak(text) {
7265
+ if (!this.enabled || !this.ready)
7266
+ return;
7267
+ if (this.speakQueue.length >= 3) {
7268
+ this.speakQueue.length = 0;
7269
+ }
7270
+ this.speakQueue.push(text);
7271
+ if (!this.speaking) {
7272
+ this.drainQueue().catch(() => {
7273
+ });
7274
+ }
7275
+ }
7276
+ dispose() {
7277
+ this.enabled = false;
7278
+ this.killPlayback();
7279
+ this.session = null;
7280
+ this.ort = null;
7281
+ this.config = null;
7282
+ }
7283
+ // -------------------------------------------------------------------------
7284
+ // Queue drain
7285
+ // -------------------------------------------------------------------------
7286
+ async drainQueue() {
7287
+ this.speaking = true;
7288
+ while (this.speakQueue.length > 0) {
7289
+ const text = this.speakQueue.pop();
7290
+ this.speakQueue.length = 0;
7291
+ try {
7292
+ await this.synthesizeAndPlay(text);
7293
+ } catch {
7294
+ }
7295
+ }
7296
+ this.speaking = false;
7297
+ }
7298
+ // -------------------------------------------------------------------------
7299
+ // Synthesis pipeline
7300
+ // -------------------------------------------------------------------------
7301
+ async synthesizeAndPlay(text) {
7302
+ if (!this.session || !this.config || !this.ort)
7303
+ return;
7304
+ const phonemeIds = this.textToPhonemeIds(text);
7305
+ if (phonemeIds.length === 0)
7306
+ return;
7307
+ const inputLength = phonemeIds.length;
7308
+ const inputTensor = new this.ort.Tensor("int64", BigInt64Array.from(phonemeIds.map((id) => BigInt(id))), [1, inputLength]);
7309
+ const lengthTensor = new this.ort.Tensor("int64", BigInt64Array.from([BigInt(inputLength)]), [1]);
7310
+ const scalesTensor = new this.ort.Tensor("float32", Float32Array.from([0.667, 1, 0.8]), [3]);
7311
+ const feeds = {
7312
+ input: inputTensor,
7313
+ input_lengths: lengthTensor,
7314
+ scales: scalesTensor
7315
+ };
7316
+ if (this.config.num_speakers > 1) {
7317
+ feeds["sid"] = new this.ort.Tensor("int64", BigInt64Array.from([BigInt(0)]), [1]);
7318
+ }
7319
+ const result = await this.session.run(feeds);
7320
+ const audioData = result["output"].data;
7321
+ if (audioData.length === 0)
7322
+ return;
7323
+ const wavPath = join13(tmpdir(), `oa-voice-${Date.now()}.wav`);
7324
+ this.writeWav(audioData, this.config.audio.sample_rate, wavPath);
7325
+ await this.playWav(wavPath);
7326
+ try {
7327
+ unlinkSync(wavPath);
7328
+ } catch {
7329
+ }
7330
+ }
7331
+ // -------------------------------------------------------------------------
7332
+ // Phonemization
7333
+ // -------------------------------------------------------------------------
7334
+ textToPhonemeIds(text) {
7335
+ const map = this.config.phoneme_id_map;
7336
+ let phonemes;
7337
+ if (this.hasEspeak) {
7338
+ try {
7339
+ const voice = this.config.espeak?.voice ?? "en-us";
7340
+ phonemes = execSync8(`espeak-ng --ipa -q --sep="" -v ${voice} "${text.replace(/"/g, '\\"')}"`, { encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
7341
+ } catch {
7342
+ phonemes = text.toLowerCase();
7343
+ }
7344
+ } else {
7345
+ phonemes = text.toLowerCase();
7346
+ }
7347
+ const ids = [];
7348
+ if (map["^"])
7349
+ ids.push(...map["^"]);
7350
+ for (const char of phonemes) {
7351
+ if (map[char]) {
7352
+ ids.push(...map[char]);
7353
+ if (map["_"])
7354
+ ids.push(...map["_"]);
7355
+ }
7356
+ }
7357
+ if (map["$"])
7358
+ ids.push(...map["$"]);
7359
+ return ids;
7360
+ }
7361
+ // -------------------------------------------------------------------------
7362
+ // WAV encoder (PCM 16-bit mono)
7363
+ // -------------------------------------------------------------------------
7364
+ writeWav(samples, sampleRate, path) {
7365
+ const numChannels = 1;
7366
+ const bitsPerSample = 16;
7367
+ const byteRate = sampleRate * numChannels * (bitsPerSample / 8);
7368
+ const blockAlign = numChannels * (bitsPerSample / 8);
7369
+ const int16 = new Int16Array(samples.length);
7370
+ for (let i = 0; i < samples.length; i++) {
7371
+ const s = Math.max(-1, Math.min(1, samples[i]));
7372
+ int16[i] = s < 0 ? s * 32768 : s * 32767;
7373
+ }
7374
+ const dataSize = int16.length * 2;
7375
+ const buffer = Buffer.alloc(44 + dataSize);
7376
+ buffer.write("RIFF", 0);
7377
+ buffer.writeUInt32LE(36 + dataSize, 4);
7378
+ buffer.write("WAVE", 8);
7379
+ buffer.write("fmt ", 12);
7380
+ buffer.writeUInt32LE(16, 16);
7381
+ buffer.writeUInt16LE(1, 20);
7382
+ buffer.writeUInt16LE(numChannels, 22);
7383
+ buffer.writeUInt32LE(sampleRate, 24);
7384
+ buffer.writeUInt32LE(byteRate, 28);
7385
+ buffer.writeUInt16LE(blockAlign, 32);
7386
+ buffer.writeUInt16LE(bitsPerSample, 34);
7387
+ buffer.write("data", 36);
7388
+ buffer.writeUInt32LE(dataSize, 40);
7389
+ Buffer.from(int16.buffer, int16.byteOffset, int16.byteLength).copy(buffer, 44);
7390
+ writeFileSync4(path, buffer);
7391
+ }
7392
+ // -------------------------------------------------------------------------
7393
+ // Audio playback (system default speakers)
7394
+ // -------------------------------------------------------------------------
7395
+ async playWav(path) {
7396
+ this.killPlayback();
7397
+ const cmd = this.getPlayCommand(path);
7398
+ if (!cmd)
7399
+ return;
7400
+ return new Promise((resolve12) => {
7401
+ const child = nodeSpawn(cmd[0], cmd.slice(1), {
7402
+ stdio: "ignore",
7403
+ detached: false
7404
+ });
7405
+ this.currentPlayback = child;
7406
+ child.on("close", () => {
7407
+ if (this.currentPlayback === child)
7408
+ this.currentPlayback = null;
7409
+ resolve12();
7410
+ });
7411
+ child.on("error", () => {
7412
+ if (this.currentPlayback === child)
7413
+ this.currentPlayback = null;
7414
+ resolve12();
7415
+ });
7416
+ setTimeout(() => {
7417
+ this.killPlayback();
7418
+ resolve12();
7419
+ }, 15e3);
7420
+ });
7421
+ }
7422
+ getPlayCommand(path) {
7423
+ const os = platform();
7424
+ if (os === "darwin")
7425
+ return ["afplay", path];
7426
+ if (os === "win32") {
7427
+ return [
7428
+ "powershell",
7429
+ "-c",
7430
+ `(New-Object Media.SoundPlayer '${path}').PlaySync()`
7431
+ ];
7432
+ }
7433
+ for (const player of ["paplay", "pw-play", "aplay"]) {
7434
+ try {
7435
+ execSync8(`which ${player}`, { stdio: "pipe" });
7436
+ return [player, path];
7437
+ } catch {
7438
+ }
7439
+ }
7440
+ return null;
7441
+ }
7442
+ killPlayback() {
7443
+ if (this.currentPlayback) {
7444
+ try {
7445
+ this.currentPlayback.kill("SIGTERM");
7446
+ } catch {
7447
+ }
7448
+ this.currentPlayback = null;
7449
+ }
7450
+ }
7451
+ // -------------------------------------------------------------------------
7452
+ // Setup: ONNX runtime installation
7453
+ // -------------------------------------------------------------------------
7454
+ async ensureRuntime() {
7455
+ if (this.ort)
7456
+ return;
7457
+ mkdirSync4(VOICE_DIR, { recursive: true });
7458
+ const pkgPath = join13(VOICE_DIR, "package.json");
7459
+ if (!existsSync8(pkgPath)) {
7460
+ writeFileSync4(pkgPath, JSON.stringify({
7461
+ name: "open-agents-voice",
7462
+ private: true,
7463
+ dependencies: { "onnxruntime-node": "^1.21.0" }
7464
+ }, null, 2));
7465
+ }
7466
+ const voiceRequire = createRequire(join13(VOICE_DIR, "index.js"));
7467
+ try {
7468
+ this.ort = voiceRequire("onnxruntime-node");
7469
+ } catch {
7470
+ renderInfo("Installing ONNX runtime for voice synthesis...");
7471
+ try {
7472
+ execSync8("npm install --no-audit --no-fund", {
7473
+ cwd: VOICE_DIR,
7474
+ stdio: "pipe",
7475
+ timeout: 12e4
7476
+ });
7477
+ this.ort = voiceRequire("onnxruntime-node");
7478
+ } catch (err) {
7479
+ throw new Error(`Failed to install onnxruntime-node. Try manually: cd ${VOICE_DIR} && npm install
7480
+ Error: ${err instanceof Error ? err.message : String(err)}`);
7481
+ }
7482
+ }
7483
+ try {
7484
+ execSync8("espeak-ng --version", { stdio: "pipe" });
7485
+ this.hasEspeak = true;
7486
+ } catch {
7487
+ this.hasEspeak = false;
7488
+ renderWarning("espeak-ng not found \u2014 using basic phonemization. For better voice quality: sudo apt install espeak-ng");
7489
+ }
7490
+ }
7491
+ // -------------------------------------------------------------------------
7492
+ // Setup: Model download
7493
+ // -------------------------------------------------------------------------
7494
+ async ensureModel(id) {
7495
+ const model = VOICE_MODELS[id];
7496
+ if (!model)
7497
+ throw new Error(`Unknown model: ${id}`);
7498
+ const dir = modelDir(id);
7499
+ const onnxPath = modelOnnxPath(id);
7500
+ const configPath = modelConfigPath(id);
7501
+ if (existsSync8(onnxPath) && existsSync8(configPath))
7502
+ return;
7503
+ mkdirSync4(dir, { recursive: true });
7504
+ if (!existsSync8(configPath)) {
7505
+ renderInfo(`Downloading ${model.label} voice config...`);
7506
+ const configResp = await fetch(model.configUrl);
7507
+ if (!configResp.ok)
7508
+ throw new Error(`Failed to download config: HTTP ${configResp.status}`);
7509
+ const configText = await configResp.text();
7510
+ writeFileSync4(configPath, configText);
7511
+ }
7512
+ if (!existsSync8(onnxPath)) {
7513
+ renderInfo(`Downloading ${model.label} voice model (this may take a minute)...`);
7514
+ const onnxResp = await fetch(model.onnxUrl);
7515
+ if (!onnxResp.ok)
7516
+ throw new Error(`Failed to download model: HTTP ${onnxResp.status}`);
7517
+ const reader = onnxResp.body?.getReader();
7518
+ if (!reader)
7519
+ throw new Error("No response body");
7520
+ const contentLength = parseInt(onnxResp.headers.get("content-length") || "0", 10);
7521
+ const chunks = [];
7522
+ let received = 0;
7523
+ while (true) {
7524
+ const { done, value } = await reader.read();
7525
+ if (done)
7526
+ break;
7527
+ chunks.push(value);
7528
+ received += value.length;
7529
+ if (contentLength > 0) {
7530
+ const pct = Math.round(received / contentLength * 100);
7531
+ process.stdout.write(`\r ${c2.dim(` ${pct}% (${formatBytes2(received)} / ${formatBytes2(contentLength)})`)}`);
7532
+ }
7533
+ }
7534
+ process.stdout.write("\r" + " ".repeat(60) + "\r");
7535
+ const fullBuffer = Buffer.concat(chunks);
7536
+ writeFileSync4(onnxPath, fullBuffer);
7537
+ renderInfo(`${model.label} model downloaded (${formatBytes2(fullBuffer.length)}).`);
7538
+ }
7539
+ }
7540
+ // -------------------------------------------------------------------------
7541
+ // Load ONNX session
7542
+ // -------------------------------------------------------------------------
7543
+ async loadSession() {
7544
+ if (!this.ort)
7545
+ throw new Error("ONNX runtime not loaded");
7546
+ const onnxPath = modelOnnxPath(this.modelId);
7547
+ const configPath = modelConfigPath(this.modelId);
7548
+ if (!existsSync8(onnxPath) || !existsSync8(configPath)) {
7549
+ throw new Error(`Model files not found for ${this.modelId}`);
7550
+ }
7551
+ this.config = JSON.parse(readFileSync6(configPath, "utf8"));
7552
+ renderInfo("Loading voice model...");
7553
+ this.session = await this.ort.InferenceSession.create(onnxPath, {
7554
+ executionProviders: ["cpu"],
7555
+ graphOptimizationLevel: "all"
7556
+ });
7557
+ renderInfo("Voice model loaded.");
7558
+ }
7559
+ };
7560
+ }
7561
+ });
7562
+
7085
7563
  // packages/cli/dist/tui/interactive.js
7086
7564
  import * as readline2 from "node:readline";
7087
7565
  import { cwd } from "node:process";
@@ -7138,7 +7616,7 @@ function buildTools(repoRoot) {
7138
7616
  ];
7139
7617
  return [...executionTools.map(adaptTool), createTaskCompleteTool()];
7140
7618
  }
7141
- async function runTask(task, config, repoRoot) {
7619
+ async function runTask(task, config, repoRoot, voice) {
7142
7620
  const backend = new OllamaAgenticBackend(config.backendUrl.replace(/\/$/, ""), config.model);
7143
7621
  const runner = new AgenticRunner(backend, {
7144
7622
  maxTurns: 30,
@@ -7153,9 +7631,16 @@ async function runTask(task, config, repoRoot) {
7153
7631
  switch (event.type) {
7154
7632
  case "tool_call":
7155
7633
  renderToolCallStart(event.toolName ?? "unknown", event.toolArgs ?? {});
7634
+ if (voice?.enabled) {
7635
+ const desc = describeToolCall(event.toolName ?? "unknown", event.toolArgs ?? {});
7636
+ voice.speak(desc);
7637
+ }
7156
7638
  break;
7157
7639
  case "tool_result":
7158
7640
  renderToolResult(event.toolName ?? "unknown", event.success ?? false, event.content ?? "");
7641
+ if (voice?.enabled && !(event.success ?? true)) {
7642
+ voice.speak(describeToolResult(event.toolName ?? "unknown", false));
7643
+ }
7159
7644
  break;
7160
7645
  case "model_response":
7161
7646
  if (config.verbose && event.content) {
@@ -7203,6 +7688,7 @@ async function startInteractive(config, repoPath) {
7203
7688
  process.exit(1);
7204
7689
  }
7205
7690
  renderHeader(config.model);
7691
+ const voiceEngine = new VoiceEngine();
7206
7692
  let currentConfig = { ...config };
7207
7693
  const rl = readline2.createInterface({
7208
7694
  input: process.stdin,
@@ -7234,7 +7720,14 @@ async function startInteractive(config, repoPath) {
7234
7720
  renderCompactHeader(currentConfig.model);
7235
7721
  },
7236
7722
  exit() {
7723
+ voiceEngine.dispose();
7237
7724
  rl.close();
7725
+ },
7726
+ async voiceToggle() {
7727
+ return voiceEngine.toggle();
7728
+ },
7729
+ async voiceSetModel(id) {
7730
+ return voiceEngine.setModel(id);
7238
7731
  }
7239
7732
  };
7240
7733
  rl.prompt();
@@ -7259,7 +7752,7 @@ ${c2.dim("Goodbye!")}
7259
7752
  }
7260
7753
  renderUserMessage(input);
7261
7754
  try {
7262
- await runTask(input, currentConfig, repoRoot);
7755
+ await runTask(input, currentConfig, repoRoot, voiceEngine);
7263
7756
  } catch (err) {
7264
7757
  renderError(err instanceof Error ? err.message : String(err));
7265
7758
  }
@@ -7318,6 +7811,7 @@ var init_interactive = __esm({
7318
7811
  init_commands();
7319
7812
  init_setup();
7320
7813
  init_render();
7814
+ init_voice();
7321
7815
  }
7322
7816
  });
7323
7817
 
@@ -7352,7 +7846,7 @@ import { glob } from "glob";
7352
7846
  import ignore from "ignore";
7353
7847
  import { readFile as readFile8, stat as stat2 } from "node:fs/promises";
7354
7848
  import { createHash } from "node:crypto";
7355
- import { join as join13, relative as relative2, extname as extname3, basename } from "node:path";
7849
+ import { join as join14, relative as relative2, extname as extname3, basename } from "node:path";
7356
7850
  var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
7357
7851
  var init_codebase_indexer = __esm({
7358
7852
  "packages/indexer/dist/codebase-indexer.js"() {
@@ -7396,7 +7890,7 @@ var init_codebase_indexer = __esm({
7396
7890
  const ig = ignore.default();
7397
7891
  if (this.config.respectGitignore) {
7398
7892
  try {
7399
- const gitignoreContent = await readFile8(join13(this.config.rootDir, ".gitignore"), "utf-8");
7893
+ const gitignoreContent = await readFile8(join14(this.config.rootDir, ".gitignore"), "utf-8");
7400
7894
  ig.add(gitignoreContent);
7401
7895
  } catch {
7402
7896
  }
@@ -7411,7 +7905,7 @@ var init_codebase_indexer = __esm({
7411
7905
  for (const relativePath of files) {
7412
7906
  if (ig.ignores(relativePath))
7413
7907
  continue;
7414
- const fullPath = join13(this.config.rootDir, relativePath);
7908
+ const fullPath = join14(this.config.rootDir, relativePath);
7415
7909
  try {
7416
7910
  const fileStat = await stat2(fullPath);
7417
7911
  if (fileStat.size > this.config.maxFileSize)
@@ -7457,7 +7951,7 @@ var init_codebase_indexer = __esm({
7457
7951
  if (!child) {
7458
7952
  child = {
7459
7953
  name: part,
7460
- path: join13(current.path, part),
7954
+ path: join14(current.path, part),
7461
7955
  type: "directory",
7462
7956
  children: []
7463
7957
  };
@@ -7532,13 +8026,13 @@ __export(index_repo_exports, {
7532
8026
  indexRepoCommand: () => indexRepoCommand
7533
8027
  });
7534
8028
  import { resolve as resolve11 } from "node:path";
7535
- import { existsSync as existsSync8, statSync as statSync4 } from "node:fs";
8029
+ import { existsSync as existsSync9, statSync as statSync4 } from "node:fs";
7536
8030
  import { cwd as cwd2 } from "node:process";
7537
8031
  async function indexRepoCommand(opts, _config) {
7538
8032
  const repoRoot = resolve11(opts.repoPath ?? cwd2());
7539
8033
  printHeader("Index Repository");
7540
8034
  printInfo(`Indexing: ${repoRoot}`);
7541
- if (!existsSync8(repoRoot)) {
8035
+ if (!existsSync9(repoRoot)) {
7542
8036
  printError(`Path does not exist: ${repoRoot}`);
7543
8037
  process.exit(1);
7544
8038
  }
@@ -7784,8 +8278,8 @@ var config_exports = {};
7784
8278
  __export(config_exports, {
7785
8279
  configCommand: () => configCommand
7786
8280
  });
7787
- import { join as join14 } from "node:path";
7788
- import { homedir as homedir4 } from "node:os";
8281
+ import { join as join15 } from "node:path";
8282
+ import { homedir as homedir5 } from "node:os";
7789
8283
  async function configCommand(opts, config) {
7790
8284
  if (opts.subCommand === "set") {
7791
8285
  return handleSet(opts, config);
@@ -7807,7 +8301,7 @@ function handleShow(opts, config) {
7807
8301
  printKeyValue("verbose", String(config.verbose), 2);
7808
8302
  printKeyValue("dbPath", config.dbPath, 2);
7809
8303
  printSection("Config File");
7810
- printInfo(`~/.open-agents/config.json (${join14(homedir4(), ".open-agents", "config.json")})`);
8304
+ printInfo(`~/.open-agents/config.json (${join15(homedir5(), ".open-agents", "config.json")})`);
7811
8305
  printSection("Environment Variables");
7812
8306
  printInfo("OPEN_AGENTS_BACKEND_URL \u2014 override backendUrl");
7813
8307
  printInfo("OPEN_AGENTS_MODEL \u2014 override model");
@@ -8038,9 +8532,9 @@ var eval_exports = {};
8038
8532
  __export(eval_exports, {
8039
8533
  evalCommand: () => evalCommand
8040
8534
  });
8041
- import { tmpdir } from "node:os";
8042
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
8043
- import { join as join15 } from "node:path";
8535
+ import { tmpdir as tmpdir2 } from "node:os";
8536
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
8537
+ import { join as join16 } from "node:path";
8044
8538
  async function evalCommand(opts, config) {
8045
8539
  const suiteName = opts.suite ?? "basic";
8046
8540
  const suite = SUITES[suiteName];
@@ -8161,9 +8655,9 @@ async function evalCommand(opts, config) {
8161
8655
  process.exit(failed > 0 ? 1 : 0);
8162
8656
  }
8163
8657
  function createTempEvalRepo() {
8164
- const dir = join15(tmpdir(), `open-agents-eval-${Date.now()}`);
8165
- mkdirSync4(dir, { recursive: true });
8166
- writeFileSync4(join15(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
8658
+ const dir = join16(tmpdir2(), `open-agents-eval-${Date.now()}`);
8659
+ mkdirSync5(dir, { recursive: true });
8660
+ writeFileSync5(join16(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
8167
8661
  return dir;
8168
8662
  }
8169
8663
  var BASIC_SUITE, FULL_SUITE, SUITES;
@@ -8221,9 +8715,9 @@ init_config();
8221
8715
  init_output();
8222
8716
  init_updater();
8223
8717
  import { parseArgs as nodeParseArgs2 } from "node:util";
8224
- import { createRequire } from "node:module";
8718
+ import { createRequire as createRequire2 } from "node:module";
8225
8719
  import { fileURLToPath } from "node:url";
8226
- import { dirname as dirname3, join as join16 } from "node:path";
8720
+ import { dirname as dirname3, join as join17 } from "node:path";
8227
8721
 
8228
8722
  // packages/cli/dist/cli.js
8229
8723
  import { createInterface } from "node:readline";
@@ -8329,8 +8823,8 @@ init_spinner();
8329
8823
  init_output();
8330
8824
  function getVersion() {
8331
8825
  try {
8332
- const require2 = createRequire(import.meta.url);
8333
- const pkgPath = join16(dirname3(fileURLToPath(import.meta.url)), "..", "package.json");
8826
+ const require2 = createRequire2(import.meta.url);
8827
+ const pkgPath = join17(dirname3(fileURLToPath(import.meta.url)), "..", "package.json");
8334
8828
  const pkg = require2(pkgPath);
8335
8829
  return pkg.version;
8336
8830
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — Claude Code-style TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,10 @@
25
25
  "tool-calling",
26
26
  "agentic",
27
27
  "code-generation",
28
- "developer-tools"
28
+ "developer-tools",
29
+ "tts",
30
+ "voice",
31
+ "onnx"
29
32
  ],
30
33
  "author": "robit-man",
31
34
  "license": "MIT",