agent.libx.js 0.87.2 → 0.89.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2482,6 +2482,30 @@ function checkSyntax(path, content) {
2482
2482
  }
2483
2483
  }
2484
2484
 
2485
+ // src/reasoning.ts
2486
+ var BUDGET = { low: 2048, medium: 8192, high: 24576 };
2487
+ function toLabel(effort) {
2488
+ if (typeof effort !== "number") return effort === "off" ? "low" : effort;
2489
+ return effort <= BUDGET.low ? "low" : effort < BUDGET.high ? "medium" : "high";
2490
+ }
2491
+ function toBudget(effort) {
2492
+ return typeof effort === "number" ? effort : BUDGET[effort];
2493
+ }
2494
+ function reasoningToChatFragment(model, effort) {
2495
+ if (effort == null || effort === "off") return {};
2496
+ const provider = model.split("/")[0];
2497
+ switch (provider) {
2498
+ case "anthropic": {
2499
+ const budget = toBudget(effort);
2500
+ return { providerOptions: { thinking: { type: "enabled", budget_tokens: budget } }, maxTokens: budget + 8192 };
2501
+ }
2502
+ case "openai":
2503
+ return { providerOptions: { reasoning_effort: toLabel(effort) } };
2504
+ default:
2505
+ return {};
2506
+ }
2507
+ }
2508
+
2485
2509
  // src/Agent.ts
2486
2510
  var log3 = forComponent("Agent");
2487
2511
  var AgentOptions = class {
@@ -2557,6 +2581,12 @@ var AgentOptions = class {
2557
2581
  /** Opt-in: after a write-class tool runs, run `command` over the VFS and append any failure to the tool result.
2558
2582
  * `tools` defaults to ['Write','Edit','MultiEdit','ApplyEdits']. */
2559
2583
  autoTest;
2584
+ /** Provider-specific options forwarded to ai.chat() (e.g. cursor mcpServers, cwd). */
2585
+ providerOptions;
2586
+ /** Extended-thinking / reasoning effort, normalized across providers (anthropic, openai).
2587
+ * `'off'`/undefined = none; `'low'|'medium'|'high'` or a raw token budget. Mapped to the
2588
+ * provider-specific request shape via {@link reasoningToChatFragment}; explicit `providerOptions` wins. */
2589
+ reasoning;
2560
2590
  };
2561
2591
  var Agent = class _Agent {
2562
2592
  options;
@@ -2572,6 +2602,12 @@ var Agent = class _Agent {
2572
2602
  // the assembled system prompt from the last prepare()
2573
2603
  started = false;
2574
2604
  // session-start lifecycle hook fires once per conversation
2605
+ /** Force the next `send()`/`run()` to rebuild the system prompt, tools, plan-mode and permission hooks
2606
+ * from `options` — apply mid-conversation changes to `planMode`/`permissions`/`model` etc. (prepare()
2607
+ * is otherwise memoized per conversation). */
2608
+ reprepare() {
2609
+ this.prepared = false;
2610
+ }
2575
2611
  /** Inject tools into a running agent (e.g. dynamically mounted MCP servers). Takes effect on the next turn. */
2576
2612
  addTools(tools) {
2577
2613
  this.activeTools.push(...tools);
@@ -2753,12 +2789,17 @@ var Agent = class _Agent {
2753
2789
  steps++;
2754
2790
  let res;
2755
2791
  const sent = this.trimContext();
2792
+ const frag = reasoningToChatFragment(o.model, o.reasoning);
2793
+ const reasonOpts = {
2794
+ ...frag,
2795
+ ...o.providerOptions ? { providerOptions: { ...frag.providerOptions, ...o.providerOptions } } : {}
2796
+ };
2756
2797
  try {
2757
2798
  if (useStream) {
2758
- const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: true, signal: o.signal });
2799
+ const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: true, signal: o.signal, ...reasonOpts });
2759
2800
  res = await this.consumeStream(r);
2760
2801
  } else {
2761
- const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: false, signal: o.signal });
2802
+ const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: false, signal: o.signal, ...reasonOpts });
2762
2803
  res = r;
2763
2804
  }
2764
2805
  } catch (err2) {
@@ -3879,6 +3920,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
3879
3920
  return [...filtered, ...realShell, ...o.extraTools ?? []];
3880
3921
  })(),
3881
3922
  maxSteps: o.maxSteps ?? 30,
3923
+ ...o.reasoning != null ? { reasoning: o.reasoning } : {},
3882
3924
  stream: o.stream ?? false,
3883
3925
  host: o.host,
3884
3926
  hooks,
@@ -3945,6 +3987,10 @@ function loadSettings(dir) {
3945
3987
  if (raw.mcpServers && typeof raw.mcpServers === "object") cfg.mcpServers = raw.mcpServers;
3946
3988
  if (raw.permissions && typeof raw.permissions === "object") cfg.permissions = raw.permissions;
3947
3989
  if (raw.model && typeof raw.model === "string") cfg.model = raw.model;
3990
+ if (raw.reasoning != null) cfg.reasoning = raw.reasoning;
3991
+ if (raw.permissionMode === "default" || raw.permissionMode === "acceptEdits" || raw.permissionMode === "plan") cfg.permissionMode = raw.permissionMode;
3992
+ if (raw.editorMode === "normal" || raw.editorMode === "vim") cfg.editorMode = raw.editorMode;
3993
+ if (typeof raw.stream === "boolean") cfg.stream = raw.stream;
3948
3994
  if (raw.env && typeof raw.env === "object") {
3949
3995
  for (const [k, v] of Object.entries(raw.env)) {
3950
3996
  if (typeof v === "string" && !process.env[k]) process.env[k] = v;
@@ -4595,12 +4641,13 @@ function completePath(listDir, ref) {
4595
4641
  import { emitKeypressEvents } from "readline";
4596
4642
  var visibleWidth = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
4597
4643
  var EditorState = class _EditorState {
4598
- // placeholder full pasted content
4599
- constructor(suggest, history = [], classifyPaste, onEmptyPaste) {
4644
+ // pending operator awaiting a motion (dd/dw/cc)
4645
+ constructor(suggest, history = [], classifyPaste, onEmptyPaste, vimEnabled = false) {
4600
4646
  this.suggest = suggest;
4601
4647
  this.history = history;
4602
4648
  this.classifyPaste = classifyPaste;
4603
4649
  this.onEmptyPaste = onEmptyPaste;
4650
+ if (vimEnabled) this.vim = "insert";
4604
4651
  }
4605
4652
  suggest;
4606
4653
  history;
@@ -4627,6 +4674,24 @@ var EditorState = class _EditorState {
4627
4674
  pasteBuf = "";
4628
4675
  pasteSeq = 0;
4629
4676
  pastes = /* @__PURE__ */ new Map();
4677
+ // placeholder → full pasted content
4678
+ // Vim modal editing (opt-in): undefined = emacs/readline keymap; else 'insert'|'normal'.
4679
+ vim;
4680
+ vimOp;
4681
+ /** The most-recent history entry that EXTENDS the current buffer — the inline ghost-suggestion suffix.
4682
+ * Empty unless the buffer is a single non-empty line with the cursor at its end and no menu/search open. */
4683
+ ghost() {
4684
+ if (!this.buf || this.cursor !== this.buf.length || this.menuOpen || this.searching || this.buf.includes("\n")) return "";
4685
+ for (const h of this.history) if (h.length > this.buf.length && h.startsWith(this.buf)) return h.slice(this.buf.length);
4686
+ return "";
4687
+ }
4688
+ /** Accept the ghost suggestion (append its remainder). Returns false when there's nothing to accept. */
4689
+ acceptGhost() {
4690
+ const g = this.ghost();
4691
+ if (!g) return false;
4692
+ this.insert(g);
4693
+ return true;
4694
+ }
4630
4695
  /** Collapse pastes that would break the single-line editor (multi-line) or flood it (≥ this many chars). */
4631
4696
  static PASTE_INLINE_MAX = 800;
4632
4697
  beginPaste() {
@@ -4880,11 +4945,119 @@ var EditorState = class _EditorState {
4880
4945
  this.searchQuery = "";
4881
4946
  this.pastes.clear();
4882
4947
  this.closeMenu();
4948
+ this.vimOp = void 0;
4949
+ if (this.vim) this.vim = "insert";
4883
4950
  }
4884
4951
  };
4885
4952
  var REWIND = "\0REWIND";
4953
+ function applyVimNormal(s, key, str) {
4954
+ const k = key?.name;
4955
+ if (key?.ctrl || key?.meta) return "pass";
4956
+ if (k === "return" || k === "enter") return "pass";
4957
+ if (k === "escape") {
4958
+ s.vimOp = void 0;
4959
+ return "pass";
4960
+ }
4961
+ const op = s.vimOp;
4962
+ s.vimOp = void 0;
4963
+ if (op) {
4964
+ if (str === "d" && op === "d") s.killToStart(), s.killToEnd();
4965
+ else if (str === "c" && op === "c") {
4966
+ s.killToStart();
4967
+ s.killToEnd();
4968
+ s.vim = "insert";
4969
+ } else if (str === "w") {
4970
+ s.killWordForward();
4971
+ while (s.buf[s.cursor] === " ") s.del();
4972
+ if (op === "c") s.vim = "insert";
4973
+ }
4974
+ return "none";
4975
+ }
4976
+ switch (str) {
4977
+ case "i":
4978
+ s.vim = "insert";
4979
+ return "none";
4980
+ case "a":
4981
+ s.right();
4982
+ s.vim = "insert";
4983
+ return "none";
4984
+ case "A":
4985
+ s.end();
4986
+ s.vim = "insert";
4987
+ return "none";
4988
+ case "I":
4989
+ s.home();
4990
+ s.vim = "insert";
4991
+ return "none";
4992
+ case "o":
4993
+ s.end();
4994
+ s.insert("\n");
4995
+ s.vim = "insert";
4996
+ return "none";
4997
+ case "h":
4998
+ s.left();
4999
+ return "none";
5000
+ case "l":
5001
+ s.right();
5002
+ return "none";
5003
+ case "0":
5004
+ s.home();
5005
+ return "none";
5006
+ case "$":
5007
+ s.end();
5008
+ return "none";
5009
+ case "w":
5010
+ case "e":
5011
+ s.wordRight();
5012
+ return "none";
5013
+ case "b":
5014
+ s.wordLeft();
5015
+ return "none";
5016
+ case "x":
5017
+ s.del();
5018
+ return "none";
5019
+ case "D":
5020
+ s.killToEnd();
5021
+ return "none";
5022
+ case "C":
5023
+ s.killToEnd();
5024
+ s.vim = "insert";
5025
+ return "none";
5026
+ case "d":
5027
+ s.vimOp = "d";
5028
+ return "none";
5029
+ case "c":
5030
+ s.vimOp = "c";
5031
+ return "none";
5032
+ }
5033
+ if (k === "left") {
5034
+ s.left();
5035
+ return "none";
5036
+ }
5037
+ if (k === "right") {
5038
+ s.right();
5039
+ return "none";
5040
+ }
5041
+ if (k === "up") {
5042
+ s.historyPrev();
5043
+ return "none";
5044
+ }
5045
+ if (k === "down") {
5046
+ s.historyNext();
5047
+ return "none";
5048
+ }
5049
+ if (k === "backspace") {
5050
+ s.left();
5051
+ return "none";
5052
+ }
5053
+ return "none";
5054
+ }
4886
5055
  function applyKey(s, key, str) {
4887
5056
  const k = key?.name;
5057
+ if (s.vim === "normal" && !s.pasting && !s.searching) {
5058
+ const r = applyVimNormal(s, key ?? {}, str);
5059
+ if (r !== "pass") return r;
5060
+ }
4888
5061
  if (k === "paste-start") {
4889
5062
  s.beginPaste();
4890
5063
  return "none";
@@ -4963,13 +5136,23 @@ function applyKey(s, key, str) {
4963
5136
  }
4964
5137
  return "submit";
4965
5138
  case "tab":
4966
- if (s.menuOpen) s.accept();
5139
+ if (s.menuOpen) {
5140
+ s.accept();
5141
+ return "none";
5142
+ }
5143
+ if (s.acceptGhost()) return "none";
4967
5144
  return "none";
5145
+ // menu accept, else accept history ghost
4968
5146
  case "escape":
4969
5147
  if (s.menuOpen) {
4970
5148
  s.closeMenu();
4971
5149
  return "none";
4972
5150
  }
5151
+ if (s.vim === "insert") {
5152
+ s.vim = "normal";
5153
+ return "none";
5154
+ }
5155
+ if (s.vim === "normal" && s.buf.length) return "none";
4973
5156
  if (s.buf.length) return "cancel";
4974
5157
  if (wasEsc) return "rewind";
4975
5158
  s.prevEsc = true;
@@ -4985,8 +5168,10 @@ function applyKey(s, key, str) {
4985
5168
  s.left();
4986
5169
  return "none";
4987
5170
  case "right":
5171
+ if (s.cursor === s.buf.length && s.acceptGhost()) return "none";
4988
5172
  s.right();
4989
5173
  return "none";
5174
+ // at EOL → accept ghost
4990
5175
  case "home":
4991
5176
  s.home();
4992
5177
  return "none";
@@ -5020,13 +5205,16 @@ function createLineEditor(out) {
5020
5205
  function render(s, promptArg, maxVisible, status) {
5021
5206
  const cols = out.columns ?? 80;
5022
5207
  const mode = !s.searching ? inputMode(s.buf) : void 0;
5023
- const prompt = s.searching ? dim2(`(${s.searchMiss ? "failing " : ""}reverse-i-search)\`${s.searchQuery}': `) : mode ? COLOR[mode.name](`${mode.pfx} ${mode.name} \u203A `) : promptArg;
5208
+ const vimTag = s.vim === "normal" && !s.searching && !mode ? inverse(" N ") + " " : "";
5209
+ const prompt = s.searching ? dim2(`(${s.searchMiss ? "failing " : ""}reverse-i-search)\`${s.searchQuery}': `) : mode ? COLOR[mode.name](`${mode.pfx} ${mode.name} \u203A `) : vimTag + promptArg;
5024
5210
  if (curRow > 0) out.write(`\x1B[${curRow}A`);
5025
5211
  out.write("\r\x1B[J");
5026
5212
  const viewBuf = mode ? s.buf.slice(mode.pfx.length) : s.buf;
5027
5213
  const viewCursor = mode ? Math.max(0, s.cursor - mode.pfx.length) : s.cursor;
5028
5214
  const { rows, cursorRow, cursorCol } = wrapLayout(visibleWidth(prompt), viewBuf, viewCursor, cols);
5029
- out.write(prompt + (rows[0] ?? ""));
5215
+ const ghost = !mode && !s.searching ? s.ghost() : "";
5216
+ const ghostFits = ghost && rows.length === 1 && visibleWidth(prompt) + viewBuf.length + ghost.length < cols;
5217
+ out.write(prompt + (rows[0] ?? "") + (ghostFits ? dim2(ghost) : ""));
5030
5218
  for (let i = 1; i < rows.length; i++) out.write("\r\n" + rows[i]);
5031
5219
  const inputRows = rows.length - 1;
5032
5220
  let menuRows = 0;
@@ -5057,7 +5245,7 @@ function createLineEditor(out) {
5057
5245
  async function readLine(opts) {
5058
5246
  const maxVisible = opts.maxVisible ?? 8;
5059
5247
  if (!isTTY) return readPlainLine();
5060
- const s = new EditorState(opts.suggest, opts.history ?? [], opts.classifyPaste, opts.onEmptyPaste);
5248
+ const s = new EditorState(opts.suggest, opts.history ?? [], opts.classifyPaste, opts.onEmptyPaste, opts.vimMode);
5061
5249
  curRow = 0;
5062
5250
  if (opts.initial) s.insert(opts.initial);
5063
5251
  s.refresh();
@@ -5072,11 +5260,31 @@ function createLineEditor(out) {
5072
5260
  };
5073
5261
  process.on("SIGWINCH", onResize);
5074
5262
  return new Promise((resolve4) => {
5263
+ const redraw = () => render(s, opts.prompt, maxVisible, opts.status);
5075
5264
  const onKey = (str, key) => {
5076
5265
  if (key?.ctrl && key.name === "l") {
5077
5266
  out.write("\x1B[2J\x1B[3J\x1B[H");
5078
5267
  curRow = 0;
5079
- render(s, opts.prompt, maxVisible, opts.status);
5268
+ redraw();
5269
+ return;
5270
+ }
5271
+ if (key?.name === "tab" && key.shift && opts.onCyclePosture) {
5272
+ opts.onCyclePosture();
5273
+ redraw();
5274
+ return;
5275
+ }
5276
+ if (key?.meta && key.name === "t" && opts.onToggleThinking) {
5277
+ opts.onToggleThinking();
5278
+ redraw();
5279
+ return;
5280
+ }
5281
+ if (key?.meta && key.name === "p" && opts.onPickModel) {
5282
+ process.stdin.off("keypress", onKey);
5283
+ void opts.onPickModel().finally(() => {
5284
+ process.stdin.on("keypress", onKey);
5285
+ curRow = 0;
5286
+ redraw();
5287
+ });
5080
5288
  return;
5081
5289
  }
5082
5290
  const action = applyKey(s, key ?? {}, str);
@@ -5450,6 +5658,12 @@ function numFlag(raw, flag) {
5450
5658
  if (!Number.isFinite(n) || n < 0) throw new Error(`invalid ${flag}: ${raw ?? "(missing value)"}`);
5451
5659
  return n;
5452
5660
  }
5661
+ function parseReasoning(raw) {
5662
+ if (raw === "off" || raw === "low" || raw === "medium" || raw === "high") return raw;
5663
+ const n = Number(raw);
5664
+ if (Number.isFinite(n) && n > 0) return n;
5665
+ throw new Error(`invalid --reasoning: ${raw} (use off|low|medium|high or a token budget)`);
5666
+ }
5453
5667
  function parseArgs(argv) {
5454
5668
  const a = { stream: true, plan: false, ask: false, yes: false, vfs: false, shell: void 0, seed: false, subagents: false, help: false, version: false, cont: false, outputFormat: "text" };
5455
5669
  const rest = [];
@@ -5487,6 +5701,9 @@ function parseArgs(argv) {
5487
5701
  else if (x === "--disallowedTools" || x === "--disallowed-tools") a.disallowedTools = val(++i, x).split(",").map((s) => s.trim()).filter(Boolean);
5488
5702
  else if (x === "--append-system-prompt") a.appendSystemPrompt = val(++i, x);
5489
5703
  else if (x === "--add-dir") (a.addDirs ??= []).push(val(++i, x));
5704
+ else if (x === "--reasoning") a.reasoning = parseReasoning(val(++i, x));
5705
+ else if (x === "--session-id") a.sessionId = val(++i, x);
5706
+ else if (x === "--fork-session") a.fork = true;
5490
5707
  else if (x === "--max-steps") a.maxSteps = numFlag(argv[++i], "--max-steps");
5491
5708
  else if (x === "--max-tokens") a.maxTokens = numFlag(argv[++i], "--max-tokens");
5492
5709
  else if (x === "--timeout") a.timeoutMs = numFlag(argv[++i], "--timeout") * 1e3;
@@ -5530,6 +5747,9 @@ Flags:
5530
5747
  --append-system-prompt <t> extra instructions appended to the system prompt for this run
5531
5748
  --add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
5532
5749
  --subagents allow the Task tool (spawn child agents)
5750
+ --reasoning <e> extended thinking: off|low|medium|high or a token budget (anthropic/openai)
5751
+ --session-id <id> use this id for the session (instead of an auto-generated one)
5752
+ --fork-session with -r/-c: branch the resumed session into a new id (source left untouched)
5533
5753
  --max-steps <n> step budget (default 30)
5534
5754
  --max-tokens <n> token budget kill-switch (default 200000)
5535
5755
  --timeout <sec> wall-clock kill-switch (default 120)
@@ -5541,7 +5761,7 @@ Prompts may reference files with @path (e.g. "explain @src/Agent.ts") \u2014 the
5541
5761
 
5542
5762
  Providers: set any of ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY.
5543
5763
  Config: ./.agent/config.{ts,js,json} (project) or ~/.agent/config.* (user).
5544
- export default { model, maxSteps, tools, apiKeys, baseUrls, hooks, permissions, mcpServers, maxTokens,
5764
+ export default { model, maxSteps, reasoning, permissionMode, editorMode, tools, apiKeys, baseUrls, hooks, permissions, mcpServers, maxTokens,
5545
5765
  timeoutMs, maxRepeats, maxToolCalls, keepToolOutputs, maxContextTokens,
5546
5766
  learnFromMistakes, reflectOnFailure, budget: {\u2026} }
5547
5767
  hooks: { preToolUse|postToolUse|onStop: [{ tool?, command, block? }] } \u2014 shell hooks
@@ -5554,10 +5774,11 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
5554
5774
  Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
5555
5775
 
5556
5776
  REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \xB7 @path inlines a file
5557
- REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /exit
5777
+ REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /exit
5558
5778
  REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
5559
5779
  REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
5560
- REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen.
5780
+ REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 \u2192 or Tab accepts the dim history ghost-suggestion.
5781
+ REPL editing (emacs/readline): Ctrl-A/E line start/end \xB7 Ctrl-B/F char \xB7 Alt-B/F or Alt/Ctrl-\u2190/\u2192 word \xB7 Ctrl-W kill word \xB7 Ctrl-U/K kill to start/end \xB7 Ctrl-Y yank \xB7 Alt-D kill word fwd \xB7 Ctrl-L clear screen. Set editorMode:'vim' (or /config) for modal vim editing.
5561
5782
  REPL paste: large/multi-line pastes collapse to a [Pasted text +N lines] preview (expands on send); a pasted image/file path attaches as [Image]/[File]; /paste grabs a clipboard image (macOS).`;
5562
5783
  function newestModel() {
5563
5784
  return listModels().slice().sort((a, b) => (getModelInfo(b)?.releaseDate ?? "").localeCompare(getModelInfo(a)?.releaseDate ?? ""))[0] ?? "";
@@ -5873,6 +6094,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
5873
6094
  permissions: rules.length ? new PermissionPolicy({ rules, default: "allow", host: makeHost(), ask: makeAskResolver(resolve3(args.cwd ?? process.cwd())) }) : void 0,
5874
6095
  subagents: args.subagents || cfg.subagents || false,
5875
6096
  maxSteps: args.maxSteps ?? cfg.maxSteps,
6097
+ reasoning: args.reasoning ?? cfg.reasoning,
5876
6098
  // kill-switches: CLI flag > config > Agent default
5877
6099
  maxTokens: args.maxTokens ?? cfg.maxTokens,
5878
6100
  timeoutMs: args.timeoutMs ?? cfg.timeoutMs,
@@ -6067,6 +6289,14 @@ function startSession(args, store, agent, cwd) {
6067
6289
  const data = args.resume ? store.load(args.resume) : store.latestData();
6068
6290
  if (data) {
6069
6291
  agent.transcript = data.messages;
6292
+ if (args.fork) {
6293
+ const now2 = Date.now();
6294
+ const forked = { meta: { ...data.meta, id: args.sessionId ?? store.newId(now2), created: now2, updated: now2, turns: data.meta.turns }, messages: data.messages };
6295
+ err(dim(` forked ${data.meta.id} \u2192 ${forked.meta.id} (${data.meta.turns} turns)
6296
+ `));
6297
+ if (!args.task) printHistory(data.messages);
6298
+ return forked;
6299
+ }
6070
6300
  err(dim(` resumed session ${data.meta.id} (${data.meta.turns} turns) \u2014 ${data.meta.title}
6071
6301
  `));
6072
6302
  if (!args.task) printHistory(data.messages);
@@ -6076,7 +6306,7 @@ function startSession(args, store, agent, cwd) {
6076
6306
  `));
6077
6307
  }
6078
6308
  const now = Date.now();
6079
- return { meta: { id: store.newId(now), created: now, updated: now, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
6309
+ return { meta: { id: args.sessionId ?? store.newId(now), created: now, updated: now, cwd, model: agent.options.model, turns: 0, title: "" }, messages: [] };
6080
6310
  }
6081
6311
  var AGENTS_MD_TEMPLATE = `# ${"${name}"}
6082
6312
 
@@ -6107,23 +6337,56 @@ function initInstructions(cwd) {
6107
6337
  err(green(` created ${path}
6108
6338
  `) + dim(" edit it, then it auto-loads into every run.\n"));
6109
6339
  }
6110
- function persistModel(cwd, model) {
6340
+ function persistSetting(cwd, key, value) {
6111
6341
  const path = join8(cwd, ".agent", "settings.json");
6112
6342
  try {
6113
6343
  const obj = existsSync7(path) ? JSON.parse(readFileSync5(path, "utf8")) : {};
6114
- if (obj.model === model) return;
6115
- obj.model = model;
6344
+ if (obj[key] === value) return;
6345
+ obj[key] = value;
6116
6346
  mkdirSync6(dirname3(path), { recursive: true });
6117
6347
  writeFileSync6(path, JSON.stringify(obj, null, 2) + "\n");
6118
6348
  } catch (e) {
6119
- err(yellow(` \u26A0 couldn't persist model to ${path} \u2014 ${e?.message ?? e}
6349
+ err(yellow(` \u26A0 couldn't persist ${key} to ${path} \u2014 ${e?.message ?? e}
6120
6350
  `));
6121
6351
  }
6122
6352
  }
6353
+ var persistModel = (cwd, model) => persistSetting(cwd, "model", model);
6123
6354
  async function repl(args, ai, cfg, cwd) {
6124
6355
  const oauth = new McpOAuth({ storePath: join8(cwd, ".agent", "mcp-auth.json") });
6125
6356
  const mounted = await mountMcp(cfg, oauth);
6126
6357
  const agent = await makeAgent(args, ai, cfg, mounted.flatMap((m) => m.tools));
6358
+ const baseRules = () => [
6359
+ ...parsePermRules({ deny: args.disallowedTools }),
6360
+ ...parsePermRules({ allow: args.allowedTools }),
6361
+ ...parsePermRules(mergePerms(loadPersistedRules(cwd), cfg.permissions))
6362
+ ];
6363
+ const askFor = (tools) => tools.map((t) => ({ tool: t, decision: "ask" }));
6364
+ const POSTURES = ["default", "acceptEdits", "plan"];
6365
+ let posture = args.plan || cfg.permissionMode === "plan" ? "plan" : cfg.permissionMode === "acceptEdits" ? "acceptEdits" : "default";
6366
+ const postureLabel = () => posture === "default" ? "ask (default)" : posture === "acceptEdits" ? "accept edits" : "plan mode";
6367
+ const applyPosture = (p) => {
6368
+ posture = p;
6369
+ const ask = p === "acceptEdits" ? askFor(["bash", "Shell"]) : askFor(ASK_MUTATING);
6370
+ agent.options.permissions = new PermissionPolicy({ rules: [...baseRules(), ...ask], default: "allow", host: makeHost(), ask: makeAskResolver(cwd) });
6371
+ agent.options.planMode = p === "plan";
6372
+ agent.reprepare();
6373
+ };
6374
+ const cyclePosture = () => {
6375
+ applyPosture(POSTURES[(POSTURES.indexOf(posture) + 1) % POSTURES.length]);
6376
+ err(dim(` \u21E5 ${postureLabel()}
6377
+ `));
6378
+ return postureLabel();
6379
+ };
6380
+ if (!args.yes) applyPosture(posture);
6381
+ const REASONING_CYCLE = ["off", "low", "medium", "high"];
6382
+ const toggleReasoning = () => {
6383
+ const cur = String(agent.options.reasoning ?? "off");
6384
+ const next = REASONING_CYCLE[(Math.max(0, REASONING_CYCLE.indexOf(cur)) + 1) % REASONING_CYCLE.length];
6385
+ agent.options.reasoning = next;
6386
+ err(dim(` ~ reasoning \u2192 ${next}
6387
+ `));
6388
+ return next;
6389
+ };
6127
6390
  const pendingImages = [];
6128
6391
  const grabClipboardAttachment = () => {
6129
6392
  const dir = join8(tmpdir(), "agentx-pasted");
@@ -6142,6 +6405,18 @@ async function repl(args, ai, cfg, cwd) {
6142
6405
  void closeMcp(mounted);
6143
6406
  process.exit(130);
6144
6407
  });
6408
+ const isStreamTeardown = (e) => /NGHTTP2_FRAME_SIZE_ERROR|ERR_HTTP2_STREAM_ERROR/.test(`${e?.code ?? ""} ${e?.rawMessage ?? ""} ${e?.message ?? ""}`);
6409
+ const onFatal = (kind) => (e) => {
6410
+ if (isStreamTeardown(e)) {
6411
+ log11.debug(`suppressed ${kind} (connect/HTTP-2 stream teardown on cancel)`, e);
6412
+ return;
6413
+ }
6414
+ console.error(e);
6415
+ void closeMcp(mounted);
6416
+ process.exit(1);
6417
+ };
6418
+ process.on("unhandledRejection", onFatal("unhandledRejection"));
6419
+ process.on("uncaughtException", onFatal("uncaughtException"));
6145
6420
  const store = new SessionStore(cwd);
6146
6421
  let session = startSession(args, store, agent, cwd);
6147
6422
  const checkpoints = args.vfs || args.boddb ? new CheckpointStack(agent.options.fs) : new GitCheckpoints({ workTree: cwd, gitDir: join8(cwd, ".agent", "checkpoints.git"), addDirs: args.addDirs, sessionId: session.meta.id });
@@ -6399,6 +6674,92 @@ ${extra}` : body, checkpoints, cwd);
6399
6674
  } else err(dim(" " + agent.options.model + "\n"));
6400
6675
  }
6401
6676
  },
6677
+ reasoning: {
6678
+ desc: "extended thinking \u2014 /reasoning <off|low|medium|high|tokens>, or alone for an interactive picker",
6679
+ run: async (a) => {
6680
+ const current = String(agent.options.reasoning ?? "off");
6681
+ let next;
6682
+ if (a[0]) {
6683
+ try {
6684
+ next = parseReasoning(a[0]);
6685
+ } catch (e) {
6686
+ err(yellow(" " + (e?.message ?? e) + "\n"));
6687
+ return;
6688
+ }
6689
+ } else {
6690
+ const items = [
6691
+ { label: "off", value: "off", desc: "no extended thinking" },
6692
+ { label: "low", value: "low", desc: "minimal reasoning (~2k tokens)" },
6693
+ { label: "medium", value: "medium", desc: "balanced (~8k tokens)" },
6694
+ { label: "high", value: "high", desc: "maximal reasoning (~24k tokens)" }
6695
+ ];
6696
+ const picked = await selectMenu(process.stderr, { title: `Reasoning effort \xB7 current: ${current}`, items, current });
6697
+ if (!picked) {
6698
+ err(dim(" " + current + "\n"));
6699
+ return;
6700
+ }
6701
+ next = picked;
6702
+ }
6703
+ agent.options.reasoning = next;
6704
+ if (next !== "off" && getModelInfo(agent.options.model)?.reasoning === false)
6705
+ err(yellow(` note: ${agent.options.model} has no reasoning capability \u2014 setting may be ignored
6706
+ `));
6707
+ err(dim(" reasoning \u2192 " + next + "\n"));
6708
+ }
6709
+ },
6710
+ config: {
6711
+ desc: "view/change settings \u2014 model, reasoning, permission posture, streaming, editor mode",
6712
+ run: async () => {
6713
+ for (; ; ) {
6714
+ const items = [
6715
+ { label: "model", value: "model", desc: agent.options.model },
6716
+ { label: "reasoning", value: "reasoning", desc: String(agent.options.reasoning ?? "off") },
6717
+ { label: "permission posture", value: "posture", desc: postureLabel() + " (Shift+Tab)" },
6718
+ { label: "streaming", value: "stream", desc: agent.options.stream ? "on" : "off" },
6719
+ { label: "editor mode", value: "editor", desc: cfg.editorMode === "vim" ? "vim" : "normal" }
6720
+ ];
6721
+ const pick = await selectMenu(process.stderr, { title: "Settings \xB7 \u21B5 change \xB7 esc close", items });
6722
+ if (!pick) return;
6723
+ if (pick === "model") {
6724
+ const m = await pickModel(agent.options.model);
6725
+ if (m) {
6726
+ agent.options.model = m;
6727
+ persistModel(cwd, m);
6728
+ }
6729
+ } else if (pick === "reasoning") {
6730
+ await builtins.reasoning.run([]);
6731
+ persistSetting(cwd, "reasoning", agent.options.reasoning ?? "off");
6732
+ } else if (pick === "posture") {
6733
+ cyclePosture();
6734
+ persistSetting(cwd, "permissionMode", posture);
6735
+ } else if (pick === "stream") {
6736
+ agent.options.stream = !agent.options.stream;
6737
+ persistSetting(cwd, "stream", agent.options.stream);
6738
+ err(dim(" streaming \u2192 " + (agent.options.stream ? "on" : "off") + "\n"));
6739
+ } else if (pick === "editor") {
6740
+ cfg.editorMode = cfg.editorMode === "vim" ? "normal" : "vim";
6741
+ persistSetting(cwd, "editorMode", cfg.editorMode);
6742
+ err(dim(" editor \u2192 " + cfg.editorMode + "\n"));
6743
+ }
6744
+ }
6745
+ }
6746
+ },
6747
+ rename: {
6748
+ desc: "rename the current session \u2014 /rename <title>",
6749
+ run: (a) => {
6750
+ const t = a.join(" ").trim();
6751
+ if (!t) {
6752
+ err(dim(" title: " + (session.meta.title || "(none)") + "\n"));
6753
+ return;
6754
+ }
6755
+ session.meta.title = t;
6756
+ try {
6757
+ store.save(session);
6758
+ } catch {
6759
+ }
6760
+ err(dim(" renamed \u2192 " + t + "\n"));
6761
+ }
6762
+ },
6402
6763
  compact: {
6403
6764
  desc: "summarize older context to free up the window",
6404
6765
  run: () => {
@@ -6693,11 +7054,20 @@ ${extra}` : body, checkpoints, cwd);
6693
7054
  return { hits, token, describe };
6694
7055
  };
6695
7056
  const editor = createLineEditor(process.stderr);
7057
+ let aborting = false;
7058
+ let pendingRewind = false;
6696
7059
  if (process.stdin.isTTY) {
6697
7060
  process.stdin.on("keypress", (_s, key) => {
6698
- if (activeTurn && (key?.name === "escape" || key?.ctrl && key?.name === "c")) {
7061
+ if (!activeTurn) return;
7062
+ const cancel = key?.name === "escape" || key?.ctrl && key?.name === "c";
7063
+ if (!cancel) return;
7064
+ if (!aborting) {
7065
+ aborting = true;
6699
7066
  activeTurn.abort();
6700
7067
  err(yellow("\n \u238B cancelling\u2026\n"));
7068
+ } else if (key?.name === "escape" && !pendingRewind) {
7069
+ pendingRewind = true;
7070
+ err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
6701
7071
  }
6702
7072
  });
6703
7073
  }
@@ -6715,14 +7085,48 @@ ${extra}` : body, checkpoints, cwd);
6715
7085
  };
6716
7086
  let prefill;
6717
7087
  while (true) {
7088
+ if (pendingRewind) {
7089
+ pendingRewind = false;
7090
+ const t = await rewindToMessage();
7091
+ if (t !== void 0) prefill = t;
7092
+ }
7093
+ aborting = false;
6718
7094
  err("\n");
6719
7095
  const initial = prefill;
6720
7096
  prefill = void 0;
6721
7097
  const ctxTok = estimateTranscriptTokens(agent.transcript);
6722
7098
  const ctxCap = agent.options.maxTokens || 2e5;
6723
7099
  const usd = session.meta.costUsd ?? 0;
6724
- const footer = ctxTok > 400 || usd > 0 ? `${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)${usd > 0 ? ` \xB7 ${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}` : ""}` : "";
6725
- const result = await readMultiline((cont) => editor.readLine({ prompt: cont ? contPrompt : promptStr, suggest, history, classifyPaste, onEmptyPaste: grabClipboardAttachment, initial: cont ? void 0 : initial, status: () => footer }));
7100
+ const computeFooter = () => {
7101
+ const parts = [];
7102
+ if (ctxTok > 400) parts.push(`${Math.round(ctxTok / ctxCap * 100)}% ctx (~${(ctxTok / 1e3).toFixed(1)}k/${Math.round(ctxCap / 1e3)}k)`);
7103
+ if (usd > 0) parts.push(`${session.meta.costEstimated ? "~" : ""}${fmtUsd(usd)}`);
7104
+ if (posture !== "default") parts.push(postureLabel());
7105
+ const r = agent.options.reasoning;
7106
+ if (r && r !== "off") parts.push(`reasoning:${r}`);
7107
+ return parts.join(" \xB7 ");
7108
+ };
7109
+ const result = await readMultiline((cont) => editor.readLine({
7110
+ prompt: cont ? contPrompt : promptStr,
7111
+ suggest,
7112
+ history,
7113
+ classifyPaste,
7114
+ onEmptyPaste: grabClipboardAttachment,
7115
+ initial: cont ? void 0 : initial,
7116
+ status: computeFooter,
7117
+ vimMode: cfg.editorMode === "vim",
7118
+ onCyclePosture: cyclePosture,
7119
+ onToggleThinking: toggleReasoning,
7120
+ onPickModel: async () => {
7121
+ const picked = await pickModel(agent.options.model);
7122
+ if (picked) {
7123
+ agent.options.model = picked;
7124
+ persistModel(cwd, picked);
7125
+ err(dim(" model \u2192 " + picked + "\n"));
7126
+ }
7127
+ return picked;
7128
+ }
7129
+ }));
6726
7130
  if (result === null) break;
6727
7131
  if (result === REWIND) {
6728
7132
  prefill = await rewindToMessage();