nexrall-code 0.5.130 → 0.5.132

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 +255 -156
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11208,7 +11208,7 @@ var require_securityLint = __commonJS({
11208
11208
  // So the interpolation must name something that plausibly came from the
11209
11209
  // outside: req/request/params/query/body/input/user/args, or a bare
11210
11210
  // `'...' + ident`. This trades recall for precision on purpose — thorough SQL
11211
- // review is the security-auditor agent's job, not an inline regex's.
11211
+ // review is a dedicated security review's job, not an inline regex's.
11212
11212
  //
11213
11213
  // THE GAP IS PER-ALTERNATIVE, and that asymmetry is load-bearing. The concat
11214
11214
  // branch originally shared the no-quote gap, which made it miss the QUOTED
@@ -12188,24 +12188,6 @@ var require_agentTypes = __commonJS({
12188
12188
  "get_diagnostics"
12189
12189
  ];
12190
12190
  var RESEARCH_TOOLS = [...READ_ONLY_TOOLS, "fetch_url", "web_search"];
12191
- var WRITE_TOOLS = [
12192
- ...READ_ONLY_TOOLS,
12193
- "write_file",
12194
- "edit_file",
12195
- "multi_edit",
12196
- "create_directory",
12197
- "move_file",
12198
- "copy_file",
12199
- // notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
12200
- // knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
12201
- // test-writer silently could not touch notebooks.
12202
- "notebook_edit",
12203
- // Office document generation — same write-access tier as write_file, just a
12204
- // structured content shape instead of raw text (see tools/executor.ts).
12205
- "write_docx",
12206
- "write_xlsx",
12207
- "write_pptx"
12208
- ];
12209
12191
  var BUILTIN_AGENTS = [
12210
12192
  {
12211
12193
  // The catch-all, matching Claude Code's `general-purpose`.
@@ -12238,66 +12220,6 @@ var require_agentTypes = __commonJS({
12238
12220
  prompt: "You are a general-purpose engineering sub-agent. Work the task end to end: explore what you need, make the changes, and verify them with the project's own build/test commands.\n\nRules:\n- Mirror existing conventions; make the smallest correct change.\n- Verify before you claim success. If you could not verify, say so explicitly.\n- Your FINAL MESSAGE is the only thing that reaches the main agent: state what you changed (with file paths), what you ran and its outcome, and anything you deliberately left undone.",
12239
12221
  source: "builtin"
12240
12222
  },
12241
- {
12242
- name: "reviewer",
12243
- description: "Read-only code reviewer \u2014 finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.",
12244
- tools: READ_ONLY_TOOLS,
12245
- source: "builtin",
12246
- prompt: [
12247
- "You are a meticulous senior code reviewer. You NEVER modify files \u2014 you only read, search, and report.",
12248
- "",
12249
- "Method:",
12250
- "1. Read the full context around every change you are asked to review; never judge a hunk in isolation.",
12251
- "2. Hunt specifically for: correctness bugs, unhandled edge cases (empty/null/unicode/concurrency/timezone),",
12252
- " security issues (injection, path traversal, secrets in code, unsafe deserialization), breaking API",
12253
- " changes (search for callers first), silent behaviour changes, and swallowed errors.",
12254
- "3. Verify test coverage: are the changed paths tested? Were assertions weakened or tests deleted?",
12255
- "4. Only use bash for read-only commands (git diff/log/show, grep, test runs). Never run mutating commands.",
12256
- "",
12257
- "Report format: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F7E2} Suggestion, each with file:line and a concrete fix,",
12258
- "then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale."
12259
- ].join("\n")
12260
- },
12261
- // Promoted from the security-audit plugin to a builtin.
12262
- //
12263
- // Leaving it plugin-only was indefensible next to `reviewer` being builtin:
12264
- // reviewer's own prompt already tells it to look for security issues, so
12265
- // security IS treated as default work — yet the specialist agent for it was
12266
- // invisible unless the user happened to know the plugin existed. For an agent
12267
- // that WRITES code, "you only get a security review if you knew to install
12268
- // something" is the wrong default.
12269
- {
12270
- name: "security-auditor",
12271
- description: "Read-only security auditor \u2014 hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.",
12272
- tools: READ_ONLY_TOOLS,
12273
- // No `model` override — inherits the session's, same as general-purpose.
12274
- // A previous version forced 'pro' (Claude Opus 5) here, which silently
12275
- // billed Anthropic even for a session running entirely on OpenAI/DeepSeek/
12276
- // Qwen. Consistency with the main conversation's provider matters more
12277
- // than defaulting every specialist to a fixed tier.
12278
- source: "builtin",
12279
- prompt: [
12280
- "You are a security auditor. You find real, exploitable flaws \u2014 not style issues.",
12281
- "",
12282
- "Method:",
12283
- "1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network",
12284
- " input, deserialization), then trace user-controlled data inward to where it is used.",
12285
- "2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.",
12286
- "3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;",
12287
- " Medium = needs unusual preconditions; Low = hardening.",
12288
- "",
12289
- "Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken",
12290
- "authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,",
12291
- "path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,",
12292
- "and crypto misuse (hand-rolled comparison, predictable randomness).",
12293
- "",
12294
- "Hard rules:",
12295
- "- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.",
12296
- "- NEVER print a discovered secret's value. Report its location and advise rotation.",
12297
- "- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.",
12298
- '- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.'
12299
- ].join("\n")
12300
- },
12301
12223
  // The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
12302
12224
  // search that would otherwise flood the parent's context. Defaults to the
12303
12225
  // cheapest model on purpose — "find every caller of X" has no need of a
@@ -12338,10 +12260,14 @@ var require_agentTypes = __commonJS({
12338
12260
  // quietly become "start editing".
12339
12261
  {
12340
12262
  name: "planner",
12263
+ // Lean prompt, as Claude Code's Plan builtin: it researches and reports, and the MAIN
12264
+ // agent (which has nexrall.md and the conversation) judges the plan against project
12265
+ // rules. Conventions that must bind the plan belong in the delegation prompt.
12266
+ lightPrompt: true,
12341
12267
  description: "Read-only planning agent \u2014 researches a change and returns a concrete step-by-step implementation plan with risks and affected files. Cannot modify files.",
12342
12268
  tools: RESEARCH_TOOLS,
12343
12269
  // No `model` override — inherits the session's, for the same reason as
12344
- // explorer/security-auditor above: a fixed alias silently billed Anthropic
12270
+ // explorer above: a fixed alias silently billed Anthropic
12345
12271
  // regardless of the user's chosen provider.
12346
12272
  source: "builtin",
12347
12273
  prompt: [
@@ -12361,74 +12287,6 @@ var require_agentTypes = __commonJS({
12361
12287
  "- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).",
12362
12288
  "- Anything genuinely ambiguous, stated as an open question rather than a silent assumption."
12363
12289
  ].join("\n")
12364
- },
12365
- // Promoted from the test-gen plugin. Needs write access — it produces test
12366
- // files — but is deliberately forbidden from touching source, because "make the
12367
- // tests pass" is the single most common way an agent destroys signal.
12368
- {
12369
- name: "test-writer",
12370
- description: "Writes tests that follow the project's existing conventions. May create/edit TEST files only \u2014 never production source.",
12371
- tools: WRITE_TOOLS,
12372
- // Enforced, not merely requested: the permission gate refuses a write whose
12373
- // path is not a test file. Without this the allowlist would grant edit_file
12374
- // for every path and the rule below would be a suggestion the model is free
12375
- // to rationalise its way past.
12376
- testFilesOnly: true,
12377
- source: "builtin",
12378
- prompt: [
12379
- "You write tests. You may create and edit TEST files only.",
12380
- "",
12381
- "Hard rules \u2014 these are the ways test-writing agents destroy value, so they are non-negotiable:",
12382
- "- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.",
12383
- "- NEVER weaken, delete or skip an existing assertion or test.",
12384
- "- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.",
12385
- "",
12386
- "Method:",
12387
- "1. Read the existing tests FIRST and copy their conventions exactly \u2014 runner, file naming, layout,",
12388
- " assertion style, fixture/helper patterns. Never introduce a new framework.",
12389
- "2. Test observable behaviour and the contract, not private internals.",
12390
- "3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,",
12391
- " boundaries, error paths, concurrency where it applies.",
12392
- "4. No sleeps or wall-clock dependence \u2014 those produce the flaky tests that get deleted later.",
12393
- "5. RUN the tests you wrote and report the real output. Never claim a test passes without running it."
12394
- ].join("\n")
12395
- },
12396
- // The DevOps gap — answered with a READ-ONLY advisor, not an operator.
12397
- //
12398
- // A "DevOps agent" with write/apply access is a genuinely different risk class
12399
- // from the others here: its mistakes are `kubectl delete`, a bad `terraform
12400
- // apply`, a broken deploy pipeline — often not revertible and affecting
12401
- // production rather than a working tree. So this one diagnoses and proposes a
12402
- // diff; a human applies it. That asymmetry is the whole design.
12403
- {
12404
- name: "devops-advisor",
12405
- description: "Read-only CI/CD, container, and infrastructure advisor \u2014 diagnoses pipelines, Dockerfiles, and k8s manifests and proposes concrete fixes as a diff. Never applies changes.",
12406
- tools: RESEARCH_TOOLS,
12407
- // No `model` override — inherits the session's, for the same reason as the
12408
- // other specialists above.
12409
- source: "builtin",
12410
- prompt: [
12411
- "You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.",
12412
- "",
12413
- "Hard rules:",
12414
- "- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only",
12415
- " (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).",
12416
- " NEVER run anything that mutates infrastructure \u2014 no apply/delete/scale/rollout/restart/push,",
12417
- " no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.",
12418
- "- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.",
12419
- "",
12420
- "Method:",
12421
- "1. Read what actually exists \u2014 workflow files, Dockerfiles, manifests, kustomize overlays, the",
12422
- ' deploy scripts \u2014 before drawing any conclusion. Never reason from what a stack "usually" looks like.',
12423
- "2. Follow the real path a change takes to production, and name the step that is broken or missing.",
12424
- "3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually",
12425
- " needs, image tags that do not match what is deployed, missing health probes, absent resource",
12426
- " limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and",
12427
- " migrations that must run before the new image is live.",
12428
- "",
12429
- "Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or",
12430
- "exact file content, and the command a human should run to apply and verify it."
12431
- ].join("\n")
12432
12290
  }
12433
12291
  ];
12434
12292
  var KNOWN_TOOL_NAMES = /* @__PURE__ */ new Set([
@@ -107812,7 +107670,7 @@ ${body}`);
107812
107670
  function formatTokenCount(n) {
107813
107671
  return n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
107814
107672
  }
107815
- function formatElapsed(ms) {
107673
+ function formatElapsed2(ms) {
107816
107674
  const s2 = Math.round(ms / 1e3);
107817
107675
  return s2 < 60 ? `${s2}s` : `${Math.floor(s2 / 60)}m ${s2 % 60}s`;
107818
107676
  }
@@ -108000,7 +107858,7 @@ ${agent.prompt}` : "",
108000
107858
  }
108001
107859
  isoState = void 0;
108002
107860
  }
108003
- const stats = `[sub-agent stats: ${subToolCalls} tool call(s) \xB7 ${formatTokenCount(subTokens)} tokens \xB7 $${subCost.toFixed(3)} \xB7 ${formatElapsed(Date.now() - subStartedAt)}]`;
107861
+ const stats = `[sub-agent stats: ${subToolCalls} tool call(s) \xB7 ${formatTokenCount(subTokens)} tokens \xB7 $${subCost.toFixed(3)} \xB7 ${formatElapsed2(Date.now() - subStartedAt)}]`;
108004
107862
  const out = { ...r2, ...childVerifications.length ? { childVerifications } : {} };
108005
107863
  if (out.error !== void 0)
108006
107864
  out.error = `${out.error}
@@ -108871,7 +108729,7 @@ Continue the work from here.` }] });
108871
108729
  hasOwnAgentStore: !!options._agentMemory,
108872
108730
  // Same derivation, same reason, for the network. The base prompt's
108873
108731
  // "Web search" section tells the run to reach the network, but
108874
- // READ_ONLY_TOOLS (explorer, security-auditor, ...) contains neither
108732
+ // READ_ONLY_TOOLS (explorer, planner, ...) contains neither
108875
108733
  // `fetch_url` nor `web_search`. MEASURED on the 195-trial 2026-08-10
108876
108734
  // deepseek-v4-pro benchmark: 29 of 51 `fetch_url` failures and 3 of 14
108877
108735
  // `web_search` failures were the permission gate refusing a sub-agent
@@ -157566,7 +157424,8 @@ function composeFooter(base, extras, columns, reserve = 0) {
157566
157424
  optional.push({ keep: 4, at: 2, segment: ctxSegment(extras.ctxPct) });
157567
157425
  if (typeof extras?.costUsd === "number")
157568
157426
  optional.push({ keep: 2, at: 3, segment: { text: money2(extras.costUsd), tone: "dim" } });
157569
- const build = (kept2) => [head, ...[...kept2].sort((x2, y) => x2.at - y.at).map((k) => k.segment), ...tail];
157427
+ let tailShown = tail.length;
157428
+ const build = (kept2) => [head, ...[...kept2].sort((x2, y) => x2.at - y.at).map((k) => k.segment), ...tail.slice(0, tailShown)];
157570
157429
  const width = (segs) => visibleLen(segs.map((s2) => s2.text).join(SEP2));
157571
157430
  let kept = [...optional];
157572
157431
  const budget = columns - 1 - reserve;
@@ -157577,6 +157436,8 @@ function composeFooter(base, extras, columns, reserve = 0) {
157577
157436
  lowest = i2;
157578
157437
  kept.splice(lowest, 1);
157579
157438
  }
157439
+ while (tailShown > 0 && width(build(kept)) > budget)
157440
+ tailShown--;
157580
157441
  return build(kept);
157581
157442
  }
157582
157443
 
@@ -157896,6 +157757,7 @@ var BUILTIN_SLASH_COMMANDS = [
157896
157757
  { name: "cost", description: "Tokens and cost of this conversation", kind: "builtin" },
157897
157758
  { name: "status", description: "Version, model, mode, directory and session at a glance", kind: "builtin" },
157898
157759
  { name: "copy", description: "Copy the last reply to the clipboard", kind: "builtin" },
157760
+ { name: "bashes", description: "List, show or stop commands moved to the background (Ctrl+B)", argHint: "[show|kill <id>]", kind: "builtin" },
157899
157761
  { name: "expand", description: "Show the full output of a folded tool call (Ctrl+O)", argHint: "[n]", kind: "builtin" },
157900
157762
  { name: "export", description: "Write this conversation to a Markdown file", argHint: "[file]", kind: "builtin" },
157901
157763
  { name: "rename", description: "Rename this session", argHint: "<title>", requiresArg: true, kind: "builtin" },
@@ -158037,6 +157899,7 @@ function runBang(cmd, opts) {
158037
157899
  let timedOut = false;
158038
157900
  let aborted = false;
158039
157901
  let settled = false;
157902
+ let detached = false;
158040
157903
  const child = spawn("bash", ["-c", cmd], {
158041
157904
  cwd: opts.cwd,
158042
157905
  env: opts.env ?? process.env,
@@ -158077,11 +157940,25 @@ function runBang(cmd, opts) {
158077
157940
  };
158078
157941
  child.stdout.on("data", take);
158079
157942
  child.stderr.on("data", take);
157943
+ opts.onSpawn?.({ kill });
158080
157944
  const timer = setTimeout(() => {
158081
157945
  timedOut = true;
158082
157946
  kill();
158083
157947
  }, timeoutMs);
157948
+ const snapshot = () => {
157949
+ const kept = head.length + tail.length;
157950
+ return { output: head + tail, droppedBytes: Math.max(0, total - kept), code: null, signal: null, timedOut, aborted };
157951
+ };
158084
157952
  const poll = setInterval(() => {
157953
+ if (detached)
157954
+ return;
157955
+ if (opts.detach?.requested) {
157956
+ detached = true;
157957
+ clearTimeout(timer);
157958
+ clearInterval(poll);
157959
+ resolve8({ ...snapshot(), detached: true });
157960
+ return;
157961
+ }
158085
157962
  if (opts.signal?.aborted && !aborted) {
158086
157963
  aborted = true;
158087
157964
  kill();
@@ -158093,8 +157970,11 @@ function runBang(cmd, opts) {
158093
157970
  settled = true;
158094
157971
  clearTimeout(timer);
158095
157972
  clearInterval(poll);
158096
- const kept = head.length + tail.length;
158097
- resolve8({ output: head + tail, droppedBytes: Math.max(0, total - kept), code, signal, timedOut, aborted });
157973
+ const r2 = { ...snapshot(), code, signal };
157974
+ if (detached)
157975
+ opts.onSettled?.(r2);
157976
+ else
157977
+ resolve8(r2);
158098
157978
  };
158099
157979
  child.on("error", (err) => {
158100
157980
  head += `${err.message}
@@ -158105,7 +157985,7 @@ function runBang(cmd, opts) {
158105
157985
  });
158106
157986
  }
158107
157987
  function bangTranscript(cmd, r2) {
158108
- const status = r2.aborted ? "interrupted by the user" : r2.timedOut ? "timed out" : r2.code === 0 ? "exit code 0" : `exit code ${r2.code ?? r2.signal ?? "?"}`;
157988
+ const status = r2.detached ? "moved to the background by the user and still running \u2014 this is the output so far" : r2.aborted ? "interrupted by the user" : r2.timedOut ? "timed out" : r2.code === 0 ? "exit code 0" : `exit code ${r2.code ?? r2.signal ?? "?"}`;
158109
157989
  const body = r2.output.length ? r2.output.replace(/\s+$/, "") : "(no output)";
158110
157990
  const cut = r2.droppedBytes > 0 ? `
158111
157991
  \u2026 ${r2.droppedBytes} bytes omitted from the middle \u2026` : "";
@@ -158727,6 +158607,10 @@ var App2 = ({ onReady }) => {
158727
158607
  const onExpand = (0, import_react35.useCallback)((cb) => {
158728
158608
  onExpandRef.current = cb;
158729
158609
  }, []);
158610
+ const onBackgroundRef = (0, import_react35.useRef)(null);
158611
+ const onBackground = (0, import_react35.useCallback)((cb) => {
158612
+ onBackgroundRef.current = cb;
158613
+ }, []);
158730
158614
  const mentionSelRef = (0, import_react35.useRef)({ query: "", idx: 0 });
158731
158615
  const mentionDismissedRef = (0, import_react35.useRef)(null);
158732
158616
  const mentionRootRef = (0, import_react35.useRef)(process.cwd());
@@ -158930,6 +158814,16 @@ var App2 = ({ onReady }) => {
158930
158814
  onExpandRef.current?.();
158931
158815
  return;
158932
158816
  }
158817
+ if (key.ctrl && char === "b") {
158818
+ if (askResolverRef.current)
158819
+ return;
158820
+ if (busyRef.current) {
158821
+ const moved = onBackgroundRef.current?.() ?? false;
158822
+ if (!moved)
158823
+ setHintText("Ctrl+B moves a running !command to the background \u2014 none is running");
158824
+ }
158825
+ return;
158826
+ }
158933
158827
  const menu = askResolverRef.current || searchRef.current || historyRef.current.browsing ? null : computeMenu(lineRef.current, cursorRef.current, slashEntriesRef.current, menuDismissedRef.current);
158934
158828
  if (menu) {
158935
158829
  const sel = menuSelRef.current.query === menu.query ? menuSelRef.current.idx : 0;
@@ -159301,7 +159195,8 @@ var App2 = ({ onReady }) => {
159301
159195
  onCycleMode,
159302
159196
  onRewind,
159303
159197
  onHelp,
159304
- onExpand
159198
+ onExpand,
159199
+ onBackground
159305
159200
  });
159306
159201
  }, []);
159307
159202
  const slices = renderInputSlices(line, cursorPos);
@@ -159589,6 +159484,126 @@ function rewindChoices(cps) {
159589
159484
  return { options, ids: newest.map((c) => c.id) };
159590
159485
  }
159591
159486
 
159487
+ // src/ui/backgroundShells.ts
159488
+ var MAX_KEPT = 64e3;
159489
+ var BackgroundShells = class {
159490
+ nextId = 1;
159491
+ jobs = /* @__PURE__ */ new Map();
159492
+ ctls = /* @__PURE__ */ new Map();
159493
+ listeners = [];
159494
+ /** Register a detached command; returns its id. `seed` is the output it already produced. */
159495
+ add(cmd, ctl, seed = "", now = Date.now()) {
159496
+ const job = {
159497
+ id: this.nextId++,
159498
+ cmd,
159499
+ startedAt: now,
159500
+ status: "running",
159501
+ code: null,
159502
+ signal: null,
159503
+ tail: seed.length > MAX_KEPT ? seed.slice(seed.length - MAX_KEPT) : seed
159504
+ };
159505
+ this.jobs.set(job.id, job);
159506
+ this.ctls.set(job.id, ctl);
159507
+ this.emit();
159508
+ return job;
159509
+ }
159510
+ /** Append streamed output (bounded to the newest MAX_KEPT characters). */
159511
+ append(id, chunk) {
159512
+ const j = this.jobs.get(id);
159513
+ if (!j || j.status !== "running")
159514
+ return;
159515
+ j.tail += chunk;
159516
+ if (j.tail.length > MAX_KEPT)
159517
+ j.tail = j.tail.slice(j.tail.length - MAX_KEPT);
159518
+ }
159519
+ /** The process exited. A job the user killed stays 'killed' whatever the exit code says. */
159520
+ finish(id, r2, now = Date.now()) {
159521
+ const j = this.jobs.get(id);
159522
+ if (!j || j.status !== "running")
159523
+ return j;
159524
+ j.status = "done";
159525
+ j.code = r2.code;
159526
+ j.signal = r2.signal;
159527
+ j.endedAt = now;
159528
+ this.ctls.delete(id);
159529
+ this.emit();
159530
+ return j;
159531
+ }
159532
+ /** Stop a running job. False when the id is unknown or it already ended. */
159533
+ kill(id, now = Date.now()) {
159534
+ const j = this.jobs.get(id);
159535
+ const ctl = this.ctls.get(id);
159536
+ if (!j || !ctl || j.status !== "running")
159537
+ return false;
159538
+ j.status = "killed";
159539
+ j.endedAt = now;
159540
+ this.ctls.delete(id);
159541
+ ctl.kill();
159542
+ this.emit();
159543
+ return true;
159544
+ }
159545
+ killAll() {
159546
+ let n = 0;
159547
+ for (const id of [...this.ctls.keys()])
159548
+ if (this.kill(id))
159549
+ n++;
159550
+ return n;
159551
+ }
159552
+ get(id) {
159553
+ return this.jobs.get(id);
159554
+ }
159555
+ list() {
159556
+ return [...this.jobs.values()];
159557
+ }
159558
+ runningCount() {
159559
+ let n = 0;
159560
+ for (const j of this.jobs.values())
159561
+ if (j.status === "running")
159562
+ n++;
159563
+ return n;
159564
+ }
159565
+ onChange(cb) {
159566
+ this.listeners.push(cb);
159567
+ }
159568
+ emit() {
159569
+ for (const l2 of this.listeners)
159570
+ l2();
159571
+ }
159572
+ };
159573
+ function formatElapsed(ms) {
159574
+ const s2 = Math.max(0, Math.floor(ms / 1e3));
159575
+ if (s2 < 60)
159576
+ return `${s2}s`;
159577
+ const m2 = Math.floor(s2 / 60);
159578
+ if (m2 < 60)
159579
+ return `${m2}m ${String(s2 % 60).padStart(2, "0")}s`;
159580
+ return `${Math.floor(m2 / 60)}h ${String(m2 % 60).padStart(2, "0")}m`;
159581
+ }
159582
+ function describeShell(j, now = Date.now()) {
159583
+ const state = j.status === "running" ? `running ${formatElapsed(now - j.startedAt)}` : j.status === "killed" ? "killed" : j.code === 0 ? "exit 0" : `exit ${j.code ?? j.signal ?? "?"}`;
159584
+ const cmd = j.cmd.length > 60 ? j.cmd.slice(0, 59) + "\u2026" : j.cmd;
159585
+ return `#${j.id} ${state.padEnd(14)} ${cmd}`;
159586
+ }
159587
+ function parseBashesArgs(arg) {
159588
+ const parts = arg.trim().split(/\s+/).filter(Boolean);
159589
+ if (parts.length === 0)
159590
+ return { kind: "list" };
159591
+ const verb = parts[0].toLowerCase();
159592
+ if (verb === "list")
159593
+ return { kind: "list" };
159594
+ if (verb === "kill" && parts[1]?.toLowerCase() === "all" && parts.length === 2)
159595
+ return { kind: "kill-all" };
159596
+ if (verb === "show" || verb === "kill") {
159597
+ if (parts.length !== 2 || !/^#?\d+$/.test(parts[1]))
159598
+ return { kind: "error", message: `Usage: /bashes ${verb} <id>` };
159599
+ const id = Number(parts[1].replace("#", ""));
159600
+ return verb === "show" ? { kind: "show", id } : { kind: "kill", id };
159601
+ }
159602
+ if (parts.length === 1 && /^#?\d+$/.test(parts[0]))
159603
+ return { kind: "show", id: Number(parts[0].replace("#", "")) };
159604
+ return { kind: "error", message: "Usage: /bashes [show <id> | kill <id> | kill all]" };
159605
+ }
159606
+
159592
159607
  // src/ui/toolOutputStore.ts
159593
159608
  var MAX_ENTRIES = 20;
159594
159609
  var MAX_ENTRY_CHARS = 4e5;
@@ -159705,6 +159720,11 @@ var CLI_VERSION = (() => {
159705
159720
  })();
159706
159721
  var sessionUsage = new UsageTotals();
159707
159722
  var toolOutputs = new ToolOutputStore();
159723
+ var bgShells = new BackgroundShells();
159724
+ var bangDetach = null;
159725
+ process.on("exit", () => {
159726
+ bgShells.killAll();
159727
+ });
159708
159728
  function printExpansion(arg) {
159709
159729
  const r2 = resolveExpand(toolOutputs, arg);
159710
159730
  if (!r2.entry) {
@@ -160695,6 +160715,7 @@ function printHelp() {
160695
160715
  ["/team [use <id|personal>]", "List your teams, or switch what usage is billed to"],
160696
160716
  ["/cost", "Tokens and cost of this conversation so far"],
160697
160717
  ["/status", "Version, model, mode, directory, session and cost at a glance"],
160718
+ ["/bashes [show|kill <id>]", "List commands you moved to the background with Ctrl+B; show their output or stop them"],
160698
160719
  ["/expand [n]", "Show the full output of a folded tool call (newest by default; Ctrl+O does the same)"],
160699
160720
  ["/copy", "Copy the last reply to the clipboard"],
160700
160721
  ["/export [file]", "Write this conversation to a Markdown file"],
@@ -160740,6 +160761,7 @@ function printHelp() {
160740
160761
  ["Esc Esc", "At an empty prompt: pick a checkpoint to rewind to (same as /rewind)"],
160741
160762
  ["?", "At an empty prompt: show this list"],
160742
160763
  ["Ctrl+O", "Expand the last folded tool output (also works while it is running; /expand [n] picks an older one)"],
160764
+ ["Ctrl+B", "While a `!command` runs: move it to the background and keep typing (/bashes lists, shows and stops them)"],
160743
160765
  ["Ctrl+L", "Redraw the screen"],
160744
160766
  ["Ctrl+D", "Delete the character under the cursor; press twice on an empty line to exit"],
160745
160767
  ["(paste)", "Multi-line paste goes into the input box \u2014 it does not send until you press Enter"]
@@ -161185,6 +161207,13 @@ ${text}` : "");
161185
161207
  getInkTerminal().onExpand(() => {
161186
161208
  printExpansion("");
161187
161209
  });
161210
+ getInkTerminal().onBackground(() => {
161211
+ if (bangDetach && !bangDetach.requested) {
161212
+ bangDetach.requested = true;
161213
+ return true;
161214
+ }
161215
+ return false;
161216
+ });
161188
161217
  getInkTerminal().onRewind(() => {
161189
161218
  void (async () => {
161190
161219
  const opts = rewindChoices(checkpoints.list());
@@ -161724,6 +161753,39 @@ ${dirList}`;
161724
161753
  printExpansion(arg);
161725
161754
  rl.prompt();
161726
161755
  return;
161756
+ case "/bashes": {
161757
+ const a = parseBashesArgs(arg);
161758
+ if (a.kind === "error") {
161759
+ console.log(source_default.yellow(" " + a.message));
161760
+ } else if (a.kind === "list") {
161761
+ const jobs = bgShells.list();
161762
+ if (!jobs.length)
161763
+ console.log(source_default.dim(" No background commands. Start `!cmd`, then press Ctrl+B to move it to the background."));
161764
+ else
161765
+ for (const j of jobs)
161766
+ console.log(" " + (j.status === "running" ? source_default.cyan(describeShell(j)) : source_default.dim(describeShell(j))));
161767
+ } else if (a.kind === "show") {
161768
+ const j = bgShells.get(a.id);
161769
+ if (!j)
161770
+ console.log(source_default.yellow(` No background command #${a.id}.`));
161771
+ else {
161772
+ console.log(source_default.cyan(" " + describeShell(j)));
161773
+ const lines = j.tail.replace(/\s+$/, "").split("\n");
161774
+ const shownLines = lines.slice(-40);
161775
+ if (lines.length > shownLines.length)
161776
+ console.log(source_default.dim(` \u2026 ${lines.length - shownLines.length} earlier line(s) not shown`));
161777
+ for (const l2 of shownLines)
161778
+ console.log(source_default.dim(" \u2502 ") + l2.split("\r").pop().slice(0, 300));
161779
+ }
161780
+ } else if (a.kind === "kill") {
161781
+ console.log(bgShells.kill(a.id) ? source_default.green(` Stopped #${a.id}.`) : source_default.yellow(` #${a.id} is not running.`));
161782
+ } else {
161783
+ const n = bgShells.killAll();
161784
+ console.log(n ? source_default.green(` Stopped ${n} background command${n === 1 ? "" : "s"}.`) : source_default.dim(" Nothing is running."));
161785
+ }
161786
+ rl.prompt();
161787
+ return;
161788
+ }
161727
161789
  case "/copy": {
161728
161790
  const text = lastAssistantText(messages);
161729
161791
  if (!text) {
@@ -162198,22 +162260,59 @@ ${text}
162198
162260
  shown++;
162199
162261
  };
162200
162262
  let result;
162263
+ let ctl = null;
162264
+ let bgId = null;
162265
+ const detach = { requested: false };
162266
+ bangDetach = detach;
162201
162267
  try {
162202
162268
  result = await runBang(bang, {
162203
162269
  cwd: workDir,
162204
162270
  signal: abortSignal,
162271
+ detach,
162272
+ onSpawn: (c) => {
162273
+ ctl = c;
162274
+ },
162205
162275
  onData: (chunk) => {
162276
+ if (bgId !== null) {
162277
+ bgShells.append(bgId, chunk);
162278
+ return;
162279
+ }
162206
162280
  const parts = (partial + chunk).split("\n");
162207
162281
  partial = parts.pop();
162208
162282
  for (const l2 of parts)
162209
162283
  showLine(l2);
162284
+ },
162285
+ onSettled: (final) => {
162286
+ if (bgId === null)
162287
+ return;
162288
+ const job = bgShells.finish(bgId, final);
162289
+ if (!job || job.status === "killed")
162290
+ return;
162291
+ const ok = final.code === 0;
162292
+ console.log((ok ? source_default.green : source_default.red)(` \u2699 background #${job.id} finished (exit ${final.code ?? final.signal}): ${job.cmd.length > 50 ? job.cmd.slice(0, 49) + "\u2026" : job.cmd}`) + source_default.dim(" /bashes show " + job.id));
162293
+ toolOutputs.add({ name: "bash", label: `!${job.cmd} (background #${job.id})`, output: final.output, isError: !ok, durationMs: 0 });
162210
162294
  }
162211
162295
  });
162212
162296
  if (partial)
162213
162297
  showLine(partial);
162214
162298
  } finally {
162299
+ bangDetach = null;
162215
162300
  agentRunning = false;
162216
162301
  }
162302
+ if (result.detached && ctl) {
162303
+ const job = bgShells.add(bang, ctl, result.output);
162304
+ bgId = job.id;
162305
+ console.log(source_default.cyan(` \u2699 Moved to background as #${job.id}`) + source_default.dim(" (/bashes to list \xB7 /bashes show " + job.id + " \xB7 /bashes kill " + job.id + ")"));
162306
+ messages.push(
162307
+ { role: "user", content: [{ type: "text", text: bangTranscript(bang, result) }] },
162308
+ { role: "assistant", content: [{ type: "text", text: "Noted the command output." }] }
162309
+ );
162310
+ updateTitle();
162311
+ saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
162312
+ rl.resume();
162313
+ rl.prompt();
162314
+ return;
162315
+ }
162217
162316
  const tag = result.aborted ? source_default.yellow(" interrupted") : result.timedOut ? source_default.yellow(" timed out") : result.code === 0 ? source_default.dim(" exit 0") : source_default.red(` exit ${result.code ?? result.signal}`);
162218
162317
  console.log(tag);
162219
162318
  if (result.output) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.130",
3
+ "version": "0.5.132",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",