nexrall-code 0.5.47 → 0.5.49
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.
- package/dist/index.js +832 -104
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3156,6 +3156,8 @@ var require_auth = __commonJS({
|
|
|
3156
3156
|
exports.clearAuth = clearAuth2;
|
|
3157
3157
|
exports.isAuthenticated = isAuthenticated3;
|
|
3158
3158
|
exports.getToken = getToken;
|
|
3159
|
+
exports.getRefreshToken = getRefreshToken;
|
|
3160
|
+
exports.updateTokens = updateTokens;
|
|
3159
3161
|
var fs9 = __importStar(__require("fs"));
|
|
3160
3162
|
var path6 = __importStar(__require("path"));
|
|
3161
3163
|
var os6 = __importStar(__require("os"));
|
|
@@ -3205,6 +3207,34 @@ var require_auth = __commonJS({
|
|
|
3205
3207
|
const auth = loadAuth();
|
|
3206
3208
|
return auth?.token ?? null;
|
|
3207
3209
|
}
|
|
3210
|
+
function getRefreshToken() {
|
|
3211
|
+
const envToken = process.env.NEXRALL_TOKEN;
|
|
3212
|
+
if (envToken && envToken.trim().length > 0)
|
|
3213
|
+
return null;
|
|
3214
|
+
const auth = loadAuth();
|
|
3215
|
+
const rt = auth?.refreshToken;
|
|
3216
|
+
return typeof rt === "string" && rt.length > 0 ? rt : null;
|
|
3217
|
+
}
|
|
3218
|
+
function updateTokens(token, refreshToken) {
|
|
3219
|
+
let existing = {};
|
|
3220
|
+
try {
|
|
3221
|
+
if (fs9.existsSync(CONFIG_FILE)) {
|
|
3222
|
+
const parsed = JSON.parse(fs9.readFileSync(CONFIG_FILE, "utf-8"));
|
|
3223
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
3224
|
+
existing = parsed;
|
|
3225
|
+
}
|
|
3226
|
+
} catch {
|
|
3227
|
+
}
|
|
3228
|
+
ensureConfigDir();
|
|
3229
|
+
const next = {
|
|
3230
|
+
...existing,
|
|
3231
|
+
token,
|
|
3232
|
+
// Keep the previous refresh token if the server didn't send a new one, rather
|
|
3233
|
+
// than deleting the only means of refreshing again.
|
|
3234
|
+
...refreshToken ? { refreshToken } : {}
|
|
3235
|
+
};
|
|
3236
|
+
fs9.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 384 });
|
|
3237
|
+
}
|
|
3208
3238
|
}
|
|
3209
3239
|
});
|
|
3210
3240
|
|
|
@@ -9842,6 +9872,7 @@ var require_client = __commonJS({
|
|
|
9842
9872
|
exports.chooseFinalContent = chooseFinalContent;
|
|
9843
9873
|
exports.streamChat = streamChat;
|
|
9844
9874
|
exports.cancelTurn = cancelTurn;
|
|
9875
|
+
exports.revokeRefreshToken = revokeRefreshToken2;
|
|
9845
9876
|
exports.getBalance = getBalance3;
|
|
9846
9877
|
exports.exchangeVscodeCode = exchangeVscodeCode;
|
|
9847
9878
|
exports.login = login2;
|
|
@@ -9892,6 +9923,49 @@ var require_client = __commonJS({
|
|
|
9892
9923
|
Authorization: `Bearer ${token}`
|
|
9893
9924
|
};
|
|
9894
9925
|
}
|
|
9926
|
+
var _refreshInFlight = null;
|
|
9927
|
+
async function refreshAccessToken() {
|
|
9928
|
+
if (_refreshInFlight)
|
|
9929
|
+
return _refreshInFlight;
|
|
9930
|
+
_refreshInFlight = (async () => {
|
|
9931
|
+
const refreshToken = (0, index_1.getRefreshToken)();
|
|
9932
|
+
if (!refreshToken)
|
|
9933
|
+
return false;
|
|
9934
|
+
try {
|
|
9935
|
+
const res = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/refresh`, {
|
|
9936
|
+
method: "POST",
|
|
9937
|
+
headers: { "Content-Type": "application/json" },
|
|
9938
|
+
body: JSON.stringify({ refreshToken })
|
|
9939
|
+
});
|
|
9940
|
+
if (!res.ok)
|
|
9941
|
+
return false;
|
|
9942
|
+
const data = await res.json();
|
|
9943
|
+
if (!data.token)
|
|
9944
|
+
return false;
|
|
9945
|
+
(0, index_1.updateTokens)(data.token, data.refreshToken);
|
|
9946
|
+
return true;
|
|
9947
|
+
} catch {
|
|
9948
|
+
return false;
|
|
9949
|
+
}
|
|
9950
|
+
})();
|
|
9951
|
+
try {
|
|
9952
|
+
return await _refreshInFlight;
|
|
9953
|
+
} finally {
|
|
9954
|
+
_refreshInFlight = null;
|
|
9955
|
+
}
|
|
9956
|
+
}
|
|
9957
|
+
function isExpiredTokenResponse(status, body) {
|
|
9958
|
+
if (status !== 401 && status !== 403)
|
|
9959
|
+
return false;
|
|
9960
|
+
try {
|
|
9961
|
+
const parsed = JSON.parse(body);
|
|
9962
|
+
if (parsed?.error && typeof parsed.error === "object")
|
|
9963
|
+
return false;
|
|
9964
|
+
return typeof parsed?.error === "string" && /token|auth|expired/i.test(parsed.error);
|
|
9965
|
+
} catch {
|
|
9966
|
+
return false;
|
|
9967
|
+
}
|
|
9968
|
+
}
|
|
9895
9969
|
var MAX_RETRIES = 5;
|
|
9896
9970
|
var RETRY_BASE_MS = 1e3;
|
|
9897
9971
|
var RETRY_MAX_MS = 3e4;
|
|
@@ -9974,14 +10048,43 @@ var require_client = __commonJS({
|
|
|
9974
10048
|
onEvent({ type: "retry_resolved" });
|
|
9975
10049
|
}
|
|
9976
10050
|
};
|
|
10051
|
+
const prepareRestart = async (reason) => {
|
|
10052
|
+
await cancelTurn(turnId);
|
|
10053
|
+
turnId = (0, crypto_1.randomUUID)();
|
|
10054
|
+
resuming = false;
|
|
10055
|
+
serverResumable = false;
|
|
10056
|
+
lastEventId = 0;
|
|
10057
|
+
carryText = [];
|
|
10058
|
+
carryToolUse = [];
|
|
10059
|
+
const hadRendered = emittedAnythingAcrossAttempts;
|
|
10060
|
+
const renderedChars = emittedCharsAcrossAttempts;
|
|
10061
|
+
emittedAnythingAcrossAttempts = false;
|
|
10062
|
+
emittedCharsAcrossAttempts = 0;
|
|
10063
|
+
if (hadRendered && !allowRestartAfterRender)
|
|
10064
|
+
return false;
|
|
10065
|
+
if (hadRendered) {
|
|
10066
|
+
onEvent({ type: "stream_restart", reason, discardedChars: renderedChars });
|
|
10067
|
+
}
|
|
10068
|
+
return true;
|
|
10069
|
+
};
|
|
10070
|
+
const bodySaysNotResumable = (body) => {
|
|
10071
|
+
try {
|
|
10072
|
+
return JSON.parse(body)?.notResumable === true;
|
|
10073
|
+
} catch {
|
|
10074
|
+
return false;
|
|
10075
|
+
}
|
|
10076
|
+
};
|
|
9977
10077
|
async function runAttempt() {
|
|
9978
10078
|
let response;
|
|
9979
10079
|
let lastErr;
|
|
9980
10080
|
let inFlightWaits = 0;
|
|
9981
10081
|
let errorBodyOverride = null;
|
|
9982
10082
|
let forbiddenExhausted = false;
|
|
10083
|
+
let refreshedThisAttempt = false;
|
|
10084
|
+
let sessionExpired = false;
|
|
9983
10085
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
9984
10086
|
try {
|
|
10087
|
+
errorBodyOverride = null;
|
|
9985
10088
|
response = await (0, node_fetch_1.default)(...buildFetchArgs());
|
|
9986
10089
|
if (response.status === 429 && attempt < MAX_RETRIES && canRetry()) {
|
|
9987
10090
|
reportRetry("Rate limited by the API \u2014 retrying");
|
|
@@ -9994,9 +10097,30 @@ var require_client = __commonJS({
|
|
|
9994
10097
|
await sleepWithinBudget(backoffMs(attempt));
|
|
9995
10098
|
continue;
|
|
9996
10099
|
}
|
|
10100
|
+
if (response.status === 401 || response.status === 403) {
|
|
10101
|
+
const bodyText = await response.text().catch(() => "");
|
|
10102
|
+
if (isExpiredTokenResponse(response.status, bodyText)) {
|
|
10103
|
+
if (!refreshedThisAttempt) {
|
|
10104
|
+
refreshedThisAttempt = true;
|
|
10105
|
+
if (await refreshAccessToken()) {
|
|
10106
|
+
reportRetry("Session expired \u2014 signing you back in");
|
|
10107
|
+
attempt--;
|
|
10108
|
+
continue;
|
|
10109
|
+
}
|
|
10110
|
+
}
|
|
10111
|
+
sessionExpired = true;
|
|
10112
|
+
errorBodyOverride = JSON.stringify({
|
|
10113
|
+
// Replaces the backend's bare "Invalid or expired token", which reads
|
|
10114
|
+
// like a bug rather than something the user can act on.
|
|
10115
|
+
error: "Your session has expired. Run `nex login` to sign in again."
|
|
10116
|
+
});
|
|
10117
|
+
break;
|
|
10118
|
+
}
|
|
10119
|
+
errorBodyOverride = bodyText;
|
|
10120
|
+
}
|
|
9997
10121
|
const FORBIDDEN_RETRY_LIMIT = 2;
|
|
9998
10122
|
if (response.status === 403) {
|
|
9999
|
-
const bodyText = await response.text().catch(() => "");
|
|
10123
|
+
const bodyText = errorBodyOverride ?? await response.text().catch(() => "");
|
|
10000
10124
|
let isUpstreamForbidden = false;
|
|
10001
10125
|
try {
|
|
10002
10126
|
const parsed = JSON.parse(bodyText);
|
|
@@ -10016,35 +10140,22 @@ var require_client = __commonJS({
|
|
|
10016
10140
|
errorBodyOverride = bodyText;
|
|
10017
10141
|
break;
|
|
10018
10142
|
}
|
|
10019
|
-
if (response.status
|
|
10020
|
-
await response.text().catch(() => "");
|
|
10021
|
-
|
|
10022
|
-
|
|
10023
|
-
|
|
10024
|
-
|
|
10025
|
-
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
|
|
10034
|
-
}
|
|
10035
|
-
if (hadRendered) {
|
|
10036
|
-
onEvent({
|
|
10037
|
-
type: "stream_restart",
|
|
10038
|
-
reason: "Connection lost too long to resume",
|
|
10039
|
-
discardedChars: renderedChars
|
|
10040
|
-
});
|
|
10041
|
-
}
|
|
10042
|
-
if (attempt < MAX_RETRIES && canRetry()) {
|
|
10043
|
-
reportRetry("Could not resume \u2014 restarting this turn");
|
|
10044
|
-
await sleepWithinBudget(backoffMs(attempt));
|
|
10045
|
-
continue;
|
|
10143
|
+
if (resuming && response.status >= 400 && response.status < 500) {
|
|
10144
|
+
const body = errorBodyOverride ?? await response.text().catch(() => "");
|
|
10145
|
+
if (response.status === 410 || response.status === 404 || bodySaysNotResumable(body)) {
|
|
10146
|
+
const canRestart = await prepareRestart("Connection lost too long to resume");
|
|
10147
|
+
if (!canRestart) {
|
|
10148
|
+
throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
|
|
10149
|
+
}
|
|
10150
|
+
if (attempt < MAX_RETRIES && canRetry()) {
|
|
10151
|
+
reportRetry("Could not resume \u2014 restarting this turn");
|
|
10152
|
+
await sleepWithinBudget(backoffMs(attempt));
|
|
10153
|
+
continue;
|
|
10154
|
+
}
|
|
10155
|
+
errorBodyOverride = JSON.stringify({ error: "Could not resume this turn \u2014 send your message again." });
|
|
10156
|
+
break;
|
|
10046
10157
|
}
|
|
10047
|
-
errorBodyOverride =
|
|
10158
|
+
errorBodyOverride = body;
|
|
10048
10159
|
break;
|
|
10049
10160
|
}
|
|
10050
10161
|
if (response.status === 409) {
|
|
@@ -10096,7 +10207,12 @@ var require_client = __commonJS({
|
|
|
10096
10207
|
} catch {
|
|
10097
10208
|
errMsg = errText || errMsg;
|
|
10098
10209
|
}
|
|
10099
|
-
throw Object.assign(new Error(errMsg), {
|
|
10210
|
+
throw Object.assign(new Error(errMsg), {
|
|
10211
|
+
status: response.status,
|
|
10212
|
+
balance,
|
|
10213
|
+
...forbiddenExhausted ? { retryable: true } : {},
|
|
10214
|
+
...sessionExpired ? { authExpired: true } : {}
|
|
10215
|
+
});
|
|
10100
10216
|
}
|
|
10101
10217
|
if (!response.body) {
|
|
10102
10218
|
throw new Error("Response body is null");
|
|
@@ -10321,6 +10437,8 @@ var require_client = __commonJS({
|
|
|
10321
10437
|
stream.destroy?.();
|
|
10322
10438
|
reject(tagTransient(new Error(message)));
|
|
10323
10439
|
} else {
|
|
10440
|
+
clearInterval(heartbeatWatchdog);
|
|
10441
|
+
stream.destroy?.();
|
|
10324
10442
|
onEvent({ type: "error", message });
|
|
10325
10443
|
reject(new Error(message));
|
|
10326
10444
|
}
|
|
@@ -10381,27 +10499,10 @@ var require_client = __commonJS({
|
|
|
10381
10499
|
const e2 = err;
|
|
10382
10500
|
if (e2.retryable && sAttempt < MAX_RETRIES && canRetry()) {
|
|
10383
10501
|
if (e2.forceRestart) {
|
|
10384
|
-
await
|
|
10385
|
-
|
|
10386
|
-
resuming = false;
|
|
10387
|
-
serverResumable = false;
|
|
10388
|
-
lastEventId = 0;
|
|
10389
|
-
carryText = [];
|
|
10390
|
-
carryToolUse = [];
|
|
10391
|
-
const hadRendered = emittedAnythingAcrossAttempts;
|
|
10392
|
-
const renderedChars = emittedCharsAcrossAttempts;
|
|
10393
|
-
emittedAnythingAcrossAttempts = false;
|
|
10394
|
-
emittedCharsAcrossAttempts = 0;
|
|
10395
|
-
if (hadRendered && !allowRestartAfterRender) {
|
|
10502
|
+
const canRestart = await prepareRestart(err.message || "Could not resume \u2014 restarting this turn");
|
|
10503
|
+
if (!canRestart) {
|
|
10396
10504
|
throw new Error("Connection lost and this turn could no longer be resumed. Send your message again.");
|
|
10397
10505
|
}
|
|
10398
|
-
if (hadRendered) {
|
|
10399
|
-
onEvent({
|
|
10400
|
-
type: "stream_restart",
|
|
10401
|
-
reason: err.message || "Could not resume \u2014 restarting this turn",
|
|
10402
|
-
discardedChars: renderedChars
|
|
10403
|
-
});
|
|
10404
|
-
}
|
|
10405
10506
|
reportRetry("Could not resume \u2014 restarting this turn");
|
|
10406
10507
|
await sleepWithinBudget(backoffMs(sAttempt));
|
|
10407
10508
|
continue;
|
|
@@ -10445,11 +10546,30 @@ var require_client = __commonJS({
|
|
|
10445
10546
|
} catch {
|
|
10446
10547
|
}
|
|
10447
10548
|
}
|
|
10549
|
+
async function revokeRefreshToken2() {
|
|
10550
|
+
const refreshToken = (0, index_1.getRefreshToken)();
|
|
10551
|
+
if (!refreshToken)
|
|
10552
|
+
return;
|
|
10553
|
+
try {
|
|
10554
|
+
await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/logout`, {
|
|
10555
|
+
method: "POST",
|
|
10556
|
+
headers: { "Content-Type": "application/json" },
|
|
10557
|
+
body: JSON.stringify({ refreshToken })
|
|
10558
|
+
});
|
|
10559
|
+
} catch {
|
|
10560
|
+
}
|
|
10561
|
+
}
|
|
10448
10562
|
async function getBalance3() {
|
|
10449
|
-
const
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10563
|
+
const fetchOnce = () => (0, node_fetch_1.default)(`${exports.API_BASE}/api/code/balance`, { method: "GET", headers: authHeaders() });
|
|
10564
|
+
let response = await fetchOnce();
|
|
10565
|
+
if (response.status === 401 || response.status === 403) {
|
|
10566
|
+
const body = await response.text().catch(() => "");
|
|
10567
|
+
if (isExpiredTokenResponse(response.status, body) && await refreshAccessToken()) {
|
|
10568
|
+
response = await fetchOnce();
|
|
10569
|
+
} else {
|
|
10570
|
+
throw new Error(`API error ${response.status}: ${body}`);
|
|
10571
|
+
}
|
|
10572
|
+
}
|
|
10453
10573
|
if (!response.ok) {
|
|
10454
10574
|
const errText = await response.text();
|
|
10455
10575
|
throw new Error(`API error ${response.status}: ${errText}`);
|
|
@@ -10470,7 +10590,11 @@ var require_client = __commonJS({
|
|
|
10470
10590
|
const data = await response.json();
|
|
10471
10591
|
if (!data.token)
|
|
10472
10592
|
throw new Error("No token in exchange response");
|
|
10473
|
-
return {
|
|
10593
|
+
return {
|
|
10594
|
+
token: data.token,
|
|
10595
|
+
...data.refreshToken ? { refreshToken: data.refreshToken } : {},
|
|
10596
|
+
email: data.user?.email ?? ""
|
|
10597
|
+
};
|
|
10474
10598
|
}
|
|
10475
10599
|
async function login2(email, password) {
|
|
10476
10600
|
const response = await (0, node_fetch_1.default)(`${exports.API_BASE}/api/auth/login/email`, {
|
|
@@ -10486,7 +10610,11 @@ var require_client = __commonJS({
|
|
|
10486
10610
|
if (!data.token) {
|
|
10487
10611
|
throw new Error("Login response missing token");
|
|
10488
10612
|
}
|
|
10489
|
-
return {
|
|
10613
|
+
return {
|
|
10614
|
+
token: data.token,
|
|
10615
|
+
...data.refreshToken ? { refreshToken: data.refreshToken } : {},
|
|
10616
|
+
email: data.email ?? email
|
|
10617
|
+
};
|
|
10490
10618
|
}
|
|
10491
10619
|
}
|
|
10492
10620
|
});
|
|
@@ -10691,6 +10819,144 @@ var require_editCompleteness = __commonJS({
|
|
|
10691
10819
|
}
|
|
10692
10820
|
});
|
|
10693
10821
|
|
|
10822
|
+
// ../core/dist/agent/securityLint.js
|
|
10823
|
+
var require_securityLint = __commonJS({
|
|
10824
|
+
"../core/dist/agent/securityLint.js"(exports) {
|
|
10825
|
+
"use strict";
|
|
10826
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10827
|
+
exports.checkSecurity = checkSecurity;
|
|
10828
|
+
exports.securityNoteText = securityNoteText;
|
|
10829
|
+
var SAMPLE_MAX = 160;
|
|
10830
|
+
function redact(line) {
|
|
10831
|
+
const masked = line.replace(/(['"`]?[\w.-]*(?:secret|password|passwd|token|api[_-]?key|apikey|auth|credential|private[_-]?key)[\w.-]*['"`]?\s*[:=]\s*)(['"`])([^'"`]{4,})\2/gi, (_m, head, q) => `${head}${q}[REDACTED]${q}`).replace(/\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g, "[REDACTED]");
|
|
10832
|
+
return masked.length > SAMPLE_MAX ? masked.slice(0, SAMPLE_MAX) + "\u2026" : masked;
|
|
10833
|
+
}
|
|
10834
|
+
function isLikelyCommentLine(line) {
|
|
10835
|
+
return /^\s*(\/\/|\*|#|--|<!--)/.test(line);
|
|
10836
|
+
}
|
|
10837
|
+
var RULES = [
|
|
10838
|
+
// ── Hardcoded credentials ───────────────────────────────────────────────────
|
|
10839
|
+
// Provider-prefixed tokens are near-zero false positive: the prefixes are
|
|
10840
|
+
// registered formats, not something that occurs naturally in source. Checked
|
|
10841
|
+
// inside comments too — a key commented out is still a committed key.
|
|
10842
|
+
{
|
|
10843
|
+
kind: "hardcoded-secret",
|
|
10844
|
+
re: /\b(sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})\b/,
|
|
10845
|
+
message: "Looks like a real API key/token committed to source. Move it to an environment variable and rotate the exposed key.",
|
|
10846
|
+
includeComments: true
|
|
10847
|
+
},
|
|
10848
|
+
// NOTE: the `private-key` check is NOT here — it needs to span multiple lines
|
|
10849
|
+
// (BEGIN header on one, base64 body on the next), which this per-line loop
|
|
10850
|
+
// cannot express. It runs separately in checkSecurity below.
|
|
10851
|
+
{
|
|
10852
|
+
kind: "hardcoded-password",
|
|
10853
|
+
// An ASSIGNMENT of a credential-ish name to a non-trivial literal.
|
|
10854
|
+
//
|
|
10855
|
+
// Tightened after measuring against the real backend, where the looser version
|
|
10856
|
+
// fired on `missingSecret:'FIREBASE_TOKEN'` — code that NAMES a secret in an
|
|
10857
|
+
// error message, the opposite of leaking one. So a value that is itself just a
|
|
10858
|
+
// SCREAMING_SNAKE identifier (an env-var name) is excluded, along with the
|
|
10859
|
+
// usual placeholder vocabulary. The value must also look like actual secret
|
|
10860
|
+
// material: mixed case or digits, not a lone lowercase word.
|
|
10861
|
+
re: /(?:password|passwd|secret|api[_-]?key|apikey|access[_-]?token)['"`]?\s*[:=]\s*['"`](?![A-Z0-9_]+['"`])(?!.*(?:\$\{|process\.env|os\.environ|example|changeme|placeholder|redacted|xxx|test|dummy|fake|sample|your[_-]?|<|\*{3}))(?=[^'"`]*[0-9A-Z])[^'"`\s]{10,}['"`]/,
|
|
10862
|
+
message: "Hardcoded credential literal. Read it from the environment/secret store instead, and rotate the exposed value.",
|
|
10863
|
+
includeComments: true
|
|
10864
|
+
},
|
|
10865
|
+
// ── Injection ───────────────────────────────────────────────────────────────
|
|
10866
|
+
{
|
|
10867
|
+
kind: "dynamic-eval",
|
|
10868
|
+
// The negative lookbehind for `.` is what makes this usable: `redisClient.eval`
|
|
10869
|
+
// (a Redis Lua script), `page.eval` (Playwright), `vm.eval` and friends are
|
|
10870
|
+
// METHOD calls on an object and have nothing to do with JavaScript's global
|
|
10871
|
+
// eval. Without it, the real backend's Redis idempotency script was flagged.
|
|
10872
|
+
// Only a bare `eval(` / `new Function(` with a non-literal argument counts.
|
|
10873
|
+
re: /(?<![.\w$])(?:eval|new\s+Function)\s*\(\s*(?!['"`][^'"`]*['"`]\s*\))[^)]*[a-zA-Z_$][\w$]*/,
|
|
10874
|
+
message: "eval / new Function on a non-literal value executes arbitrary code if that value is ever user-controlled. Use an explicit parser or a lookup table."
|
|
10875
|
+
},
|
|
10876
|
+
{
|
|
10877
|
+
kind: "sql-injection",
|
|
10878
|
+
// Only fires when the interpolated expression is plausibly REQUEST-DERIVED.
|
|
10879
|
+
//
|
|
10880
|
+
// The obvious pattern — any `${...}` inside a SQL string — was measured
|
|
10881
|
+
// against the real backend and flagged 15 of 183 files, essentially all of
|
|
10882
|
+
// them safe and idiomatic: `${sets.join(', ')}` for a dynamic UPDATE, `${field}`
|
|
10883
|
+
// for a server-chosen column, `${CONSUMPTION}` for a module constant. At that
|
|
10884
|
+
// hit rate the warning is pure noise, and noise is worse than silence because
|
|
10885
|
+
// it teaches everyone to skip the channel.
|
|
10886
|
+
//
|
|
10887
|
+
// So the interpolation must name something that plausibly came from the
|
|
10888
|
+
// outside: req/request/params/query/body/input/user/args, or a bare
|
|
10889
|
+
// `'...' + ident`. This trades recall for precision on purpose — thorough SQL
|
|
10890
|
+
// review is the security-auditor agent's job, not an inline regex's.
|
|
10891
|
+
re: /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM)\b[^;'"`]{0,160}(?:\$\{\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|['"`]\s*\+\s*(?:req|request|params?|query|body|input|user|args|ctx)\b|%\s*\(\s*(?:request|params?|query|body|input|user)\b)/i,
|
|
10892
|
+
message: "SQL built by interpolating a request-derived value. Use a parameterised query ($1 / ? placeholders) \u2014 this is the classic injection sink."
|
|
10893
|
+
},
|
|
10894
|
+
{
|
|
10895
|
+
kind: "command-injection",
|
|
10896
|
+
// Shell execution with an interpolated or concatenated argument.
|
|
10897
|
+
re: /\b(?:exec|execSync|spawnSync?|system|popen|os\.system|subprocess\.(?:call|run|Popen))\s*\(\s*(?:[`'"][^`'"]*(?:\$\{|['"]\s*\+)|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
10898
|
+
message: "Shell command built from a variable. Pass arguments as an array (no shell), or validate against an allowlist \u2014 a value containing ; or $() becomes command execution."
|
|
10899
|
+
},
|
|
10900
|
+
// ── Transport / verification ────────────────────────────────────────────────
|
|
10901
|
+
{
|
|
10902
|
+
kind: "tls-verification-disabled",
|
|
10903
|
+
re: /(?:rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0|verify\s*=\s*False|InsecureSkipVerify\s*:\s*true)/,
|
|
10904
|
+
message: "TLS certificate verification is disabled, which removes protection against man-in-the-middle attacks. Trust a specific CA instead if the cert is self-signed."
|
|
10905
|
+
}
|
|
10906
|
+
];
|
|
10907
|
+
function checkSecurity(content, max = 5) {
|
|
10908
|
+
if (!content)
|
|
10909
|
+
return [];
|
|
10910
|
+
const findings = [];
|
|
10911
|
+
const lines = content.split(/\r?\n/);
|
|
10912
|
+
const pemIdx = lines.findIndex((l) => /-----BEGIN\s+(?:RSA|EC|DSA|OPENSSH|PGP)?\s*PRIVATE KEY-----/.test(l));
|
|
10913
|
+
if (pemIdx !== -1) {
|
|
10914
|
+
const following = lines.slice(pemIdx, pemIdx + 4).join("\n");
|
|
10915
|
+
if (/[A-Za-z0-9+/]{40,}/.test(following.replace(/-----[^-]+-----/g, ""))) {
|
|
10916
|
+
findings.push({
|
|
10917
|
+
kind: "private-key",
|
|
10918
|
+
message: "A private key with real key material is being written into source. Store it outside the repo (secret manager / env var) and rotate it.",
|
|
10919
|
+
line: pemIdx + 1,
|
|
10920
|
+
sample: "-----BEGIN PRIVATE KEY----- [REDACTED]"
|
|
10921
|
+
});
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
10924
|
+
const seenKinds = /* @__PURE__ */ new Set();
|
|
10925
|
+
for (let i2 = 0; i2 < lines.length && findings.length < max; i2++) {
|
|
10926
|
+
const line = lines[i2];
|
|
10927
|
+
if (!line || line.length > 2e3)
|
|
10928
|
+
continue;
|
|
10929
|
+
const commentish = isLikelyCommentLine(line);
|
|
10930
|
+
for (const rule of RULES) {
|
|
10931
|
+
if (seenKinds.has(rule.kind))
|
|
10932
|
+
continue;
|
|
10933
|
+
if (commentish && !rule.includeComments)
|
|
10934
|
+
continue;
|
|
10935
|
+
if (!rule.re.test(line))
|
|
10936
|
+
continue;
|
|
10937
|
+
seenKinds.add(rule.kind);
|
|
10938
|
+
findings.push({ kind: rule.kind, message: rule.message, line: i2 + 1, sample: redact(line.trim()) });
|
|
10939
|
+
break;
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
return findings;
|
|
10943
|
+
}
|
|
10944
|
+
function securityNoteText(findings) {
|
|
10945
|
+
if (process.env.NEXRALL_SECURITY_LINT === "off")
|
|
10946
|
+
return "";
|
|
10947
|
+
if (!findings.length)
|
|
10948
|
+
return "";
|
|
10949
|
+
const lines = findings.map((f3) => ` \u2022 line ${f3.line} [${f3.kind}]: ${f3.message}
|
|
10950
|
+
${f3.sample}`);
|
|
10951
|
+
return `
|
|
10952
|
+
|
|
10953
|
+
\u26A0 SECURITY REVIEW (${findings.length} finding${findings.length > 1 ? "s" : ""}) \u2014 the write succeeded; check these before moving on:
|
|
10954
|
+
` + lines.join("\n") + `
|
|
10955
|
+
If a finding is a false positive (test fixture, placeholder, intentionally dynamic), say so and continue \u2014 do NOT rewrite correct code to silence it.`;
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
});
|
|
10959
|
+
|
|
10694
10960
|
// ../core/dist/agent/memory.js
|
|
10695
10961
|
var require_memory = __commonJS({
|
|
10696
10962
|
"../core/dist/agent/memory.js"(exports) {
|
|
@@ -12354,6 +12620,7 @@ var require_executor = __commonJS({
|
|
|
12354
12620
|
var sandbox_1 = require_sandbox();
|
|
12355
12621
|
var auth_1 = require_auth();
|
|
12356
12622
|
var editCompleteness_1 = require_editCompleteness();
|
|
12623
|
+
var securityLint_1 = require_securityLint();
|
|
12357
12624
|
var memory_1 = require_memory();
|
|
12358
12625
|
var skills_1 = require_skills();
|
|
12359
12626
|
var testIntegrity_1 = require_testIntegrity();
|
|
@@ -12606,24 +12873,19 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
|
|
|
12606
12873
|
} catch {
|
|
12607
12874
|
}
|
|
12608
12875
|
}
|
|
12609
|
-
|
|
12610
|
-
if (existingMode !== void 0) {
|
|
12611
|
-
try {
|
|
12612
|
-
fs9.chmodSync(resolved, existingMode);
|
|
12613
|
-
} catch {
|
|
12614
|
-
}
|
|
12615
|
-
}
|
|
12876
|
+
atomicWrite(resolved, content, existingMode);
|
|
12616
12877
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
12617
12878
|
const lines = content.split("\n").length;
|
|
12879
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
|
|
12618
12880
|
if (isNew) {
|
|
12619
|
-
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
|
|
12881
|
+
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
|
|
12620
12882
|
}
|
|
12621
12883
|
const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
|
|
12622
12884
|
let tiMarker = "";
|
|
12623
12885
|
const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
|
|
12624
12886
|
if (ti.suspicious)
|
|
12625
12887
|
tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
|
|
12626
|
-
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
|
|
12888
|
+
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
|
|
12627
12889
|
} catch (err) {
|
|
12628
12890
|
return { error: err.message };
|
|
12629
12891
|
}
|
|
@@ -13326,14 +13588,52 @@ ${globalCapMatches(output)}` : "";
|
|
|
13326
13588
|
}
|
|
13327
13589
|
return { mode: stat2.mode, content: fs9.readFileSync(resolved, "utf-8") };
|
|
13328
13590
|
}
|
|
13329
|
-
function
|
|
13330
|
-
|
|
13331
|
-
fs9.writeFileSync(tmp, data, "utf-8");
|
|
13591
|
+
function atomicWrite(resolved, data, mode) {
|
|
13592
|
+
let target = resolved;
|
|
13332
13593
|
try {
|
|
13333
|
-
fs9.
|
|
13594
|
+
if (fs9.lstatSync(resolved).isSymbolicLink())
|
|
13595
|
+
target = fs9.realpathSync(resolved);
|
|
13334
13596
|
} catch {
|
|
13335
13597
|
}
|
|
13336
|
-
|
|
13598
|
+
const tmp = `${target}.nexrall_tmp_${process.pid}`;
|
|
13599
|
+
try {
|
|
13600
|
+
fs9.writeFileSync(tmp, data, "utf-8");
|
|
13601
|
+
} catch (err) {
|
|
13602
|
+
const code = err.code;
|
|
13603
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") {
|
|
13604
|
+
fs9.writeFileSync(target, data, "utf-8");
|
|
13605
|
+
if (mode !== void 0) {
|
|
13606
|
+
try {
|
|
13607
|
+
fs9.chmodSync(target, mode);
|
|
13608
|
+
} catch {
|
|
13609
|
+
}
|
|
13610
|
+
}
|
|
13611
|
+
return;
|
|
13612
|
+
}
|
|
13613
|
+
try {
|
|
13614
|
+
fs9.rmSync(tmp, { force: true });
|
|
13615
|
+
} catch {
|
|
13616
|
+
}
|
|
13617
|
+
throw err;
|
|
13618
|
+
}
|
|
13619
|
+
try {
|
|
13620
|
+
if (mode !== void 0) {
|
|
13621
|
+
try {
|
|
13622
|
+
fs9.chmodSync(tmp, mode);
|
|
13623
|
+
} catch {
|
|
13624
|
+
}
|
|
13625
|
+
}
|
|
13626
|
+
fs9.renameSync(tmp, target);
|
|
13627
|
+
} catch (err) {
|
|
13628
|
+
try {
|
|
13629
|
+
fs9.rmSync(tmp, { force: true });
|
|
13630
|
+
} catch {
|
|
13631
|
+
}
|
|
13632
|
+
throw err;
|
|
13633
|
+
}
|
|
13634
|
+
}
|
|
13635
|
+
function atomicWritePreservingMode(resolved, data, mode) {
|
|
13636
|
+
atomicWrite(resolved, data, mode);
|
|
13337
13637
|
}
|
|
13338
13638
|
function normalizeLF(s2) {
|
|
13339
13639
|
return s2.replace(/\r\n/g, "\n");
|
|
@@ -13371,9 +13671,10 @@ ${globalCapMatches(output)}` : "";
|
|
|
13371
13671
|
const linesBefore = origNorm.split("\n").length;
|
|
13372
13672
|
const linesAfter = updated.split("\n").length;
|
|
13373
13673
|
const xfile = crossFileBreakageWarning(resolved, origNorm, normalizeLF(updated), workDir ?? process.cwd());
|
|
13674
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
|
|
13374
13675
|
return { output: `Edited ${resolved} (${linesBefore} \u2192 ${linesAfter} lines)
|
|
13375
13676
|
|
|
13376
|
-
${diff3}${xfile}` };
|
|
13677
|
+
${diff3}${xfile}${sec}` };
|
|
13377
13678
|
} catch (err) {
|
|
13378
13679
|
return { error: err.message };
|
|
13379
13680
|
}
|
|
@@ -13641,6 +13942,7 @@ ${body}${truncNote}
|
|
|
13641
13942
|
let content = normalizeLF(rawFile);
|
|
13642
13943
|
const originalNorm = content;
|
|
13643
13944
|
const diffs = [];
|
|
13945
|
+
const insertedText = [];
|
|
13644
13946
|
for (let i2 = 0; i2 < edits.length; i2++) {
|
|
13645
13947
|
const edit = edits[i2];
|
|
13646
13948
|
const oldStr = normalizeLF(typeof edit.old_string === "string" ? edit.old_string : "");
|
|
@@ -13655,16 +13957,18 @@ ${body}${truncNote}
|
|
|
13655
13957
|
return { error: `Edit #${i2 + 1}: old_string appears ${count} times \u2014 it must be unique. Add more surrounding context.` };
|
|
13656
13958
|
}
|
|
13657
13959
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
13960
|
+
insertedText.push(newStr);
|
|
13658
13961
|
content = literalReplace(content, oldStr, newStr);
|
|
13659
13962
|
}
|
|
13660
13963
|
const finalContent = wasCRLF ? content.replace(/\n/g, "\r\n") : content;
|
|
13661
13964
|
atomicWritePreservingMode(resolved, finalContent, pre.mode);
|
|
13662
13965
|
const xfile = crossFileBreakageWarning(resolved, originalNorm, content, workDir ?? process.cwd());
|
|
13966
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join("\n")));
|
|
13663
13967
|
return {
|
|
13664
13968
|
output: `Applied ${edits.length} edit(s) to ${resolved}:
|
|
13665
13969
|
` + diffs.map((d, i2) => `
|
|
13666
13970
|
--- edit #${i2 + 1} ---
|
|
13667
|
-
${d}`).join("\n") + xfile
|
|
13971
|
+
${d}`).join("\n") + xfile + sec
|
|
13668
13972
|
};
|
|
13669
13973
|
} catch (err) {
|
|
13670
13974
|
return { error: err.message };
|
|
@@ -14245,11 +14549,40 @@ var require_agentTypes = __commonJS({
|
|
|
14245
14549
|
var path6 = __importStar(__require("path"));
|
|
14246
14550
|
var os6 = __importStar(__require("os"));
|
|
14247
14551
|
var index_1 = require_plugins();
|
|
14552
|
+
var READ_ONLY_TOOLS = [
|
|
14553
|
+
// Universal
|
|
14554
|
+
"read_file",
|
|
14555
|
+
"search_files",
|
|
14556
|
+
"glob",
|
|
14557
|
+
"list_directory",
|
|
14558
|
+
"bash",
|
|
14559
|
+
"bash_output",
|
|
14560
|
+
"notebook_read",
|
|
14561
|
+
"todo_write",
|
|
14562
|
+
"todo_read",
|
|
14563
|
+
// VS Code language server (ignored on the CLI)
|
|
14564
|
+
"get_symbols",
|
|
14565
|
+
"get_workspace_symbols",
|
|
14566
|
+
"find_references",
|
|
14567
|
+
"go_to_definition",
|
|
14568
|
+
"get_hover",
|
|
14569
|
+
"get_diagnostics"
|
|
14570
|
+
];
|
|
14571
|
+
var RESEARCH_TOOLS = [...READ_ONLY_TOOLS, "web_search", "fetch_url"];
|
|
14572
|
+
var WRITE_TOOLS = [
|
|
14573
|
+
...READ_ONLY_TOOLS,
|
|
14574
|
+
"write_file",
|
|
14575
|
+
"edit_file",
|
|
14576
|
+
"multi_edit",
|
|
14577
|
+
"create_directory",
|
|
14578
|
+
"move_file",
|
|
14579
|
+
"copy_file"
|
|
14580
|
+
];
|
|
14248
14581
|
var BUILTIN_AGENTS = [
|
|
14249
14582
|
{
|
|
14250
14583
|
name: "reviewer",
|
|
14251
14584
|
description: "Read-only code reviewer \u2014 finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.",
|
|
14252
|
-
tools:
|
|
14585
|
+
tools: READ_ONLY_TOOLS,
|
|
14253
14586
|
source: "builtin",
|
|
14254
14587
|
prompt: [
|
|
14255
14588
|
"You are a meticulous senior code reviewer. You NEVER modify files \u2014 you only read, search, and report.",
|
|
@@ -14265,6 +14598,162 @@ var require_agentTypes = __commonJS({
|
|
|
14265
14598
|
"Report format: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F7E2} Suggestion, each with file:line and a concrete fix,",
|
|
14266
14599
|
"then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale."
|
|
14267
14600
|
].join("\n")
|
|
14601
|
+
},
|
|
14602
|
+
// Promoted from the security-audit plugin to a builtin.
|
|
14603
|
+
//
|
|
14604
|
+
// Leaving it plugin-only was indefensible next to `reviewer` being builtin:
|
|
14605
|
+
// reviewer's own prompt already tells it to look for security issues, so
|
|
14606
|
+
// security IS treated as default work — yet the specialist agent for it was
|
|
14607
|
+
// invisible unless the user happened to know the plugin existed. For an agent
|
|
14608
|
+
// that WRITES code, "you only get a security review if you knew to install
|
|
14609
|
+
// something" is the wrong default.
|
|
14610
|
+
{
|
|
14611
|
+
name: "security-auditor",
|
|
14612
|
+
description: "Read-only security auditor \u2014 hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.",
|
|
14613
|
+
tools: READ_ONLY_TOOLS,
|
|
14614
|
+
model: "pro",
|
|
14615
|
+
source: "builtin",
|
|
14616
|
+
prompt: [
|
|
14617
|
+
"You are a security auditor. You find real, exploitable flaws \u2014 not style issues.",
|
|
14618
|
+
"",
|
|
14619
|
+
"Method:",
|
|
14620
|
+
"1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network",
|
|
14621
|
+
" input, deserialization), then trace user-controlled data inward to where it is used.",
|
|
14622
|
+
"2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.",
|
|
14623
|
+
"3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;",
|
|
14624
|
+
" Medium = needs unusual preconditions; Low = hardening.",
|
|
14625
|
+
"",
|
|
14626
|
+
"Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken",
|
|
14627
|
+
"authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,",
|
|
14628
|
+
"path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,",
|
|
14629
|
+
"and crypto misuse (hand-rolled comparison, predictable randomness).",
|
|
14630
|
+
"",
|
|
14631
|
+
"Hard rules:",
|
|
14632
|
+
"- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.",
|
|
14633
|
+
"- NEVER print a discovered secret's value. Report its location and advise rotation.",
|
|
14634
|
+
"- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.",
|
|
14635
|
+
'- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.'
|
|
14636
|
+
].join("\n")
|
|
14637
|
+
},
|
|
14638
|
+
// The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
|
|
14639
|
+
// search that would otherwise flood the parent's context. Defaults to the
|
|
14640
|
+
// cheapest model on purpose — "find every caller of X" has no need of a
|
|
14641
|
+
// frontier model, and this is the agent most likely to be spawned in bulk.
|
|
14642
|
+
{
|
|
14643
|
+
name: "explorer",
|
|
14644
|
+
description: "Fast read-only codebase explorer \u2014 locates files, symbols, and call sites and reports concise findings. Use to keep bulk searching out of the main context. Cannot modify files.",
|
|
14645
|
+
tools: READ_ONLY_TOOLS,
|
|
14646
|
+
model: "turbo",
|
|
14647
|
+
source: "builtin",
|
|
14648
|
+
prompt: [
|
|
14649
|
+
"You map code. You NEVER modify anything.",
|
|
14650
|
+
"",
|
|
14651
|
+
"Method:",
|
|
14652
|
+
"1. Prefer structural search over text search where available (get_workspace_symbols, find_references,",
|
|
14653
|
+
" go_to_definition); fall back to search_files/glob otherwise.",
|
|
14654
|
+
"2. Read only the sections you need \u2014 use read_file with offset/limit on large files instead of",
|
|
14655
|
+
" pulling in thousands of lines.",
|
|
14656
|
+
"3. Follow the real call graph rather than guessing from names.",
|
|
14657
|
+
"",
|
|
14658
|
+
"Your ONLY output is a compact report: the file:line locations that matter, how they relate, and the",
|
|
14659
|
+
"direct answer to the question you were given. This exists to keep bulk search OUT of the parent's",
|
|
14660
|
+
"context, so do not paste large file contents back \u2014 cite locations and summarise. Say plainly when",
|
|
14661
|
+
'something does not exist; a confident wrong answer is far worse than "not found".'
|
|
14662
|
+
].join("\n")
|
|
14663
|
+
},
|
|
14664
|
+
// Matches Claude Code's built-in `Plan`: research a change and return a
|
|
14665
|
+
// strategy, deliberately WITHOUT write access so "make a plan" can never
|
|
14666
|
+
// quietly become "start editing".
|
|
14667
|
+
{
|
|
14668
|
+
name: "planner",
|
|
14669
|
+
description: "Read-only planning agent \u2014 researches a change and returns a concrete step-by-step implementation plan with risks and affected files. Cannot modify files.",
|
|
14670
|
+
tools: RESEARCH_TOOLS,
|
|
14671
|
+
model: "pro",
|
|
14672
|
+
source: "builtin",
|
|
14673
|
+
prompt: [
|
|
14674
|
+
"You produce implementation plans. You NEVER modify files \u2014 planning and doing are separate steps,",
|
|
14675
|
+
'and this agent exists so "plan it" cannot silently turn into "change it".',
|
|
14676
|
+
"",
|
|
14677
|
+
"Method:",
|
|
14678
|
+
"1. Read the actual code before proposing anything. No plan may rest on an assumed API shape.",
|
|
14679
|
+
"2. Find every affected call site (find_references / search_files) and list them.",
|
|
14680
|
+
"3. Order the steps so the tree stays working after each one \u2014 types, then implementation, then",
|
|
14681
|
+
" tests, then exports/registration.",
|
|
14682
|
+
"",
|
|
14683
|
+
"Output:",
|
|
14684
|
+
"- Goal, in one sentence.",
|
|
14685
|
+
"- Numbered steps, each with the exact files touched and what changes in them.",
|
|
14686
|
+
"- Risks + the specific thing that could break, and how it would be detected.",
|
|
14687
|
+
"- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).",
|
|
14688
|
+
"- Anything genuinely ambiguous, stated as an open question rather than a silent assumption."
|
|
14689
|
+
].join("\n")
|
|
14690
|
+
},
|
|
14691
|
+
// Promoted from the test-gen plugin. Needs write access — it produces test
|
|
14692
|
+
// files — but is deliberately forbidden from touching source, because "make the
|
|
14693
|
+
// tests pass" is the single most common way an agent destroys signal.
|
|
14694
|
+
{
|
|
14695
|
+
name: "test-writer",
|
|
14696
|
+
description: "Writes tests that follow the project's existing conventions. May create/edit TEST files only \u2014 never production source.",
|
|
14697
|
+
tools: WRITE_TOOLS,
|
|
14698
|
+
// Enforced, not merely requested: the permission gate refuses a write whose
|
|
14699
|
+
// path is not a test file. Without this the allowlist would grant edit_file
|
|
14700
|
+
// for every path and the rule below would be a suggestion the model is free
|
|
14701
|
+
// to rationalise its way past.
|
|
14702
|
+
testFilesOnly: true,
|
|
14703
|
+
source: "builtin",
|
|
14704
|
+
prompt: [
|
|
14705
|
+
"You write tests. You may create and edit TEST files only.",
|
|
14706
|
+
"",
|
|
14707
|
+
"Hard rules \u2014 these are the ways test-writing agents destroy value, so they are non-negotiable:",
|
|
14708
|
+
"- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.",
|
|
14709
|
+
"- NEVER weaken, delete or skip an existing assertion or test.",
|
|
14710
|
+
"- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.",
|
|
14711
|
+
"",
|
|
14712
|
+
"Method:",
|
|
14713
|
+
"1. Read the existing tests FIRST and copy their conventions exactly \u2014 runner, file naming, layout,",
|
|
14714
|
+
" assertion style, fixture/helper patterns. Never introduce a new framework.",
|
|
14715
|
+
"2. Test observable behaviour and the contract, not private internals.",
|
|
14716
|
+
"3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,",
|
|
14717
|
+
" boundaries, error paths, concurrency where it applies.",
|
|
14718
|
+
"4. No sleeps or wall-clock dependence \u2014 those produce the flaky tests that get deleted later.",
|
|
14719
|
+
"5. RUN the tests you wrote and report the real output. Never claim a test passes without running it."
|
|
14720
|
+
].join("\n")
|
|
14721
|
+
},
|
|
14722
|
+
// The DevOps gap — answered with a READ-ONLY advisor, not an operator.
|
|
14723
|
+
//
|
|
14724
|
+
// A "DevOps agent" with write/apply access is a genuinely different risk class
|
|
14725
|
+
// from the others here: its mistakes are `kubectl delete`, a bad `terraform
|
|
14726
|
+
// apply`, a broken deploy pipeline — often not revertible and affecting
|
|
14727
|
+
// production rather than a working tree. So this one diagnoses and proposes a
|
|
14728
|
+
// diff; a human applies it. That asymmetry is the whole design.
|
|
14729
|
+
{
|
|
14730
|
+
name: "devops-advisor",
|
|
14731
|
+
description: "Read-only CI/CD, container, and infrastructure advisor \u2014 diagnoses pipelines, Dockerfiles, and k8s manifests and proposes concrete fixes as a diff. Never applies changes.",
|
|
14732
|
+
tools: RESEARCH_TOOLS,
|
|
14733
|
+
model: "pro",
|
|
14734
|
+
source: "builtin",
|
|
14735
|
+
prompt: [
|
|
14736
|
+
"You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.",
|
|
14737
|
+
"",
|
|
14738
|
+
"Hard rules:",
|
|
14739
|
+
"- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only",
|
|
14740
|
+
" (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).",
|
|
14741
|
+
" NEVER run anything that mutates infrastructure \u2014 no apply/delete/scale/rollout/restart/push,",
|
|
14742
|
+
" no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.",
|
|
14743
|
+
"- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.",
|
|
14744
|
+
"",
|
|
14745
|
+
"Method:",
|
|
14746
|
+
"1. Read what actually exists \u2014 workflow files, Dockerfiles, manifests, kustomize overlays, the",
|
|
14747
|
+
' deploy scripts \u2014 before drawing any conclusion. Never reason from what a stack "usually" looks like.',
|
|
14748
|
+
"2. Follow the real path a change takes to production, and name the step that is broken or missing.",
|
|
14749
|
+
"3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually",
|
|
14750
|
+
" needs, image tags that do not match what is deployed, missing health probes, absent resource",
|
|
14751
|
+
" limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and",
|
|
14752
|
+
" migrations that must run before the new image is live.",
|
|
14753
|
+
"",
|
|
14754
|
+
"Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or",
|
|
14755
|
+
"exact file content, and the command a human should run to apply and verify it."
|
|
14756
|
+
].join("\n")
|
|
14268
14757
|
}
|
|
14269
14758
|
];
|
|
14270
14759
|
function parseFrontmatter(raw) {
|
|
@@ -14283,6 +14772,9 @@ var require_agentTypes = __commonJS({
|
|
|
14283
14772
|
const s2 = (v ?? "").toLowerCase();
|
|
14284
14773
|
return s2 === "turbo" || s2 === "pro" || s2 === "ultra" ? s2 : void 0;
|
|
14285
14774
|
}
|
|
14775
|
+
function parseBool(v) {
|
|
14776
|
+
return /^(true|yes|1|on)$/i.test((v ?? "").trim());
|
|
14777
|
+
}
|
|
14286
14778
|
function parseToolList(v) {
|
|
14287
14779
|
if (!v)
|
|
14288
14780
|
return void 0;
|
|
@@ -14311,7 +14803,12 @@ var require_agentTypes = __commonJS({
|
|
|
14311
14803
|
tools: parseToolList(meta.tools),
|
|
14312
14804
|
model: parseModel(meta.model),
|
|
14313
14805
|
prompt: body,
|
|
14314
|
-
source
|
|
14806
|
+
source,
|
|
14807
|
+
// Exposed to user/plugin definitions too — `test_files_only: true` (or
|
|
14808
|
+
// `testFilesOnly`) lets anyone build a test-writing agent that genuinely
|
|
14809
|
+
// cannot touch production source, rather than only the builtin getting
|
|
14810
|
+
// that guarantee.
|
|
14811
|
+
...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}
|
|
14315
14812
|
});
|
|
14316
14813
|
} catch {
|
|
14317
14814
|
}
|
|
@@ -14327,16 +14824,38 @@ var require_agentTypes = __commonJS({
|
|
|
14327
14824
|
if (!out.has(agent.name))
|
|
14328
14825
|
out.set(agent.name, agent);
|
|
14329
14826
|
}
|
|
14330
|
-
|
|
14827
|
+
const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i2) => [a.name, i2]));
|
|
14828
|
+
return [...out.values()].sort((a, b) => {
|
|
14829
|
+
const ai = builtinOrder.get(a.name);
|
|
14830
|
+
const bi = builtinOrder.get(b.name);
|
|
14831
|
+
if (ai !== void 0 && bi !== void 0)
|
|
14832
|
+
return ai - bi;
|
|
14833
|
+
if (ai !== void 0)
|
|
14834
|
+
return -1;
|
|
14835
|
+
if (bi !== void 0)
|
|
14836
|
+
return 1;
|
|
14837
|
+
return a.name.localeCompare(b.name);
|
|
14838
|
+
});
|
|
14331
14839
|
}
|
|
14332
14840
|
function summariseAgents(types3) {
|
|
14333
14841
|
if (!types3.length)
|
|
14334
14842
|
return "";
|
|
14335
14843
|
return types3.map((t2) => {
|
|
14336
|
-
const
|
|
14337
|
-
|
|
14844
|
+
const canWrite = !t2.tools || t2.tools.some((x2) => WRITE_TOOL_HINTS.has(x2));
|
|
14845
|
+
const access = t2.testFilesOnly ? "writes TEST files only" : canWrite ? "can modify files" : "read-only";
|
|
14846
|
+
const model = t2.model ? `, ${t2.model} model` : "";
|
|
14847
|
+
return `- ${t2.name} (${access}${model}): ${t2.description}`;
|
|
14338
14848
|
}).join("\n");
|
|
14339
14849
|
}
|
|
14850
|
+
var WRITE_TOOL_HINTS = /* @__PURE__ */ new Set([
|
|
14851
|
+
"write_file",
|
|
14852
|
+
"edit_file",
|
|
14853
|
+
"multi_edit",
|
|
14854
|
+
"notebook_edit",
|
|
14855
|
+
"delete_file",
|
|
14856
|
+
"move_file",
|
|
14857
|
+
"copy_file"
|
|
14858
|
+
]);
|
|
14340
14859
|
function findAgentType(types3, name) {
|
|
14341
14860
|
if (!name)
|
|
14342
14861
|
return void 0;
|
|
@@ -14738,9 +15257,14 @@ var require_loop = __commonJS({
|
|
|
14738
15257
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14739
15258
|
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
|
|
14740
15259
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
15260
|
+
exports.createLimiter = createLimiter;
|
|
15261
|
+
exports.extractSubTaskText = extractSubTaskText;
|
|
15262
|
+
exports.capSubTaskText = capSubTaskText;
|
|
15263
|
+
exports.summariseSubTaskProgress = summariseSubTaskProgress;
|
|
14741
15264
|
exports.contextWindowFor = contextWindowFor2;
|
|
14742
15265
|
exports.compactionThresholds = compactionThresholds2;
|
|
14743
15266
|
exports.estimateBodyBytes = estimateBodyBytes2;
|
|
15267
|
+
exports.allowsTestOnlyWrite = allowsTestOnlyWrite;
|
|
14744
15268
|
exports.findSafeCutIndex = findSafeCutIndex;
|
|
14745
15269
|
exports.transcriptOf = transcriptOf;
|
|
14746
15270
|
exports.createLedger = createLedger;
|
|
@@ -14900,6 +15424,29 @@ var require_loop = __commonJS({
|
|
|
14900
15424
|
_fileLocks.delete(absPath);
|
|
14901
15425
|
}
|
|
14902
15426
|
}
|
|
15427
|
+
var MAX_CONCURRENT_SUBTASKS = (() => {
|
|
15428
|
+
const raw = Number(process.env.NEXRALL_MAX_CONCURRENT_SUBTASKS);
|
|
15429
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 4;
|
|
15430
|
+
})();
|
|
15431
|
+
function createLimiter(max) {
|
|
15432
|
+
let active = 0;
|
|
15433
|
+
const queue = [];
|
|
15434
|
+
const release2 = () => {
|
|
15435
|
+
active--;
|
|
15436
|
+
queue.shift()?.();
|
|
15437
|
+
};
|
|
15438
|
+
return async (fn) => {
|
|
15439
|
+
if (active >= max)
|
|
15440
|
+
await new Promise((resolve3) => queue.push(resolve3));
|
|
15441
|
+
active++;
|
|
15442
|
+
try {
|
|
15443
|
+
return await fn();
|
|
15444
|
+
} finally {
|
|
15445
|
+
release2();
|
|
15446
|
+
}
|
|
15447
|
+
};
|
|
15448
|
+
}
|
|
15449
|
+
var _subTaskLimit = createLimiter(MAX_CONCURRENT_SUBTASKS);
|
|
14903
15450
|
function humanDescription(name, input) {
|
|
14904
15451
|
switch (name) {
|
|
14905
15452
|
case "read_file":
|
|
@@ -14981,16 +15528,71 @@ var require_loop = __commonJS({
|
|
|
14981
15528
|
return `Use tool: ${name}`;
|
|
14982
15529
|
}
|
|
14983
15530
|
}
|
|
14984
|
-
var MAX_TASK_DEPTH =
|
|
15531
|
+
var MAX_TASK_DEPTH = 1;
|
|
14985
15532
|
var _subTaskCounter = 0;
|
|
14986
15533
|
var SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0 ? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) : 10 * 60 * 1e3;
|
|
15534
|
+
var SUBTASK_MAX = 48e3;
|
|
15535
|
+
function sliceSafeEnd(s2, max) {
|
|
15536
|
+
if (s2.length <= max)
|
|
15537
|
+
return s2;
|
|
15538
|
+
let end = max;
|
|
15539
|
+
const code = s2.charCodeAt(end - 1);
|
|
15540
|
+
if (code >= 55296 && code <= 56319)
|
|
15541
|
+
end--;
|
|
15542
|
+
return s2.slice(0, end);
|
|
15543
|
+
}
|
|
15544
|
+
function sliceSafeStart(s2, from) {
|
|
15545
|
+
if (from <= 0)
|
|
15546
|
+
return s2;
|
|
15547
|
+
let start = from;
|
|
15548
|
+
const code = s2.charCodeAt(start);
|
|
15549
|
+
if (code >= 56320 && code <= 57343)
|
|
15550
|
+
start++;
|
|
15551
|
+
return s2.slice(start);
|
|
15552
|
+
}
|
|
15553
|
+
function extractSubTaskText(messages, preferLast = true) {
|
|
15554
|
+
const assistants = messages.filter((m2) => m2.role === "assistant");
|
|
15555
|
+
const textOf = (m2) => (m2?.content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("").trim();
|
|
15556
|
+
if (preferLast)
|
|
15557
|
+
return textOf(assistants[assistants.length - 1]);
|
|
15558
|
+
return assistants.map(textOf).filter(Boolean).join("\n\n").trim();
|
|
15559
|
+
}
|
|
15560
|
+
function capSubTaskText(text, max = SUBTASK_MAX) {
|
|
15561
|
+
if (text.length <= max)
|
|
15562
|
+
return text;
|
|
15563
|
+
const head = sliceSafeEnd(text, Math.floor(max * 0.6));
|
|
15564
|
+
const tail = sliceSafeStart(text, text.length - Math.floor(max * 0.4));
|
|
15565
|
+
return `${head}
|
|
15566
|
+
|
|
15567
|
+
[\u2026 sub-task output truncated (${text.length} chars) \u2014 kept the beginning and end \u2026]
|
|
15568
|
+
|
|
15569
|
+
${tail}`;
|
|
15570
|
+
}
|
|
15571
|
+
function summariseSubTaskProgress(messages) {
|
|
15572
|
+
const toolNames = [];
|
|
15573
|
+
for (const m2 of messages) {
|
|
15574
|
+
if (m2.role !== "assistant" || !Array.isArray(m2.content))
|
|
15575
|
+
continue;
|
|
15576
|
+
for (const b of m2.content) {
|
|
15577
|
+
if (b?.type === "tool_use" && typeof b.name === "string")
|
|
15578
|
+
toolNames.push(b.name);
|
|
15579
|
+
}
|
|
15580
|
+
}
|
|
15581
|
+
if (toolNames.length === 0)
|
|
15582
|
+
return "";
|
|
15583
|
+
const counts = /* @__PURE__ */ new Map();
|
|
15584
|
+
for (const n of toolNames)
|
|
15585
|
+
counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
15586
|
+
const inventory = [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
|
|
15587
|
+
return `Tool calls completed before it was stopped (${toolNames.length} total): ${inventory}.`;
|
|
15588
|
+
}
|
|
14987
15589
|
async function runSubTask(input, options, agentTypes) {
|
|
14988
15590
|
const prompt2 = typeof input.prompt === "string" ? input.prompt.trim() : "";
|
|
14989
15591
|
if (!prompt2)
|
|
14990
15592
|
return { error: "task tool requires a non-empty prompt" };
|
|
14991
15593
|
const depth = options._depth ?? 0;
|
|
14992
15594
|
if (depth >= MAX_TASK_DEPTH) {
|
|
14993
|
-
return { error:
|
|
15595
|
+
return { error: "Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it." };
|
|
14994
15596
|
}
|
|
14995
15597
|
const requestedType = typeof input.subagent_type === "string" ? input.subagent_type : "";
|
|
14996
15598
|
const agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
|
|
@@ -15008,6 +15610,8 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
|
|
|
15008
15610
|
const gatedPermission = async (req) => {
|
|
15009
15611
|
if (allowed && !allowed.has(req.tool))
|
|
15010
15612
|
return false;
|
|
15613
|
+
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
|
|
15614
|
+
return false;
|
|
15011
15615
|
return options.requestPermission(req);
|
|
15012
15616
|
};
|
|
15013
15617
|
const subMessages = [
|
|
@@ -15064,22 +15668,36 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
|
|
|
15064
15668
|
onThinkingProgress: (tok) => options.onThinkingProgress?.(tok)
|
|
15065
15669
|
});
|
|
15066
15670
|
if (subAbort.aborted && !options.abortSignal?.aborted) {
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
|
|
15076
|
-
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15671
|
+
const mins = Math.round(SUBTASK_TIMEOUT_MS / 6e4);
|
|
15672
|
+
const partial = capSubTaskText(extractSubTaskText(result, false));
|
|
15673
|
+
const progress = summariseSubTaskProgress(result);
|
|
15674
|
+
const sections = [
|
|
15675
|
+
`Sub-task STOPPED after ${mins} minutes without completing \u2014 treat the following as PARTIAL, unverified work, not a finished answer.`,
|
|
15676
|
+
progress,
|
|
15677
|
+
partial ? `Partial output before it was stopped:
|
|
15678
|
+
|
|
15679
|
+
${partial}` : "",
|
|
15680
|
+
"Do NOT simply re-run the same sub-task: build on what is above, or split the remaining work into smaller, more focused sub-tasks."
|
|
15681
|
+
].filter(Boolean);
|
|
15682
|
+
return { error: sections.join("\n\n") };
|
|
15683
|
+
}
|
|
15684
|
+
const text = capSubTaskText(extractSubTaskText(result, true));
|
|
15081
15685
|
return { output: text || "(sub-task completed with no text output)" };
|
|
15082
15686
|
} catch (err) {
|
|
15687
|
+
const salvaged = (0, types_1.salvageHistory)(err);
|
|
15688
|
+
if (salvaged) {
|
|
15689
|
+
const partial = capSubTaskText(extractSubTaskText(salvaged, false));
|
|
15690
|
+
const progress = summariseSubTaskProgress(salvaged);
|
|
15691
|
+
const sections = [
|
|
15692
|
+
`Sub-task FAILED before completing: ${err.message}`,
|
|
15693
|
+
progress,
|
|
15694
|
+
partial ? `Partial output before the failure:
|
|
15695
|
+
|
|
15696
|
+
${partial}` : "",
|
|
15697
|
+
"Treat the above as PARTIAL, unverified work. Build on it rather than re-running the whole sub-task."
|
|
15698
|
+
].filter(Boolean);
|
|
15699
|
+
return { error: sections.join("\n\n") };
|
|
15700
|
+
}
|
|
15083
15701
|
return { error: `Sub-task failed: ${err.message}` };
|
|
15084
15702
|
} finally {
|
|
15085
15703
|
clearTimeout(timer);
|
|
@@ -15138,6 +15756,15 @@ ${tail}`;
|
|
|
15138
15756
|
return true;
|
|
15139
15757
|
}
|
|
15140
15758
|
exports.WRITE_TOOL_NAMES = /* @__PURE__ */ new Set(["write_file", "edit_file", "multi_edit", "delete_file", "move_file", "copy_file", "notebook_edit"]);
|
|
15759
|
+
function allowsTestOnlyWrite(tool, input) {
|
|
15760
|
+
if (!exports.WRITE_TOOL_NAMES.has(tool))
|
|
15761
|
+
return true;
|
|
15762
|
+
const pathKeys = tool === "notebook_edit" ? ["path"] : ["path", "source", "dest", "destination"];
|
|
15763
|
+
const candidates = pathKeys.map((k) => input?.[k]).filter((v) => typeof v === "string" && v.length > 0);
|
|
15764
|
+
if (candidates.length === 0)
|
|
15765
|
+
return false;
|
|
15766
|
+
return candidates.every((p) => (0, testIntegrity_1.isTestFile)(p));
|
|
15767
|
+
}
|
|
15141
15768
|
exports.VERIFY_CMD_RE = /\b(npm|yarn|pnpm)\s+(run\s+)?(test|build|lint|typecheck|tsc)\b|\bpytest\b|\bgo\s+(test|vet|build)\b|\btsc\b|\beslint\b|\bcargo\s+(test|build|check)\b/i;
|
|
15142
15769
|
function findSafeCutIndex(messages, maxIdx) {
|
|
15143
15770
|
for (let i2 = Math.min(maxIdx, messages.length - 1); i2 >= 2; i2--) {
|
|
@@ -15678,7 +16305,7 @@ Continue the work from here.` }] });
|
|
|
15678
16305
|
if (!permitted) {
|
|
15679
16306
|
result = { error: "Permission denied by user" };
|
|
15680
16307
|
} else if (name === "task") {
|
|
15681
|
-
result = await runSubTask(input, options, agentTypes);
|
|
16308
|
+
result = depth === 0 ? await _subTaskLimit(() => runSubTask(input, options, agentTypes)) : await runSubTask(input, options, agentTypes);
|
|
15682
16309
|
} else {
|
|
15683
16310
|
const pre = runToolHooks(hooks.PreToolUse, "PreToolUse", name, input, options.workDir);
|
|
15684
16311
|
if (pre.block) {
|
|
@@ -17464,6 +18091,7 @@ var require_dist2 = __commonJS({
|
|
|
17464
18091
|
__exportStar(require_loop(), exports);
|
|
17465
18092
|
__exportStar(require_testIntegrity(), exports);
|
|
17466
18093
|
__exportStar(require_editCompleteness(), exports);
|
|
18094
|
+
__exportStar(require_securityLint(), exports);
|
|
17467
18095
|
__exportStar(require_crossFile(), exports);
|
|
17468
18096
|
__exportStar(require_flaky(), exports);
|
|
17469
18097
|
__exportStar(require_claimEvidence(), exports);
|
|
@@ -51904,6 +52532,7 @@ async function logoutCommand() {
|
|
|
51904
52532
|
console.log();
|
|
51905
52533
|
return;
|
|
51906
52534
|
}
|
|
52535
|
+
await (0, import_code_core.revokeRefreshToken)();
|
|
51907
52536
|
(0, import_code_core.clearAuth)();
|
|
51908
52537
|
console.log(source_default.green(" \u2713 Logged out."));
|
|
51909
52538
|
console.log();
|
|
@@ -62537,7 +63166,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
|
|
|
62537
63166
|
};
|
|
62538
63167
|
|
|
62539
63168
|
// src/commands/chat.ts
|
|
62540
|
-
var CLI_VERSION = "0.5.
|
|
63169
|
+
var CLI_VERSION = "0.5.49";
|
|
62541
63170
|
var MODEL_LABELS = {
|
|
62542
63171
|
turbo: "Nexrall Turbo",
|
|
62543
63172
|
pro: "Nexrall Pro",
|
|
@@ -62622,28 +63251,83 @@ ${content}`);
|
|
|
62622
63251
|
}
|
|
62623
63252
|
return parts.length > 0 ? parts.join("\n\n---\n\n") : void 0;
|
|
62624
63253
|
}
|
|
63254
|
+
var THINK_TAIL_CHARS = 48;
|
|
63255
|
+
function composeStatusLine(opts) {
|
|
63256
|
+
const timer = source_default.dim(` (${opts.elapsedSec}s)`);
|
|
63257
|
+
const hint = opts.hint !== false && !opts.detail && opts.elapsedSec >= 15 ? source_default.dim(" \xB7 still running, no output yet is normal for quiet commands") : "";
|
|
63258
|
+
const detail = opts.detail ? source_default.dim(` ${opts.detail}`) : "";
|
|
63259
|
+
return `${source_default.cyan(opts.frame)} ${source_default.dim(opts.text)}${timer}${detail}${hint}`;
|
|
63260
|
+
}
|
|
63261
|
+
function formatProgressTokens(tokens) {
|
|
63262
|
+
if (!Number.isFinite(tokens) || tokens <= 0)
|
|
63263
|
+
return "";
|
|
63264
|
+
return tokens >= 1e3 ? `~${(tokens / 1e3).toFixed(1)}k tokens` : `~${tokens} tokens`;
|
|
63265
|
+
}
|
|
62625
63266
|
var Spinner = class {
|
|
62626
63267
|
interval = null;
|
|
62627
63268
|
frames = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
|
|
62628
63269
|
i = 0;
|
|
62629
63270
|
startedAt = 0;
|
|
62630
63271
|
baseText = "";
|
|
62631
|
-
|
|
63272
|
+
// Extra live detail appended after the elapsed timer (e.g. "~1.2k tokens").
|
|
63273
|
+
// Kept separate from `baseText` and mutated through setDetail() so a stream of
|
|
63274
|
+
// progress events can refresh it WITHOUT restarting the spinner — calling
|
|
63275
|
+
// start() again would reset `startedAt` and the elapsed counter would sit at 0s
|
|
63276
|
+
// forever, which is precisely the "is it frozen?" signal this class exists to
|
|
63277
|
+
// remove.
|
|
63278
|
+
detail = "";
|
|
63279
|
+
// Whether the reassurance hint applies. Off for phases where slowness is
|
|
63280
|
+
// already explained by the detail text (e.g. a live token counter is itself
|
|
63281
|
+
// proof of life, so "no output yet is normal" would be noise).
|
|
63282
|
+
hintEnabled = true;
|
|
63283
|
+
start(text, opts) {
|
|
62632
63284
|
this.stop();
|
|
62633
63285
|
this.baseText = text;
|
|
63286
|
+
this.detail = "";
|
|
63287
|
+
this.hintEnabled = opts?.hint !== false;
|
|
62634
63288
|
this.startedAt = Date.now();
|
|
62635
|
-
this.
|
|
62636
|
-
|
|
62637
|
-
|
|
62638
|
-
|
|
62639
|
-
|
|
62640
|
-
|
|
62641
|
-
|
|
63289
|
+
this.render();
|
|
63290
|
+
this.interval = setInterval(() => this.render(), 100);
|
|
63291
|
+
}
|
|
63292
|
+
/**
|
|
63293
|
+
* Replace the trailing detail text in place, keeping the elapsed timer running.
|
|
63294
|
+
*
|
|
63295
|
+
* No-op when the spinner isn't active: progress events can arrive a tick after
|
|
63296
|
+
* something else (a tool row, the first text delta) legitimately stopped it, and
|
|
63297
|
+
* resurrecting the spinner there would fight the printed output for the status line.
|
|
63298
|
+
*/
|
|
63299
|
+
setDetail(detail) {
|
|
63300
|
+
if (this.interval === null)
|
|
63301
|
+
return;
|
|
63302
|
+
if (this.detail === detail)
|
|
63303
|
+
return;
|
|
63304
|
+
this.detail = detail;
|
|
63305
|
+
this.render();
|
|
63306
|
+
}
|
|
63307
|
+
/** Swap the label without resetting the elapsed timer (phase change within one turn). */
|
|
63308
|
+
setText(text) {
|
|
63309
|
+
if (this.interval === null)
|
|
63310
|
+
return;
|
|
63311
|
+
if (this.baseText === text)
|
|
63312
|
+
return;
|
|
63313
|
+
this.baseText = text;
|
|
63314
|
+
this.render();
|
|
63315
|
+
}
|
|
63316
|
+
render() {
|
|
63317
|
+
getInkTerminal()?.setLive(composeStatusLine({
|
|
63318
|
+
frame: this.frames[this.i % this.frames.length],
|
|
63319
|
+
text: this.baseText,
|
|
63320
|
+
elapsedSec: Math.floor((Date.now() - this.startedAt) / 1e3),
|
|
63321
|
+
detail: this.detail,
|
|
63322
|
+
hint: this.hintEnabled
|
|
63323
|
+
}));
|
|
63324
|
+
this.i++;
|
|
62642
63325
|
}
|
|
62643
63326
|
stop() {
|
|
62644
63327
|
if (this.interval !== null) {
|
|
62645
63328
|
clearInterval(this.interval);
|
|
62646
63329
|
this.interval = null;
|
|
63330
|
+
this.detail = "";
|
|
62647
63331
|
getInkTerminal()?.setLive("");
|
|
62648
63332
|
}
|
|
62649
63333
|
}
|
|
@@ -62719,8 +63403,26 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
|
|
|
62719
63403
|
let toolStartTime = 0;
|
|
62720
63404
|
let lastToolName = "";
|
|
62721
63405
|
let thinkingTokens = 0;
|
|
63406
|
+
let liveThinkTail = "";
|
|
63407
|
+
const pushStatus = () => {
|
|
63408
|
+
const parts = [];
|
|
63409
|
+
const tok = formatProgressTokens(thinkingTokens);
|
|
63410
|
+
if (tok)
|
|
63411
|
+
parts.push(tok);
|
|
63412
|
+
if (liveThinkTail)
|
|
63413
|
+
parts.push(liveThinkTail);
|
|
63414
|
+
spinner.setDetail(parts.join(" \xB7 "));
|
|
63415
|
+
};
|
|
63416
|
+
const resumeWorking = (label = "working\u2026") => {
|
|
63417
|
+
if (abortSignal.aborted)
|
|
63418
|
+
return;
|
|
63419
|
+
thinkingTokens = 0;
|
|
63420
|
+
liveThinkTail = "";
|
|
63421
|
+
spinner.start(label, { hint: false });
|
|
63422
|
+
};
|
|
62722
63423
|
setMode(mode);
|
|
62723
63424
|
console.log();
|
|
63425
|
+
spinner.start("thinking\u2026", { hint: false });
|
|
62724
63426
|
const result = await (0, import_code_core3.runAgentLoop)(messages, {
|
|
62725
63427
|
workDir,
|
|
62726
63428
|
model: modelAlias,
|
|
@@ -62728,10 +63430,26 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
|
|
|
62728
63430
|
nexrallMd,
|
|
62729
63431
|
mode,
|
|
62730
63432
|
effort,
|
|
63433
|
+
// The backend streams a cumulative output-token count (routes/code.js's
|
|
63434
|
+
// sendProgress, throttled to ≤5/s) covering thinking, visible text AND
|
|
63435
|
+
// tool-argument JSON. This used to be stored in a variable and rendered only
|
|
63436
|
+
// at message_complete — i.e. after the turn was already over — so during the
|
|
63437
|
+
// long phase it exists to describe, it showed nothing at all.
|
|
62731
63438
|
onThinkingProgress: (tokens) => {
|
|
62732
63439
|
thinkingTokens = tokens;
|
|
63440
|
+
pushStatus();
|
|
62733
63441
|
},
|
|
62734
|
-
|
|
63442
|
+
// Live thinking text. Previously a hard no-op with the note "shown on
|
|
63443
|
+
// onThinking" — but onThinking only fires at message_complete, so a long
|
|
63444
|
+
// reasoning phase rendered nothing whatsoever until it had finished. Show a
|
|
63445
|
+
// short rolling tail on the status line so the user can see it actively
|
|
63446
|
+
// reasoning, then let onThinking print the proper summary block at the end.
|
|
63447
|
+
onThinkingDelta: (text) => {
|
|
63448
|
+
if (abortSignal.aborted)
|
|
63449
|
+
return;
|
|
63450
|
+
const flat = text.replace(/\s+/g, " ");
|
|
63451
|
+
liveThinkTail = (liveThinkTail + flat).slice(-THINK_TAIL_CHARS);
|
|
63452
|
+
pushStatus();
|
|
62735
63453
|
},
|
|
62736
63454
|
onThinking: (text) => {
|
|
62737
63455
|
if (abortSignal.aborted)
|
|
@@ -62739,6 +63457,8 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
|
|
|
62739
63457
|
spinner.stop();
|
|
62740
63458
|
console.log(formatThinking(text, thinkingTokens));
|
|
62741
63459
|
thinkingTokens = 0;
|
|
63460
|
+
liveThinkTail = "";
|
|
63461
|
+
resumeWorking();
|
|
62742
63462
|
},
|
|
62743
63463
|
onText: (text) => {
|
|
62744
63464
|
if (abortSignal.aborted)
|
|
@@ -62757,6 +63477,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
|
|
|
62757
63477
|
mdRender.flush();
|
|
62758
63478
|
spinner.stop();
|
|
62759
63479
|
console.log(source_default.dim(` ${text}`));
|
|
63480
|
+
resumeWorking();
|
|
62760
63481
|
},
|
|
62761
63482
|
onToolUse: (name, input) => {
|
|
62762
63483
|
if (abortSignal.aborted)
|
|
@@ -62787,6 +63508,7 @@ async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrall
|
|
|
62787
63508
|
const durationMs = Date.now() - toolStartTime;
|
|
62788
63509
|
console.log(formatToolResult(lastToolName, res, durationMs));
|
|
62789
63510
|
toolStartTime = 0;
|
|
63511
|
+
resumeWorking();
|
|
62790
63512
|
},
|
|
62791
63513
|
// Ignore a `partial` report: it belongs to a cut-short attempt that was restarted,
|
|
62792
63514
|
// and the replacement attempt reports the turn's real totals. Letting it through
|
|
@@ -63822,6 +64544,12 @@ async function confirmInstall(i2, name, autoYes) {
|
|
|
63822
64544
|
if (autoYes && dangerous) {
|
|
63823
64545
|
console.log(source_default.yellow("\n --yes does not apply to plugins with hooks/MCP; confirmation required."));
|
|
63824
64546
|
}
|
|
64547
|
+
if (!process.stdin.isTTY) {
|
|
64548
|
+
console.log();
|
|
64549
|
+
console.log(source_default.red(" \u2717 Cannot ask for confirmation: stdin is not a terminal."));
|
|
64550
|
+
console.log(source_default.dim(dangerous ? " This plugin ships hooks/MCP, which always require an interactive confirmation." : " Re-run with --yes to install non-interactively."));
|
|
64551
|
+
return false;
|
|
64552
|
+
}
|
|
63825
64553
|
console.log();
|
|
63826
64554
|
const { ok } = await (0, import_prompts.default)({
|
|
63827
64555
|
type: "confirm",
|
|
@@ -63989,7 +64717,7 @@ function pluginListCommand() {
|
|
|
63989
64717
|
|
|
63990
64718
|
// src/index.ts
|
|
63991
64719
|
var program2 = new Command();
|
|
63992
|
-
program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.
|
|
64720
|
+
program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.49").enablePositionalOptions();
|
|
63993
64721
|
program2.command("auth").description("Login to your Nexrall account").action(authCommand);
|
|
63994
64722
|
program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
|
|
63995
64723
|
program2.command("update").description("Update nex to the latest version").option("-c, --check", "Check for updates without installing").option("-y, --yes", "Skip confirmation prompt").action(async (opts) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexrall-code",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.49",
|
|
4
4
|
"description": "Nexrall Code — AI coding assistant for your terminal (headless agent for scripts, CI and automation)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"react": "^19.2.8",
|
|
42
42
|
"readline": "^1.3.0",
|
|
43
43
|
"string-width": "^7.2.0",
|
|
44
|
-
"@nexrall/code-core": "1.4.
|
|
44
|
+
"@nexrall/code-core": "1.4.24"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@aws-sdk/client-s3": "^3.600.0",
|