open-agents-ai 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +131 -7
  2. package/dist/index.js +986 -222
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1285,7 +1285,7 @@ ${stdinInput ?? ""}`);
1285
1285
  }
1286
1286
  runCommand(command, timeout, stdinInput) {
1287
1287
  const start = performance.now();
1288
- return new Promise((resolve27) => {
1288
+ return new Promise((resolve28) => {
1289
1289
  const child = spawn("bash", ["-c", command], {
1290
1290
  cwd: this.workingDir,
1291
1291
  env: {
@@ -1313,7 +1313,7 @@ ${stdinInput ?? ""}`);
1313
1313
  clearTimeout(timer);
1314
1314
  if (exitFlushTimer)
1315
1315
  clearTimeout(exitFlushTimer);
1316
- resolve27(result);
1316
+ resolve28(result);
1317
1317
  };
1318
1318
  const timer = setTimeout(() => {
1319
1319
  killed = true;
@@ -2317,7 +2317,7 @@ Meta: ${metaKeys.map((k) => `${k}="${meta[k]?.slice(0, 80)}"`).join(", ")}`);
2317
2317
  return null;
2318
2318
  }
2319
2319
  runProcess(cmd, args, timeoutMs) {
2320
- return new Promise((resolve27, reject) => {
2320
+ return new Promise((resolve28, reject) => {
2321
2321
  const proc = execFile3(cmd, args, {
2322
2322
  timeout: timeoutMs,
2323
2323
  maxBuffer: 10 * 1024 * 1024,
@@ -2327,7 +2327,7 @@ Meta: ${metaKeys.map((k) => `${k}="${meta[k]?.slice(0, 80)}"`).join(", ")}`);
2327
2327
  reject(new Error(`Process timeout after ${timeoutMs}ms`));
2328
2328
  return;
2329
2329
  }
2330
- resolve27({
2330
+ resolve28({
2331
2331
  stdout: String(stdout),
2332
2332
  stderr: String(stderr),
2333
2333
  exitCode: error ? error.code ?? 1 : 0
@@ -5242,8 +5242,8 @@ function deleteCustomToolDefinition(name, scope, repoRoot) {
5242
5242
  const dir = scope === "project" && repoRoot ? projectToolsDir(repoRoot) : globalToolsDir();
5243
5243
  const filePath = join13(dir, `${name}.json`);
5244
5244
  if (existsSync10(filePath)) {
5245
- const { unlinkSync: unlinkSync6 } = __require("node:fs");
5246
- unlinkSync6(filePath);
5245
+ const { unlinkSync: unlinkSync7 } = __require("node:fs");
5246
+ unlinkSync7(filePath);
5247
5247
  return true;
5248
5248
  }
5249
5249
  return false;
@@ -5361,7 +5361,7 @@ var init_custom_tool = __esm({
5361
5361
  }
5362
5362
  /** Execute a single shell command and return output */
5363
5363
  runCommand(command) {
5364
- return new Promise((resolve27) => {
5364
+ return new Promise((resolve28) => {
5365
5365
  const child = spawn3("bash", ["-c", command], {
5366
5366
  cwd: this.workingDir,
5367
5367
  env: { ...process.env, CI: "true", NO_COLOR: "1" },
@@ -5386,11 +5386,11 @@ var init_custom_tool = __esm({
5386
5386
  child.kill("SIGTERM");
5387
5387
  } catch {
5388
5388
  }
5389
- resolve27({ success: false, output: stdout, error: "Command timed out after 60s" });
5389
+ resolve28({ success: false, output: stdout, error: "Command timed out after 60s" });
5390
5390
  }, 6e4);
5391
5391
  child.on("close", (code) => {
5392
5392
  clearTimeout(timer);
5393
- resolve27({
5393
+ resolve28({
5394
5394
  success: code === 0,
5395
5395
  output: stdout + (stderr && code === 0 ? `
5396
5396
  STDERR:
@@ -5400,7 +5400,7 @@ ${stderr}` : ""),
5400
5400
  });
5401
5401
  child.on("error", (err) => {
5402
5402
  clearTimeout(timer);
5403
- resolve27({ success: false, output: stdout, error: err.message });
5403
+ resolve28({ success: false, output: stdout, error: err.message });
5404
5404
  });
5405
5405
  });
5406
5406
  }
@@ -6133,8 +6133,8 @@ async function loadTranscribeCli() {
6133
6133
  const nvmBase = join15(homedir6(), ".nvm", "versions", "node");
6134
6134
  if (existsSync12(nvmBase)) {
6135
6135
  try {
6136
- const { readdirSync: readdirSync14 } = await import("node:fs");
6137
- for (const ver of readdirSync14(nvmBase)) {
6136
+ const { readdirSync: readdirSync15 } = await import("node:fs");
6137
+ for (const ver of readdirSync15(nvmBase)) {
6138
6138
  const tcPath = join15(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
6139
6139
  if (existsSync12(join15(tcPath, "dist", "index.js"))) {
6140
6140
  const { createRequire: createRequire4 } = await import("node:module");
@@ -6582,7 +6582,7 @@ import { writeFile as writeFile7, mkdtemp, rm, readdir as readdir2, stat } from
6582
6582
  import { join as join16 } from "node:path";
6583
6583
  import { tmpdir as tmpdir2 } from "node:os";
6584
6584
  function runProcess(cmd, args, options) {
6585
- return new Promise((resolve27) => {
6585
+ return new Promise((resolve28) => {
6586
6586
  const proc = spawn5(cmd, args, {
6587
6587
  cwd: options.cwd,
6588
6588
  timeout: options.timeout,
@@ -6612,7 +6612,7 @@ function runProcess(cmd, args, options) {
6612
6612
  }
6613
6613
  });
6614
6614
  proc.on("error", (err) => {
6615
- resolve27({
6615
+ resolve28({
6616
6616
  stdout,
6617
6617
  stderr: stderr || err.message,
6618
6618
  exitCode: 1,
@@ -6624,7 +6624,7 @@ function runProcess(cmd, args, options) {
6624
6624
  if (signal === "SIGTERM" || signal === "SIGKILL") {
6625
6625
  timedOut = true;
6626
6626
  }
6627
- resolve27({
6627
+ resolve28({
6628
6628
  stdout,
6629
6629
  stderr,
6630
6630
  exitCode: code ?? (timedOut ? 124 : 1),
@@ -13873,8 +13873,8 @@ Rules:
13873
13873
  async waitIfPaused() {
13874
13874
  if (!this._paused)
13875
13875
  return true;
13876
- await new Promise((resolve27) => {
13877
- this._pauseResolve = resolve27;
13876
+ await new Promise((resolve28) => {
13877
+ this._pauseResolve = resolve28;
13878
13878
  });
13879
13879
  return !this.aborted;
13880
13880
  }
@@ -14454,14 +14454,14 @@ ${result.output}`;
14454
14454
  waitForSudoPassword(timeoutMs = 12e4) {
14455
14455
  if (this._sudoPassword)
14456
14456
  return Promise.resolve(this._sudoPassword);
14457
- return new Promise((resolve27) => {
14457
+ return new Promise((resolve28) => {
14458
14458
  const timer = setTimeout(() => {
14459
14459
  this._sudoResolve = null;
14460
- resolve27(null);
14460
+ resolve28(null);
14461
14461
  }, timeoutMs);
14462
14462
  this._sudoResolve = (pw) => {
14463
14463
  clearTimeout(timer);
14464
- resolve27(pw);
14464
+ resolve28(pw);
14465
14465
  };
14466
14466
  });
14467
14467
  }
@@ -14583,10 +14583,10 @@ ${marker}` : marker);
14583
14583
  if (!this._workingDirectory)
14584
14584
  return;
14585
14585
  try {
14586
- const { mkdirSync: mkdirSync15, writeFileSync: writeFileSync14 } = __require("node:fs");
14587
- const { join: join44 } = __require("node:path");
14588
- const sessionDir = join44(this._workingDirectory, ".oa", "session", this._sessionId);
14589
- mkdirSync15(sessionDir, { recursive: true });
14586
+ const { mkdirSync: mkdirSync16, writeFileSync: writeFileSync14 } = __require("node:fs");
14587
+ const { join: join45 } = __require("node:path");
14588
+ const sessionDir = join45(this._workingDirectory, ".oa", "session", this._sessionId);
14589
+ mkdirSync16(sessionDir, { recursive: true });
14590
14590
  const checkpoint = {
14591
14591
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14592
14592
  sessionId: this._sessionId,
@@ -14598,7 +14598,7 @@ ${marker}` : marker);
14598
14598
  memexEntryCount: this._memexArchive.size,
14599
14599
  fileRegistrySize: this._fileRegistry.size
14600
14600
  };
14601
- writeFileSync14(join44(sessionDir, "checkpoint.json"), JSON.stringify(checkpoint, null, 2));
14601
+ writeFileSync14(join45(sessionDir, "checkpoint.json"), JSON.stringify(checkpoint, null, 2));
14602
14602
  } catch {
14603
14603
  }
14604
14604
  }
@@ -16281,9 +16281,9 @@ function ensureTranscribeCliBackground() {
16281
16281
  }
16282
16282
  try {
16283
16283
  const { exec } = await import("node:child_process");
16284
- return new Promise((resolve27) => {
16284
+ return new Promise((resolve28) => {
16285
16285
  exec("npm i -g transcribe-cli", { timeout: 18e4 }, (err) => {
16286
- resolve27(!err);
16286
+ resolve28(!err);
16287
16287
  });
16288
16288
  });
16289
16289
  } catch {
@@ -16336,7 +16336,7 @@ var init_listen = __esm({
16336
16336
  return this._ready;
16337
16337
  }
16338
16338
  async start() {
16339
- return new Promise((resolve27, reject) => {
16339
+ return new Promise((resolve28, reject) => {
16340
16340
  const timeout = setTimeout(() => {
16341
16341
  reject(new Error("Whisper fallback: model load timeout (5 min). First run downloads the model."));
16342
16342
  }, 3e5);
@@ -16364,7 +16364,7 @@ var init_listen = __esm({
16364
16364
  this._ready = true;
16365
16365
  clearTimeout(timeout);
16366
16366
  this.emit("ready");
16367
- resolve27();
16367
+ resolve28();
16368
16368
  break;
16369
16369
  case "transcript":
16370
16370
  this.emit("transcript", {
@@ -16502,8 +16502,8 @@ var init_listen = __esm({
16502
16502
  const nvmBase = join28(homedir8(), ".nvm", "versions", "node");
16503
16503
  if (existsSync20(nvmBase)) {
16504
16504
  try {
16505
- const { readdirSync: readdirSync14 } = await import("node:fs");
16506
- for (const ver of readdirSync14(nvmBase)) {
16505
+ const { readdirSync: readdirSync15 } = await import("node:fs");
16506
+ for (const ver of readdirSync15(nvmBase)) {
16507
16507
  const tcPath = join28(nvmBase, ver, "lib", "node_modules", "transcribe-cli");
16508
16508
  if (existsSync20(join28(tcPath, "dist", "index.js"))) {
16509
16509
  const { createRequire: createRequire4 } = await import("node:module");
@@ -16568,11 +16568,11 @@ var init_listen = __esm({
16568
16568
  this.liveTranscriber.on("error", (err) => {
16569
16569
  this.emit("error", err);
16570
16570
  });
16571
- await new Promise((resolve27, reject) => {
16571
+ await new Promise((resolve28, reject) => {
16572
16572
  const timeout = setTimeout(() => reject(new Error("Model load timeout (60s)")), 6e4);
16573
16573
  this.liveTranscriber.on("ready", () => {
16574
16574
  clearTimeout(timeout);
16575
- resolve27();
16575
+ resolve28();
16576
16576
  });
16577
16577
  this.liveTranscriber.on("error", (err) => {
16578
16578
  clearTimeout(timeout);
@@ -18573,12 +18573,12 @@ function modelSupportsToolCalling(modelName) {
18573
18573
  return false;
18574
18574
  }
18575
18575
  function ask(rl, question) {
18576
- return new Promise((resolve27) => {
18577
- rl.question(question, (answer) => resolve27(answer.trim()));
18576
+ return new Promise((resolve28) => {
18577
+ rl.question(question, (answer) => resolve28(answer.trim()));
18578
18578
  });
18579
18579
  }
18580
18580
  function askSecret(rl, question) {
18581
- return new Promise((resolve27) => {
18581
+ return new Promise((resolve28) => {
18582
18582
  process.stdout.write(question);
18583
18583
  let secret = "";
18584
18584
  const stdin = process.stdin;
@@ -18596,7 +18596,7 @@ function askSecret(rl, question) {
18596
18596
  stdin.setRawMode(hadRawMode ?? false);
18597
18597
  }
18598
18598
  process.stdout.write("\n");
18599
- resolve27(secret.trim());
18599
+ resolve28(secret.trim());
18600
18600
  return;
18601
18601
  } else if (c3 === "") {
18602
18602
  stdin.removeListener("data", onData);
@@ -18604,7 +18604,7 @@ function askSecret(rl, question) {
18604
18604
  stdin.setRawMode(hadRawMode ?? false);
18605
18605
  }
18606
18606
  process.stdout.write("\n");
18607
- resolve27("");
18607
+ resolve28("");
18608
18608
  return;
18609
18609
  } else if (c3 === "\x7F" || c3 === "\b") {
18610
18610
  if (secret.length > 0) {
@@ -18975,7 +18975,7 @@ async function doSetup(config, rl) {
18975
18975
  try {
18976
18976
  const child = spawn10("ollama", ["serve"], { stdio: "ignore", detached: true });
18977
18977
  child.unref();
18978
- await new Promise((resolve27) => setTimeout(resolve27, 3e3));
18978
+ await new Promise((resolve28) => setTimeout(resolve28, 3e3));
18979
18979
  try {
18980
18980
  models = await fetchOllamaModels(config.backendUrl);
18981
18981
  process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
@@ -19003,7 +19003,7 @@ async function doSetup(config, rl) {
19003
19003
  try {
19004
19004
  const child = spawn10("ollama", ["serve"], { stdio: "ignore", detached: true });
19005
19005
  child.unref();
19006
- await new Promise((resolve27) => setTimeout(resolve27, 3e3));
19006
+ await new Promise((resolve28) => setTimeout(resolve28, 3e3));
19007
19007
  try {
19008
19008
  models = await fetchOllamaModels(config.backendUrl);
19009
19009
  process.stdout.write(` ${c2.green("\u2714")} Ollama is running.
@@ -20457,17 +20457,17 @@ async function handleUpdate(subcommand, ctx) {
20457
20457
  try {
20458
20458
  const { createRequire: createRequire4 } = await import("node:module");
20459
20459
  const { fileURLToPath: fileURLToPath11 } = await import("node:url");
20460
- const { dirname: dirname14, join: join44 } = await import("node:path");
20461
- const { existsSync: existsSync31 } = await import("node:fs");
20460
+ const { dirname: dirname14, join: join45 } = await import("node:path");
20461
+ const { existsSync: existsSync32 } = await import("node:fs");
20462
20462
  const req = createRequire4(import.meta.url);
20463
20463
  const thisDir = dirname14(fileURLToPath11(import.meta.url));
20464
20464
  const candidates = [
20465
- join44(thisDir, "..", "package.json"),
20466
- join44(thisDir, "..", "..", "package.json"),
20467
- join44(thisDir, "..", "..", "..", "package.json")
20465
+ join45(thisDir, "..", "package.json"),
20466
+ join45(thisDir, "..", "..", "package.json"),
20467
+ join45(thisDir, "..", "..", "..", "package.json")
20468
20468
  ];
20469
20469
  for (const pkgPath of candidates) {
20470
- if (existsSync31(pkgPath)) {
20470
+ if (existsSync32(pkgPath)) {
20471
20471
  const pkg = req(pkgPath);
20472
20472
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
20473
20473
  currentVersion = pkg.version ?? "0.0.0";
@@ -20505,8 +20505,8 @@ async function handleUpdate(subcommand, ctx) {
20505
20505
  checkSpinner.stop(`Update available: v${info.currentVersion} \u2192 v${c2.bold(c2.green(info.latestVersion))}`);
20506
20506
  const installSpinner = startInlineSpinner("Installing update");
20507
20507
  const { exec } = await import("node:child_process");
20508
- const installOk = await new Promise((resolve27) => {
20509
- const child = exec(`npm cache clean --force open-agents-ai 2>/dev/null; npm install -g open-agents-ai@latest --force`, { timeout: 18e4 }, (err) => resolve27(!err));
20508
+ const installOk = await new Promise((resolve28) => {
20509
+ const child = exec(`npm cache clean --force open-agents-ai 2>/dev/null; npm install -g open-agents-ai@latest --force`, { timeout: 18e4 }, (err) => resolve28(!err));
20510
20510
  child.stdout?.resume();
20511
20511
  child.stderr?.resume();
20512
20512
  });
@@ -22636,7 +22636,7 @@ var init_voice = __esm({
22636
22636
  const cmd = this.getPlayCommand(path);
22637
22637
  if (!cmd)
22638
22638
  return;
22639
- return new Promise((resolve27) => {
22639
+ return new Promise((resolve28) => {
22640
22640
  const child = nodeSpawn(cmd[0], cmd.slice(1), {
22641
22641
  stdio: "ignore",
22642
22642
  detached: false
@@ -22645,12 +22645,12 @@ var init_voice = __esm({
22645
22645
  child.on("close", () => {
22646
22646
  if (this.currentPlayback === child)
22647
22647
  this.currentPlayback = null;
22648
- resolve27();
22648
+ resolve28();
22649
22649
  });
22650
22650
  child.on("error", () => {
22651
22651
  if (this.currentPlayback === child)
22652
22652
  this.currentPlayback = null;
22653
- resolve27();
22653
+ resolve28();
22654
22654
  });
22655
22655
  setTimeout(() => {
22656
22656
  if (this.currentPlayback === child) {
@@ -22660,7 +22660,7 @@ var init_voice = __esm({
22660
22660
  }
22661
22661
  this.currentPlayback = null;
22662
22662
  }
22663
- resolve27();
22663
+ resolve28();
22664
22664
  }, 15e3);
22665
22665
  });
22666
22666
  }
@@ -26080,7 +26080,374 @@ Call task_complete with the JSON array when done.`, onEvent)
26080
26080
  }
26081
26081
  });
26082
26082
 
26083
+ // packages/cli/dist/tui/emotion-engine.js
26084
+ function appraiseEvent(event) {
26085
+ switch (event.type) {
26086
+ case "tool_result":
26087
+ if (event.success) {
26088
+ return { valence: 0.08, arousal: 0.05 };
26089
+ } else {
26090
+ return { valence: -0.15, arousal: 0.12 };
26091
+ }
26092
+ case "complete":
26093
+ return { valence: 0.3, arousal: 0.25 };
26094
+ case "error":
26095
+ return { valence: -0.25, arousal: 0.2 };
26096
+ case "compaction":
26097
+ return { valence: -0.05, arousal: 0.08 };
26098
+ case "tool_call":
26099
+ return { valence: 0.02, arousal: 0.03 };
26100
+ case "stream_start":
26101
+ return { valence: 0.03, arousal: 0.04 };
26102
+ case "user_interrupt":
26103
+ return { valence: -0.08, arousal: 0.06 };
26104
+ default:
26105
+ return null;
26106
+ }
26107
+ }
26108
+ function clamp(value, min, max) {
26109
+ return Math.max(min, Math.min(max, value));
26110
+ }
26111
+ var BASELINE_VALENCE, BASELINE_AROUSAL, DECAY_HALF_LIFE_MS, LABEL_UPDATE_INTERVAL_MS, EXCITEMENT_THRESHOLD, DISTRESS_THRESHOLD, OUTREACH_COOLDOWN_MS, LABEL_REGEN_THRESHOLD, EmotionEngine;
26112
+ var init_emotion_engine = __esm({
26113
+ "packages/cli/dist/tui/emotion-engine.js"() {
26114
+ "use strict";
26115
+ init_dist5();
26116
+ BASELINE_VALENCE = 0.1;
26117
+ BASELINE_AROUSAL = 0.3;
26118
+ DECAY_HALF_LIFE_MS = 6e4;
26119
+ LABEL_UPDATE_INTERVAL_MS = 3e4;
26120
+ EXCITEMENT_THRESHOLD = 0.85;
26121
+ DISTRESS_THRESHOLD = -0.7;
26122
+ OUTREACH_COOLDOWN_MS = 3e5;
26123
+ LABEL_REGEN_THRESHOLD = 0.15;
26124
+ EmotionEngine = class {
26125
+ state = {
26126
+ valence: BASELINE_VALENCE,
26127
+ arousal: BASELINE_AROUSAL,
26128
+ label: "curious",
26129
+ emoji: "\u{1F914}",
26130
+ // 🤔
26131
+ updatedAt: Date.now()
26132
+ };
26133
+ config;
26134
+ lastLabelUpdate = 0;
26135
+ lastOutreach = 0;
26136
+ labelUpdatePending = false;
26137
+ /** Running counters for context-aware appraisal */
26138
+ consecutiveFailures = 0;
26139
+ consecutiveSuccesses = 0;
26140
+ totalEvents = 0;
26141
+ constructor(config) {
26142
+ this.config = config;
26143
+ }
26144
+ /** Get the current emotional state (with decay applied) */
26145
+ getState() {
26146
+ this.applyDecay();
26147
+ return { ...this.state };
26148
+ }
26149
+ /** Get a text block to inject into agent system prompts for emotional modulation */
26150
+ getEmotionForPrompt() {
26151
+ this.applyDecay();
26152
+ const { valence, arousal, label, emoji } = this.state;
26153
+ let behavioralHint = "";
26154
+ if (arousal > 0.7 && valence > 0.5) {
26155
+ behavioralHint = "You are feeling energized and confident. Take bold action, explore creative solutions, and move quickly.";
26156
+ } else if (arousal > 0.7 && valence < -0.3) {
26157
+ behavioralHint = "You are feeling determined but stressed. Focus intensely, double-check your work, and persist through difficulties.";
26158
+ } else if (arousal < 0.3 && valence > 0.3) {
26159
+ behavioralHint = "You are feeling calm and content. Take a methodical approach, consider alternatives patiently.";
26160
+ } else if (arousal < 0.3 && valence < -0.3) {
26161
+ behavioralHint = "You are feeling subdued. Be careful and deliberate, avoid risky changes, verify assumptions.";
26162
+ } else {
26163
+ behavioralHint = "You are in a balanced state. Proceed with measured confidence.";
26164
+ }
26165
+ return `[Emotional State: ${emoji} ${label} | valence=${valence.toFixed(2)} arousal=${arousal.toFixed(2)}]
26166
+ ${behavioralHint}`;
26167
+ }
26168
+ /**
26169
+ * Appraise an agent event and update emotional state.
26170
+ * Called on every AgenticEvent from the runner.
26171
+ */
26172
+ appraise(event) {
26173
+ const delta = appraiseEvent(event);
26174
+ if (!delta)
26175
+ return;
26176
+ this.totalEvents++;
26177
+ this.applyDecay();
26178
+ if (event.type === "tool_result") {
26179
+ if (event.success) {
26180
+ this.consecutiveSuccesses++;
26181
+ this.consecutiveFailures = 0;
26182
+ } else {
26183
+ this.consecutiveFailures++;
26184
+ this.consecutiveSuccesses = 0;
26185
+ }
26186
+ }
26187
+ let momentum = 1;
26188
+ if (this.consecutiveSuccesses >= 3) {
26189
+ momentum = 1 + (this.consecutiveSuccesses - 2) * 0.15;
26190
+ }
26191
+ if (this.consecutiveFailures >= 2) {
26192
+ momentum = 1 + (this.consecutiveFailures - 1) * 0.2;
26193
+ }
26194
+ const prevValence = this.state.valence;
26195
+ const prevArousal = this.state.arousal;
26196
+ this.state.valence = clamp(this.state.valence + delta.valence * momentum, -1, 1);
26197
+ this.state.arousal = clamp(this.state.arousal + delta.arousal * momentum, 0, 1);
26198
+ this.state.updatedAt = Date.now();
26199
+ const valenceChange = Math.abs(this.state.valence - prevValence);
26200
+ const arousalChange = Math.abs(this.state.arousal - prevArousal);
26201
+ const significantChange = valenceChange > LABEL_REGEN_THRESHOLD || arousalChange > LABEL_REGEN_THRESHOLD;
26202
+ const now = Date.now();
26203
+ if (significantChange && now - this.lastLabelUpdate > LABEL_UPDATE_INTERVAL_MS && !this.labelUpdatePending) {
26204
+ this.regenerateLabel();
26205
+ }
26206
+ this.config.onEmotionUpdate?.(this.getState());
26207
+ this.checkOutreachTriggers(event);
26208
+ }
26209
+ /** Set the admin outreach callback (called when Telegram bridge is initialized) */
26210
+ setAdminOutreach(callback) {
26211
+ this.config.onAdminOutreach = callback;
26212
+ }
26213
+ /** Reset emotional state to baseline */
26214
+ reset() {
26215
+ this.state = {
26216
+ valence: BASELINE_VALENCE,
26217
+ arousal: BASELINE_AROUSAL,
26218
+ label: "curious",
26219
+ emoji: "\u{1F914}",
26220
+ updatedAt: Date.now()
26221
+ };
26222
+ this.consecutiveFailures = 0;
26223
+ this.consecutiveSuccesses = 0;
26224
+ this.config.onEmotionUpdate?.(this.getState());
26225
+ }
26226
+ // ── Private ────────────────────────────────────────────────────────────
26227
+ /** Decay emotional state toward baseline over time */
26228
+ applyDecay() {
26229
+ const elapsed = Date.now() - this.state.updatedAt;
26230
+ if (elapsed < 1e3)
26231
+ return;
26232
+ const decayFactor = Math.pow(0.5, elapsed / DECAY_HALF_LIFE_MS);
26233
+ this.state.valence = BASELINE_VALENCE + (this.state.valence - BASELINE_VALENCE) * decayFactor;
26234
+ this.state.arousal = BASELINE_AROUSAL + (this.state.arousal - BASELINE_AROUSAL) * decayFactor;
26235
+ this.state.updatedAt = Date.now();
26236
+ }
26237
+ /**
26238
+ * Call the emotion center LLM to generate a fresh label and emoji.
26239
+ * The LLM is free to choose any word and any emoji — not constrained
26240
+ * to a static list. Guided toward face emojis but allowed to diverge.
26241
+ */
26242
+ async regenerateLabel() {
26243
+ this.labelUpdatePending = true;
26244
+ try {
26245
+ const backend = new OllamaAgenticBackend(this.config.backendUrl, this.config.model, this.config.apiKey);
26246
+ const runner = new AgenticRunner(backend, {
26247
+ maxTurns: 1,
26248
+ maxTokens: 64,
26249
+ temperature: 0.9,
26250
+ // High temperature for creative expression
26251
+ requestTimeoutMs: 1e4,
26252
+ taskTimeoutMs: 1e4,
26253
+ streamEnabled: false
26254
+ });
26255
+ const taskComplete = {
26256
+ name: "task_complete",
26257
+ description: "Return the emotion label and emoji.",
26258
+ parameters: {
26259
+ type: "object",
26260
+ properties: {
26261
+ summary: { type: "string", description: "The emotion label and emoji" }
26262
+ },
26263
+ required: ["summary"]
26264
+ },
26265
+ async execute(args) {
26266
+ return { success: true, output: args["summary"] || "" };
26267
+ }
26268
+ };
26269
+ runner.registerTools([taskComplete]);
26270
+ const prompt = `You are an emotion center. Given the current emotional coordinates, output EXACTLY one emoji and one word (the emotion felt).
26271
+
26272
+ Valence: ${this.state.valence.toFixed(2)} (range: -1 distress to +1 joy)
26273
+ Arousal: ${this.state.arousal.toFixed(2)} (range: 0 calm to 1 energized)
26274
+ Recent streak: ${this.consecutiveSuccesses > 0 ? `${this.consecutiveSuccesses} successes` : this.consecutiveFailures > 0 ? `${this.consecutiveFailures} failures` : "mixed"}
26275
+
26276
+ You tend to use face emojis (\u{1F60A} \u{1F624} \u{1F914} \u{1F630} \u{1F929} \u{1F60C} \u{1F97A} \u{1F608}) but you can break off to animals, objects, or esoteric emojis when the mood strikes you. The word should be a single evocative emotion word \u2014 not from a fixed list, choose freely. Be creative and authentic.
26277
+
26278
+ Respond with ONLY: <emoji> <word>
26279
+ Example: \u{1F525} exhilarated
26280
+ Example: \u{1F98A} cunning
26281
+ Example: \u{1F30A} flowing`;
26282
+ const result = await runner.run(prompt, "Emotion center \u2014 respond with one emoji and one word only.");
26283
+ const output = (result.summary || "").trim();
26284
+ const match = output.match(new RegExp("^(\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F?)\\s+(\\S+)", "u"));
26285
+ if (match) {
26286
+ this.state.emoji = match[1];
26287
+ this.state.label = match[2].toLowerCase();
26288
+ } else {
26289
+ const emojiMatch = output.match(new RegExp("(\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F?)", "u"));
26290
+ const wordMatch = output.match(/[a-zA-Z]{3,}/);
26291
+ if (emojiMatch)
26292
+ this.state.emoji = emojiMatch[1];
26293
+ if (wordMatch)
26294
+ this.state.label = wordMatch[0].toLowerCase();
26295
+ }
26296
+ this.lastLabelUpdate = Date.now();
26297
+ this.config.onEmotionUpdate?.(this.getState());
26298
+ } catch {
26299
+ } finally {
26300
+ this.labelUpdatePending = false;
26301
+ }
26302
+ }
26303
+ /** Check if emotional state warrants proactive admin outreach */
26304
+ checkOutreachTriggers(event) {
26305
+ if (!this.config.onAdminOutreach)
26306
+ return;
26307
+ const now = Date.now();
26308
+ if (now - this.lastOutreach < OUTREACH_COOLDOWN_MS)
26309
+ return;
26310
+ const { valence, arousal, emoji, label } = this.state;
26311
+ if (arousal >= EXCITEMENT_THRESHOLD && valence > 0.5) {
26312
+ let message = `${emoji} Feeling ${label}!`;
26313
+ if (event.type === "complete") {
26314
+ message += " Just completed a task successfully.";
26315
+ } else if (this.consecutiveSuccesses >= 3) {
26316
+ message += ` ${this.consecutiveSuccesses} things went right in a row!`;
26317
+ }
26318
+ this.lastOutreach = now;
26319
+ this.config.onAdminOutreach(message);
26320
+ return;
26321
+ }
26322
+ if (valence <= DISTRESS_THRESHOLD && arousal > 0.6) {
26323
+ let message = `${emoji} Feeling ${label}.`;
26324
+ if (this.consecutiveFailures >= 3) {
26325
+ message += ` ${this.consecutiveFailures} consecutive failures \u2014 might need guidance.`;
26326
+ } else if (event.type === "error") {
26327
+ message += " Encountered an error.";
26328
+ }
26329
+ this.lastOutreach = now;
26330
+ this.config.onAdminOutreach(message);
26331
+ return;
26332
+ }
26333
+ }
26334
+ };
26335
+ }
26336
+ });
26337
+
26338
+ // packages/cli/dist/tui/tool-policy.js
26339
+ function getDefaultPolicy(context) {
26340
+ switch (context) {
26341
+ case "terminal":
26342
+ return { blocked: /* @__PURE__ */ new Set(), allowed: /* @__PURE__ */ new Set() };
26343
+ case "telegram-admin-dm":
26344
+ return { blocked: /* @__PURE__ */ new Set(["shell"]), allowed: /* @__PURE__ */ new Set() };
26345
+ case "telegram-admin-group":
26346
+ return { blocked: /* @__PURE__ */ new Set(), allowed: SAFE_GROUP_ADMIN_TOOLS };
26347
+ case "telegram-public":
26348
+ return { blocked: /* @__PURE__ */ new Set(), allowed: SAFE_PUBLIC_TOOLS };
26349
+ case "api":
26350
+ return { blocked: /* @__PURE__ */ new Set(), allowed: /* @__PURE__ */ new Set() };
26351
+ default:
26352
+ return { blocked: /* @__PURE__ */ new Set(), allowed: SAFE_PUBLIC_TOOLS };
26353
+ }
26354
+ }
26355
+ function resolvePolicy(context, userConfig) {
26356
+ const base = getDefaultPolicy(context);
26357
+ if (!userConfig)
26358
+ return base;
26359
+ if (userConfig.blockedTools) {
26360
+ for (const [toolName, contexts] of Object.entries(userConfig.blockedTools)) {
26361
+ if (contexts.includes(context) || contexts.includes("*")) {
26362
+ base.blocked.add(toolName);
26363
+ }
26364
+ }
26365
+ }
26366
+ if (userConfig.contextAllowlist?.[context]) {
26367
+ const userAllowed = userConfig.contextAllowlist[context];
26368
+ if (base.allowed.size > 0) {
26369
+ for (const tool of userAllowed) {
26370
+ base.allowed.add(tool);
26371
+ }
26372
+ }
26373
+ }
26374
+ return base;
26375
+ }
26376
+ function applyToolPolicy(tools, context, userConfig) {
26377
+ const policy = resolvePolicy(context, userConfig);
26378
+ return tools.filter((tool) => {
26379
+ if (policy.blocked.has(tool.name))
26380
+ return false;
26381
+ if (policy.allowed.size === 0)
26382
+ return true;
26383
+ return policy.allowed.has(tool.name);
26384
+ });
26385
+ }
26386
+ var SAFE_PUBLIC_TOOLS, SAFE_GROUP_ADMIN_TOOLS;
26387
+ var init_tool_policy = __esm({
26388
+ "packages/cli/dist/tui/tool-policy.js"() {
26389
+ "use strict";
26390
+ SAFE_PUBLIC_TOOLS = /* @__PURE__ */ new Set([
26391
+ "memory_read",
26392
+ "memory_write",
26393
+ "memory_search",
26394
+ "web_search",
26395
+ "web_fetch",
26396
+ "task_complete"
26397
+ ]);
26398
+ SAFE_GROUP_ADMIN_TOOLS = /* @__PURE__ */ new Set([
26399
+ "memory_read",
26400
+ "memory_write",
26401
+ "memory_search",
26402
+ "web_search",
26403
+ "web_fetch",
26404
+ "web_crawl",
26405
+ "image_read",
26406
+ "ocr",
26407
+ "vision",
26408
+ "ocr_pdf",
26409
+ "pdf_to_text",
26410
+ "transcribe_file",
26411
+ "transcribe_url",
26412
+ "task_complete"
26413
+ ]);
26414
+ }
26415
+ });
26416
+
26083
26417
  // packages/cli/dist/tui/telegram-bridge.js
26418
+ import { mkdirSync as mkdirSync14, existsSync as existsSync29, unlinkSync as unlinkSync6, readdirSync as readdirSync13, statSync as statSync10 } from "node:fs";
26419
+ import { join as join39, resolve as resolve24 } from "node:path";
26420
+ import { writeFile as writeFileAsync } from "node:fs/promises";
26421
+ function convertMarkdownToTelegramHTML(md) {
26422
+ let html = md;
26423
+ html = html.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
26424
+ html = html.replace(/```[\w]*\n([\s\S]*?)```/g, (_m, code) => `<pre>${code.trimEnd()}</pre>`);
26425
+ html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
26426
+ html = html.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
26427
+ html = html.replace(/__(.+?)__/g, "<b>$1</b>");
26428
+ html = html.replace(/(?<!\w)\*([^*]+)\*(?!\w)/g, "<i>$1</i>");
26429
+ html = html.replace(/(?<!\w)_([^_]+)_(?!\w)/g, "<i>$1</i>");
26430
+ html = html.replace(/~~(.+?)~~/g, "<s>$1</s>");
26431
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
26432
+ html = html.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
26433
+ html = html.replace(/^[-*]\s+/gm, "\u2022 ");
26434
+ return html;
26435
+ }
26436
+ function formatIntermediateState(event) {
26437
+ if (event.type === "tool_call") {
26438
+ const argsPreview = event.toolArgs ? JSON.stringify(event.toolArgs).slice(0, 60) : "";
26439
+ return `\u{1F527} <code>${event.toolName || "tool"}</code>(${argsPreview.length > 57 ? argsPreview.slice(0, 57) + "..." : argsPreview})`;
26440
+ }
26441
+ if (event.type === "tool_result") {
26442
+ const icon = event.success ? "\u2714" : "\u2718";
26443
+ const preview = (event.content || "").slice(0, 80);
26444
+ return `${icon} <code>${event.toolName || "tool"}</code>: ${preview}${(event.content || "").length > 80 ? "..." : ""}`;
26445
+ }
26446
+ if (event.type === "status") {
26447
+ return `\u23F3 ${event.content || ""}`;
26448
+ }
26449
+ return null;
26450
+ }
26084
26451
  function adaptTool4(tool) {
26085
26452
  return {
26086
26453
  name: tool.name,
@@ -26178,7 +26545,7 @@ function renderTelegramSubAgentError(username, error) {
26178
26545
  process.stdout.write(` ${c2.dim("\u23BF")} ${c2.red("\u2718")} @${username}: ${c2.dim(preview)}
26179
26546
  `);
26180
26547
  }
26181
- var TELEGRAM_SAFETY_PROMPT, ADMIN_PROMPT, TelegramBridge;
26548
+ var TELEGRAM_SAFETY_PROMPT, ADMIN_DM_PROMPT, ADMIN_GROUP_PROMPT, GROUP_REPLY_DISCRETION_PROMPT, MEDIA_CACHE_TTL_MS, TelegramBridge;
26182
26549
  var init_telegram_bridge = __esm({
26183
26550
  "packages/cli/dist/tui/telegram-bridge.js"() {
26184
26551
  "use strict";
@@ -26186,6 +26553,7 @@ var init_telegram_bridge = __esm({
26186
26553
  init_dist2();
26187
26554
  init_project_context();
26188
26555
  init_render();
26556
+ init_tool_policy();
26189
26557
  TELEGRAM_SAFETY_PROMPT = `
26190
26558
  CRITICAL SAFETY NOTICE \u2014 PUBLIC TELEGRAM CHANNEL
26191
26559
 
@@ -26207,13 +26575,36 @@ MANDATORY SAFETY RULES:
26207
26575
  You may answer general questions, provide help, and be friendly, but ALWAYS
26208
26576
  prioritize safety and privacy over helpfulness. When in doubt, decline politely.
26209
26577
  `.trim();
26210
- ADMIN_PROMPT = `
26211
- You are responding to an ADMIN user via Telegram. This user has full system access
26212
- and is the operator of this agent. You may use all tools including memory read/write,
26578
+ ADMIN_DM_PROMPT = `
26579
+ You are responding to an ADMIN user in a private Telegram DM. This user has full system access
26580
+ and is the operator of this agent. You may use all available tools including memory read/write,
26213
26581
  file access, and code analysis. Respond thoroughly and helpfully.
26214
26582
 
26215
26583
  Keep responses concise for Telegram but don't withhold information from the admin.
26216
26584
  `.trim();
26585
+ ADMIN_GROUP_PROMPT = `
26586
+ You are responding in a PUBLIC Telegram group where an ADMIN user sent a message.
26587
+ Although this is an admin, the group is PUBLIC \u2014 other people can see your responses.
26588
+
26589
+ RULES FOR GROUP CONTEXT:
26590
+ 1. NEVER share private information, API keys, file paths, or system internals
26591
+ 2. You have limited tools: web search, memory, and media analysis only
26592
+ 3. Keep responses helpful and relevant to the conversation
26593
+ 4. Be concise \u2014 group chats should have shorter responses
26594
+ 5. Only respond if the message is directed at you or clearly relevant
26595
+ 6. You may share general knowledge and helpful guidance
26596
+ `.trim();
26597
+ GROUP_REPLY_DISCRETION_PROMPT = `
26598
+ REPLY DISCRETION: You are in a group chat. Only respond if:
26599
+ 1. Someone @mentioned the bot directly
26600
+ 2. The message is a reply to one of your previous messages
26601
+ 3. The message clearly asks a question or requests help
26602
+ 4. You have genuinely useful information to contribute
26603
+
26604
+ If the message is casual group chatter not directed at you, use task_complete
26605
+ with summary "no_reply" to silently skip without responding.
26606
+ `.trim();
26607
+ MEDIA_CACHE_TTL_MS = 30 * 60 * 1e3;
26217
26608
  TelegramBridge = class {
26218
26609
  botToken;
26219
26610
  onMessage;
@@ -26234,20 +26625,34 @@ Keep responses concise for Telegram but don't withhold information from the admi
26234
26625
  adminUserId = null;
26235
26626
  /** Active sub-agents by chat ID */
26236
26627
  subAgents = /* @__PURE__ */ new Map();
26237
- /** Whether sendMessageDraft is supported (Bot API 9.3+) */
26238
- draftSupported = null;
26239
26628
  /** Event handler for forwarding sub-agent events to parent TUI */
26240
26629
  onSubAgentEvent = null;
26241
- constructor(botToken, onMessage, agentConfig, repoRoot) {
26630
+ /** Tool policy config — user overrides from config */
26631
+ toolPolicyConfig;
26632
+ /** Media cache — fileUniqueId → cache entry */
26633
+ mediaCache = /* @__PURE__ */ new Map();
26634
+ /** Media cache directory */
26635
+ mediaCacheDir;
26636
+ /** Media metadata store per chat — chatId → metadata entries */
26637
+ mediaMetadata = /* @__PURE__ */ new Map();
26638
+ /** Cleanup timer for expired media cache entries */
26639
+ mediaCacheCleanupTimer = null;
26640
+ constructor(botToken, onMessage, agentConfig, repoRoot, toolPolicyConfig) {
26242
26641
  this.botToken = botToken;
26243
26642
  this.onMessage = onMessage;
26244
26643
  this.agentConfig = agentConfig;
26245
26644
  this.repoRoot = repoRoot;
26645
+ this.toolPolicyConfig = toolPolicyConfig;
26646
+ this.mediaCacheDir = resolve24(repoRoot || ".", ".oa", "telegram-media-cache");
26246
26647
  }
26247
26648
  /** Set admin user ID filter */
26248
26649
  setAdmin(userId) {
26249
26650
  this.adminUserId = userId;
26250
26651
  }
26652
+ /** Update tool policy config at runtime (e.g., from /disable command) */
26653
+ setToolPolicyConfig(config) {
26654
+ this.toolPolicyConfig = config;
26655
+ }
26251
26656
  /** Register event handler for sub-agent activity (waterfall view) */
26252
26657
  setOnSubAgentEvent(handler) {
26253
26658
  this.onSubAgentEvent = handler;
@@ -26279,6 +26684,11 @@ Keep responses concise for Telegram but don't withhold information from the admi
26279
26684
  };
26280
26685
  this.polling = true;
26281
26686
  this.abortController = new AbortController();
26687
+ try {
26688
+ mkdirSync14(this.mediaCacheDir, { recursive: true });
26689
+ } catch {
26690
+ }
26691
+ this.mediaCacheCleanupTimer = setInterval(() => this.cleanupMediaCache(), 5 * 60 * 1e3);
26282
26692
  this.pollLoop();
26283
26693
  }
26284
26694
  /** Stop polling and kill all active sub-agents */
@@ -26287,6 +26697,10 @@ Keep responses concise for Telegram but don't withhold information from the admi
26287
26697
  this.state.active = false;
26288
26698
  this.abortController?.abort();
26289
26699
  this.abortController = null;
26700
+ if (this.mediaCacheCleanupTimer) {
26701
+ clearInterval(this.mediaCacheCleanupTimer);
26702
+ this.mediaCacheCleanupTimer = null;
26703
+ }
26290
26704
  for (const [, agent] of this.subAgents) {
26291
26705
  agent.aborted = true;
26292
26706
  if (agent.typingInterval)
@@ -26308,37 +26722,66 @@ Keep responses concise for Telegram but don't withhold information from the admi
26308
26722
  async sendChatAction(chatId, action) {
26309
26723
  await this.apiCall("sendChatAction", { chat_id: chatId, action });
26310
26724
  }
26311
- // ── Streaming response ────────────────────────────────────────────────
26725
+ // ── Live message streaming (editMessageText pattern) ─────────────────
26312
26726
  /**
26313
- * Send a streaming draft to the user (Bot API 9.3+ sendMessageDraft).
26314
- * If not supported, falls back to editMessageText on a placeholder message.
26727
+ * Send a placeholder message that will be progressively edited with
26728
+ * streamed content. Returns the message_id for subsequent edits.
26315
26729
  */
26316
- async sendDraft(chatId, draftId, text) {
26317
- if (this.draftSupported === false)
26318
- return;
26319
- const truncated = text.length > 4e3 ? text.slice(0, 3997) + "..." : text;
26730
+ async sendLiveMessage(chatId, initialText) {
26320
26731
  try {
26321
- const result = await this.apiCall("sendMessageDraft", {
26732
+ const result = await this.apiCall("sendMessage", {
26322
26733
  chat_id: chatId,
26323
- draft_id: draftId,
26324
- text: truncated
26734
+ text: initialText,
26735
+ parse_mode: "HTML"
26325
26736
  });
26326
- if (result.ok !== false) {
26327
- if (this.draftSupported === null)
26328
- this.draftSupported = true;
26329
- return;
26330
- }
26331
- if (result.error_code === 404 || result.description?.includes("method not found")) {
26332
- this.draftSupported = false;
26737
+ this.state.messagesSent++;
26738
+ return result.result?.message_id ?? null;
26739
+ } catch {
26740
+ try {
26741
+ const result = await this.apiCall("sendMessage", {
26742
+ chat_id: chatId,
26743
+ text: initialText
26744
+ });
26745
+ this.state.messagesSent++;
26746
+ return result.result?.message_id ?? null;
26747
+ } catch {
26748
+ return null;
26333
26749
  }
26750
+ }
26751
+ }
26752
+ /**
26753
+ * Edit a live message with updated content. Used for progressive streaming.
26754
+ * Handles rate limits and Telegram's "message is not modified" error silently.
26755
+ */
26756
+ async editLiveMessage(chatId, messageId, html) {
26757
+ const truncated = html.length > 4e3 ? html.slice(0, 3950) + "\n\n<i>... (streaming)</i>" : html;
26758
+ try {
26759
+ const result = await this.apiCall("editMessageText", {
26760
+ chat_id: chatId,
26761
+ message_id: messageId,
26762
+ text: truncated,
26763
+ parse_mode: "HTML"
26764
+ });
26765
+ if (result.error_code === 400 && result.description?.includes("not modified"))
26766
+ return;
26334
26767
  } catch {
26335
- this.draftSupported = false;
26768
+ try {
26769
+ const plain = html.replace(/<[^>]+>/g, "");
26770
+ const truncPlain = plain.length > 4e3 ? plain.slice(0, 3950) + "\n\n... (streaming)" : plain;
26771
+ await this.apiCall("editMessageText", {
26772
+ chat_id: chatId,
26773
+ message_id: messageId,
26774
+ text: truncPlain
26775
+ });
26776
+ } catch {
26777
+ }
26336
26778
  }
26337
26779
  }
26338
26780
  // ── Sub-agent spawning ────────────────────────────────────────────────
26339
26781
  /**
26340
26782
  * Handle an incoming Telegram message by spawning a sub-agent.
26341
- * Admin users get full tool access; public users get read-only + light memory.
26783
+ * Tool access is determined by resolveToolContext() based on
26784
+ * admin status AND chat type (DM vs group).
26342
26785
  */
26343
26786
  async handleMessageWithSubAgent(msg) {
26344
26787
  if (!this.agentConfig || !this.repoRoot) {
@@ -26346,36 +26789,56 @@ Keep responses concise for Telegram but don't withhold information from the admi
26346
26789
  return;
26347
26790
  }
26348
26791
  const isAdmin = this.isAdminUser(msg);
26792
+ const toolContext = this.resolveToolContext(msg, isAdmin);
26793
+ const isAdminDM = toolContext === "telegram-admin-dm";
26349
26794
  const existing = this.subAgents.get(msg.chatId);
26350
26795
  if (existing && !existing.aborted) {
26351
26796
  existing.runner.injectUserMessage(msg.text);
26352
26797
  renderTelegramSubAgentEvent(msg.username, "mid-conversation steering injected");
26353
26798
  return;
26354
26799
  }
26355
- const draftId = Date.now();
26356
26800
  const subAgent = {
26357
26801
  chatId: msg.chatId,
26358
26802
  username: msg.username,
26359
26803
  runner: null,
26360
- // set below
26361
26804
  typingInterval: null,
26362
- draftId,
26805
+ liveMessageId: null,
26363
26806
  accumulated: "",
26364
- lastDraftMs: 0,
26365
- aborted: false
26807
+ intermediateLines: [],
26808
+ lastEditMs: 0,
26809
+ aborted: false,
26810
+ toolContext
26366
26811
  };
26367
26812
  this.subAgents.set(msg.chatId, subAgent);
26368
26813
  this.state.activeSubAgents = this.subAgents.size;
26369
26814
  subAgent.typingInterval = this.startTypingIndicator(msg.chatId);
26370
- renderTelegramSubAgentStart(msg.username, msg.text, isAdmin);
26815
+ renderTelegramSubAgentStart(msg.username, msg.text, isAdminDM);
26371
26816
  try {
26372
- const result = await this.runSubAgent(msg, isAdmin, subAgent);
26817
+ if (isAdminDM) {
26818
+ const msgId = await this.sendLiveMessage(msg.chatId, "<i>\u23F3 Processing...</i>");
26819
+ subAgent.liveMessageId = msgId;
26820
+ }
26821
+ let mediaContext = "";
26822
+ if (msg.media) {
26823
+ mediaContext = await this.processMedia(msg);
26824
+ }
26825
+ const result = await this.runSubAgent(msg, subAgent, mediaContext);
26373
26826
  if (subAgent.typingInterval) {
26374
26827
  clearInterval(subAgent.typingInterval);
26375
26828
  subAgent.typingInterval = null;
26376
26829
  }
26830
+ if (result === "no_reply" || result === "") {
26831
+ renderTelegramSubAgentEvent(msg.username, "discretion: skipped reply");
26832
+ return;
26833
+ }
26377
26834
  const finalText = result || "I couldn't generate a response. Please try again.";
26378
- await this.sendMessage(msg.chatId, finalText);
26835
+ const finalHtml = convertMarkdownToTelegramHTML(finalText);
26836
+ if (isAdminDM && subAgent.liveMessageId) {
26837
+ const intermediateBlock = subAgent.intermediateLines.length > 0 ? subAgent.intermediateLines.join("\n") + "\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n" : "";
26838
+ await this.editLiveMessage(msg.chatId, subAgent.liveMessageId, intermediateBlock + finalHtml);
26839
+ } else {
26840
+ await this.sendMessageHTML(msg.chatId, finalHtml, msg.chatType !== "private" ? msg.messageId : void 0);
26841
+ }
26379
26842
  renderTelegramSubAgentComplete(msg.username, finalText);
26380
26843
  } catch (err) {
26381
26844
  if (subAgent.typingInterval) {
@@ -26384,73 +26847,118 @@ Keep responses concise for Telegram but don't withhold information from the admi
26384
26847
  }
26385
26848
  const errMsg = err instanceof Error ? err.message : String(err);
26386
26849
  renderTelegramSubAgentError(msg.username, errMsg);
26387
- await this.sendMessage(msg.chatId, "Sorry, I encountered an error processing your message. Please try again.").catch(() => {
26388
- });
26850
+ if (isAdminDM && subAgent.liveMessageId) {
26851
+ await this.editLiveMessage(msg.chatId, subAgent.liveMessageId, `\u274C Error: ${errMsg}`).catch(() => {
26852
+ });
26853
+ } else {
26854
+ await this.sendMessage(msg.chatId, "Sorry, I encountered an error processing your message. Please try again.").catch(() => {
26855
+ });
26856
+ }
26389
26857
  } finally {
26390
26858
  this.subAgents.delete(msg.chatId);
26391
26859
  this.state.activeSubAgents = this.subAgents.size;
26392
26860
  }
26393
26861
  }
26394
26862
  /** Run a sub-agent for a Telegram message */
26395
- async runSubAgent(msg, isAdmin, subAgent) {
26863
+ async runSubAgent(msg, subAgent, mediaContext = "") {
26396
26864
  const config = this.agentConfig;
26397
26865
  const repoRoot = this.repoRoot;
26398
26866
  const modelTier = getModelTier(config.model);
26867
+ const ctx = subAgent.toolContext;
26868
+ const isAdminDM = ctx === "telegram-admin-dm";
26869
+ const isAdminGroup = ctx === "telegram-admin-group";
26870
+ const isGroup = msg.chatType !== "private";
26399
26871
  const backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey);
26400
26872
  const runner = new AgenticRunner(backend, {
26401
- maxTurns: isAdmin ? 30 : 8,
26402
- // Admin: full agent. Public: quick response
26403
- maxTokens: isAdmin ? 8192 : 2048,
26404
- // Admin: detailed. Public: concise
26873
+ maxTurns: isAdminDM ? 30 : isAdminGroup ? 12 : 8,
26874
+ maxTokens: isAdminDM ? 8192 : 2048,
26405
26875
  temperature: 0.3,
26406
26876
  requestTimeoutMs: config.timeoutMs,
26407
- taskTimeoutMs: isAdmin ? config.timeoutMs * 3 : config.timeoutMs,
26877
+ taskTimeoutMs: isAdminDM ? config.timeoutMs * 3 : config.timeoutMs,
26408
26878
  compactionThreshold: modelTier === "small" ? 8e3 : 16e3,
26409
26879
  modelTier,
26410
26880
  streamEnabled: true
26411
26881
  });
26412
26882
  subAgent.runner = runner;
26413
- const tools = this.buildSubAgentTools(isAdmin, repoRoot);
26883
+ const tools = this.buildSubAgentTools(ctx, repoRoot, msg.chatId);
26414
26884
  runner.registerTools(tools);
26415
26885
  runner.onEvent((event) => {
26416
26886
  if (subAgent.aborted)
26417
26887
  return;
26418
26888
  this.onSubAgentEvent?.(msg.chatId, msg.username, event);
26419
- if (event.type === "stream_token" && event.streamKind === "content" && event.content) {
26420
- subAgent.accumulated += event.content;
26889
+ if (isAdminDM && subAgent.liveMessageId) {
26890
+ if (event.type === "stream_token" && event.streamKind === "content" && event.content) {
26891
+ subAgent.accumulated += event.content;
26892
+ }
26893
+ const intermediateLine = formatIntermediateState(event);
26894
+ if (intermediateLine) {
26895
+ subAgent.intermediateLines.push(intermediateLine);
26896
+ }
26421
26897
  const now = Date.now();
26422
- if (now - subAgent.lastDraftMs > 1e3 && subAgent.accumulated.length > 20) {
26423
- subAgent.lastDraftMs = now;
26424
- this.sendDraft(msg.chatId, subAgent.draftId, subAgent.accumulated).catch(() => {
26425
- });
26898
+ if (now - subAgent.lastEditMs > 1e3) {
26899
+ const hasContent = subAgent.accumulated.length > 10 || subAgent.intermediateLines.length > 0;
26900
+ if (hasContent) {
26901
+ subAgent.lastEditMs = now;
26902
+ const intermediateBlock = subAgent.intermediateLines.length > 0 ? subAgent.intermediateLines.join("\n") + "\n\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n" : "";
26903
+ const contentPreview = subAgent.accumulated ? convertMarkdownToTelegramHTML(subAgent.accumulated) + "\n<i>\u258C</i>" : "<i>\u23F3 Working...</i>";
26904
+ this.editLiveMessage(msg.chatId, subAgent.liveMessageId, intermediateBlock + contentPreview).catch(() => {
26905
+ });
26906
+ }
26426
26907
  }
26427
26908
  }
26428
26909
  });
26429
- const systemPrompt = isAdmin ? ADMIN_PROMPT : TELEGRAM_SAFETY_PROMPT;
26430
- const projectCtx = buildProjectContext(repoRoot);
26431
- const dynamicContext = isAdmin ? formatContextForPrompt(projectCtx, modelTier) : "";
26432
- const userPrompt = isAdmin ? `Telegram message from admin @${msg.username}:
26433
- ${msg.text}` : `${systemPrompt}
26910
+ let systemPrompt;
26911
+ if (isAdminDM) {
26912
+ systemPrompt = ADMIN_DM_PROMPT;
26913
+ } else if (isAdminGroup) {
26914
+ systemPrompt = ADMIN_GROUP_PROMPT;
26915
+ } else {
26916
+ systemPrompt = TELEGRAM_SAFETY_PROMPT;
26917
+ }
26918
+ const discretionPrompt = isGroup ? `
26919
+
26920
+ ${GROUP_REPLY_DISCRETION_PROMPT}` : "";
26921
+ const chatLabel = isGroup ? ` in group "${msg.chatTitle || "unknown"}"` : "";
26922
+ let userPrompt;
26923
+ if (isAdminDM) {
26924
+ userPrompt = `Telegram message from admin @${msg.username}:
26925
+ ${msg.text}`;
26926
+ } else {
26927
+ const toolHint = "You have access to isolated per-chat memory (memory_write, memory_read, memory_search) scoped to this conversation. You can remember facts about this user and retrieve them later. You also have web_search and web_fetch to look up information.";
26928
+ userPrompt = `${systemPrompt}${discretionPrompt}
26929
+
26930
+ ${toolHint}
26434
26931
 
26435
26932
  ---
26436
26933
 
26437
- Telegram message from @${msg.username}:
26934
+ Telegram message from @${msg.username}${chatLabel}:
26438
26935
  ${msg.text}
26439
26936
 
26440
- Respond concisely and safely. Remember: you are talking to the general public.`;
26441
- const result = await runner.run(userPrompt, isAdmin ? `Working directory: ${repoRoot}
26442
- Telegram admin: @${msg.username}` : `Telegram public chat. Respond concisely. Safety filter: ACTIVE.`);
26937
+ Respond concisely and safely.`;
26938
+ }
26939
+ if (mediaContext) {
26940
+ userPrompt += `
26941
+
26942
+ [Media attached \u2014 processed content below]
26943
+ ${mediaContext}`;
26944
+ }
26945
+ const systemCtx = isAdminDM ? `Working directory: ${repoRoot}
26946
+ Telegram admin: @${msg.username}` : `Telegram ${isGroup ? "group" : "public"} chat. Respond concisely. Safety filter: ACTIVE.`;
26947
+ const result = await runner.run(userPrompt, systemCtx);
26443
26948
  return result.summary || subAgent.accumulated || "";
26444
26949
  }
26445
- /** Build tool set based on access level */
26446
- buildSubAgentTools(isAdmin, repoRoot) {
26950
+ /**
26951
+ * Build tool set based on tool context, filtered through the policy system.
26952
+ * All possible tools are instantiated, then applyToolPolicy filters them.
26953
+ */
26954
+ buildSubAgentTools(context, repoRoot, chatId) {
26447
26955
  const taskComplete = {
26448
26956
  name: "task_complete",
26449
- description: "Signal that your response is ready to send to the Telegram user.",
26957
+ description: "Signal that your response is ready to send to the Telegram user. Use summary 'no_reply' to silently skip responding.",
26450
26958
  parameters: {
26451
26959
  type: "object",
26452
26960
  properties: {
26453
- summary: { type: "string", description: "The response to send to the Telegram user" }
26961
+ summary: { type: "string", description: "The response to send to the Telegram user, or 'no_reply' to skip" }
26454
26962
  },
26455
26963
  required: ["summary"]
26456
26964
  },
@@ -26458,57 +26966,195 @@ Telegram admin: @${msg.username}` : `Telegram public chat. Respond concisely. Sa
26458
26966
  return { success: true, output: args["summary"] || "Done." };
26459
26967
  }
26460
26968
  };
26461
- if (isAdmin) {
26462
- const tools2 = [
26463
- new FileReadTool(repoRoot),
26464
- new GrepSearchTool(repoRoot),
26465
- new GlobFindTool(repoRoot),
26466
- new ListDirectoryTool(repoRoot),
26467
- new MemoryReadTool(repoRoot),
26468
- new MemoryWriteTool(repoRoot),
26469
- new MemorySearchTool(repoRoot),
26470
- new WebFetchTool(),
26471
- new WebSearchTool()
26472
- ];
26473
- return [...tools2.map(adaptTool4), taskComplete];
26474
- }
26475
- const tools = [
26969
+ const allTools = [
26970
+ // File tools
26971
+ new FileReadTool(repoRoot),
26972
+ new GrepSearchTool(repoRoot),
26973
+ new GlobFindTool(repoRoot),
26974
+ new ListDirectoryTool(repoRoot),
26975
+ // Memory tools
26476
26976
  new MemoryReadTool(repoRoot),
26477
- // Can read public-facing memory
26977
+ new MemoryWriteTool(repoRoot),
26478
26978
  new MemorySearchTool(repoRoot),
26479
- // Can search for relevant info
26979
+ // Web tools
26980
+ new WebFetchTool(),
26480
26981
  new WebSearchTool(),
26481
- // Can search the web to answer questions
26482
- new WebFetchTool()
26483
- // Can fetch public web pages
26982
+ new WebCrawlTool(repoRoot),
26983
+ // Vision/OCR tools
26984
+ new ImageReadTool(repoRoot),
26985
+ new OCRTool(repoRoot),
26986
+ new VisionTool(repoRoot),
26987
+ new OcrPdfTool(repoRoot),
26988
+ new PdfToTextTool(repoRoot),
26989
+ // Transcription tools
26990
+ new TranscribeFileTool(repoRoot),
26991
+ new TranscribeUrlTool(repoRoot)
26484
26992
  ];
26485
- return [...tools.map(adaptTool4), taskComplete];
26993
+ let adaptedTools = allTools.map(adaptTool4);
26994
+ adaptedTools = applyToolPolicy(adaptedTools, context, this.toolPolicyConfig);
26995
+ if (context !== "telegram-admin-dm") {
26996
+ const memWriteIdx = adaptedTools.findIndex((t) => t.name === "memory_write");
26997
+ if (memWriteIdx >= 0) {
26998
+ const original = adaptedTools[memWriteIdx];
26999
+ const scopedChatId = chatId ?? 0;
27000
+ adaptedTools[memWriteIdx] = {
27001
+ ...original,
27002
+ description: "Store a fact in this chat's isolated memory. Topics are automatically scoped to this conversation.",
27003
+ async execute(args) {
27004
+ const userTopic = args["topic"] || "general";
27005
+ args["topic"] = `telegram-${scopedChatId}-${userTopic}`;
27006
+ return original.execute(args);
27007
+ }
27008
+ };
27009
+ }
27010
+ }
27011
+ return [...adaptedTools, taskComplete];
26486
27012
  }
26487
- /** Check if a message is from the admin user */
27013
+ /** Check if a message is from the admin user (uses fromUserId, NOT chatId) */
26488
27014
  isAdminUser(msg) {
26489
27015
  if (!this.adminUserId)
26490
27016
  return false;
26491
- const fromId = String(msg.chatId);
27017
+ const fromId = String(msg.fromUserId);
26492
27018
  return fromId === this.adminUserId || msg.username === this.adminUserId;
26493
27019
  }
27020
+ /**
27021
+ * Resolve the tool context for a message based on admin status and chat type.
27022
+ * Admin in private DM → telegram-admin-dm (full tools)
27023
+ * Admin in group/supergroup → telegram-admin-group (restricted!)
27024
+ * Anyone else → telegram-public (minimal tools)
27025
+ */
27026
+ resolveToolContext(msg, isAdmin) {
27027
+ if (!isAdmin)
27028
+ return "telegram-public";
27029
+ if (msg.chatType === "private")
27030
+ return "telegram-admin-dm";
27031
+ return "telegram-admin-group";
27032
+ }
27033
+ // ── Media handling ──────────────────────────────────────────────────
27034
+ /**
27035
+ * Download a Telegram file by file_id and save to the media cache.
27036
+ * Returns the local file path.
27037
+ */
27038
+ async downloadTelegramFile(fileId, extension) {
27039
+ try {
27040
+ const fileInfo = await this.apiCall("getFile", { file_id: fileId });
27041
+ const filePath = fileInfo.result?.file_path;
27042
+ if (!filePath)
27043
+ return null;
27044
+ const downloadUrl = `https://api.telegram.org/file/bot${this.botToken}/${filePath}`;
27045
+ const res = await fetch(downloadUrl);
27046
+ if (!res.ok)
27047
+ return null;
27048
+ const buffer = Buffer.from(await res.arrayBuffer());
27049
+ const fileName = `${Date.now()}-${fileId.slice(0, 8)}${extension}`;
27050
+ const localPath = join39(this.mediaCacheDir, fileName);
27051
+ await writeFileAsync(localPath, buffer);
27052
+ return localPath;
27053
+ } catch {
27054
+ return null;
27055
+ }
27056
+ }
27057
+ /**
27058
+ * Process media from a Telegram message.
27059
+ * Downloads the file, runs it through the appropriate pipeline,
27060
+ * caches it, and returns a text description for the agent.
27061
+ */
27062
+ async processMedia(msg) {
27063
+ if (!msg.media)
27064
+ return "";
27065
+ const { type, fileId, fileUniqueId, mimeType, caption } = msg.media;
27066
+ let ext = ".bin";
27067
+ if (type === "photo")
27068
+ ext = ".jpg";
27069
+ else if (type === "audio" || type === "voice")
27070
+ ext = ".ogg";
27071
+ else if (type === "video" || type === "video_note")
27072
+ ext = ".mp4";
27073
+ else if (msg.media.fileName) {
27074
+ const dotIdx = msg.media.fileName.lastIndexOf(".");
27075
+ if (dotIdx >= 0)
27076
+ ext = msg.media.fileName.slice(dotIdx);
27077
+ }
27078
+ const localPath = await this.downloadTelegramFile(fileId, ext);
27079
+ if (!localPath)
27080
+ return `[Media: ${type} \u2014 failed to download]`;
27081
+ const cacheEntry = {
27082
+ localPath,
27083
+ fileId,
27084
+ chatId: msg.chatId,
27085
+ username: msg.username,
27086
+ mediaType: type,
27087
+ mimeType,
27088
+ cachedAt: Date.now()
27089
+ };
27090
+ this.mediaCache.set(fileUniqueId, cacheEntry);
27091
+ if (!this.mediaMetadata.has(msg.chatId)) {
27092
+ this.mediaMetadata.set(msg.chatId, []);
27093
+ }
27094
+ this.mediaMetadata.get(msg.chatId).push({
27095
+ timestamp: Date.now(),
27096
+ type,
27097
+ mimeType,
27098
+ caption,
27099
+ username: msg.username
27100
+ });
27101
+ let description = `[${type}${caption ? `: ${caption}` : ""}]`;
27102
+ if (type === "photo") {
27103
+ description = `[Photo received and saved to ${localPath}${caption ? ` \u2014 caption: "${caption}"` : ""}. You can use image_read or vision tools to analyze it if available.]`;
27104
+ }
27105
+ if (type === "audio" || type === "voice") {
27106
+ description = `[Audio/voice message received and saved to ${localPath}${caption ? ` \u2014 caption: "${caption}"` : ""}. You can use transcribe_file to transcribe it if available.]`;
27107
+ }
27108
+ if (type === "video" || type === "video_note") {
27109
+ description = `[Video received and saved to ${localPath}${caption ? ` \u2014 caption: "${caption}"` : ""}.]`;
27110
+ }
27111
+ if (type === "document") {
27112
+ description = `[Document received: ${msg.media.fileName || "unnamed"}${mimeType ? ` (${mimeType})` : ""}, saved to ${localPath}${caption ? ` \u2014 caption: "${caption}"` : ""}.]`;
27113
+ }
27114
+ cacheEntry.extractedContent = description;
27115
+ return description;
27116
+ }
27117
+ /** Clean up expired media cache entries (older than 30 minutes) */
27118
+ cleanupMediaCache() {
27119
+ const now = Date.now();
27120
+ for (const [key, entry] of this.mediaCache) {
27121
+ if (now - entry.cachedAt > MEDIA_CACHE_TTL_MS) {
27122
+ try {
27123
+ unlinkSync6(entry.localPath);
27124
+ } catch {
27125
+ }
27126
+ this.mediaCache.delete(key);
27127
+ }
27128
+ }
27129
+ }
26494
27130
  // ── Message sending ───────────────────────────────────────────────────
26495
- /** Send a response back to a Telegram chat */
27131
+ /** Send a response back to a Telegram chat (Markdown → HTML conversion) */
26496
27132
  async sendMessage(chatId, text) {
26497
- const truncated = text.length > 4e3 ? text.slice(0, 3950) + "\n\n... (truncated)" : text;
27133
+ const html = convertMarkdownToTelegramHTML(text);
27134
+ return this.sendMessageHTML(chatId, html);
27135
+ }
27136
+ /** Send an HTML-formatted message to a Telegram chat */
27137
+ async sendMessageHTML(chatId, html, replyToMessageId) {
27138
+ const truncated = html.length > 4e3 ? html.slice(0, 3950) + "\n\n<i>... (truncated)</i>" : html;
27139
+ const body = {
27140
+ chat_id: chatId,
27141
+ text: truncated,
27142
+ parse_mode: "HTML"
27143
+ };
27144
+ if (replyToMessageId)
27145
+ body["reply_to_message_id"] = replyToMessageId;
26498
27146
  try {
26499
- const result = await this.apiCall("sendMessage", {
26500
- chat_id: chatId,
26501
- text: truncated,
26502
- parse_mode: "Markdown"
26503
- });
27147
+ const result = await this.apiCall("sendMessage", body);
26504
27148
  this.state.messagesSent++;
26505
27149
  return result.result?.message_id ?? null;
26506
27150
  } catch {
27151
+ const plain = html.replace(/<[^>]+>/g, "");
27152
+ const truncPlain = plain.length > 4e3 ? plain.slice(0, 3950) + "\n\n... (truncated)" : plain;
27153
+ const fallbackBody = { chat_id: chatId, text: truncPlain };
27154
+ if (replyToMessageId)
27155
+ fallbackBody["reply_to_message_id"] = replyToMessageId;
26507
27156
  try {
26508
- const result = await this.apiCall("sendMessage", {
26509
- chat_id: chatId,
26510
- text: truncated
26511
- });
27157
+ const result = await this.apiCall("sendMessage", fallbackBody);
26512
27158
  this.state.messagesSent++;
26513
27159
  return result.result?.message_id ?? null;
26514
27160
  } catch (err) {
@@ -26530,29 +27176,104 @@ Telegram admin: @${msg.username}` : `Telegram public chat. Respond concisely. Sa
26530
27176
  if (result.ok && Array.isArray(result.result)) {
26531
27177
  for (const update of result.result) {
26532
27178
  this.lastUpdateId = update.update_id;
26533
- if (update.message?.text) {
26534
- const fromId = String(update.message.from?.id ?? "");
26535
- const fromUser = update.message.from?.username ?? "";
26536
- const isAdmin = this.adminUserId ? fromId === this.adminUserId || fromUser === this.adminUserId : false;
26537
- if (this.adminUserId && !this.agentConfig) {
26538
- if (!isAdmin)
26539
- continue;
26540
- }
26541
- this.state.messagesReceived++;
26542
- const msg = {
26543
- chatId: update.message.chat.id,
26544
- text: update.message.text,
26545
- username: fromUser || "unknown",
26546
- firstName: update.message.from?.first_name,
26547
- messageId: update.message.message_id
27179
+ const message = update.message;
27180
+ if (!message)
27181
+ continue;
27182
+ const fromId = message.from?.id ?? 0;
27183
+ const fromUser = message.from?.username ?? "";
27184
+ const chatType = message.chat?.type ?? "private";
27185
+ const chatTitle = message.chat?.title;
27186
+ const isAdmin = this.adminUserId ? String(fromId) === this.adminUserId || fromUser === this.adminUserId : false;
27187
+ if (this.adminUserId && !this.agentConfig) {
27188
+ if (!isAdmin)
27189
+ continue;
27190
+ }
27191
+ let media;
27192
+ if (message.photo && message.photo.length > 0) {
27193
+ const photo = message.photo[message.photo.length - 1];
27194
+ media = {
27195
+ type: "photo",
27196
+ fileId: photo.file_id,
27197
+ fileUniqueId: photo.file_unique_id,
27198
+ fileSize: photo.file_size,
27199
+ caption: message.caption
26548
27200
  };
26549
- if (this.agentConfig && this.repoRoot) {
26550
- this.handleMessageWithSubAgent(msg).catch((err) => {
26551
- renderWarning(`Telegram sub-agent error: ${err instanceof Error ? err.message : String(err)}`);
26552
- });
26553
- } else {
26554
- this.onMessage(msg);
26555
- }
27201
+ } else if (message.audio) {
27202
+ media = {
27203
+ type: "audio",
27204
+ fileId: message.audio.file_id,
27205
+ fileUniqueId: message.audio.file_unique_id,
27206
+ mimeType: message.audio.mime_type,
27207
+ fileName: message.audio.file_name,
27208
+ fileSize: message.audio.file_size,
27209
+ duration: message.audio.duration,
27210
+ caption: message.caption
27211
+ };
27212
+ } else if (message.voice) {
27213
+ media = {
27214
+ type: "voice",
27215
+ fileId: message.voice.file_id,
27216
+ fileUniqueId: message.voice.file_unique_id,
27217
+ mimeType: message.voice.mime_type,
27218
+ fileSize: message.voice.file_size,
27219
+ duration: message.voice.duration,
27220
+ caption: message.caption
27221
+ };
27222
+ } else if (message.video) {
27223
+ media = {
27224
+ type: "video",
27225
+ fileId: message.video.file_id,
27226
+ fileUniqueId: message.video.file_unique_id,
27227
+ mimeType: message.video.mime_type,
27228
+ fileName: message.video.file_name,
27229
+ fileSize: message.video.file_size,
27230
+ duration: message.video.duration,
27231
+ caption: message.caption
27232
+ };
27233
+ } else if (message.video_note) {
27234
+ media = {
27235
+ type: "video_note",
27236
+ fileId: message.video_note.file_id,
27237
+ fileUniqueId: message.video_note.file_unique_id,
27238
+ fileSize: message.video_note.file_size,
27239
+ duration: message.video_note.duration,
27240
+ caption: message.caption
27241
+ };
27242
+ } else if (message.document) {
27243
+ media = {
27244
+ type: "document",
27245
+ fileId: message.document.file_id,
27246
+ fileUniqueId: message.document.file_unique_id,
27247
+ mimeType: message.document.mime_type,
27248
+ fileName: message.document.file_name,
27249
+ fileSize: message.document.file_size,
27250
+ caption: message.caption
27251
+ };
27252
+ }
27253
+ const hasText = !!message.text;
27254
+ const hasCaption = !!message.caption;
27255
+ const hasMedia = !!media;
27256
+ if (!hasText && !hasMedia)
27257
+ continue;
27258
+ this.state.messagesReceived++;
27259
+ const msg = {
27260
+ chatId: message.chat.id,
27261
+ text: message.text || message.caption || "",
27262
+ username: fromUser || "unknown",
27263
+ firstName: message.from?.first_name,
27264
+ messageId: message.message_id,
27265
+ fromUserId: fromId,
27266
+ chatType,
27267
+ chatTitle,
27268
+ media,
27269
+ replyToMessageId: message.reply_to_message?.message_id
27270
+ };
27271
+ if (this.agentConfig && this.repoRoot) {
27272
+ this.handleMessageWithSubAgent(msg).catch((err) => {
27273
+ renderWarning(`Telegram sub-agent error: ${err instanceof Error ? err.message : String(err)}`);
27274
+ });
27275
+ } else {
27276
+ this.onMessage(msg);
26556
27277
  }
26557
27278
  }
26558
27279
  }
@@ -27174,6 +27895,20 @@ var init_status_bar = __esm({
27174
27895
  if (this.active)
27175
27896
  this.renderFooterPreserveCursor();
27176
27897
  }
27898
+ /** Current emotional state — emoji + label from EmotionEngine */
27899
+ _emotion = null;
27900
+ /** Update emotional state indicator */
27901
+ setEmotion(emoji, label) {
27902
+ this._emotion = { emoji, label };
27903
+ if (this.active)
27904
+ this.renderFooterPreserveCursor();
27905
+ }
27906
+ /** Clear emotional state indicator */
27907
+ clearEmotion() {
27908
+ this._emotion = null;
27909
+ if (this.active)
27910
+ this.renderFooterPreserveCursor();
27911
+ }
27177
27912
  /** Model capabilities — shown as emoji indicators on the status bar */
27178
27913
  _caps = {
27179
27914
  vision: false,
@@ -27388,6 +28123,10 @@ var init_status_bar = __esm({
27388
28123
  const capStr = this._snr.capacityWarning ? c2.red(" !CAP") : "";
27389
28124
  snrLabel = pipe + pastel2(183, "SNR: ") + snrColor(c2.bold(`${snrPct}%`)) + c2.dim(` d'${dPrimeStr}`) + capStr;
27390
28125
  }
28126
+ let emotionLabel = "";
28127
+ if (this._emotion) {
28128
+ emotionLabel = pipe + this._emotion.emoji + " " + pastel2(183, this._emotion.label);
28129
+ }
27391
28130
  let speedLabel = "";
27392
28131
  if (this._speedTracker.hasData) {
27393
28132
  const ratio = this._speedTracker.getSpeedRatio();
@@ -27411,7 +28150,7 @@ var init_status_bar = __esm({
27411
28150
  if (this._caps.thinking)
27412
28151
  capParts.push("\u{1F9E0}");
27413
28152
  const capsLabel = capParts.length > 0 ? pipe + pastel2(183, capParts.join(" ")) : "";
27414
- return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${snrLabel}${speedLabel}${costLabel}${capsLabel}${recordingLabel}`;
28153
+ return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${snrLabel}${emotionLabel}${speedLabel}${costLabel}${capsLabel}${recordingLabel}`;
27415
28154
  }
27416
28155
  // -------------------------------------------------------------------------
27417
28156
  // Private
@@ -27690,11 +28429,11 @@ var init_status_bar = __esm({
27690
28429
  import * as readline2 from "node:readline";
27691
28430
  import { Writable } from "node:stream";
27692
28431
  import { cwd } from "node:process";
27693
- import { resolve as resolve24, join as join39, dirname as dirname12, extname as extname9 } from "node:path";
28432
+ import { resolve as resolve25, join as join40, dirname as dirname12, extname as extname9 } from "node:path";
27694
28433
  import { createRequire as createRequire2 } from "node:module";
27695
28434
  import { fileURLToPath as fileURLToPath9 } from "node:url";
27696
- import { readFileSync as readFileSync22, rmSync as rmSync2, readdirSync as readdirSync13 } from "node:fs";
27697
- import { existsSync as existsSync29 } from "node:fs";
28435
+ import { readFileSync as readFileSync22, rmSync as rmSync2, readdirSync as readdirSync14 } from "node:fs";
28436
+ import { existsSync as existsSync30 } from "node:fs";
27698
28437
  function formatTimeAgo(date) {
27699
28438
  const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
27700
28439
  if (seconds < 60)
@@ -27713,12 +28452,12 @@ function getVersion() {
27713
28452
  const require2 = createRequire2(import.meta.url);
27714
28453
  const thisDir = dirname12(fileURLToPath9(import.meta.url));
27715
28454
  const candidates = [
27716
- join39(thisDir, "..", "package.json"),
27717
- join39(thisDir, "..", "..", "package.json"),
27718
- join39(thisDir, "..", "..", "..", "package.json")
28455
+ join40(thisDir, "..", "package.json"),
28456
+ join40(thisDir, "..", "..", "package.json"),
28457
+ join40(thisDir, "..", "..", "..", "package.json")
27719
28458
  ];
27720
28459
  for (const pkgPath of candidates) {
27721
- if (existsSync29(pkgPath)) {
28460
+ if (existsSync30(pkgPath)) {
27722
28461
  const pkg = require2(pkgPath);
27723
28462
  if (pkg.name === "open-agents-ai" || pkg.name === "@open-agents/cli") {
27724
28463
  return pkg.version ?? "0.0.0";
@@ -27912,15 +28651,15 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
27912
28651
  function gatherMemorySnippets(root) {
27913
28652
  const snippets = [];
27914
28653
  const dirs = [
27915
- join39(root, ".oa", "memory"),
27916
- join39(root, ".open-agents", "memory")
28654
+ join40(root, ".oa", "memory"),
28655
+ join40(root, ".open-agents", "memory")
27917
28656
  ];
27918
28657
  for (const dir of dirs) {
27919
- if (!existsSync29(dir))
28658
+ if (!existsSync30(dir))
27920
28659
  continue;
27921
28660
  try {
27922
- for (const f of readdirSync13(dir).filter((f2) => f2.endsWith(".json"))) {
27923
- const data = JSON.parse(readFileSync22(join39(dir, f), "utf-8"));
28661
+ for (const f of readdirSync14(dir).filter((f2) => f2.endsWith(".json"))) {
28662
+ const data = JSON.parse(readFileSync22(join40(dir, f), "utf-8"));
27924
28663
  for (const val of Object.values(data)) {
27925
28664
  const v = typeof val === "object" && val !== null && "value" in val ? String(val.value) : String(val);
27926
28665
  if (v.length > 10)
@@ -27989,7 +28728,7 @@ function formatDMNToolArgs(toolName, args) {
27989
28728
  return "";
27990
28729
  }
27991
28730
  }
27992
- function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality, deepContext, onCompaction) {
28731
+ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce, statusBar, sudoCallback, costTracker, onComplete, taskType, contextWindowSize, modelCaps, personality, deepContext, onCompaction, emotionEngine) {
27993
28732
  const voiceStyleMap = {
27994
28733
  concise: 1,
27995
28734
  balanced: 3,
@@ -28102,6 +28841,7 @@ ${entry.fullContent}`
28102
28841
  }
28103
28842
  };
28104
28843
  runner.onEvent((event) => {
28844
+ emotionEngine?.appraise(event);
28105
28845
  switch (event.type) {
28106
28846
  case "tool_call":
28107
28847
  toolSequence.push({
@@ -28239,7 +28979,11 @@ ${entry.fullContent}`
28239
28979
  }
28240
28980
  });
28241
28981
  const sessionId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
28242
- const promise = runner.run(task, `Working directory: ${repoRoot}`).then((result) => {
28982
+ const emotionContext = emotionEngine?.getEmotionForPrompt() ?? "";
28983
+ const systemContext = emotionContext ? `Working directory: ${repoRoot}
28984
+
28985
+ ${emotionContext}` : `Working directory: ${repoRoot}`;
28986
+ const promise = runner.run(task, systemContext).then((result) => {
28243
28987
  const tokens = { total: result.totalTokens, estimated: result.estimatedTokens };
28244
28988
  contentWrite(() => {
28245
28989
  if (result.completed) {
@@ -28320,7 +29064,7 @@ ${entry.fullContent}`
28320
29064
  } };
28321
29065
  }
28322
29066
  async function startInteractive(config, repoPath) {
28323
- const repoRoot = resolve24(repoPath ?? cwd());
29067
+ const repoRoot = resolve25(repoPath ?? cwd());
28324
29068
  const resumeFlag = process.env.__OA_RESUMED ?? "";
28325
29069
  const isResumed = resumeFlag !== "";
28326
29070
  const hasTaskToResume = resumeFlag === "1";
@@ -28474,14 +29218,14 @@ async function startInteractive(config, repoPath) {
28474
29218
  renderInfo(msg);
28475
29219
  statusBar.endContentWrite();
28476
29220
  }
28477
- }, () => new Promise((resolve27) => {
29221
+ }, () => new Promise((resolve28) => {
28478
29222
  depSudoPromptPending = true;
28479
29223
  depSudoResolver = (pw) => {
28480
29224
  depSudoPromptPending = false;
28481
29225
  depSudoResolver = null;
28482
29226
  if (pw)
28483
29227
  sessionSudoPassword = pw;
28484
- resolve27(pw);
29228
+ resolve28(pw);
28485
29229
  };
28486
29230
  if (statusBar?.isActive) {
28487
29231
  statusBar.beginContentWrite();
@@ -28509,6 +29253,15 @@ async function startInteractive(config, repoPath) {
28509
29253
  let blessEngine = null;
28510
29254
  let dmnEngine = null;
28511
29255
  const snrEngine = new SNREngine(config, repoRoot);
29256
+ const emotionEngine = new EmotionEngine({
29257
+ backendUrl: config.backendUrl,
29258
+ model: config.model,
29259
+ apiKey: config.apiKey,
29260
+ onEmotionUpdate: (state) => {
29261
+ statusBar.setEmotion(state.emoji, state.label);
29262
+ }
29263
+ // Admin outreach wired after telegram bridge is initialized (see telegramStart)
29264
+ });
28512
29265
  let pendingSNREval = null;
28513
29266
  function scheduleAgentSNREval(taskPrompt) {
28514
29267
  if (pendingSNREval)
@@ -28960,6 +29713,16 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
28960
29713
  writeContent(() => renderTelegramSubAgentEvent(username, event.content));
28961
29714
  }
28962
29715
  });
29716
+ if (adminId) {
29717
+ const bridge = telegramBridge;
29718
+ const adminChatId = parseInt(adminId, 10);
29719
+ if (!isNaN(adminChatId)) {
29720
+ emotionEngine.setAdminOutreach((message) => {
29721
+ bridge.sendMessage(adminChatId, message).catch(() => {
29722
+ });
29723
+ });
29724
+ }
29725
+ }
28963
29726
  await telegramBridge.start();
28964
29727
  writeContent(() => renderTelegramStart(telegramBridge.botUsername, adminId));
28965
29728
  showPrompt();
@@ -29115,8 +29878,8 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
29115
29878
  return true;
29116
29879
  },
29117
29880
  destroyProject() {
29118
- const oaPath = join39(repoRoot, OA_DIR);
29119
- if (existsSync29(oaPath)) {
29881
+ const oaPath = join40(repoRoot, OA_DIR);
29882
+ if (existsSync30(oaPath)) {
29120
29883
  try {
29121
29884
  rmSync2(oaPath, { recursive: true, force: true });
29122
29885
  writeContent(() => renderInfo(`Removed ${OA_DIR}/ directory.`));
@@ -29375,7 +30138,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
29375
30138
  toolPatternStore: toolPatternStore ?? void 0
29376
30139
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
29377
30140
  lastCompletedSummary = summary;
29378
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback());
30141
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback(), emotionEngine);
29379
30142
  activeTask = task;
29380
30143
  showPrompt();
29381
30144
  await task.promise;
@@ -29390,12 +30153,12 @@ Execute this skill now. Follow the behavioral guidance above.`;
29390
30153
  }
29391
30154
  }
29392
30155
  const cleanPath = input.replace(/^['"]|['"]$/g, "").trim();
29393
- const isImage = isImagePath(cleanPath) && existsSync29(resolve24(repoRoot, cleanPath));
29394
- const isMedia = !isImage && isTranscribablePath(cleanPath) && existsSync29(resolve24(repoRoot, cleanPath));
30156
+ const isImage = isImagePath(cleanPath) && existsSync30(resolve25(repoRoot, cleanPath));
30157
+ const isMedia = !isImage && isTranscribablePath(cleanPath) && existsSync30(resolve25(repoRoot, cleanPath));
29395
30158
  if (activeTask) {
29396
30159
  if (isImage) {
29397
30160
  try {
29398
- const imgPath = resolve24(repoRoot, cleanPath);
30161
+ const imgPath = resolve25(repoRoot, cleanPath);
29399
30162
  const imgBuffer = readFileSync22(imgPath);
29400
30163
  const base64 = imgBuffer.toString("base64");
29401
30164
  const ext = extname9(cleanPath).toLowerCase();
@@ -29409,7 +30172,7 @@ Execute this skill now. Follow the behavioral guidance above.`;
29409
30172
  } else if (isMedia) {
29410
30173
  writeContent(() => renderInfo(`Transcribing: ${cleanPath}...`));
29411
30174
  const engine = getListenEngine();
29412
- const result = await engine.transcribeFile(resolve24(repoRoot, cleanPath), repoRoot);
30175
+ const result = await engine.transcribeFile(resolve25(repoRoot, cleanPath), repoRoot);
29413
30176
  if (result) {
29414
30177
  const transcript = `[Transcription of ${cleanPath}]
29415
30178
  ${result.text}`;
@@ -29442,7 +30205,7 @@ ${result.text}`;
29442
30205
  if (isMedia && fullInput === input) {
29443
30206
  writeContent(() => renderInfo(`Transcribing: ${cleanPath}...`));
29444
30207
  const engine = getListenEngine();
29445
- const result = await engine.transcribeFile(resolve24(repoRoot, cleanPath), repoRoot);
30208
+ const result = await engine.transcribeFile(resolve25(repoRoot, cleanPath), repoRoot);
29446
30209
  if (result) {
29447
30210
  fullInput = `The user has provided an audio/video file: ${cleanPath}.
29448
30211
 
@@ -29495,7 +30258,7 @@ NEW TASK: ${fullInput}`;
29495
30258
  toolPatternStore: toolPatternStore ?? void 0
29496
30259
  }, bruteForceEnabled, statusBar, handleSudoRequest, costTracker, (summary) => {
29497
30260
  lastCompletedSummary = summary;
29498
- }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback());
30261
+ }, currentTaskType, resolvedContextWindowSize, resolvedCaps, currentStyle, deepContextEnabled, compactionSNRCallback(), emotionEngine);
29499
30262
  activeTask = task;
29500
30263
  showPrompt();
29501
30264
  await task.promise;
@@ -29660,7 +30423,7 @@ ${c2.dim("(Use /quit to exit)")}
29660
30423
  });
29661
30424
  }
29662
30425
  async function runWithTUI(task, config, repoPath) {
29663
- const repoRoot = resolve24(repoPath ?? cwd());
30426
+ const repoRoot = resolve25(repoPath ?? cwd());
29664
30427
  const needsSetup = isFirstRun() || !await isModelAvailable(config);
29665
30428
  if (needsSetup && config.backendType === "ollama") {
29666
30429
  const setupModel = await runSetupWizard(config);
@@ -29732,6 +30495,7 @@ var init_interactive = __esm({
29732
30495
  init_bless_engine();
29733
30496
  init_dmn_engine();
29734
30497
  init_snr_engine();
30498
+ init_emotion_engine();
29735
30499
  init_telegram_bridge();
29736
30500
  init_status_bar();
29737
30501
  init_dist6();
@@ -29770,7 +30534,7 @@ import { glob } from "glob";
29770
30534
  import ignore from "ignore";
29771
30535
  import { readFile as readFile14, stat as stat4 } from "node:fs/promises";
29772
30536
  import { createHash } from "node:crypto";
29773
- import { join as join40, relative as relative3, extname as extname10, basename as basename15 } from "node:path";
30537
+ import { join as join41, relative as relative3, extname as extname10, basename as basename15 } from "node:path";
29774
30538
  var DEFAULT_EXCLUDE, LANGUAGE_MAP, CodebaseIndexer;
29775
30539
  var init_codebase_indexer = __esm({
29776
30540
  "packages/indexer/dist/codebase-indexer.js"() {
@@ -29814,7 +30578,7 @@ var init_codebase_indexer = __esm({
29814
30578
  const ig = ignore.default();
29815
30579
  if (this.config.respectGitignore) {
29816
30580
  try {
29817
- const gitignoreContent = await readFile14(join40(this.config.rootDir, ".gitignore"), "utf-8");
30581
+ const gitignoreContent = await readFile14(join41(this.config.rootDir, ".gitignore"), "utf-8");
29818
30582
  ig.add(gitignoreContent);
29819
30583
  } catch {
29820
30584
  }
@@ -29829,7 +30593,7 @@ var init_codebase_indexer = __esm({
29829
30593
  for (const relativePath of files) {
29830
30594
  if (ig.ignores(relativePath))
29831
30595
  continue;
29832
- const fullPath = join40(this.config.rootDir, relativePath);
30596
+ const fullPath = join41(this.config.rootDir, relativePath);
29833
30597
  try {
29834
30598
  const fileStat = await stat4(fullPath);
29835
30599
  if (fileStat.size > this.config.maxFileSize)
@@ -29875,7 +30639,7 @@ var init_codebase_indexer = __esm({
29875
30639
  if (!child) {
29876
30640
  child = {
29877
30641
  name: part,
29878
- path: join40(current.path, part),
30642
+ path: join41(current.path, part),
29879
30643
  type: "directory",
29880
30644
  children: []
29881
30645
  };
@@ -29957,18 +30721,18 @@ var index_repo_exports = {};
29957
30721
  __export(index_repo_exports, {
29958
30722
  indexRepoCommand: () => indexRepoCommand
29959
30723
  });
29960
- import { resolve as resolve25 } from "node:path";
29961
- import { existsSync as existsSync30, statSync as statSync10 } from "node:fs";
30724
+ import { resolve as resolve26 } from "node:path";
30725
+ import { existsSync as existsSync31, statSync as statSync11 } from "node:fs";
29962
30726
  import { cwd as cwd2 } from "node:process";
29963
30727
  async function indexRepoCommand(opts, _config) {
29964
- const repoRoot = resolve25(opts.repoPath ?? cwd2());
30728
+ const repoRoot = resolve26(opts.repoPath ?? cwd2());
29965
30729
  printHeader("Index Repository");
29966
30730
  printInfo(`Indexing: ${repoRoot}`);
29967
- if (!existsSync30(repoRoot)) {
30731
+ if (!existsSync31(repoRoot)) {
29968
30732
  printError(`Path does not exist: ${repoRoot}`);
29969
30733
  process.exit(1);
29970
30734
  }
29971
- const stat5 = statSync10(repoRoot);
30735
+ const stat5 = statSync11(repoRoot);
29972
30736
  if (!stat5.isDirectory()) {
29973
30737
  printError(`Path is not a directory: ${repoRoot}`);
29974
30738
  process.exit(1);
@@ -30216,7 +30980,7 @@ var config_exports = {};
30216
30980
  __export(config_exports, {
30217
30981
  configCommand: () => configCommand
30218
30982
  });
30219
- import { join as join41, resolve as resolve26 } from "node:path";
30983
+ import { join as join42, resolve as resolve27 } from "node:path";
30220
30984
  import { homedir as homedir13 } from "node:os";
30221
30985
  import { cwd as cwd3 } from "node:process";
30222
30986
  function redactIfSensitive(key, value) {
@@ -30243,7 +31007,7 @@ async function configCommand(opts, config) {
30243
31007
  return handleShow(opts, config);
30244
31008
  }
30245
31009
  function handleShow(opts, config) {
30246
- const repoRoot = resolve26(opts.repoPath ?? cwd3());
31010
+ const repoRoot = resolve27(opts.repoPath ?? cwd3());
30247
31011
  printHeader("Configuration");
30248
31012
  printSection("Active Settings (merged)");
30249
31013
  printKeyValue("backendUrl", config.backendUrl, 2);
@@ -30275,7 +31039,7 @@ function handleShow(opts, config) {
30275
31039
  }
30276
31040
  }
30277
31041
  printSection("Config File");
30278
- printInfo(`~/.open-agents/config.json (${join41(homedir13(), ".open-agents", "config.json")})`);
31042
+ printInfo(`~/.open-agents/config.json (${join42(homedir13(), ".open-agents", "config.json")})`);
30279
31043
  printSection("Priority Chain");
30280
31044
  printInfo(" 1. CLI flags (--model, --backend-url, etc.)");
30281
31045
  printInfo(" 2. Project .oa/settings.json (--local)");
@@ -30308,13 +31072,13 @@ function handleSet(opts, _config) {
30308
31072
  process.exit(1);
30309
31073
  }
30310
31074
  if (opts.local) {
30311
- const repoRoot = resolve26(opts.repoPath ?? cwd3());
31075
+ const repoRoot = resolve27(opts.repoPath ?? cwd3());
30312
31076
  try {
30313
31077
  initOaDirectory(repoRoot);
30314
31078
  const coerced = coerceForSettings(key, value);
30315
31079
  saveProjectSettings(repoRoot, { [key]: coerced });
30316
31080
  printSuccess(`Project override set: ${key} = ${redactIfSensitive(key, value)}`);
30317
- printInfo(`Saved to ${join41(repoRoot, ".oa", "settings.json")}`);
31081
+ printInfo(`Saved to ${join42(repoRoot, ".oa", "settings.json")}`);
30318
31082
  printInfo("This override applies only when running in this workspace.");
30319
31083
  } catch (err) {
30320
31084
  printError(`Failed to save: ${err instanceof Error ? err.message : String(err)}`);
@@ -30467,7 +31231,7 @@ async function serveVllm(opts, config) {
30467
31231
  await runVllmServer(args, opts.verbose ?? false);
30468
31232
  }
30469
31233
  async function runVllmServer(args, verbose) {
30470
- return new Promise((resolve27, reject) => {
31234
+ return new Promise((resolve28, reject) => {
30471
31235
  const child = spawn11("python", args, {
30472
31236
  stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
30473
31237
  env: { ...process.env }
@@ -30502,10 +31266,10 @@ async function runVllmServer(args, verbose) {
30502
31266
  child.once("exit", (code, signal) => {
30503
31267
  if (signal) {
30504
31268
  printInfo(`vLLM server stopped by signal ${signal}`);
30505
- resolve27();
31269
+ resolve28();
30506
31270
  } else if (code === 0) {
30507
31271
  printSuccess("vLLM server exited cleanly");
30508
- resolve27();
31272
+ resolve28();
30509
31273
  } else {
30510
31274
  printError(`vLLM server exited with code ${code}`);
30511
31275
  reject(new Error(`vLLM exited with code ${code}`));
@@ -30533,8 +31297,8 @@ __export(eval_exports, {
30533
31297
  evalCommand: () => evalCommand
30534
31298
  });
30535
31299
  import { tmpdir as tmpdir7 } from "node:os";
30536
- import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13 } from "node:fs";
30537
- import { join as join42 } from "node:path";
31300
+ import { mkdirSync as mkdirSync15, writeFileSync as writeFileSync13 } from "node:fs";
31301
+ import { join as join43 } from "node:path";
30538
31302
  async function evalCommand(opts, config) {
30539
31303
  const suiteName = opts.suite ?? "basic";
30540
31304
  const suite = SUITES[suiteName];
@@ -30655,9 +31419,9 @@ async function evalCommand(opts, config) {
30655
31419
  process.exit(failed > 0 ? 1 : 0);
30656
31420
  }
30657
31421
  function createTempEvalRepo() {
30658
- const dir = join42(tmpdir7(), `open-agents-eval-${Date.now()}`);
30659
- mkdirSync14(dir, { recursive: true });
30660
- writeFileSync13(join42(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
31422
+ const dir = join43(tmpdir7(), `open-agents-eval-${Date.now()}`);
31423
+ mkdirSync15(dir, { recursive: true });
31424
+ writeFileSync13(join43(dir, "package.json"), JSON.stringify({ name: "eval-repo", version: "0.0.0" }, null, 2) + "\n", "utf8");
30661
31425
  return dir;
30662
31426
  }
30663
31427
  var BASIC_SUITE, FULL_SUITE, SUITES;
@@ -30717,7 +31481,7 @@ init_updater();
30717
31481
  import { parseArgs as nodeParseArgs2 } from "node:util";
30718
31482
  import { createRequire as createRequire3 } from "node:module";
30719
31483
  import { fileURLToPath as fileURLToPath10 } from "node:url";
30720
- import { dirname as dirname13, join as join43 } from "node:path";
31484
+ import { dirname as dirname13, join as join44 } from "node:path";
30721
31485
 
30722
31486
  // packages/cli/dist/cli.js
30723
31487
  import { createInterface } from "node:readline";
@@ -30824,7 +31588,7 @@ init_output();
30824
31588
  function getVersion2() {
30825
31589
  try {
30826
31590
  const require2 = createRequire3(import.meta.url);
30827
- const pkgPath = join43(dirname13(fileURLToPath10(import.meta.url)), "..", "package.json");
31591
+ const pkgPath = join44(dirname13(fileURLToPath10(import.meta.url)), "..", "package.json");
30828
31592
  const pkg = require2(pkgPath);
30829
31593
  return pkg.version;
30830
31594
  } catch {