ai-whisper 0.9.0 → 0.10.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
@@ -59,7 +59,7 @@ You pair any two of four agents — `claude`, `codex`, `ezio`, and `agy`. ai-whi
59
59
  - **ezio** *(optional)* — bundled with ai-whisper; mount it with `whisper collab mount ezio`, no separate install.
60
60
  - **agy** *(optional)* — the `agy` command (Antigravity CLI), signed in. Mount it with `whisper collab mount agy` (manual-mount parity; no auto-launch via `collab start`).
61
61
  - **Node.js 22+**.
62
- - **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. See [Evaluator configuration](docs/evaluator-configuration.md).
62
+ - **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. It supports four providers: **Anthropic** (default), a local **Ollama** model, **OpenAI** (and any OpenAI-compatible backend via a `baseURL` — Azure, OpenRouter, vLLM, LM Studio, …), or reusing an **already-mounted agent CLI** (`claude`/`codex`/`agy`) in non-interactive mode (no separate key — it reuses that CLI's own auth). See [Evaluator configuration](docs/evaluator-configuration.md).
63
63
  - **tmux** *(optional)* — only for `whisper collab start`, which auto-arranges both agents into panes. The mount flow below does not need it.
64
64
 
65
65
  > **Platform support:** ai-whisper is terminal-native and Unix-oriented — it drives interactive PTY sessions, so it runs on **macOS and Linux**. It is **not supported natively on Windows**: `whisper collab mount` / `reconnect` require a Unix tty-backed shell and will exit with an error pointing here. On Windows, run ai-whisper inside **[WSL2](https://learn.microsoft.com/windows/wsl/install)** — install Node, your agent CLI, and ai-whisper inside the WSL2 distro and run the commands there, where everything works as-is.
@@ -5918,7 +5918,45 @@ function createWorkflowEventBridge(input) {
5918
5918
  import * as crypto3 from "node:crypto";
5919
5919
  import Anthropic from "@anthropic-ai/sdk";
5920
5920
  import { Ollama } from "ollama";
5921
+ import OpenAI from "openai";
5921
5922
  import { z as z18 } from "zod";
5923
+
5924
+ // src/runtime/agent-cli-presets.ts
5925
+ import { spawn as nodeSpawn } from "node:child_process";
5926
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
5927
+ import { delimiter, join as join6 } from "node:path";
5928
+ var defaultSpawn = (command, args, options) => nodeSpawn(command, [...args], options);
5929
+ var PRESETS = {
5930
+ claude: { executable: "claude", execArgs: ["-p"], promptVia: "arg" },
5931
+ codex: { executable: "codex", execArgs: ["exec"], promptVia: "arg" },
5932
+ agy: { executable: "agy", execArgs: ["-p"], promptVia: "arg" }
5933
+ };
5934
+ function resolveAgentCliInvocation(input) {
5935
+ const preset = PRESETS[input.agent];
5936
+ return {
5937
+ executable: input.executable ?? preset.executable,
5938
+ execArgs: input.execArgs ?? preset.execArgs,
5939
+ promptVia: input.promptVia ?? preset.promptVia
5940
+ };
5941
+ }
5942
+ function isExecutableOnPath(executable) {
5943
+ const isExecFile = (p) => {
5944
+ try {
5945
+ if (!statSync3(p).isFile()) return false;
5946
+ accessSync(p, constants.X_OK);
5947
+ return true;
5948
+ } catch {
5949
+ return false;
5950
+ }
5951
+ };
5952
+ if (executable.includes("/") || executable.includes("\\")) return isExecFile(executable);
5953
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
5954
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
5955
+ return dirs.some((dir) => exts.some((ext) => isExecFile(join6(dir, executable + ext))));
5956
+ }
5957
+
5958
+ // src/runtime/relay-orchestrator-evaluator.ts
5959
+ var defaultOpenAIFactory = (opts) => new OpenAI(opts);
5922
5960
  var baseFields = {
5923
5961
  confidence: z18.number().min(0).max(1),
5924
5962
  reason: z18.string()
@@ -6158,7 +6196,10 @@ function selectBranch(payload) {
6158
6196
  }
6159
6197
  return legacyBranch;
6160
6198
  }
6199
+ var ProviderUnavailableError = class extends Error {
6200
+ };
6161
6201
  function isProviderUnavailableError(error) {
6202
+ if (error instanceof ProviderUnavailableError) return true;
6162
6203
  if (!(error instanceof Error)) return false;
6163
6204
  const code = error.code;
6164
6205
  if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || code === "ECONNRESET")
@@ -6222,6 +6263,77 @@ function buildOllamaCaller(config) {
6222
6263
  return { raw: response.message.content };
6223
6264
  };
6224
6265
  }
6266
+ function buildOpenAICaller(config) {
6267
+ const client = (config.createClient ?? defaultOpenAIFactory)({
6268
+ apiKey: config.apiKey,
6269
+ ...config.baseURL ? { baseURL: config.baseURL } : {}
6270
+ });
6271
+ const model = config.model;
6272
+ return async function(systemPrompt, payload, jsonSchema) {
6273
+ const response = await client.chat.completions.create({
6274
+ model,
6275
+ messages: [
6276
+ { role: "system", content: systemPrompt },
6277
+ { role: "user", content: JSON.stringify(payload) }
6278
+ ],
6279
+ // Non-strict json_schema: schema-shaped guidance only; branch.parse() is authoritative.
6280
+ // `strict` is intentionally omitted (see spec §5.2.1) — do not set it true.
6281
+ response_format: { type: "json_schema", json_schema: { name: "verdict", schema: jsonSchema } },
6282
+ temperature: 0.3
6283
+ });
6284
+ const raw = response.choices[0]?.message.content ?? "";
6285
+ return {
6286
+ raw,
6287
+ ...typeof response.usage?.prompt_tokens === "number" ? { inputTokens: response.usage.prompt_tokens } : {},
6288
+ ...typeof response.usage?.completion_tokens === "number" ? { outputTokens: response.usage.completion_tokens } : {}
6289
+ };
6290
+ };
6291
+ }
6292
+ function buildAgentCliCaller(config) {
6293
+ const { executable, execArgs, promptVia } = resolveAgentCliInvocation(config);
6294
+ const spawnImpl = config.spawnImpl ?? defaultSpawn;
6295
+ return function(systemPrompt, payload) {
6296
+ const prompt = `${systemPrompt}
6297
+
6298
+ === INPUT (JSON) ===
6299
+ ${JSON.stringify(payload)}
6300
+ === END INPUT ===
6301
+
6302
+ Reply with ONLY the JSON object described above \u2014 no prose, no code fence.`;
6303
+ return new Promise((resolve2, reject) => {
6304
+ const child = promptVia === "arg" ? spawnImpl(executable, [...execArgs, prompt], { stdio: ["ignore", "pipe", "pipe"] }) : spawnImpl(executable, execArgs, { stdio: ["pipe", "pipe", "pipe"] });
6305
+ let stdout = "";
6306
+ let stderr = "";
6307
+ let settled = false;
6308
+ child.stdout.on("data", (chunk) => {
6309
+ stdout += String(chunk);
6310
+ });
6311
+ child.stderr.on("data", (chunk) => {
6312
+ stderr += String(chunk);
6313
+ });
6314
+ child.on("error", (err) => {
6315
+ if (settled) return;
6316
+ settled = true;
6317
+ reject(new ProviderUnavailableError(`agent-cli spawn failed (${executable}): ${err.message}`));
6318
+ });
6319
+ child.on("close", (code) => {
6320
+ if (settled) return;
6321
+ settled = true;
6322
+ if (code !== 0) {
6323
+ reject(new ProviderUnavailableError(`agent-cli ${executable} exited with code ${code}: ${stderr.trim()}`));
6324
+ return;
6325
+ }
6326
+ resolve2({ raw: stdout });
6327
+ });
6328
+ if (promptVia === "stdin") {
6329
+ child.stdin.on("error", () => {
6330
+ });
6331
+ child.stdin.write(prompt);
6332
+ child.stdin.end();
6333
+ }
6334
+ });
6335
+ };
6336
+ }
6225
6337
  function buildSingleProviderCaller(config) {
6226
6338
  if (config.provider === "anthropic") {
6227
6339
  const call2 = buildAnthropicCaller(config);
@@ -6264,6 +6376,46 @@ function buildSingleProviderCaller(config) {
6264
6376
  }
6265
6377
  };
6266
6378
  }
6379
+ if (config.provider === "openai") {
6380
+ const call2 = buildOpenAICaller(config);
6381
+ return async function(payload, branch) {
6382
+ const started = Date.now();
6383
+ let providerResult;
6384
+ try {
6385
+ providerResult = await call2(branch.systemPrompt, payload, branch.jsonSchema);
6386
+ } catch (callErr) {
6387
+ const providerLatencyMs2 = Date.now() - started;
6388
+ return { ok: false, error: callErr instanceof Error ? callErr : new Error(String(callErr)), raw: null, inputTokens: null, outputTokens: null, providerLatencyMs: providerLatencyMs2 };
6389
+ }
6390
+ const providerLatencyMs = Date.now() - started;
6391
+ try {
6392
+ const verdict = branch.parse(providerResult.raw);
6393
+ return { ok: true, verdict, raw: providerResult.raw, inputTokens: providerResult.inputTokens ?? null, outputTokens: providerResult.outputTokens ?? null, providerLatencyMs };
6394
+ } catch (parseErr) {
6395
+ return { ok: false, error: parseErr instanceof Error ? parseErr : new Error(String(parseErr)), raw: providerResult.raw, inputTokens: providerResult.inputTokens ?? null, outputTokens: providerResult.outputTokens ?? null, providerLatencyMs };
6396
+ }
6397
+ };
6398
+ }
6399
+ if (config.provider === "agent-cli") {
6400
+ const call2 = buildAgentCliCaller(config);
6401
+ return async function(payload, branch) {
6402
+ const started = Date.now();
6403
+ let raw;
6404
+ try {
6405
+ ({ raw } = await call2(branch.systemPrompt, payload));
6406
+ } catch (callErr) {
6407
+ const providerLatencyMs2 = Date.now() - started;
6408
+ return { ok: false, error: callErr instanceof Error ? callErr : new Error(String(callErr)), raw: null, inputTokens: null, outputTokens: null, providerLatencyMs: providerLatencyMs2 };
6409
+ }
6410
+ const providerLatencyMs = Date.now() - started;
6411
+ try {
6412
+ const verdict = branch.parse(raw);
6413
+ return { ok: true, verdict, raw, inputTokens: null, outputTokens: null, providerLatencyMs };
6414
+ } catch (parseErr) {
6415
+ return { ok: false, error: parseErr instanceof Error ? parseErr : new Error(String(parseErr)), raw, inputTokens: null, outputTokens: null, providerLatencyMs };
6416
+ }
6417
+ };
6418
+ }
6267
6419
  const call = buildOllamaCaller(config);
6268
6420
  return async function(payload, branch) {
6269
6421
  const started = Date.now();
@@ -6710,8 +6862,8 @@ function writeOwnPidToBrokerDaemon(db, input) {
6710
6862
  }
6711
6863
 
6712
6864
  // src/runtime/evaluator-config.ts
6713
- import { readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
6714
- import { join as join6 } from "node:path";
6865
+ import { readFileSync as readFileSync2, statSync as statSync4 } from "node:fs";
6866
+ import { join as join7 } from "node:path";
6715
6867
 
6716
6868
  // src/runtime/state-root.ts
6717
6869
  import os from "node:os";
@@ -6745,7 +6897,7 @@ function warnIfLoosePerms(path2) {
6745
6897
  if (process.platform === "win32") return;
6746
6898
  let mode;
6747
6899
  try {
6748
- mode = statSync3(path2).mode;
6900
+ mode = statSync4(path2).mode;
6749
6901
  } catch {
6750
6902
  return;
6751
6903
  }
@@ -6773,31 +6925,46 @@ function loadEvaluatorConfig() {
6773
6925
  const root = getStateRoot();
6774
6926
  let dotenv = {};
6775
6927
  try {
6776
- dotenv = parseDotEnv(readFileSync2(join6(root, ".env"), "utf8"));
6928
+ dotenv = parseDotEnv(readFileSync2(join7(root, ".env"), "utf8"));
6777
6929
  } catch (err) {
6778
6930
  if (err.code !== "ENOENT") throw err;
6779
6931
  }
6780
6932
  const envGet = (k) => process.env[k] ?? dotenv[k];
6781
- const authPath = join6(root, "auth.json");
6933
+ const authPath = join7(root, "auth.json");
6782
6934
  warnIfLoosePerms(authPath);
6783
6935
  const auth = readJsonFile(authPath, "auth.json");
6784
- const config = readJsonFile(join6(root, "config.json"), "config.json");
6936
+ const config = readJsonFile(join7(root, "config.json"), "config.json");
6785
6937
  const evalCfg = config?.evaluator ?? {};
6786
6938
  const evalOllama = evalCfg.ollama ?? {};
6787
- const provider = (envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider) === "ollama" ? "ollama" : "anthropic";
6939
+ const provider = envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider;
6940
+ const resolvedProvider = provider === "ollama" || provider === "openai" || provider === "agent-cli" ? provider : "anthropic";
6788
6941
  const rawFallback = envGet("AI_WHISPER_EVALUATOR_FALLBACK") ?? evalCfg.fallback;
6789
- const fallback = rawFallback === "anthropic" || rawFallback === "ollama" ? rawFallback : null;
6942
+ const fallback = rawFallback === "anthropic" || rawFallback === "ollama" || rawFallback === "openai" || rawFallback === "agent-cli" ? rawFallback : null;
6790
6943
  const apiKey = envGet("ANTHROPIC_API_KEY") ?? auth?.ANTHROPIC_API_KEY ?? null;
6944
+ const evalOpenai = evalCfg.openai ?? {};
6945
+ const openaiKey = envGet("OPENAI_API_KEY") ?? auth?.OPENAI_API_KEY ?? null;
6946
+ const evalAgentCli = evalCfg.agentCli ?? {};
6947
+ const rawAgent = envGet("AI_WHISPER_EVALUATOR_AGENT_CLI_AGENT") ?? evalAgentCli.agent;
6948
+ const agent = rawAgent === "claude" || rawAgent === "codex" || rawAgent === "agy" ? rawAgent : null;
6791
6949
  return {
6792
- provider,
6950
+ provider: resolvedProvider,
6793
6951
  fallback,
6794
- anthropic: {
6795
- apiKey: apiKey && apiKey.length > 0 ? apiKey : null,
6796
- model: evalCfg.anthropicModel ?? null
6797
- },
6952
+ anthropic: { apiKey: apiKey && apiKey.length > 0 ? apiKey : null, model: evalCfg.anthropicModel ?? null },
6798
6953
  ollama: {
6799
6954
  host: envGet("AI_WHISPER_EVALUATOR_OLLAMA_HOST") ?? evalOllama.host ?? null,
6800
6955
  model: envGet("AI_WHISPER_EVALUATOR_OLLAMA_MODEL") ?? evalOllama.model ?? null
6956
+ },
6957
+ openai: {
6958
+ apiKey: openaiKey && openaiKey.length > 0 ? openaiKey : null,
6959
+ model: envGet("AI_WHISPER_EVALUATOR_OPENAI_MODEL") ?? evalOpenai.model ?? null,
6960
+ baseURL: envGet("AI_WHISPER_EVALUATOR_OPENAI_BASE_URL") ?? evalOpenai.baseURL ?? null
6961
+ },
6962
+ agentCli: {
6963
+ agent,
6964
+ executable: evalAgentCli.executable ?? null,
6965
+ execArgs: Array.isArray(evalAgentCli.execArgs) ? evalAgentCli.execArgs : null,
6966
+ promptVia: evalAgentCli.promptVia === "arg" || evalAgentCli.promptVia === "stdin" ? evalAgentCli.promptVia : null,
6967
+ model: evalAgentCli.model ?? null
6801
6968
  }
6802
6969
  };
6803
6970
  }
@@ -6807,6 +6974,21 @@ function computeEvaluatorStatus(cfg, ctx) {
6807
6974
  if (cfg.provider === "anthropic" && cfg.anthropic.apiKey === null) {
6808
6975
  return "missing_anthropic_key";
6809
6976
  }
6977
+ if (cfg.provider === "openai") {
6978
+ if (cfg.openai.apiKey === null) return "missing_openai_key";
6979
+ if (cfg.openai.model === null) return "invalid_config";
6980
+ }
6981
+ if (cfg.provider === "agent-cli") {
6982
+ if (cfg.agentCli.agent === null) return "invalid_config";
6983
+ const { executable } = resolveAgentCliInvocation({
6984
+ agent: cfg.agentCli.agent,
6985
+ executable: cfg.agentCli.executable,
6986
+ execArgs: cfg.agentCli.execArgs,
6987
+ promptVia: cfg.agentCli.promptVia
6988
+ });
6989
+ const exists = (ctx.executableExists ?? isExecutableOnPath)(executable);
6990
+ if (!exists) return "agent_cli_unavailable";
6991
+ }
6810
6992
  return "ready";
6811
6993
  }
6812
6994
 
@@ -6815,7 +6997,9 @@ var NOT_CONFIGURED = {
6815
6997
  provider: "anthropic",
6816
6998
  fallback: null,
6817
6999
  anthropic: { apiKey: null, model: null },
6818
- ollama: { host: null, model: null }
7000
+ ollama: { host: null, model: null },
7001
+ openai: { apiKey: null, model: null, baseURL: null },
7002
+ agentCli: { agent: null, executable: null, execArgs: null, promptVia: null, model: null }
6819
7003
  };
6820
7004
  function recordEvaluatorStatus(db, input) {
6821
7005
  const status = computeEvaluatorStatus(input.resolved ?? NOT_CONFIGURED, {
@@ -6828,7 +7012,7 @@ function recordEvaluatorStatus(db, input) {
6828
7012
 
6829
7013
  // src/bin/broker-daemon.ts
6830
7014
  import { execFile as execFile2 } from "node:child_process";
6831
- import { join as join7 } from "node:path";
7015
+ import { join as join8 } from "node:path";
6832
7016
 
6833
7017
  // src/runtime/cli-package-info.ts
6834
7018
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -6878,7 +7062,7 @@ if (collabId) {
6878
7062
  });
6879
7063
  workflowEventBridge.start();
6880
7064
  eventSocket = await createEventSocketServer({
6881
- socketPath: join7(getStateSocketsDir(), `events-${collabId}.sock`),
7065
+ socketPath: join8(getStateSocketsDir(), `events-${collabId}.sock`),
6882
7066
  events: [broker.events, externalEvents],
6883
7067
  engineVersion: resolveCliVersion()
6884
7068
  });
@@ -6905,10 +7089,30 @@ function providerConfigFrom(kind) {
6905
7089
  if (!resolved || resolved.anthropic.apiKey === null) return null;
6906
7090
  return { provider: "anthropic", apiKey: resolved.anthropic.apiKey };
6907
7091
  }
7092
+ if (kind === "openai") {
7093
+ if (!resolved || resolved.openai.apiKey === null || resolved.openai.model === null) return null;
7094
+ return {
7095
+ provider: "openai",
7096
+ apiKey: resolved.openai.apiKey,
7097
+ model: resolved.openai.model,
7098
+ ...resolved.openai.baseURL ? { baseURL: resolved.openai.baseURL } : {}
7099
+ };
7100
+ }
7101
+ if (kind === "ollama") {
7102
+ return {
7103
+ provider: "ollama",
7104
+ ...resolved?.ollama.host ? { host: resolved.ollama.host } : {},
7105
+ ...resolved?.ollama.model ? { model: resolved.ollama.model } : {}
7106
+ };
7107
+ }
7108
+ if (!resolved || resolved.agentCli.agent === null) return null;
6908
7109
  return {
6909
- provider: "ollama",
6910
- ...resolved?.ollama.host ? { host: resolved.ollama.host } : {},
6911
- ...resolved?.ollama.model ? { model: resolved.ollama.model } : {}
7110
+ provider: "agent-cli",
7111
+ agent: resolved.agentCli.agent,
7112
+ ...resolved.agentCli.executable ? { executable: resolved.agentCli.executable } : {},
7113
+ ...resolved.agentCli.execArgs ? { execArgs: resolved.agentCli.execArgs } : {},
7114
+ ...resolved.agentCli.promptVia ? { promptVia: resolved.agentCli.promptVia } : {},
7115
+ ...resolved.agentCli.model ? { model: resolved.agentCli.model } : {}
6912
7116
  };
6913
7117
  }
6914
7118
  var collab = broker.control.getCollab(collabId);
@@ -7506,6 +7506,8 @@ function haxSpawnEnv(base = process.env, transcriptPath) {
7506
7506
  const env = { ...base, HAX_EXTRA_SKILLS_DIR: aiEzioGlobalSkillsDir(base) };
7507
7507
  if (transcriptPath)
7508
7508
  env.HAX_TRANSCRIPT = transcriptPath;
7509
+ if (env.HAX_COMPACT_AUTO === void 0)
7510
+ env.HAX_COMPACT_AUTO = "0";
7509
7511
  return env;
7510
7512
  }
7511
7513
  function spawnHax(options = {}) {
@@ -7828,7 +7830,9 @@ var Session = class {
7828
7830
  async spawnAndPump(options) {
7829
7831
  this._transcriptPath = options.transcriptPath;
7830
7832
  const gen = this.generation;
7831
- const spawned = (this.options.spawn ?? spawnHax)(options);
7833
+ const overrides = this.options.engineEnvOverrides;
7834
+ const spawnOptions = overrides ? { ...options, env: { ...options.env ?? process.env, ...overrides } } : options;
7835
+ const spawned = (this.options.spawn ?? spawnHax)(spawnOptions);
7832
7836
  this.spawned = spawned;
7833
7837
  spawned.child.on("exit", (code, signal) => {
7834
7838
  if (gen !== this.generation)
@@ -8861,6 +8865,7 @@ function profileEnv(profile, parentEnv) {
8861
8865
  env.HAX_REASONING_EFFORT = profile.effort;
8862
8866
  else
8863
8867
  delete env.HAX_REASONING_EFFORT;
8868
+ env.HAX_COMPACT_AUTO = "1";
8864
8869
  return env;
8865
8870
  }
8866
8871
  function validateProfile(profile, parentEnv) {
@@ -9948,6 +9953,14 @@ function builtinCommands(listCommands) {
9948
9953
  {
9949
9954
  name: "compact",
9950
9955
  summary: "summarize old history and free context",
9956
+ // NOTE (post-hax-sync): upstream hax now ships its own `/compact` slash
9957
+ // command (`agent_compact`) that silently rewrites conversation history
9958
+ // inside the engine. This ezio-owned handler intercepts `/compact` here,
9959
+ // routes it to `ctx.compactor.compactNow()` (the harness-controlled protocol
9960
+ // `compact` control), and returns `action: "handled"` — so the literal text
9961
+ // "/compact" is NEVER forwarded as a raw `submit` to the hax session where
9962
+ // the engine's own slash registry would trigger `agent_compact`.
9963
+ // Do NOT remove this command or change its return path to `action: "submit"`.
9951
9964
  run: async (ctx) => {
9952
9965
  if (!ctx.compactor) {
9953
9966
  ctx.write("compaction unavailable\n");
@@ -10548,6 +10548,8 @@ function haxSpawnEnv(base = process.env, transcriptPath) {
10548
10548
  const env = { ...base, HAX_EXTRA_SKILLS_DIR: aiEzioGlobalSkillsDir(base) };
10549
10549
  if (transcriptPath)
10550
10550
  env.HAX_TRANSCRIPT = transcriptPath;
10551
+ if (env.HAX_COMPACT_AUTO === void 0)
10552
+ env.HAX_COMPACT_AUTO = "0";
10551
10553
  return env;
10552
10554
  }
10553
10555
  function spawnHax(options = {}) {
@@ -10870,7 +10872,9 @@ var Session = class {
10870
10872
  async spawnAndPump(options) {
10871
10873
  this._transcriptPath = options.transcriptPath;
10872
10874
  const gen = this.generation;
10873
- const spawned = (this.options.spawn ?? spawnHax)(options);
10875
+ const overrides = this.options.engineEnvOverrides;
10876
+ const spawnOptions = overrides ? { ...options, env: { ...options.env ?? process.env, ...overrides } } : options;
10877
+ const spawned = (this.options.spawn ?? spawnHax)(spawnOptions);
10874
10878
  this.spawned = spawned;
10875
10879
  spawned.child.on("exit", (code, signal) => {
10876
10880
  if (gen !== this.generation)
@@ -11904,6 +11908,7 @@ function profileEnv(profile, parentEnv) {
11904
11908
  env.HAX_REASONING_EFFORT = profile.effort;
11905
11909
  else
11906
11910
  delete env.HAX_REASONING_EFFORT;
11911
+ env.HAX_COMPACT_AUTO = "1";
11907
11912
  return env;
11908
11913
  }
11909
11914
  function validateProfile(profile, parentEnv) {
@@ -12991,6 +12996,14 @@ function builtinCommands(listCommands) {
12991
12996
  {
12992
12997
  name: "compact",
12993
12998
  summary: "summarize old history and free context",
12999
+ // NOTE (post-hax-sync): upstream hax now ships its own `/compact` slash
13000
+ // command (`agent_compact`) that silently rewrites conversation history
13001
+ // inside the engine. This ezio-owned handler intercepts `/compact` here,
13002
+ // routes it to `ctx.compactor.compactNow()` (the harness-controlled protocol
13003
+ // `compact` control), and returns `action: "handled"` — so the literal text
13004
+ // "/compact" is NEVER forwarded as a raw `submit` to the hax session where
13005
+ // the engine's own slash registry would trigger `agent_compact`.
13006
+ // Do NOT remove this command or change its return path to `action: "submit"`.
12994
13007
  run: async (ctx) => {
12995
13008
  if (!ctx.compactor) {
12996
13009
  ctx.write("compaction unavailable\n");
@@ -13919,10 +13932,10 @@ function compareSemver(a, b) {
13919
13932
 
13920
13933
  // src/generated/ezio-provenance.ts
13921
13934
  var EZIO_PROVENANCE = {
13922
- ezioCliVersion: "0.2.0-beta.5",
13923
- ezioGitSha: "09022cd",
13924
- builtAt: "2026-06-27T22:52:35.353Z",
13925
- whisperVersion: "0.9.0"
13935
+ ezioCliVersion: "0.3.0",
13936
+ ezioGitSha: "ffd95dc",
13937
+ builtAt: "2026-06-30T04:51:15.900Z",
13938
+ whisperVersion: "0.10.0"
13926
13939
  };
13927
13940
 
13928
13941
  // src/ezio-provenance-types.ts
@@ -17533,8 +17546,43 @@ import Database2 from "better-sqlite3";
17533
17546
  import { existsSync as existsSync15 } from "node:fs";
17534
17547
 
17535
17548
  // src/runtime/evaluator-config.ts
17536
- import { readFileSync as readFileSync10, statSync as statSync4 } from "node:fs";
17537
- import { join as join23 } from "node:path";
17549
+ import { readFileSync as readFileSync10, statSync as statSync5 } from "node:fs";
17550
+ import { join as join24 } from "node:path";
17551
+
17552
+ // src/runtime/agent-cli-presets.ts
17553
+ import { spawn as nodeSpawn } from "node:child_process";
17554
+ import { accessSync, constants, statSync as statSync4 } from "node:fs";
17555
+ import { delimiter, join as join23 } from "node:path";
17556
+ var PRESETS = {
17557
+ claude: { executable: "claude", execArgs: ["-p"], promptVia: "arg" },
17558
+ codex: { executable: "codex", execArgs: ["exec"], promptVia: "arg" },
17559
+ agy: { executable: "agy", execArgs: ["-p"], promptVia: "arg" }
17560
+ };
17561
+ function resolveAgentCliInvocation(input) {
17562
+ const preset = PRESETS[input.agent];
17563
+ return {
17564
+ executable: input.executable ?? preset.executable,
17565
+ execArgs: input.execArgs ?? preset.execArgs,
17566
+ promptVia: input.promptVia ?? preset.promptVia
17567
+ };
17568
+ }
17569
+ function isExecutableOnPath(executable) {
17570
+ const isExecFile = (p) => {
17571
+ try {
17572
+ if (!statSync4(p).isFile()) return false;
17573
+ accessSync(p, constants.X_OK);
17574
+ return true;
17575
+ } catch {
17576
+ return false;
17577
+ }
17578
+ };
17579
+ if (executable.includes("/") || executable.includes("\\")) return isExecFile(executable);
17580
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
17581
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
17582
+ return dirs.some((dir) => exts.some((ext) => isExecFile(join23(dir, executable + ext))));
17583
+ }
17584
+
17585
+ // src/runtime/evaluator-config.ts
17538
17586
  function parseDotEnv(text) {
17539
17587
  const out = {};
17540
17588
  for (const rawLine of text.split(/\r?\n/)) {
@@ -17556,7 +17604,7 @@ function warnIfLoosePerms(path4) {
17556
17604
  if (process.platform === "win32") return;
17557
17605
  let mode;
17558
17606
  try {
17559
- mode = statSync4(path4).mode;
17607
+ mode = statSync5(path4).mode;
17560
17608
  } catch {
17561
17609
  return;
17562
17610
  }
@@ -17584,31 +17632,46 @@ function loadEvaluatorConfig() {
17584
17632
  const root = getStateRoot();
17585
17633
  let dotenv = {};
17586
17634
  try {
17587
- dotenv = parseDotEnv(readFileSync10(join23(root, ".env"), "utf8"));
17635
+ dotenv = parseDotEnv(readFileSync10(join24(root, ".env"), "utf8"));
17588
17636
  } catch (err) {
17589
17637
  if (err.code !== "ENOENT") throw err;
17590
17638
  }
17591
17639
  const envGet = (k) => process.env[k] ?? dotenv[k];
17592
- const authPath = join23(root, "auth.json");
17640
+ const authPath = join24(root, "auth.json");
17593
17641
  warnIfLoosePerms(authPath);
17594
17642
  const auth = readJsonFile(authPath, "auth.json");
17595
- const config = readJsonFile(join23(root, "config.json"), "config.json");
17643
+ const config = readJsonFile(join24(root, "config.json"), "config.json");
17596
17644
  const evalCfg = config?.evaluator ?? {};
17597
17645
  const evalOllama = evalCfg.ollama ?? {};
17598
- const provider = (envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider) === "ollama" ? "ollama" : "anthropic";
17646
+ const provider = envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider;
17647
+ const resolvedProvider = provider === "ollama" || provider === "openai" || provider === "agent-cli" ? provider : "anthropic";
17599
17648
  const rawFallback = envGet("AI_WHISPER_EVALUATOR_FALLBACK") ?? evalCfg.fallback;
17600
- const fallback = rawFallback === "anthropic" || rawFallback === "ollama" ? rawFallback : null;
17649
+ const fallback = rawFallback === "anthropic" || rawFallback === "ollama" || rawFallback === "openai" || rawFallback === "agent-cli" ? rawFallback : null;
17601
17650
  const apiKey = envGet("ANTHROPIC_API_KEY") ?? auth?.ANTHROPIC_API_KEY ?? null;
17651
+ const evalOpenai = evalCfg.openai ?? {};
17652
+ const openaiKey = envGet("OPENAI_API_KEY") ?? auth?.OPENAI_API_KEY ?? null;
17653
+ const evalAgentCli = evalCfg.agentCli ?? {};
17654
+ const rawAgent = envGet("AI_WHISPER_EVALUATOR_AGENT_CLI_AGENT") ?? evalAgentCli.agent;
17655
+ const agent = rawAgent === "claude" || rawAgent === "codex" || rawAgent === "agy" ? rawAgent : null;
17602
17656
  return {
17603
- provider,
17657
+ provider: resolvedProvider,
17604
17658
  fallback,
17605
- anthropic: {
17606
- apiKey: apiKey && apiKey.length > 0 ? apiKey : null,
17607
- model: evalCfg.anthropicModel ?? null
17608
- },
17659
+ anthropic: { apiKey: apiKey && apiKey.length > 0 ? apiKey : null, model: evalCfg.anthropicModel ?? null },
17609
17660
  ollama: {
17610
17661
  host: envGet("AI_WHISPER_EVALUATOR_OLLAMA_HOST") ?? evalOllama.host ?? null,
17611
17662
  model: envGet("AI_WHISPER_EVALUATOR_OLLAMA_MODEL") ?? evalOllama.model ?? null
17663
+ },
17664
+ openai: {
17665
+ apiKey: openaiKey && openaiKey.length > 0 ? openaiKey : null,
17666
+ model: envGet("AI_WHISPER_EVALUATOR_OPENAI_MODEL") ?? evalOpenai.model ?? null,
17667
+ baseURL: envGet("AI_WHISPER_EVALUATOR_OPENAI_BASE_URL") ?? evalOpenai.baseURL ?? null
17668
+ },
17669
+ agentCli: {
17670
+ agent,
17671
+ executable: evalAgentCli.executable ?? null,
17672
+ execArgs: Array.isArray(evalAgentCli.execArgs) ? evalAgentCli.execArgs : null,
17673
+ promptVia: evalAgentCli.promptVia === "arg" || evalAgentCli.promptVia === "stdin" ? evalAgentCli.promptVia : null,
17674
+ model: evalAgentCli.model ?? null
17612
17675
  }
17613
17676
  };
17614
17677
  }
@@ -17618,13 +17681,28 @@ function computeEvaluatorStatus(cfg, ctx) {
17618
17681
  if (cfg.provider === "anthropic" && cfg.anthropic.apiKey === null) {
17619
17682
  return "missing_anthropic_key";
17620
17683
  }
17684
+ if (cfg.provider === "openai") {
17685
+ if (cfg.openai.apiKey === null) return "missing_openai_key";
17686
+ if (cfg.openai.model === null) return "invalid_config";
17687
+ }
17688
+ if (cfg.provider === "agent-cli") {
17689
+ if (cfg.agentCli.agent === null) return "invalid_config";
17690
+ const { executable } = resolveAgentCliInvocation({
17691
+ agent: cfg.agentCli.agent,
17692
+ executable: cfg.agentCli.executable,
17693
+ execArgs: cfg.agentCli.execArgs,
17694
+ promptVia: cfg.agentCli.promptVia
17695
+ });
17696
+ const exists = (ctx.executableExists ?? isExecutableOnPath)(executable);
17697
+ if (!exists) return "agent_cli_unavailable";
17698
+ }
17621
17699
  return "ready";
17622
17700
  }
17623
17701
  function isEvaluatorReady(status) {
17624
17702
  return status === "ready" || status === "unknown";
17625
17703
  }
17626
17704
  function isEvaluatorPreflightBlocked(status) {
17627
- return status === "missing_anthropic_key" || status === "invalid_config";
17705
+ return status === "missing_anthropic_key" || status === "missing_openai_key" || status === "agent_cli_unavailable" || status === "invalid_config";
17628
17706
  }
17629
17707
 
17630
17708
  // src/commands/collab/status.ts
@@ -17971,7 +18049,7 @@ async function runCollabTell(input) {
17971
18049
  }
17972
18050
 
17973
18051
  // src/runtime/launcher.ts
17974
- import { execSync as execSync3, spawn as nodeSpawn } from "node:child_process";
18052
+ import { execSync as execSync3, spawn as nodeSpawn2 } from "node:child_process";
17975
18053
  import { dirname as dirname8, resolve as resolve4 } from "node:path";
17976
18054
  import { fileURLToPath as fileURLToPath5 } from "node:url";
17977
18055
  var __dirname = dirname8(fileURLToPath5(import.meta.url));
@@ -18017,7 +18095,7 @@ function buildRelayMonitorCommand(envPrefix, workspaceRoot) {
18017
18095
  return `AI_WHISPER_WORKSPACE_ROOT=${shellQuote(workspaceRoot)} ${envPrefix} ${shellQuote(process.execPath)} ${shellQuote(relayMonitorBinPath)}`;
18018
18096
  }
18019
18097
  function defaultSpawn(command) {
18020
- const child = nodeSpawn("sh", ["-c", command], {
18098
+ const child = nodeSpawn2("sh", ["-c", command], {
18021
18099
  detached: true,
18022
18100
  stdio: "ignore"
18023
18101
  });
@@ -18116,9 +18194,17 @@ async function runWorkflowStart(deps) {
18116
18194
  throw new Error(
18117
18195
  'Evaluator is not configured: ANTHROPIC_API_KEY is missing. Add it to ~/.ai-whisper/auth.json as { "ANTHROPIC_API_KEY": "sk-ant-..." } (mode 600), then restart the daemon: whisper collab stop and re-mount. ' + EVALUATOR_README_HINT
18118
18196
  );
18197
+ } else if (evaluatorStatus === "missing_openai_key") {
18198
+ throw new Error(
18199
+ 'Evaluator is not configured: OPENAI_API_KEY is missing. Add it to ~/.ai-whisper/auth.json as { "OPENAI_API_KEY": "sk-..." } (mode 600) or export OPENAI_API_KEY, then restart the daemon: whisper collab stop and re-mount. ' + EVALUATOR_README_HINT
18200
+ );
18201
+ } else if (evaluatorStatus === "agent_cli_unavailable") {
18202
+ throw new Error(
18203
+ "Evaluator is not configured: the agent-cli executable was not found on PATH. Install/mount the agent CLI (claude/codex/agy) or set evaluator.agentCli.executable to its absolute path, then restart the daemon: whisper collab stop and re-mount. " + EVALUATOR_README_HINT
18204
+ );
18119
18205
  } else {
18120
18206
  throw new Error(
18121
- "Evaluator configuration is invalid: ~/.ai-whisper/auth.json or config.json contains malformed JSON. Fix the file, then restart the daemon: whisper collab stop and re-mount. " + EVALUATOR_README_HINT
18207
+ "Evaluator configuration is invalid: a required setting is missing or ~/.ai-whisper/auth.json / config.json contains malformed JSON. For provider=openai set evaluator.openai.model (AI_WHISPER_EVALUATOR_OPENAI_MODEL); for provider=agent-cli set the agent (AI_WHISPER_EVALUATOR_AGENT_CLI_AGENT). Fix the config, then restart the daemon: whisper collab stop and re-mount. " + EVALUATOR_README_HINT
18122
18208
  );
18123
18209
  }
18124
18210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-whisper",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Terminal-first relay for paired AI coding agents (Claude + Codex), driven by structured workflows.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,6 +43,7 @@
43
43
  "nanoid": "^5.1.5",
44
44
  "node-pty": "^1.1.0",
45
45
  "ollama": "^0.6.3",
46
+ "openai": "^6.45.0",
46
47
  "react": "^19.2.6",
47
48
  "string-width": "^8",
48
49
  "zod": "^3.24.2"
@@ -52,10 +53,10 @@
52
53
  "@types/react": "^19.2.14",
53
54
  "ink-testing-library": "^4.0.0",
54
55
  "@ai-whisper/adapter-antigravity": "0.0.0",
55
- "@ai-whisper/adapter-claude": "0.0.0",
56
56
  "@ai-whisper/adapter-codex": "0.0.0",
57
- "@ai-whisper/broker": "0.0.0",
57
+ "@ai-whisper/adapter-claude": "0.0.0",
58
58
  "@ai-whisper/adapter-ai-ezio": "0.0.0",
59
+ "@ai-whisper/broker": "0.0.0",
59
60
  "@ai-whisper/companion-core": "0.0.0",
60
61
  "@ai-whisper/shared": "0.0.0"
61
62
  },