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.
- package/README.md +22 -0
- package/dist/index.js +522 -28
- 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:
|
|
6646
|
+
const { createRequire: createRequire3 } = await import("node:module");
|
|
6635
6647
|
const { fileURLToPath: fileURLToPath2 } = await import("node:url");
|
|
6636
|
-
const { dirname: dirname4, join:
|
|
6637
|
-
const require2 =
|
|
6638
|
-
const pkgPath =
|
|
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
|
|
6999
|
-
mkdirSync3(
|
|
7000
|
-
const modelfilePath = join12(
|
|
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
|
|
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(
|
|
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 =
|
|
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:
|
|
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
|
|
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 (!
|
|
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
|
|
7788
|
-
import { homedir as
|
|
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 (${
|
|
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
|
|
8043
|
-
import { join as
|
|
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 =
|
|
8165
|
-
|
|
8166
|
-
|
|
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
|
|
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 =
|
|
8333
|
-
const pkgPath =
|
|
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.
|
|
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",
|