open-agents-ai 0.11.2 → 0.11.4

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 (2) hide show
  1. package/dist/index.js +625 -70
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1164,7 +1164,7 @@ var init_shell = __esm({
1164
1164
  const timeout = args["timeout"] ?? this.defaultTimeout;
1165
1165
  const stdinInput = args["stdin"];
1166
1166
  const start = performance.now();
1167
- return new Promise((resolve13) => {
1167
+ return new Promise((resolve14) => {
1168
1168
  const child = spawn("bash", ["-c", command], {
1169
1169
  cwd: this.workingDir,
1170
1170
  env: {
@@ -1217,7 +1217,7 @@ var init_shell = __esm({
1217
1217
  const combined = stdout + stderr;
1218
1218
  const looksInteractive = /\? .+[›>]|y\/n|yes\/no|\(Y\/n\)|\[y\/N\]/i.test(combined);
1219
1219
  const hint = looksInteractive ? " The command appears to be waiting for interactive input. Use non-interactive flags (e.g., --yes, --no-input) or provide input via the stdin parameter." : "";
1220
- resolve13({
1220
+ resolve14({
1221
1221
  success: false,
1222
1222
  output: stdout,
1223
1223
  error: `Command timed out after ${timeout}ms.${hint}`,
@@ -1226,7 +1226,7 @@ var init_shell = __esm({
1226
1226
  return;
1227
1227
  }
1228
1228
  const success = code === 0;
1229
- resolve13({
1229
+ resolve14({
1230
1230
  success,
1231
1231
  output: stdout + (stderr && success ? `
1232
1232
  STDERR:
@@ -1237,7 +1237,7 @@ ${stderr}` : ""),
1237
1237
  });
1238
1238
  child.on("error", (err) => {
1239
1239
  clearTimeout(timer);
1240
- resolve13({
1240
+ resolve14({
1241
1241
  success: false,
1242
1242
  output: stdout,
1243
1243
  error: err.message,
@@ -2919,11 +2919,11 @@ var init_diagnostic = __esm({
2919
2919
  }
2920
2920
  return steps;
2921
2921
  }
2922
- runStep(step, command, cwd3) {
2922
+ runStep(step, command, cwd4) {
2923
2923
  const start = performance.now();
2924
2924
  try {
2925
2925
  const output = execSync5(command, {
2926
- cwd: cwd3,
2926
+ cwd: cwd4,
2927
2927
  encoding: "utf8",
2928
2928
  timeout: 12e4,
2929
2929
  stdio: ["pipe", "pipe", "pipe"],
@@ -3066,10 +3066,10 @@ var init_git_info = __esm({
3066
3066
  durationMs: performance.now() - start
3067
3067
  };
3068
3068
  }
3069
- git(cwd3, cmd) {
3069
+ git(cwd4, cmd) {
3070
3070
  try {
3071
3071
  return execSync6(`git ${cmd}`, {
3072
- cwd: cwd3,
3072
+ cwd: cwd4,
3073
3073
  encoding: "utf8",
3074
3074
  timeout: 1e4,
3075
3075
  stdio: ["pipe", "pipe", "pipe"]
@@ -3103,10 +3103,10 @@ var init_background_task = __esm({
3103
3103
  BackgroundTaskManager = class {
3104
3104
  tasks = /* @__PURE__ */ new Map();
3105
3105
  nextId = 1;
3106
- spawn(command, cwd3, timeoutMs = 6e5) {
3106
+ spawn(command, cwd4, timeoutMs = 6e5) {
3107
3107
  const id = `task-${this.nextId++}`;
3108
3108
  const child = spawn2("bash", ["-c", command], {
3109
- cwd: cwd3,
3109
+ cwd: cwd4,
3110
3110
  env: { ...process.env, CI: "true", NONINTERACTIVE: "1", NO_COLOR: "1" },
3111
3111
  stdio: ["ignore", "pipe", "pipe"],
3112
3112
  detached: false
@@ -6587,7 +6587,8 @@ Commands run non-interactively (CI=true). When running scaffolding tools:
6587
6587
  requestTimeoutMs: options?.requestTimeoutMs ?? 3e5,
6588
6588
  taskTimeoutMs: options?.taskTimeoutMs ?? 12e5,
6589
6589
  compactionThreshold: options?.compactionThreshold ?? 4e4,
6590
- dynamicContext: options?.dynamicContext ?? ""
6590
+ dynamicContext: options?.dynamicContext ?? "",
6591
+ streamEnabled: options?.streamEnabled ?? false
6591
6592
  };
6592
6593
  }
6593
6594
  /** Register a tool for the agent to use */
@@ -6683,13 +6684,14 @@ Integrate this guidance into your current approach. Continue working on the task
6683
6684
  });
6684
6685
  }
6685
6686
  const compacted = this.compactMessages(messages);
6686
- const response = await this.backend.chatCompletion({
6687
+ const chatRequest = {
6687
6688
  messages: compacted,
6688
6689
  tools: toolDefs,
6689
6690
  temperature: this.options.temperature,
6690
6691
  maxTokens: this.options.maxTokens,
6691
6692
  timeoutMs: this.options.requestTimeoutMs
6692
- });
6693
+ };
6694
+ const response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
6693
6695
  totalTokens += response.usage?.totalTokens ?? 0;
6694
6696
  const choice = response.choices[0];
6695
6697
  if (!choice)
@@ -6916,6 +6918,90 @@ ${summary}
6916
6918
  }
6917
6919
  }));
6918
6920
  }
6921
+ // -------------------------------------------------------------------------
6922
+ // Streaming support — parallel path that emits token events
6923
+ // -------------------------------------------------------------------------
6924
+ /** Check whether the backend supports SSE streaming */
6925
+ hasStreamingSupport() {
6926
+ return typeof this.backend.chatCompletionStream === "function";
6927
+ }
6928
+ /**
6929
+ * Streaming request: calls the SSE endpoint, emits stream events,
6930
+ * assembles and returns the same response format as chatCompletion().
6931
+ * The non-streaming chatCompletion path is NEVER touched by this code.
6932
+ */
6933
+ async streamingRequest(request, turn) {
6934
+ const backend = this.backend;
6935
+ let content = "";
6936
+ let inThinkTag = false;
6937
+ const toolCallAccumulators = /* @__PURE__ */ new Map();
6938
+ this.emit({ type: "stream_start", turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
6939
+ for await (const chunk of backend.chatCompletionStream(request)) {
6940
+ if (this.aborted)
6941
+ break;
6942
+ if (chunk.type === "content" && chunk.content) {
6943
+ content += chunk.content;
6944
+ let kind = inThinkTag ? "thinking" : "content";
6945
+ const fragment = chunk.content;
6946
+ if (fragment.includes("<think>")) {
6947
+ inThinkTag = true;
6948
+ kind = "thinking";
6949
+ }
6950
+ if (fragment.includes("</think>")) {
6951
+ inThinkTag = false;
6952
+ kind = "content";
6953
+ }
6954
+ this.emit({
6955
+ type: "stream_token",
6956
+ content: fragment,
6957
+ streamKind: kind,
6958
+ turn,
6959
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6960
+ });
6961
+ }
6962
+ if (chunk.type === "tool_call_delta") {
6963
+ const idx = chunk.toolCallIndex ?? 0;
6964
+ if (!toolCallAccumulators.has(idx)) {
6965
+ toolCallAccumulators.set(idx, {
6966
+ id: chunk.toolCallId ?? crypto.randomUUID(),
6967
+ name: chunk.toolCallName ?? "",
6968
+ args: ""
6969
+ });
6970
+ }
6971
+ const acc = toolCallAccumulators.get(idx);
6972
+ if (chunk.toolCallName)
6973
+ acc.name = chunk.toolCallName;
6974
+ if (chunk.toolCallId)
6975
+ acc.id = chunk.toolCallId;
6976
+ if (chunk.toolCallArgs) {
6977
+ acc.args += chunk.toolCallArgs;
6978
+ this.emit({
6979
+ type: "stream_token",
6980
+ content: chunk.toolCallArgs,
6981
+ streamKind: "tool_args",
6982
+ turn,
6983
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
6984
+ });
6985
+ }
6986
+ }
6987
+ }
6988
+ this.emit({ type: "stream_end", content, turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
6989
+ const cleanContent = content.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
6990
+ const toolCalls = toolCallAccumulators.size > 0 ? Array.from(toolCallAccumulators.values()).map((tc) => {
6991
+ let args;
6992
+ try {
6993
+ args = JSON.parse(tc.args);
6994
+ } catch {
6995
+ args = { _raw: tc.args };
6996
+ }
6997
+ return { id: tc.id, name: tc.name, arguments: args };
6998
+ }) : void 0;
6999
+ return {
7000
+ choices: [{ message: { content: cleanContent || null, toolCalls } }],
7001
+ usage: void 0
7002
+ // SSE responses typically don't include usage in chunks
7003
+ };
7004
+ }
6919
7005
  };
6920
7006
  OllamaAgenticBackend = class {
6921
7007
  baseUrl;
@@ -6941,7 +7027,9 @@ ${summary}
6941
7027
  });
6942
7028
  if (!resp.ok) {
6943
7029
  const text = await resp.text().catch(() => "");
6944
- throw new Error(`Ollama HTTP ${resp.status}: ${text.slice(0, 200)}`);
7030
+ const isHtml = text.trimStart().startsWith("<!") || text.trimStart().startsWith("<html");
7031
+ const detail = isHtml ? `(received HTML error page \u2014 backend may be behind a proxy/CDN that is timing out)` : text.slice(0, 200);
7032
+ throw new Error(`Backend HTTP ${resp.status}: ${detail}`);
6945
7033
  }
6946
7034
  const data = await resp.json();
6947
7035
  const choices = data.choices ?? [];
@@ -6973,6 +7061,79 @@ ${summary}
6973
7061
  usage: usage ? { totalTokens: usage.total_tokens ?? 0 } : void 0
6974
7062
  };
6975
7063
  }
7064
+ /**
7065
+ * SSE streaming variant — yields StreamChunks as tokens arrive.
7066
+ * Uses `stream: true` and `think: true` so thinking tokens are visible.
7067
+ * The existing chatCompletion() method is completely unmodified.
7068
+ */
7069
+ async *chatCompletionStream(request) {
7070
+ const body = {
7071
+ model: this.model,
7072
+ messages: request.messages,
7073
+ tools: request.tools,
7074
+ temperature: request.temperature,
7075
+ max_tokens: request.maxTokens,
7076
+ stream: true,
7077
+ think: true
7078
+ };
7079
+ const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
7080
+ method: "POST",
7081
+ headers: { "Content-Type": "application/json" },
7082
+ body: JSON.stringify(body),
7083
+ signal: AbortSignal.timeout(request.timeoutMs)
7084
+ });
7085
+ if (!resp.ok) {
7086
+ const text = await resp.text().catch(() => "");
7087
+ const isHtml = text.trimStart().startsWith("<!") || text.trimStart().startsWith("<html");
7088
+ const detail = isHtml ? `(received HTML error page \u2014 backend may be behind a proxy/CDN that is timing out)` : text.slice(0, 200);
7089
+ throw new Error(`Backend HTTP ${resp.status}: ${detail}`);
7090
+ }
7091
+ let sseBuffer = "";
7092
+ const decoder = new TextDecoder();
7093
+ for await (const rawChunk of resp.body) {
7094
+ sseBuffer += decoder.decode(rawChunk, { stream: true });
7095
+ const parts = sseBuffer.split("\n\n");
7096
+ sseBuffer = parts.pop();
7097
+ for (const part of parts) {
7098
+ const line = part.trim();
7099
+ if (!line)
7100
+ continue;
7101
+ if (line === "data: [DONE]")
7102
+ return;
7103
+ if (!line.startsWith("data: "))
7104
+ continue;
7105
+ try {
7106
+ const data = JSON.parse(line.slice(6));
7107
+ const choices = data.choices ?? [];
7108
+ const choice = choices[0];
7109
+ if (!choice)
7110
+ continue;
7111
+ const delta = choice.delta;
7112
+ const finishReason = choice.finish_reason;
7113
+ if (delta?.content) {
7114
+ yield { type: "content", content: delta.content };
7115
+ }
7116
+ const tcDeltas = delta?.tool_calls;
7117
+ if (tcDeltas) {
7118
+ for (const tcd of tcDeltas) {
7119
+ const fn = tcd.function;
7120
+ yield {
7121
+ type: "tool_call_delta",
7122
+ toolCallIndex: tcd.index ?? 0,
7123
+ toolCallId: tcd.id || void 0,
7124
+ toolCallName: fn?.name || void 0,
7125
+ toolCallArgs: fn?.arguments || void 0
7126
+ };
7127
+ }
7128
+ }
7129
+ if (finishReason) {
7130
+ yield { type: "finish", finishReason };
7131
+ }
7132
+ } catch {
7133
+ }
7134
+ }
7135
+ }
7136
+ }
6976
7137
  };
6977
7138
  }
6978
7139
  });
@@ -7230,6 +7391,7 @@ function renderSlashHelp() {
7230
7391
  ["/update", "Check for updates and auto-install"],
7231
7392
  ["/voice", "Toggle TTS voice feedback (GLaDOS)"],
7232
7393
  ["/voice <model>", "Set voice: glados, overwatch"],
7394
+ ["/stream", "Toggle real-time token streaming (pastel syntax highlighting)"],
7233
7395
  ["/verbose", "Toggle verbose mode"],
7234
7396
  ["/clear", "Clear the screen"],
7235
7397
  ["/help", "Show this help"],
@@ -7243,6 +7405,14 @@ function renderSlashHelp() {
7243
7405
  process.stdout.write(` ${c2.cyan(cmd.padEnd(30))} ${c2.dim(desc)}
7244
7406
  `);
7245
7407
  }
7408
+ process.stdout.write(`
7409
+ ${c2.bold("Project-local overrides:")}
7410
+
7411
+ `);
7412
+ process.stdout.write(` ${c2.dim("Append")} ${c2.yellow("--local")} ${c2.dim("to save settings to .oa/settings.json (this project only).")}
7413
+ `);
7414
+ process.stdout.write(` ${c2.dim("Example:")} ${c2.cyan("/model qwen3:32b --local")} ${c2.dim("/endpoint http://remote:8000/v1 --local")}
7415
+ `);
7246
7416
  process.stdout.write(`
7247
7417
  ${c2.bold("Mid-task steering:")}
7248
7418
 
@@ -7499,6 +7669,7 @@ var init_render = __esm({
7499
7669
  "/config",
7500
7670
  "/update",
7501
7671
  "/voice",
7672
+ "/stream",
7502
7673
  "/verbose",
7503
7674
  "/clear",
7504
7675
  "/help",
@@ -7513,7 +7684,9 @@ async function handleSlashCommand(input, ctx) {
7513
7684
  if (!trimmed.startsWith("/"))
7514
7685
  return "not_a_command";
7515
7686
  const [cmd, ...rest] = trimmed.slice(1).split(/\s+/);
7516
- const arg = rest.join(" ").trim();
7687
+ const hasLocal = rest.includes("--local");
7688
+ const filteredRest = rest.filter((r) => r !== "--local");
7689
+ const arg = filteredRest.join(" ").trim();
7517
7690
  switch (cmd) {
7518
7691
  case "help":
7519
7692
  case "h":
@@ -7531,8 +7704,13 @@ async function handleSlashCommand(input, ctx) {
7531
7704
  case "verbose":
7532
7705
  case "v":
7533
7706
  ctx.setVerbose(!ctx.config.verbose);
7534
- ctx.saveSettings({ verbose: ctx.config.verbose });
7535
- renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"}`);
7707
+ if (hasLocal) {
7708
+ ctx.saveLocalSettings({ verbose: ctx.config.verbose });
7709
+ renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"} (project-local)`);
7710
+ } else {
7711
+ ctx.saveSettings({ verbose: ctx.config.verbose });
7712
+ renderInfo(`Verbose mode: ${ctx.config.verbose ? "on" : "off"}`);
7713
+ }
7536
7714
  return "handled";
7537
7715
  case "config":
7538
7716
  case "cfg":
@@ -7548,7 +7726,7 @@ async function handleSlashCommand(input, ctx) {
7548
7726
  return "handled";
7549
7727
  case "model":
7550
7728
  if (arg) {
7551
- await switchModel(arg, ctx);
7729
+ await switchModel(arg, ctx, hasLocal);
7552
7730
  } else {
7553
7731
  await showModelPicker(ctx);
7554
7732
  }
@@ -7558,25 +7736,33 @@ async function handleSlashCommand(input, ctx) {
7558
7736
  return "handled";
7559
7737
  case "endpoint":
7560
7738
  case "ep":
7561
- await handleEndpoint(arg, ctx);
7739
+ await handleEndpoint(arg, ctx, hasLocal);
7562
7740
  return "handled";
7563
7741
  case "update":
7564
7742
  case "upgrade":
7565
7743
  await handleUpdate();
7566
7744
  return "handled";
7567
7745
  case "voice": {
7746
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
7568
7747
  if (arg) {
7569
7748
  const msg = await ctx.voiceSetModel(arg);
7570
- ctx.saveSettings({ voice: true, voiceModel: arg });
7571
- renderInfo(msg);
7749
+ save({ voice: true, voiceModel: arg });
7750
+ renderInfo(msg + (hasLocal ? " (project-local)" : ""));
7572
7751
  } else {
7573
7752
  const msg = await ctx.voiceToggle();
7574
7753
  const isOn = msg.toLowerCase().includes("enabled") || msg.toLowerCase().includes("on");
7575
- ctx.saveSettings({ voice: isOn });
7576
- renderInfo(msg);
7754
+ save({ voice: isOn });
7755
+ renderInfo(msg + (hasLocal ? " (project-local)" : ""));
7577
7756
  }
7578
7757
  return "handled";
7579
7758
  }
7759
+ case "stream": {
7760
+ const isOn = ctx.streamToggle();
7761
+ const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
7762
+ save({ stream: isOn });
7763
+ renderInfo(`Token streaming: ${isOn ? "on" : "off"}${hasLocal ? " (project-local)" : ""}` + (isOn ? " \u2014 thinking tokens in grey italics, responses with pastel syntax highlighting" : ""));
7764
+ return "handled";
7765
+ }
7580
7766
  default:
7581
7767
  renderWarning(`Unknown command: /${cmd}. Type /help for available commands.`);
7582
7768
  return "handled";
@@ -7602,7 +7788,7 @@ async function showModelPicker(ctx) {
7602
7788
  renderError(`Failed to fetch models: ${err instanceof Error ? err.message : String(err)}`);
7603
7789
  }
7604
7790
  }
7605
- async function handleEndpoint(arg, ctx) {
7791
+ async function handleEndpoint(arg, ctx, local = false) {
7606
7792
  if (!arg) {
7607
7793
  process.stdout.write(`
7608
7794
  ${c2.bold("Current endpoint:")}
@@ -7665,14 +7851,19 @@ async function handleEndpoint(arg, ctx) {
7665
7851
  renderInfo("Setting endpoint anyway \u2014 it may come online later.");
7666
7852
  }
7667
7853
  ctx.setEndpoint(url, backendType, apiKey);
7668
- setConfigValue("backendUrl", url);
7669
- setConfigValue("backendType", backendType);
7670
- if (apiKey) {
7671
- setConfigValue("apiKey", apiKey);
7854
+ const endpointSettings = { backendUrl: url, backendType, ...apiKey ? { apiKey } : {} };
7855
+ if (local) {
7856
+ ctx.saveLocalSettings(endpointSettings);
7857
+ } else {
7858
+ setConfigValue("backendUrl", url);
7859
+ setConfigValue("backendType", backendType);
7860
+ if (apiKey) {
7861
+ setConfigValue("apiKey", apiKey);
7862
+ }
7863
+ ctx.saveSettings(endpointSettings);
7672
7864
  }
7673
- ctx.saveSettings({ backendUrl: url, backendType, ...apiKey ? { apiKey } : {} });
7674
7865
  process.stdout.write(`
7675
- ${c2.green("\u2714")} Endpoint updated and saved:
7866
+ ${c2.green("\u2714")} Endpoint updated and saved${local ? " (project-local)" : ""}:
7676
7867
  `);
7677
7868
  process.stdout.write(` ${c2.cyan("URL".padEnd(8))} ${url}
7678
7869
  `);
@@ -7738,7 +7929,7 @@ async function handleUpdate() {
7738
7929
  `);
7739
7930
  restartProcess();
7740
7931
  }
7741
- async function switchModel(query, ctx) {
7932
+ async function switchModel(query, ctx, local = false) {
7742
7933
  try {
7743
7934
  const models = await fetchOllamaModels(ctx.config.backendUrl);
7744
7935
  const match = findModel(models, query);
@@ -7752,8 +7943,15 @@ async function switchModel(query, ctx) {
7752
7943
  }
7753
7944
  const oldModel = ctx.config.model;
7754
7945
  ctx.setModel(match.name);
7755
- ctx.saveSettings({ model: match.name });
7946
+ if (local) {
7947
+ ctx.saveLocalSettings({ model: match.name });
7948
+ } else {
7949
+ ctx.saveSettings({ model: match.name });
7950
+ }
7756
7951
  renderModelSwitch(oldModel, match.name);
7952
+ if (local) {
7953
+ renderInfo("Saved as project-local override.");
7954
+ }
7757
7955
  } catch (err) {
7758
7956
  renderError(`Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
7759
7957
  }
@@ -7856,8 +8054,8 @@ function modelSupportsToolCalling(modelName) {
7856
8054
  return false;
7857
8055
  }
7858
8056
  function ask(rl, question) {
7859
- return new Promise((resolve13) => {
7860
- rl.question(question, (answer) => resolve13(answer.trim()));
8057
+ return new Promise((resolve14) => {
8058
+ rl.question(question, (answer) => resolve14(answer.trim()));
7861
8059
  });
7862
8060
  }
7863
8061
  function pullModelWithAutoUpdate(tag) {
@@ -9284,7 +9482,7 @@ var init_voice = __esm({
9284
9482
  const cmd = this.getPlayCommand(path);
9285
9483
  if (!cmd)
9286
9484
  return;
9287
- return new Promise((resolve13) => {
9485
+ return new Promise((resolve14) => {
9288
9486
  const child = nodeSpawn(cmd[0], cmd.slice(1), {
9289
9487
  stdio: "ignore",
9290
9488
  detached: false
@@ -9293,16 +9491,16 @@ var init_voice = __esm({
9293
9491
  child.on("close", () => {
9294
9492
  if (this.currentPlayback === child)
9295
9493
  this.currentPlayback = null;
9296
- resolve13();
9494
+ resolve14();
9297
9495
  });
9298
9496
  child.on("error", () => {
9299
9497
  if (this.currentPlayback === child)
9300
9498
  this.currentPlayback = null;
9301
- resolve13();
9499
+ resolve14();
9302
9500
  });
9303
9501
  setTimeout(() => {
9304
9502
  this.killPlayback();
9305
- resolve13();
9503
+ resolve14();
9306
9504
  }, 15e3);
9307
9505
  });
9308
9506
  }
@@ -9472,6 +9670,266 @@ Error: ${err instanceof Error ? err.message : String(err)}`);
9472
9670
  }
9473
9671
  });
9474
9672
 
9673
+ // packages/cli/dist/tui/stream-renderer.js
9674
+ function fg256(code, text) {
9675
+ return isTTY4 ? `\x1B[38;5;${code}m${text}\x1B[0m` : text;
9676
+ }
9677
+ function dimText(text) {
9678
+ return isTTY4 ? `\x1B[2m${text}\x1B[0m` : text;
9679
+ }
9680
+ function dimItalic(text) {
9681
+ return isTTY4 ? `\x1B[2;3m${text}\x1B[0m` : text;
9682
+ }
9683
+ var isTTY4, PASTEL, StreamRenderer;
9684
+ var init_stream_renderer = __esm({
9685
+ "packages/cli/dist/tui/stream-renderer.js"() {
9686
+ "use strict";
9687
+ isTTY4 = process.stdout.isTTY ?? false;
9688
+ PASTEL = {
9689
+ key: 222,
9690
+ // light gold — JSON keys
9691
+ string: 183,
9692
+ // light lavender — "string values"
9693
+ number: 156,
9694
+ // soft green — 42, 3.14
9695
+ boolean: 114,
9696
+ // mint green — true, false
9697
+ null: 109,
9698
+ // grey-blue — null
9699
+ bracket: 75,
9700
+ // soft blue — { } [ ]
9701
+ colon: 245,
9702
+ // neutral grey — : ,
9703
+ keyword: 117,
9704
+ // sky blue — function, return, if, else
9705
+ comment: 243,
9706
+ // dim grey — // comments
9707
+ thinking: 245,
9708
+ // neutral grey for thinking tokens
9709
+ toolArg: 111
9710
+ // dim periwinkle for tool arg tokens
9711
+ };
9712
+ StreamRenderer = class {
9713
+ lineBuffer = "";
9714
+ inThinkBlock = false;
9715
+ inCodeBlock = false;
9716
+ codeLang = "";
9717
+ lineStarted = false;
9718
+ flushTimer = null;
9719
+ enabled = false;
9720
+ tokenCount = 0;
9721
+ startTime = 0;
9722
+ /** Track if we're mid-tool-arg display */
9723
+ inToolArgs = false;
9724
+ /** Called when a new model response starts streaming */
9725
+ onStreamStart() {
9726
+ this.lineBuffer = "";
9727
+ this.inThinkBlock = false;
9728
+ this.inCodeBlock = false;
9729
+ this.codeLang = "";
9730
+ this.lineStarted = false;
9731
+ this.inToolArgs = false;
9732
+ this.enabled = true;
9733
+ this.tokenCount = 0;
9734
+ this.startTime = Date.now();
9735
+ this.cancelFlush();
9736
+ }
9737
+ /**
9738
+ * Feed a streamed token into the renderer.
9739
+ * Tokens are buffered per-line and flushed with syntax highlighting.
9740
+ */
9741
+ write(token, kind) {
9742
+ if (!this.enabled)
9743
+ return;
9744
+ this.tokenCount++;
9745
+ if (kind === "tool_args" && !this.inToolArgs) {
9746
+ this.flushPartial(kind);
9747
+ this.inToolArgs = true;
9748
+ } else if (kind !== "tool_args" && this.inToolArgs) {
9749
+ this.flushPartial(kind);
9750
+ this.inToolArgs = false;
9751
+ }
9752
+ for (const char of token) {
9753
+ this.lineBuffer += char;
9754
+ if (char === "\n") {
9755
+ this.flushLine(kind);
9756
+ }
9757
+ }
9758
+ this.scheduleFlush(kind);
9759
+ }
9760
+ /** Called when streaming ends for this response */
9761
+ onStreamEnd() {
9762
+ if (!this.enabled)
9763
+ return;
9764
+ this.cancelFlush();
9765
+ if (this.lineBuffer.length > 0) {
9766
+ const kind = this.inThinkBlock ? "thinking" : this.inToolArgs ? "tool_args" : "content";
9767
+ this.writeHighlighted(this.lineBuffer, kind);
9768
+ this.lineBuffer = "";
9769
+ }
9770
+ if (this.lineStarted) {
9771
+ process.stdout.write("\n");
9772
+ this.lineStarted = false;
9773
+ }
9774
+ this.enabled = false;
9775
+ }
9776
+ /** Get streaming stats */
9777
+ getStats() {
9778
+ return {
9779
+ tokens: this.tokenCount,
9780
+ durationMs: Date.now() - this.startTime
9781
+ };
9782
+ }
9783
+ // -------------------------------------------------------------------------
9784
+ // Internal rendering
9785
+ // -------------------------------------------------------------------------
9786
+ /**
9787
+ * Flush a complete line (ending with \n) with full syntax highlighting.
9788
+ */
9789
+ flushLine(kind) {
9790
+ const line = this.lineBuffer;
9791
+ this.lineBuffer = "";
9792
+ if (!line || line === "\n") {
9793
+ if (this.lineStarted) {
9794
+ process.stdout.write("\n");
9795
+ this.lineStarted = false;
9796
+ } else {
9797
+ process.stdout.write("\n");
9798
+ }
9799
+ return;
9800
+ }
9801
+ if (line.includes("<think>")) {
9802
+ this.inThinkBlock = true;
9803
+ const after = line.replace(/<think>/g, "");
9804
+ if (after.trim()) {
9805
+ this.writeHighlighted(after, "thinking");
9806
+ }
9807
+ return;
9808
+ }
9809
+ if (line.includes("</think>")) {
9810
+ this.inThinkBlock = false;
9811
+ const after = line.replace(/<\/think>/g, "");
9812
+ if (after.trim()) {
9813
+ this.writeHighlighted(after, "content");
9814
+ }
9815
+ return;
9816
+ }
9817
+ const trimmedLine = line.replace(/\n$/, "");
9818
+ if (trimmedLine.trimStart().startsWith("```")) {
9819
+ if (this.inCodeBlock) {
9820
+ this.writeRaw(dimText(" \u23BF ") + dimText("```") + "\n");
9821
+ this.inCodeBlock = false;
9822
+ this.codeLang = "";
9823
+ this.lineStarted = false;
9824
+ } else {
9825
+ this.codeLang = trimmedLine.replace(/```/g, "").trim();
9826
+ this.writeRaw(dimText(" \u23BF ") + dimText("```" + this.codeLang) + "\n");
9827
+ this.inCodeBlock = true;
9828
+ this.lineStarted = false;
9829
+ }
9830
+ return;
9831
+ }
9832
+ const effectiveKind = this.inThinkBlock ? "thinking" : kind;
9833
+ this.writeHighlighted(line, effectiveKind);
9834
+ }
9835
+ /**
9836
+ * Write a highlighted line/fragment to stdout.
9837
+ */
9838
+ writeHighlighted(text, kind) {
9839
+ const raw = text.replace(/\n$/, "");
9840
+ if (!raw)
9841
+ return;
9842
+ const prefix = this.lineStarted ? "" : " \u23BF ";
9843
+ let rendered;
9844
+ switch (kind) {
9845
+ case "thinking":
9846
+ rendered = dimItalic(raw);
9847
+ break;
9848
+ case "tool_args":
9849
+ rendered = this.highlightJson(raw, true);
9850
+ break;
9851
+ case "content":
9852
+ if (this.inCodeBlock) {
9853
+ rendered = this.highlightCode(raw);
9854
+ } else if (this.looksLikeJson(raw)) {
9855
+ rendered = this.highlightJson(raw, false);
9856
+ } else {
9857
+ rendered = raw;
9858
+ }
9859
+ break;
9860
+ }
9861
+ const hasNewline = text.endsWith("\n");
9862
+ this.writeRaw(dimText(prefix) + rendered + (hasNewline ? "\n" : ""));
9863
+ this.lineStarted = !hasNewline;
9864
+ }
9865
+ /** Write raw ANSI text to stdout */
9866
+ writeRaw(text) {
9867
+ process.stdout.write(text);
9868
+ }
9869
+ /** Flush partial buffer (non-newline-terminated tokens) */
9870
+ flushPartial(kind) {
9871
+ if (this.lineBuffer.length === 0)
9872
+ return;
9873
+ const effectiveKind = this.inThinkBlock ? "thinking" : kind;
9874
+ this.writeHighlighted(this.lineBuffer, effectiveKind);
9875
+ this.lineBuffer = "";
9876
+ }
9877
+ /** Schedule a timer to flush partial buffer (for streaming smoothness) */
9878
+ scheduleFlush(kind) {
9879
+ this.cancelFlush();
9880
+ this.flushTimer = setTimeout(() => {
9881
+ if (this.lineBuffer.length > 0) {
9882
+ this.flushPartial(kind);
9883
+ }
9884
+ }, 80);
9885
+ }
9886
+ cancelFlush() {
9887
+ if (this.flushTimer) {
9888
+ clearTimeout(this.flushTimer);
9889
+ this.flushTimer = null;
9890
+ }
9891
+ }
9892
+ // -------------------------------------------------------------------------
9893
+ // Syntax highlighting — pastel palette
9894
+ // -------------------------------------------------------------------------
9895
+ /** Check if a string looks like JSON (starts with { [ " or has key: patterns) */
9896
+ looksLikeJson(text) {
9897
+ const trimmed = text.trimStart();
9898
+ return trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith("}") || trimmed.startsWith("]") || /^\s*"[^"]+"\s*:/.test(trimmed);
9899
+ }
9900
+ /**
9901
+ * Highlight a JSON line with pastel colors.
9902
+ * @param dim If true, apply dimmer colors (for tool args)
9903
+ */
9904
+ highlightJson(line, dim) {
9905
+ const colorKey = dim ? PASTEL.toolArg : PASTEL.key;
9906
+ const colorStr = dim ? PASTEL.toolArg : PASTEL.string;
9907
+ let result = line;
9908
+ result = result.replace(/"([^"]*)"(\s*:)/g, (_m, key, colon) => fg256(colorKey, `"${key}"`) + fg256(PASTEL.colon, colon));
9909
+ result = result.replace(/(:\s*)"([^"]*)"/g, (_m, prefix, val) => fg256(PASTEL.colon, prefix) + fg256(colorStr, `"${val}"`));
9910
+ result = result.replace(/(:\s*)(\d+\.?\d*)/g, (_m, prefix, num) => fg256(PASTEL.colon, prefix) + fg256(PASTEL.number, num));
9911
+ result = result.replace(/(:\s*)(true|false)/g, (_m, prefix, bool) => fg256(PASTEL.colon, prefix) + fg256(PASTEL.boolean, bool));
9912
+ result = result.replace(/(:\s*)(null)/g, (_m, prefix, n) => fg256(PASTEL.colon, prefix) + fg256(PASTEL.null, n));
9913
+ result = result.replace(/([{}[\]])/g, (_m, b) => fg256(PASTEL.bracket, b));
9914
+ return dim ? dimText(result) : result;
9915
+ }
9916
+ /**
9917
+ * Highlight a code line with basic pastel syntax coloring.
9918
+ */
9919
+ highlightCode(line) {
9920
+ let result = line;
9921
+ result = result.replace(/"([^"]*)"/g, (_m, s) => fg256(PASTEL.string, `"${s}"`));
9922
+ result = result.replace(/'([^']*)'/g, (_m, s) => fg256(PASTEL.string, `'${s}'`));
9923
+ result = result.replace(/\b(\d+\.?\d*)\b/g, (_m, n) => fg256(PASTEL.number, n));
9924
+ result = result.replace(/\b(true|false|null|undefined|None|True|False)\b/g, (_m, kw) => fg256(PASTEL.boolean, kw));
9925
+ result = result.replace(/\b(function|const|let|var|return|if|else|for|while|import|export|from|class|async|await|def|self|try|catch|finally|throw|new|typeof|instanceof)\b/g, (_m, kw) => fg256(PASTEL.keyword, kw));
9926
+ result = result.replace(/(\/\/.*$|#.*$)/gm, (_m, c3) => fg256(PASTEL.comment, c3));
9927
+ return result;
9928
+ }
9929
+ };
9930
+ }
9931
+ });
9932
+
9475
9933
  // packages/cli/dist/tui/interactive.js
9476
9934
  import * as readline2 from "node:readline";
9477
9935
  import { cwd } from "node:process";
@@ -9639,7 +10097,7 @@ Use task_status("${taskId}") or task_output("${taskId}") to check progress.`
9639
10097
  }
9640
10098
  };
9641
10099
  }
9642
- function startTask(task, config, repoRoot, voice) {
10100
+ function startTask(task, config, repoRoot, voice, stream) {
9643
10101
  const projectCtx = buildProjectContext(repoRoot);
9644
10102
  const dynamicContext = formatContextForPrompt(projectCtx);
9645
10103
  const backend = new OllamaAgenticBackend(config.backendUrl.replace(/\/$/, ""), config.model);
@@ -9650,7 +10108,8 @@ function startTask(task, config, repoRoot, voice) {
9650
10108
  requestTimeoutMs: config.timeoutMs,
9651
10109
  taskTimeoutMs: config.timeoutMs * 4,
9652
10110
  compactionThreshold: 4e4,
9653
- dynamicContext
10111
+ dynamicContext,
10112
+ streamEnabled: stream?.enabled ?? false
9654
10113
  });
9655
10114
  runner.registerTools(buildTools(repoRoot, config));
9656
10115
  runner.onEvent((event) => {
@@ -9674,10 +10133,23 @@ function startTask(task, config, repoRoot, voice) {
9674
10133
  }
9675
10134
  break;
9676
10135
  case "model_response":
9677
- if (config.verbose && event.content) {
10136
+ if (config.verbose && !stream?.enabled && event.content) {
9678
10137
  renderAssistantText(event.content);
9679
10138
  }
9680
10139
  break;
10140
+ case "stream_start":
10141
+ if (stream?.enabled)
10142
+ stream.renderer.onStreamStart();
10143
+ break;
10144
+ case "stream_token":
10145
+ if (stream?.enabled) {
10146
+ stream.renderer.write(event.content ?? "", event.streamKind ?? "content");
10147
+ }
10148
+ break;
10149
+ case "stream_end":
10150
+ if (stream?.enabled)
10151
+ stream.renderer.onStreamEnd();
10152
+ break;
9681
10153
  case "user_interrupt":
9682
10154
  break;
9683
10155
  case "compaction":
@@ -9739,6 +10211,15 @@ async function startInteractive(config, repoPath) {
9739
10211
  config = { ...config, apiKey: savedSettings.apiKey };
9740
10212
  if (savedSettings.verbose !== void 0)
9741
10213
  config = { ...config, verbose: savedSettings.verbose };
10214
+ if (savedSettings.maxRetries !== void 0)
10215
+ config = { ...config, maxRetries: savedSettings.maxRetries };
10216
+ if (savedSettings.timeoutMs !== void 0)
10217
+ config = { ...config, timeoutMs: savedSettings.timeoutMs };
10218
+ if (savedSettings.dryRun !== void 0)
10219
+ config = { ...config, dryRun: savedSettings.dryRun };
10220
+ if (savedSettings.dbPath)
10221
+ config = { ...config, dbPath: savedSettings.dbPath };
10222
+ let streamEnabled = savedSettings.stream ?? false;
9742
10223
  if (!isResumed) {
9743
10224
  const needsSetup = isFirstRun() || !await isModelAvailable(config);
9744
10225
  if (needsSetup && config.backendType === "ollama") {
@@ -9782,6 +10263,7 @@ async function startInteractive(config, repoPath) {
9782
10263
  `);
9783
10264
  }
9784
10265
  const voiceEngine = new VoiceEngine();
10266
+ const streamRenderer = new StreamRenderer();
9785
10267
  if (savedSettings.voice) {
9786
10268
  voiceEngine.toggle().catch(() => {
9787
10269
  });
@@ -9856,12 +10338,22 @@ async function startInteractive(config, repoPath) {
9856
10338
  async voiceSetModel(id) {
9857
10339
  return voiceEngine.setModel(id);
9858
10340
  },
10341
+ streamToggle() {
10342
+ streamEnabled = !streamEnabled;
10343
+ return streamEnabled;
10344
+ },
9859
10345
  saveSettings(settings) {
9860
10346
  try {
9861
10347
  saveProjectSettings(repoRoot, settings);
9862
10348
  saveGlobalSettings(settings);
9863
10349
  } catch {
9864
10350
  }
10351
+ },
10352
+ saveLocalSettings(settings) {
10353
+ try {
10354
+ saveProjectSettings(repoRoot, settings);
10355
+ } catch {
10356
+ }
9865
10357
  }
9866
10358
  };
9867
10359
  showPrompt();
@@ -9926,7 +10418,10 @@ ${c2.dim("Goodbye!")}
9926
10418
  }
9927
10419
  renderUserMessage(isImage ? `[Image: ${cleanPath}]` : fullInput);
9928
10420
  try {
9929
- const task = startTask(fullInput, currentConfig, repoRoot, voiceEngine);
10421
+ const task = startTask(fullInput, currentConfig, repoRoot, voiceEngine, {
10422
+ enabled: streamEnabled,
10423
+ renderer: streamRenderer
10424
+ });
9930
10425
  activeTask = task;
9931
10426
  showPrompt();
9932
10427
  await task.promise;
@@ -10022,6 +10517,7 @@ var init_interactive = __esm({
10022
10517
  init_render();
10023
10518
  init_carousel();
10024
10519
  init_voice();
10520
+ init_stream_renderer();
10025
10521
  taskManager = new BackgroundTaskManager();
10026
10522
  }
10027
10523
  });
@@ -10489,8 +10985,17 @@ var config_exports = {};
10489
10985
  __export(config_exports, {
10490
10986
  configCommand: () => configCommand
10491
10987
  });
10492
- import { join as join19 } from "node:path";
10988
+ import { join as join19, resolve as resolve13 } from "node:path";
10493
10989
  import { homedir as homedir7 } from "node:os";
10990
+ import { cwd as cwd3 } from "node:process";
10991
+ function coerceForSettings(key, value) {
10992
+ if (INT_KEYS.has(key))
10993
+ return parseInt(value, 10);
10994
+ if (BOOL_KEYS.has(key)) {
10995
+ return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes";
10996
+ }
10997
+ return value;
10998
+ }
10494
10999
  async function configCommand(opts, config) {
10495
11000
  if (opts.subCommand === "set") {
10496
11001
  return handleSet(opts, config);
@@ -10501,9 +11006,11 @@ async function configCommand(opts, config) {
10501
11006
  return handleShow(opts, config);
10502
11007
  }
10503
11008
  function handleShow(opts, config) {
11009
+ const repoRoot = resolve13(opts.repoPath ?? cwd3());
10504
11010
  printHeader("Configuration");
10505
- printSection("Active Settings");
11011
+ printSection("Active Settings (merged)");
10506
11012
  printKeyValue("backendUrl", config.backendUrl, 2);
11013
+ printKeyValue("backendType", config.backendType, 2);
10507
11014
  printKeyValue("model", config.model, 2);
10508
11015
  printKeyValue("apiKey", config.apiKey ? "[set]" : "[not set]", 2);
10509
11016
  printKeyValue("maxRetries", String(config.maxRetries), 2);
@@ -10511,18 +11018,34 @@ function handleShow(opts, config) {
10511
11018
  printKeyValue("dryRun", String(config.dryRun), 2);
10512
11019
  printKeyValue("verbose", String(config.verbose), 2);
10513
11020
  printKeyValue("dbPath", config.dbPath, 2);
11021
+ const projectSettings = loadProjectSettings(repoRoot);
11022
+ const projectKeys = Object.entries(projectSettings).filter(([, v]) => v !== void 0);
11023
+ if (projectKeys.length > 0) {
11024
+ printSection(`Project Overrides (.oa/settings.json)`);
11025
+ for (const [k, v] of projectKeys) {
11026
+ printKeyValue(k, String(v), 2);
11027
+ }
11028
+ } else {
11029
+ printSection("Project Overrides");
11030
+ printInfo(" (none \u2014 use 'config set KEY VALUE --local' to add)");
11031
+ }
11032
+ const globalSettings = loadGlobalSettings();
11033
+ const globalKeys = Object.entries(globalSettings).filter(([, v]) => v !== void 0);
11034
+ if (globalKeys.length > 0) {
11035
+ printSection("Global Settings (~/.open-agents/settings.json)");
11036
+ for (const [k, v] of globalKeys) {
11037
+ printKeyValue(k, String(v), 2);
11038
+ }
11039
+ }
10514
11040
  printSection("Config File");
10515
11041
  printInfo(`~/.open-agents/config.json (${join19(homedir7(), ".open-agents", "config.json")})`);
10516
- printSection("Environment Variables");
10517
- printInfo("OPEN_AGENTS_BACKEND_URL \u2014 override backendUrl");
10518
- printInfo("OPEN_AGENTS_MODEL \u2014 override model");
10519
- printInfo("OPEN_AGENTS_API_KEY \u2014 override apiKey");
10520
- printInfo("OPEN_AGENTS_MAX_RETRIES \u2014 override maxRetries");
10521
- printInfo("OPEN_AGENTS_TIMEOUT_MS \u2014 override timeoutMs");
10522
- printInfo("OPEN_AGENTS_DRY_RUN \u2014 override dryRun (true/false)");
10523
- printInfo("OPEN_AGENTS_VERBOSE \u2014 override verbose (true/false)");
10524
- printInfo("OPEN_AGENTS_DB_PATH \u2014 override dbPath");
10525
- printInfo("VLLM_BASE_URL \u2014 fallback for backendUrl");
11042
+ printSection("Priority Chain");
11043
+ printInfo(" 1. CLI flags (--model, --backend-url, etc.)");
11044
+ printInfo(" 2. Project .oa/settings.json (--local)");
11045
+ printInfo(" 3. Global ~/.open-agents/settings.json");
11046
+ printInfo(" 4. Environment variables (OPEN_AGENTS_*)");
11047
+ printInfo(" 5. Global ~/.open-agents/config.json");
11048
+ printInfo(" 6. Built-in defaults");
10526
11049
  if (opts.verbose) {
10527
11050
  printSection("All Settable Keys");
10528
11051
  for (const [key, desc] of Object.entries(CONFIG_KEYS)) {
@@ -10533,7 +11056,7 @@ function handleShow(opts, config) {
10533
11056
  function handleSet(opts, _config) {
10534
11057
  const { key, value } = opts;
10535
11058
  if (!key) {
10536
- printError("Usage: open-agents config set KEY VALUE");
11059
+ printError("Usage: open-agents config set KEY VALUE [--local]");
10537
11060
  printInfo("Run 'open-agents config keys' to see available keys");
10538
11061
  process.exit(1);
10539
11062
  }
@@ -10547,37 +11070,64 @@ function handleSet(opts, _config) {
10547
11070
  printInfo("Run 'open-agents config keys' to see available keys");
10548
11071
  process.exit(1);
10549
11072
  }
10550
- try {
10551
- setConfigValue(key, value);
10552
- printSuccess(`Config updated: ${key} = ${value}`);
10553
- printInfo(`Saved to ~/.open-agents/config.json`);
10554
- } catch (err) {
10555
- printError(`Failed to save config: ${err instanceof Error ? err.message : String(err)}`);
10556
- process.exit(1);
11073
+ if (opts.local) {
11074
+ const repoRoot = resolve13(opts.repoPath ?? cwd3());
11075
+ try {
11076
+ initOaDirectory(repoRoot);
11077
+ const coerced = coerceForSettings(key, value);
11078
+ saveProjectSettings(repoRoot, { [key]: coerced });
11079
+ printSuccess(`Project override set: ${key} = ${value}`);
11080
+ printInfo(`Saved to ${join19(repoRoot, ".oa", "settings.json")}`);
11081
+ printInfo("This override applies only when running in this workspace.");
11082
+ } catch (err) {
11083
+ printError(`Failed to save: ${err instanceof Error ? err.message : String(err)}`);
11084
+ process.exit(1);
11085
+ }
11086
+ } else {
11087
+ try {
11088
+ setConfigValue(key, value);
11089
+ printSuccess(`Config updated: ${key} = ${value}`);
11090
+ printInfo(`Saved to ~/.open-agents/config.json`);
11091
+ printInfo("Tip: Use --local to set project-specific overrides.");
11092
+ } catch (err) {
11093
+ printError(`Failed to save config: ${err instanceof Error ? err.message : String(err)}`);
11094
+ process.exit(1);
11095
+ }
10557
11096
  }
10558
11097
  }
10559
11098
  function handleKeys() {
10560
11099
  printHeader("Config Keys");
11100
+ printInfo("All keys can be set globally or per-project (--local):\n");
10561
11101
  for (const [key, desc] of Object.entries(CONFIG_KEYS)) {
10562
11102
  printKeyValue(key, desc, 2);
10563
11103
  }
11104
+ printInfo("\nUsage:");
11105
+ printInfo(" oa config set model qwen3.5:122b # global default");
11106
+ printInfo(" oa config set model qwen3.5:122b --local # this project only");
10564
11107
  }
10565
- var CONFIG_KEYS;
11108
+ var CONFIG_KEYS, INT_KEYS, BOOL_KEYS;
10566
11109
  var init_config3 = __esm({
10567
11110
  "packages/cli/dist/commands/config.js"() {
10568
11111
  "use strict";
10569
11112
  init_config();
11113
+ init_oa_directory();
10570
11114
  init_output();
10571
11115
  CONFIG_KEYS = {
10572
- backendUrl: "vLLM backend base URL",
10573
- model: "Model name served by vLLM",
11116
+ backendUrl: "Backend base URL (Ollama or OpenAI-compatible)",
11117
+ backendType: "Backend type: ollama, vllm, fake",
11118
+ model: "Model name to use",
10574
11119
  apiKey: "Bearer token for authenticated deployments",
10575
11120
  maxRetries: "Maximum HTTP retries (integer)",
10576
11121
  timeoutMs: "Per-request timeout in milliseconds (integer)",
10577
11122
  dryRun: "Dry-run mode - patches not written (true/false)",
10578
11123
  verbose: "Verbose output (true/false)",
10579
- dbPath: "Path to SQLite memory database"
11124
+ dbPath: "Path to SQLite memory database",
11125
+ voice: "Enable TTS voice feedback (true/false)",
11126
+ voiceModel: "TTS voice model: glados, overwatch",
11127
+ stream: "Enable real-time token streaming with pastel syntax highlighting (true/false)"
10580
11128
  };
11129
+ INT_KEYS = /* @__PURE__ */ new Set(["maxRetries", "timeoutMs"]);
11130
+ BOOL_KEYS = /* @__PURE__ */ new Set(["dryRun", "verbose", "voice", "stream"]);
10581
11131
  }
10582
11132
  });
10583
11133
 
@@ -10678,7 +11228,7 @@ async function serveVllm(opts, config) {
10678
11228
  await runVllmServer(args, opts.verbose ?? false);
10679
11229
  }
10680
11230
  async function runVllmServer(args, verbose) {
10681
- return new Promise((resolve13, reject) => {
11231
+ return new Promise((resolve14, reject) => {
10682
11232
  const child = spawn3("python", args, {
10683
11233
  stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
10684
11234
  env: { ...process.env }
@@ -10713,10 +11263,10 @@ async function runVllmServer(args, verbose) {
10713
11263
  child.once("exit", (code, signal) => {
10714
11264
  if (signal) {
10715
11265
  printInfo(`vLLM server stopped by signal ${signal}`);
10716
- resolve13();
11266
+ resolve14();
10717
11267
  } else if (code === 0) {
10718
11268
  printSuccess("vLLM server exited cleanly");
10719
- resolve13();
11269
+ resolve14();
10720
11270
  } else {
10721
11271
  printError(`vLLM server exited with code ${code}`);
10722
11272
  reject(new Error(`vLLM exited with code ${code}`));
@@ -11060,6 +11610,7 @@ function parseCliArgs(argv) {
11060
11610
  "max-retries": { type: "string" },
11061
11611
  "timeout-ms": { type: "string" },
11062
11612
  offline: { type: "boolean" },
11613
+ local: { type: "boolean", short: "l" },
11063
11614
  port: { type: "string" },
11064
11615
  suite: { type: "string" },
11065
11616
  help: { type: "boolean", short: "h" },
@@ -11081,6 +11632,7 @@ function parseCliArgs(argv) {
11081
11632
  maxRetries: typeof values["max-retries"] === "string" ? parseInt(values["max-retries"], 10) : void 0,
11082
11633
  timeoutMs: typeof values["timeout-ms"] === "string" ? parseInt(values["timeout-ms"], 10) : void 0,
11083
11634
  offline: values.offline === true,
11635
+ local: values.local === true,
11084
11636
  help: values.help === true,
11085
11637
  version: values.version === true
11086
11638
  };
@@ -11145,6 +11697,7 @@ Flags:
11145
11697
  -r, --repo <path> Repository root (default: cwd)
11146
11698
  --dry-run Validate patches, don't write to disk
11147
11699
  --offline Use FakeBackend, no backend connection needed
11700
+ -l, --local Save settings to .oa/settings.json (project-local)
11148
11701
  -v, --verbose Verbose output
11149
11702
  --max-retries <n> Max retries per model request
11150
11703
  --timeout-ms <ms> Overall task timeout
@@ -11239,7 +11792,9 @@ async function main() {
11239
11792
  subCommand: parsed.configSubCommand,
11240
11793
  key: parsed.configKey,
11241
11794
  value: parsed.configValue,
11242
- verbose: parsed.verbose
11795
+ verbose: parsed.verbose,
11796
+ local: parsed.local,
11797
+ repoPath: parsed.repoPath
11243
11798
  }, config);
11244
11799
  break;
11245
11800
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",