nexrall-code 0.5.2 → 0.5.18

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 +1855 -615
  2. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -3070,6 +3070,27 @@ var require_types = __commonJS({
3070
3070
  "../core/dist/types.js"(exports2) {
3071
3071
  "use strict";
3072
3072
  Object.defineProperty(exports2, "__esModule", { value: true });
3073
+ exports2.AgentTurnError = void 0;
3074
+ exports2.isAgentTurnError = isAgentTurnError;
3075
+ exports2.salvageHistory = salvageHistory2;
3076
+ var AgentTurnError = class extends Error {
3077
+ constructor(message, messages, progressCount, cause) {
3078
+ super(message);
3079
+ this.name = "AgentTurnError";
3080
+ this.messages = messages;
3081
+ this.progressCount = Math.max(0, progressCount);
3082
+ this.cause = cause;
3083
+ }
3084
+ };
3085
+ exports2.AgentTurnError = AgentTurnError;
3086
+ function isAgentTurnError(err) {
3087
+ return err instanceof AgentTurnError;
3088
+ }
3089
+ function salvageHistory2(err) {
3090
+ if (!isAgentTurnError(err))
3091
+ return null;
3092
+ return err.progressCount > 0 ? err.messages : null;
3093
+ }
3073
3094
  }
3074
3095
  });
3075
3096
 
@@ -9810,14 +9831,49 @@ var require_client = __commonJS({
9810
9831
  };
9811
9832
  Object.defineProperty(exports2, "__esModule", { value: true });
9812
9833
  exports2.API_BASE = void 0;
9834
+ exports2.chooseFinalContent = chooseFinalContent;
9813
9835
  exports2.streamChat = streamChat;
9836
+ exports2.cancelTurn = cancelTurn;
9814
9837
  exports2.getBalance = getBalance3;
9815
9838
  exports2.exchangeVscodeCode = exchangeVscodeCode;
9816
9839
  exports2.login = login2;
9817
9840
  var eventsource_parser_1 = require_dist();
9818
9841
  var node_fetch_1 = __importDefault((init_src(), __toCommonJS(src_exports)));
9842
+ var crypto_1 = require("crypto");
9819
9843
  var index_1 = require_auth();
9820
- exports2.API_BASE = "https://api.nexrall.com";
9844
+ var DEFAULT_API_BASE = "https://api.nexrall.com";
9845
+ function resolveApiBase() {
9846
+ const raw = process.env.NEXRALL_API_BASE?.trim();
9847
+ if (!raw)
9848
+ return DEFAULT_API_BASE;
9849
+ const isLoopback = /^http:\/\/(127\.0\.0\.1|\[::1\]|localhost)(:\d+)?(\/|$)/i.test(raw);
9850
+ if (isLoopback) {
9851
+ console.error(`\u26A0\uFE0F Nexrall API override in effect (loopback): ${raw.replace(/\/+$/, "")}`);
9852
+ return raw.replace(/\/+$/, "");
9853
+ }
9854
+ if (process.env.NEXRALL_ALLOW_API_OVERRIDE !== "1") {
9855
+ console.error(`\u26A0\uFE0F Ignoring NEXRALL_API_BASE="${raw}": redirecting to a remote host also requires NEXRALL_ALLOW_API_OVERRIDE=1, because this variable receives your auth token and source code. Falling back to ${DEFAULT_API_BASE}.`);
9856
+ return DEFAULT_API_BASE;
9857
+ }
9858
+ if (!/^https:\/\//i.test(raw)) {
9859
+ console.error(`\u26A0\uFE0F Ignoring NEXRALL_API_BASE="${raw}": a remote override must be https:// so your token is never sent in plaintext. Falling back to ${DEFAULT_API_BASE}.`);
9860
+ return DEFAULT_API_BASE;
9861
+ }
9862
+ const base = raw.replace(/\/+$/, "");
9863
+ console.error(`\u26A0\uFE0F Nexrall API override in effect: ${base}`);
9864
+ return base;
9865
+ }
9866
+ exports2.API_BASE = resolveApiBase();
9867
+ function chooseFinalContent(rebuilt, rawContent) {
9868
+ if (!Array.isArray(rawContent))
9869
+ return rebuilt;
9870
+ const hasServerSideBlocks = rawContent.some((b) => b && b.type !== "text" && b.type !== "tool_use");
9871
+ if (hasServerSideBlocks)
9872
+ return rawContent;
9873
+ if (rebuilt.length === 0 && rawContent.length > 0)
9874
+ return rawContent;
9875
+ return rebuilt;
9876
+ }
9821
9877
  function authHeaders() {
9822
9878
  const token = (0, index_1.getToken)();
9823
9879
  if (!token) {
@@ -9828,103 +9884,265 @@ var require_client = __commonJS({
9828
9884
  Authorization: `Bearer ${token}`
9829
9885
  };
9830
9886
  }
9831
- var MAX_RETRIES = 3;
9887
+ var MAX_RETRIES = 5;
9832
9888
  var RETRY_BASE_MS = 1e3;
9889
+ var RETRY_MAX_MS = 3e4;
9890
+ var MAX_TOTAL_RETRY_MS = (() => {
9891
+ const raw = Number(process.env.NEXRALL_MAX_RETRY_MS);
9892
+ return Number.isFinite(raw) && raw > 0 ? raw : 5 * 6e4;
9893
+ })();
9894
+ function backoffMs(attempt) {
9895
+ const exp = Math.min(RETRY_MAX_MS, RETRY_BASE_MS * Math.pow(2, attempt));
9896
+ return Math.round(exp / 2 + Math.random() * (exp / 2));
9897
+ }
9833
9898
  function sleep(ms) {
9834
9899
  return new Promise((r2) => setTimeout(r2, ms));
9835
9900
  }
9901
+ var MAX_TOTAL_ATTEMPTS = (MAX_RETRIES + 1) * (MAX_RETRIES + 1);
9836
9902
  async function streamChat(messages, options, onEvent) {
9837
- const { model, env: env2, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents } = options;
9903
+ const { model, env: env2, editorContext, nexrallMd, mode, effort, clientType, abortSignal, extraTools, agents, skills, allowRestartAfterRender } = options;
9904
+ let turnId = (0, crypto_1.randomUUID)();
9838
9905
  const controller = new AbortController();
9839
9906
  if (abortSignal?.aborted)
9840
9907
  throw Object.assign(new Error("Aborted"), { name: "AbortError" });
9841
9908
  let abortPoll;
9909
+ let cancelSent = false;
9842
9910
  if (abortSignal) {
9843
9911
  abortPoll = setInterval(() => {
9844
- if (abortSignal.aborted)
9845
- controller.abort();
9912
+ if (!abortSignal.aborted)
9913
+ return;
9914
+ if (cancelSent)
9915
+ return;
9916
+ cancelSent = true;
9917
+ void cancelTurn(turnId);
9918
+ controller.abort();
9846
9919
  }, 50);
9847
9920
  }
9848
- const fetchArgs = [
9849
- `${exports2.API_BASE}/api/code/chat`,
9850
- {
9851
- method: "POST",
9852
- headers: authHeaders(),
9853
- body: JSON.stringify({ messages, model, env: env2, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents }),
9854
- signal: controller.signal
9921
+ let serverResumable = false;
9922
+ let lastEventId = 0;
9923
+ let resuming = false;
9924
+ let carryText = [];
9925
+ let carryToolUse = [];
9926
+ let emittedAnythingAcrossAttempts = false;
9927
+ let emittedCharsAcrossAttempts = 0;
9928
+ const buildFetchArgs = () => {
9929
+ if (resuming) {
9930
+ return [
9931
+ `${exports2.API_BASE}/api/code/chat/resume?turnId=${encodeURIComponent(turnId)}`,
9932
+ {
9933
+ method: "GET",
9934
+ headers: { ...authHeaders(), "Last-Event-ID": String(lastEventId) },
9935
+ signal: controller.signal
9936
+ }
9937
+ ];
9855
9938
  }
9856
- ];
9939
+ return [
9940
+ `${exports2.API_BASE}/api/code/chat`,
9941
+ {
9942
+ method: "POST",
9943
+ headers: authHeaders(),
9944
+ body: JSON.stringify({ messages, model, env: env2, editorContext, nexrallMd, mode, effort, clientType, extraTools, agents, skills, turnId }),
9945
+ signal: controller.signal
9946
+ }
9947
+ ];
9948
+ };
9857
9949
  const isRetryableStreamMsg = (m2) => /overloaded|rate.?limit|temporarily|unavailable|try again|internal server error/i.test(String(m2 ?? ""));
9950
+ let didRetry = false;
9951
+ let totalAttemptsMade = 0;
9952
+ let retryDeadline = 0;
9953
+ const retryBudgetLeft = () => retryDeadline === 0 ? MAX_TOTAL_RETRY_MS : retryDeadline - Date.now();
9954
+ const canRetry = () => retryBudgetLeft() > RETRY_BASE_MS;
9955
+ const sleepWithinBudget = (ms) => sleep(Math.max(0, Math.min(ms, retryBudgetLeft())));
9956
+ const reportRetry = (reason) => {
9957
+ if (retryDeadline === 0)
9958
+ retryDeadline = Date.now() + MAX_TOTAL_RETRY_MS;
9959
+ totalAttemptsMade++;
9960
+ didRetry = true;
9961
+ onEvent({ type: "retry", attempt: totalAttemptsMade, maxAttempts: MAX_TOTAL_ATTEMPTS, reason });
9962
+ };
9963
+ const clearRetryIfNeeded = () => {
9964
+ if (didRetry) {
9965
+ didRetry = false;
9966
+ onEvent({ type: "retry_resolved" });
9967
+ }
9968
+ };
9858
9969
  async function runAttempt() {
9859
9970
  let response;
9860
9971
  let lastErr;
9972
+ let inFlightWaits = 0;
9973
+ let errorBodyOverride = null;
9974
+ let forbiddenExhausted = false;
9861
9975
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
9862
9976
  try {
9863
- response = await (0, node_fetch_1.default)(...fetchArgs);
9864
- if (response.status === 429 && attempt < MAX_RETRIES) {
9977
+ response = await (0, node_fetch_1.default)(...buildFetchArgs());
9978
+ if (response.status === 429 && attempt < MAX_RETRIES && canRetry()) {
9979
+ reportRetry("Rate limited by the API \u2014 retrying");
9865
9980
  const retryAfter = parseInt(response.headers.get("retry-after") ?? "0", 10);
9866
- await sleep(retryAfter > 0 ? retryAfter * 1e3 : RETRY_BASE_MS * Math.pow(2, attempt));
9981
+ await sleepWithinBudget(retryAfter > 0 ? retryAfter * 1e3 : backoffMs(attempt));
9867
9982
  continue;
9868
9983
  }
9869
- if (response.status >= 500 && response.status < 600 && attempt < MAX_RETRIES) {
9870
- await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
9984
+ if (response.status >= 500 && response.status < 600 && attempt < MAX_RETRIES && canRetry()) {
9985
+ reportRetry(`Server error (${response.status}) \u2014 retrying`);
9986
+ await sleepWithinBudget(backoffMs(attempt));
9871
9987
  continue;
9872
9988
  }
9989
+ const FORBIDDEN_RETRY_LIMIT = 2;
9990
+ if (response.status === 403) {
9991
+ const bodyText = await response.text().catch(() => "");
9992
+ let isUpstreamForbidden = false;
9993
+ try {
9994
+ const parsed = JSON.parse(bodyText);
9995
+ isUpstreamForbidden = parsed?.error?.type === "forbidden";
9996
+ } catch {
9997
+ }
9998
+ if (isUpstreamForbidden) {
9999
+ if (attempt < FORBIDDEN_RETRY_LIMIT && canRetry()) {
10000
+ reportRetry("Upstream rejected the request \u2014 retrying");
10001
+ await sleepWithinBudget(backoffMs(attempt));
10002
+ continue;
10003
+ }
10004
+ forbiddenExhausted = true;
10005
+ errorBodyOverride = bodyText;
10006
+ break;
10007
+ }
10008
+ errorBodyOverride = bodyText;
10009
+ break;
10010
+ }
10011
+ if (response.status === 410 && resuming) {
10012
+ await response.text().catch(() => "");
10013
+ await cancelTurn(turnId);
10014
+ turnId = (0, crypto_1.randomUUID)();
10015
+ resuming = false;
10016
+ serverResumable = false;
10017
+ lastEventId = 0;
10018
+ carryText = [];
10019
+ carryToolUse = [];
10020
+ const hadRendered = emittedAnythingAcrossAttempts;
10021
+ const renderedChars = emittedCharsAcrossAttempts;
10022
+ emittedAnythingAcrossAttempts = false;
10023
+ emittedCharsAcrossAttempts = 0;
10024
+ if (hadRendered && !allowRestartAfterRender) {
10025
+ throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
10026
+ }
10027
+ if (hadRendered) {
10028
+ onEvent({
10029
+ type: "stream_restart",
10030
+ reason: "Connection lost too long to resume",
10031
+ discardedChars: renderedChars
10032
+ });
10033
+ }
10034
+ if (attempt < MAX_RETRIES && canRetry()) {
10035
+ reportRetry("Could not resume \u2014 restarting this turn");
10036
+ await sleepWithinBudget(backoffMs(attempt));
10037
+ continue;
10038
+ }
10039
+ errorBodyOverride = JSON.stringify({ error: "Could not resume this turn \u2014 send your message again." });
10040
+ break;
10041
+ }
10042
+ if (response.status === 409) {
10043
+ const body = await response.text();
10044
+ let inFlight = false;
10045
+ try {
10046
+ inFlight = JSON.parse(body)?.turnInFlight === true;
10047
+ } catch {
10048
+ }
10049
+ if (inFlight && canRetry()) {
10050
+ inFlightWaits++;
10051
+ reportRetry("This turn is already being processed \u2014 waiting for it");
10052
+ const retryAfter = parseInt(response.headers.get("retry-after") ?? "0", 10);
10053
+ await sleepWithinBudget(Math.max(retryAfter * 1e3, backoffMs(inFlightWaits)));
10054
+ attempt--;
10055
+ continue;
10056
+ }
10057
+ errorBodyOverride = body;
10058
+ break;
10059
+ }
9873
10060
  break;
9874
10061
  } catch (err) {
9875
10062
  if (err.name === "AbortError")
9876
10063
  throw err;
9877
10064
  lastErr = err;
9878
- if (attempt < MAX_RETRIES) {
9879
- await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
10065
+ if (attempt < MAX_RETRIES && canRetry()) {
10066
+ reportRetry("Connection lost \u2014 attempting to reconnect");
10067
+ await sleepWithinBudget(backoffMs(attempt));
9880
10068
  continue;
9881
10069
  }
10070
+ if (!canRetry() && lastErr && typeof lastErr === "object") {
10071
+ Object.assign(lastErr, { retryable: true });
10072
+ }
10073
+ break;
9882
10074
  }
9883
10075
  }
9884
10076
  if (!response)
9885
10077
  throw lastErr ?? new Error("Request failed after max retries");
9886
10078
  if (!response.ok) {
9887
- const errText = await response.text();
10079
+ const errText = errorBodyOverride ?? await response.text();
9888
10080
  let errMsg = `API error ${response.status}`;
10081
+ let balance;
9889
10082
  try {
9890
10083
  const parsed = JSON.parse(errText);
9891
10084
  if (parsed.error)
9892
10085
  errMsg = parsed.error;
10086
+ if (typeof parsed.balance === "number")
10087
+ balance = parsed.balance;
9893
10088
  } catch {
9894
10089
  errMsg = errText || errMsg;
9895
10090
  }
9896
- throw new Error(errMsg);
10091
+ throw Object.assign(new Error(errMsg), { status: response.status, balance, ...forbiddenExhausted ? { retryable: true } : {} });
9897
10092
  }
9898
10093
  if (!response.body) {
9899
10094
  throw new Error("Response body is null");
9900
10095
  }
9901
- const textParts = [];
9902
- const toolUseBlocks = [];
10096
+ const textParts = resuming ? [...carryText] : [];
10097
+ const toolUseBlocks = resuming ? [...carryToolUse] : [];
9903
10098
  let completedMessage = null;
10099
+ carryText = textParts;
10100
+ carryToolUse = toolUseBlocks;
9904
10101
  let emittedToCaller = false;
10102
+ let emittedChars = 0;
10103
+ const tagTransient = (err) => {
10104
+ if (!emittedToCaller)
10105
+ return Object.assign(err, { retryable: true });
10106
+ if (allowRestartAfterRender) {
10107
+ return Object.assign(err, { retryable: true, needsRestart: true, discardedChars: emittedChars });
10108
+ }
10109
+ return err;
10110
+ };
10111
+ const haveCompleteMessage = () => completedMessage !== null;
9905
10112
  const partialInputs = {};
9906
10113
  await new Promise((resolve3, reject) => {
9907
10114
  const stream = response.body;
9908
- const HEARTBEAT_TIMEOUT_MS = 45e3;
9909
- const FIRST_EVENT_TIMEOUT_MS = 3e5;
9910
- const PROGRESS_TIMEOUT_MS = 15e4;
10115
+ const HEARTBEAT_TIMEOUT_MS = 9e4;
10116
+ const FIRST_EVENT_TIMEOUT_MS = Number(process.env.NEXRALL_FIRST_EVENT_TIMEOUT_MS) || 3e5;
10117
+ const PROGRESS_TIMEOUT_MS = Number(process.env.NEXRALL_PROGRESS_TIMEOUT_MS) || 15e4;
9911
10118
  let sawModelEvent = false;
9912
10119
  let lastDataAt = Date.now();
9913
10120
  let lastProgressAt = Date.now();
9914
10121
  const heartbeatWatchdog = setInterval(() => {
9915
10122
  const now = Date.now();
10123
+ if (haveCompleteMessage() && now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
10124
+ clearInterval(heartbeatWatchdog);
10125
+ stream.destroy?.();
10126
+ resolve3();
10127
+ return;
10128
+ }
9916
10129
  if (now - lastDataAt > HEARTBEAT_TIMEOUT_MS) {
9917
10130
  clearInterval(heartbeatWatchdog);
9918
10131
  stream.destroy?.();
9919
- reject(new Error("Connection lost \u2014 no data received for 45 s. Retry your message."));
10132
+ const recoverable = !emittedToCaller || !!allowRestartAfterRender;
10133
+ const secs = Math.round(HEARTBEAT_TIMEOUT_MS / 1e3);
10134
+ reject(tagTransient(new Error(recoverable ? `Connection lost \u2014 no data received for ${secs} s. Reconnecting\u2026` : `Connection lost \u2014 no data received for ${secs} s. Retry your message.`)));
9920
10135
  return;
9921
10136
  }
9922
10137
  const stallLimitMs = sawModelEvent ? PROGRESS_TIMEOUT_MS : FIRST_EVENT_TIMEOUT_MS;
9923
10138
  if (now - lastProgressAt > stallLimitMs) {
9924
10139
  clearInterval(heartbeatWatchdog);
9925
10140
  stream.destroy?.();
9926
- const stallErr = new Error(sawModelEvent ? `The model stopped responding mid-stream (no output for ${Math.round(stallLimitMs / 1e3)} s). Retry your message.` : `The model did not start responding within ${Math.round(stallLimitMs / 1e3)} s (large context can take a while to process). Retry your message.`);
9927
- reject(emittedToCaller ? stallErr : Object.assign(stallErr, { retryable: true }));
10141
+ if (haveCompleteMessage()) {
10142
+ resolve3();
10143
+ return;
10144
+ }
10145
+ reject(tagTransient(new Error(sawModelEvent ? `The model stopped responding mid-stream (no output for ${Math.round(stallLimitMs / 1e3)} s).` : `The model did not start responding within ${Math.round(stallLimitMs / 1e3)} s (large context can take a while to process).`)));
9928
10146
  return;
9929
10147
  }
9930
10148
  }, 5e3);
@@ -9934,6 +10152,12 @@ var require_client = __commonJS({
9934
10152
  return;
9935
10153
  }
9936
10154
  {
10155
+ const frameId = event.id;
10156
+ if (frameId) {
10157
+ const n = Number(frameId);
10158
+ if (Number.isFinite(n) && n > lastEventId)
10159
+ lastEventId = n;
10160
+ }
9937
10161
  const raw = event.data;
9938
10162
  if (!raw || raw === "[DONE]") {
9939
10163
  resolve3();
@@ -9951,11 +10175,15 @@ var require_client = __commonJS({
9951
10175
  const evt = parsed;
9952
10176
  lastProgressAt = Date.now();
9953
10177
  sawModelEvent = true;
10178
+ clearRetryIfNeeded();
9954
10179
  switch (evt.type) {
9955
10180
  case "text": {
9956
10181
  const text = typeof evt.text === "string" ? evt.text : "";
9957
10182
  textParts.push(text);
9958
10183
  emittedToCaller = true;
10184
+ emittedChars += text.length;
10185
+ emittedAnythingAcrossAttempts = true;
10186
+ emittedCharsAcrossAttempts += text.length;
9959
10187
  onEvent({ type: "text", text });
9960
10188
  break;
9961
10189
  }
@@ -9976,19 +10204,25 @@ var require_client = __commonJS({
9976
10204
  }
9977
10205
  toolUseBlocks.push(block);
9978
10206
  emittedToCaller = true;
10207
+ emittedAnythingAcrossAttempts = true;
9979
10208
  onEvent({ type: "tool_use", id: block.id, name: block.name, input: block.input });
9980
10209
  break;
9981
10210
  }
9982
10211
  case "message_complete": {
10212
+ const nestedMessage = evt.message;
10213
+ const stopReason = typeof nestedMessage?.stop_reason === "string" ? nestedMessage.stop_reason : null;
9983
10214
  const contentBlocks2 = [];
9984
10215
  const fullText2 = textParts.join("");
9985
10216
  if (fullText2) {
9986
10217
  contentBlocks2.push({ type: "text", text: fullText2 });
9987
10218
  }
9988
10219
  contentBlocks2.push(...toolUseBlocks);
10220
+ const rawContent = Array.isArray(nestedMessage?.content) ? nestedMessage.content : null;
10221
+ const finalContent = chooseFinalContent(contentBlocks2, rawContent);
9989
10222
  completedMessage = {
9990
10223
  role: "assistant",
9991
- content: contentBlocks2
10224
+ content: finalContent,
10225
+ stopReason
9992
10226
  };
9993
10227
  onEvent({ type: "message_complete", message: completedMessage });
9994
10228
  break;
@@ -9997,6 +10231,17 @@ var require_client = __commonJS({
9997
10231
  if (typeof evt.input_tokens === "number" && typeof evt.output_tokens === "number") {
9998
10232
  onEvent({
9999
10233
  type: "usage",
10234
+ // Forwarded, not dropped: the backend tags the usage of an attempt it
10235
+ // billed but never completed (its stream `abort` path). Without this
10236
+ // the flag dies here and every consumer that sums usage silently folds
10237
+ // a discarded attempt's tokens into the successful turn's total.
10238
+ ...evt.partial === true ? { partial: true } : {},
10239
+ // `replayed` means these tokens are being reported a SECOND time: the
10240
+ // turn completed and was billed on an earlier attempt whose `done` never
10241
+ // reached us, and the backend served this one from its idempotency cache
10242
+ // instead of re-running the model. Nothing new was charged, so a cost
10243
+ // display must not add them again.
10244
+ ...evt.replayed === true ? { replayed: true } : {},
10000
10245
  usage: {
10001
10246
  input_tokens: evt.input_tokens,
10002
10247
  output_tokens: evt.output_tokens,
@@ -10017,6 +10262,9 @@ var require_client = __commonJS({
10017
10262
  const text = typeof evt.text === "string" ? evt.text : "";
10018
10263
  if (text) {
10019
10264
  emittedToCaller = true;
10265
+ emittedChars += text.length;
10266
+ emittedAnythingAcrossAttempts = true;
10267
+ emittedCharsAcrossAttempts += text.length;
10020
10268
  onEvent({ type: "thinking", text });
10021
10269
  }
10022
10270
  break;
@@ -10030,10 +10278,23 @@ var require_client = __commonJS({
10030
10278
  const text = typeof evt.text === "string" ? evt.text : "";
10031
10279
  if (text) {
10032
10280
  emittedToCaller = true;
10281
+ emittedChars += text.length;
10282
+ emittedAnythingAcrossAttempts = true;
10283
+ emittedCharsAcrossAttempts += text.length;
10033
10284
  onEvent({ type: "thinking_delta", text });
10034
10285
  }
10035
10286
  break;
10036
10287
  }
10288
+ case "resumable": {
10289
+ serverResumable = true;
10290
+ break;
10291
+ }
10292
+ case "balance_status": {
10293
+ const balance = typeof evt.balance === "number" ? evt.balance : 0;
10294
+ const zero = !!evt.zero;
10295
+ onEvent({ type: "balance_status", balance, zero });
10296
+ break;
10297
+ }
10037
10298
  case "done": {
10038
10299
  onEvent({ type: "done" });
10039
10300
  resolve3();
@@ -10041,10 +10302,19 @@ var require_client = __commonJS({
10041
10302
  }
10042
10303
  case "error": {
10043
10304
  const message = typeof evt.message === "string" ? evt.message : typeof evt.error === "string" ? evt.error : "Unknown SSE error";
10044
- if (isRetryableStreamMsg(message) && !emittedToCaller) {
10305
+ const notResumable = evt.notResumable === true;
10306
+ if (haveCompleteMessage()) {
10307
+ clearInterval(heartbeatWatchdog);
10308
+ onEvent({ type: "error", message });
10309
+ resolve3();
10310
+ } else if (notResumable && (!emittedToCaller || allowRestartAfterRender)) {
10045
10311
  clearInterval(heartbeatWatchdog);
10046
10312
  stream.destroy?.();
10047
- reject(Object.assign(new Error(message), { retryable: true }));
10313
+ reject(Object.assign(tagTransient(new Error(message)), { forceRestart: true }));
10314
+ } else if (isRetryableStreamMsg(message) && (!emittedToCaller || allowRestartAfterRender)) {
10315
+ clearInterval(heartbeatWatchdog);
10316
+ stream.destroy?.();
10317
+ reject(tagTransient(new Error(message)));
10048
10318
  } else {
10049
10319
  onEvent({ type: "error", message });
10050
10320
  reject(new Error(message));
@@ -10076,11 +10346,11 @@ var require_client = __commonJS({
10076
10346
  resolve3();
10077
10347
  return;
10078
10348
  }
10079
- if (!emittedToCaller) {
10080
- reject(Object.assign(err, { retryable: true }));
10349
+ if (haveCompleteMessage()) {
10350
+ resolve3();
10081
10351
  return;
10082
10352
  }
10083
- reject(err);
10353
+ reject(tagTransient(err));
10084
10354
  });
10085
10355
  });
10086
10356
  if (completedMessage) {
@@ -10097,14 +10367,60 @@ var require_client = __commonJS({
10097
10367
  try {
10098
10368
  for (let sAttempt = 0; sAttempt <= MAX_RETRIES; sAttempt++) {
10099
10369
  try {
10100
- return await runAttempt();
10370
+ const result = await runAttempt();
10371
+ clearRetryIfNeeded();
10372
+ return result;
10101
10373
  } catch (err) {
10102
10374
  if (abortSignal?.aborted || controller.signal.aborted || err.name === "AbortError")
10103
10375
  throw err;
10104
- if (err.retryable && sAttempt < MAX_RETRIES) {
10105
- await sleep(RETRY_BASE_MS * Math.pow(2, sAttempt));
10376
+ const e2 = err;
10377
+ if (e2.retryable && sAttempt < MAX_RETRIES && canRetry()) {
10378
+ if (e2.forceRestart) {
10379
+ await cancelTurn(turnId);
10380
+ turnId = (0, crypto_1.randomUUID)();
10381
+ resuming = false;
10382
+ serverResumable = false;
10383
+ lastEventId = 0;
10384
+ carryText = [];
10385
+ carryToolUse = [];
10386
+ const hadRendered = emittedAnythingAcrossAttempts;
10387
+ const renderedChars = emittedCharsAcrossAttempts;
10388
+ emittedAnythingAcrossAttempts = false;
10389
+ emittedCharsAcrossAttempts = 0;
10390
+ if (hadRendered && !allowRestartAfterRender) {
10391
+ throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
10392
+ }
10393
+ if (hadRendered) {
10394
+ onEvent({
10395
+ type: "stream_restart",
10396
+ reason: err.message || "Could not resume \u2014 restarting this turn",
10397
+ discardedChars: renderedChars
10398
+ });
10399
+ }
10400
+ reportRetry("Could not resume \u2014 restarting this turn");
10401
+ await sleepWithinBudget(backoffMs(sAttempt));
10402
+ continue;
10403
+ }
10404
+ if (serverResumable && lastEventId > 0) {
10405
+ resuming = true;
10406
+ reportRetry(err.message || "Connection interrupted \u2014 resuming");
10407
+ await sleepWithinBudget(backoffMs(sAttempt));
10408
+ continue;
10409
+ }
10410
+ if (e2.needsRestart) {
10411
+ onEvent({
10412
+ type: "stream_restart",
10413
+ reason: err.message || "Connection interrupted",
10414
+ discardedChars: e2.discardedChars ?? 0
10415
+ });
10416
+ }
10417
+ reportRetry(err.message || "Connection interrupted \u2014 reconnecting");
10418
+ await sleepWithinBudget(backoffMs(sAttempt));
10106
10419
  continue;
10107
10420
  }
10421
+ if (e2.retryable && !canRetry()) {
10422
+ throw Object.assign(new Error(`${err.message} \u2014 gave up after ${Math.round(MAX_TOTAL_RETRY_MS / 6e4)} minutes of reconnect attempts. Check your connection and resend; completed work in this turn is preserved.`), { retryBudgetExhausted: true });
10423
+ }
10108
10424
  throw err;
10109
10425
  }
10110
10426
  }
@@ -10114,6 +10430,16 @@ var require_client = __commonJS({
10114
10430
  clearInterval(abortPoll);
10115
10431
  }
10116
10432
  }
10433
+ async function cancelTurn(turnId) {
10434
+ try {
10435
+ await (0, node_fetch_1.default)(`${exports2.API_BASE}/api/code/chat/cancel`, {
10436
+ method: "POST",
10437
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
10438
+ body: JSON.stringify({ turnId })
10439
+ });
10440
+ } catch {
10441
+ }
10442
+ }
10117
10443
  async function getBalance3() {
10118
10444
  const response = await (0, node_fetch_1.default)(`${exports2.API_BASE}/api/code/balance`, {
10119
10445
  method: "GET",
@@ -10360,123 +10686,887 @@ var require_editCompleteness = __commonJS({
10360
10686
  }
10361
10687
  });
10362
10688
 
10363
- // ../core/dist/agent/testIntegrity.js
10364
- var require_testIntegrity = __commonJS({
10365
- "../core/dist/agent/testIntegrity.js"(exports2) {
10689
+ // ../core/dist/agent/memory.js
10690
+ var require_memory = __commonJS({
10691
+ "../core/dist/agent/memory.js"(exports2) {
10366
10692
  "use strict";
10693
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m2, k, k2) {
10694
+ if (k2 === void 0)
10695
+ k2 = k;
10696
+ var desc = Object.getOwnPropertyDescriptor(m2, k);
10697
+ if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
10698
+ desc = { enumerable: true, get: function() {
10699
+ return m2[k];
10700
+ } };
10701
+ }
10702
+ Object.defineProperty(o, k2, desc);
10703
+ } : function(o, m2, k, k2) {
10704
+ if (k2 === void 0)
10705
+ k2 = k;
10706
+ o[k2] = m2[k];
10707
+ });
10708
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? function(o, v) {
10709
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
10710
+ } : function(o, v) {
10711
+ o["default"] = v;
10712
+ });
10713
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ function() {
10714
+ var ownKeys = function(o) {
10715
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
10716
+ var ar = [];
10717
+ for (var k in o2)
10718
+ if (Object.prototype.hasOwnProperty.call(o2, k))
10719
+ ar[ar.length] = k;
10720
+ return ar;
10721
+ };
10722
+ return ownKeys(o);
10723
+ };
10724
+ return function(mod) {
10725
+ if (mod && mod.__esModule)
10726
+ return mod;
10727
+ var result = {};
10728
+ if (mod != null) {
10729
+ for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
10730
+ if (k[i2] !== "default")
10731
+ __createBinding(result, mod, k[i2]);
10732
+ }
10733
+ __setModuleDefault(result, mod);
10734
+ return result;
10735
+ };
10736
+ }();
10367
10737
  Object.defineProperty(exports2, "__esModule", { value: true });
10368
- exports2.isTestFile = isTestFile;
10369
- exports2.analyzeTestEdit = analyzeTestEdit;
10370
- exports2.analyzeWriteToolForTestIntegrity = analyzeWriteToolForTestIntegrity;
10371
- exports2.encodeTestIntegrityMarker = encodeTestIntegrityMarker;
10372
- exports2.decodeTestIntegrityMarker = decodeTestIntegrityMarker;
10373
- exports2.stripTestIntegrityMarker = stripTestIntegrityMarker;
10374
- var TEST_PATH_RE = /(?:^|[\/\\])(?:tests?|spec|__tests__|testing)[\/\\]|(?:\.|_|-)(?:test|spec|tests)\.[a-z]+$|(?:^|[\/\\])test_[^\/\\]+\.py$|(?:^|[\/\\])[^\/\\]+_test\.(?:go|py|rb)$|Test[^\/\\]*\.(?:java|kt|cs)$|(?:^|[\/\\])[^\/\\]*Tests?\.(?:java|kt|cs)$/i;
10375
- function isTestFile(path5) {
10376
- if (!path5 || typeof path5 !== "string")
10377
- return false;
10378
- return TEST_PATH_RE.test(path5);
10379
- }
10380
- var ASSERTION_RES = [
10381
- /\bexpect\s*\(/g,
10382
- // jest / chai / vitest / jasmine
10383
- /\bassert(?:Equals?|True|False|That|Null|NotNull|Same)?\b/g,
10384
- // junit / python unittest / generic
10385
- /\bassert\s*[.(]/g,
10386
- // node:assert (assert.equal / assert( ) )
10387
- /\.should\b/g,
10388
- // chai should / rspec should
10389
- /\bshould\s*[.(]/g,
10390
- /\bEXPECT_[A-Z]+\s*\(/g,
10391
- // gtest
10392
- /\bASSERT_[A-Z]+\s*\(/g,
10393
- // gtest
10394
- /\brequire\.\w+\s*\(/g,
10395
- // testify require
10396
- /\bassert\.\w+\s*\(/g,
10397
- // testify assert
10398
- /\bt\.(?:Error|Fatal|Errorf|Fatalf)\b/g,
10399
- // go testing
10400
- /\bassert!\s*\(|\bassert_eq!\s*\(|\bassert_ne!\s*\(/g
10401
- // rust
10402
- ];
10403
- var TESTCASE_RES = [
10404
- /\b(?:it|test)\s*\(/g,
10405
- // jest / mocha / jasmine
10406
- /\bdef\s+test_\w+/g,
10407
- // python
10408
- /\bfunc\s+Test\w+/g,
10409
- // go
10410
- /@Test\b/g,
10411
- // junit
10412
- /\bfn\s+\w*test\w*\s*\(/gi
10413
- // rust (best-effort)
10414
- ];
10415
- var SKIP_RES = [
10416
- /\b(?:it|test|describe|context|suite)\s*\.\s*(?:skip|only)\b/g,
10417
- /\bx(?:it|describe|test|context)\s*\(/g,
10418
- // xit / xdescribe
10419
- /\bf(?:it|describe)\s*\(/g,
10420
- // fit / fdescribe (focus)
10421
- /@pytest\.mark\.skip\b|@pytest\.mark\.skipif\b|@unittest\.skip\b|@skip\b/g,
10422
- /\bpytest\.skip\s*\(/g,
10423
- /\bt\.Skip\s*\(|\bt\.SkipNow\s*\(/g,
10424
- // go
10425
- /@Disabled\b|@Ignore\b/g,
10426
- // junit / kotlin
10427
- /\.only\s*\(/g
10428
- // test.only leaks CI coverage
10429
- ];
10430
- var TAUTOLOGY_RES = [
10431
- /\bassert\s+True\b|\bassert\s+1\b|\bassert\s+not\s+False\b/g,
10432
- // python
10433
- /\bassert\s*\(\s*true\s*\)|\bassert\.ok\s*\(\s*true\s*\)/gi,
10434
- // node
10435
- /\bexpect\s*\(\s*true\s*\)\s*\.\s*to(?:Be|Equal|BeTruthy)\s*\(\s*true\s*\)?/gi,
10436
- // jest
10437
- /\bexpect\s*\(\s*(\w+)\s*\)\s*\.\s*toBe\s*\(\s*\1\s*\)/g,
10438
- // expect(x).toBe(x)
10439
- /\bassert_eq!\s*\(\s*true\s*,\s*true\s*\)/g,
10440
- // rust
10441
- /\bassert!\s*\(\s*true\s*\)/g
10442
- ];
10443
- function countMatches(res, text) {
10444
- let n = 0;
10445
- for (const re of res) {
10446
- re.lastIndex = 0;
10447
- const m2 = text.match(re);
10448
- if (m2)
10449
- n += m2.length;
10738
+ exports2.MEMORY_HARD_CAP_BYTES = exports2.MEMORY_COMPACT_TRIGGER_BYTES = exports2.MEMORY_MAX_BYTES = exports2.MEMORY_ENTRY_MAX_CHARS = void 0;
10739
+ exports2.memoryFilePath = memoryFilePath;
10740
+ exports2.writeMemory = writeMemory;
10741
+ exports2.readMemory = readMemory2;
10742
+ exports2.readAllMemory = readAllMemory2;
10743
+ exports2.clearMemory = clearMemory2;
10744
+ exports2.memoryStats = memoryStats2;
10745
+ exports2.compactMemoryIfNeeded = compactMemoryIfNeeded2;
10746
+ var fs6 = __importStar(require("fs"));
10747
+ var os5 = __importStar(require("os"));
10748
+ var path5 = __importStar(require("path"));
10749
+ var crypto2 = __importStar(require("crypto"));
10750
+ var MEMORY_ROOT = process.env.NEXRALL_MEMORY_DIR || path5.join(os5.homedir(), ".nexrall", "memory");
10751
+ var GLOBAL_FILE = path5.join(MEMORY_ROOT, "global.md");
10752
+ function memoryFilePath(scope, workDir) {
10753
+ if (scope === "global" || !workDir)
10754
+ return GLOBAL_FILE;
10755
+ const key = crypto2.createHash("sha1").update(path5.resolve(workDir)).digest("hex").slice(0, 16);
10756
+ return path5.join(MEMORY_ROOT, `project-${key}.md`);
10757
+ }
10758
+ exports2.MEMORY_ENTRY_MAX_CHARS = 400;
10759
+ exports2.MEMORY_MAX_BYTES = 12e3;
10760
+ exports2.MEMORY_COMPACT_TRIGGER_BYTES = 16e3;
10761
+ exports2.MEMORY_HARD_CAP_BYTES = 4 * exports2.MEMORY_COMPACT_TRIGGER_BYTES;
10762
+ var FINGERPRINT_LEN = 60;
10763
+ var _locks = /* @__PURE__ */ new Map();
10764
+ async function withLock(key, fn) {
10765
+ const prev = _locks.get(key) ?? Promise.resolve();
10766
+ let release2;
10767
+ const next = new Promise((res) => {
10768
+ release2 = res;
10769
+ });
10770
+ _locks.set(key, prev.then(() => next));
10771
+ try {
10772
+ await prev;
10773
+ return await fn();
10774
+ } finally {
10775
+ release2();
10776
+ if (_locks.get(key) === next)
10777
+ _locks.delete(key);
10450
10778
  }
10451
- return n;
10452
10779
  }
10453
- function countCommentedOutTestLines(oldText, newText) {
10454
- const oldLines = new Set(oldText.split("\n").map((l) => l.trim()));
10455
- let n = 0;
10456
- for (const raw of newText.split("\n")) {
10457
- const line = raw.trim();
10458
- const isComment = /^(?:\/\/|#|\/\*|\*)/.test(line);
10459
- if (!isComment)
10460
- continue;
10461
- const body = line.replace(/^(?:\/\/+|#+|\/\*+|\*+)\s?/, "");
10462
- const looksLikeTest = /\b(expect|assert|it\(|test\(|def test_|func Test|EXPECT_|ASSERT_)\b/.test(body);
10463
- if (looksLikeTest && oldLines.has(body))
10464
- n += 1;
10780
+ function readMemoryFile(file) {
10781
+ try {
10782
+ return fs6.readFileSync(file, "utf-8");
10783
+ } catch {
10784
+ return "";
10465
10785
  }
10466
- return n;
10467
10786
  }
10468
- function analyzeTestEdit(path5, oldText, newText) {
10469
- if (!isTestFile(path5)) {
10470
- return { isTestFile: false, suspicious: false, findings: [] };
10787
+ function writeMemoryFile(file, content) {
10788
+ fs6.mkdirSync(path5.dirname(file), { recursive: true });
10789
+ fs6.writeFileSync(file, content, "utf-8");
10790
+ }
10791
+ function evictOldest(content, maxBytes) {
10792
+ if (Buffer.byteLength(content, "utf-8") <= maxBytes)
10793
+ return content;
10794
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
10795
+ while (lines.length > 1 && Buffer.byteLength(lines.join("\n"), "utf-8") > maxBytes)
10796
+ lines.shift();
10797
+ return "\n" + lines.join("\n");
10798
+ }
10799
+ async function writeMemory(content, scope, workDir) {
10800
+ let trimmed = content.trim();
10801
+ if (!trimmed)
10802
+ return { ok: false, already: false, scope, file: "" };
10803
+ if (trimmed.length > exports2.MEMORY_ENTRY_MAX_CHARS)
10804
+ trimmed = trimmed.slice(0, exports2.MEMORY_ENTRY_MAX_CHARS - 1).trimEnd() + "\u2026";
10805
+ const file = memoryFilePath(scope, workDir);
10806
+ return withLock(file, async () => {
10807
+ const existing = readMemoryFile(file);
10808
+ const fingerprint = trimmed.toLowerCase().slice(0, FINGERPRINT_LEN);
10809
+ if (existing.toLowerCase().includes(fingerprint)) {
10810
+ return { ok: true, already: true, scope, file };
10811
+ }
10812
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10813
+ let next = existing + `
10814
+ - [${today}] ${trimmed}`;
10815
+ if (Buffer.byteLength(next, "utf-8") > exports2.MEMORY_HARD_CAP_BYTES) {
10816
+ next = evictOldest(next, exports2.MEMORY_MAX_BYTES);
10817
+ }
10818
+ writeMemoryFile(file, next);
10819
+ return { ok: true, already: false, scope, file };
10820
+ });
10821
+ }
10822
+ function readMemory2(scope, workDir) {
10823
+ const file = memoryFilePath(scope, workDir);
10824
+ return readMemoryFile(file).trim();
10825
+ }
10826
+ function readAllMemory2(workDir) {
10827
+ const parts = [];
10828
+ const globalMem = readMemory2("global");
10829
+ if (globalMem)
10830
+ parts.push(`[Global memories \u2014 apply to every project]
10831
+ ${globalMem}`);
10832
+ if (workDir) {
10833
+ const projMem = readMemory2("project", workDir);
10834
+ if (projMem)
10835
+ parts.push(`[Project memories \u2014 ${path5.basename(path5.resolve(workDir))}]
10836
+ ${projMem}`);
10837
+ }
10838
+ return parts.join("\n\n");
10839
+ }
10840
+ function clearMemory2(scope, workDir) {
10841
+ const file = memoryFilePath(scope, workDir);
10842
+ try {
10843
+ fs6.rmSync(file, { force: true });
10844
+ } catch {
10471
10845
  }
10472
- const findings = [];
10473
- const skipsBefore = countMatches(SKIP_RES, oldText);
10474
- const skipsAfter = countMatches(SKIP_RES, newText);
10475
- if (skipsAfter > skipsBefore) {
10476
- findings.push({
10477
- kind: "skip-added",
10478
- reason: `added ${skipsAfter - skipsBefore} skip/only/disabled marker(s) \u2014 tests are being skipped, not fixed`
10479
- });
10846
+ }
10847
+ function memoryStats2(scope, workDir) {
10848
+ const file = memoryFilePath(scope, workDir);
10849
+ const content = readMemoryFile(file);
10850
+ const entries = content.split("\n").filter((l) => /^-\s\[\d{4}-\d{2}-\d{2}\]/.test(l)).length;
10851
+ return { file, bytes: Buffer.byteLength(content, "utf-8"), entries };
10852
+ }
10853
+ async function compactMemoryIfNeeded2(scope, workDir, summarize) {
10854
+ const file = memoryFilePath(scope, workDir);
10855
+ return withLock(file, async () => {
10856
+ const content = readMemoryFile(file);
10857
+ if (Buffer.byteLength(content, "utf-8") <= exports2.MEMORY_COMPACT_TRIGGER_BYTES)
10858
+ return false;
10859
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
10860
+ if (lines.length < 8)
10861
+ return false;
10862
+ const cut = Math.floor(lines.length / 2);
10863
+ const older = lines.slice(0, cut);
10864
+ const recent = lines.slice(cut);
10865
+ const prompt2 = `Consolidate these persistent-memory bullet entries into a shorter set of bullets, merging duplicates/near-duplicates and dropping anything clearly stale or superseded by a later entry. Keep each resulting bullet under ${exports2.MEMORY_ENTRY_MAX_CHARS} characters, one fact per line, prefixed "- [YYYY-MM-DD] " using the LATEST date among the entries it draws from. Output ONLY the bullet lines, nothing else.
10866
+
10867
+ ${older.join("\n")}`;
10868
+ let summarized;
10869
+ try {
10870
+ summarized = (await summarize(prompt2)).trim();
10871
+ } catch {
10872
+ return false;
10873
+ }
10874
+ if (!summarized)
10875
+ return false;
10876
+ const next = summarized + "\n" + recent.join("\n");
10877
+ writeMemoryFile(file, Buffer.byteLength(next, "utf-8") > exports2.MEMORY_MAX_BYTES ? evictOldest(next, exports2.MEMORY_MAX_BYTES) : next);
10878
+ return true;
10879
+ });
10880
+ }
10881
+ }
10882
+ });
10883
+
10884
+ // ../core/dist/plugins/index.js
10885
+ var require_plugins = __commonJS({
10886
+ "../core/dist/plugins/index.js"(exports2) {
10887
+ "use strict";
10888
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m2, k, k2) {
10889
+ if (k2 === void 0)
10890
+ k2 = k;
10891
+ var desc = Object.getOwnPropertyDescriptor(m2, k);
10892
+ if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
10893
+ desc = { enumerable: true, get: function() {
10894
+ return m2[k];
10895
+ } };
10896
+ }
10897
+ Object.defineProperty(o, k2, desc);
10898
+ } : function(o, m2, k, k2) {
10899
+ if (k2 === void 0)
10900
+ k2 = k;
10901
+ o[k2] = m2[k];
10902
+ });
10903
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? function(o, v) {
10904
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
10905
+ } : function(o, v) {
10906
+ o["default"] = v;
10907
+ });
10908
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ function() {
10909
+ var ownKeys = function(o) {
10910
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
10911
+ var ar = [];
10912
+ for (var k in o2)
10913
+ if (Object.prototype.hasOwnProperty.call(o2, k))
10914
+ ar[ar.length] = k;
10915
+ return ar;
10916
+ };
10917
+ return ownKeys(o);
10918
+ };
10919
+ return function(mod) {
10920
+ if (mod && mod.__esModule)
10921
+ return mod;
10922
+ var result = {};
10923
+ if (mod != null) {
10924
+ for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
10925
+ if (k[i2] !== "default")
10926
+ __createBinding(result, mod, k[i2]);
10927
+ }
10928
+ __setModuleDefault(result, mod);
10929
+ return result;
10930
+ };
10931
+ }();
10932
+ Object.defineProperty(exports2, "__esModule", { value: true });
10933
+ exports2.loadPlugins = loadPlugins3;
10934
+ exports2.pluginAssetDirs = pluginAssetDirs;
10935
+ exports2.pluginHooks = pluginHooks;
10936
+ exports2.pluginMcpServers = pluginMcpServers;
10937
+ var fs6 = __importStar(require("fs"));
10938
+ var path5 = __importStar(require("path"));
10939
+ var os5 = __importStar(require("os"));
10940
+ function readMeta(dir) {
10941
+ try {
10942
+ const raw = fs6.readFileSync(path5.join(dir, "plugin.json"), "utf-8");
10943
+ const j = JSON.parse(raw);
10944
+ return {
10945
+ name: typeof j.name === "string" ? j.name : void 0,
10946
+ version: typeof j.version === "string" ? j.version : void 0,
10947
+ description: typeof j.description === "string" ? j.description : void 0
10948
+ };
10949
+ } catch {
10950
+ return {};
10951
+ }
10952
+ }
10953
+ function scanRoot(root, scope, into) {
10954
+ let entries;
10955
+ try {
10956
+ entries = fs6.readdirSync(root, { withFileTypes: true });
10957
+ } catch {
10958
+ return;
10959
+ }
10960
+ for (const entry of entries) {
10961
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
10962
+ continue;
10963
+ const dir = path5.join(root, entry.name);
10964
+ try {
10965
+ if (!fs6.statSync(dir).isDirectory())
10966
+ continue;
10967
+ } catch {
10968
+ continue;
10969
+ }
10970
+ const meta = readMeta(dir);
10971
+ const name = meta.name || entry.name;
10972
+ if (into.has(name))
10973
+ continue;
10974
+ into.set(name, { name, version: meta.version, description: meta.description, dir, scope });
10975
+ }
10976
+ }
10977
+ function loadPlugins3(workDir) {
10978
+ const out = /* @__PURE__ */ new Map();
10979
+ scanRoot(path5.join(workDir, ".nexrall", "plugins"), "project", out);
10980
+ scanRoot(path5.join(os5.homedir(), ".nexrall", "plugins"), "global", out);
10981
+ return [...out.values()];
10982
+ }
10983
+ function pluginAssetDirs(workDir, kind) {
10984
+ return loadPlugins3(workDir).map((p) => path5.join(p.dir, kind)).filter((d) => {
10985
+ try {
10986
+ return fs6.statSync(d).isDirectory();
10987
+ } catch {
10988
+ return false;
10989
+ }
10990
+ });
10991
+ }
10992
+ function pluginHooks(workDir) {
10993
+ const merged = {};
10994
+ for (const p of loadPlugins3(workDir)) {
10995
+ try {
10996
+ const raw = fs6.readFileSync(path5.join(p.dir, "hooks.json"), "utf-8");
10997
+ const j = JSON.parse(raw);
10998
+ const hooks = j.hooks ?? j;
10999
+ for (const [phase, entries] of Object.entries(hooks)) {
11000
+ if (!Array.isArray(entries))
11001
+ continue;
11002
+ merged[phase] = [...merged[phase] ?? [], ...entries];
11003
+ }
11004
+ } catch {
11005
+ }
11006
+ }
11007
+ return merged;
11008
+ }
11009
+ function pluginMcpServers(workDir) {
11010
+ const merged = {};
11011
+ for (const p of loadPlugins3(workDir)) {
11012
+ try {
11013
+ const raw = fs6.readFileSync(path5.join(p.dir, "mcp.json"), "utf-8");
11014
+ const j = JSON.parse(raw);
11015
+ const servers = j.mcpServers ?? j;
11016
+ for (const [name, cfg] of Object.entries(servers)) {
11017
+ if (!(name in merged))
11018
+ merged[name] = cfg;
11019
+ }
11020
+ } catch {
11021
+ }
11022
+ }
11023
+ return merged;
11024
+ }
11025
+ }
11026
+ });
11027
+
11028
+ // ../core/dist/commands/loader.js
11029
+ var require_loader = __commonJS({
11030
+ "../core/dist/commands/loader.js"(exports2) {
11031
+ "use strict";
11032
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m2, k, k2) {
11033
+ if (k2 === void 0)
11034
+ k2 = k;
11035
+ var desc = Object.getOwnPropertyDescriptor(m2, k);
11036
+ if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
11037
+ desc = { enumerable: true, get: function() {
11038
+ return m2[k];
11039
+ } };
11040
+ }
11041
+ Object.defineProperty(o, k2, desc);
11042
+ } : function(o, m2, k, k2) {
11043
+ if (k2 === void 0)
11044
+ k2 = k;
11045
+ o[k2] = m2[k];
11046
+ });
11047
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? function(o, v) {
11048
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11049
+ } : function(o, v) {
11050
+ o["default"] = v;
11051
+ });
11052
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ function() {
11053
+ var ownKeys = function(o) {
11054
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
11055
+ var ar = [];
11056
+ for (var k in o2)
11057
+ if (Object.prototype.hasOwnProperty.call(o2, k))
11058
+ ar[ar.length] = k;
11059
+ return ar;
11060
+ };
11061
+ return ownKeys(o);
11062
+ };
11063
+ return function(mod) {
11064
+ if (mod && mod.__esModule)
11065
+ return mod;
11066
+ var result = {};
11067
+ if (mod != null) {
11068
+ for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
11069
+ if (k[i2] !== "default")
11070
+ __createBinding(result, mod, k[i2]);
11071
+ }
11072
+ __setModuleDefault(result, mod);
11073
+ return result;
11074
+ };
11075
+ }();
11076
+ Object.defineProperty(exports2, "__esModule", { value: true });
11077
+ exports2.loadSlashCommands = loadSlashCommands;
11078
+ exports2.findSlashCommand = findSlashCommand;
11079
+ exports2.expandBody = expandBody;
11080
+ exports2.expandCommand = expandCommand;
11081
+ var fs6 = __importStar(require("fs"));
11082
+ var path5 = __importStar(require("path"));
11083
+ var os5 = __importStar(require("os"));
11084
+ var child_process_1 = require("child_process");
11085
+ var index_1 = require_plugins();
11086
+ var BUILTIN_COMMANDS = [
11087
+ {
11088
+ name: "review",
11089
+ description: "Review uncommitted changes (or a PR/branch diff) for bugs and risks",
11090
+ source: "builtin",
11091
+ body: [
11092
+ "Review the following diff like a meticulous senior engineer. Target: $ARGUMENTS",
11093
+ "(If no target given, review the uncommitted working-tree changes below. If a branch or PR",
11094
+ "number is given, run the appropriate `git diff <base>...` or `gh pr diff <n>` yourself first.)",
11095
+ "",
11096
+ "Branch: !`git branch --show-current`",
11097
+ "Status: !`git status --short`",
11098
+ "",
11099
+ "Diff (uncommitted):",
11100
+ "```diff",
11101
+ "!`git diff HEAD --unified=5 --no-color | head -4000`",
11102
+ "```",
11103
+ "",
11104
+ "Review methodology:",
11105
+ "1. Read the surrounding code of every changed hunk (read_file with offset/limit) \u2014 never judge a hunk in isolation.",
11106
+ "2. Look for: correctness bugs, edge cases (empty/null/unicode/concurrency), security issues",
11107
+ " (injection, path traversal, secrets), breaking API changes (find_references / search callers),",
11108
+ " silent behaviour changes, and missing error handling.",
11109
+ "3. Check tests: do existing tests cover the change? Are assertions weakened?",
11110
+ "",
11111
+ "Output format:",
11112
+ "- \u{1F534} Critical (must fix before merge) \u2014 with file:line and a concrete fix",
11113
+ "- \u{1F7E1} Warning (should fix) \u2014 with file:line",
11114
+ "- \u{1F7E2} Suggestion (nice to have)",
11115
+ "- Verdict: APPROVE / REQUEST CHANGES with a one-paragraph summary.",
11116
+ "Do NOT modify any files \u2014 this is a read-only review."
11117
+ ].join("\n")
11118
+ }
11119
+ ];
11120
+ function parseFrontmatter(raw) {
11121
+ const m2 = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
11122
+ if (!m2)
11123
+ return { meta: {}, body: raw.trim() };
11124
+ const meta = {};
11125
+ for (const line of m2[1].split(/\r?\n/)) {
11126
+ const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
11127
+ if (kv)
11128
+ meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
11129
+ }
11130
+ return { meta, body: (m2[2] ?? "").trim() };
11131
+ }
11132
+ function loadDir(dir, source, into) {
11133
+ let files;
11134
+ try {
11135
+ files = fs6.readdirSync(dir).filter((f3) => f3.endsWith(".md"));
11136
+ } catch {
11137
+ return;
11138
+ }
11139
+ for (const file of files) {
11140
+ try {
11141
+ const raw = fs6.readFileSync(path5.join(dir, file), "utf-8");
11142
+ const { meta, body } = parseFrontmatter(raw);
11143
+ const name = (meta.name || path5.basename(file, ".md")).trim().toLowerCase();
11144
+ if (!name)
11145
+ continue;
11146
+ if (source !== "project" && into.has(name))
11147
+ continue;
11148
+ const model = ["turbo", "pro", "ultra"].find((x2) => x2 === (meta.model ?? "").toLowerCase());
11149
+ into.set(name, {
11150
+ name,
11151
+ description: meta.description || `Custom /${name} command`,
11152
+ model,
11153
+ mode: meta.mode || void 0,
11154
+ body,
11155
+ source
11156
+ });
11157
+ } catch {
11158
+ }
11159
+ }
11160
+ }
11161
+ function loadSlashCommands(workDir) {
11162
+ const out = /* @__PURE__ */ new Map();
11163
+ loadDir(path5.join(workDir, ".nexrall", "commands"), "project", out);
11164
+ loadDir(path5.join(os5.homedir(), ".nexrall", "commands"), "global", out);
11165
+ for (const dir of (0, index_1.pluginAssetDirs)(workDir, "commands"))
11166
+ loadDir(dir, "plugin", out);
11167
+ for (const cmd of BUILTIN_COMMANDS) {
11168
+ if (!out.has(cmd.name))
11169
+ out.set(cmd.name, cmd);
11170
+ }
11171
+ return [...out.values()];
11172
+ }
11173
+ function findSlashCommand(cmds, name) {
11174
+ const want = name.replace(/^\//, "").trim().toLowerCase();
11175
+ return cmds.find((c) => c.name === want);
11176
+ }
11177
+ function expandBody(body, argString, workDir) {
11178
+ const args = argString.trim();
11179
+ const positional = args ? args.split(/\s+/) : [];
11180
+ let out = body;
11181
+ out = out.replace(/!`([^`]+)`/g, (_m, c) => {
11182
+ try {
11183
+ const stdout = (0, child_process_1.execSync)(c, { cwd: workDir, encoding: "utf-8", timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] });
11184
+ return stdout.trim();
11185
+ } catch (err) {
11186
+ return `[command failed: ${c} \u2014 ${err.message}]`;
11187
+ }
11188
+ });
11189
+ out = out.replace(/(^|\s)@([^\s]+)/g, (_m, lead, rel) => {
11190
+ const abs = path5.isAbsolute(rel) ? rel : path5.join(workDir, rel);
11191
+ try {
11192
+ const content = fs6.readFileSync(abs, "utf-8").slice(0, 12e3);
11193
+ return `${lead}
11194
+ [File: ${rel}]
11195
+ \`\`\`
11196
+ ${content}
11197
+ \`\`\`
11198
+ `;
11199
+ } catch {
11200
+ return `${lead}[missing file: ${rel}]`;
11201
+ }
11202
+ });
11203
+ out = out.replace(/\$(\d+)/g, (_m, n) => positional[Number(n) - 1] ?? "");
11204
+ const hadArgsToken = /\$ARGUMENTS/.test(out);
11205
+ out = out.replace(/\$ARGUMENTS/g, args);
11206
+ if (!hadArgsToken && !/\$\d+/.test(body) && args) {
11207
+ out = `${out}
11208
+
11209
+ ${args}`;
11210
+ }
11211
+ return out.trim();
11212
+ }
11213
+ function expandCommand(cmd, argString, workDir) {
11214
+ return expandBody(cmd.body, argString, workDir);
11215
+ }
11216
+ }
11217
+ });
11218
+
11219
+ // ../core/dist/agent/skills.js
11220
+ var require_skills = __commonJS({
11221
+ "../core/dist/agent/skills.js"(exports2) {
11222
+ "use strict";
11223
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m2, k, k2) {
11224
+ if (k2 === void 0)
11225
+ k2 = k;
11226
+ var desc = Object.getOwnPropertyDescriptor(m2, k);
11227
+ if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
11228
+ desc = { enumerable: true, get: function() {
11229
+ return m2[k];
11230
+ } };
11231
+ }
11232
+ Object.defineProperty(o, k2, desc);
11233
+ } : function(o, m2, k, k2) {
11234
+ if (k2 === void 0)
11235
+ k2 = k;
11236
+ o[k2] = m2[k];
11237
+ });
11238
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? function(o, v) {
11239
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11240
+ } : function(o, v) {
11241
+ o["default"] = v;
11242
+ });
11243
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ function() {
11244
+ var ownKeys = function(o) {
11245
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
11246
+ var ar = [];
11247
+ for (var k in o2)
11248
+ if (Object.prototype.hasOwnProperty.call(o2, k))
11249
+ ar[ar.length] = k;
11250
+ return ar;
11251
+ };
11252
+ return ownKeys(o);
11253
+ };
11254
+ return function(mod) {
11255
+ if (mod && mod.__esModule)
11256
+ return mod;
11257
+ var result = {};
11258
+ if (mod != null) {
11259
+ for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
11260
+ if (k[i2] !== "default")
11261
+ __createBinding(result, mod, k[i2]);
11262
+ }
11263
+ __setModuleDefault(result, mod);
11264
+ return result;
11265
+ };
11266
+ }();
11267
+ Object.defineProperty(exports2, "__esModule", { value: true });
11268
+ exports2.loadSkills = loadSkills2;
11269
+ exports2.findSkill = findSkill2;
11270
+ exports2.autoInvokableSkills = autoInvokableSkills;
11271
+ exports2.userInvokableSkills = userInvokableSkills2;
11272
+ exports2.summariseSkills = summariseSkills;
11273
+ exports2.expandSkill = expandSkill2;
11274
+ var fs6 = __importStar(require("fs"));
11275
+ var path5 = __importStar(require("path"));
11276
+ var os5 = __importStar(require("os"));
11277
+ var index_1 = require_plugins();
11278
+ var loader_1 = require_loader();
11279
+ function parseFrontmatter(raw) {
11280
+ const m2 = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
11281
+ if (!m2)
11282
+ return { meta: {}, body: raw.trim() };
11283
+ const meta = {};
11284
+ for (const line of m2[1].split(/\r?\n/)) {
11285
+ const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
11286
+ if (kv)
11287
+ meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
11288
+ }
11289
+ return { meta, body: (m2[2] ?? "").trim() };
11290
+ }
11291
+ function parseBool(v, fallback) {
11292
+ if (v === void 0)
11293
+ return fallback;
11294
+ const s2 = v.trim().toLowerCase();
11295
+ if (s2 === "true" || s2 === "1" || s2 === "yes")
11296
+ return true;
11297
+ if (s2 === "false" || s2 === "0" || s2 === "no")
11298
+ return false;
11299
+ return fallback;
11300
+ }
11301
+ function toSkill(meta, body, name, source, dir) {
11302
+ const model = ["turbo", "pro", "ultra"].find((x2) => x2 === (meta.model ?? "").toLowerCase());
11303
+ return {
11304
+ name,
11305
+ description: meta.description || `Custom /${name} skill`,
11306
+ model,
11307
+ mode: meta.mode || void 0,
11308
+ body,
11309
+ source,
11310
+ dir,
11311
+ disableModelInvocation: parseBool(meta["disable-model-invocation"], false),
11312
+ userInvocable: parseBool(meta["user-invocable"], true)
11313
+ };
11314
+ }
11315
+ function loadFlatCommandDir(dir, source, into) {
11316
+ let files;
11317
+ try {
11318
+ files = fs6.readdirSync(dir).filter((f3) => f3.endsWith(".md"));
11319
+ } catch {
11320
+ return;
11321
+ }
11322
+ for (const file of files) {
11323
+ try {
11324
+ const raw = fs6.readFileSync(path5.join(dir, file), "utf-8");
11325
+ const { meta, body } = parseFrontmatter(raw);
11326
+ const name = (meta.name || path5.basename(file, ".md")).trim().toLowerCase();
11327
+ if (!name)
11328
+ continue;
11329
+ if (source !== "project" && into.has(name))
11330
+ continue;
11331
+ into.set(name, toSkill(meta, body, name, source));
11332
+ } catch {
11333
+ }
11334
+ }
11335
+ }
11336
+ function loadSkillDir(root, source, into) {
11337
+ let entries;
11338
+ try {
11339
+ entries = fs6.readdirSync(root, { withFileTypes: true });
11340
+ } catch {
11341
+ return;
11342
+ }
11343
+ for (const entry of entries) {
11344
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
11345
+ continue;
11346
+ const skillDir = path5.join(root, entry.name);
11347
+ const skillFile = path5.join(skillDir, "SKILL.md");
11348
+ try {
11349
+ if (!fs6.statSync(skillFile).isFile())
11350
+ continue;
11351
+ } catch {
11352
+ continue;
11353
+ }
11354
+ try {
11355
+ const raw = fs6.readFileSync(skillFile, "utf-8");
11356
+ const { meta, body } = parseFrontmatter(raw);
11357
+ const name = (meta.name || entry.name).trim().toLowerCase();
11358
+ if (!name)
11359
+ continue;
11360
+ if (source !== "project" && into.has(name))
11361
+ continue;
11362
+ into.set(name, toSkill(meta, body, name, source, skillDir));
11363
+ } catch {
11364
+ }
11365
+ }
11366
+ }
11367
+ var BUILTIN_SKILLS = [
11368
+ {
11369
+ name: "review",
11370
+ description: "Review uncommitted changes (or a PR/branch diff) for correctness bugs, edge cases, and security issues. Use when the user asks to review a diff, check their changes before committing, or audit a PR.",
11371
+ source: "builtin",
11372
+ disableModelInvocation: false,
11373
+ userInvocable: true,
11374
+ body: [
11375
+ "Review the following diff like a meticulous senior engineer. Target: $ARGUMENTS",
11376
+ "(If no target given, review the uncommitted working-tree changes below. If a branch or PR",
11377
+ "number is given, run the appropriate `git diff <base>...` or `gh pr diff <n>` yourself first.)",
11378
+ "",
11379
+ "Branch: !`git branch --show-current`",
11380
+ "Status: !`git status --short`",
11381
+ "",
11382
+ "Diff (uncommitted):",
11383
+ "```diff",
11384
+ "!`git diff HEAD --unified=5 --no-color | head -4000`",
11385
+ "```",
11386
+ "",
11387
+ "Review methodology:",
11388
+ "1. Read the surrounding code of every changed hunk (read_file with offset/limit) \u2014 never judge a hunk in isolation.",
11389
+ "2. Look for: correctness bugs, edge cases (empty/null/unicode/concurrency), security issues",
11390
+ " (injection, path traversal, secrets), breaking API changes (find_references / search callers),",
11391
+ " silent behaviour changes, and missing error handling.",
11392
+ "3. Check tests: do existing tests cover the change? Are assertions weakened?",
11393
+ "",
11394
+ "Output format:",
11395
+ "- \u{1F534} Critical (must fix before merge) \u2014 with file:line and a concrete fix",
11396
+ "- \u{1F7E1} Warning (should fix) \u2014 with file:line",
11397
+ "- \u{1F7E2} Suggestion (nice to have)",
11398
+ "- Verdict: APPROVE / REQUEST CHANGES with a one-paragraph summary.",
11399
+ "Do NOT modify any files \u2014 this is a read-only review."
11400
+ ].join("\n")
11401
+ }
11402
+ ];
11403
+ function loadSkills2(workDir) {
11404
+ const out = /* @__PURE__ */ new Map();
11405
+ loadFlatCommandDir(path5.join(workDir, ".nexrall", "commands"), "project", out);
11406
+ loadSkillDir(path5.join(workDir, ".nexrall", "skills"), "project", out);
11407
+ loadFlatCommandDir(path5.join(os5.homedir(), ".nexrall", "commands"), "global", out);
11408
+ loadSkillDir(path5.join(os5.homedir(), ".nexrall", "skills"), "global", out);
11409
+ for (const dir of (0, index_1.pluginAssetDirs)(workDir, "commands"))
11410
+ loadFlatCommandDir(dir, "plugin", out);
11411
+ for (const dir of (0, index_1.pluginAssetDirs)(workDir, "skills"))
11412
+ loadSkillDir(dir, "plugin", out);
11413
+ for (const skill of BUILTIN_SKILLS) {
11414
+ if (!out.has(skill.name))
11415
+ out.set(skill.name, skill);
11416
+ }
11417
+ return [...out.values()];
11418
+ }
11419
+ function findSkill2(skills, name) {
11420
+ const want = name.replace(/^\//, "").trim().toLowerCase();
11421
+ return skills.find((s2) => s2.name === want);
11422
+ }
11423
+ function autoInvokableSkills(skills) {
11424
+ return skills.filter((s2) => !s2.disableModelInvocation);
11425
+ }
11426
+ function userInvokableSkills2(skills) {
11427
+ return skills.filter((s2) => s2.userInvocable);
11428
+ }
11429
+ function summariseSkills(skills) {
11430
+ const list = autoInvokableSkills(skills);
11431
+ if (!list.length)
11432
+ return "";
11433
+ return list.map((s2) => `- ${s2.name}: ${s2.description}`).join("\n");
11434
+ }
11435
+ function expandSkill2(skill, argString, workDir) {
11436
+ const expanded = (0, loader_1.expandBody)(skill.body, argString, workDir);
11437
+ if (!skill.dir)
11438
+ return expanded;
11439
+ let siblings = [];
11440
+ try {
11441
+ siblings = fs6.readdirSync(skill.dir).filter((f3) => f3 !== "SKILL.md");
11442
+ } catch {
11443
+ }
11444
+ if (!siblings.length)
11445
+ return expanded;
11446
+ return `[Skill "${skill.name}" \u2014 supporting files available in ${skill.dir} (use read_file with an absolute path to load any of these only if the instructions below need them): ${siblings.join(", ")}]
11447
+
11448
+ ${expanded}`;
11449
+ }
11450
+ }
11451
+ });
11452
+
11453
+ // ../core/dist/agent/testIntegrity.js
11454
+ var require_testIntegrity = __commonJS({
11455
+ "../core/dist/agent/testIntegrity.js"(exports2) {
11456
+ "use strict";
11457
+ Object.defineProperty(exports2, "__esModule", { value: true });
11458
+ exports2.isTestFile = isTestFile;
11459
+ exports2.analyzeTestEdit = analyzeTestEdit;
11460
+ exports2.analyzeWriteToolForTestIntegrity = analyzeWriteToolForTestIntegrity;
11461
+ exports2.encodeTestIntegrityMarker = encodeTestIntegrityMarker;
11462
+ exports2.decodeTestIntegrityMarker = decodeTestIntegrityMarker;
11463
+ exports2.stripTestIntegrityMarker = stripTestIntegrityMarker;
11464
+ var TEST_PATH_RE = /(?:^|[\/\\])(?:tests?|spec|__tests__|testing)[\/\\]|(?:\.|_|-)(?:test|spec|tests)\.[a-z]+$|(?:^|[\/\\])test_[^\/\\]+\.py$|(?:^|[\/\\])[^\/\\]+_test\.(?:go|py|rb)$|Test[^\/\\]*\.(?:java|kt|cs)$|(?:^|[\/\\])[^\/\\]*Tests?\.(?:java|kt|cs)$/i;
11465
+ function isTestFile(path5) {
11466
+ if (!path5 || typeof path5 !== "string")
11467
+ return false;
11468
+ return TEST_PATH_RE.test(path5);
11469
+ }
11470
+ var ASSERTION_RES = [
11471
+ /\bexpect\s*\(/g,
11472
+ // jest / chai / vitest / jasmine
11473
+ /\bassert(?:Equals?|True|False|That|Null|NotNull|Same)?\b/g,
11474
+ // junit / python unittest / generic
11475
+ /\bassert\s*[.(]/g,
11476
+ // node:assert (assert.equal / assert( ) )
11477
+ /\.should\b/g,
11478
+ // chai should / rspec should
11479
+ /\bshould\s*[.(]/g,
11480
+ /\bEXPECT_[A-Z]+\s*\(/g,
11481
+ // gtest
11482
+ /\bASSERT_[A-Z]+\s*\(/g,
11483
+ // gtest
11484
+ /\brequire\.\w+\s*\(/g,
11485
+ // testify require
11486
+ /\bassert\.\w+\s*\(/g,
11487
+ // testify assert
11488
+ /\bt\.(?:Error|Fatal|Errorf|Fatalf)\b/g,
11489
+ // go testing
11490
+ /\bassert!\s*\(|\bassert_eq!\s*\(|\bassert_ne!\s*\(/g
11491
+ // rust
11492
+ ];
11493
+ var TESTCASE_RES = [
11494
+ /\b(?:it|test)\s*\(/g,
11495
+ // jest / mocha / jasmine
11496
+ /\bdef\s+test_\w+/g,
11497
+ // python
11498
+ /\bfunc\s+Test\w+/g,
11499
+ // go
11500
+ /@Test\b/g,
11501
+ // junit
11502
+ /\bfn\s+\w*test\w*\s*\(/gi
11503
+ // rust (best-effort)
11504
+ ];
11505
+ var SKIP_RES = [
11506
+ /\b(?:it|test|describe|context|suite)\s*\.\s*(?:skip|only)\b/g,
11507
+ /\bx(?:it|describe|test|context)\s*\(/g,
11508
+ // xit / xdescribe
11509
+ /\bf(?:it|describe)\s*\(/g,
11510
+ // fit / fdescribe (focus)
11511
+ /@pytest\.mark\.skip\b|@pytest\.mark\.skipif\b|@unittest\.skip\b|@skip\b/g,
11512
+ /\bpytest\.skip\s*\(/g,
11513
+ /\bt\.Skip\s*\(|\bt\.SkipNow\s*\(/g,
11514
+ // go
11515
+ /@Disabled\b|@Ignore\b/g,
11516
+ // junit / kotlin
11517
+ /\.only\s*\(/g
11518
+ // test.only leaks CI coverage
11519
+ ];
11520
+ var TAUTOLOGY_RES = [
11521
+ /\bassert\s+True\b|\bassert\s+1\b|\bassert\s+not\s+False\b/g,
11522
+ // python
11523
+ /\bassert\s*\(\s*true\s*\)|\bassert\.ok\s*\(\s*true\s*\)/gi,
11524
+ // node
11525
+ /\bexpect\s*\(\s*true\s*\)\s*\.\s*to(?:Be|Equal|BeTruthy)\s*\(\s*true\s*\)?/gi,
11526
+ // jest
11527
+ /\bexpect\s*\(\s*(\w+)\s*\)\s*\.\s*toBe\s*\(\s*\1\s*\)/g,
11528
+ // expect(x).toBe(x)
11529
+ /\bassert_eq!\s*\(\s*true\s*,\s*true\s*\)/g,
11530
+ // rust
11531
+ /\bassert!\s*\(\s*true\s*\)/g
11532
+ ];
11533
+ function countMatches(res, text) {
11534
+ let n = 0;
11535
+ for (const re of res) {
11536
+ re.lastIndex = 0;
11537
+ const m2 = text.match(re);
11538
+ if (m2)
11539
+ n += m2.length;
11540
+ }
11541
+ return n;
11542
+ }
11543
+ function countCommentedOutTestLines(oldText, newText) {
11544
+ const oldLines = new Set(oldText.split("\n").map((l) => l.trim()));
11545
+ let n = 0;
11546
+ for (const raw of newText.split("\n")) {
11547
+ const line = raw.trim();
11548
+ const isComment = /^(?:\/\/|#|\/\*|\*)/.test(line);
11549
+ if (!isComment)
11550
+ continue;
11551
+ const body = line.replace(/^(?:\/\/+|#+|\/\*+|\*+)\s?/, "");
11552
+ const looksLikeTest = /\b(expect|assert|it\(|test\(|def test_|func Test|EXPECT_|ASSERT_)\b/.test(body);
11553
+ if (looksLikeTest && oldLines.has(body))
11554
+ n += 1;
11555
+ }
11556
+ return n;
11557
+ }
11558
+ function analyzeTestEdit(path5, oldText, newText) {
11559
+ if (!isTestFile(path5)) {
11560
+ return { isTestFile: false, suspicious: false, findings: [] };
11561
+ }
11562
+ const findings = [];
11563
+ const skipsBefore = countMatches(SKIP_RES, oldText);
11564
+ const skipsAfter = countMatches(SKIP_RES, newText);
11565
+ if (skipsAfter > skipsBefore) {
11566
+ findings.push({
11567
+ kind: "skip-added",
11568
+ reason: `added ${skipsAfter - skipsBefore} skip/only/disabled marker(s) \u2014 tests are being skipped, not fixed`
11569
+ });
10480
11570
  }
10481
11571
  const tautoBefore = countMatches(TAUTOLOGY_RES, oldText);
10482
11572
  const tautoAfter = countMatches(TAUTOLOGY_RES, newText);
@@ -11259,6 +12349,8 @@ var require_executor = __commonJS({
11259
12349
  var sandbox_1 = require_sandbox();
11260
12350
  var auth_1 = require_auth();
11261
12351
  var editCompleteness_1 = require_editCompleteness();
12352
+ var memory_1 = require_memory();
12353
+ var skills_1 = require_skills();
11262
12354
  var testIntegrity_1 = require_testIntegrity();
11263
12355
  var crossFile_1 = require_crossFile();
11264
12356
  var client_1 = require_client();
@@ -11268,6 +12360,10 @@ var require_executor = __commonJS({
11268
12360
  var MAX_FETCH_BYTES = 200 * 1024;
11269
12361
  var MAX_READ_BYTES = 500 * 1024;
11270
12362
  var MAX_OUTPUT_CHARS = 1e5;
12363
+ var MAX_READ_LINES = 2e3;
12364
+ var MAX_READ_TOKENS = 25e3;
12365
+ var READ_CHARS_PER_TOKEN = 4;
12366
+ var MAX_READ_CHARS = MAX_READ_TOKENS * READ_CHARS_PER_TOKEN;
11271
12367
  var MAX_SPILL_BYTES = 20 * 1024 * 1024;
11272
12368
  var SPILL_DIR = path5.join(os5.tmpdir(), "nexrall-code", "bash-output");
11273
12369
  var BLOCKED_REGEXES = [
@@ -11362,24 +12458,50 @@ var require_executor = __commonJS({
11362
12458
  return false;
11363
12459
  }
11364
12460
  }
11365
- async function readLargeFileWindow(resolved, offset, limit, kb) {
12461
+ function formatNumberedLine(offset, indexInKept, line) {
12462
+ return `${String(offset + indexInKept + 1).padStart(4, " ")} ${line}`;
12463
+ }
12464
+ async function readFileWindowed(resolved, offset, requestedLimit) {
11366
12465
  return new Promise((resolve3) => {
11367
- const endLine = offset + limit;
12466
+ const explicitLimit = requestedLimit > 0;
12467
+ const lineScanCap = explicitLimit ? Math.min(requestedLimit, MAX_READ_LINES) : MAX_READ_LINES;
12468
+ const hardEndLine = offset + lineScanCap;
11368
12469
  const kept = [];
12470
+ let keptChars = 0;
11369
12471
  let lineNo = 0;
11370
12472
  let carry = "";
11371
12473
  let stopped = false;
12474
+ let hitTokenCap = false;
12475
+ let sawEof = false;
11372
12476
  const stream = fs6.createReadStream(resolved, { encoding: "utf-8", highWaterMark: 256 * 1024 });
12477
+ const tryPushLine = (line) => {
12478
+ const formatted = formatNumberedLine(offset, kept.length, line);
12479
+ const added = formatted.length + 1;
12480
+ if (keptChars + added > MAX_READ_CHARS) {
12481
+ hitTokenCap = true;
12482
+ return false;
12483
+ }
12484
+ kept.push(line);
12485
+ keptChars += added;
12486
+ return true;
12487
+ };
11373
12488
  const finish = () => {
11374
12489
  if (stopped)
11375
12490
  return;
11376
12491
  stopped = true;
11377
12492
  stream.destroy();
12493
+ if (hitTokenCap && explicitLimit) {
12494
+ resolve3({
12495
+ error: `Requested range (offset:${offset}, limit:${requestedLimit}) is too large \u2014 exceeds ${MAX_READ_TOKENS} tokens. Pass a smaller limit, or use search_files to locate the relevant part first.`
12496
+ });
12497
+ return;
12498
+ }
11378
12499
  const first = offset + 1;
11379
12500
  const last = offset + kept.length;
11380
- const numbered = kept.map((l, i2) => `${String(offset + i2 + 1).padStart(4, " ")} ${l}`).join("\n");
11381
- const note = kept.length < limit ? ` (reached end of file at line ${last})` : ` (more lines follow \u2014 increase offset to continue)`;
11382
- resolve3({ output: `[File: ${resolved} \u2014 ${kb} KB, showing lines ${first}-${last}${note}]
12501
+ const numbered = kept.map((l, i2) => formatNumberedLine(offset, i2, l)).join("\n");
12502
+ const totalNote = sawEof ? `/${lineNo}` : "";
12503
+ const note = sawEof ? ` (reached end of file at line ${last})` : ` (more lines follow \u2014 pass offset:${last} to continue, or search_files to locate the relevant part)`;
12504
+ resolve3({ output: `[File: ${resolved} \u2014 lines ${first}-${last}${totalNote}${note}]
11383
12505
  ${numbered}` });
11384
12506
  };
11385
12507
  stream.on("data", (chunk) => {
@@ -11387,18 +12509,30 @@ ${numbered}` });
11387
12509
  const lines = text.split("\n");
11388
12510
  carry = lines.pop() ?? "";
11389
12511
  for (const line of lines) {
11390
- if (lineNo >= offset && lineNo < endLine)
11391
- kept.push(line);
12512
+ if (lineNo >= offset) {
12513
+ if (lineNo >= hardEndLine) {
12514
+ finish();
12515
+ return;
12516
+ }
12517
+ if (!tryPushLine(line)) {
12518
+ finish();
12519
+ return;
12520
+ }
12521
+ }
11392
12522
  lineNo++;
11393
- if (lineNo >= endLine) {
12523
+ }
12524
+ });
12525
+ stream.on("end", () => {
12526
+ if (stopped)
12527
+ return;
12528
+ if (carry !== "" && lineNo >= offset && lineNo < hardEndLine) {
12529
+ if (!tryPushLine(carry)) {
11394
12530
  finish();
11395
12531
  return;
11396
12532
  }
12533
+ lineNo++;
11397
12534
  }
11398
- });
11399
- stream.on("end", () => {
11400
- if (!stopped && carry !== "" && lineNo >= offset && lineNo < endLine)
11401
- kept.push(carry);
12535
+ sawEof = true;
11402
12536
  finish();
11403
12537
  });
11404
12538
  stream.on("error", (err) => {
@@ -11417,31 +12551,12 @@ ${numbered}` });
11417
12551
  return { error: "Missing required parameter: path" };
11418
12552
  try {
11419
12553
  const resolved = resolvePath(filePath, workDir);
11420
- const stat2 = fs6.statSync(resolved);
12554
+ fs6.statSync(resolved);
11421
12555
  if (isBinaryFile(resolved)) {
11422
12556
  const ext = path5.extname(resolved).toLowerCase();
11423
12557
  return { error: `Cannot read binary file: ${resolved} (${ext || "no extension"}). Use a text-based tool or convert it first.` };
11424
12558
  }
11425
- if (stat2.size > MAX_READ_BYTES) {
11426
- const kb = (stat2.size / 1024).toFixed(0);
11427
- if (limit <= 0) {
11428
- return { error: `File too large (${kb} KB, max ${MAX_READ_BYTES / 1024} KB). Pass offset + limit to read a section (e.g. {offset:0, limit:500}), or search_files to locate the relevant part first.` };
11429
- }
11430
- return await readLargeFileWindow(resolved, offset, limit, kb);
11431
- }
11432
- const content = fs6.readFileSync(resolved, "utf-8");
11433
- const allLines = content.split("\n");
11434
- const totalLines = allLines.length;
11435
- const startLine = offset;
11436
- const endLine = limit > 0 ? Math.min(startLine + limit, totalLines) : totalLines;
11437
- const slice = allLines.slice(startLine, endLine);
11438
- const numbered = slice.map((l, i2) => {
11439
- const lineNo = String(startLine + i2 + 1).padStart(4, " ");
11440
- return `${lineNo} ${l}`;
11441
- }).join("\n");
11442
- const rangeNote = offset > 0 || limit > 0 ? ` lines ${startLine + 1}-${endLine}/${totalLines}` : ` ${totalLines} lines`;
11443
- return { output: `[File: ${resolved} (${rangeNote})]
11444
- ${numbered}` };
12559
+ return await readFileWindowed(resolved, offset, limit);
11445
12560
  } catch (err) {
11446
12561
  return { error: err.message };
11447
12562
  }
@@ -11977,7 +13092,9 @@ Update these call-sites (or restore the symbol), then run a build/typecheck to c
11977
13092
  walkDir(resolved, (filePath) => {
11978
13093
  const name = path5.basename(filePath);
11979
13094
  const nameLower = name.toLowerCase();
11980
- if (nameLower.includes(patternLower) || matchesGlob(name, pattern) || matchesPattern(name, pattern)) {
13095
+ const rel = path5.relative(resolved, filePath).replace(/\\/g, "/");
13096
+ const relLower = rel.toLowerCase();
13097
+ if (nameLower.includes(patternLower) || matchesGlob(name, pattern) || matchesPattern(name, pattern) || relLower.includes(patternLower) || matchesGlob(rel, pattern) || matchesPattern(rel, pattern)) {
11981
13098
  matches.push(filePath);
11982
13099
  }
11983
13100
  });
@@ -12002,7 +13119,7 @@ Update these call-sites (or restore the symbol), then run a build/typecheck to c
12002
13119
  args.push("-m", "200", "--regexp", pattern, resolved);
12003
13120
  result = (0, child_process_1.spawnSync)("rg", args, spawnOpts);
12004
13121
  } else {
12005
- const args = ["-rn", "--binary-files=without-match", "--color=never"];
13122
+ const args = ["-rnE", "--binary-files=without-match", "--color=never"];
12006
13123
  if (ignoreCase)
12007
13124
  args.push("-i");
12008
13125
  if (contextLines > 0)
@@ -12031,8 +13148,10 @@ ${globalCapMatches(output)}` : "";
12031
13148
  }
12032
13149
  if (result.status === 1 && !output)
12033
13150
  return { output: "No matches found." };
12034
- if (result.status !== 0 && result.status !== 1)
12035
- return { error: stderr || "search failed" };
13151
+ if (result.status !== 0 && result.status !== 1) {
13152
+ const hint = /parenthes|bracket|brace|Unmatched|repetition-operator|invalid regex/i.test(stderr) ? ' \u2014 the pattern has invalid/unbalanced regex syntax. If you meant to match literal parentheses/brackets, escape them (e.g. "\\(", "\\)"), or simplify the pattern.' : "";
13153
+ return { error: (stderr || "search failed").trim() + hint };
13154
+ }
12036
13155
  output = globalCapMatches(output);
12037
13156
  return { output: output || "No matches found." };
12038
13157
  }
@@ -12373,6 +13492,14 @@ ${diff2}${xfile}` };
12373
13492
  return callback(null, address, family);
12374
13493
  });
12375
13494
  };
13495
+ let settled = false;
13496
+ let selfAborted = false;
13497
+ const finish = (result) => {
13498
+ if (settled)
13499
+ return;
13500
+ settled = true;
13501
+ resolve3(result);
13502
+ };
12376
13503
  const req = transport.get(url, {
12377
13504
  timeout: DEFAULT_TIMEOUT_MS,
12378
13505
  lookup: guardedLookup,
@@ -12385,15 +13512,16 @@ ${diff2}${xfile}` };
12385
13512
  }, (res) => {
12386
13513
  const status = res.statusCode ?? 0;
12387
13514
  if ((status === 301 || status === 302 || status === 307 || status === 308) && res.headers.location) {
13515
+ selfAborted = true;
12388
13516
  req.destroy();
12389
13517
  let nextUrl;
12390
13518
  try {
12391
13519
  nextUrl = new URL(res.headers.location, url).href;
12392
13520
  } catch {
12393
- resolve3({ error: `Invalid redirect location: ${res.headers.location}` });
13521
+ finish({ error: `Invalid redirect location: ${res.headers.location}` });
12394
13522
  return;
12395
13523
  }
12396
- fetchUrl({ url: nextUrl }, void 0, _redirectCount + 1).then(resolve3);
13524
+ fetchUrl({ url: nextUrl }, void 0, _redirectCount + 1).then(finish);
12397
13525
  return;
12398
13526
  }
12399
13527
  const contentType = res.headers["content-type"] ?? "";
@@ -12401,6 +13529,19 @@ ${diff2}${xfile}` };
12401
13529
  const chunks = [];
12402
13530
  let totalBytes = 0;
12403
13531
  let truncated = false;
13532
+ const emitBody = () => {
13533
+ let body = Buffer.concat(chunks).toString("utf-8");
13534
+ if (isHtml)
13535
+ body = stripHtml(body);
13536
+ const truncNote = truncated ? `
13537
+
13538
+ [Truncated at ${MAX_FETCH_BYTES / 1024}KB]` : "";
13539
+ finish({ output: `HTTP ${status}
13540
+
13541
+ <untrusted_web_content url="${url}">
13542
+ ${body}${truncNote}
13543
+ </untrusted_web_content>` });
13544
+ };
12404
13545
  res.on("data", (chunk) => {
12405
13546
  if (totalBytes + chunk.length > MAX_FETCH_BYTES) {
12406
13547
  const remaining = MAX_FETCH_BYTES - totalBytes;
@@ -12408,31 +13549,28 @@ ${diff2}${xfile}` };
12408
13549
  chunks.push(chunk.subarray(0, remaining));
12409
13550
  totalBytes = MAX_FETCH_BYTES;
12410
13551
  truncated = true;
13552
+ selfAborted = true;
12411
13553
  req.destroy();
13554
+ emitBody();
12412
13555
  } else {
12413
13556
  chunks.push(chunk);
12414
13557
  totalBytes += chunk.length;
12415
13558
  }
12416
13559
  });
12417
- res.on("end", () => {
12418
- let body = Buffer.concat(chunks).toString("utf-8");
12419
- if (isHtml)
12420
- body = stripHtml(body);
12421
- const truncNote = truncated ? `
12422
-
12423
- [Truncated at ${MAX_FETCH_BYTES / 1024}KB]` : "";
12424
- resolve3({ output: `HTTP ${status}
12425
-
12426
- <untrusted_web_content url="${url}">
12427
- ${body}${truncNote}
12428
- </untrusted_web_content>` });
13560
+ res.on("end", emitBody);
13561
+ res.on("error", (err) => {
13562
+ if (!selfAborted)
13563
+ finish({ error: err.message });
12429
13564
  });
12430
- res.on("error", (err) => resolve3({ error: err.message }));
12431
13565
  });
12432
- req.on("error", (err) => resolve3({ error: err.message }));
13566
+ req.on("error", (err) => {
13567
+ if (!selfAborted)
13568
+ finish({ error: err.message });
13569
+ });
12433
13570
  req.on("timeout", () => {
13571
+ selfAborted = true;
12434
13572
  req.destroy();
12435
- resolve3({ error: `Request timed out after ${DEFAULT_TIMEOUT_MS}ms` });
13573
+ finish({ error: `Request timed out after ${DEFAULT_TIMEOUT_MS}ms` });
12436
13574
  });
12437
13575
  });
12438
13576
  }
@@ -12860,36 +13998,48 @@ ${lines.join("\n")}`;
12860
13998
  return { output: `Found ${photos.length} photo(s) for "${query}":
12861
13999
  ${lines.join("\n")}` };
12862
14000
  }
12863
- var MEMORY_FILE = path5.join(os5.homedir(), ".nexrall", "memory.md");
12864
- async function memoryWrite(input) {
14001
+ function memoryScopeOf(input) {
14002
+ return input.scope === "global" ? "global" : "project";
14003
+ }
14004
+ async function memoryWrite(input, workDir) {
12865
14005
  const content = typeof input.content === "string" ? input.content.trim() : "";
12866
14006
  if (!content)
12867
14007
  return { error: "content is required" };
12868
- const dir = path5.dirname(MEMORY_FILE);
12869
- fs6.mkdirSync(dir, { recursive: true });
12870
- if (fs6.existsSync(MEMORY_FILE)) {
12871
- const existing = fs6.readFileSync(MEMORY_FILE, "utf-8").toLowerCase();
12872
- const fingerprint = content.toLowerCase().slice(0, 60);
12873
- if (existing.includes(fingerprint)) {
12874
- return { output: `Memory already recorded (skipped duplicate): ${content}` };
12875
- }
12876
- }
12877
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
12878
- const entry = `
12879
- - [${timestamp}] ${content}`;
12880
- fs6.appendFileSync(MEMORY_FILE, entry, "utf-8");
12881
- return { output: `Memory saved: ${content}` };
12882
- }
12883
- async function memoryRead(_input) {
14008
+ const scope = memoryScopeOf(input);
14009
+ const r2 = await (0, memory_1.writeMemory)(content, scope, workDir);
14010
+ if (!r2.ok)
14011
+ return { error: "failed to save memory" };
14012
+ return { output: r2.already ? `Memory already recorded (skipped duplicate): ${content}` : `Memory saved (${scope}): ${content}` };
14013
+ }
14014
+ async function memoryRead(input, workDir) {
12884
14015
  try {
12885
- if (!fs6.existsSync(MEMORY_FILE))
12886
- return { output: "No memories saved yet." };
12887
- const content = fs6.readFileSync(MEMORY_FILE, "utf-8").trim();
12888
- return { output: content || "No memories saved yet." };
14016
+ if (input && (input.scope === "project" || input.scope === "global")) {
14017
+ const mem2 = (0, memory_1.readMemory)(input.scope, workDir);
14018
+ return { output: mem2 || "No memories saved yet." };
14019
+ }
14020
+ const mem = (0, memory_1.readAllMemory)(workDir);
14021
+ return { output: mem || "No memories saved yet." };
12889
14022
  } catch (err) {
12890
14023
  return { error: err.message };
12891
14024
  }
12892
14025
  }
14026
+ async function useSkill(input, workDir) {
14027
+ const name = typeof input.name === "string" ? input.name.trim() : "";
14028
+ if (!name)
14029
+ return { error: "name is required (the skill to invoke)" };
14030
+ const dir = workDir ?? process.cwd();
14031
+ const skills = (0, skills_1.loadSkills)(dir);
14032
+ const skill = (0, skills_1.findSkill)(skills, name);
14033
+ if (!skill) {
14034
+ const known = skills.map((s2) => s2.name).join(", ") || "(none defined)";
14035
+ return { error: `Unknown skill "${name}". Available: ${known}.` };
14036
+ }
14037
+ const args = typeof input.args === "string" ? input.args : "";
14038
+ const expanded = (0, skills_1.expandSkill)(skill, args, dir);
14039
+ return { output: `[Skill "${skill.name}" loaded \u2014 follow these instructions:]
14040
+
14041
+ ${expanded}` };
14042
+ }
12893
14043
  function semanticPreamble(input, workDir) {
12894
14044
  const p = typeof input.path === "string" ? input.path : "";
12895
14045
  const line = typeof input.line === "number" ? Math.floor(input.line) : 0;
@@ -12951,6 +14101,7 @@ ${lines.join("\n")}` };
12951
14101
  notebook_edit: notebookEdit,
12952
14102
  memory_write: memoryWrite,
12953
14103
  memory_read: memoryRead,
14104
+ use_skill: useSkill,
12954
14105
  bash_output: bashOutput,
12955
14106
  kill_shell: killShell,
12956
14107
  // LSP-lite fallback (regex-based). In VS Code these names are intercepted by
@@ -12983,7 +14134,10 @@ ${lines.join("\n")}` };
12983
14134
  "get_workspace_symbols",
12984
14135
  "go_to_definition",
12985
14136
  "find_references",
12986
- "get_hover"
14137
+ "get_hover",
14138
+ "memory_write",
14139
+ "memory_read",
14140
+ "use_skill"
12987
14141
  ]);
12988
14142
  async function executeTool(name, input, abortSignal, sandbox, workDir, agentScope) {
12989
14143
  if (!TOOL_MAP[name])
@@ -13006,150 +14160,6 @@ ${lines.join("\n")}` };
13006
14160
  }
13007
14161
  });
13008
14162
 
13009
- // ../core/dist/plugins/index.js
13010
- var require_plugins = __commonJS({
13011
- "../core/dist/plugins/index.js"(exports2) {
13012
- "use strict";
13013
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m2, k, k2) {
13014
- if (k2 === void 0)
13015
- k2 = k;
13016
- var desc = Object.getOwnPropertyDescriptor(m2, k);
13017
- if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
13018
- desc = { enumerable: true, get: function() {
13019
- return m2[k];
13020
- } };
13021
- }
13022
- Object.defineProperty(o, k2, desc);
13023
- } : function(o, m2, k, k2) {
13024
- if (k2 === void 0)
13025
- k2 = k;
13026
- o[k2] = m2[k];
13027
- });
13028
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? function(o, v) {
13029
- Object.defineProperty(o, "default", { enumerable: true, value: v });
13030
- } : function(o, v) {
13031
- o["default"] = v;
13032
- });
13033
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ function() {
13034
- var ownKeys = function(o) {
13035
- ownKeys = Object.getOwnPropertyNames || function(o2) {
13036
- var ar = [];
13037
- for (var k in o2)
13038
- if (Object.prototype.hasOwnProperty.call(o2, k))
13039
- ar[ar.length] = k;
13040
- return ar;
13041
- };
13042
- return ownKeys(o);
13043
- };
13044
- return function(mod) {
13045
- if (mod && mod.__esModule)
13046
- return mod;
13047
- var result = {};
13048
- if (mod != null) {
13049
- for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
13050
- if (k[i2] !== "default")
13051
- __createBinding(result, mod, k[i2]);
13052
- }
13053
- __setModuleDefault(result, mod);
13054
- return result;
13055
- };
13056
- }();
13057
- Object.defineProperty(exports2, "__esModule", { value: true });
13058
- exports2.loadPlugins = loadPlugins3;
13059
- exports2.pluginAssetDirs = pluginAssetDirs;
13060
- exports2.pluginHooks = pluginHooks;
13061
- exports2.pluginMcpServers = pluginMcpServers;
13062
- var fs6 = __importStar(require("fs"));
13063
- var path5 = __importStar(require("path"));
13064
- var os5 = __importStar(require("os"));
13065
- function readMeta(dir) {
13066
- try {
13067
- const raw = fs6.readFileSync(path5.join(dir, "plugin.json"), "utf-8");
13068
- const j = JSON.parse(raw);
13069
- return {
13070
- name: typeof j.name === "string" ? j.name : void 0,
13071
- version: typeof j.version === "string" ? j.version : void 0,
13072
- description: typeof j.description === "string" ? j.description : void 0
13073
- };
13074
- } catch {
13075
- return {};
13076
- }
13077
- }
13078
- function scanRoot(root, scope, into) {
13079
- let entries;
13080
- try {
13081
- entries = fs6.readdirSync(root, { withFileTypes: true });
13082
- } catch {
13083
- return;
13084
- }
13085
- for (const entry of entries) {
13086
- if (!entry.isDirectory() && !entry.isSymbolicLink())
13087
- continue;
13088
- const dir = path5.join(root, entry.name);
13089
- try {
13090
- if (!fs6.statSync(dir).isDirectory())
13091
- continue;
13092
- } catch {
13093
- continue;
13094
- }
13095
- const meta = readMeta(dir);
13096
- const name = meta.name || entry.name;
13097
- if (into.has(name))
13098
- continue;
13099
- into.set(name, { name, version: meta.version, description: meta.description, dir, scope });
13100
- }
13101
- }
13102
- function loadPlugins3(workDir) {
13103
- const out = /* @__PURE__ */ new Map();
13104
- scanRoot(path5.join(workDir, ".nexrall", "plugins"), "project", out);
13105
- scanRoot(path5.join(os5.homedir(), ".nexrall", "plugins"), "global", out);
13106
- return [...out.values()];
13107
- }
13108
- function pluginAssetDirs(workDir, kind) {
13109
- return loadPlugins3(workDir).map((p) => path5.join(p.dir, kind)).filter((d) => {
13110
- try {
13111
- return fs6.statSync(d).isDirectory();
13112
- } catch {
13113
- return false;
13114
- }
13115
- });
13116
- }
13117
- function pluginHooks(workDir) {
13118
- const merged = {};
13119
- for (const p of loadPlugins3(workDir)) {
13120
- try {
13121
- const raw = fs6.readFileSync(path5.join(p.dir, "hooks.json"), "utf-8");
13122
- const j = JSON.parse(raw);
13123
- const hooks = j.hooks ?? j;
13124
- for (const [phase, entries] of Object.entries(hooks)) {
13125
- if (!Array.isArray(entries))
13126
- continue;
13127
- merged[phase] = [...merged[phase] ?? [], ...entries];
13128
- }
13129
- } catch {
13130
- }
13131
- }
13132
- return merged;
13133
- }
13134
- function pluginMcpServers(workDir) {
13135
- const merged = {};
13136
- for (const p of loadPlugins3(workDir)) {
13137
- try {
13138
- const raw = fs6.readFileSync(path5.join(p.dir, "mcp.json"), "utf-8");
13139
- const j = JSON.parse(raw);
13140
- const servers = j.mcpServers ?? j;
13141
- for (const [name, cfg] of Object.entries(servers)) {
13142
- if (!(name in merged))
13143
- merged[name] = cfg;
13144
- }
13145
- } catch {
13146
- }
13147
- }
13148
- return merged;
13149
- }
13150
- }
13151
- });
13152
-
13153
14163
  // ../core/dist/agent/agentTypes.js
13154
14164
  var require_agentTypes = __commonJS({
13155
14165
  "../core/dist/agent/agentTypes.js"(exports2) {
@@ -13706,16 +14716,22 @@ var require_loop = __commonJS({
13706
14716
  exports2.ledgerRecord = ledgerRecord;
13707
14717
  exports2.ledgerSummary = ledgerSummary;
13708
14718
  exports2.pruneOldToolResults = pruneOldToolResults;
14719
+ exports2.estimateTokensRough = estimateTokensRough;
14720
+ exports2.compactMessagesForResume = compactMessagesForResume2;
13709
14721
  exports2.runAgentLoop = runAgentLoop2;
14722
+ exports2.trimToResumableBoundary = trimToResumableBoundary;
14723
+ var types_1 = require_types();
13710
14724
  var client_1 = require_client();
13711
14725
  var executor_1 = require_executor();
13712
14726
  var agentTypes_1 = require_agentTypes();
14727
+ var skills_1 = require_skills();
13713
14728
  var rules_1 = require_rules();
13714
14729
  var sandbox_1 = require_sandbox();
13715
14730
  var index_1 = require_plugins();
13716
14731
  var testIntegrity_1 = require_testIntegrity();
13717
14732
  var flaky_1 = require_flaky();
13718
14733
  var claimEvidence_1 = require_claimEvidence();
14734
+ var memory_1 = require_memory();
13719
14735
  var fs6 = __importStar(require("fs"));
13720
14736
  var path5 = __importStar(require("path"));
13721
14737
  var child_process_1 = require("child_process");
@@ -13914,6 +14930,8 @@ var require_loop = __commonJS({
13914
14930
  const preview = typeof input.prompt === "string" ? input.prompt.slice(0, 60) : "";
13915
14931
  return `Sub-task: ${desc || preview}${!desc && preview.length === 60 ? "\u2026" : ""}`;
13916
14932
  }
14933
+ case "use_skill":
14934
+ return `Use skill: /${input.name ?? "(unknown)"}`;
13917
14935
  case "get_diagnostics":
13918
14936
  return input.path ? `Get diagnostics: ${input.path}` : "Get workspace diagnostics";
13919
14937
  case "go_to_definition":
@@ -13977,6 +14995,22 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
13977
14995
  onText: () => {
13978
14996
  },
13979
14997
  // sub-agent text is returned as the tool result, not streamed live
14998
+ // Forwarded DELIBERATELY, and it must be a real handler rather than a no-op.
14999
+ //
15000
+ // A sub-agent streams no text (onText above is a no-op) but it DOES stream thinking
15001
+ // through the parent's UI (see onThinking/onThinkingDelta below), and thinking sets
15002
+ // `emittedToCaller`. So a sub-agent stream that dies after reasoning genuinely has
15003
+ // rendered output to discard — a no-op here would let the restart proceed and then
15004
+ // re-stream that reasoning on top of the copy still on screen, the exact duplication
15005
+ // the opt-in exists to prevent.
15006
+ //
15007
+ // Forwarding is safe because the parent's own output is already closed by this
15008
+ // point: dispatching the `task` tool goes through options.onToolUse, which finalizes
15009
+ // the parent's bubble (VS Code) / flushes the renderer (CLI) before the sub-agent
15010
+ // starts. The only live, discardable element at restart time is the sub-agent's own
15011
+ // thinking block. Completed tool rows are left alone — those are real side effects
15012
+ // that actually happened.
15013
+ onStreamRestart: (reason, chars) => options.onStreamRestart?.(reason, chars),
13980
15014
  // Forward tool events with isSubTask=true so the UI can render a badge
13981
15015
  // instead of prepending "[sub-task]" to the tool name (which caused double-prefix
13982
15016
  // when the name was already labelled, and mixed display concerns into the data layer).
@@ -14009,7 +15043,13 @@ ${tail}`;
14009
15043
  pro: 1e6,
14010
15044
  ultra: 1e6
14011
15045
  };
14012
- var AUTO_COMPACT_THRESHOLD = 0.8;
15046
+ function envFraction(name, fallback) {
15047
+ const v = Number(process.env[name]);
15048
+ return Number.isFinite(v) && v > 0 && v < 1 ? v : fallback;
15049
+ }
15050
+ var AUTO_PRUNE_THRESHOLD = envFraction("NEXRALL_PRUNE_THRESHOLD", 0.35);
15051
+ var AUTO_COMPACT_THRESHOLD = envFraction("NEXRALL_COMPACT_THRESHOLD", 0.8);
15052
+ var PRUNE_MIN_RECLAIM_BYTES = 256 * 1024;
14013
15053
  var COMPACT_KEEP_MIN = 6;
14014
15054
  var MAX_BODY_BYTES = 8 * 1024 * 1024;
14015
15055
  function estimateBodyBytes(messages) {
@@ -14160,11 +15200,12 @@ ${tail}`;
14160
15200
  var PRUNE_STUB_KEEP_CHARS = 400;
14161
15201
  var PRUNE_MARKER = "\n\n[\u2026 ";
14162
15202
  var PRUNE_MARKER_TAIL = " pruned to conserve context. Re-run the tool if you need the full result.]";
14163
- function pruneOldToolResults(messages) {
15203
+ function pruneOldToolResults(messages, minReclaimBytes = 0) {
14164
15204
  const cutoff = messages.length - PRUNE_KEEP_RECENT;
14165
15205
  if (cutoff <= 1)
14166
15206
  return 0;
14167
- let reclaimed = 0;
15207
+ const targets = [];
15208
+ let total = 0;
14168
15209
  for (let i2 = 0; i2 < cutoff; i2++) {
14169
15210
  const m2 = messages[i2];
14170
15211
  if (!Array.isArray(m2.content))
@@ -14179,10 +15220,17 @@ ${tail}`;
14179
15220
  continue;
14180
15221
  const head = text.slice(0, PRUNE_STUB_KEEP_CHARS);
14181
15222
  const omitted = text.length - head.length;
14182
- b.content = `${head}${PRUNE_MARKER}${omitted} chars of earlier tool output${PRUNE_MARKER_TAIL}`;
14183
- reclaimed += omitted;
15223
+ targets.push({ block: b, head, omitted });
15224
+ total += omitted;
14184
15225
  }
14185
15226
  }
15227
+ if (total < minReclaimBytes)
15228
+ return 0;
15229
+ let reclaimed = 0;
15230
+ for (const { block, head, omitted } of targets) {
15231
+ block.content = `${head}${PRUNE_MARKER}${omitted} chars of earlier tool output${PRUNE_MARKER_TAIL}`;
15232
+ reclaimed += omitted;
15233
+ }
14186
15234
  return reclaimed;
14187
15235
  }
14188
15236
  function originalTaskText(messages) {
@@ -14204,12 +15252,24 @@ ${tail}`;
14204
15252
  let summary = "";
14205
15253
  try {
14206
15254
  const reply = await (0, client_1.streamChat)([{ role: "user", content: [{ type: "text", text: summaryPrompt }] }], {
14207
- model: options.model ?? "turbo",
15255
+ // COST: the summariser is a mechanical "bullet-point this transcript" task —
15256
+ // its quality is indistinguishable across model tiers, so there is no reason
15257
+ // to run it on the user's (possibly expensive) tier. Force 'turbo' (Sonnet 5):
15258
+ // it is the cheapest 1M-context tier (in $2.0/1M vs Opus $2.5, Fable $3.0;
15259
+ // out $15/1M vs $25 / $50), so this is always ≤ the user's cost, and its 1M
15260
+ // window comfortably holds the transcript (capped at ~170K tokens by
15261
+ // transcriptOf) even on the largest sessions.
15262
+ model: "turbo",
14208
15263
  mode: "ask",
14209
15264
  // summariser must not call tools; ask-mode discourages action
14210
15265
  env: options.env,
14211
15266
  clientType: options.clientType,
14212
- abortSignal: options.abortSignal
15267
+ abortSignal: options.abortSignal,
15268
+ // The summariser renders NOTHING (onEvent below is a no-op) and its result is
15269
+ // read only from the returned message, so a restart has nothing to roll back —
15270
+ // always safe. Worth enabling: a blip here used to abandon compaction entirely,
15271
+ // which then let the very next turn hit the context wall it was meant to prevent.
15272
+ allowRestartAfterRender: true
14213
15273
  }, () => {
14214
15274
  });
14215
15275
  summary = reply.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("").trim();
@@ -14235,6 +15295,88 @@ ${summary}
14235
15295
  Continue the work from here.` }] });
14236
15296
  return true;
14237
15297
  }
15298
+ async function maybeCompactMemory(scope, options) {
15299
+ try {
15300
+ await (0, memory_1.compactMemoryIfNeeded)(scope, options.workDir, async (prompt2) => {
15301
+ const reply = await (0, client_1.streamChat)([{ role: "user", content: [{ type: "text", text: prompt2 }] }], {
15302
+ // Same reasoning as autoCompactMessages' summariser: a mechanical
15303
+ // consolidation task, always run on the cheapest 1M-context tier
15304
+ // regardless of the user's chosen model for the actual conversation.
15305
+ model: "turbo",
15306
+ mode: "ask",
15307
+ env: options.env,
15308
+ clientType: options.clientType,
15309
+ abortSignal: options.abortSignal,
15310
+ // Same as the transcript summariser: no rendered output, result read only from
15311
+ // the returned message, so restarting on a blip is always safe.
15312
+ allowRestartAfterRender: true
15313
+ }, () => {
15314
+ });
15315
+ return reply.content.filter((b) => b.type === "text").map((b) => b.text ?? "").join("");
15316
+ });
15317
+ } catch {
15318
+ }
15319
+ }
15320
+ var RESUME_CHARS_PER_TOKEN = 4;
15321
+ function estimateTokensRough(messages) {
15322
+ return Math.ceil(estimateBodyBytes(messages) / RESUME_CHARS_PER_TOKEN);
15323
+ }
15324
+ async function compactMessagesForResume2(messages, opts) {
15325
+ if (messages.length <= COMPACT_KEEP_MIN + 2)
15326
+ return false;
15327
+ const settings = (0, rules_1.loadSettings)(opts.workDir);
15328
+ if (!resolveAutoCompact(void 0, settings.raw))
15329
+ return false;
15330
+ const contextWindow = MODEL_CONTEXT_TOKENS[opts.model ?? "turbo"] ?? 1e6;
15331
+ let bodyBytes = estimateBodyBytes(messages);
15332
+ let tokenGuess = estimateTokensRough(messages);
15333
+ const overPruneThreshold = () => tokenGuess > contextWindow * AUTO_PRUNE_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
15334
+ const overCompactThreshold = () => tokenGuess > contextWindow * AUTO_COMPACT_THRESHOLD || bodyBytes > MAX_BODY_BYTES;
15335
+ if (!overPruneThreshold())
15336
+ return false;
15337
+ let compacted = false;
15338
+ if (messages.length > PRUNE_KEEP_RECENT + 2) {
15339
+ const reclaimed = pruneOldToolResults(messages, PRUNE_MIN_RECLAIM_BYTES);
15340
+ if (reclaimed > 0) {
15341
+ bodyBytes = estimateBodyBytes(messages);
15342
+ tokenGuess = estimateTokensRough(messages);
15343
+ compacted = true;
15344
+ opts.onNotice?.(`
15345
+ \u267B\uFE0F Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of older tool output before resuming this chat.
15346
+ `);
15347
+ }
15348
+ }
15349
+ let guard = 0;
15350
+ while (overCompactThreshold() && messages.length > COMPACT_KEEP_MIN + 2 && guard < 5) {
15351
+ guard += 1;
15352
+ const did = await autoCompactMessages(messages, {
15353
+ workDir: opts.workDir,
15354
+ model: opts.model,
15355
+ clientType: opts.clientType,
15356
+ env: opts.env,
15357
+ onText: () => {
15358
+ },
15359
+ onToolUse: () => {
15360
+ },
15361
+ onToolResult: () => {
15362
+ },
15363
+ onUsage: () => {
15364
+ },
15365
+ requestPermission: async () => false
15366
+ });
15367
+ if (!did)
15368
+ break;
15369
+ compacted = true;
15370
+ bodyBytes = estimateBodyBytes(messages);
15371
+ tokenGuess = estimateTokensRough(messages);
15372
+ }
15373
+ if (compacted) {
15374
+ opts.onNotice?.(`
15375
+ \u267B\uFE0F Auto-compacted this chat's earlier history before resuming, to avoid resending it at full cost.
15376
+ `);
15377
+ }
15378
+ return compacted;
15379
+ }
14238
15380
  async function runAgentLoop2(initialMessages, options) {
14239
15381
  const messages = [...initialMessages];
14240
15382
  const model = options.model ?? "turbo";
@@ -14243,6 +15385,7 @@ Continue the work from here.` }] });
14243
15385
  const agentScope = options._agentScope ?? "root";
14244
15386
  const agentTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
14245
15387
  const agentsCatalogue = depth === 0 ? (0, agentTypes_1.summariseAgents)(agentTypes) : "";
15388
+ const skillsCatalogue = (0, skills_1.summariseSkills)((0, skills_1.loadSkills)(options.workDir));
14246
15389
  const settings = (0, rules_1.loadSettings)(options.workDir);
14247
15390
  const sandboxCfg = (0, sandbox_1.parseSandboxConfig)(settings.raw.sandbox) ?? void 0;
14248
15391
  const maxIterations = resolveMaxIterations(options.maxIterations, settings.raw);
@@ -14254,6 +15397,7 @@ Continue the work from here.` }] });
14254
15397
  let compacting = false;
14255
15398
  const hardCap = autoContinue ? Math.max(maxIterations, MAX_ITERATIONS_CEILING) : maxIterations;
14256
15399
  let completedCleanly = false;
15400
+ let completedRounds = 0;
14257
15401
  let stalledOut = false;
14258
15402
  let consecutiveErrorRounds = 0;
14259
15403
  let budget = maxIterations;
@@ -14270,16 +15414,15 @@ Continue the work from here.` }] });
14270
15414
  if (options.abortSignal?.aborted)
14271
15415
  break;
14272
15416
  let bodyBytes = estimateBodyBytes(messages);
15417
+ const prunePressure = lastPromptTokens > contextWindow * AUTO_PRUNE_THRESHOLD;
14273
15418
  const tokenPressure = lastPromptTokens > contextWindow * AUTO_COMPACT_THRESHOLD;
14274
15419
  let bytePressure = bodyBytes > MAX_BODY_BYTES;
14275
- if (autoCompact && !compacting && bytePressure && messages.length > PRUNE_KEEP_RECENT + 2) {
14276
- const reclaimed = pruneOldToolResults(messages);
15420
+ if (autoCompact && !compacting && (prunePressure || bytePressure) && messages.length > PRUNE_KEEP_RECENT + 2) {
15421
+ const reclaimed = pruneOldToolResults(messages, PRUNE_MIN_RECLAIM_BYTES);
14277
15422
  if (reclaimed > 0) {
14278
15423
  bodyBytes = estimateBodyBytes(messages);
14279
15424
  bytePressure = bodyBytes > MAX_BODY_BYTES;
14280
- options.onText(`
14281
- \u267B\uFE0F Trimmed ~${(reclaimed / (1024 * 1024)).toFixed(1)}MB of older tool output to conserve context.
14282
- `);
15425
+ (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.`);
14283
15426
  }
14284
15427
  }
14285
15428
  if (autoCompact && !compacting && (tokenPressure || bytePressure) && messages.length > COMPACT_KEEP_MIN + 2) {
@@ -14289,9 +15432,7 @@ Continue the work from here.` }] });
14289
15432
  if (did) {
14290
15433
  lastPromptTokens = 0;
14291
15434
  const reason = bytePressure ? `body ~${(bodyBytes / (1024 * 1024)).toFixed(1)}MB` : "context window";
14292
- options.onText(`
14293
- \u267B\uFE0F Auto-compacted earlier conversation to stay within the ${reason}.
14294
- `);
15435
+ (options.onNotice ?? options.onText)(`\u267B\uFE0F Auto-compacted earlier conversation to stay within the ${reason}.`);
14295
15436
  }
14296
15437
  } finally {
14297
15438
  compacting = false;
@@ -14302,7 +15443,6 @@ Continue the work from here.` }] });
14302
15443
  \u26A0\uFE0F Approaching the ${budget}-step limit (step ${iteration + 1}). Please wrap up and summarise what has been done.
14303
15444
  `);
14304
15445
  }
14305
- const pendingToolUse = [];
14306
15446
  const onEvent = (event) => {
14307
15447
  switch (event.type) {
14308
15448
  case "text":
@@ -14318,21 +15458,31 @@ Continue the work from here.` }] });
14318
15458
  options.onThinkingProgress?.(event.tokens);
14319
15459
  break;
14320
15460
  case "tool_use":
14321
- pendingToolUse.push({
14322
- type: "tool_use",
14323
- id: event.id,
14324
- name: event.name,
14325
- input: event.input
14326
- });
14327
15461
  break;
14328
15462
  case "usage":
14329
- lastPromptTokens = (event.usage.input_tokens ?? 0) + (event.usage.cache_creation_input_tokens ?? 0) + (event.usage.cache_read_input_tokens ?? 0);
14330
- options.onUsage(event.usage);
15463
+ if (!event.partial) {
15464
+ lastPromptTokens = (event.usage.input_tokens ?? 0) + (event.usage.cache_creation_input_tokens ?? 0) + (event.usage.cache_read_input_tokens ?? 0);
15465
+ }
15466
+ options.onUsage(event.usage, event.partial === true || event.replayed === true);
14331
15467
  break;
14332
15468
  case "message_complete":
14333
15469
  break;
15470
+ case "retry":
15471
+ options.onRetry?.(event.attempt, event.maxAttempts, event.reason);
15472
+ break;
15473
+ case "retry_resolved":
15474
+ options.onRetryResolved?.();
15475
+ break;
15476
+ case "stream_restart":
15477
+ options.onStreamRestart?.(event.reason, event.discardedChars);
15478
+ break;
15479
+ case "balance_status":
15480
+ options.onBalanceStatus?.(event.balance, event.zero);
15481
+ break;
14334
15482
  case "done":
15483
+ break;
14335
15484
  case "error":
15485
+ options.onNotice?.(`\u26A0\uFE0F ${event.message}`);
14336
15486
  break;
14337
15487
  }
14338
15488
  };
@@ -14348,13 +15498,25 @@ Continue the work from here.` }] });
14348
15498
  clientType: options.clientType,
14349
15499
  abortSignal: options.abortSignal,
14350
15500
  extraTools: options.mcpManager?.getAnthropicTools(),
14351
- agents: agentsCatalogue || void 0
15501
+ agents: agentsCatalogue || void 0,
15502
+ skills: skillsCatalogue || void 0,
15503
+ // Only allow a post-render restart when the caller actually implements the
15504
+ // rollback. Without a handler the partial output can't be un-rendered, so we
15505
+ // keep the old conservative behaviour (fail the turn) rather than duplicate
15506
+ // text on screen.
15507
+ allowRestartAfterRender: !!options.onStreamRestart
14352
15508
  }, onEvent);
14353
15509
  } catch (err) {
14354
15510
  if (options.abortSignal?.aborted || err.name === "AbortError")
14355
15511
  break;
15512
+ const status = err.status;
15513
+ if (status === 402) {
15514
+ const balance = err.balance ?? 0;
15515
+ options.onBalanceStatus?.(balance, true);
15516
+ break;
15517
+ }
14356
15518
  runSimpleHooks(hooks.OnError, options.workDir);
14357
- throw new Error(`Stream failed: ${err.message}`);
15519
+ throw new types_1.AgentTurnError(`Stream failed: ${err.message}`, trimToResumableBoundary(messages), completedRounds, err);
14358
15520
  }
14359
15521
  if (options.abortSignal?.aborted)
14360
15522
  break;
@@ -14371,9 +15533,12 @@ Continue the work from here.` }] });
14371
15533
  completedCleanly = true;
14372
15534
  break;
14373
15535
  }
14374
- messages.push(assistantMessage);
15536
+ const { stopReason: _stopReason, ...historyMessage } = assistantMessage;
15537
+ messages.push(historyMessage);
14375
15538
  const serverSideResultIds = new Set(assistantMessage.content.filter((b) => b.type === "tool_result").map((b) => b.tool_use_id).filter((id) => !!id));
14376
15539
  const toolUseBlocks = assistantMessage.content.filter((block) => block.type === "tool_use" && !serverSideResultIds.has(block.id));
15540
+ const lastBlock = assistantMessage.content[assistantMessage.content.length - 1];
15541
+ const truncatedToolUseId = assistantMessage.stopReason === "max_tokens" && lastBlock?.type === "tool_use" ? lastBlock.id : void 0;
14377
15542
  if (toolUseBlocks.length === 0) {
14378
15543
  const queued = options.takePendingInput?.() ?? [];
14379
15544
  if (queued.length) {
@@ -14441,8 +15606,15 @@ Continue the work from here.` }] });
14441
15606
  const toolResults = await Promise.all(toolUseBlocks.map(async (block) => {
14442
15607
  const { id, name, input } = block;
14443
15608
  options.onToolUse(name, input);
14444
- const description = humanDescription(name, input);
14445
15609
  let result;
15610
+ if (id === truncatedToolUseId) {
15611
+ result = {
15612
+ error: `This tool call (${name}) was CUT OFF because the response hit the model's output-token limit mid-generation (stop_reason: max_tokens) \u2014 its arguments may be incomplete or missing fields entirely, so it was NOT executed to avoid a silent partial edit. Retry with a SMALLER call: ` + (name === "multi_edit" || name === "edit_file" ? "split this into fewer edits per call (or call edit_file once per change instead of one large multi_edit), " : name === "write_file" ? "write the file in smaller chunks via write_file + edit_file follow-ups instead of one large write_file, " : "") + `so the full response fits comfortably under the per-turn output budget.`
15613
+ };
15614
+ options.onToolResult(name, result);
15615
+ return { block: { ...block, id }, result };
15616
+ }
15617
+ const description = humanDescription(name, input);
14446
15618
  let permitted;
14447
15619
  try {
14448
15620
  permitted = await options.requestPermission({ tool: name, input, description });
@@ -14479,6 +15651,10 @@ Continue the work from here.` }] });
14479
15651
  } catch (err) {
14480
15652
  result = { error: `Tool execution failed: ${err.message}` };
14481
15653
  }
15654
+ if (name === "memory_write" && result.error === void 0) {
15655
+ const scope = input.scope === "global" ? "global" : "project";
15656
+ void maybeCompactMemory(scope, options);
15657
+ }
14482
15658
  const post = runToolHooks(hooks.PostToolUse, "PostToolUse", name, input, options.workDir, result);
14483
15659
  const injected = [pre.context, post.context].filter(Boolean).join("\n");
14484
15660
  if (injected) {
@@ -14540,6 +15716,7 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
14540
15716
  content: toolResultContent
14541
15717
  };
14542
15718
  messages.push(toolResultMessage);
15719
+ completedRounds++;
14543
15720
  options.onProgress?.(messages);
14544
15721
  const allErrored = toolResults.length > 0 && toolResults.every(({ result }) => result.error !== void 0);
14545
15722
  consecutiveErrorRounds = allErrored ? consecutiveErrorRounds + 1 : 0;
@@ -14565,11 +15742,32 @@ ${result.output}` : `Error: ${result.error}` : result.output ?? "",
14565
15742
  `);
14566
15743
  }
14567
15744
  }
15745
+ } catch (err) {
15746
+ if (err instanceof types_1.AgentTurnError)
15747
+ throw err;
15748
+ if (options.abortSignal?.aborted || err.name === "AbortError")
15749
+ throw err;
15750
+ throw new types_1.AgentTurnError(err?.message || "The turn ended unexpectedly", trimToResumableBoundary(messages), completedRounds, err);
14568
15751
  } finally {
14569
15752
  runSimpleHooks(hooks.OnStop, options.workDir);
14570
15753
  }
14571
15754
  return messages;
14572
15755
  }
15756
+ function trimToResumableBoundary(messages) {
15757
+ const isUnansweredInvocation = (m2) => {
15758
+ if (m2?.role !== "assistant" || !Array.isArray(m2.content))
15759
+ return false;
15760
+ const blocks = m2.content;
15761
+ if (blocks.some((b) => b?.type === "tool_use"))
15762
+ return true;
15763
+ const answeredIds = new Set(blocks.filter((b) => /_tool_result$/.test(b?.type ?? "")).map((b) => b?.tool_use_id));
15764
+ return blocks.some((b) => b?.type === "server_tool_use" && !answeredIds.has(b?.id));
15765
+ };
15766
+ let end = messages.length;
15767
+ while (end > 0 && isUnansweredInvocation(messages[end - 1]))
15768
+ end--;
15769
+ return end === messages.length ? messages : messages.slice(0, end);
15770
+ }
14573
15771
  }
14574
15772
  });
14575
15773
 
@@ -15668,193 +16866,6 @@ var require_manager2 = __commonJS({
15668
16866
  }
15669
16867
  });
15670
16868
 
15671
- // ../core/dist/commands/loader.js
15672
- var require_loader = __commonJS({
15673
- "../core/dist/commands/loader.js"(exports2) {
15674
- "use strict";
15675
- var __createBinding = exports2 && exports2.__createBinding || (Object.create ? function(o, m2, k, k2) {
15676
- if (k2 === void 0)
15677
- k2 = k;
15678
- var desc = Object.getOwnPropertyDescriptor(m2, k);
15679
- if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
15680
- desc = { enumerable: true, get: function() {
15681
- return m2[k];
15682
- } };
15683
- }
15684
- Object.defineProperty(o, k2, desc);
15685
- } : function(o, m2, k, k2) {
15686
- if (k2 === void 0)
15687
- k2 = k;
15688
- o[k2] = m2[k];
15689
- });
15690
- var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? function(o, v) {
15691
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15692
- } : function(o, v) {
15693
- o["default"] = v;
15694
- });
15695
- var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ function() {
15696
- var ownKeys = function(o) {
15697
- ownKeys = Object.getOwnPropertyNames || function(o2) {
15698
- var ar = [];
15699
- for (var k in o2)
15700
- if (Object.prototype.hasOwnProperty.call(o2, k))
15701
- ar[ar.length] = k;
15702
- return ar;
15703
- };
15704
- return ownKeys(o);
15705
- };
15706
- return function(mod) {
15707
- if (mod && mod.__esModule)
15708
- return mod;
15709
- var result = {};
15710
- if (mod != null) {
15711
- for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
15712
- if (k[i2] !== "default")
15713
- __createBinding(result, mod, k[i2]);
15714
- }
15715
- __setModuleDefault(result, mod);
15716
- return result;
15717
- };
15718
- }();
15719
- Object.defineProperty(exports2, "__esModule", { value: true });
15720
- exports2.loadSlashCommands = loadSlashCommands2;
15721
- exports2.findSlashCommand = findSlashCommand2;
15722
- exports2.expandCommand = expandCommand2;
15723
- var fs6 = __importStar(require("fs"));
15724
- var path5 = __importStar(require("path"));
15725
- var os5 = __importStar(require("os"));
15726
- var child_process_1 = require("child_process");
15727
- var index_1 = require_plugins();
15728
- var BUILTIN_COMMANDS = [
15729
- {
15730
- name: "review",
15731
- description: "Review uncommitted changes (or a PR/branch diff) for bugs and risks",
15732
- source: "builtin",
15733
- body: [
15734
- "Review the following diff like a meticulous senior engineer. Target: $ARGUMENTS",
15735
- "(If no target given, review the uncommitted working-tree changes below. If a branch or PR",
15736
- "number is given, run the appropriate `git diff <base>...` or `gh pr diff <n>` yourself first.)",
15737
- "",
15738
- "Branch: !`git branch --show-current`",
15739
- "Status: !`git status --short`",
15740
- "",
15741
- "Diff (uncommitted):",
15742
- "```diff",
15743
- "!`git diff HEAD --unified=5 --no-color | head -4000`",
15744
- "```",
15745
- "",
15746
- "Review methodology:",
15747
- "1. Read the surrounding code of every changed hunk (read_file with offset/limit) \u2014 never judge a hunk in isolation.",
15748
- "2. Look for: correctness bugs, edge cases (empty/null/unicode/concurrency), security issues",
15749
- " (injection, path traversal, secrets), breaking API changes (find_references / search callers),",
15750
- " silent behaviour changes, and missing error handling.",
15751
- "3. Check tests: do existing tests cover the change? Are assertions weakened?",
15752
- "",
15753
- "Output format:",
15754
- "- \u{1F534} Critical (must fix before merge) \u2014 with file:line and a concrete fix",
15755
- "- \u{1F7E1} Warning (should fix) \u2014 with file:line",
15756
- "- \u{1F7E2} Suggestion (nice to have)",
15757
- "- Verdict: APPROVE / REQUEST CHANGES with a one-paragraph summary.",
15758
- "Do NOT modify any files \u2014 this is a read-only review."
15759
- ].join("\n")
15760
- }
15761
- ];
15762
- function parseFrontmatter(raw) {
15763
- const m2 = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
15764
- if (!m2)
15765
- return { meta: {}, body: raw.trim() };
15766
- const meta = {};
15767
- for (const line of m2[1].split(/\r?\n/)) {
15768
- const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
15769
- if (kv)
15770
- meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
15771
- }
15772
- return { meta, body: (m2[2] ?? "").trim() };
15773
- }
15774
- function loadDir(dir, source, into) {
15775
- let files;
15776
- try {
15777
- files = fs6.readdirSync(dir).filter((f3) => f3.endsWith(".md"));
15778
- } catch {
15779
- return;
15780
- }
15781
- for (const file of files) {
15782
- try {
15783
- const raw = fs6.readFileSync(path5.join(dir, file), "utf-8");
15784
- const { meta, body } = parseFrontmatter(raw);
15785
- const name = (meta.name || path5.basename(file, ".md")).trim().toLowerCase();
15786
- if (!name)
15787
- continue;
15788
- if (source !== "project" && into.has(name))
15789
- continue;
15790
- const model = ["turbo", "pro", "ultra"].find((x2) => x2 === (meta.model ?? "").toLowerCase());
15791
- into.set(name, {
15792
- name,
15793
- description: meta.description || `Custom /${name} command`,
15794
- model,
15795
- mode: meta.mode || void 0,
15796
- body,
15797
- source
15798
- });
15799
- } catch {
15800
- }
15801
- }
15802
- }
15803
- function loadSlashCommands2(workDir) {
15804
- const out = /* @__PURE__ */ new Map();
15805
- loadDir(path5.join(workDir, ".nexrall", "commands"), "project", out);
15806
- loadDir(path5.join(os5.homedir(), ".nexrall", "commands"), "global", out);
15807
- for (const dir of (0, index_1.pluginAssetDirs)(workDir, "commands"))
15808
- loadDir(dir, "plugin", out);
15809
- for (const cmd of BUILTIN_COMMANDS) {
15810
- if (!out.has(cmd.name))
15811
- out.set(cmd.name, cmd);
15812
- }
15813
- return [...out.values()];
15814
- }
15815
- function findSlashCommand2(cmds, name) {
15816
- const want = name.replace(/^\//, "").trim().toLowerCase();
15817
- return cmds.find((c) => c.name === want);
15818
- }
15819
- function expandCommand2(cmd, argString, workDir) {
15820
- const args = argString.trim();
15821
- const positional = args ? args.split(/\s+/) : [];
15822
- let out = cmd.body;
15823
- out = out.replace(/!`([^`]+)`/g, (_m, c) => {
15824
- try {
15825
- const stdout = (0, child_process_1.execSync)(c, { cwd: workDir, encoding: "utf-8", timeout: 15e3, stdio: ["ignore", "pipe", "pipe"] });
15826
- return stdout.trim();
15827
- } catch (err) {
15828
- return `[command failed: ${c} \u2014 ${err.message}]`;
15829
- }
15830
- });
15831
- out = out.replace(/(^|\s)@([^\s]+)/g, (_m, lead, rel) => {
15832
- const abs = path5.isAbsolute(rel) ? rel : path5.join(workDir, rel);
15833
- try {
15834
- const content = fs6.readFileSync(abs, "utf-8").slice(0, 12e3);
15835
- return `${lead}
15836
- [File: ${rel}]
15837
- \`\`\`
15838
- ${content}
15839
- \`\`\`
15840
- `;
15841
- } catch {
15842
- return `${lead}[missing file: ${rel}]`;
15843
- }
15844
- });
15845
- out = out.replace(/\$(\d+)/g, (_m, n) => positional[Number(n) - 1] ?? "");
15846
- const hadArgsToken = /\$ARGUMENTS/.test(out);
15847
- out = out.replace(/\$ARGUMENTS/g, args);
15848
- if (!hadArgsToken && !/\$\d+/.test(cmd.body) && args) {
15849
- out = `${out}
15850
-
15851
- ${args}`;
15852
- }
15853
- return out.trim();
15854
- }
15855
- }
15856
- });
15857
-
15858
16869
  // ../core/dist/permissions/destructive.js
15859
16870
  var require_destructive = __commonJS({
15860
16871
  "../core/dist/permissions/destructive.js"(exports2) {
@@ -16401,6 +17412,8 @@ var require_dist2 = __commonJS({
16401
17412
  __exportStar(require_crossFile(), exports2);
16402
17413
  __exportStar(require_flaky(), exports2);
16403
17414
  __exportStar(require_claimEvidence(), exports2);
17415
+ __exportStar(require_memory(), exports2);
17416
+ __exportStar(require_skills(), exports2);
16404
17417
  __exportStar(require_client2(), exports2);
16405
17418
  __exportStar(require_httpClient(), exports2);
16406
17419
  __exportStar(require_manager(), exports2);
@@ -22186,6 +23199,25 @@ var MarkdownStreamRenderer = class {
22186
23199
  for (const line of parts)
22187
23200
  this._line(line);
22188
23201
  }
23202
+ /**
23203
+ * Drop all buffered/partial state without emitting it.
23204
+ *
23205
+ * Used when a stream died mid-render and the same turn is being restarted: the
23206
+ * replacement attempt re-sends the response from the beginning, so any half-parsed
23207
+ * line, unterminated code fence or partially accumulated table from the dead attempt
23208
+ * must be discarded. Flushing instead would print a fragment and then leave the
23209
+ * renderer in `_inCode`/`_tbl` state that corrupts everything the retry prints.
23210
+ *
23211
+ * Note this only resets the PARSER — characters already written to stdout have
23212
+ * scrolled away and cannot be unprinted; the caller prints a visible restart marker
23213
+ * so the duplicated prefix is explained rather than silently confusing.
23214
+ */
23215
+ reset() {
23216
+ this._buf = "";
23217
+ this._inCode = false;
23218
+ this._codeLang = "";
23219
+ this._tbl = [];
23220
+ }
22189
23221
  flush() {
22190
23222
  if (this._buf) {
22191
23223
  this._line(this._buf);
@@ -22883,11 +23915,15 @@ var import_code_core2 = __toESM(require_dist2());
22883
23915
  var autoApproved = /* @__PURE__ */ new Set();
22884
23916
  var _rules = { allow: [], ask: [], deny: [] };
22885
23917
  var _workDir = process.cwd();
23918
+ var _mode = "auto";
22886
23919
  function initPermissions(workDir) {
22887
23920
  _workDir = workDir;
22888
23921
  _rules = (0, import_code_core2.loadSettings)(workDir).permissions;
22889
23922
  return _rules;
22890
23923
  }
23924
+ function setMode(mode) {
23925
+ _mode = mode || "auto";
23926
+ }
22891
23927
  function setAutoApprove(category) {
22892
23928
  autoApproved.add(category);
22893
23929
  }
@@ -22944,6 +23980,7 @@ async function requestPermission(req) {
22944
23980
  "todo_write",
22945
23981
  "memory_read",
22946
23982
  "memory_write",
23983
+ "use_skill",
22947
23984
  "notebook_read",
22948
23985
  "get_diagnostics",
22949
23986
  "go_to_definition",
@@ -22955,6 +23992,10 @@ async function requestPermission(req) {
22955
23992
  ];
22956
23993
  if (readOnlyTools.includes(tool))
22957
23994
  return true;
23995
+ if (_mode === "plan") {
23996
+ console.error(source_default.yellow(` \u2298 Plan mode \u2014 refused ${tool} (read-only until you switch mode).`));
23997
+ return false;
23998
+ }
22958
23999
  if (tool === "write_file" || tool === "create_file") {
22959
24000
  if (isAutoApproved("write"))
22960
24001
  return true;
@@ -23356,6 +24397,23 @@ function tryExec(cmd, cwd) {
23356
24397
  return void 0;
23357
24398
  }
23358
24399
  }
24400
+ var BILLING_URL = "https://app.nexrall.com/?settings=billing";
24401
+ function hyperlink(label, url) {
24402
+ return `\x1B]8;;${url}\x1B\\${label}\x1B]8;;\x1B\\`;
24403
+ }
24404
+ var lastBalanceNoticeState = null;
24405
+ function printBalanceNotice(balance, zero) {
24406
+ const state = zero ? "zero" : "low";
24407
+ if (lastBalanceNoticeState === state)
24408
+ return;
24409
+ lastBalanceNoticeState = state;
24410
+ const title = zero ? "Out of balance" : "Balance running low";
24411
+ const detail = zero ? "You're out of funds \u2014 top up to keep the agent working." : `Your wallet is under $5${typeof balance === "number" ? ` ($${balance.toFixed(2)} left)` : ""}. Top up before it runs out.`;
24412
+ console.log();
24413
+ console.log(source_default.bgYellow.black(` ${title} `) + " " + source_default.dim(detail));
24414
+ console.log(" " + source_default.yellow(hyperlink("\u2192 Top up", BILLING_URL)) + source_default.dim(` (${BILLING_URL})`));
24415
+ console.log();
24416
+ }
23359
24417
  function collectEnv(workDir) {
23360
24418
  return {
23361
24419
  cwd: workDir,
@@ -23383,10 +24441,9 @@ var tryRead = (p) => {
23383
24441
  };
23384
24442
  function readNexrallMd(workDir) {
23385
24443
  const parts = [];
23386
- const memContent = tryRead(path3.join(os4.homedir(), ".nexrall", "memory.md"));
24444
+ const memContent = (0, import_code_core3.readAllMemory)(workDir);
23387
24445
  if (memContent)
23388
- parts.push(`[Memories from previous conversations]
23389
- ${memContent}`);
24446
+ parts.push(memContent);
23390
24447
  const globalContent = tryRead(path3.join(os4.homedir(), ".nexrall", "nexrall.md"));
23391
24448
  if (globalContent)
23392
24449
  parts.push(`[Global instructions]
@@ -23430,6 +24487,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrall
23430
24487
  let toolStartTime = 0;
23431
24488
  let lastToolName = "";
23432
24489
  let thinkingTokens = 0;
24490
+ setMode(mode);
23433
24491
  process.stdout.write("\n");
23434
24492
  const result = await (0, import_code_core3.runAgentLoop)(messages, {
23435
24493
  workDir,
@@ -23456,6 +24514,18 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrall
23456
24514
  spinner.stop();
23457
24515
  mdRender.feed(text);
23458
24516
  },
24517
+ // System notices (mid-run auto-prune/auto-compact housekeeping) are NOT part
24518
+ // of the model's own reply — used to go through onText, which spliced
24519
+ // "♻️ Trimmed ~0.3MB…" straight into the markdown stream renderer as if the
24520
+ // model itself had said it. Print it as its own dim line instead (same
24521
+ // treatment as the resume-time compaction notice below).
24522
+ onNotice: (text) => {
24523
+ if (abortSignal.aborted)
24524
+ return;
24525
+ mdRender.flush();
24526
+ spinner.stop();
24527
+ console.log(source_default.dim(` ${text}`));
24528
+ },
23459
24529
  onToolUse: (name, input) => {
23460
24530
  if (abortSignal.aborted)
23461
24531
  return;
@@ -23473,8 +24543,50 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env2, nexrall
23473
24543
  console.log(formatToolResult(lastToolName, res, durationMs));
23474
24544
  toolStartTime = 0;
23475
24545
  },
23476
- onUsage: (u) => {
23477
- lastUsage = u;
24546
+ // Ignore a `partial` report: it belongs to a cut-short attempt that was restarted,
24547
+ // and the replacement attempt reports the turn's real totals. Letting it through
24548
+ // would print a token count for output the user never saw.
24549
+ onUsage: (u, partial) => {
24550
+ if (!partial)
24551
+ lastUsage = u;
24552
+ },
24553
+ // A transient disconnect (network drop, machine sleep/wake, overloaded upstream)
24554
+ // is retried transparently by the network layer — without this, that pause was
24555
+ // completely invisible: the CLI just appeared to freeze and then resume with no
24556
+ // explanation. Reuse the same spinner to show what's actually happening.
24557
+ onRetry: (attempt, _maxAttempts, reason) => {
24558
+ if (abortSignal.aborted)
24559
+ return;
24560
+ spinner.start(`${reason}\u2026 (attempt ${attempt})`);
24561
+ },
24562
+ onRetryResolved: () => {
24563
+ spinner.stop();
24564
+ },
24565
+ // The stream died after part of the answer had already been printed, and the
24566
+ // turn is being restarted from the top. Providing this handler is what OPTS US
24567
+ // IN to post-render restarts at all (see AgentLoopOptions.onStreamRestart) —
24568
+ // without it a mid-answer disconnect kills the whole turn.
24569
+ //
24570
+ // A terminal can't unprint scrolled-away output, so instead of pretending the
24571
+ // fragment never happened we (a) reset the markdown parser so the dead attempt's
24572
+ // half-open code fence/table can't corrupt everything the retry prints, and
24573
+ // (b) draw an explicit marker so the user understands why the answer restarts.
24574
+ onStreamRestart: (reason, discardedChars) => {
24575
+ if (abortSignal.aborted)
24576
+ return;
24577
+ spinner.stop();
24578
+ mdRender.reset();
24579
+ if (discardedChars > 0) {
24580
+ console.log(
24581
+ "\n" + source_default.yellow(" \u21BA Connection dropped mid-answer \u2014 restarting this response.") + source_default.dim(`
24582
+ (${reason}. The ${discardedChars} characters above are incomplete; the full answer follows.)`) + "\n"
24583
+ );
24584
+ }
24585
+ },
24586
+ onBalanceStatus: (balance, zero) => {
24587
+ spinner.stop();
24588
+ mdRender.flush();
24589
+ printBalanceNotice(balance, zero);
23478
24590
  },
23479
24591
  requestPermission,
23480
24592
  checkpointManager,
@@ -23508,6 +24620,13 @@ async function runTurnHeadless(messages, modelAlias, workDir, _abortSignal, env2
23508
24620
  resultText += text;
23509
24621
  emit({ type: "text", text });
23510
24622
  },
24623
+ // System notice (mid-run auto-prune/auto-compact) — emit as its own event
24624
+ // type instead of falling through to onText, so a stream-json consumer
24625
+ // doesn't see compaction housekeeping text mixed into the model's `text`
24626
+ // events or accumulated into resultText.
24627
+ onNotice: (text) => {
24628
+ emit({ type: "notice", text });
24629
+ },
23511
24630
  onToolUse: (name, input) => {
23512
24631
  emit({ type: "tool_use", tool: name, input });
23513
24632
  },
@@ -23515,9 +24634,26 @@ async function runTurnHeadless(messages, modelAlias, workDir, _abortSignal, env2
23515
24634
  toolCallCount++;
23516
24635
  emit({ type: "tool_result", tool: name, ok: res.error === void 0, ...res.error ? { error: res.error } : {} });
23517
24636
  },
23518
- onUsage: (u) => {
23519
- lastUsage = u;
23520
- emit({ type: "usage", usage: u });
24637
+ // Headless consumers get the partial report too — tagged, so a script can account
24638
+ // for the real cost of a restarted turn — but it never becomes `lastUsage`, which
24639
+ // represents the turn's actual output.
24640
+ onUsage: (u, partial) => {
24641
+ if (!partial)
24642
+ lastUsage = u;
24643
+ emit({ type: "usage", usage: u, ...partial ? { partial: true } : {} });
24644
+ },
24645
+ onRetry: (attempt, maxAttempts, reason) => {
24646
+ emit({ type: "retry", attempt, max_attempts: maxAttempts, reason });
24647
+ },
24648
+ // Headless consumers parse NDJSON, so a restart is trivially clean for them:
24649
+ // they simply drop every `text`/`thinking` event seen since the turn started.
24650
+ // Emitting it also opts headless mode into post-render restarts, so a scripted
24651
+ // /CI run survives a blip instead of exiting non-zero halfway through.
24652
+ onStreamRestart: (reason, discardedChars) => {
24653
+ emit({ type: "stream_restart", reason, discarded_chars: discardedChars });
24654
+ },
24655
+ onBalanceStatus: (balance, zero) => {
24656
+ emit({ type: "balance_status", balance, zero, billing_url: BILLING_URL });
23521
24657
  },
23522
24658
  // Headless → auto-approve (no TTY to ask on). Destructive/irreversible
23523
24659
  // commands (DB drops, force-push, terraform destroy…) fail CLOSED here: with
@@ -23559,6 +24695,7 @@ function printHelp() {
23559
24695
  ["/rewind [id]", "List file checkpoints, or roll back to one"],
23560
24696
  ["/compact", "Summarize conversation to save tokens"],
23561
24697
  ["/init", "Generate nexrall.md for this project"],
24698
+ ["/memory [global] [clear]", "View persistent memory (project or global); /memory clear to wipe"],
23562
24699
  ["/help", "Show this help"],
23563
24700
  ["/model [turbo|pro|ultra]", "Switch model"],
23564
24701
  ["/mode [ask|edit|plan|auto]", "Set agent mode"],
@@ -23566,7 +24703,7 @@ function printHelp() {
23566
24703
  ["/yolo", "Auto-approve all permissions"],
23567
24704
  ["/balance", "Show wallet balance"],
23568
24705
  ["/add <filepath>", "Add a file to conversation context"],
23569
- ["/commands", "List custom commands (.nexrall/commands/*.md)"],
24706
+ ["/skills", "List skills (.nexrall/skills/<name>/SKILL.md or .nexrall/commands/*.md) \u2014 the model can also auto-invoke these"],
23570
24707
  ["/plugins", "List installed plugins (.nexrall/plugins/)"],
23571
24708
  ["/update", "Update nex to the latest version"]
23572
24709
  ];
@@ -23625,9 +24762,9 @@ async function startChatSession(options) {
23625
24762
  const ruleCount = permRules.allow.length + permRules.ask.length + permRules.deny.length;
23626
24763
  if (ruleCount && !headless)
23627
24764
  console.log(source_default.dim(` ${ruleCount} permission rule(s) loaded`));
23628
- let slashCommands = (0, import_code_core3.loadSlashCommands)(workDir);
23629
- if (slashCommands.length && !headless)
23630
- console.log(source_default.dim(` ${slashCommands.length} custom command(s) loaded`));
24765
+ let skills = (0, import_code_core3.loadSkills)(workDir);
24766
+ if (skills.length && !headless)
24767
+ console.log(source_default.dim(` ${skills.length} skill(s) loaded`));
23631
24768
  if (headless && !options.prompt && !options.stdinText) {
23632
24769
  process.stdout.write(JSON.stringify({ type: "error", error: "--output-format json/stream-json requires a one-shot prompt (or piped stdin)." }) + "\n");
23633
24770
  process.exit(1);
@@ -23642,9 +24779,23 @@ async function startChatSession(options) {
23642
24779
  messages = stored.messages;
23643
24780
  sessionId = stored.id;
23644
24781
  sessionTitle = stored.title;
23645
- console.log(source_default.green(` Resumed session: ${source_default.bold(stored.title.slice(0, 60))}`));
23646
- console.log(source_default.dim(` ${messages.length} messages restored`));
23647
- console.log();
24782
+ try {
24783
+ const compacted = await (0, import_code_core3.compactMessagesForResume)(messages, {
24784
+ workDir,
24785
+ model: modelAlias,
24786
+ clientType: "cli",
24787
+ onNotice: (text) => {
24788
+ if (!headless)
24789
+ console.log(source_default.dim(text.trim()));
24790
+ }
24791
+ });
24792
+ } catch {
24793
+ }
24794
+ if (!headless) {
24795
+ console.log(source_default.green(` Resumed session: ${source_default.bold(stored.title.slice(0, 60))}`));
24796
+ console.log(source_default.dim(` ${messages.length} messages restored`));
24797
+ console.log();
24798
+ }
23648
24799
  }
23649
24800
  const checkpoints = new import_code_core3.CheckpointManager(workDir, sessionId);
23650
24801
  const updateTitle = () => {
@@ -23654,6 +24805,19 @@ async function startChatSession(options) {
23654
24805
  const raw = first?.content[0]?.text ?? "";
23655
24806
  sessionTitle = raw.length > 60 ? raw.slice(0, 60) + "\u2026" : raw;
23656
24807
  };
24808
+ const recoverTurn = (err, current) => {
24809
+ const salvaged = (0, import_code_core3.salvageHistory)(err);
24810
+ console.error("\n" + source_default.red("Error: ") + String(err.message));
24811
+ if (!salvaged)
24812
+ return current;
24813
+ const rounds = err.progressCount ?? 1;
24814
+ console.error(
24815
+ source_default.dim(
24816
+ ` Kept the ${rounds} step${rounds === 1 ? "" : "s"} completed before the interruption \u2014 send "continue" to pick up where it stopped.`
24817
+ )
24818
+ );
24819
+ return salvaged;
24820
+ };
23657
24821
  let lastProgressSave = 0;
23658
24822
  const saveProgress = (live) => {
23659
24823
  const now = Date.now();
@@ -23689,10 +24853,27 @@ ${text}` : "");
23689
24853
  updateTitle();
23690
24854
  saveSession(sessionId, sessionTitle, workDir, messages);
23691
24855
  } catch (err) {
24856
+ const salvaged = (0, import_code_core3.salvageHistory)(err);
24857
+ if (salvaged) {
24858
+ messages = salvaged;
24859
+ try {
24860
+ updateTitle();
24861
+ saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
24862
+ checkpoints.commitTurn();
24863
+ } catch {
24864
+ }
24865
+ }
23692
24866
  if (options.outputFormat === "json" || options.outputFormat === "stream-json") {
23693
- process.stdout.write(JSON.stringify({ type: "error", error: String(err.message) }) + "\n");
24867
+ process.stdout.write(JSON.stringify({
24868
+ type: "error",
24869
+ error: String(err.message),
24870
+ ...salvaged ? { resumable: true, messagesCompleted: salvaged.length } : {}
24871
+ }) + "\n");
23694
24872
  } else {
23695
24873
  console.error(source_default.red("\nError: ") + String(err.message));
24874
+ if (salvaged) {
24875
+ console.error(source_default.dim(" Progress was saved \u2014 run `nex --continue` to resume this session."));
24876
+ }
23696
24877
  }
23697
24878
  process.exit(1);
23698
24879
  } finally {
@@ -23789,6 +24970,15 @@ ${text}` : "");
23789
24970
  messages = stored.messages;
23790
24971
  sessionId = stored.id;
23791
24972
  sessionTitle = stored.title;
24973
+ try {
24974
+ await (0, import_code_core3.compactMessagesForResume)(messages, {
24975
+ workDir,
24976
+ model: modelAlias,
24977
+ clientType: "cli",
24978
+ onNotice: (text) => console.log(source_default.dim(text.trim()))
24979
+ });
24980
+ } catch {
24981
+ }
23792
24982
  console.log(source_default.green(` Resumed: ${source_default.bold(stored.title.slice(0, 60))}`));
23793
24983
  console.log(source_default.dim(` ${messages.length} messages restored`));
23794
24984
  rl.prompt();
@@ -23945,6 +25135,49 @@ ${dirList}`;
23945
25135
  rl.prompt();
23946
25136
  return;
23947
25137
  }
25138
+ case "/memory": {
25139
+ const memArgs = arg.toLowerCase().split(/\s+/).filter(Boolean);
25140
+ const wantsGlobalOnly = memArgs.includes("global");
25141
+ const wantsClear = memArgs.includes("clear");
25142
+ const memScope = wantsGlobalOnly ? "global" : "project";
25143
+ if (wantsClear) {
25144
+ rl.pause();
25145
+ const rl2 = readline3.createInterface({ input: process.stdin, output: process.stdout });
25146
+ const label = memScope === "global" ? "GLOBAL" : "this PROJECT's";
25147
+ const answer = await new Promise((resolve3) => rl2.question(source_default.yellow(` Clear ${label} memory? This cannot be undone. [y/n] `), (a) => {
25148
+ rl2.close();
25149
+ resolve3(a.trim());
25150
+ }));
25151
+ if (answer === "y" || answer === "yes") {
25152
+ (0, import_code_core3.clearMemory)(memScope, workDir);
25153
+ console.log(source_default.green(` ${memScope === "global" ? "Global" : "Project"} memory cleared.`));
25154
+ } else {
25155
+ console.log(source_default.dim(" Cancelled."));
25156
+ }
25157
+ rl.resume();
25158
+ rl.prompt();
25159
+ return;
25160
+ }
25161
+ console.log();
25162
+ if (wantsGlobalOnly) {
25163
+ const stats = (0, import_code_core3.memoryStats)("global");
25164
+ const content = (0, import_code_core3.readMemory)("global");
25165
+ console.log(source_default.bold(" Global memory ") + source_default.dim(`(${stats.entries} entries, ${(stats.bytes / 1024).toFixed(1)}KB) \u2014 ${stats.file}`));
25166
+ console.log();
25167
+ console.log(content || source_default.dim(" No global memories saved yet."));
25168
+ } else {
25169
+ const projStats = (0, import_code_core3.memoryStats)("project", workDir);
25170
+ const globalStats = (0, import_code_core3.memoryStats)("global");
25171
+ console.log(source_default.bold(" Memory") + source_default.dim(` \u2014 project: ${projStats.entries} entries (${(projStats.bytes / 1024).toFixed(1)}KB) \xB7 global: ${globalStats.entries} entries (${(globalStats.bytes / 1024).toFixed(1)}KB)`));
25172
+ console.log();
25173
+ const merged = (0, import_code_core3.readAllMemory)(workDir);
25174
+ console.log(merged || source_default.dim(" No memories saved yet."));
25175
+ }
25176
+ console.log();
25177
+ console.log(source_default.dim(" Tip: /memory global \xB7 /memory clear \xB7 /memory global clear"));
25178
+ rl.prompt();
25179
+ return;
25180
+ }
23948
25181
  case "/help":
23949
25182
  printHelp();
23950
25183
  rl.prompt();
@@ -24048,20 +25281,23 @@ ${content}
24048
25281
  }
24049
25282
  console.log();
24050
25283
  }
24051
- slashCommands = (0, import_code_core3.loadSlashCommands)(workDir);
25284
+ skills = (0, import_code_core3.loadSkills)(workDir);
24052
25285
  rl.prompt();
24053
25286
  return;
24054
25287
  }
25288
+ case "/skills":
24055
25289
  case "/commands": {
24056
- slashCommands = (0, import_code_core3.loadSlashCommands)(workDir);
24057
- if (!slashCommands.length) {
24058
- console.log(source_default.dim(" No custom commands. Add .nexrall/commands/<name>.md to create one."));
25290
+ skills = (0, import_code_core3.loadSkills)(workDir);
25291
+ const invocable = (0, import_code_core3.userInvokableSkills)(skills);
25292
+ if (!invocable.length) {
25293
+ console.log(source_default.dim(" No skills. Add .nexrall/skills/<name>/SKILL.md (or .nexrall/commands/<name>.md) to create one."));
24059
25294
  } else {
24060
25295
  console.log();
24061
- console.log(source_default.bold(" Custom commands:"));
24062
- for (const c of slashCommands) {
24063
- const scope = c.source === "project" ? "" : source_default.dim(` (${c.source})`);
24064
- console.log(" " + source_default.cyan(`/${c.name}`.padEnd(20)) + source_default.dim(c.description) + scope);
25296
+ console.log(source_default.bold(" Skills:"));
25297
+ for (const s2 of invocable) {
25298
+ const scope = s2.source === "project" ? "" : source_default.dim(` (${s2.source})`);
25299
+ const auto = s2.disableModelInvocation ? source_default.dim(" [manual only]") : "";
25300
+ console.log(" " + source_default.cyan(`/${s2.name}`.padEnd(20)) + source_default.dim(s2.description) + scope + auto);
24065
25301
  }
24066
25302
  console.log();
24067
25303
  }
@@ -24069,14 +25305,14 @@ ${content}
24069
25305
  return;
24070
25306
  }
24071
25307
  default: {
24072
- const custom = (0, import_code_core3.findSlashCommand)(slashCommands, cmd);
25308
+ const custom = (0, import_code_core3.findSkill)(skills, cmd);
24073
25309
  if (!custom) {
24074
25310
  console.log(source_default.red(` Unknown command: ${cmd}`));
24075
- console.log(source_default.dim(" Type /help for built-ins or /commands for custom ones."));
25311
+ console.log(source_default.dim(" Type /help for built-ins or /skills for custom ones."));
24076
25312
  rl.prompt();
24077
25313
  return;
24078
25314
  }
24079
- const expanded = (0, import_code_core3.expandCommand)(custom, arg, workDir);
25315
+ const expanded = (0, import_code_core3.expandSkill)(custom, arg, workDir);
24080
25316
  const turnModel = custom.model ?? modelAlias;
24081
25317
  const turnMode = custom.mode ?? agentMode;
24082
25318
  console.log(source_default.dim(` Running /${custom.name}\u2026`));
@@ -24091,7 +25327,9 @@ ${content}
24091
25327
  updateTitle();
24092
25328
  saveSession(sessionId, sessionTitle, workDir, messages);
24093
25329
  } catch (err) {
24094
- console.error("\n" + source_default.red("Error: ") + String(err.message));
25330
+ messages = recoverTurn(err, messages);
25331
+ updateTitle();
25332
+ saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
24095
25333
  } finally {
24096
25334
  agentRunning = false;
24097
25335
  checkpoints.commitTurn();
@@ -24113,7 +25351,9 @@ ${content}
24113
25351
  updateTitle();
24114
25352
  saveSession(sessionId, sessionTitle, workDir, messages);
24115
25353
  } catch (err) {
24116
- console.error("\n" + source_default.red("Error: ") + String(err.message));
25354
+ messages = recoverTurn(err, messages);
25355
+ updateTitle();
25356
+ saveSession(sessionId, sessionTitle || "Untitled", workDir, messages);
24117
25357
  } finally {
24118
25358
  agentRunning = false;
24119
25359
  checkpoints.commitTurn();
@@ -24335,7 +25575,7 @@ function pluginListCommand() {
24335
25575
 
24336
25576
  // src/index.ts
24337
25577
  var program2 = new Command();
24338
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.2");
25578
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.17");
24339
25579
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
24340
25580
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
24341
25581
  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) => {