nexrall-code 0.5.46 → 0.5.48

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 +464 -96
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3156,6 +3156,8 @@ var require_auth = __commonJS({
3156
3156
  exports.clearAuth = clearAuth2;
3157
3157
  exports.isAuthenticated = isAuthenticated3;
3158
3158
  exports.getToken = getToken;
3159
+ exports.getRefreshToken = getRefreshToken;
3160
+ exports.updateTokens = updateTokens;
3159
3161
  var fs9 = __importStar(__require("fs"));
3160
3162
  var path6 = __importStar(__require("path"));
3161
3163
  var os6 = __importStar(__require("os"));
@@ -3205,6 +3207,34 @@ var require_auth = __commonJS({
3205
3207
  const auth = loadAuth();
3206
3208
  return auth?.token ?? null;
3207
3209
  }
3210
+ function getRefreshToken() {
3211
+ const envToken = process.env.NEXRALL_TOKEN;
3212
+ if (envToken && envToken.trim().length > 0)
3213
+ return null;
3214
+ const auth = loadAuth();
3215
+ const rt = auth?.refreshToken;
3216
+ return typeof rt === "string" && rt.length > 0 ? rt : null;
3217
+ }
3218
+ function updateTokens(token, refreshToken) {
3219
+ let existing = {};
3220
+ try {
3221
+ if (fs9.existsSync(CONFIG_FILE)) {
3222
+ const parsed = JSON.parse(fs9.readFileSync(CONFIG_FILE, "utf-8"));
3223
+ if (typeof parsed === "object" && parsed !== null)
3224
+ existing = parsed;
3225
+ }
3226
+ } catch {
3227
+ }
3228
+ ensureConfigDir();
3229
+ const next = {
3230
+ ...existing,
3231
+ token,
3232
+ // Keep the previous refresh token if the server didn't send a new one, rather
3233
+ // than deleting the only means of refreshing again.
3234
+ ...refreshToken ? { refreshToken } : {}
3235
+ };
3236
+ fs9.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 384 });
3237
+ }
3208
3238
  }
3209
3239
  });
3210
3240
 
@@ -9842,6 +9872,7 @@ var require_client = __commonJS({
9842
9872
  exports.chooseFinalContent = chooseFinalContent;
9843
9873
  exports.streamChat = streamChat;
9844
9874
  exports.cancelTurn = cancelTurn;
9875
+ exports.revokeRefreshToken = revokeRefreshToken2;
9845
9876
  exports.getBalance = getBalance3;
9846
9877
  exports.exchangeVscodeCode = exchangeVscodeCode;
9847
9878
  exports.login = login2;
@@ -9892,6 +9923,49 @@ var require_client = __commonJS({
9892
9923
  Authorization: `Bearer ${token}`
9893
9924
  };
9894
9925
  }
9926
+ var _refreshInFlight = null;
9927
+ async function refreshAccessToken() {
9928
+ if (_refreshInFlight)
9929
+ return _refreshInFlight;
9930
+ _refreshInFlight = (async () => {
9931
+ const refreshToken = (0, index_1.getRefreshToken)();
9932
+ if (!refreshToken)
9933
+ return false;
9934
+ try {
9935
+ const res = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/refresh`, {
9936
+ method: "POST",
9937
+ headers: { "Content-Type": "application/json" },
9938
+ body: JSON.stringify({ refreshToken })
9939
+ });
9940
+ if (!res.ok)
9941
+ return false;
9942
+ const data = await res.json();
9943
+ if (!data.token)
9944
+ return false;
9945
+ (0, index_1.updateTokens)(data.token, data.refreshToken);
9946
+ return true;
9947
+ } catch {
9948
+ return false;
9949
+ }
9950
+ })();
9951
+ try {
9952
+ return await _refreshInFlight;
9953
+ } finally {
9954
+ _refreshInFlight = null;
9955
+ }
9956
+ }
9957
+ function isExpiredTokenResponse(status, body) {
9958
+ if (status !== 401 && status !== 403)
9959
+ return false;
9960
+ try {
9961
+ const parsed = JSON.parse(body);
9962
+ if (parsed?.error && typeof parsed.error === "object")
9963
+ return false;
9964
+ return typeof parsed?.error === "string" && /token|auth|expired/i.test(parsed.error);
9965
+ } catch {
9966
+ return false;
9967
+ }
9968
+ }
9895
9969
  var MAX_RETRIES = 5;
9896
9970
  var RETRY_BASE_MS = 1e3;
9897
9971
  var RETRY_MAX_MS = 3e4;
@@ -9974,14 +10048,43 @@ var require_client = __commonJS({
9974
10048
  onEvent({ type: "retry_resolved" });
9975
10049
  }
9976
10050
  };
10051
+ const prepareRestart = async (reason) => {
10052
+ await cancelTurn(turnId);
10053
+ turnId = (0, crypto_1.randomUUID)();
10054
+ resuming = false;
10055
+ serverResumable = false;
10056
+ lastEventId = 0;
10057
+ carryText = [];
10058
+ carryToolUse = [];
10059
+ const hadRendered = emittedAnythingAcrossAttempts;
10060
+ const renderedChars = emittedCharsAcrossAttempts;
10061
+ emittedAnythingAcrossAttempts = false;
10062
+ emittedCharsAcrossAttempts = 0;
10063
+ if (hadRendered && !allowRestartAfterRender)
10064
+ return false;
10065
+ if (hadRendered) {
10066
+ onEvent({ type: "stream_restart", reason, discardedChars: renderedChars });
10067
+ }
10068
+ return true;
10069
+ };
10070
+ const bodySaysNotResumable = (body) => {
10071
+ try {
10072
+ return JSON.parse(body)?.notResumable === true;
10073
+ } catch {
10074
+ return false;
10075
+ }
10076
+ };
9977
10077
  async function runAttempt() {
9978
10078
  let response;
9979
10079
  let lastErr;
9980
10080
  let inFlightWaits = 0;
9981
10081
  let errorBodyOverride = null;
9982
10082
  let forbiddenExhausted = false;
10083
+ let refreshedThisAttempt = false;
10084
+ let sessionExpired = false;
9983
10085
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
9984
10086
  try {
10087
+ errorBodyOverride = null;
9985
10088
  response = await (0, node_fetch_1.default)(...buildFetchArgs());
9986
10089
  if (response.status === 429 && attempt < MAX_RETRIES && canRetry()) {
9987
10090
  reportRetry("Rate limited by the API \u2014 retrying");
@@ -9994,9 +10097,30 @@ var require_client = __commonJS({
9994
10097
  await sleepWithinBudget(backoffMs(attempt));
9995
10098
  continue;
9996
10099
  }
10100
+ if (response.status === 401 || response.status === 403) {
10101
+ const bodyText = await response.text().catch(() => "");
10102
+ if (isExpiredTokenResponse(response.status, bodyText)) {
10103
+ if (!refreshedThisAttempt) {
10104
+ refreshedThisAttempt = true;
10105
+ if (await refreshAccessToken()) {
10106
+ reportRetry("Session expired \u2014 signing you back in");
10107
+ attempt--;
10108
+ continue;
10109
+ }
10110
+ }
10111
+ sessionExpired = true;
10112
+ errorBodyOverride = JSON.stringify({
10113
+ // Replaces the backend's bare "Invalid or expired token", which reads
10114
+ // like a bug rather than something the user can act on.
10115
+ error: "Your session has expired. Run `nex login` to sign in again."
10116
+ });
10117
+ break;
10118
+ }
10119
+ errorBodyOverride = bodyText;
10120
+ }
9997
10121
  const FORBIDDEN_RETRY_LIMIT = 2;
9998
10122
  if (response.status === 403) {
9999
- const bodyText = await response.text().catch(() => "");
10123
+ const bodyText = errorBodyOverride ?? await response.text().catch(() => "");
10000
10124
  let isUpstreamForbidden = false;
10001
10125
  try {
10002
10126
  const parsed = JSON.parse(bodyText);
@@ -10016,35 +10140,22 @@ var require_client = __commonJS({
10016
10140
  errorBodyOverride = bodyText;
10017
10141
  break;
10018
10142
  }
10019
- if (response.status === 410 && resuming) {
10020
- await response.text().catch(() => "");
10021
- await cancelTurn(turnId);
10022
- turnId = (0, crypto_1.randomUUID)();
10023
- resuming = false;
10024
- serverResumable = false;
10025
- lastEventId = 0;
10026
- carryText = [];
10027
- carryToolUse = [];
10028
- const hadRendered = emittedAnythingAcrossAttempts;
10029
- const renderedChars = emittedCharsAcrossAttempts;
10030
- emittedAnythingAcrossAttempts = false;
10031
- emittedCharsAcrossAttempts = 0;
10032
- if (hadRendered && !allowRestartAfterRender) {
10033
- throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
10034
- }
10035
- if (hadRendered) {
10036
- onEvent({
10037
- type: "stream_restart",
10038
- reason: "Connection lost too long to resume",
10039
- discardedChars: renderedChars
10040
- });
10041
- }
10042
- if (attempt < MAX_RETRIES && canRetry()) {
10043
- reportRetry("Could not resume \u2014 restarting this turn");
10044
- await sleepWithinBudget(backoffMs(attempt));
10045
- continue;
10143
+ if (resuming && response.status >= 400 && response.status < 500) {
10144
+ const body = errorBodyOverride ?? await response.text().catch(() => "");
10145
+ if (response.status === 410 || response.status === 404 || bodySaysNotResumable(body)) {
10146
+ const canRestart = await prepareRestart("Connection lost too long to resume");
10147
+ if (!canRestart) {
10148
+ throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
10149
+ }
10150
+ if (attempt < MAX_RETRIES && canRetry()) {
10151
+ reportRetry("Could not resume \u2014 restarting this turn");
10152
+ await sleepWithinBudget(backoffMs(attempt));
10153
+ continue;
10154
+ }
10155
+ errorBodyOverride = JSON.stringify({ error: "Could not resume this turn \u2014 send your message again." });
10156
+ break;
10046
10157
  }
10047
- errorBodyOverride = JSON.stringify({ error: "Could not resume this turn \u2014 send your message again." });
10158
+ errorBodyOverride = body;
10048
10159
  break;
10049
10160
  }
10050
10161
  if (response.status === 409) {
@@ -10096,7 +10207,12 @@ var require_client = __commonJS({
10096
10207
  } catch {
10097
10208
  errMsg = errText || errMsg;
10098
10209
  }
10099
- throw Object.assign(new Error(errMsg), { status: response.status, balance, ...forbiddenExhausted ? { retryable: true } : {} });
10210
+ throw Object.assign(new Error(errMsg), {
10211
+ status: response.status,
10212
+ balance,
10213
+ ...forbiddenExhausted ? { retryable: true } : {},
10214
+ ...sessionExpired ? { authExpired: true } : {}
10215
+ });
10100
10216
  }
10101
10217
  if (!response.body) {
10102
10218
  throw new Error("Response body is null");
@@ -10321,6 +10437,8 @@ var require_client = __commonJS({
10321
10437
  stream.destroy?.();
10322
10438
  reject(tagTransient(new Error(message)));
10323
10439
  } else {
10440
+ clearInterval(heartbeatWatchdog);
10441
+ stream.destroy?.();
10324
10442
  onEvent({ type: "error", message });
10325
10443
  reject(new Error(message));
10326
10444
  }
@@ -10381,27 +10499,10 @@ var require_client = __commonJS({
10381
10499
  const e2 = err;
10382
10500
  if (e2.retryable && sAttempt < MAX_RETRIES && canRetry()) {
10383
10501
  if (e2.forceRestart) {
10384
- await cancelTurn(turnId);
10385
- turnId = (0, crypto_1.randomUUID)();
10386
- resuming = false;
10387
- serverResumable = false;
10388
- lastEventId = 0;
10389
- carryText = [];
10390
- carryToolUse = [];
10391
- const hadRendered = emittedAnythingAcrossAttempts;
10392
- const renderedChars = emittedCharsAcrossAttempts;
10393
- emittedAnythingAcrossAttempts = false;
10394
- emittedCharsAcrossAttempts = 0;
10395
- if (hadRendered && !allowRestartAfterRender) {
10502
+ const canRestart = await prepareRestart(err.message || "Could not resume \u2014 restarting this turn");
10503
+ if (!canRestart) {
10396
10504
  throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
10397
10505
  }
10398
- if (hadRendered) {
10399
- onEvent({
10400
- type: "stream_restart",
10401
- reason: err.message || "Could not resume \u2014 restarting this turn",
10402
- discardedChars: renderedChars
10403
- });
10404
- }
10405
10506
  reportRetry("Could not resume \u2014 restarting this turn");
10406
10507
  await sleepWithinBudget(backoffMs(sAttempt));
10407
10508
  continue;
@@ -10445,11 +10546,30 @@ var require_client = __commonJS({
10445
10546
  } catch {
10446
10547
  }
10447
10548
  }
10549
+ async function revokeRefreshToken2() {
10550
+ const refreshToken = (0, index_1.getRefreshToken)();
10551
+ if (!refreshToken)
10552
+ return;
10553
+ try {
10554
+ await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/logout`, {
10555
+ method: "POST",
10556
+ headers: { "Content-Type": "application/json" },
10557
+ body: JSON.stringify({ refreshToken })
10558
+ });
10559
+ } catch {
10560
+ }
10561
+ }
10448
10562
  async function getBalance3() {
10449
- const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, {
10450
- method: "GET",
10451
- headers: authHeaders()
10452
- });
10563
+ const fetchOnce = () => (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, { method: "GET", headers: authHeaders() });
10564
+ let response = await fetchOnce();
10565
+ if (response.status === 401 || response.status === 403) {
10566
+ const body = await response.text().catch(() => "");
10567
+ if (isExpiredTokenResponse(response.status, body) && await refreshAccessToken()) {
10568
+ response = await fetchOnce();
10569
+ } else {
10570
+ throw new Error(`API error ${response.status}: ${body}`);
10571
+ }
10572
+ }
10453
10573
  if (!response.ok) {
10454
10574
  const errText = await response.text();
10455
10575
  throw new Error(`API error ${response.status}: ${errText}`);
@@ -10470,7 +10590,11 @@ var require_client = __commonJS({
10470
10590
  const data = await response.json();
10471
10591
  if (!data.token)
10472
10592
  throw new Error("No token in exchange response");
10473
- return { token: data.token, email: data.user?.email ?? "" };
10593
+ return {
10594
+ token: data.token,
10595
+ ...data.refreshToken ? { refreshToken: data.refreshToken } : {},
10596
+ email: data.user?.email ?? ""
10597
+ };
10474
10598
  }
10475
10599
  async function login2(email, password) {
10476
10600
  const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/login/email`, {
@@ -10486,7 +10610,11 @@ var require_client = __commonJS({
10486
10610
  if (!data.token) {
10487
10611
  throw new Error("Login response missing token");
10488
10612
  }
10489
- return { token: data.token, email: data.email ?? email };
10613
+ return {
10614
+ token: data.token,
10615
+ ...data.refreshToken ? { refreshToken: data.refreshToken } : {},
10616
+ email: data.email ?? email
10617
+ };
10490
10618
  }
10491
10619
  }
10492
10620
  });
@@ -12606,13 +12734,7 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
12606
12734
  } catch {
12607
12735
  }
12608
12736
  }
12609
- fs9.writeFileSync(resolved, content, "utf-8");
12610
- if (existingMode !== void 0) {
12611
- try {
12612
- fs9.chmodSync(resolved, existingMode);
12613
- } catch {
12614
- }
12615
- }
12737
+ atomicWrite(resolved, content, existingMode);
12616
12738
  const bytes = Buffer.byteLength(content, "utf-8");
12617
12739
  const lines = content.split("\n").length;
12618
12740
  if (isNew) {
@@ -13326,14 +13448,52 @@ ${globalCapMatches(output)}` : "";
13326
13448
  }
13327
13449
  return { mode: stat2.mode, content: fs9.readFileSync(resolved, "utf-8") };
13328
13450
  }
13329
- function atomicWritePreservingMode(resolved, data, mode) {
13330
- const tmp = resolved + ".nexrall_tmp";
13331
- fs9.writeFileSync(tmp, data, "utf-8");
13451
+ function atomicWrite(resolved, data, mode) {
13452
+ let target = resolved;
13332
13453
  try {
13333
- fs9.chmodSync(tmp, mode);
13454
+ if (fs9.lstatSync(resolved).isSymbolicLink())
13455
+ target = fs9.realpathSync(resolved);
13334
13456
  } catch {
13335
13457
  }
13336
- fs9.renameSync(tmp, resolved);
13458
+ const tmp = `${target}.nexrall_tmp_${process.pid}`;
13459
+ try {
13460
+ fs9.writeFileSync(tmp, data, "utf-8");
13461
+ } catch (err) {
13462
+ const code = err.code;
13463
+ if (code === "EACCES" || code === "EPERM" || code === "EROFS") {
13464
+ fs9.writeFileSync(target, data, "utf-8");
13465
+ if (mode !== void 0) {
13466
+ try {
13467
+ fs9.chmodSync(target, mode);
13468
+ } catch {
13469
+ }
13470
+ }
13471
+ return;
13472
+ }
13473
+ try {
13474
+ fs9.rmSync(tmp, { force: true });
13475
+ } catch {
13476
+ }
13477
+ throw err;
13478
+ }
13479
+ try {
13480
+ if (mode !== void 0) {
13481
+ try {
13482
+ fs9.chmodSync(tmp, mode);
13483
+ } catch {
13484
+ }
13485
+ }
13486
+ fs9.renameSync(tmp, target);
13487
+ } catch (err) {
13488
+ try {
13489
+ fs9.rmSync(tmp, { force: true });
13490
+ } catch {
13491
+ }
13492
+ throw err;
13493
+ }
13494
+ }
13495
+ function atomicWritePreservingMode(resolved, data, mode) {
13496
+ atomicWrite(resolved, data, mode);
13337
13497
  }
13338
13498
  function normalizeLF(s2) {
13339
13499
  return s2.replace(/\r\n/g, "\n");
@@ -14738,6 +14898,10 @@ var require_loop = __commonJS({
14738
14898
  Object.defineProperty(exports, "__esModule", { value: true });
14739
14899
  exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
14740
14900
  exports.resolveMaxIterations = resolveMaxIterations;
14901
+ exports.createLimiter = createLimiter;
14902
+ exports.extractSubTaskText = extractSubTaskText;
14903
+ exports.capSubTaskText = capSubTaskText;
14904
+ exports.summariseSubTaskProgress = summariseSubTaskProgress;
14741
14905
  exports.contextWindowFor = contextWindowFor2;
14742
14906
  exports.compactionThresholds = compactionThresholds2;
14743
14907
  exports.estimateBodyBytes = estimateBodyBytes2;
@@ -14900,6 +15064,29 @@ var require_loop = __commonJS({
14900
15064
  _fileLocks.delete(absPath);
14901
15065
  }
14902
15066
  }
15067
+ var MAX_CONCURRENT_SUBTASKS = (() => {
15068
+ const raw = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
15069
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
15070
+ })();
15071
+ function createLimiter(max) {
15072
+ let active = 0;
15073
+ const queue = [];
15074
+ const release2 = () => {
15075
+ active--;
15076
+ queue.shift()?.();
15077
+ };
15078
+ return async (fn) => {
15079
+ if (active >= max)
15080
+ await new Promise((resolve3) => queue.push(resolve3));
15081
+ active++;
15082
+ try {
15083
+ return await fn();
15084
+ } finally {
15085
+ release2();
15086
+ }
15087
+ };
15088
+ }
15089
+ var _subTaskLimit = createLimiter(MAX_CONCURRENT_SUBTASKS);
14903
15090
  function humanDescription(name, input) {
14904
15091
  switch (name) {
14905
15092
  case "read_file":
@@ -14981,16 +15168,71 @@ var require_loop = __commonJS({
14981
15168
  return `Use tool: ${name}`;
14982
15169
  }
14983
15170
  }
14984
- var MAX_TASK_DEPTH = 2;
15171
+ var MAX_TASK_DEPTH = 1;
14985
15172
  var _subTaskCounter = 0;
14986
15173
  var SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0 ? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) : 10 * 60 * 1e3;
15174
+ var SUBTASK_MAX = 48e3;
15175
+ function sliceSafeEnd(s2, max) {
15176
+ if (s2.length <= max)
15177
+ return s2;
15178
+ let end = max;
15179
+ const code = s2.charCodeAt(end - 1);
15180
+ if (code >= 55296 && code <= 56319)
15181
+ end--;
15182
+ return s2.slice(0, end);
15183
+ }
15184
+ function sliceSafeStart(s2, from) {
15185
+ if (from <= 0)
15186
+ return s2;
15187
+ let start = from;
15188
+ const code = s2.charCodeAt(start);
15189
+ if (code >= 56320 && code <= 57343)
15190
+ start++;
15191
+ return s2.slice(start);
15192
+ }
15193
+ function extractSubTaskText(messages, preferLast = true) {
15194
+ const assistants = messages.filter((m2) => m2.role === "assistant");
15195
+ const textOf = (m2) => (m2?.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim();
15196
+ if (preferLast)
15197
+ return textOf(assistants[assistants.length - 1]);
15198
+ return assistants.map(textOf).filter(Boolean).join("\n\n").trim();
15199
+ }
15200
+ function capSubTaskText(text, max = SUBTASK_MAX) {
15201
+ if (text.length <= max)
15202
+ return text;
15203
+ const head = sliceSafeEnd(text, Math.floor(max * 0.6));
15204
+ const tail = sliceSafeStart(text, text.length - Math.floor(max * 0.4));
15205
+ return `${head}
15206
+
15207
+ [\u2026 sub-task output truncated (${text.length} chars) \u2014 kept the beginning and end \u2026]
15208
+
15209
+ ${tail}`;
15210
+ }
15211
+ function summariseSubTaskProgress(messages) {
15212
+ const toolNames = [];
15213
+ for (const m2 of messages) {
15214
+ if (m2.role !== "assistant" || !Array.isArray(m2.content))
15215
+ continue;
15216
+ for (const b of m2.content) {
15217
+ if (b?.type === "tool_use" && typeof b.name === "string")
15218
+ toolNames.push(b.name);
15219
+ }
15220
+ }
15221
+ if (toolNames.length === 0)
15222
+ return "";
15223
+ const counts = /* @__PURE__ */ new Map();
15224
+ for (const n of toolNames)
15225
+ counts.set(n, (counts.get(n) ?? 0) + 1);
15226
+ const inventory = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
15227
+ return `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
15228
+ }
14987
15229
  async function runSubTask(input, options, agentTypes) {
14988
15230
  const prompt2 = typeof input.prompt === "string" ? input.prompt.trim() : "";
14989
15231
  if (!prompt2)
14990
15232
  return { error: "task tool requires a non-empty prompt" };
14991
15233
  const depth = options._depth ?? 0;
14992
15234
  if (depth >= MAX_TASK_DEPTH) {
14993
- return { error: `Sub-task depth limit (${MAX_TASK_DEPTH}) reached \u2014 sub-agents cannot spawn further sub-agents.` };
15235
+ return { error: "Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it." };
14994
15236
  }
14995
15237
  const requestedType = typeof input.subagent_type === "string" ? input.subagent_type : "";
14996
15238
  const agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
@@ -15064,22 +15306,36 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
15064
15306
  onThinkingProgress: (tok) => options.onThinkingProgress?.(tok)
15065
15307
  });
15066
15308
  if (subAbort.aborted && !options.abortSignal?.aborted) {
15067
- 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).` };
15068
- }
15069
- const lastAssistant = [...result].reverse().find((m2) => m2.role === "assistant");
15070
- let text = (lastAssistant?.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("").trim();
15071
- const SUBTASK_MAX = 48e3;
15072
- if (text.length > SUBTASK_MAX) {
15073
- const head = text.slice(0, Math.floor(SUBTASK_MAX * 0.6));
15074
- const tail = text.slice(text.length - Math.floor(SUBTASK_MAX * 0.4));
15075
- text = `${head}
15076
-
15077
- [\u2026 sub-task output truncated (${text.length} chars) \u2014 kept the beginning and end \u2026]
15078
-
15079
- ${tail}`;
15080
- }
15309
+ const mins = Math.round(SUBTASK_TIMEOUT_MS / 6e4);
15310
+ const partial = capSubTaskText(extractSubTaskText(result, false));
15311
+ const progress = summariseSubTaskProgress(result);
15312
+ const sections = [
15313
+ `Sub-task STOPPED after ${mins} minutes without completing \u2014 treat the following as PARTIAL, unverified work, not a finished answer.`,
15314
+ progress,
15315
+ partial ? `Partial output before it was stopped:
15316
+
15317
+ ${partial}` : "",
15318
+ "Do NOT simply re-run the same sub-task: build on what is above, or split the remaining work into smaller, more focused sub-tasks."
15319
+ ].filter(Boolean);
15320
+ return { error: sections.join("\n\n") };
15321
+ }
15322
+ const text = capSubTaskText(extractSubTaskText(result, true));
15081
15323
  return { output: text || "(sub-task completed with no text output)" };
15082
15324
  } catch (err) {
15325
+ const salvaged = (0, types_1.salvageHistory)(err);
15326
+ if (salvaged) {
15327
+ const partial = capSubTaskText(extractSubTaskText(salvaged, false));
15328
+ const progress = summariseSubTaskProgress(salvaged);
15329
+ const sections = [
15330
+ `Sub-task FAILED before completing: ${err.message}`,
15331
+ progress,
15332
+ partial ? `Partial output before the failure:
15333
+
15334
+ ${partial}` : "",
15335
+ "Treat the above as PARTIAL, unverified work. Build on it rather than re-running the whole sub-task."
15336
+ ].filter(Boolean);
15337
+ return { error: sections.join("\n\n") };
15338
+ }
15083
15339
  return { error: `Sub-task failed: ${err.message}` };
15084
15340
  } finally {
15085
15341
  clearTimeout(timer);
@@ -15678,7 +15934,7 @@ Continue the work from here.` }] });
15678
15934
  if (!permitted) {
15679
15935
  result = { error: "Permission denied by user" };
15680
15936
  } else if (name === "task") {
15681
- result = await runSubTask(input, options, agentTypes);
15937
+ result = depth === 0 ? await _subTaskLimit(() => runSubTask(input, options, agentTypes)) : await runSubTask(input, options, agentTypes);
15682
15938
  } else {
15683
15939
  const pre = runToolHooks(hooks.PreToolUse, "PreToolUse", name, input, options.workDir);
15684
15940
  if (pre.block) {
@@ -51904,6 +52160,7 @@ async function logoutCommand() {
51904
52160
  console.log();
51905
52161
  return;
51906
52162
  }
52163
+ await (0, import_code_core.revokeRefreshToken)();
51907
52164
  (0, import_code_core.clearAuth)();
51908
52165
  console.log(source_default.green(" \u2713 Logged out."));
51909
52166
  console.log();
@@ -62255,6 +62512,8 @@ var use_app_default = useApp;
62255
62512
 
62256
62513
  // ../../node_modules/.pnpm/ink@7.1.1_@types+react@19.2.18_react@19.2.8/node_modules/ink/build/hooks/use-stdout.js
62257
62514
  var import_react25 = __toESM(require_react(), 1);
62515
+ var useStdout = () => (0, import_react25.useContext)(StdoutContext_default);
62516
+ var use_stdout_default = useStdout;
62258
62517
 
62259
62518
  // ../../node_modules/.pnpm/ink@7.1.1_@types+react@19.2.18_react@19.2.8/node_modules/ink/build/hooks/use-stderr.js
62260
62519
  var import_react26 = __toESM(require_react(), 1);
@@ -62276,6 +62535,21 @@ var import_react31 = __toESM(require_react(), 1);
62276
62535
 
62277
62536
  // ../../node_modules/.pnpm/ink@7.1.1_@types+react@19.2.18_react@19.2.8/node_modules/ink/build/hooks/use-window-size.js
62278
62537
  var import_react32 = __toESM(require_react(), 1);
62538
+ var useWindowSize = () => {
62539
+ const { stdout } = use_stdout_default();
62540
+ const [size, setSize] = (0, import_react32.useState)(() => getWindowSize(stdout));
62541
+ (0, import_react32.useEffect)(() => {
62542
+ const onResize = () => {
62543
+ setSize(getWindowSize(stdout));
62544
+ };
62545
+ stdout.on("resize", onResize);
62546
+ return () => {
62547
+ stdout.off("resize", onResize);
62548
+ };
62549
+ }, [stdout]);
62550
+ return size;
62551
+ };
62552
+ var use_window_size_default = useWindowSize;
62279
62553
 
62280
62554
  // ../../node_modules/.pnpm/ink@7.1.1_@types+react@19.2.18_react@19.2.8/node_modules/ink/build/hooks/use-box-metrics.js
62281
62555
  var import_react33 = __toESM(require_react(), 1);
@@ -62378,6 +62652,7 @@ var DEFAULT_PROMPT = colors.primary.bold("> ");
62378
62652
  var App2 = ({ onReady }) => {
62379
62653
  const [items, setItems] = (0, import_react35.useState)([]);
62380
62654
  const [footer, setFooterState] = (0, import_react35.useState)({ mode: "auto", autoApprove: false });
62655
+ const { columns } = use_window_size_default();
62381
62656
  const [inputEnabled, setInputEnabledState] = (0, import_react35.useState)(true);
62382
62657
  const [line, setLineState] = (0, import_react35.useState)("");
62383
62658
  const [prompt2, setPrompt] = (0, import_react35.useState)(DEFAULT_PROMPT);
@@ -62446,7 +62721,7 @@ var App2 = ({ onReady }) => {
62446
62721
  import_react35.default.useEffect(() => {
62447
62722
  onReady({ print, setFooter, setLive, askLine, onLine, setInputEnabled, close: () => exit() });
62448
62723
  }, []);
62449
- return /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column" }, /* @__PURE__ */ import_react35.default.createElement(Static, { items }, (item) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: item.id }, item.content)), live ? /* @__PURE__ */ import_react35.default.createElement(Text, null, live) : null, /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, "\u2500".repeat(Math.max(1, (process.stdout.columns || 80) - 1))), inputEnabled ? /* @__PURE__ */ import_react35.default.createElement(Box_default, null, /* @__PURE__ */ import_react35.default.createElement(Text, null, prompt2), /* @__PURE__ */ import_react35.default.createElement(Text, null, line), /* @__PURE__ */ import_react35.default.createElement(Text, { inverse: true }, " ")) : /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, " (working\u2026)"), /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, footerText(footer)));
62724
+ return /* @__PURE__ */ import_react35.default.createElement(Box_default, { flexDirection: "column" }, /* @__PURE__ */ import_react35.default.createElement(Static, { items }, (item) => /* @__PURE__ */ import_react35.default.createElement(Text, { key: item.id }, item.content)), live ? /* @__PURE__ */ import_react35.default.createElement(Text, null, live) : null, /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, "\u2500".repeat(Math.max(1, columns - 1))), inputEnabled ? /* @__PURE__ */ import_react35.default.createElement(Box_default, null, /* @__PURE__ */ import_react35.default.createElement(Text, null, prompt2), /* @__PURE__ */ import_react35.default.createElement(Text, null, line), /* @__PURE__ */ import_react35.default.createElement(Text, { inverse: true }, " ")) : /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, " (working\u2026)"), /* @__PURE__ */ import_react35.default.createElement(Text, { dimColor: true }, footerText(footer)));
62450
62725
  };
62451
62726
  function startInkTerminal() {
62452
62727
  if (handle)
@@ -62519,7 +62794,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
62519
62794
  };
62520
62795
 
62521
62796
  // src/commands/chat.ts
62522
- var CLI_VERSION = "0.5.46";
62797
+ var CLI_VERSION = "0.5.48";
62523
62798
  var MODEL_LABELS = {
62524
62799
  turbo: "Nexrall Turbo",
62525
62800
  pro: "Nexrall Pro",
@@ -62604,28 +62879,83 @@ ${content}`);
62604
62879
  }
62605
62880
  return parts.length > 0 ? parts.join("\n\n---\n\n") : void 0;
62606
62881
  }
62882
+ var THINK_TAIL_CHARS = 48;
62883
+ function composeStatusLine(opts) {
62884
+ const timer = source_default.dim(` (${opts.elapsedSec}s)`);
62885
+ const hint = opts.hint !== false && !opts.detail && opts.elapsedSec >= 15 ? source_default.dim(" \xB7 still running, no output yet is normal for quiet commands") : "";
62886
+ const detail = opts.detail ? source_default.dim(` ${opts.detail}`) : "";
62887
+ return `${source_default.cyan(opts.frame)} ${source_default.dim(opts.text)}${timer}${detail}${hint}`;
62888
+ }
62889
+ function formatProgressTokens(tokens) {
62890
+ if (!Number.isFinite(tokens) || tokens <= 0)
62891
+ return "";
62892
+ return tokens >= 1e3 ? `~${(tokens / 1e3).toFixed(1)}k tokens` : `~${tokens} tokens`;
62893
+ }
62607
62894
  var Spinner = class {
62608
62895
  interval = null;
62609
62896
  frames = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
62610
62897
  i = 0;
62611
62898
  startedAt = 0;
62612
62899
  baseText = "";
62613
- start(text) {
62900
+ // Extra live detail appended after the elapsed timer (e.g. "~1.2k tokens").
62901
+ // Kept separate from `baseText` and mutated through setDetail() so a stream of
62902
+ // progress events can refresh it WITHOUT restarting the spinner — calling
62903
+ // start() again would reset `startedAt` and the elapsed counter would sit at 0s
62904
+ // forever, which is precisely the "is it frozen?" signal this class exists to
62905
+ // remove.
62906
+ detail = "";
62907
+ // Whether the reassurance hint applies. Off for phases where slowness is
62908
+ // already explained by the detail text (e.g. a live token counter is itself
62909
+ // proof of life, so "no output yet is normal" would be noise).
62910
+ hintEnabled = true;
62911
+ start(text, opts) {
62614
62912
  this.stop();
62615
62913
  this.baseText = text;
62914
+ this.detail = "";
62915
+ this.hintEnabled = opts?.hint !== false;
62616
62916
  this.startedAt = Date.now();
62617
- this.interval = setInterval(() => {
62618
- const elapsedSec = Math.floor((Date.now() - this.startedAt) / 1e3);
62619
- const timer = source_default.dim(` (${elapsedSec}s)`);
62620
- const hint = elapsedSec >= 15 ? source_default.dim(" \xB7 still running, no output yet is normal for quiet commands") : "";
62621
- getInkTerminal()?.setLive(`${source_default.cyan(this.frames[this.i % this.frames.length])} ${source_default.dim(this.baseText)}${timer}${hint}`);
62622
- this.i++;
62623
- }, 100);
62917
+ this.render();
62918
+ this.interval = setInterval(() => this.render(), 100);
62919
+ }
62920
+ /**
62921
+ * Replace the trailing detail text in place, keeping the elapsed timer running.
62922
+ *
62923
+ * No-op when the spinner isn't active: progress events can arrive a tick after
62924
+ * something else (a tool row, the first text delta) legitimately stopped it, and
62925
+ * resurrecting the spinner there would fight the printed output for the status line.
62926
+ */
62927
+ setDetail(detail) {
62928
+ if (this.interval === null)
62929
+ return;
62930
+ if (this.detail === detail)
62931
+ return;
62932
+ this.detail = detail;
62933
+ this.render();
62934
+ }
62935
+ /** Swap the label without resetting the elapsed timer (phase change within one turn). */
62936
+ setText(text) {
62937
+ if (this.interval === null)
62938
+ return;
62939
+ if (this.baseText === text)
62940
+ return;
62941
+ this.baseText = text;
62942
+ this.render();
62943
+ }
62944
+ render() {
62945
+ getInkTerminal()?.setLive(composeStatusLine({
62946
+ frame: this.frames[this.i % this.frames.length],
62947
+ text: this.baseText,
62948
+ elapsedSec: Math.floor((Date.now() - this.startedAt) / 1e3),
62949
+ detail: this.detail,
62950
+ hint: this.hintEnabled
62951
+ }));
62952
+ this.i++;
62624
62953
  }
62625
62954
  stop() {
62626
62955
  if (this.interval !== null) {
62627
62956
  clearInterval(this.interval);
62628
62957
  this.interval = null;
62958
+ this.detail = "";
62629
62959
  getInkTerminal()?.setLive("");
62630
62960
  }
62631
62961
  }
@@ -62701,8 +63031,26 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
62701
63031
  let toolStartTime = 0;
62702
63032
  let lastToolName = "";
62703
63033
  let thinkingTokens = 0;
63034
+ let liveThinkTail = "";
63035
+ const pushStatus = () => {
63036
+ const parts = [];
63037
+ const tok = formatProgressTokens(thinkingTokens);
63038
+ if (tok)
63039
+ parts.push(tok);
63040
+ if (liveThinkTail)
63041
+ parts.push(liveThinkTail);
63042
+ spinner.setDetail(parts.join(" \xB7 "));
63043
+ };
63044
+ const resumeWorking = (label = "working\u2026") => {
63045
+ if (abortSignal.aborted)
63046
+ return;
63047
+ thinkingTokens = 0;
63048
+ liveThinkTail = "";
63049
+ spinner.start(label, { hint: false });
63050
+ };
62704
63051
  setMode(mode);
62705
63052
  console.log();
63053
+ spinner.start("thinking\u2026", { hint: false });
62706
63054
  const result = await (0, import_code_core3.runAgentLoop)(messages, {
62707
63055
  workDir,
62708
63056
  model: modelAlias,
@@ -62710,10 +63058,26 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
62710
63058
  nexrallMd,
62711
63059
  mode,
62712
63060
  effort,
63061
+ // The backend streams a cumulative output-token count (routes/code.js's
63062
+ // sendProgress, throttled to ≤5/s) covering thinking, visible text AND
63063
+ // tool-argument JSON. This used to be stored in a variable and rendered only
63064
+ // at message_complete — i.e. after the turn was already over — so during the
63065
+ // long phase it exists to describe, it showed nothing at all.
62713
63066
  onThinkingProgress: (tokens) => {
62714
63067
  thinkingTokens = tokens;
63068
+ pushStatus();
62715
63069
  },
62716
- onThinkingDelta: () => {
63070
+ // Live thinking text. Previously a hard no-op with the note "shown on
63071
+ // onThinking" — but onThinking only fires at message_complete, so a long
63072
+ // reasoning phase rendered nothing whatsoever until it had finished. Show a
63073
+ // short rolling tail on the status line so the user can see it actively
63074
+ // reasoning, then let onThinking print the proper summary block at the end.
63075
+ onThinkingDelta: (text) => {
63076
+ if (abortSignal.aborted)
63077
+ return;
63078
+ const flat = text.replace(/\s+/g, " ");
63079
+ liveThinkTail = (liveThinkTail + flat).slice(-THINK_TAIL_CHARS);
63080
+ pushStatus();
62717
63081
  },
62718
63082
  onThinking: (text) => {
62719
63083
  if (abortSignal.aborted)
@@ -62721,6 +63085,8 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
62721
63085
  spinner.stop();
62722
63086
  console.log(formatThinking(text, thinkingTokens));
62723
63087
  thinkingTokens = 0;
63088
+ liveThinkTail = "";
63089
+ resumeWorking();
62724
63090
  },
62725
63091
  onText: (text) => {
62726
63092
  if (abortSignal.aborted)
@@ -62739,6 +63105,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
62739
63105
  mdRender.flush();
62740
63106
  spinner.stop();
62741
63107
  console.log(source_default.dim(` ${text}`));
63108
+ resumeWorking();
62742
63109
  },
62743
63110
  onToolUse: (name, input) => {
62744
63111
  if (abortSignal.aborted)
@@ -62769,6 +63136,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
62769
63136
  const durationMs = Date.now() - toolStartTime;
62770
63137
  console.log(formatToolResult(lastToolName, res, durationMs));
62771
63138
  toolStartTime = 0;
63139
+ resumeWorking();
62772
63140
  },
62773
63141
  // Ignore a `partial` report: it belongs to a cut-short attempt that was restarted,
62774
63142
  // and the replacement attempt reports the turn's real totals. Letting it through
@@ -63971,7 +64339,7 @@ function pluginListCommand() {
63971
64339
 
63972
64340
  // src/index.ts
63973
64341
  var program2 = new Command();
63974
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.46");
64342
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.48");
63975
64343
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
63976
64344
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
63977
64345
  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.46",
3
+ "version": "0.5.48",
4
4
  "description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
5
5
  "keywords": [
6
6
  "ai",
@@ -41,7 +41,7 @@
41
41
  "react": "^19.2.8",
42
42
  "readline": "^1.3.0",
43
43
  "string-width": "^7.2.0",
44
- "@nexrall/code-core": "1.4.22"
44
+ "@nexrall/code-core": "1.4.23"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@aws-sdk/client-s3": "^3.600.0",