nexrall-code 0.5.185 → 0.5.187

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 +220 -8
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -40550,9 +40550,9 @@ var require_react_development = __commonJS({
40550
40550
  false === didWarnAboutMessageChannel && (didWarnAboutMessageChannel = true, "undefined" === typeof MessageChannel && console.error(
40551
40551
  "This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."
40552
40552
  ));
40553
- var channel = new MessageChannel();
40554
- channel.port1.onmessage = callback;
40555
- channel.port2.postMessage(void 0);
40553
+ var channel2 = new MessageChannel();
40554
+ channel2.port1.onmessage = callback;
40555
+ channel2.port2.postMessage(void 0);
40556
40556
  };
40557
40557
  }
40558
40558
  return enqueueTaskImpl(task);
@@ -41524,8 +41524,8 @@ var require_scheduler_production = __commonJS({
41524
41524
  localSetImmediate(performWorkUntilDeadline);
41525
41525
  };
41526
41526
  else if ("undefined" !== typeof MessageChannel) {
41527
- channel = new MessageChannel(), port = channel.port2;
41528
- channel.port1.onmessage = performWorkUntilDeadline;
41527
+ channel2 = new MessageChannel(), port = channel2.port2;
41528
+ channel2.port1.onmessage = performWorkUntilDeadline;
41529
41529
  schedulePerformWorkUntilDeadline = function() {
41530
41530
  port.postMessage(null);
41531
41531
  };
@@ -41533,7 +41533,7 @@ var require_scheduler_production = __commonJS({
41533
41533
  schedulePerformWorkUntilDeadline = function() {
41534
41534
  localSetTimeout(performWorkUntilDeadline, 0);
41535
41535
  };
41536
- var channel;
41536
+ var channel2;
41537
41537
  var port;
41538
41538
  function requestHostTimeout(callback, ms) {
41539
41539
  taskTimeoutID = localSetTimeout(function() {
@@ -41797,8 +41797,8 @@ var require_scheduler_development = __commonJS({
41797
41797
  localSetImmediate(performWorkUntilDeadline);
41798
41798
  };
41799
41799
  else if ("undefined" !== typeof MessageChannel) {
41800
- var channel = new MessageChannel(), port = channel.port2;
41801
- channel.port1.onmessage = performWorkUntilDeadline;
41800
+ var channel2 = new MessageChannel(), port = channel2.port2;
41801
+ channel2.port1.onmessage = performWorkUntilDeadline;
41802
41802
  schedulePerformWorkUntilDeadline = function() {
41803
41803
  port.postMessage(null);
41804
41804
  };
@@ -66643,6 +66643,157 @@ async function scheduleRunsCommand(id, opts = {}) {
66643
66643
  }
66644
66644
  }
66645
66645
 
66646
+ // src/commands/channelCmd.ts
66647
+ var CHANNEL_WAIT_EXIT = { OK: 0, FAILED: 1, DISCARDED: 3, TIMEOUT: 4 };
66648
+ function mapRelayError(status, error) {
66649
+ if (status === 400)
66650
+ return "the relay refused the request shape (bad session id, nonce or text).";
66651
+ if (status === 401)
66652
+ return "missing token \u2014 pass --token or set NEXRALL_CHANNEL_TOKEN.";
66653
+ if (status === 404)
66654
+ return "no such channel \u2014 it is closed, the terminal is gone, or the token is wrong.";
66655
+ if (status === 413)
66656
+ return "the message is too large for a push (8 KB max).";
66657
+ if (status === 429)
66658
+ return "rate limited \u2014 this channel is being pushed faster than it allows.";
66659
+ if (status === 503)
66660
+ return "the relay is unavailable \u2014 try again in a moment.";
66661
+ return `the relay refused the request (HTTP ${status}${error ? `: ${error}` : ""}).`;
66662
+ }
66663
+ async function channelPushCommand(textParts, opts, deps = {}) {
66664
+ const fetchFn = deps.fetchFn ?? fetch;
66665
+ const stdout = deps.stdout ?? ((line) => console.log(line));
66666
+ const stderr = deps.stderr ?? ((line) => console.error(paint.dim(line)));
66667
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve14) => setTimeout(resolve14, ms)));
66668
+ const pollMs = deps.pollMs ?? 2e3;
66669
+ const message = (textParts ?? []).join(" ").trim();
66670
+ const base = String(opts.url ?? "").replace(/\/+$/, "");
66671
+ const token = opts.token && opts.token.trim() || (process.env.NEXRALL_CHANNEL_TOKEN ?? "").trim();
66672
+ const timeoutSec = (() => {
66673
+ const t2 = parseFloat(String(opts.timeout ?? ""));
66674
+ return Number.isFinite(t2) && t2 > 0 ? t2 : 180;
66675
+ })();
66676
+ if (!message) {
66677
+ stderr("Usage: nex channel push --url <push URL> [--token <token>] [--wait] <message...>");
66678
+ return CHANNEL_WAIT_EXIT.FAILED;
66679
+ }
66680
+ if (!/^https?:\/\//.test(base)) {
66681
+ stderr("--url must be the push URL printed by `/channel open` (https://\u2026/api/cli-sessions/<id>/push).");
66682
+ return CHANNEL_WAIT_EXIT.FAILED;
66683
+ }
66684
+ if (!token) {
66685
+ stderr("No channel token \u2014 pass --token or set NEXRALL_CHANNEL_TOKEN (env keeps it out of `ps`).");
66686
+ return CHANNEL_WAIT_EXIT.FAILED;
66687
+ }
66688
+ if (opts.wait && !/\/push$/.test(base)) {
66689
+ stderr("--wait needs the \u2026/push URL itself, so the answer can be fetched back from <push URL>/<nonce>.");
66690
+ return CHANNEL_WAIT_EXIT.FAILED;
66691
+ }
66692
+ let res;
66693
+ try {
66694
+ res = await fetchFn(base, {
66695
+ method: "POST",
66696
+ headers: { "content-type": "application/json", "x-channel-token": token },
66697
+ body: JSON.stringify({ text: message, ...opts.source ? { source: opts.source } : {} })
66698
+ });
66699
+ } catch (e2) {
66700
+ stderr(`Push failed: ${e2.message}`);
66701
+ return CHANNEL_WAIT_EXIT.FAILED;
66702
+ }
66703
+ if (res.status !== 202) {
66704
+ let body = {};
66705
+ try {
66706
+ body = await res.json();
66707
+ } catch {
66708
+ }
66709
+ stderr(`\u2716 ${mapRelayError(res.status, body.error)}`);
66710
+ return CHANNEL_WAIT_EXIT.FAILED;
66711
+ }
66712
+ const pushed = await res.json();
66713
+ const nonce = typeof pushed.nonce === "string" ? pushed.nonce : "";
66714
+ if (!nonce) {
66715
+ stderr("The relay answered 202 without a nonce \u2014 there is nothing to watch.");
66716
+ return CHANNEL_WAIT_EXIT.FAILED;
66717
+ }
66718
+ if (!opts.wait) {
66719
+ if (opts.json)
66720
+ stdout(JSON.stringify({ ok: true, nonce, ts: pushed.ts ?? null, status: "pending" }));
66721
+ else
66722
+ stdout(nonce);
66723
+ stderr(`Pushed. The session decides what to do with it (a person, maybe; read the outcome at ${base}/${nonce}).`);
66724
+ return CHANNEL_WAIT_EXIT.OK;
66725
+ }
66726
+ stderr(`Waiting for the session's answer (nonce ${nonce}, up to ${timeoutSec}s)\u2026`);
66727
+ const deadline = Date.now() + timeoutSec * 1e3;
66728
+ let lastStatus = "pending";
66729
+ let lastError = "";
66730
+ for (; ; ) {
66731
+ if (Date.now() >= deadline) {
66732
+ if (opts.json)
66733
+ stdout(JSON.stringify({ ok: false, error: "timeout", nonce, status: lastStatus }));
66734
+ stderr(
66735
+ `\u2716 No answer after ${timeoutSec}s (still ${lastStatus}${lastError ? `; last error: ${lastError}` : ""}). The push was delivered \u2014 the session may still be working on it.`
66736
+ );
66737
+ return CHANNEL_WAIT_EXIT.TIMEOUT;
66738
+ }
66739
+ await sleep(Math.min(pollMs, Math.max(10, deadline - Date.now())));
66740
+ let status;
66741
+ let body = {};
66742
+ try {
66743
+ const r2 = await fetchFn(`${base}/${nonce}`, { headers: { "x-channel-token": token } });
66744
+ status = r2.status;
66745
+ try {
66746
+ body = await r2.json();
66747
+ } catch {
66748
+ }
66749
+ } catch (e2) {
66750
+ lastError = e2.message;
66751
+ continue;
66752
+ }
66753
+ if (status === 404) {
66754
+ if (opts.json)
66755
+ stdout(JSON.stringify({ ok: false, error: body.error ?? "no_channel", nonce }));
66756
+ stderr("\u2716 The channel is gone (closed, terminal exited, or the token changed). The answer died with it.");
66757
+ return CHANNEL_WAIT_EXIT.FAILED;
66758
+ }
66759
+ if (status === 400 || status === 401) {
66760
+ if (opts.json)
66761
+ stdout(JSON.stringify({ ok: false, error: body.error ?? "refused", nonce }));
66762
+ stderr(`\u2716 ${mapRelayError(status, body.error)}`);
66763
+ return CHANNEL_WAIT_EXIT.FAILED;
66764
+ }
66765
+ if (status === 429 || status >= 500) {
66766
+ lastError = `HTTP ${status}`;
66767
+ continue;
66768
+ }
66769
+ if (status !== 200) {
66770
+ lastError = `HTTP ${status}`;
66771
+ continue;
66772
+ }
66773
+ lastStatus = typeof body.status === "string" ? body.status : "pending";
66774
+ if (lastStatus === "discarded") {
66775
+ if (opts.json)
66776
+ stdout(JSON.stringify(body));
66777
+ else
66778
+ stderr("\u{1F4E8} The session discarded the message \u2014 a person there refused it.");
66779
+ return CHANNEL_WAIT_EXIT.DISCARDED;
66780
+ }
66781
+ const answered = lastStatus === "accepted" && (typeof body.reply === "string" || body.noText === true);
66782
+ if (answered) {
66783
+ if (opts.json)
66784
+ stdout(JSON.stringify(body));
66785
+ else if (typeof body.reply === "string")
66786
+ stdout(body.reply);
66787
+ else {
66788
+ stderr(
66789
+ `(The session accepted the message, but the turn produced no text${body.interrupted ? " \u2014 it was interrupted" : ""}.)`
66790
+ );
66791
+ }
66792
+ return CHANNEL_WAIT_EXIT.OK;
66793
+ }
66794
+ }
66795
+ }
66796
+
66646
66797
  // src/commands/chat.ts
66647
66798
  import * as readline3 from "readline";
66648
66799
  import * as path31 from "path";
@@ -75417,7 +75568,9 @@ import { createHash as createHash2, randomBytes as randomBytes2 } from "node:cry
75417
75568
  var CHANNEL_TOKEN_PREFIX = "nxch_";
75418
75569
  var CHANNEL_QUEUE_CAP = 10;
75419
75570
  var CHANNEL_TEXT_MAX_CHARS = 8192;
75571
+ var CHANNEL_REPLY_MAX_CHARS = 8192;
75420
75572
  var SEEN_NONCE_CAP = 512;
75573
+ var AWAITING_REPLY_CAP = 64;
75421
75574
  var NONCE_RE = /^[a-f0-9]{16}$/;
75422
75575
  var SOURCE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
75423
75576
  function defaultMintToken() {
@@ -75451,6 +75604,8 @@ var SessionChannel = class {
75451
75604
  policy = "ask";
75452
75605
  queue = [];
75453
75606
  seenNonces = /* @__PURE__ */ new Set();
75607
+ /** S13c: nonces delivered but not yet answered by a finished turn. */
75608
+ awaitingReply = /* @__PURE__ */ new Set();
75454
75609
  /** Minted once per open; rotated by a re-open after /channel close. */
75455
75610
  token = null;
75456
75611
  tokenHash = null;
@@ -75584,6 +75739,7 @@ var SessionChannel = class {
75584
75739
  return;
75585
75740
  this.stopped = true;
75586
75741
  this.queue = [];
75742
+ this.awaitingReply.clear();
75587
75743
  if (this.socket?.connected)
75588
75744
  this.emitTo("cli:channel-close", { id: this.id, reason });
75589
75745
  this.token = null;
@@ -75609,6 +75765,39 @@ var SessionChannel = class {
75609
75765
  }
75610
75766
  this.notice({ kind: "delivered", msg, auto, folded: verdict === "queued" });
75611
75767
  this.ack(msg, verdict);
75768
+ if (this.awaitingReply.size >= AWAITING_REPLY_CAP) {
75769
+ this.awaitingReply.delete(this.awaitingReply.values().next().value);
75770
+ }
75771
+ this.awaitingReply.add(msg.nonce);
75772
+ }
75773
+ /**
75774
+ * S13c — close the loop: hand every delivered-but-unanswered push the
75775
+ * closing text of the turn that consumed it (chat.ts calls this when a turn
75776
+ * ends; `null`/blank means the turn said nothing useful, which is reported
75777
+ * as an EXPLICIT no-text marker rather than silence). Frames are keyed by
75778
+ * nonce and stored by the relay, readable only with the channel token.
75779
+ * Returns how many nonces were answered.
75780
+ */
75781
+ publishReplies(reply, opts = {}) {
75782
+ if (this.stopped || this.awaitingReply.size === 0)
75783
+ return 0;
75784
+ const text = (reply ?? "").trim();
75785
+ let body;
75786
+ if (!text)
75787
+ body = { noText: true };
75788
+ else if (text.length > CHANNEL_REPLY_MAX_CHARS) {
75789
+ body = { reply: text.slice(0, CHANNEL_REPLY_MAX_CHARS), truncated: true };
75790
+ } else
75791
+ body = { reply: text };
75792
+ if (opts.interrupted)
75793
+ body.interrupted = true;
75794
+ let answered = 0;
75795
+ for (const nonce of this.awaitingReply) {
75796
+ this.emitTo("cli:channel-result", { id: this.id, nonce, ...body });
75797
+ answered += 1;
75798
+ }
75799
+ this.awaitingReply.clear();
75800
+ return answered;
75612
75801
  }
75613
75802
  /** Shape-check a pushed frame — the relay is a pipe, delivery trusts nothing. */
75614
75803
  validateMessage(payload) {
@@ -97681,6 +97870,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
97681
97870
  const subPrefix = (meta) => paint.dim(` \u21B3 ${meta?.agentName ?? "sub-agent"} `);
97682
97871
  let thinkingTokens = 0;
97683
97872
  let axTextBlockOpen = false;
97873
+ let turnReply = "";
97684
97874
  let liveThinkText = "";
97685
97875
  const pushStatus = () => {
97686
97876
  spinner.setThinking(thinkingTokens, liveThinkText);
@@ -97782,6 +97972,9 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
97782
97972
  axTextBlockOpen = true;
97783
97973
  }
97784
97974
  mdRender.feed(text);
97975
+ turnReply += text;
97976
+ if (turnReply.length > 32768)
97977
+ turnReply = turnReply.slice(-16384);
97785
97978
  },
97786
97979
  // System notices (mid-run auto-prune/auto-compact housekeeping) are NOT part
97787
97980
  // of the model's own reply — used to go through onText, which spliced
@@ -97801,6 +97994,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
97801
97994
  if (abortSignal.aborted)
97802
97995
  return;
97803
97996
  mdRender.flush();
97997
+ turnReply = "";
97804
97998
  spinner.stop();
97805
97999
  if (meta?.id)
97806
98000
  toolCalls.set(meta.id, {
@@ -98063,6 +98257,10 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
98063
98257
  });
98064
98258
  if (sessionTurns.length > MAX_SESSION_TURNS)
98065
98259
  sessionTurns.splice(0, sessionTurns.length - MAX_SESSION_TURNS);
98260
+ const channel2 = activeSessionChannel();
98261
+ const replied = channel2?.publishReplies(turnReply, { interrupted: abortSignal.aborted }) ?? 0;
98262
+ if (replied > 0)
98263
+ console.log(paint.dim(` \u21A9 channel result sent (${replied})`));
98066
98264
  return { messages: result, usage: lastUsage };
98067
98265
  }
98068
98266
  function toolIds(isSubTask, meta) {
@@ -101466,6 +101664,11 @@ ${dirList}`;
101466
101664
  ` Channel ${st.state === "live" ? paint.green("live") : paint.yellow(st.state)} \xB7 policy ${ch.activePolicy} \xB7 ${pending2.length} waiting`
101467
101665
  );
101468
101666
  console.log(paint.dim(` Push: ${ch.pushUrl}`));
101667
+ console.log(
101668
+ paint.dim(
101669
+ " Replies: the pusher reads the turn's answer back \u2014 GET <push URL>/<nonce>, or `nex channel push --wait`."
101670
+ )
101671
+ );
101469
101672
  pending2.forEach(
101470
101673
  (m2, i2) => console.log(paint.dim(` #${i2 + 1} (${m2.source}) ${channelPreview(m2.text)}`))
101471
101674
  );
@@ -102911,6 +103114,11 @@ ${text}
102911
103114
  worktree: worktreeState,
102912
103115
  disableHooks: noDiscovery,
102913
103116
  _extraAgentTypes: _cliAgentTypes,
103117
+ // Threaded exactly as runTurn does: a typed fork must not silently
103118
+ // lose the MCP servers the session has connected, and a billed
103119
+ // model run belongs in the audit trail like any other.
103120
+ mcpManager: _mcpManager ?? void 0,
103121
+ audit: currentAudit(),
102914
103122
  backgroundAgents: willBackground ? cliBackgroundAgents : void 0,
102915
103123
  // Background: it runs while the user keeps typing, so it can never print
102916
103124
  // into the transcript. Foreground: the user is waiting and the prompt is
@@ -104094,6 +104302,10 @@ schedule.command("runs <id>").description("Recent runs of a schedule, newest fir
104094
104302
  schedule.action(async () => {
104095
104303
  await scheduleListCommand();
104096
104304
  });
104305
+ var channel = program2.command("channel").description("Push messages into a running session from CI or a script (channels)");
104306
+ channel.command("push <text...>").description("Push a message into a live session; --wait prints the answer back").requiredOption("--url <url>", "The push URL printed by `/channel open` inside the session").option("--token <token>", "Channel token (prefer NEXRALL_CHANNEL_TOKEN \u2014 argv is visible in `ps`)").option("--source <slug>", "Where it came from, e.g. ci (default: channel)").option("-w, --wait", "Wait for the session's answer and print it (exit 0 answered, 3 discarded, 4 timeout)").option("--timeout <seconds>", "How long --wait waits (default 180)").option("--json", "Machine-readable output").action(async (text, opts) => {
104307
+ process.exitCode = await channelPushCommand(text, opts);
104308
+ });
104097
104309
  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) => {
104098
104310
  await updateCommand({ check: opts.check, yes: opts.yes });
104099
104311
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nexrall-code",
3
- "version": "0.5.185",
4
- "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
3
+ "version": "0.5.187",
4
+ "description": "Nexrall Code \u2014 AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",
7
7
  "cli",