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 +1 -1
- package/dist/bin/broker-daemon.js +223 -19
- package/dist/bin/companion-agent.js +14 -1
- package/dist/bin/whisper.js +108 -22
- package/package.json +4 -3
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
|
|
6714
|
-
import { join as
|
|
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 =
|
|
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(
|
|
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 =
|
|
6933
|
+
const authPath = join7(root, "auth.json");
|
|
6782
6934
|
warnIfLoosePerms(authPath);
|
|
6783
6935
|
const auth = readJsonFile(authPath, "auth.json");
|
|
6784
|
-
const config = readJsonFile(
|
|
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 =
|
|
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
|
|
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:
|
|
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: "
|
|
6910
|
-
|
|
6911
|
-
...resolved
|
|
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
|
|
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");
|
package/dist/bin/whisper.js
CHANGED
|
@@ -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
|
|
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.
|
|
13923
|
-
ezioGitSha: "
|
|
13924
|
-
builtAt: "2026-06-
|
|
13925
|
-
whisperVersion: "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
|
|
17537
|
-
import { join as
|
|
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 =
|
|
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(
|
|
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 =
|
|
17640
|
+
const authPath = join24(root, "auth.json");
|
|
17593
17641
|
warnIfLoosePerms(authPath);
|
|
17594
17642
|
const auth = readJsonFile(authPath, "auth.json");
|
|
17595
|
-
const config = readJsonFile(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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/
|
|
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
|
},
|