nexrall-code 0.5.48 → 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 +389 -11
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10819,6 +10819,144 @@ var require_editCompleteness = __commonJS({
|
|
|
10819
10819
|
}
|
|
10820
10820
|
});
|
|
10821
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
|
+
|
|
10822
10960
|
// ../core/dist/agent/memory.js
|
|
10823
10961
|
var require_memory = __commonJS({
|
|
10824
10962
|
"../core/dist/agent/memory.js"(exports) {
|
|
@@ -12482,6 +12620,7 @@ var require_executor = __commonJS({
|
|
|
12482
12620
|
var sandbox_1 = require_sandbox();
|
|
12483
12621
|
var auth_1 = require_auth();
|
|
12484
12622
|
var editCompleteness_1 = require_editCompleteness();
|
|
12623
|
+
var securityLint_1 = require_securityLint();
|
|
12485
12624
|
var memory_1 = require_memory();
|
|
12486
12625
|
var skills_1 = require_skills();
|
|
12487
12626
|
var testIntegrity_1 = require_testIntegrity();
|
|
@@ -12737,15 +12876,16 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
|
|
|
12737
12876
|
atomicWrite(resolved, content, existingMode);
|
|
12738
12877
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
12739
12878
|
const lines = content.split("\n").length;
|
|
12879
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
|
|
12740
12880
|
if (isNew) {
|
|
12741
|
-
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
|
|
12881
|
+
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
|
|
12742
12882
|
}
|
|
12743
12883
|
const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
|
|
12744
12884
|
let tiMarker = "";
|
|
12745
12885
|
const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
|
|
12746
12886
|
if (ti.suspicious)
|
|
12747
12887
|
tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
|
|
12748
|
-
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
|
|
12888
|
+
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
|
|
12749
12889
|
} catch (err) {
|
|
12750
12890
|
return { error: err.message };
|
|
12751
12891
|
}
|
|
@@ -13531,9 +13671,10 @@ ${globalCapMatches(output)}` : "";
|
|
|
13531
13671
|
const linesBefore = origNorm.split("\n").length;
|
|
13532
13672
|
const linesAfter = updated.split("\n").length;
|
|
13533
13673
|
const xfile = crossFileBreakageWarning(resolved, origNorm, normalizeLF(updated), workDir ?? process.cwd());
|
|
13674
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
|
|
13534
13675
|
return { output: `Edited ${resolved} (${linesBefore} \u2192 ${linesAfter} lines)
|
|
13535
13676
|
|
|
13536
|
-
${diff3}${xfile}` };
|
|
13677
|
+
${diff3}${xfile}${sec}` };
|
|
13537
13678
|
} catch (err) {
|
|
13538
13679
|
return { error: err.message };
|
|
13539
13680
|
}
|
|
@@ -13801,6 +13942,7 @@ ${body}${truncNote}
|
|
|
13801
13942
|
let content = normalizeLF(rawFile);
|
|
13802
13943
|
const originalNorm = content;
|
|
13803
13944
|
const diffs = [];
|
|
13945
|
+
const insertedText = [];
|
|
13804
13946
|
for (let i2 = 0; i2 < edits.length; i2++) {
|
|
13805
13947
|
const edit = edits[i2];
|
|
13806
13948
|
const oldStr = normalizeLF(typeof edit.old_string === "string" ? edit.old_string : "");
|
|
@@ -13815,16 +13957,18 @@ ${body}${truncNote}
|
|
|
13815
13957
|
return { error: `Edit #${i2 + 1}: old_string appears ${count} times \u2014 it must be unique. Add more surrounding context.` };
|
|
13816
13958
|
}
|
|
13817
13959
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
13960
|
+
insertedText.push(newStr);
|
|
13818
13961
|
content = literalReplace(content, oldStr, newStr);
|
|
13819
13962
|
}
|
|
13820
13963
|
const finalContent = wasCRLF ? content.replace(/\n/g, "\r\n") : content;
|
|
13821
13964
|
atomicWritePreservingMode(resolved, finalContent, pre.mode);
|
|
13822
13965
|
const xfile = crossFileBreakageWarning(resolved, originalNorm, content, workDir ?? process.cwd());
|
|
13966
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join("\n")));
|
|
13823
13967
|
return {
|
|
13824
13968
|
output: `Applied ${edits.length} edit(s) to ${resolved}:
|
|
13825
13969
|
` + diffs.map((d, i2) => `
|
|
13826
13970
|
--- edit #${i2 + 1} ---
|
|
13827
|
-
${d}`).join("\n") + xfile
|
|
13971
|
+
${d}`).join("\n") + xfile + sec
|
|
13828
13972
|
};
|
|
13829
13973
|
} catch (err) {
|
|
13830
13974
|
return { error: err.message };
|
|
@@ -14405,11 +14549,40 @@ var require_agentTypes = __commonJS({
|
|
|
14405
14549
|
var path6 = __importStar(__require("path"));
|
|
14406
14550
|
var os6 = __importStar(__require("os"));
|
|
14407
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
|
+
];
|
|
14408
14581
|
var BUILTIN_AGENTS = [
|
|
14409
14582
|
{
|
|
14410
14583
|
name: "reviewer",
|
|
14411
14584
|
description: "Read-only code reviewer \u2014 finds correctness bugs, edge cases, and security issues in a diff or file set. Cannot modify files.",
|
|
14412
|
-
tools:
|
|
14585
|
+
tools: READ_ONLY_TOOLS,
|
|
14413
14586
|
source: "builtin",
|
|
14414
14587
|
prompt: [
|
|
14415
14588
|
"You are a meticulous senior code reviewer. You NEVER modify files \u2014 you only read, search, and report.",
|
|
@@ -14425,6 +14598,162 @@ var require_agentTypes = __commonJS({
|
|
|
14425
14598
|
"Report format: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F7E2} Suggestion, each with file:line and a concrete fix,",
|
|
14426
14599
|
"then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale."
|
|
14427
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")
|
|
14428
14757
|
}
|
|
14429
14758
|
];
|
|
14430
14759
|
function parseFrontmatter(raw) {
|
|
@@ -14443,6 +14772,9 @@ var require_agentTypes = __commonJS({
|
|
|
14443
14772
|
const s2 = (v ?? "").toLowerCase();
|
|
14444
14773
|
return s2 === "turbo" || s2 === "pro" || s2 === "ultra" ? s2 : void 0;
|
|
14445
14774
|
}
|
|
14775
|
+
function parseBool(v) {
|
|
14776
|
+
return /^(true|yes|1|on)$/i.test((v ?? "").trim());
|
|
14777
|
+
}
|
|
14446
14778
|
function parseToolList(v) {
|
|
14447
14779
|
if (!v)
|
|
14448
14780
|
return void 0;
|
|
@@ -14471,7 +14803,12 @@ var require_agentTypes = __commonJS({
|
|
|
14471
14803
|
tools: parseToolList(meta.tools),
|
|
14472
14804
|
model: parseModel(meta.model),
|
|
14473
14805
|
prompt: body,
|
|
14474
|
-
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 } : {}
|
|
14475
14812
|
});
|
|
14476
14813
|
} catch {
|
|
14477
14814
|
}
|
|
@@ -14487,16 +14824,38 @@ var require_agentTypes = __commonJS({
|
|
|
14487
14824
|
if (!out.has(agent.name))
|
|
14488
14825
|
out.set(agent.name, agent);
|
|
14489
14826
|
}
|
|
14490
|
-
|
|
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
|
+
});
|
|
14491
14839
|
}
|
|
14492
14840
|
function summariseAgents(types3) {
|
|
14493
14841
|
if (!types3.length)
|
|
14494
14842
|
return "";
|
|
14495
14843
|
return types3.map((t2) => {
|
|
14496
|
-
const
|
|
14497
|
-
|
|
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}`;
|
|
14498
14848
|
}).join("\n");
|
|
14499
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
|
+
]);
|
|
14500
14859
|
function findAgentType(types3, name) {
|
|
14501
14860
|
if (!name)
|
|
14502
14861
|
return void 0;
|
|
@@ -14905,6 +15264,7 @@ var require_loop = __commonJS({
|
|
|
14905
15264
|
exports.contextWindowFor = contextWindowFor2;
|
|
14906
15265
|
exports.compactionThresholds = compactionThresholds2;
|
|
14907
15266
|
exports.estimateBodyBytes = estimateBodyBytes2;
|
|
15267
|
+
exports.allowsTestOnlyWrite = allowsTestOnlyWrite;
|
|
14908
15268
|
exports.findSafeCutIndex = findSafeCutIndex;
|
|
14909
15269
|
exports.transcriptOf = transcriptOf;
|
|
14910
15270
|
exports.createLedger = createLedger;
|
|
@@ -15250,6 +15610,8 @@ ${options.nexrallMd}` : "") : options.nexrallMd;
|
|
|
15250
15610
|
const gatedPermission = async (req) => {
|
|
15251
15611
|
if (allowed && !allowed.has(req.tool))
|
|
15252
15612
|
return false;
|
|
15613
|
+
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
|
|
15614
|
+
return false;
|
|
15253
15615
|
return options.requestPermission(req);
|
|
15254
15616
|
};
|
|
15255
15617
|
const subMessages = [
|
|
@@ -15394,6 +15756,15 @@ ${partial}` : "",
|
|
|
15394
15756
|
return true;
|
|
15395
15757
|
}
|
|
15396
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
|
+
}
|
|
15397
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;
|
|
15398
15769
|
function findSafeCutIndex(messages, maxIdx) {
|
|
15399
15770
|
for (let i2 = Math.min(maxIdx, messages.length - 1); i2 >= 2; i2--) {
|
|
@@ -17720,6 +18091,7 @@ var require_dist2 = __commonJS({
|
|
|
17720
18091
|
__exportStar(require_loop(), exports);
|
|
17721
18092
|
__exportStar(require_testIntegrity(), exports);
|
|
17722
18093
|
__exportStar(require_editCompleteness(), exports);
|
|
18094
|
+
__exportStar(require_securityLint(), exports);
|
|
17723
18095
|
__exportStar(require_crossFile(), exports);
|
|
17724
18096
|
__exportStar(require_flaky(), exports);
|
|
17725
18097
|
__exportStar(require_claimEvidence(), exports);
|
|
@@ -62794,7 +63166,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
|
|
|
62794
63166
|
};
|
|
62795
63167
|
|
|
62796
63168
|
// src/commands/chat.ts
|
|
62797
|
-
var CLI_VERSION = "0.5.
|
|
63169
|
+
var CLI_VERSION = "0.5.49";
|
|
62798
63170
|
var MODEL_LABELS = {
|
|
62799
63171
|
turbo: "Nexrall Turbo",
|
|
62800
63172
|
pro: "Nexrall Pro",
|
|
@@ -64172,6 +64544,12 @@ async function confirmInstall(i2, name, autoYes) {
|
|
|
64172
64544
|
if (autoYes && dangerous) {
|
|
64173
64545
|
console.log(source_default.yellow("\n --yes does not apply to plugins with hooks/MCP; confirmation required."));
|
|
64174
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
|
+
}
|
|
64175
64553
|
console.log();
|
|
64176
64554
|
const { ok } = await (0, import_prompts.default)({
|
|
64177
64555
|
type: "confirm",
|
|
@@ -64339,7 +64717,7 @@ function pluginListCommand() {
|
|
|
64339
64717
|
|
|
64340
64718
|
// src/index.ts
|
|
64341
64719
|
var program2 = new Command();
|
|
64342
|
-
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();
|
|
64343
64721
|
program2.command("auth").description("Login to your Nexrall account").action(authCommand);
|
|
64344
64722
|
program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
|
|
64345
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",
|