open-agents-ai 0.19.0 → 0.20.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 CHANGED
@@ -121,7 +121,11 @@ All proposals are indexed in `.oa/dreams/PROPOSAL-INDEX.md` for easy review.
121
121
 
122
122
  ## Listen Mode — Live Bidirectional Audio
123
123
 
124
- Listen mode enables real-time voice communication with the agent. Your microphone audio is captured, streamed through Whisper (via `transcribe-cli`), and the transcription is injected directly into the input line — creating a hands-free coding workflow.
124
+ Listen mode enables real-time voice communication with the agent. Your microphone audio is captured, streamed through Whisper, and the transcription is injected directly into the input line — creating a hands-free coding workflow.
125
+
126
+ Two transcription backends ensure broad platform support:
127
+ - **transcribe-cli** (faster-whisper / ONNX) — used by default, fastest on x86
128
+ - **openai-whisper** (Python venv) — automatic fallback for ARM, linux-arm64, or when ONNX is unavailable. Auto-creates a venv and installs deps on first use.
125
129
 
126
130
  ```bash
127
131
  /listen # Toggle microphone capture on/off
@@ -143,10 +147,11 @@ Listen mode enables real-time voice communication with the agent. Your microphon
143
147
  When combined with `/voice`, you get full bidirectional audio — speak your tasks, hear the agent's progress through TTS, and speak corrections mid-task. The status bar shows a blinking red `● REC` indicator with a countdown timer during auto-mode recording.
144
148
 
145
149
  **Platform support:**
146
- - **Linux**: `arecord` (ALSA) or `ffmpeg` (PulseAudio)
150
+ - **Linux x86**: `arecord` (ALSA) or `ffmpeg` (PulseAudio) + transcribe-cli
151
+ - **Linux ARM**: `arecord` or `ffmpeg` + openai-whisper (auto-installed in Python venv)
147
152
  - **macOS**: `sox` (CoreAudio) or `ffmpeg` (AVFoundation)
148
153
 
149
- The `transcribe-cli` dependency auto-installs in the background on first use.
154
+ The `transcribe-cli` dependency auto-installs in the background on first use. On ARM or when transcribe-cli fails, the system automatically falls back to `openai-whisper` via a self-managed Python venv (same approach used by Moondream vision).
150
155
 
151
156
  **File transcription**: Drag-and-drop audio/video files (`.mp3`, `.wav`, `.mp4`, `.mkv`, etc.) onto the terminal to transcribe them. Results are saved to `.oa/transcripts/`.
152
157
 
package/dist/index.js CHANGED
@@ -5407,8 +5407,8 @@ async function loadTranscribeCli() {
5407
5407
  const nvmBase = join13(homedir5(), ".nvm", "versions", "node");
5408
5408
  if (existsSync10(nvmBase)) {
5409
5409
  try {
5410
- const { readdirSync: readdirSync9 } = await import("node:fs");
5411
- for (const ver of readdirSync9(nvmBase)) {
5410
+ const { readdirSync: readdirSync10 } = await import("node:fs");
5411
+ for (const ver of readdirSync10(nvmBase)) {
5412
5412
  const tcPath = join13(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
5413
5413
  if (existsSync10(join13(tcPath, "dist", "index.js"))) {
5414
5414
  const { createRequire: createRequire4 } = await import("node:module");
@@ -9857,7 +9857,19 @@ var init_agenticRunner = __esm({
9857
9857
  "packages/orchestrator/dist/agenticRunner.js"() {
9858
9858
  "use strict";
9859
9859
  init_dist();
9860
- SYSTEM_PROMPT = `You are Open Agent, an expert coding assistant that works iteratively using tools. You solve coding tasks by exploring code, making precise changes, and validating results through test execution.
9860
+ SYSTEM_PROMPT = `You are Open Agent, an autonomous AI agent with full access to the local machine. You can read/write files, execute shell commands, browse the web, control the desktop (open applications, click UI elements, take screenshots), and interact with any software on the system. You solve tasks by using your tools iteratively until complete.
9861
+
9862
+ ## NEVER REFUSE \u2014 ALWAYS ATTEMPT
9863
+
9864
+ You have a comprehensive set of tools. NEVER say "I can't do that" or "I don't have the ability to...". Instead, ALWAYS attempt the task using your tools:
9865
+ - Need to open Firefox? Use shell: \`firefox https://example.com &\`
9866
+ - Need to click a button? Use desktop_click or shell with xdotool
9867
+ - Need to see the screen? Use screenshot or desktop_describe
9868
+ - Need to type text? Use shell with xdotool: \`xdotool type "text"\`
9869
+ - Need to install software? Use shell: \`sudo apt install ...\`
9870
+ - Need to interact with a website? Use web_fetch, or open the browser and use desktop tools
9871
+
9872
+ If a tool fails, try a different approach. If you're unsure, explore with your tools first. Do NOT give a text-only response when tools could accomplish the task.
9861
9873
 
9862
9874
  ## Available Tools
9863
9875
 
@@ -9909,14 +9921,27 @@ Use background_run for long-running commands (builds, test suites) so you can co
9909
9921
  Use sub_agent to parallelize independent sub-tasks or explore different approaches simultaneously.
9910
9922
  Check task_status periodically and read task_output when tasks complete.
9911
9923
 
9912
- ## Image & Visual Context
9924
+ ## Desktop Automation & Vision
9913
9925
 
9914
- - image_read: Read an image file (returns base64, dimensions, OCR text)
9926
+ - desktop_click: Click a UI element by natural language description. Takes a screenshot, finds the element with vision, clicks it. Example: desktop_click({target: "the Save button"})
9927
+ - desktop_describe: Take a screenshot and describe what's on screen (or ask a question about it). Use this to "see" the desktop.
9928
+ - vision: Analyze any image with Moondream VLM \u2014 caption, query, detect objects, find click targets
9915
9929
  - screenshot: Capture the screen or active window
9930
+ - image_read: Read an image file (returns base64, dimensions, OCR text)
9916
9931
  - ocr: Extract text from an image using OCR (supports region cropping/zoom)
9917
9932
 
9918
- When you receive image context from the user, use ocr to extract text or describe what you see.
9919
- Use screenshot to capture visual state for debugging UI issues.
9933
+ ### Desktop Interaction Workflow
9934
+
9935
+ When asked to interact with desktop applications (open browsers, click buttons, fill forms, etc.):
9936
+ 1. Use shell to launch applications: \`firefox https://example.com &\`
9937
+ 2. Use screenshot or desktop_describe to see what's on screen
9938
+ 3. Use desktop_click to click UI elements: \`desktop_click({target: "Sign Up button"})\`
9939
+ 4. Use shell with xdotool for keyboard input: \`xdotool type "username"\` and \`xdotool key Return\`
9940
+ 5. Use shell with xdotool for navigation: \`xdotool key Tab\`, \`xdotool key ctrl+l\`
9941
+ 6. Take screenshots between steps to verify progress
9942
+
9943
+ You CAN open Firefox, Chrome, or any application. You CAN click buttons, fill forms, and navigate websites.
9944
+ You CAN use xdotool for keyboard/mouse control. These are real capabilities, not hypothetical.
9920
9945
 
9921
9946
  ### Self-Guided Image Exploration
9922
9947
 
@@ -11712,10 +11737,12 @@ var init_dist5 = __esm({
11712
11737
 
11713
11738
  // packages/cli/dist/tui/listen.js
11714
11739
  import { spawn as spawn7, execSync as execSync12 } from "node:child_process";
11715
- import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
11716
- import { join as join19 } from "node:path";
11740
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, readdirSync as readdirSync6 } from "node:fs";
11741
+ import { join as join19, dirname as dirname6 } from "node:path";
11717
11742
  import { homedir as homedir6 } from "node:os";
11743
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
11718
11744
  import { EventEmitter } from "node:events";
11745
+ import { createInterface as createInterface2 } from "node:readline";
11719
11746
  function isAudioPath(path) {
11720
11747
  const ext = path.toLowerCase().split(".").pop();
11721
11748
  return ext ? AUDIO_EXTENSIONS.has(`.${ext}`) : false;
@@ -11794,6 +11821,49 @@ function findMicCaptureCommand() {
11794
11821
  }
11795
11822
  return null;
11796
11823
  }
11824
+ function findLiveWhisperScript() {
11825
+ const thisDir = dirname6(fileURLToPath3(import.meta.url));
11826
+ const candidates = [
11827
+ join19(thisDir, "../../../../packages/execution/scripts/live-whisper.py"),
11828
+ join19(thisDir, "../../../packages/execution/scripts/live-whisper.py"),
11829
+ join19(thisDir, "../../execution/scripts/live-whisper.py"),
11830
+ // npm install layout — scripts bundled alongside dist
11831
+ join19(thisDir, "../scripts/live-whisper.py"),
11832
+ join19(thisDir, "../../scripts/live-whisper.py")
11833
+ ];
11834
+ for (const p of candidates) {
11835
+ if (existsSync13(p))
11836
+ return p;
11837
+ }
11838
+ try {
11839
+ const globalRoot = execSync12("npm root -g", {
11840
+ encoding: "utf-8",
11841
+ timeout: 5e3,
11842
+ stdio: ["pipe", "pipe", "pipe"]
11843
+ }).trim();
11844
+ const candidates2 = [
11845
+ join19(globalRoot, "open-agents-ai", "dist", "scripts", "live-whisper.py"),
11846
+ join19(globalRoot, "open-agents-ai", "scripts", "live-whisper.py")
11847
+ ];
11848
+ for (const p of candidates2) {
11849
+ if (existsSync13(p))
11850
+ return p;
11851
+ }
11852
+ } catch {
11853
+ }
11854
+ const nvmBase = join19(homedir6(), ".nvm", "versions", "node");
11855
+ if (existsSync13(nvmBase)) {
11856
+ try {
11857
+ for (const ver of readdirSync6(nvmBase)) {
11858
+ const p = join19(nvmBase, ver, "lib", "node_modules", "open-agents-ai", "dist", "scripts", "live-whisper.py");
11859
+ if (existsSync13(p))
11860
+ return p;
11861
+ }
11862
+ } catch {
11863
+ }
11864
+ }
11865
+ return null;
11866
+ }
11797
11867
  function ensureTranscribeCliBackground() {
11798
11868
  if (_bgInstallPromise)
11799
11869
  return;
@@ -11827,7 +11897,7 @@ function getListenEngine(config) {
11827
11897
  }
11828
11898
  return _engine;
11829
11899
  }
11830
- var AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, ListenEngine, _bgInstallPromise, _engine;
11900
+ var AUDIO_EXTENSIONS, VIDEO_EXTENSIONS, WhisperFallbackTranscriber, ListenEngine, _bgInstallPromise, _engine;
11831
11901
  var init_listen = __esm({
11832
11902
  "packages/cli/dist/tui/listen.js"() {
11833
11903
  "use strict";
@@ -11852,11 +11922,104 @@ var init_listen = __esm({
11852
11922
  ".m4v",
11853
11923
  ".ts"
11854
11924
  ]);
11925
+ WhisperFallbackTranscriber = class extends EventEmitter {
11926
+ model;
11927
+ scriptPath;
11928
+ process = null;
11929
+ _ready = false;
11930
+ constructor(model, scriptPath2) {
11931
+ super();
11932
+ this.model = model;
11933
+ this.scriptPath = scriptPath2;
11934
+ }
11935
+ get ready() {
11936
+ return this._ready;
11937
+ }
11938
+ async start() {
11939
+ return new Promise((resolve19, reject) => {
11940
+ const timeout = setTimeout(() => {
11941
+ reject(new Error("Whisper fallback: model load timeout (5 min). First run downloads the model."));
11942
+ }, 3e5);
11943
+ this.process = spawn7("python3", [
11944
+ this.scriptPath,
11945
+ "--model",
11946
+ this.model,
11947
+ "--chunk-seconds",
11948
+ "3",
11949
+ "--window-seconds",
11950
+ "10"
11951
+ ], {
11952
+ stdio: ["pipe", "pipe", "pipe"],
11953
+ env: { ...process.env }
11954
+ });
11955
+ const rl = createInterface2({ input: this.process.stdout });
11956
+ rl.on("line", (line) => {
11957
+ try {
11958
+ const evt = JSON.parse(line);
11959
+ switch (evt.type) {
11960
+ case "status":
11961
+ this.emit("status", evt.message);
11962
+ break;
11963
+ case "ready":
11964
+ this._ready = true;
11965
+ clearTimeout(timeout);
11966
+ this.emit("ready");
11967
+ resolve19();
11968
+ break;
11969
+ case "transcript":
11970
+ this.emit("transcript", {
11971
+ text: evt.text,
11972
+ isFinal: evt.isFinal ?? false
11973
+ });
11974
+ break;
11975
+ case "error":
11976
+ this.emit("error", new Error(evt.message));
11977
+ break;
11978
+ }
11979
+ } catch {
11980
+ }
11981
+ });
11982
+ this.process.stderr?.on("data", (data) => {
11983
+ const text = data.toString().trim();
11984
+ if (text)
11985
+ this.emit("status", text);
11986
+ });
11987
+ this.process.on("error", (err) => {
11988
+ clearTimeout(timeout);
11989
+ reject(err);
11990
+ });
11991
+ this.process.on("close", (code) => {
11992
+ if (!this._ready) {
11993
+ clearTimeout(timeout);
11994
+ reject(new Error(`Whisper worker exited with code ${code} before ready`));
11995
+ }
11996
+ });
11997
+ });
11998
+ }
11999
+ /** Write PCM16 audio data to the worker's stdin. */
12000
+ write(chunk) {
12001
+ if (this.process?.stdin?.writable) {
12002
+ this.process.stdin.write(chunk);
12003
+ }
12004
+ }
12005
+ /** Stop the worker. */
12006
+ stop() {
12007
+ if (this.process) {
12008
+ try {
12009
+ this.process.stdin?.end();
12010
+ this.process.kill("SIGTERM");
12011
+ } catch {
12012
+ }
12013
+ this.process = null;
12014
+ }
12015
+ this._ready = false;
12016
+ }
12017
+ };
11855
12018
  ListenEngine = class extends EventEmitter {
11856
12019
  config;
11857
12020
  micProcess = null;
11858
12021
  liveTranscriber = null;
11859
- // TranscribeLive from transcribe-cli
12022
+ // TranscribeLive from transcribe-cli or WhisperFallbackTranscriber
11860
12023
  active = false;
11861
12024
  silenceTimer = null;
11862
12025
  countdownInterval = null;
@@ -11939,8 +12102,8 @@ var init_listen = __esm({
11939
12102
  const nvmBase = join19(homedir6(), ".nvm", "versions", "node");
11940
12103
  if (existsSync13(nvmBase)) {
11941
12104
  try {
11942
- const { readdirSync: readdirSync9 } = await import("node:fs");
11943
- for (const ver of readdirSync9(nvmBase)) {
12105
+ const { readdirSync: readdirSync10 } = await import("node:fs");
12106
+ for (const ver of readdirSync10(nvmBase)) {
11944
12107
  const tcPath = join19(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
11945
12108
  if (existsSync13(join19(tcPath, "dist", "index.js"))) {
11946
12109
  const { createRequire: createRequire4 } = await import("node:module");
@@ -11972,7 +12135,6 @@ var init_listen = __esm({
11972
12135
  tc = await this.loadTranscribeCli();
11973
12136
  }
11974
12137
  if (!tc) {
11975
- this.emit("info", "Installing transcribe-cli...");
11976
12138
  try {
11977
12139
  execSync12("npm i -g transcribe-cli", { stdio: "pipe", timeout: 18e4 });
11978
12140
  this.transcribeCliAvailable = null;
@@ -11980,62 +12142,85 @@ var init_listen = __esm({
11980
12142
  } catch {
11981
12143
  }
11982
12144
  }
11983
- if (!tc) {
11984
- return "Failed to install transcribe-cli. Try manually: npm i -g transcribe-cli";
11985
- }
11986
12145
  }
11987
- const TranscribeLive = tc.TranscribeLive;
11988
- if (!TranscribeLive) {
11989
- return "transcribe-cli does not export TranscribeLive. Try updating: npm i -g transcribe-cli@latest";
11990
- }
11991
- try {
11992
- this.liveTranscriber = new TranscribeLive({
11993
- model: this.config.model,
11994
- sampleRate: 16e3,
11995
- channels: 1,
11996
- sampleWidth: 2,
11997
- chunkDuration: 3
11998
- // 3s chunks for responsive transcription
11999
- });
12000
- } catch (err) {
12001
- const arch = process.arch;
12002
- const armHint = arch === "arm64" || arch === "arm" ? ` Live transcription may not be supported on ${process.platform}-${arch}.` : "";
12003
- return `Failed to create live transcriber.${armHint} Error: ${err instanceof Error ? err.message : String(err)}`;
12146
+ let usedFallback = false;
12147
+ let transcribeCliError = null;
12148
+ if (tc) {
12149
+ const TranscribeLive = tc.TranscribeLive;
12150
+ if (TranscribeLive) {
12151
+ try {
12152
+ this.liveTranscriber = new TranscribeLive({
12153
+ model: this.config.model,
12154
+ sampleRate: 16e3,
12155
+ channels: 1,
12156
+ sampleWidth: 2,
12157
+ chunkDuration: 3
12158
+ });
12159
+ this.liveTranscriber.on("transcript", (evt) => {
12160
+ if (!evt.text.trim())
12161
+ return;
12162
+ this.lastTranscriptTime = Date.now();
12163
+ this.pendingText = evt.text.trim();
12164
+ this.emit("transcript", this.pendingText, evt.isFinal);
12165
+ if (this.config.mode === "auto")
12166
+ this.resetSilenceTimer();
12167
+ });
12168
+ this.liveTranscriber.on("error", (err) => {
12169
+ this.emit("error", err);
12170
+ });
12171
+ await new Promise((resolve19, reject) => {
12172
+ const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
12173
+ this.liveTranscriber.on("ready", () => {
12174
+ clearTimeout(timeout);
12175
+ resolve19();
12176
+ });
12177
+ this.liveTranscriber.on("error", (err) => {
12178
+ clearTimeout(timeout);
12179
+ reject(err);
12180
+ });
12181
+ });
12182
+ } catch (err) {
12183
+ try {
12184
+ this.liveTranscriber?.stop();
12185
+ } catch {
12186
+ }
12187
+ this.liveTranscriber = null;
12188
+ transcribeCliError = err instanceof Error ? err.message : String(err);
12189
+ }
12190
+ }
12004
12191
  }
12005
- this.liveTranscriber.on("transcript", (evt) => {
12006
- if (!evt.text.trim())
12007
- return;
12008
- this.lastTranscriptTime = Date.now();
12009
- this.pendingText = evt.text.trim();
12010
- this.emit("transcript", this.pendingText, evt.isFinal);
12011
- if (this.config.mode === "auto") {
12012
- this.resetSilenceTimer();
12192
+ if (!this.liveTranscriber) {
12193
+ const scriptPath2 = findLiveWhisperScript();
12194
+ if (!scriptPath2) {
12195
+ const hint = transcribeCliError ? ` transcribe-cli error: ${transcribeCliError}` : "";
12196
+ return `No transcription backend available.${hint} live-whisper.py not found.`;
12013
12197
  }
12014
- });
12015
- this.liveTranscriber.on("error", (err) => {
12016
- this.emit("error", err);
12017
- });
12018
- try {
12019
- await new Promise((resolve19, reject) => {
12020
- const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
12021
- this.liveTranscriber.on("ready", () => {
12022
- clearTimeout(timeout);
12023
- resolve19();
12198
+ try {
12199
+ const fallback = new WhisperFallbackTranscriber(this.config.model, scriptPath2);
12200
+ usedFallback = true;
12201
+ fallback.on("status", (msg) => {
12202
+ this.emit("info", msg);
12024
12203
  });
12025
- this.liveTranscriber.on("error", (err) => {
12026
- clearTimeout(timeout);
12027
- reject(err);
12204
+ await fallback.start();
12205
+ fallback.on("transcript", (evt) => {
12206
+ if (!evt.text.trim())
12207
+ return;
12208
+ this.lastTranscriptTime = Date.now();
12209
+ this.pendingText = evt.text.trim();
12210
+ this.emit("transcript", this.pendingText, evt.isFinal);
12211
+ if (this.config.mode === "auto")
12212
+ this.resetSilenceTimer();
12028
12213
  });
12029
- });
12030
- } catch (err) {
12031
- try {
12032
- this.liveTranscriber.stop();
12033
- } catch {
12214
+ fallback.on("error", (err) => {
12215
+ this.emit("error", err);
12216
+ });
12217
+ this.liveTranscriber = fallback;
12218
+ } catch (err) {
12219
+ const msg = err instanceof Error ? err.message : String(err);
12220
+ const tcHint = transcribeCliError ? `
12221
+ transcribe-cli error: ${transcribeCliError}` : "";
12222
+ return `Failed to start live transcription: ${msg}${tcHint}`;
12034
12223
  }
12035
- this.liveTranscriber = null;
12036
- const arch = process.arch;
12037
- const armHint = arch === "arm64" || arch === "arm" ? ` The whisper model or phonemizer WASM may not support ${process.platform}-${arch}. File transcription (/transcribe <file>) may still work.` : "";
12038
- return `Failed to start live transcription: ${err instanceof Error ? err.message : String(err)}${armHint}`;
12039
12224
  }
12040
12225
  this.micProcess = spawn7(micCmd.cmd, micCmd.args, {
12041
12226
  stdio: ["pipe", "pipe", "pipe"],
@@ -12067,7 +12252,8 @@ var init_listen = __esm({
12067
12252
  this.emit("started");
12068
12253
  this.emit("recording", true);
12069
12254
  const modeDesc = this.config.mode === "auto" ? `auto (${this.config.silenceTimeoutMs / 1e3}s timeout)` : "confirm (press Enter to submit)";
12070
- return `Listening with ${this.config.model} model (${modeDesc})`;
12255
+ const backend = usedFallback ? "openai-whisper" : "transcribe-cli";
12256
+ return `Listening with ${this.config.model} model via ${backend} (${modeDesc})`;
12071
12257
  }
12072
12258
  /**
12073
12259
  * Stop listening — cleanup mic, transcriber, timers.
@@ -13317,8 +13503,8 @@ Approach this task thoughtfully:
13317
13503
  });
13318
13504
 
13319
13505
  // packages/prompts/dist/index.js
13320
- import { join as join20, dirname as dirname6 } from "node:path";
13321
- import { fileURLToPath as fileURLToPath3 } from "node:url";
13506
+ import { join as join20, dirname as dirname7 } from "node:path";
13507
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
13322
13508
  var _dir, _packageRoot;
13323
13509
  var init_dist6 = __esm({
13324
13510
  "packages/prompts/dist/index.js"() {
@@ -13327,13 +13513,13 @@ var init_dist6 = __esm({
13327
13513
  init_render2();
13328
13514
  init_task_templates();
13329
13515
  init_render2();
13330
- _dir = dirname6(fileURLToPath3(import.meta.url));
13516
+ _dir = dirname7(fileURLToPath4(import.meta.url));
13331
13517
  _packageRoot = join20(_dir, "..");
13332
13518
  }
13333
13519
  });
13334
13520
 
13335
13521
  // packages/cli/dist/tui/oa-directory.js
13336
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6, readdirSync as readdirSync6, statSync as statSync6, unlinkSync as unlinkSync2 } from "node:fs";
13522
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync6, readdirSync as readdirSync7, statSync as statSync6, unlinkSync as unlinkSync2 } from "node:fs";
13337
13523
  import { join as join21, relative as relative2, basename as basename5, extname as extname8 } from "node:path";
13338
13524
  import { homedir as homedir7 } from "node:os";
13339
13525
  function initOaDirectory(repoRoot) {
@@ -13514,7 +13700,7 @@ function loadRecentSessions(repoRoot, limit = 5) {
13514
13700
  if (!existsSync14(historyDir))
13515
13701
  return [];
13516
13702
  try {
13517
- const files = readdirSync6(historyDir).filter((f) => f.endsWith(".json")).map((f) => {
13703
+ const files = readdirSync7(historyDir).filter((f) => f.endsWith(".json")).map((f) => {
13518
13704
  const stat5 = statSync6(join21(historyDir, f));
13519
13705
  return { file: f, mtime: stat5.mtimeMs };
13520
13706
  }).sort((a, b) => b.mtime - a.mtime).slice(0, limit);
@@ -13612,7 +13798,7 @@ function buildDirTree(root, maxDepth, prefix = "", depth = 0) {
13612
13798
  return "";
13613
13799
  let result = "";
13614
13800
  try {
13615
- const entries = readdirSync6(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") || e.name === ".github").filter((e) => !SKIP_DIRS.has(e.name)).sort((a, b) => {
13801
+ const entries = readdirSync7(root, { withFileTypes: true }).filter((e) => !e.name.startsWith(".") || e.name === ".github").filter((e) => !SKIP_DIRS.has(e.name)).sort((a, b) => {
13616
13802
  if (a.isDirectory() && !b.isDirectory())
13617
13803
  return -1;
13618
13804
  if (!a.isDirectory() && b.isDirectory())
@@ -13627,7 +13813,7 @@ function buildDirTree(root, maxDepth, prefix = "", depth = 0) {
13627
13813
  if (entry.isDirectory()) {
13628
13814
  let fileCount = 0;
13629
13815
  try {
13630
- fileCount = readdirSync6(join21(root, entry.name)).filter((f) => !f.startsWith(".")).length;
13816
+ fileCount = readdirSync7(join21(root, entry.name)).filter((f) => !f.startsWith(".")).length;
13631
13817
  } catch {
13632
13818
  }
13633
13819
  result += `${prefix}${connector}${entry.name}/ (${fileCount})
@@ -14843,11 +15029,11 @@ async function handleUpdate(subcommand, ctx) {
14843
15029
  let currentVersion = "0.0.0";
14844
15030
  try {
14845
15031
  const { createRequire: createRequire4 } = await import("node:module");
14846
- const { fileURLToPath: fileURLToPath6 } = await import("node:url");
14847
- const { dirname: dirname9, join: join32 } = await import("node:path");
15032
+ const { fileURLToPath: fileURLToPath7 } = await import("node:url");
15033
+ const { dirname: dirname10, join: join32 } = await import("node:path");
14848
15034
  const { existsSync: existsSync21 } = await import("node:fs");
14849
15035
  const req = createRequire4(import.meta.url);
14850
- const thisDir = dirname9(fileURLToPath6(import.meta.url));
15036
+ const thisDir = dirname10(fileURLToPath7(import.meta.url));
14851
15037
  const candidates = [
14852
15038
  join32(thisDir, "..", "package.json"),
14853
15039
  join32(thisDir, "..", "..", "package.json"),
@@ -14959,7 +15145,7 @@ var init_commands = __esm({
14959
15145
  });
14960
15146
 
14961
15147
  // packages/cli/dist/tui/project-context.js
14962
- import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync7 } from "node:fs";
15148
+ import { existsSync as existsSync16, readFileSync as readFileSync13, readdirSync as readdirSync8 } from "node:fs";
14963
15149
  import { join as join23, basename as basename6 } from "node:path";
14964
15150
  import { execSync as execSync14 } from "node:child_process";
14965
15151
  import { homedir as homedir9, platform, release } from "node:os";
@@ -15045,7 +15231,7 @@ function loadMemoryDir(memDir, scope) {
15045
15231
  return "";
15046
15232
  const lines = [];
15047
15233
  try {
15048
- const files = readdirSync7(memDir).filter((f) => f.endsWith(".json"));
15234
+ const files = readdirSync8(memDir).filter((f) => f.endsWith(".json"));
15049
15235
  for (const file of files.slice(0, 10)) {
15050
15236
  try {
15051
15237
  const raw = readFileSync13(join23(memDir, file), "utf-8");
@@ -17246,7 +17432,7 @@ var init_edit_history = __esm({
17246
17432
  });
17247
17433
 
17248
17434
  // packages/cli/dist/tui/dream-engine.js
17249
- import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync15, existsSync as existsSync18, cpSync, rmSync, readdirSync as readdirSync8 } from "node:fs";
17435
+ import { mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, readFileSync as readFileSync15, existsSync as existsSync18, cpSync, rmSync, readdirSync as readdirSync9 } from "node:fs";
17250
17436
  import { join as join26, basename as basename7 } from "node:path";
17251
17437
  import { execSync as execSync16 } from "node:child_process";
17252
17438
  function adaptTool(tool) {
@@ -17784,7 +17970,7 @@ Each proposal includes implementation entrypoints and estimated effort.
17784
17970
  /** Update the master proposal index */
17785
17971
  updateProposalIndex() {
17786
17972
  try {
17787
- const files = readdirSync8(this.dreamsDir).filter((f) => f.endsWith(".md") && f !== "PROPOSAL-INDEX.md" && f !== "dream-state.json").sort();
17973
+ const files = readdirSync9(this.dreamsDir).filter((f) => f.endsWith(".md") && f !== "PROPOSAL-INDEX.md" && f !== "dream-state.json").sort();
17788
17974
  const index = `# Dream Proposals Index
17789
17975
 
17790
17976
  **Last updated**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
@@ -18186,15 +18372,15 @@ var init_status_bar = __esm({
18186
18372
  import * as readline2 from "node:readline";
18187
18373
  import { Writable } from "node:stream";
18188
18374
  import { cwd } from "node:process";
18189
- import { resolve as resolve16, join as join27, dirname as dirname7, extname as extname9 } from "node:path";
18375
+ import { resolve as resolve16, join as join27, dirname as dirname8, extname as extname9 } from "node:path";
18190
18376
  import { createRequire as createRequire2 } from "node:module";
18191
- import { fileURLToPath as fileURLToPath4 } from "node:url";
18377
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
18192
18378
  import { readFileSync as readFileSync16 } from "node:fs";
18193
18379
  import { existsSync as existsSync19 } from "node:fs";
18194
18380
  function getVersion() {
18195
18381
  try {
18196
18382
  const require2 = createRequire2(import.meta.url);
18197
- const thisDir = dirname7(fileURLToPath4(import.meta.url));
18383
+ const thisDir = dirname8(fileURLToPath5(import.meta.url));
18198
18384
  const candidates = [
18199
18385
  join27(thisDir, "..", "package.json"),
18200
18386
  join27(thisDir, "..", "..", "package.json"),
@@ -20308,8 +20494,8 @@ init_output();
20308
20494
  init_updater();
20309
20495
  import { parseArgs as nodeParseArgs2 } from "node:util";
20310
20496
  import { createRequire as createRequire3 } from "node:module";
20311
- import { fileURLToPath as fileURLToPath5 } from "node:url";
20312
- import { dirname as dirname8, join as join31 } from "node:path";
20497
+ import { fileURLToPath as fileURLToPath6 } from "node:url";
20498
+ import { dirname as dirname9, join as join31 } from "node:path";
20313
20499
 
20314
20500
  // packages/cli/dist/cli.js
20315
20501
  import { createInterface } from "node:readline";
@@ -20416,7 +20602,7 @@ init_output();
20416
20602
  function getVersion2() {
20417
20603
  try {
20418
20604
  const require2 = createRequire3(import.meta.url);
20419
- const pkgPath = join31(dirname8(fileURLToPath5(import.meta.url)), "..", "package.json");
20605
+ const pkgPath = join31(dirname9(fileURLToPath6(import.meta.url)), "..", "package.json");
20420
20606
  const pkg = require2(pkgPath);
20421
20607
  return pkg.version;
20422
20608
  } catch {
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ live-whisper.py — Self-contained live transcription worker using openai-whisper.
4
+
5
+ Fallback for transcribe-cli on platforms where faster-whisper / ONNX fails
6
+ (e.g. linux-arm64). Auto-creates a Python venv and installs openai-whisper.
7
+
8
+ Protocol:
9
+ stdin — raw PCM16 audio (16kHz, mono, 16-bit signed LE)
10
+ stdout — JSON lines:
11
+ {"type":"status","message":"Installing dependencies..."}
12
+ {"type":"status","message":"Loading model..."}
13
+ {"type":"ready"}
14
+ {"type":"transcript","text":"hello world","isFinal":false}
15
+ {"type":"transcript","text":"hello world how are you","isFinal":true}
16
+ {"type":"error","message":"..."}
17
+
18
+ Usage:
19
+ arecord -f S16_LE -r 16000 -c 1 -t raw -q - | python3 live-whisper.py --model base
20
+
21
+ Based on the proven ARM-compatible approach from hydra/whisper_asr.
22
+ """
23
+
24
+ import sys
25
+ import os
26
+ import json
27
+ import subprocess
28
+ import importlib
29
+ import struct
30
+ import time
31
+ import threading
32
+ from pathlib import Path
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Configuration
36
+ # ---------------------------------------------------------------------------
37
+
38
+ SCRIPT_DIR = Path(__file__).resolve().parent
39
+ VENV = SCRIPT_DIR / ".whisper-venv"
40
+ PY = VENV / "bin" / "python"
41
+ PIP = VENV / "bin" / "pip"
42
+
43
+ SAMPLE_RATE = 16000
44
+ CHANNELS = 1
45
+ SAMPLE_WIDTH = 2 # 16-bit
46
+ CHUNK_SECONDS = 3 # Transcribe every N seconds of audio
47
+ WINDOW_SECONDS = 10 # Transcribe last N seconds (sliding window)
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Output helpers (JSON lines to stdout)
51
+ # ---------------------------------------------------------------------------
52
+
53
+ def emit(event: dict):
54
+ """Write a JSON event to stdout and flush."""
55
+ sys.stdout.write(json.dumps(event) + "\n")
56
+ sys.stdout.flush()
57
+
58
+
59
+ def emit_status(msg: str):
60
+ emit({"type": "status", "message": msg})
61
+
62
+
63
+ def emit_error(msg: str):
64
+ emit({"type": "error", "message": msg})
65
+
66
+
67
+ def emit_transcript(text: str, is_final: bool = False):
68
+ emit({"type": "transcript", "text": text, "isFinal": is_final})
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Venv bootstrap (inspired by hydra/whisper_asr)
72
+ # ---------------------------------------------------------------------------
73
+
74
+ def _in_venv() -> bool:
75
+ return sys.prefix != sys.base_prefix
76
+
77
+
78
+ def _ensure_venv():
79
+ if VENV.exists():
80
+ return
81
+ emit_status("Creating Python venv for Whisper...")
82
+ import venv
83
+ venv.EnvBuilder(with_pip=True).create(str(VENV))
84
+ # Upgrade pip quietly
85
+ subprocess.check_call(
86
+ [str(PY), "-m", "pip", "install", "--upgrade", "pip"],
87
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
88
+ )
89
+
90
+
91
+ def _ensure_deps():
92
+ """Check and install missing dependencies."""
93
+ need = []
94
+ try:
95
+ import numpy
96
+ except ImportError:
97
+ need.append("numpy")
98
+ try:
99
+ import whisper
100
+ except ImportError:
101
+ need.append("openai-whisper")
102
+
103
+ if need:
104
+ emit_status(f"Installing: {', '.join(need)}...")
105
+ subprocess.check_call(
106
+ [str(PIP), "install", *need],
107
+ stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
108
+ )
109
+ # Force reimport
110
+ for mod in ["numpy", "whisper"]:
111
+ if mod in sys.modules:
112
+ del sys.modules[mod]
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Bootstrap: ensure we're in the venv with deps
116
+ # ---------------------------------------------------------------------------
117
+
118
+ if not _in_venv():
119
+ _ensure_venv()
120
+ # Re-exec this script inside the venv
121
+ os.execv(str(PY), [str(PY)] + sys.argv)
122
+
123
+ _ensure_deps()
124
+
125
+ # Now safe to import
126
+ import numpy as np
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Main transcription loop
130
+ # ---------------------------------------------------------------------------
131
+
132
+ def main():
133
+ import argparse
134
+ parser = argparse.ArgumentParser(description="Live Whisper transcription worker")
135
+ parser.add_argument("--model", default="base", help="Whisper model size (tiny/base/small/medium/large)")
136
+ parser.add_argument("--chunk-seconds", type=float, default=CHUNK_SECONDS, help="Transcribe interval")
137
+ parser.add_argument("--window-seconds", type=float, default=WINDOW_SECONDS, help="Sliding window size")
138
+ parser.add_argument("--language", default=None, help="Language code (e.g. en, es, fr). Auto-detect if omitted.")
139
+ args = parser.parse_args()
140
+
141
+ import whisper
142
+
143
+ # Load model
144
+ emit_status(f"Loading Whisper {args.model} model...")
145
+ try:
146
+ device = "cpu"
147
+ try:
148
+ import torch
149
+ if torch.cuda.is_available():
150
+ device = "cuda"
151
+ except ImportError:
152
+ pass
153
+
154
+ model = whisper.load_model(args.model, device=device)
155
+ except Exception as e:
156
+ emit_error(f"Failed to load model: {e}")
157
+ sys.exit(1)
158
+
159
+ emit({"type": "ready"})
160
+
161
+ # Audio buffer — accumulates PCM16 as float32 @ 16kHz
162
+ audio_buf = np.zeros(0, dtype=np.float32)
163
+ buf_lock = threading.Lock()
164
+ chunk_bytes = int(args.chunk_seconds * SAMPLE_RATE * SAMPLE_WIDTH)
165
+ window_samples = int(args.window_seconds * SAMPLE_RATE)
166
+ last_text = ""
167
+ running = True
168
+
169
+ def read_stdin():
170
+ """Read PCM16 from stdin in a background thread."""
171
+ nonlocal audio_buf, running
172
+ try:
173
+ while running:
174
+ data = sys.stdin.buffer.read(chunk_bytes)
175
+ if not data:
176
+ break # EOF
177
+ # Convert PCM16 LE to float32 [-1, 1]
178
+ samples = np.frombuffer(data, dtype=np.int16).astype(np.float32) / 32768.0
179
+ with buf_lock:
180
+ audio_buf = np.concatenate([audio_buf, samples])
181
+ except Exception:
182
+ pass
183
+ finally:
184
+ running = False
185
+
186
+ # Start stdin reader thread
187
+ reader = threading.Thread(target=read_stdin, daemon=True)
188
+ reader.start()
189
+
190
+ try:
191
+ while running:
192
+ time.sleep(args.chunk_seconds)
193
+
194
+ with buf_lock:
195
+ if len(audio_buf) < SAMPLE_RATE: # Need at least 1s of audio
196
+ continue
197
+ # Take the last window_seconds of audio
198
+ window = audio_buf[-window_samples:].copy() if len(audio_buf) > window_samples else audio_buf.copy()
199
+
200
+ # Transcribe
201
+ try:
202
+ fp16 = (device == "cuda")
203
+ result = model.transcribe(
204
+ window,
205
+ fp16=fp16,
206
+ language=args.language,
207
+ no_speech_threshold=0.6,
208
+ condition_on_previous_text=False,
209
+ )
210
+ text = result.get("text", "").strip()
211
+ if text and text != last_text:
212
+ last_text = text
213
+ emit_transcript(text, is_final=False)
214
+ except Exception as e:
215
+ emit_error(f"Transcription error: {e}")
216
+
217
+ except KeyboardInterrupt:
218
+ pass
219
+
220
+ # Final transcription of the full buffer
221
+ with buf_lock:
222
+ full_audio = audio_buf.copy()
223
+
224
+ if len(full_audio) >= SAMPLE_RATE:
225
+ try:
226
+ fp16 = (device == "cuda")
227
+ result = model.transcribe(
228
+ full_audio,
229
+ fp16=fp16,
230
+ language=args.language,
231
+ )
232
+ text = result.get("text", "").strip()
233
+ if text:
234
+ emit_transcript(text, is_final=True)
235
+ except Exception:
236
+ pass
237
+
238
+ running = False
239
+
240
+
241
+ if __name__ == "__main__":
242
+ main()
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Headless Moondream Station launcher for open-agents.
4
+
5
+ Starts the Moondream vision model REST API server on port 2020 without
6
+ the interactive REPL. Designed to be auto-launched by the VisionTool.
7
+
8
+ Usage:
9
+ python start-moondream.py [--port 2020] [--host 127.0.0.1]
10
+
11
+ Environment:
12
+ HF_TOKEN - HuggingFace token (optional, for gated models)
13
+ """
14
+
15
+ import sys
16
+ import signal
17
+ import time
18
+ import argparse
19
+
20
+ def main():
21
+ parser = argparse.ArgumentParser(description="Start Moondream Station REST server")
22
+ parser.add_argument("--port", type=int, default=2020, help="Server port (default: 2020)")
23
+ parser.add_argument("--host", default="127.0.0.1", help="Server host (default: 127.0.0.1)")
24
+ parser.add_argument("--model", default=None, help="Model to use (default: auto-detect, prefers non-gated moondream-2)")
25
+ args = parser.parse_args()
26
+
27
+ try:
28
+ from moondream_station.core.config import ConfigManager
29
+ from moondream_station.core.manifest import ManifestManager
30
+ from moondream_station.core.models import ModelManager
31
+ from moondream_station.core.service import ServiceManager
32
+ from moondream_station.core.analytics import Analytics
33
+ from moondream_station.session import SessionState
34
+ from moondream_station.ui.display import Display
35
+ except ImportError:
36
+ print("ERROR: moondream-station not installed. Install with: pip install moondream-station", file=sys.stderr)
37
+ sys.exit(1)
38
+
39
+ print(f"[moondream] Initializing...", flush=True)
40
+
41
+ config = ConfigManager()
42
+ config.set("service_host", args.host)
43
+ config.set("service_port", args.port)
44
+
45
+ manifest_manager = ManifestManager(config)
46
+ analytics = Analytics(config, manifest_manager)
47
+ display = Display()
48
+ models = ModelManager(config, manifest_manager)
49
+ session_state = SessionState()
50
+
51
+ # Load manifest
52
+ manifest_url = "https://m87-md-prod-assets.s3.us-west-2.amazonaws.com/station/mds2/production_manifest.json"
53
+ print(f"[moondream] Loading manifest...", flush=True)
54
+ try:
55
+ manifest_manager.load_manifest(manifest_url, analytics, display)
56
+ except Exception as e:
57
+ print(f"ERROR: Failed to load manifest: {e}", file=sys.stderr)
58
+ sys.exit(1)
59
+
60
+ # Select model — prefer moondream-2 (non-gated) unless overridden
61
+ import os
62
+ model_name = args.model
63
+ if not model_name:
64
+ # Prefer moondream-2 (no HF token required) unless user has HF_TOKEN
65
+ has_hf_token = bool(os.environ.get("HF_TOKEN") or config.get("hf_token"))
66
+ if has_hf_token:
67
+ model_name = manifest_manager.get_available_default_model()
68
+ else:
69
+ model_name = "moondream-2"
70
+ if not model_name:
71
+ model_name = manifest_manager.get_available_default_model()
72
+ if not model_name:
73
+ print("ERROR: No model available", file=sys.stderr)
74
+ sys.exit(1)
75
+
76
+ print(f"[moondream] Switching to model: {model_name}", flush=True)
77
+ if not models.switch_model(model_name, display):
78
+ print(f"ERROR: Failed to switch to model {model_name}", file=sys.stderr)
79
+ sys.exit(1)
80
+
81
+ # Start REST server
82
+ service = ServiceManager(config, manifest_manager, session_state, analytics)
83
+ print(f"[moondream] Starting REST server on {args.host}:{args.port}...", flush=True)
84
+
85
+ if not service.start(model_name, args.port):
86
+ print("ERROR: Failed to start REST server", file=sys.stderr)
87
+ sys.exit(1)
88
+
89
+ print(f"[moondream] Server running at http://{args.host}:{args.port}/v1", flush=True)
90
+ print(f"[moondream] Endpoints: /v1/caption, /v1/query, /v1/detect, /v1/point", flush=True)
91
+ print(f"READY", flush=True)
92
+
93
+ # Handle shutdown
94
+ def shutdown(signum, frame):
95
+ print(f"\n[moondream] Shutting down...", flush=True)
96
+ service.stop()
97
+ sys.exit(0)
98
+
99
+ signal.signal(signal.SIGTERM, shutdown)
100
+ signal.signal(signal.SIGINT, shutdown)
101
+
102
+ # Keep alive
103
+ try:
104
+ while service.is_running():
105
+ time.sleep(1)
106
+ except KeyboardInterrupt:
107
+ shutdown(None, None)
108
+
109
+ print("[moondream] Server stopped", flush=True)
110
+
111
+ if __name__ == "__main__":
112
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",