nexrall-code 0.5.23 → 0.5.27

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 +390 -31
  2. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -12673,7 +12673,7 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
12673
12673
  var _bgCounter = 0;
12674
12674
  function startBackgroundShell(command, displayCommand, workDir, note) {
12675
12675
  const id = `bg_${++_bgCounter}`;
12676
- const child = (0, child_process_1.spawn)(command, { shell: true, cwd: workDir ?? process.cwd(), env: process.env, detached: true });
12676
+ const child = (0, child_process_1.spawn)(command, { shell: true, cwd: workDir ?? process.cwd(), env: process.env, detached: true, stdio: ["ignore", "pipe", "pipe"] });
12677
12677
  const shell = {
12678
12678
  id,
12679
12679
  command: displayCommand,
@@ -12800,7 +12800,7 @@ ${fresh || "(no new output)"}${trunc}` };
12800
12800
  }
12801
12801
  return { output: `Background shell ${id} ${shell.status}.` };
12802
12802
  }
12803
- async function bash(input, abortSignal, sandbox, workDir) {
12803
+ async function bash(input, abortSignal, sandbox, workDir, onStream) {
12804
12804
  const command = typeof input.command === "string" ? input.command : "";
12805
12805
  const _rawDesc = typeof input.description === "string" ? input.description : "";
12806
12806
  void _rawDesc.slice(0, 200);
@@ -12835,7 +12835,8 @@ ${r2.output}`;
12835
12835
  shell: true,
12836
12836
  cwd: workDir ?? process.cwd(),
12837
12837
  env: process.env,
12838
- detached: true
12838
+ detached: true,
12839
+ stdio: ["ignore", "pipe", "pipe"]
12839
12840
  });
12840
12841
  const killTree = (sig) => {
12841
12842
  try {
@@ -12951,8 +12952,22 @@ ${body}` : void 0 }));
12951
12952
  resolve3(prependNote({ output: body, exitCode: 0 }));
12952
12953
  }
12953
12954
  };
12954
- child.stdout?.on("data", (chunk) => appendOutput(chunk.toString("utf-8")));
12955
- child.stderr?.on("data", (chunk) => appendOutput(chunk.toString("utf-8")));
12955
+ child.stdout?.on("data", (chunk) => {
12956
+ const text = chunk.toString("utf-8");
12957
+ appendOutput(text);
12958
+ try {
12959
+ onStream?.(text);
12960
+ } catch {
12961
+ }
12962
+ });
12963
+ child.stderr?.on("data", (chunk) => {
12964
+ const text = chunk.toString("utf-8");
12965
+ appendOutput(text);
12966
+ try {
12967
+ onStream?.(text);
12968
+ } catch {
12969
+ }
12970
+ });
12956
12971
  child.on("close", (code, signal) => done(code, signal));
12957
12972
  child.on("error", (err) => {
12958
12973
  settled = true;
@@ -14148,12 +14163,12 @@ ${expanded}` };
14148
14163
  "memory_read",
14149
14164
  "use_skill"
14150
14165
  ]);
14151
- async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope) {
14166
+ async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope, onStream) {
14152
14167
  if (!TOOL_MAP[name])
14153
14168
  return { error: `Unknown tool: ${name}` };
14154
14169
  try {
14155
14170
  if (name === "bash")
14156
- return await bash(input, abortSignal, sandbox, workDir);
14171
+ return await bash(input, abortSignal, sandbox, workDir, input.run_in_background === true ? void 0 : onStream);
14157
14172
  if (name === "todo_write")
14158
14173
  return await todoWrite(input, agentScope);
14159
14174
  if (name === "todo_read")
@@ -14218,7 +14233,7 @@ var require_agentTypes = __commonJS({
14218
14233
  };
14219
14234
  }();
14220
14235
  Object.defineProperty(exports2, "__esModule", { value: true });
14221
- exports2.loadAgentTypes = loadAgentTypes;
14236
+ exports2.loadAgentTypes = loadAgentTypes2;
14222
14237
  exports2.summariseAgents = summariseAgents;
14223
14238
  exports2.findAgentType = findAgentType;
14224
14239
  var fs6 = __importStar(require("fs"));
@@ -14297,7 +14312,7 @@ var require_agentTypes = __commonJS({
14297
14312
  }
14298
14313
  }
14299
14314
  }
14300
- function loadAgentTypes(workDir) {
14315
+ function loadAgentTypes2(workDir) {
14301
14316
  const out = /* @__PURE__ */ new Map();
14302
14317
  loadDir(path5.join(workDir, ".nexrall", "agents"), "project", out);
14303
14318
  loadDir(path5.join(os5.homedir(), ".nexrall", "agents"), "global", out);
@@ -14718,14 +14733,16 @@ var require_loop = __commonJS({
14718
14733
  Object.defineProperty(exports2, "__esModule", { value: true });
14719
14734
  exports2.VERIFY_CMD_RE = exports2.WRITE_TOOL_NAMES = void 0;
14720
14735
  exports2.resolveMaxIterations = resolveMaxIterations;
14721
- exports2.estimateBodyBytes = estimateBodyBytes;
14736
+ exports2.contextWindowFor = contextWindowFor2;
14737
+ exports2.compactionThresholds = compactionThresholds2;
14738
+ exports2.estimateBodyBytes = estimateBodyBytes2;
14722
14739
  exports2.findSafeCutIndex = findSafeCutIndex;
14723
14740
  exports2.transcriptOf = transcriptOf;
14724
14741
  exports2.createLedger = createLedger;
14725
14742
  exports2.ledgerRecord = ledgerRecord;
14726
14743
  exports2.ledgerSummary = ledgerSummary;
14727
14744
  exports2.pruneOldToolResults = pruneOldToolResults;
14728
- exports2.estimateTokensRough = estimateTokensRough;
14745
+ exports2.estimateTokensRough = estimateTokensRough2;
14729
14746
  exports2.compactMessagesForResume = compactMessagesForResume2;
14730
14747
  exports2.runAgentLoop = runAgentLoop2;
14731
14748
  exports2.trimToResumableBoundary = trimToResumableBoundary;
@@ -14961,6 +14978,7 @@ var require_loop = __commonJS({
14961
14978
  }
14962
14979
  var MAX_TASK_DEPTH = 2;
14963
14980
  var _subTaskCounter = 0;
14981
+ var SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0 ? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) : 10 * 60 * 1e3;
14964
14982
  async function runSubTask(input, options, agentTypes) {
14965
14983
  const prompt2 = typeof input.prompt === "string" ? input.prompt.trim() : "";
14966
14984
  if (!prompt2)
@@ -14990,6 +15008,14 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
14990
15008
  const subMessages = [
14991
15009
  { role: "user", content: [{ type: "text", text: prompt2 }] }
14992
15010
  ];
15011
+ const subAbort = { aborted: false };
15012
+ const timer = setTimeout(() => {
15013
+ subAbort.aborted = true;
15014
+ }, SUBTASK_TIMEOUT_MS);
15015
+ const parentAbortPoll = setInterval(() => {
15016
+ if (options.abortSignal?.aborted)
15017
+ subAbort.aborted = true;
15018
+ }, 250);
14993
15019
  try {
14994
15020
  const result = await runAgentLoop2(subMessages, {
14995
15021
  ...options,
@@ -15000,6 +15026,7 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
15000
15026
  // fresh isolated context for sub-agent
15001
15027
  model: agent?.model ?? options.model,
15002
15028
  nexrallMd: subNexrallMd,
15029
+ abortSignal: subAbort,
15003
15030
  requestPermission: gatedPermission,
15004
15031
  onText: () => {
15005
15032
  },
@@ -15025,11 +15052,15 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
15025
15052
  // when the name was already labelled, and mixed display concerns into the data layer).
15026
15053
  onToolUse: (n, i2) => options.onToolUse(n, i2, true),
15027
15054
  onToolResult: (n, r2) => options.onToolResult(n, r2, true),
15055
+ onToolStreamChunk: (n, c) => options.onToolStreamChunk?.(n, c, true),
15028
15056
  // Forward thinking so the UI shows the indicator while sub-agent reasons
15029
15057
  onThinking: (text2) => options.onThinking?.(text2),
15030
15058
  onThinkingDelta: (text2) => options.onThinkingDelta?.(text2),
15031
15059
  onThinkingProgress: (tok) => options.onThinkingProgress?.(tok)
15032
15060
  });
15061
+ if (subAbort.aborted && !options.abortSignal?.aborted) {
15062
+ return { error: `Sub-task stalled and was stopped after ${Math.round(SUBTASK_TIMEOUT_MS / 6e4)} minutes with no completion. Consider breaking it into smaller sub-tasks or investigating what it may have been stuck on (a hung command, an unresponsive MCP/tool call, or a very large scope).` };
15063
+ }
15033
15064
  const lastAssistant = [...result].reverse().find((m2) => m2.role === "assistant");
15034
15065
  let text = (lastAssistant?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("").trim();
15035
15066
  const SUBTASK_MAX = 48e3;
@@ -15045,6 +15076,9 @@ ${tail}`;
15045
15076
  return { output: text || "(sub-task completed with no text output)" };
15046
15077
  } catch (err) {
15047
15078
  return { error: `Sub-task failed: ${err.message}` };
15079
+ } finally {
15080
+ clearTimeout(timer);
15081
+ clearInterval(parentAbortPoll);
15048
15082
  }
15049
15083
  }
15050
15084
  var MODEL_CONTEXT_TOKENS = {
@@ -15052,16 +15086,22 @@ ${tail}`;
15052
15086
  pro: 1e6,
15053
15087
  ultra: 1e6
15054
15088
  };
15089
+ function contextWindowFor2(model) {
15090
+ return MODEL_CONTEXT_TOKENS[model ?? "turbo"] ?? 1e6;
15091
+ }
15055
15092
  function envFraction(name, fallback) {
15056
15093
  const v = Number(process.env[name]);
15057
15094
  return Number.isFinite(v) && v > 0 && v < 1 ? v : fallback;
15058
15095
  }
15059
15096
  var AUTO_PRUNE_THRESHOLD = envFraction("NEXRALL_PRUNE_THRESHOLD", 0.35);
15060
15097
  var AUTO_COMPACT_THRESHOLD = envFraction("NEXRALL_COMPACT_THRESHOLD", 0.8);
15098
+ function compactionThresholds2() {
15099
+ return { prune: AUTO_PRUNE_THRESHOLD, compact: AUTO_COMPACT_THRESHOLD };
15100
+ }
15061
15101
  var PRUNE_MIN_RECLAIM_BYTES = 256 * 1024;
15062
15102
  var COMPACT_KEEP_MIN = 6;
15063
15103
  var MAX_BODY_BYTES = 8 * 1024 * 1024;
15064
- function estimateBodyBytes(messages) {
15104
+ function estimateBodyBytes2(messages) {
15065
15105
  try {
15066
15106
  return Buffer.byteLength(JSON.stringify(messages), "utf-8");
15067
15107
  } catch {
@@ -15327,8 +15367,8 @@ Continue the work from here.` }] });
15327
15367
  }
15328
15368
  }
15329
15369
  var RESUME_CHARS_PER_TOKEN = 4;
15330
- function estimateTokensRough(messages) {
15331
- return Math.ceil(estimateBodyBytes(messages) / RESUME_CHARS_PER_TOKEN);
15370
+ function estimateTokensRough2(messages) {
15371
+ return Math.ceil(estimateBodyBytes2(messages) / RESUME_CHARS_PER_TOKEN);
15332
15372
  }
15333
15373
  async function compactMessagesForResume2(messages, opts) {
15334
15374
  if (messages.length <= COMPACT_KEEP_MIN + 2)
@@ -15337,8 +15377,8 @@ Continue the work from here.` }] });
15337
15377
  if (!resolveAutoCompact(void 0, settings.raw))
15338
15378
  return false;
15339
15379
  const contextWindow = MODEL_CONTEXT_TOKENS[opts.model ?? "turbo"] ?? 1e6;
15340
- let bodyBytes = estimateBodyBytes(messages);
15341
- let tokenGuess = estimateTokensRough(messages);
15380
+ let bodyBytes = estimateBodyBytes2(messages);
15381
+ let tokenGuess = estimateTokensRough2(messages);
15342
15382
  const overPruneThreshold = () => tokenGuess > contextWindow * AUTO_PRUNE_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
15343
15383
  const overCompactThreshold = () => tokenGuess > contextWindow * AUTO_COMPACT_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
15344
15384
  if (!overPruneThreshold())
@@ -15347,8 +15387,8 @@ Continue the work from here.` }] });
15347
15387
  if (messages.length > PRUNE_KEEP_RECENT + 2) {
15348
15388
  const reclaimed = pruneOldToolResults(messages, PRUNE_MIN_RECLAIM_BYTES);
15349
15389
  if (reclaimed > 0) {
15350
- bodyBytes = estimateBodyBytes(messages);
15351
- tokenGuess = estimateTokensRough(messages);
15390
+ bodyBytes = estimateBodyBytes2(messages);
15391
+ tokenGuess = estimateTokensRough2(messages);
15352
15392
  compacted = true;
15353
15393
  opts.onNotice?.(`
15354
15394
  \u267B\uFE0F Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of older tool output before resuming this chat.
@@ -15376,8 +15416,8 @@ Continue the work from here.` }] });
15376
15416
  if (!did)
15377
15417
  break;
15378
15418
  compacted = true;
15379
- bodyBytes = estimateBodyBytes(messages);
15380
- tokenGuess = estimateTokensRough(messages);
15419
+ bodyBytes = estimateBodyBytes2(messages);
15420
+ tokenGuess = estimateTokensRough2(messages);
15381
15421
  }
15382
15422
  if (compacted) {
15383
15423
  opts.onNotice?.(`
@@ -15422,14 +15462,14 @@ Continue the work from here.` }] });
15422
15462
  for (; iteration < budget; iteration++) {
15423
15463
  if (options.abortSignal?.aborted)
15424
15464
  break;
15425
- let bodyBytes = estimateBodyBytes(messages);
15465
+ let bodyBytes = estimateBodyBytes2(messages);
15426
15466
  const prunePressure = lastPromptTokens > contextWindow * AUTO_PRUNE_THRESHOLD;
15427
15467
  const tokenPressure = lastPromptTokens > contextWindow * AUTO_COMPACT_THRESHOLD;
15428
15468
  let bytePressure = bodyBytes > MAX_BODY_BYTES;
15429
15469
  if (autoCompact && !compacting && (prunePressure || bytePressure) && messages.length > PRUNE_KEEP_RECENT + 2) {
15430
15470
  const reclaimed = pruneOldToolResults(messages, PRUNE_MIN_RECLAIM_BYTES);
15431
15471
  if (reclaimed > 0) {
15432
- bodyBytes = estimateBodyBytes(messages);
15472
+ bodyBytes = estimateBodyBytes2(messages);
15433
15473
  bytePressure = bodyBytes > MAX_BODY_BYTES;
15434
15474
  (options.onNotice ?? options.onText)(`\u267B\uFE0F Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of already-processed tool output to keep this chat cheap to continue.`);
15435
15475
  }
@@ -15649,12 +15689,13 @@ Continue the work from here.` }] });
15649
15689
  } else {
15650
15690
  options.checkpointManager?.recordBeforeMutation(name, input);
15651
15691
  const WRITE_TOOLS = /* @__PURE__ */ new Set(["write_file", "edit_file", "multi_edit", "delete_file", "move_file", "copy_file", "notebook_edit"]);
15692
+ const onStream = options.onToolStreamChunk ? (chunk) => options.onToolStreamChunk(name, chunk) : void 0;
15652
15693
  if (WRITE_TOOLS.has(name)) {
15653
15694
  const targetPath = typeof input.path === "string" ? input.path : typeof input.source === "string" ? input.source : "";
15654
15695
  const absTarget = targetPath && options.workDir ? path5.resolve(options.workDir, targetPath) : targetPath;
15655
- result = await withFileLock(absTarget || name, () => (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope));
15696
+ result = await withFileLock(absTarget || name, () => (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope, onStream));
15656
15697
  } else {
15657
- result = await (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope);
15698
+ result = await (0, executor_1.executeTool)(name, input, options.abortSignal, sandboxCfg, options.workDir, agentScope, onStream);
15658
15699
  }
15659
15700
  }
15660
15701
  } catch (err) {
@@ -16323,7 +16364,7 @@ var require_manager = __commonJS({
16323
16364
  var httpClient_1 = require_httpClient();
16324
16365
  var sseClient_1 = require_sseClient();
16325
16366
  var index_1 = require_plugins();
16326
- var McpManager = class _McpManager {
16367
+ var McpManager2 = class _McpManager {
16327
16368
  constructor() {
16328
16369
  this._clients = /* @__PURE__ */ new Map();
16329
16370
  this._toolMap = /* @__PURE__ */ new Map();
@@ -16496,7 +16537,7 @@ var require_manager = __commonJS({
16496
16537
  this._needsAuth.clear();
16497
16538
  }
16498
16539
  };
16499
- exports2.McpManager = McpManager;
16540
+ exports2.McpManager = McpManager2;
16500
16541
  }
16501
16542
  });
16502
16543
 
@@ -23365,6 +23406,49 @@ function formatToolUse(name, input) {
23365
23406
  const argDisplay = primaryArg ? source_default.dim(" ") + source_default.white(String(primaryArg).replace(/\n/g, "\u21B5").slice(0, 80)) : "";
23366
23407
  return source_default.yellow(icons.tool) + " " + source_default.bold(name) + argDisplay;
23367
23408
  }
23409
+ var ToolStreamPrinter = class _ToolStreamPrinter {
23410
+ carry = "";
23411
+ linesShown = 0;
23412
+ // Cap visible lines per command so a runaway/very chatty process (e.g. a
23413
+ // build tool re-printing a progress bar thousands of times) doesn't flood
23414
+ // the terminal — the full output is still available afterward via
23415
+ // formatToolResult's summary and, for large output, the spill file.
23416
+ static MAX_LINES = 200;
23417
+ capped = false;
23418
+ feed(chunk) {
23419
+ if (this.capped)
23420
+ return;
23421
+ this.carry += chunk;
23422
+ const lines = this.carry.split("\n");
23423
+ this.carry = lines.pop() ?? "";
23424
+ for (const line of lines) {
23425
+ if (!this.printLine(line))
23426
+ return;
23427
+ }
23428
+ }
23429
+ printLine(line) {
23430
+ if (this.linesShown >= _ToolStreamPrinter.MAX_LINES) {
23431
+ if (!this.capped) {
23432
+ this.capped = true;
23433
+ process.stdout.write(source_default.dim(" \u2502 \u2026 output continues (full log in the result above once finished)\n"));
23434
+ }
23435
+ return false;
23436
+ }
23437
+ this.linesShown++;
23438
+ process.stdout.write(source_default.dim(" \u2502 ") + source_default.gray(line.slice(0, 300)) + "\n");
23439
+ return true;
23440
+ }
23441
+ flush() {
23442
+ if (!this.capped && this.carry)
23443
+ this.printLine(this.carry);
23444
+ this.carry = "";
23445
+ }
23446
+ reset() {
23447
+ this.carry = "";
23448
+ this.linesShown = 0;
23449
+ this.capped = false;
23450
+ }
23451
+ };
23368
23452
  function formatToolResult(name, result, durationMs) {
23369
23453
  if (result.error) {
23370
23454
  return source_default.red(icons.cross) + " " + source_default.dim(name) + source_default.dim(" \xB7 ") + source_default.red("error") + source_default.dim(` \xB7 ${durationMs}ms`);
@@ -23385,6 +23469,77 @@ function formatUsage(outputTokens) {
23385
23469
  return source_default.dim(`\u2193 ${tok} tokens`);
23386
23470
  }
23387
23471
 
23472
+ // src/ui/paste.ts
23473
+ var PASTE_START = "\x1B[200~";
23474
+ var PASTE_END = "\x1B[201~";
23475
+ function enableBracketedPaste() {
23476
+ if (process.stdout.isTTY)
23477
+ process.stdout.write("\x1B[?2004h");
23478
+ }
23479
+ function disableBracketedPaste() {
23480
+ if (process.stdout.isTTY)
23481
+ process.stdout.write("\x1B[?2004l");
23482
+ }
23483
+ function installPasteHandling(rl, stdin) {
23484
+ const originalListeners = stdin.listeners("data");
23485
+ for (const l of originalListeners)
23486
+ stdin.removeListener("data", l);
23487
+ const forward = (buf) => {
23488
+ if (!buf.length)
23489
+ return;
23490
+ for (const l of originalListeners)
23491
+ l.call(stdin, buf);
23492
+ };
23493
+ let buffering = false;
23494
+ let buffer = "";
23495
+ const insertPastedText = (text) => {
23496
+ if (!text)
23497
+ return;
23498
+ const flattened = text.replace(/\r\n|\r|\n/g, " ");
23499
+ const anyRl = rl;
23500
+ if (typeof anyRl._insertString === "function") {
23501
+ anyRl._insertString(flattened);
23502
+ } else {
23503
+ rl.write(flattened);
23504
+ }
23505
+ };
23506
+ const onData = (chunk) => {
23507
+ let text = chunk.toString("utf-8");
23508
+ for (; ; ) {
23509
+ if (!buffering) {
23510
+ const startIdx = text.indexOf(PASTE_START);
23511
+ if (startIdx === -1) {
23512
+ forward(Buffer.from(text, "utf-8"));
23513
+ return;
23514
+ }
23515
+ const before = text.slice(0, startIdx);
23516
+ forward(Buffer.from(before, "utf-8"));
23517
+ text = text.slice(startIdx + PASTE_START.length);
23518
+ buffering = true;
23519
+ buffer = "";
23520
+ }
23521
+ const endIdx = text.indexOf(PASTE_END);
23522
+ if (endIdx === -1) {
23523
+ buffer += text;
23524
+ return;
23525
+ }
23526
+ buffer += text.slice(0, endIdx);
23527
+ insertPastedText(buffer);
23528
+ buffer = "";
23529
+ buffering = false;
23530
+ text = text.slice(endIdx + PASTE_END.length);
23531
+ if (!text)
23532
+ return;
23533
+ }
23534
+ };
23535
+ stdin.on("data", onData);
23536
+ return () => {
23537
+ stdin.removeListener("data", onData);
23538
+ for (const l of originalListeners)
23539
+ stdin.on("data", l);
23540
+ };
23541
+ }
23542
+
23388
23543
  // src/permissions/handler.ts
23389
23544
  var readline2 = __toESM(require("readline"));
23390
23545
  var fs2 = __toESM(require("fs"));
@@ -24166,7 +24321,7 @@ async function requestPermission(req) {
24166
24321
  var fs3 = __toESM(require("fs"));
24167
24322
  var path = __toESM(require("path"));
24168
24323
  var os2 = __toESM(require("os"));
24169
- var SESSIONS_DIR = path.join(os2.homedir(), ".nexrall", "cli-sessions");
24324
+ var SESSIONS_DIR = process.env.NEXRALL_CLI_SESSIONS_DIR || path.join(os2.homedir(), ".nexrall", "cli-sessions");
24170
24325
  function ensureDir() {
24171
24326
  fs3.mkdirSync(SESSIONS_DIR, { recursive: true });
24172
24327
  }
@@ -24482,10 +24637,17 @@ var Spinner = class {
24482
24637
  interval = null;
24483
24638
  frames = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
24484
24639
  i = 0;
24640
+ startedAt = 0;
24641
+ baseText = "";
24485
24642
  start(text) {
24486
24643
  this.stop();
24644
+ this.baseText = text;
24645
+ this.startedAt = Date.now();
24487
24646
  this.interval = setInterval(() => {
24488
- process.stdout.write(`\r${source_default.cyan(this.frames[this.i % this.frames.length])} ${source_default.dim(text)}`);
24647
+ const elapsedSec = Math.floor((Date.now() - this.startedAt) / 1e3);
24648
+ const timer = source_default.dim(` (${elapsedSec}s)`);
24649
+ const hint = elapsedSec >= 15 ? source_default.dim(" \xB7 still running, no output yet is normal for quiet commands") : "";
24650
+ process.stdout.write(`\r\x1B[K${source_default.cyan(this.frames[this.i % this.frames.length])} ${source_default.dim(this.baseText)}${timer}${hint}`);
24489
24651
  this.i++;
24490
24652
  }, 100);
24491
24653
  }
@@ -24500,10 +24662,71 @@ var Spinner = class {
24500
24662
  return this.interval !== null;
24501
24663
  }
24502
24664
  };
24665
+ var _mcpManager = null;
24666
+ var _mcpConfig = null;
24667
+ async function initMcp(workDir) {
24668
+ _mcpConfig = import_code_core3.McpManager.loadConfig(workDir);
24669
+ if (Object.keys(_mcpConfig.mcpServers).length === 0) {
24670
+ _mcpManager = null;
24671
+ return;
24672
+ }
24673
+ const manager = new import_code_core3.McpManager();
24674
+ await manager.connectAll(_mcpConfig);
24675
+ _mcpManager = manager;
24676
+ }
24677
+ function formatMcpStatus() {
24678
+ if (!_mcpConfig || Object.keys(_mcpConfig.mcpServers).length === 0) {
24679
+ return source_default.dim(" No MCP servers configured. Add one to .nexrall/mcp.json or ~/.nexrall/mcp.json.");
24680
+ }
24681
+ if (!_mcpManager)
24682
+ return source_default.dim(" MCP servers configured but not connected.");
24683
+ const rows = _mcpManager.getStatus();
24684
+ const lines = rows.map((s2) => {
24685
+ if (s2.connected) {
24686
+ return " " + source_default.green("\u25CF") + " " + source_default.bold(s2.name) + source_default.dim(` (${s2.transport}) \u2014 ${s2.tools.length} tool(s)`);
24687
+ }
24688
+ const authNote = s2.needsAuth ? source_default.yellow(" [needs OAuth \u2014 not supported in CLI yet, add a static Authorization header]") : "";
24689
+ return " " + source_default.red("\u25CF") + " " + source_default.bold(s2.name) + source_default.dim(` (${s2.transport}) \u2014 `) + source_default.red(s2.error ?? "failed to connect") + authNote;
24690
+ });
24691
+ return lines.join("\n");
24692
+ }
24693
+ function formatContextUsage(messages, modelAlias) {
24694
+ const window = (0, import_code_core3.contextWindowFor)(modelAlias);
24695
+ const tokens = (0, import_code_core3.estimateTokensRough)(messages);
24696
+ const bytes = (0, import_code_core3.estimateBodyBytes)(messages);
24697
+ const { prune, compact } = (0, import_code_core3.compactionThresholds)();
24698
+ const pct = Math.min(100, tokens / window * 100);
24699
+ const barWidth = 30;
24700
+ const filled = Math.round(pct / 100 * barWidth);
24701
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
24702
+ const barColor = pct >= compact * 100 ? source_default.red : pct >= prune * 100 ? source_default.yellow : source_default.green;
24703
+ const lines = [
24704
+ "",
24705
+ " " + source_default.bold("Context usage"),
24706
+ " " + barColor(bar) + source_default.dim(` ~${pct.toFixed(1)}%`),
24707
+ source_default.dim(` ~${tokens.toLocaleString()} / ${window.toLocaleString()} tokens (rough estimate) \xB7 ${(bytes / 1024).toFixed(0)}KB serialized \xB7 ${messages.length} messages`),
24708
+ source_default.dim(` auto-prune at ~${(prune * 100).toFixed(0)}% \xB7 auto-compact (summarise) at ~${(compact * 100).toFixed(0)}%`),
24709
+ ""
24710
+ ];
24711
+ return lines.join("\n");
24712
+ }
24713
+ function formatAgentsList(workDir) {
24714
+ const types3 = (0, import_code_core3.loadAgentTypes)(workDir);
24715
+ if (!types3.length)
24716
+ return source_default.dim(" No agent types found (this should not happen \u2014 builtins always register).");
24717
+ const lines = types3.map((t2) => {
24718
+ const tools = t2.tools ? source_default.dim(` \u2014 tools: ${t2.tools.join(", ")}`) : source_default.dim(" \u2014 all tools");
24719
+ const model = t2.model ? source_default.dim(` \u2014 model: ${t2.model}`) : "";
24720
+ const scope = source_default.dim(` (${t2.source})`);
24721
+ return " " + source_default.cyan(t2.name.padEnd(16)) + source_default.white(t2.description) + scope + tools + model;
24722
+ });
24723
+ return lines.join("\n");
24724
+ }
24503
24725
  async function runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrallMd, mode, effort, checkpointManager, onProgress) {
24504
24726
  let lastUsage;
24505
24727
  const spinner = new Spinner();
24506
24728
  const mdRender = new MarkdownStreamRenderer();
24729
+ const toolStream = new ToolStreamPrinter();
24507
24730
  let toolStartTime = 0;
24508
24731
  let lastToolName = "";
24509
24732
  let thinkingTokens = 0;
@@ -24551,14 +24774,27 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrall
24551
24774
  return;
24552
24775
  mdRender.flush();
24553
24776
  spinner.stop();
24777
+ toolStream.reset();
24554
24778
  process.stdout.write("\n");
24555
24779
  console.log(formatToolUse(name, input));
24556
24780
  lastToolName = name;
24557
24781
  toolStartTime = Date.now();
24558
24782
  spinner.start(`running ${name}\u2026`);
24559
24783
  },
24784
+ // Live progress for foreground bash: the FIRST chunk stops the "running…"
24785
+ // spinner (which would otherwise overwrite/interleave badly with printed
24786
+ // lines) and every subsequent chunk streams straight to the terminal —
24787
+ // see ToolStreamPrinter for the line-buffering/cap logic.
24788
+ onToolStreamChunk: (_name, chunk) => {
24789
+ if (abortSignal.aborted)
24790
+ return;
24791
+ if (spinner.active)
24792
+ spinner.stop();
24793
+ toolStream.feed(chunk);
24794
+ },
24560
24795
  onToolResult: (_name, res) => {
24561
24796
  spinner.stop();
24797
+ toolStream.flush();
24562
24798
  const durationMs = Date.now() - toolStartTime;
24563
24799
  console.log(formatToolResult(lastToolName, res, durationMs));
24564
24800
  toolStartTime = 0;
@@ -24610,7 +24846,8 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrall
24610
24846
  },
24611
24847
  requestPermission,
24612
24848
  checkpointManager,
24613
- onProgress
24849
+ onProgress,
24850
+ mcpManager: _mcpManager ?? void 0
24614
24851
  });
24615
24852
  mdRender.flush();
24616
24853
  spinner.stop();
@@ -24650,6 +24887,13 @@ async function runTurnHeadless(messages, modelAlias, workDir, _abortSignal, env2
24650
24887
  onToolUse: (name, input) => {
24651
24888
  emit({ type: "tool_use", tool: name, input });
24652
24889
  },
24890
+ // Live bash progress for scripted/CI consumers — lets a wrapper tail a
24891
+ // long-running build/test command instead of blocking silently until
24892
+ // tool_result. Chunk is raw and un-truncated (unlike the final result,
24893
+ // which is head/tail-capped), so a chatty command can emit many of these.
24894
+ onToolStreamChunk: (name, chunk) => {
24895
+ emit({ type: "tool_stream", tool: name, chunk });
24896
+ },
24653
24897
  onToolResult: (name, res) => {
24654
24898
  toolCallCount++;
24655
24899
  emit({ type: "tool_result", tool: name, ok: res.error === void 0, ...res.error ? { error: res.error } : {} });
@@ -24688,7 +24932,8 @@ async function runTurnHeadless(messages, modelAlias, workDir, _abortSignal, env2
24688
24932
  return true;
24689
24933
  },
24690
24934
  checkpointManager,
24691
- onProgress
24935
+ onProgress,
24936
+ mcpManager: _mcpManager ?? void 0
24692
24937
  });
24693
24938
  if (format === "json") {
24694
24939
  process.stdout.write(JSON.stringify({
@@ -24723,8 +24968,12 @@ function printHelp() {
24723
24968
  ["/yolo", "Auto-approve all permissions"],
24724
24969
  ["/balance", "Show wallet balance"],
24725
24970
  ["/add <filepath>", "Add a file to conversation context"],
24971
+ ["/image <filepath> [caption]", "Attach an image or PDF (png/jpg/gif/webp/pdf, max 10MB) and send it to the model"],
24726
24972
  ["/skills", "List skills (.nexrall/skills/<name>/SKILL.md or .nexrall/commands/*.md) \u2014 the model can also auto-invoke these"],
24727
24973
  ["/plugins", "List installed plugins (.nexrall/plugins/)"],
24974
+ ["/mcp", "Show MCP server connection status"],
24975
+ ["/agents", "List available sub-agent types (for the task tool)"],
24976
+ ["/context", "Show context window usage for this conversation"],
24728
24977
  ["/update", "Update nex to the latest version"]
24729
24978
  ];
24730
24979
  for (const [cmd, desc] of cmds) {
@@ -24785,6 +25034,15 @@ async function startChatSession(options) {
24785
25034
  let skills = (0, import_code_core3.loadSkills)(workDir);
24786
25035
  if (skills.length && !headless)
24787
25036
  console.log(source_default.dim(` ${skills.length} skill(s) loaded`));
25037
+ try {
25038
+ await initMcp(workDir);
25039
+ const connected = _mcpManager?.serverNames.length ?? 0;
25040
+ const total = _mcpConfig ? Object.keys(_mcpConfig.mcpServers).length : 0;
25041
+ if (total && !headless) {
25042
+ console.log(source_default.dim(` ${connected}/${total} MCP server(s) connected`) + source_default.dim(" (/mcp for details)"));
25043
+ }
25044
+ } catch {
25045
+ }
24788
25046
  if (headless && !options.prompt && !options.stdinText) {
24789
25047
  process.stdout.write(JSON.stringify({ type: "error", error: "--output-format json/stream-json requires a one-shot prompt (or piped stdin)." }) + "\n");
24790
25048
  process.exit(1);
@@ -24925,6 +25183,9 @@ ${text}` : "");
24925
25183
  prompt: colors.primary.bold("> ")
24926
25184
  });
24927
25185
  setReadlineInterface(rl);
25186
+ enableBracketedPaste();
25187
+ process.on("exit", disableBracketedPaste);
25188
+ const uninstallPasteHandling = installPasteHandling(rl, process.stdin);
24928
25189
  rl.prompt();
24929
25190
  let inputBuffer = "";
24930
25191
  rl.on("line", async (rawLine) => {
@@ -25283,6 +25544,80 @@ ${content}
25283
25544
  rl.prompt();
25284
25545
  return;
25285
25546
  }
25547
+ case "/image": {
25548
+ if (agentRunning) {
25549
+ console.log(source_default.red(" Cannot attach while agent is running."));
25550
+ rl.prompt();
25551
+ return;
25552
+ }
25553
+ if (!arg) {
25554
+ console.log(source_default.red(" Usage: /image <filepath> [caption]"));
25555
+ rl.prompt();
25556
+ return;
25557
+ }
25558
+ const [rawPath, ...captionParts] = arg.split(/\s+/);
25559
+ const caption = captionParts.join(" ").trim();
25560
+ const filePath = path3.isAbsolute(rawPath) ? rawPath : path3.join(workDir, rawPath);
25561
+ const ext = path3.extname(filePath).toLowerCase();
25562
+ const IMAGE_MIME = {
25563
+ ".png": "image/png",
25564
+ ".jpg": "image/jpeg",
25565
+ ".jpeg": "image/jpeg",
25566
+ ".gif": "image/gif",
25567
+ ".webp": "image/webp"
25568
+ };
25569
+ const isPdf = ext === ".pdf";
25570
+ const mimeType = IMAGE_MIME[ext];
25571
+ if (!mimeType && !isPdf) {
25572
+ console.log(source_default.red(` Unsupported file type: ${ext || "(none)"}. Supported: png, jpg, jpeg, gif, webp, pdf.`));
25573
+ rl.prompt();
25574
+ return;
25575
+ }
25576
+ let buf;
25577
+ try {
25578
+ buf = fs5.readFileSync(filePath);
25579
+ } catch (err) {
25580
+ console.error(source_default.red(" Failed: ") + String(err.message));
25581
+ rl.prompt();
25582
+ return;
25583
+ }
25584
+ const MAX_BYTES = 10 * 1024 * 1024;
25585
+ if (buf.length > MAX_BYTES) {
25586
+ console.log(source_default.red(` File too large (${(buf.length / 1024 / 1024).toFixed(1)}MB) \u2014 max 10MB.`));
25587
+ rl.prompt();
25588
+ return;
25589
+ }
25590
+ const rel = path3.relative(workDir, filePath);
25591
+ const data = buf.toString("base64");
25592
+ const label = caption ? `[Attached: ${rel}]
25593
+ ${caption}` : `[Attached: ${rel}]`;
25594
+ const content = [{ type: "text", text: label }];
25595
+ content.push(
25596
+ isPdf ? { type: "document", source: { type: "base64", media_type: "application/pdf", data } } : { type: "image", source: { type: "base64", media_type: mimeType, data } }
25597
+ );
25598
+ console.log(source_default.dim(` Attached ${rel} (${(buf.length / 1024).toFixed(0)}KB) \u2014 sending\u2026`));
25599
+ checkpoints.beginTurn(`/image ${rel}${caption ? " " + caption : ""}`, messages.length);
25600
+ messages.push({ role: "user", content });
25601
+ rl.pause();
25602
+ abortSignal.aborted = false;
25603
+ agentRunning = true;
25604
+ try {
25605
+ const result = await runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrallMd, agentMode, effortLevel, checkpoints, saveProgress);
25606
+ messages = result.messages;
25607
+ updateTitle();
25608
+ saveSession(sessionId, sessionTitle, workDir, messages);
25609
+ } catch (err) {
25610
+ messages = recoverTurn(err, messages);
25611
+ updateTitle();
25612
+ saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
25613
+ } finally {
25614
+ agentRunning = false;
25615
+ checkpoints.commitTurn();
25616
+ }
25617
+ rl.resume();
25618
+ rl.prompt();
25619
+ return;
25620
+ }
25286
25621
  case "/update": {
25287
25622
  rl.close();
25288
25623
  await updateCommand({ yes: false });
@@ -25306,6 +25641,29 @@ ${content}
25306
25641
  rl.prompt();
25307
25642
  return;
25308
25643
  }
25644
+ case "/mcp": {
25645
+ console.log();
25646
+ console.log(source_default.bold(" MCP servers:"));
25647
+ console.log(formatMcpStatus());
25648
+ console.log();
25649
+ rl.prompt();
25650
+ return;
25651
+ }
25652
+ case "/agents": {
25653
+ console.log();
25654
+ console.log(source_default.bold(" Sub-agent types (used by the `task` tool):"));
25655
+ console.log(formatAgentsList(workDir));
25656
+ console.log();
25657
+ console.log(source_default.dim(" Define your own in .nexrall/agents/<name>.md or ~/.nexrall/agents/<name>.md"));
25658
+ console.log();
25659
+ rl.prompt();
25660
+ return;
25661
+ }
25662
+ case "/context": {
25663
+ console.log(formatContextUsage(messages, modelAlias));
25664
+ rl.prompt();
25665
+ return;
25666
+ }
25309
25667
  case "/skills":
25310
25668
  case "/commands": {
25311
25669
  skills = (0, import_code_core3.loadSkills)(workDir);
@@ -25383,6 +25741,7 @@ ${content}
25383
25741
  rl.prompt();
25384
25742
  });
25385
25743
  rl.on("close", () => {
25744
+ uninstallPasteHandling();
25386
25745
  updateTitle();
25387
25746
  if (messages.length)
25388
25747
  saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
@@ -25596,7 +25955,7 @@ function pluginListCommand() {
25596
25955
 
25597
25956
  // src/index.ts
25598
25957
  var program2 = new Command();
25599
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.23");
25958
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.27");
25600
25959
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
25601
25960
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
25602
25961
  program2.command("update").description("Update nex to the latest version").option("-c, --check", "Check for updates without installing").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.23",
3
+ "version": "0.5.27",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",
@@ -37,7 +37,7 @@
37
37
  "ora": "^8.0.1",
38
38
  "prompts": "^2.4.2",
39
39
  "readline": "^1.3.0",
40
- "@nexrall/code-core": "1.4.18"
40
+ "@nexrall/code-core": "1.4.21"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@aws-sdk/client-s3": "^3.600.0",
@@ -52,6 +52,7 @@
52
52
  "build": "node build.js",
53
53
  "dev": "tsx src/index.ts",
54
54
  "start": "node dist/index.js",
55
+ "test": "tsc --noEmit && tsc --noEmit -p test/tsconfig.json && node --import tsx --test test/*.test.ts test/*.test.mts",
55
56
  "release": "node build.js && node scripts/upload-release.js"
56
57
  }
57
58
  }