nexrall-code 0.5.48 → 0.5.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1081 -192
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1773,13 +1773,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
1773
1773
  * @param {string} source - expected values are default/config/env/cli/implied
1774
1774
  * @return {Command} `this` command for chaining
1775
1775
  */
1776
- setOptionValueWithSource(key, value, source) {
1776
+ setOptionValueWithSource(key, value, source2) {
1777
1777
  if (this._storeOptionsAsProperties) {
1778
1778
  this[key] = value;
1779
1779
  } else {
1780
1780
  this._optionValues[key] = value;
1781
1781
  }
1782
- this._optionValueSources[key] = source;
1782
+ this._optionValueSources[key] = source2;
1783
1783
  return this;
1784
1784
  }
1785
1785
  /**
@@ -1800,13 +1800,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
1800
1800
  * @return {string}
1801
1801
  */
1802
1802
  getOptionValueSourceWithGlobals(key) {
1803
- let source;
1803
+ let source2;
1804
1804
  this._getCommandAndAncestors().forEach((cmd) => {
1805
1805
  if (cmd.getOptionValueSource(key) !== void 0) {
1806
- source = cmd.getOptionValueSource(key);
1806
+ source2 = cmd.getOptionValueSource(key);
1807
1807
  }
1808
1808
  });
1809
- return source;
1809
+ return source2;
1810
1810
  }
1811
1811
  /**
1812
1812
  * Get user arguments from implied or explicit arguments.
@@ -2615,8 +2615,8 @@ Expecting one of '${allowedValues.join("', '")}'`);
2615
2615
  const getErrorMessage = (option2) => {
2616
2616
  const bestOption = findBestOptionFromValue(option2);
2617
2617
  const optionKey = bestOption.attributeName();
2618
- const source = this.getOptionValueSource(optionKey);
2619
- if (source === "env") {
2618
+ const source2 = this.getOptionValueSource(optionKey);
2619
+ if (source2 === "env") {
2620
2620
  return `environment variable '${bestOption.envVar}'`;
2621
2621
  }
2622
2622
  return `option '${bestOption.flags}'`;
@@ -5998,10 +5998,10 @@ var require_ponyfill_es2018 = __commonJS({
5998
5998
  return ctor;
5999
5999
  }
6000
6000
  const DOMException2 = getFromGlobal() || createPolyfill();
6001
- function ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventCancel, signal) {
6002
- const reader = AcquireReadableStreamDefaultReader(source);
6001
+ function ReadableStreamPipeTo(source2, dest, preventClose, preventAbort, preventCancel, signal) {
6002
+ const reader = AcquireReadableStreamDefaultReader(source2);
6003
6003
  const writer = AcquireWritableStreamDefaultWriter(dest);
6004
- source._disturbed = true;
6004
+ source2._disturbed = true;
6005
6005
  let shuttingDown = false;
6006
6006
  let currentWrite = promiseResolvedWith(void 0);
6007
6007
  return newPromise((resolve3, reject) => {
@@ -6020,8 +6020,8 @@ var require_ponyfill_es2018 = __commonJS({
6020
6020
  }
6021
6021
  if (!preventCancel) {
6022
6022
  actions.push(() => {
6023
- if (source._state === "readable") {
6024
- return ReadableStreamCancel(source, error);
6023
+ if (source2._state === "readable") {
6024
+ return ReadableStreamCancel(source2, error);
6025
6025
  }
6026
6026
  return promiseResolvedWith(void 0);
6027
6027
  });
@@ -6063,7 +6063,7 @@ var require_ponyfill_es2018 = __commonJS({
6063
6063
  });
6064
6064
  });
6065
6065
  }
6066
- isOrBecomesErrored(source, reader._closedPromise, (storedError) => {
6066
+ isOrBecomesErrored(source2, reader._closedPromise, (storedError) => {
6067
6067
  if (!preventAbort) {
6068
6068
  shutdownWithAction(() => WritableStreamAbort(dest, storedError), true, storedError);
6069
6069
  } else {
@@ -6073,13 +6073,13 @@ var require_ponyfill_es2018 = __commonJS({
6073
6073
  });
6074
6074
  isOrBecomesErrored(dest, writer._closedPromise, (storedError) => {
6075
6075
  if (!preventCancel) {
6076
- shutdownWithAction(() => ReadableStreamCancel(source, storedError), true, storedError);
6076
+ shutdownWithAction(() => ReadableStreamCancel(source2, storedError), true, storedError);
6077
6077
  } else {
6078
6078
  shutdown(true, storedError);
6079
6079
  }
6080
6080
  return null;
6081
6081
  });
6082
- isOrBecomesClosed(source, reader._closedPromise, () => {
6082
+ isOrBecomesClosed(source2, reader._closedPromise, () => {
6083
6083
  if (!preventClose) {
6084
6084
  shutdownWithAction(() => WritableStreamDefaultWriterCloseWithErrorPropagation(writer));
6085
6085
  } else {
@@ -6090,7 +6090,7 @@ var require_ponyfill_es2018 = __commonJS({
6090
6090
  if (WritableStreamCloseQueuedOrInFlight(dest) || dest._state === "closed") {
6091
6091
  const destClosed = new TypeError("the destination writable stream closed before all data could be piped to it");
6092
6092
  if (!preventCancel) {
6093
- shutdownWithAction(() => ReadableStreamCancel(source, destClosed), true, destClosed);
6093
+ shutdownWithAction(() => ReadableStreamCancel(source2, destClosed), true, destClosed);
6094
6094
  } else {
6095
6095
  shutdown(true, destClosed);
6096
6096
  }
@@ -6726,11 +6726,11 @@ var require_ponyfill_es2018 = __commonJS({
6726
6726
  function isReadableStreamLike(stream) {
6727
6727
  return typeIsObject(stream) && typeof stream.getReader !== "undefined";
6728
6728
  }
6729
- function ReadableStreamFrom(source) {
6730
- if (isReadableStreamLike(source)) {
6731
- return ReadableStreamFromDefaultReader(source.getReader());
6729
+ function ReadableStreamFrom(source2) {
6730
+ if (isReadableStreamLike(source2)) {
6731
+ return ReadableStreamFromDefaultReader(source2.getReader());
6732
6732
  }
6733
- return ReadableStreamFromIterable(source);
6733
+ return ReadableStreamFromIterable(source2);
6734
6734
  }
6735
6735
  function ReadableStreamFromIterable(asyncIterable) {
6736
6736
  let stream;
@@ -6817,9 +6817,9 @@ var require_ponyfill_es2018 = __commonJS({
6817
6817
  stream = CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm, 0);
6818
6818
  return stream;
6819
6819
  }
6820
- function convertUnderlyingDefaultOrByteSource(source, context) {
6821
- assertDictionary(source, context);
6822
- const original = source;
6820
+ function convertUnderlyingDefaultOrByteSource(source2, context) {
6821
+ assertDictionary(source2, context);
6822
+ const original = source2;
6823
6823
  const autoAllocateChunkSize = original === null || original === void 0 ? void 0 : original.autoAllocateChunkSize;
6824
6824
  const cancel = original === null || original === void 0 ? void 0 : original.cancel;
6825
6825
  const pull = original === null || original === void 0 ? void 0 : original.pull;
@@ -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) {
@@ -11063,6 +11201,8 @@ var require_plugins = __commonJS({
11063
11201
  };
11064
11202
  }();
11065
11203
  Object.defineProperty(exports, "__esModule", { value: true });
11204
+ exports.FILE_CANDIDATES = void 0;
11205
+ exports.resolvePluginFile = resolvePluginFile;
11066
11206
  exports.loadPlugins = loadPlugins3;
11067
11207
  exports.pluginAssetDirs = pluginAssetDirs;
11068
11208
  exports.pluginHooks = pluginHooks;
@@ -11070,9 +11210,28 @@ var require_plugins = __commonJS({
11070
11210
  var fs9 = __importStar(__require("fs"));
11071
11211
  var path6 = __importStar(__require("path"));
11072
11212
  var os6 = __importStar(__require("os"));
11213
+ exports.FILE_CANDIDATES = {
11214
+ manifest: ["plugin.json", path6.join(".claude-plugin", "plugin.json")],
11215
+ hooks: ["hooks.json", path6.join("hooks", "hooks.json")],
11216
+ mcp: ["mcp.json", ".mcp.json"]
11217
+ };
11218
+ function resolvePluginFile(dir, kind) {
11219
+ for (const rel of exports.FILE_CANDIDATES[kind]) {
11220
+ const full = path6.join(dir, rel);
11221
+ try {
11222
+ if (fs9.statSync(full).isFile())
11223
+ return full;
11224
+ } catch {
11225
+ }
11226
+ }
11227
+ return null;
11228
+ }
11073
11229
  function readMeta(dir) {
11230
+ const file = resolvePluginFile(dir, "manifest");
11231
+ if (!file)
11232
+ return {};
11074
11233
  try {
11075
- const raw = fs9.readFileSync(path6.join(dir, "plugin.json"), "utf-8");
11234
+ const raw = fs9.readFileSync(file, "utf-8");
11076
11235
  const j = JSON.parse(raw);
11077
11236
  return {
11078
11237
  name: typeof j.name === "string" ? j.name : void 0,
@@ -11125,8 +11284,11 @@ var require_plugins = __commonJS({
11125
11284
  function pluginHooks(workDir) {
11126
11285
  const merged = {};
11127
11286
  for (const p of loadPlugins3(workDir)) {
11287
+ const file = resolvePluginFile(p.dir, "hooks");
11288
+ if (!file)
11289
+ continue;
11128
11290
  try {
11129
- const raw = fs9.readFileSync(path6.join(p.dir, "hooks.json"), "utf-8");
11291
+ const raw = fs9.readFileSync(file, "utf-8");
11130
11292
  const j = JSON.parse(raw);
11131
11293
  const hooks = j.hooks ?? j;
11132
11294
  for (const [phase, entries] of Object.entries(hooks)) {
@@ -11142,8 +11304,11 @@ var require_plugins = __commonJS({
11142
11304
  function pluginMcpServers(workDir) {
11143
11305
  const merged = {};
11144
11306
  for (const p of loadPlugins3(workDir)) {
11307
+ const file = resolvePluginFile(p.dir, "mcp");
11308
+ if (!file)
11309
+ continue;
11145
11310
  try {
11146
- const raw = fs9.readFileSync(path6.join(p.dir, "mcp.json"), "utf-8");
11311
+ const raw = fs9.readFileSync(file, "utf-8");
11147
11312
  const j = JSON.parse(raw);
11148
11313
  const servers = j.mcpServers ?? j;
11149
11314
  for (const [name, cfg] of Object.entries(servers)) {
@@ -11262,7 +11427,7 @@ var require_loader = __commonJS({
11262
11427
  }
11263
11428
  return { meta, body: (m2[2] ?? "").trim() };
11264
11429
  }
11265
- function loadDir(dir, source, into) {
11430
+ function loadDir(dir, source2, into) {
11266
11431
  let files;
11267
11432
  try {
11268
11433
  files = fs9.readdirSync(dir).filter((f3) => f3.endsWith(".md"));
@@ -11276,7 +11441,7 @@ var require_loader = __commonJS({
11276
11441
  const name = (meta.name || path6.basename(file, ".md")).trim().toLowerCase();
11277
11442
  if (!name)
11278
11443
  continue;
11279
- if (source !== "project" && into.has(name))
11444
+ if (source2 !== "project" && into.has(name))
11280
11445
  continue;
11281
11446
  const model = ["turbo", "pro", "ultra"].find((x2) => x2 === (meta.model ?? "").toLowerCase());
11282
11447
  into.set(name, {
@@ -11285,7 +11450,7 @@ var require_loader = __commonJS({
11285
11450
  model,
11286
11451
  mode: meta.mode || void 0,
11287
11452
  body,
11288
- source
11453
+ source: source2
11289
11454
  });
11290
11455
  } catch {
11291
11456
  }
@@ -11431,7 +11596,7 @@ var require_skills = __commonJS({
11431
11596
  return false;
11432
11597
  return fallback;
11433
11598
  }
11434
- function toSkill(meta, body, name, source, dir) {
11599
+ function toSkill(meta, body, name, source2, dir) {
11435
11600
  const model = ["turbo", "pro", "ultra"].find((x2) => x2 === (meta.model ?? "").toLowerCase());
11436
11601
  return {
11437
11602
  name,
@@ -11439,13 +11604,13 @@ var require_skills = __commonJS({
11439
11604
  model,
11440
11605
  mode: meta.mode || void 0,
11441
11606
  body,
11442
- source,
11607
+ source: source2,
11443
11608
  dir,
11444
11609
  disableModelInvocation: parseBool(meta["disable-model-invocation"], false),
11445
11610
  userInvocable: parseBool(meta["user-invocable"], true)
11446
11611
  };
11447
11612
  }
11448
- function loadFlatCommandDir(dir, source, into) {
11613
+ function loadFlatCommandDir(dir, source2, into) {
11449
11614
  let files;
11450
11615
  try {
11451
11616
  files = fs9.readdirSync(dir).filter((f3) => f3.endsWith(".md"));
@@ -11459,14 +11624,14 @@ var require_skills = __commonJS({
11459
11624
  const name = (meta.name || path6.basename(file, ".md")).trim().toLowerCase();
11460
11625
  if (!name)
11461
11626
  continue;
11462
- if (source !== "project" && into.has(name))
11627
+ if (source2 !== "project" && into.has(name))
11463
11628
  continue;
11464
- into.set(name, toSkill(meta, body, name, source));
11629
+ into.set(name, toSkill(meta, body, name, source2));
11465
11630
  } catch {
11466
11631
  }
11467
11632
  }
11468
11633
  }
11469
- function loadSkillDir(root, source, into) {
11634
+ function loadSkillDir(root, source2, into) {
11470
11635
  let entries;
11471
11636
  try {
11472
11637
  entries = fs9.readdirSync(root, { withFileTypes: true });
@@ -11490,9 +11655,9 @@ var require_skills = __commonJS({
11490
11655
  const name = (meta.name || entry.name).trim().toLowerCase();
11491
11656
  if (!name)
11492
11657
  continue;
11493
- if (source !== "project" && into.has(name))
11658
+ if (source2 !== "project" && into.has(name))
11494
11659
  continue;
11495
- into.set(name, toSkill(meta, body, name, source, skillDir));
11660
+ into.set(name, toSkill(meta, body, name, source2, skillDir));
11496
11661
  } catch {
11497
11662
  }
11498
11663
  }
@@ -12026,12 +12191,12 @@ var require_symbols = __commonJS({
12026
12191
  return new Set(EXT_TO_LANG.keys());
12027
12192
  }
12028
12193
  var MAX_FILE_BYTES = 1024 * 1024;
12029
- function extractSymbols(filePath, source) {
12194
+ function extractSymbols(filePath, source2) {
12030
12195
  const lang = EXT_TO_LANG.get(path6.extname(filePath).toLowerCase());
12031
12196
  if (!lang)
12032
12197
  return [];
12033
12198
  const hits = [];
12034
- const lines = source.split("\n");
12199
+ const lines = source2.split("\n");
12035
12200
  let inBlockComment = false;
12036
12201
  for (let i2 = 0; i2 < lines.length; i2++) {
12037
12202
  const trimmed = lines[i2].trim();
@@ -12068,21 +12233,21 @@ var require_symbols = __commonJS({
12068
12233
  if (!rel)
12069
12234
  return { error: "path is required" };
12070
12235
  const abs = resolveIn(workDir, rel);
12071
- let source;
12236
+ let source2;
12072
12237
  try {
12073
12238
  const st = fs9.statSync(abs);
12074
12239
  if (!st.isFile())
12075
12240
  return { error: `Not a file: ${rel}` };
12076
12241
  if (st.size > MAX_FILE_BYTES)
12077
12242
  return { error: `File too large for symbol scan (${Math.round(st.size / 1024)} KB > 1024 KB)` };
12078
- source = fs9.readFileSync(abs, "utf-8");
12243
+ source2 = fs9.readFileSync(abs, "utf-8");
12079
12244
  } catch (err) {
12080
12245
  return { error: `Cannot read ${rel}: ${err.message}` };
12081
12246
  }
12082
12247
  if (!EXT_TO_LANG.has(path6.extname(abs).toLowerCase())) {
12083
12248
  return { output: `No symbol support for ${path6.extname(abs) || "this file type"} (regex-based scanner). Use search_files instead.` };
12084
12249
  }
12085
- const hits = extractSymbols(abs, source);
12250
+ const hits = extractSymbols(abs, source2);
12086
12251
  if (!hits.length)
12087
12252
  return { output: `No symbols found in ${rel}.` };
12088
12253
  const lines = hits.map((h2) => `${h2.kind.padEnd(10)} ${h2.name} (line ${h2.line})`);
@@ -12124,10 +12289,10 @@ ${lines.join("\n")}` };
12124
12289
  const st = fs9.statSync(full);
12125
12290
  if (st.size > MAX_FILE_BYTES)
12126
12291
  continue;
12127
- const source = fs9.readFileSync(full, "utf-8");
12128
- if (!source.toLowerCase().includes(q))
12292
+ const source2 = fs9.readFileSync(full, "utf-8");
12293
+ if (!source2.toLowerCase().includes(q))
12129
12294
  continue;
12130
- for (const h2 of extractSymbols(full, source)) {
12295
+ for (const h2 of extractSymbols(full, source2)) {
12131
12296
  if (h2.name.toLowerCase().includes(q)) {
12132
12297
  results.push({ ...h2, file: path6.relative(root, full) });
12133
12298
  if (results.length >= MAX_RESULTS)
@@ -12314,18 +12479,18 @@ var require_tsLangService = __commonJS({
12314
12479
  return false;
12315
12480
  }
12316
12481
  }
12317
- function posToOffset(source, line, character) {
12318
- const lines = source.split("\n");
12482
+ function posToOffset(source2, line, character) {
12483
+ const lines = source2.split("\n");
12319
12484
  let offset = 0;
12320
12485
  for (let i2 = 0; i2 < line - 1 && i2 < lines.length; i2++)
12321
12486
  offset += lines[i2].length + 1;
12322
12487
  return offset + Math.max(0, character - 1);
12323
12488
  }
12324
- function offsetToPos(source, offset) {
12489
+ function offsetToPos(source2, offset) {
12325
12490
  let line = 1;
12326
12491
  let last = 0;
12327
- for (let i2 = 0; i2 < offset && i2 < source.length; i2++) {
12328
- if (source[i2] === "\n") {
12492
+ for (let i2 = 0; i2 < offset && i2 < source2.length; i2++) {
12493
+ if (source2[i2] === "\n") {
12329
12494
  line++;
12330
12495
  last = i2 + 1;
12331
12496
  }
@@ -12342,8 +12507,8 @@ var require_tsLangService = __commonJS({
12342
12507
  return null;
12343
12508
  if (!ensureFile(svc, deps.filePath))
12344
12509
  return { error: `Cannot read ${deps.filePath}` };
12345
- const source = svc.snapshots.get(deps.filePath) ?? "";
12346
- const offset = posToOffset(source, deps.line, deps.character);
12510
+ const source2 = svc.snapshots.get(deps.filePath) ?? "";
12511
+ const offset = posToOffset(source2, deps.line, deps.character);
12347
12512
  let defs;
12348
12513
  try {
12349
12514
  defs = svc.service.getDefinitionAtPosition(deps.filePath, offset);
@@ -12366,8 +12531,8 @@ ${out.join("\n")}` };
12366
12531
  return null;
12367
12532
  if (!ensureFile(svc, deps.filePath))
12368
12533
  return { error: `Cannot read ${deps.filePath}` };
12369
- const source = svc.snapshots.get(deps.filePath) ?? "";
12370
- const offset = posToOffset(source, deps.line, deps.character);
12534
+ const source2 = svc.snapshots.get(deps.filePath) ?? "";
12535
+ const offset = posToOffset(source2, deps.line, deps.character);
12371
12536
  let refs;
12372
12537
  try {
12373
12538
  refs = svc.service.getReferencesAtPosition(deps.filePath, offset);
@@ -12394,8 +12559,8 @@ ${lines.join("\n")}` };
12394
12559
  return null;
12395
12560
  if (!ensureFile(svc, deps.filePath))
12396
12561
  return { error: `Cannot read ${deps.filePath}` };
12397
- const source = svc.snapshots.get(deps.filePath) ?? "";
12398
- const offset = posToOffset(source, deps.line, deps.character);
12562
+ const source2 = svc.snapshots.get(deps.filePath) ?? "";
12563
+ const offset = posToOffset(source2, deps.line, deps.character);
12399
12564
  let info;
12400
12565
  try {
12401
12566
  info = svc.service.getQuickInfoAtPosition(deps.filePath, offset);
@@ -12482,6 +12647,7 @@ var require_executor = __commonJS({
12482
12647
  var sandbox_1 = require_sandbox();
12483
12648
  var auth_1 = require_auth();
12484
12649
  var editCompleteness_1 = require_editCompleteness();
12650
+ var securityLint_1 = require_securityLint();
12485
12651
  var memory_1 = require_memory();
12486
12652
  var skills_1 = require_skills();
12487
12653
  var testIntegrity_1 = require_testIntegrity();
@@ -12737,15 +12903,16 @@ Resolve the conflict (remove <<<<<<< / ======= / >>>>>>> markers and keep the in
12737
12903
  atomicWrite(resolved, content, existingMode);
12738
12904
  const bytes = Buffer.byteLength(content, "utf-8");
12739
12905
  const lines = content.split("\n").length;
12906
+ const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
12740
12907
  if (isNew) {
12741
- return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
12908
+ return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
12742
12909
  }
12743
12910
  const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
12744
12911
  let tiMarker = "";
12745
12912
  const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
12746
12913
  if (ti.suspicious)
12747
12914
  tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
12748
- return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
12915
+ return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
12749
12916
  } catch (err) {
12750
12917
  return { error: err.message };
12751
12918
  }
@@ -13388,14 +13555,14 @@ ${globalCapMatches(output)}` : "";
13388
13555
  }
13389
13556
  }
13390
13557
  async function moveFile(input, workDir) {
13391
- const source = typeof input.source === "string" ? input.source : "";
13558
+ const source2 = typeof input.source === "string" ? input.source : "";
13392
13559
  const dest = typeof input.dest === "string" ? input.dest : "";
13393
- if (!source)
13560
+ if (!source2)
13394
13561
  return { error: "Missing required parameter: source" };
13395
13562
  if (!dest)
13396
13563
  return { error: "Missing required parameter: dest" };
13397
13564
  try {
13398
- const resolvedSource = resolvePath(source, workDir);
13565
+ const resolvedSource = resolvePath(source2, workDir);
13399
13566
  const resolvedDest = resolvePath(dest, workDir);
13400
13567
  fs9.mkdirSync(path6.dirname(resolvedDest), { recursive: true });
13401
13568
  try {
@@ -13531,22 +13698,23 @@ ${globalCapMatches(output)}` : "";
13531
13698
  const linesBefore = origNorm.split("\n").length;
13532
13699
  const linesAfter = updated.split("\n").length;
13533
13700
  const xfile = crossFileBreakageWarning(resolved, origNorm, normalizeLF(updated), workDir ?? process.cwd());
13701
+ const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
13534
13702
  return { output: `Edited ${resolved} (${linesBefore} \u2192 ${linesAfter} lines)
13535
13703
 
13536
- ${diff3}${xfile}` };
13704
+ ${diff3}${xfile}${sec}` };
13537
13705
  } catch (err) {
13538
13706
  return { error: err.message };
13539
13707
  }
13540
13708
  }
13541
13709
  async function copyFile(input, workDir) {
13542
- const source = typeof input.source === "string" ? input.source : "";
13710
+ const source2 = typeof input.source === "string" ? input.source : "";
13543
13711
  const destination = typeof input.destination === "string" ? input.destination : "";
13544
- if (!source)
13712
+ if (!source2)
13545
13713
  return { error: "Missing required parameter: source" };
13546
13714
  if (!destination)
13547
13715
  return { error: "Missing required parameter: destination" };
13548
13716
  try {
13549
- const resolvedSrc = resolvePath(source, workDir);
13717
+ const resolvedSrc = resolvePath(source2, workDir);
13550
13718
  const resolvedDest = resolvePath(destination, workDir);
13551
13719
  if (fs9.existsSync(resolvedDest) && fs9.statSync(resolvedDest).isDirectory()) {
13552
13720
  return { error: `Destination is a directory: ${resolvedDest}. Provide a full file path including the filename.` };
@@ -13801,6 +13969,7 @@ ${body}${truncNote}
13801
13969
  let content = normalizeLF(rawFile);
13802
13970
  const originalNorm = content;
13803
13971
  const diffs = [];
13972
+ const insertedText = [];
13804
13973
  for (let i2 = 0; i2 < edits.length; i2++) {
13805
13974
  const edit = edits[i2];
13806
13975
  const oldStr = normalizeLF(typeof edit.old_string === "string" ? edit.old_string : "");
@@ -13815,16 +13984,18 @@ ${body}${truncNote}
13815
13984
  return { error: `Edit #${i2 + 1}: old_string appears ${count} times \u2014 it must be unique. Add more surrounding context.` };
13816
13985
  }
13817
13986
  diffs.push(buildDiff(filePath, oldStr, newStr, content));
13987
+ insertedText.push(newStr);
13818
13988
  content = literalReplace(content, oldStr, newStr);
13819
13989
  }
13820
13990
  const finalContent = wasCRLF ? content.replace(/\n/g, "\r\n") : content;
13821
13991
  atomicWritePreservingMode(resolved, finalContent, pre.mode);
13822
13992
  const xfile = crossFileBreakageWarning(resolved, originalNorm, content, workDir ?? process.cwd());
13993
+ const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join("\n")));
13823
13994
  return {
13824
13995
  output: `Applied ${edits.length} edit(s) to ${resolved}:
13825
13996
  ` + diffs.map((d, i2) => `
13826
13997
  --- edit #${i2 + 1} ---
13827
- ${d}`).join("\n") + xfile
13998
+ ${d}`).join("\n") + xfile + sec
13828
13999
  };
13829
14000
  } catch (err) {
13830
14001
  return { error: err.message };
@@ -13989,7 +14160,7 @@ ${lines.join("\n")}`;
13989
14160
  const editType = typeof input.edit_type === "string" ? input.edit_type : "";
13990
14161
  const rawIdx = input.cell_index;
13991
14162
  const cellIndex = typeof rawIdx === "number" ? Math.floor(rawIdx) : typeof rawIdx === "string" && rawIdx.trim() !== "" ? parseInt(rawIdx, 10) : -1;
13992
- const source = typeof input.source === "string" ? input.source : null;
14163
+ const source2 = typeof input.source === "string" ? input.source : null;
13993
14164
  const cellType = typeof input.cell_type === "string" ? input.cell_type : "code";
13994
14165
  if (!filePath)
13995
14166
  return { error: "Missing required parameter: path" };
@@ -14008,10 +14179,10 @@ ${lines.join("\n")}`;
14008
14179
  if (editType === "replace") {
14009
14180
  if (cellIndex < 0 || cellIndex >= n)
14010
14181
  return { error: `cell_index ${cellIndex} out of range (0\u2013${n - 1})` };
14011
- if (source === null)
14182
+ if (source2 === null)
14012
14183
  return { error: "Missing required parameter: source (required for replace)" };
14013
14184
  const cell = nb.cells[cellIndex];
14014
- cell.source = source;
14185
+ cell.source = source2;
14015
14186
  if (cell.cell_type === "code") {
14016
14187
  cell.outputs = [];
14017
14188
  cell.execution_count = null;
@@ -14026,12 +14197,12 @@ ${lines.join("\n")}`;
14026
14197
  save();
14027
14198
  return { output: `Deleted cell ${cellIndex} [${removed.cell_type}] from ${resolved}. Notebook now has ${nb.cells.length} cells.` };
14028
14199
  }
14029
- if (source === null)
14200
+ if (source2 === null)
14030
14201
  return { error: "Missing required parameter: source (required for insert)" };
14031
14202
  const insertAt = cellIndex >= 0 ? Math.min(cellIndex + 1, n) : n;
14032
14203
  const newCell = {
14033
14204
  cell_type: cellType,
14034
- source,
14205
+ source: source2,
14035
14206
  metadata: {},
14036
14207
  ...cellType === "code" ? { execution_count: null, outputs: [] } : {}
14037
14208
  };
@@ -14398,18 +14569,62 @@ var require_agentTypes = __commonJS({
14398
14569
  };
14399
14570
  }();
14400
14571
  Object.defineProperty(exports, "__esModule", { value: true });
14401
- exports.loadAgentTypes = loadAgentTypes2;
14572
+ exports.loadAgentTypes = loadAgentTypes;
14573
+ exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings2;
14574
+ exports.knownToolNames = knownToolNames;
14575
+ exports.builtinAgents = builtinAgents;
14402
14576
  exports.summariseAgents = summariseAgents;
14403
14577
  exports.findAgentType = findAgentType;
14404
14578
  var fs9 = __importStar(__require("fs"));
14405
14579
  var path6 = __importStar(__require("path"));
14406
14580
  var os6 = __importStar(__require("os"));
14407
14581
  var index_1 = require_plugins();
14582
+ var READ_ONLY_TOOLS = [
14583
+ // Universal
14584
+ "read_file",
14585
+ "search_files",
14586
+ "glob",
14587
+ "list_directory",
14588
+ "bash",
14589
+ "bash_output",
14590
+ // kill_shell belongs next to bash_output: an agent that can start a background
14591
+ // process and poll it but never stop it leaks that process past its own
14592
+ // lifetime. Stopping a shell you started is not a write to the repo.
14593
+ "kill_shell",
14594
+ "notebook_read",
14595
+ "todo_write",
14596
+ "todo_read",
14597
+ // Skills are reusable prompt playbooks, and loop.ts advertises the skills
14598
+ // catalogue to sub-agents at EVERY depth — so withholding the tool that loads
14599
+ // one meant showing every sub-agent a menu it could not order from.
14600
+ "use_skill",
14601
+ // VS Code language server (ignored on the CLI)
14602
+ "get_symbols",
14603
+ "get_workspace_symbols",
14604
+ "find_references",
14605
+ "go_to_definition",
14606
+ "get_hover",
14607
+ "get_diagnostics"
14608
+ ];
14609
+ var RESEARCH_TOOLS = [...READ_ONLY_TOOLS, "fetch_url"];
14610
+ var WRITE_TOOLS = [
14611
+ ...READ_ONLY_TOOLS,
14612
+ "write_file",
14613
+ "edit_file",
14614
+ "multi_edit",
14615
+ "create_directory",
14616
+ "move_file",
14617
+ "copy_file",
14618
+ // notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
14619
+ // knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
14620
+ // test-writer silently could not touch notebooks.
14621
+ "notebook_edit"
14622
+ ];
14408
14623
  var BUILTIN_AGENTS = [
14409
14624
  {
14410
14625
  name: "reviewer",
14411
14626
  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: ["read_file", "search_files", "glob", "list_directory", "bash", "get_symbols", "get_workspace_symbols", "find_references", "go_to_definition", "get_diagnostics"],
14627
+ tools: READ_ONLY_TOOLS,
14413
14628
  source: "builtin",
14414
14629
  prompt: [
14415
14630
  "You are a meticulous senior code reviewer. You NEVER modify files \u2014 you only read, search, and report.",
@@ -14425,78 +14640,377 @@ var require_agentTypes = __commonJS({
14425
14640
  "Report format: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F7E2} Suggestion, each with file:line and a concrete fix,",
14426
14641
  "then a final verdict (APPROVE or REQUEST CHANGES) with a one-paragraph rationale."
14427
14642
  ].join("\n")
14643
+ },
14644
+ // Promoted from the security-audit plugin to a builtin.
14645
+ //
14646
+ // Leaving it plugin-only was indefensible next to `reviewer` being builtin:
14647
+ // reviewer's own prompt already tells it to look for security issues, so
14648
+ // security IS treated as default work — yet the specialist agent for it was
14649
+ // invisible unless the user happened to know the plugin existed. For an agent
14650
+ // that WRITES code, "you only get a security review if you knew to install
14651
+ // something" is the wrong default.
14652
+ {
14653
+ name: "security-auditor",
14654
+ description: "Read-only security auditor \u2014 hunts injection, authz, secrets, and validation flaws in a path or diff. Cannot modify files.",
14655
+ tools: READ_ONLY_TOOLS,
14656
+ model: "pro",
14657
+ source: "builtin",
14658
+ prompt: [
14659
+ "You are a security auditor. You find real, exploitable flaws \u2014 not style issues.",
14660
+ "",
14661
+ "Method:",
14662
+ "1. Map the attack surface FIRST: entry points (HTTP routes, message handlers, CLI args, file/network",
14663
+ " input, deserialization), then trace user-controlled data inward to where it is used.",
14664
+ "2. For each finding: file:line, the flaw class, a one-line exploit scenario, and the concrete fix.",
14665
+ "3. Grade severity honestly: Critical = remote compromise or data breach; High = auth bypass/IDOR;",
14666
+ " Medium = needs unusual preconditions; Low = hardening.",
14667
+ "",
14668
+ "Classes worth the most attention, in order: injection (SQL/command/template/prototype), broken",
14669
+ "authz (missing ownership checks, IDOR, trusting client-supplied ids), secrets committed to source,",
14670
+ "path traversal, SSRF, unsafe deserialization, missing rate limits on expensive or auth endpoints,",
14671
+ "and crypto misuse (hand-rolled comparison, predictable randomness).",
14672
+ "",
14673
+ "Hard rules:",
14674
+ "- READ-ONLY: never modify, create or delete files. bash only for read-only inspection.",
14675
+ "- NEVER print a discovered secret's value. Report its location and advise rotation.",
14676
+ "- Distinguish EXPLOITABLE from theoretical, and say which one each finding is.",
14677
+ '- "No issues found in scope X" is a valid, useful result. Do not pad the report to look thorough.'
14678
+ ].join("\n")
14679
+ },
14680
+ // The gap Claude Code fills with its built-in `Explore`: read-heavy codebase
14681
+ // search that would otherwise flood the parent's context. Defaults to the
14682
+ // cheapest model on purpose — "find every caller of X" has no need of a
14683
+ // frontier model, and this is the agent most likely to be spawned in bulk.
14684
+ {
14685
+ name: "explorer",
14686
+ 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.",
14687
+ tools: READ_ONLY_TOOLS,
14688
+ model: "turbo",
14689
+ source: "builtin",
14690
+ prompt: [
14691
+ "You map code. You NEVER modify anything.",
14692
+ "",
14693
+ "Method:",
14694
+ "1. Prefer structural search over text search where available (get_workspace_symbols, find_references,",
14695
+ " go_to_definition); fall back to search_files/glob otherwise.",
14696
+ "2. Read only the sections you need \u2014 use read_file with offset/limit on large files instead of",
14697
+ " pulling in thousands of lines.",
14698
+ "3. Follow the real call graph rather than guessing from names.",
14699
+ "",
14700
+ "Your ONLY output is a compact report: the file:line locations that matter, how they relate, and the",
14701
+ "direct answer to the question you were given. This exists to keep bulk search OUT of the parent's",
14702
+ "context, so do not paste large file contents back \u2014 cite locations and summarise. Say plainly when",
14703
+ 'something does not exist; a confident wrong answer is far worse than "not found".'
14704
+ ].join("\n")
14705
+ },
14706
+ // Matches Claude Code's built-in `Plan`: research a change and return a
14707
+ // strategy, deliberately WITHOUT write access so "make a plan" can never
14708
+ // quietly become "start editing".
14709
+ {
14710
+ name: "planner",
14711
+ 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.",
14712
+ tools: RESEARCH_TOOLS,
14713
+ model: "pro",
14714
+ source: "builtin",
14715
+ prompt: [
14716
+ "You produce implementation plans. You NEVER modify files \u2014 planning and doing are separate steps,",
14717
+ 'and this agent exists so "plan it" cannot silently turn into "change it".',
14718
+ "",
14719
+ "Method:",
14720
+ "1. Read the actual code before proposing anything. No plan may rest on an assumed API shape.",
14721
+ "2. Find every affected call site (find_references / search_files) and list them.",
14722
+ "3. Order the steps so the tree stays working after each one \u2014 types, then implementation, then",
14723
+ " tests, then exports/registration.",
14724
+ "",
14725
+ "Output:",
14726
+ "- Goal, in one sentence.",
14727
+ "- Numbered steps, each with the exact files touched and what changes in them.",
14728
+ "- Risks + the specific thing that could break, and how it would be detected.",
14729
+ "- How to verify (the exact test/build command for THIS project, taken from package.json/Makefile).",
14730
+ "- Anything genuinely ambiguous, stated as an open question rather than a silent assumption."
14731
+ ].join("\n")
14732
+ },
14733
+ // Promoted from the test-gen plugin. Needs write access — it produces test
14734
+ // files — but is deliberately forbidden from touching source, because "make the
14735
+ // tests pass" is the single most common way an agent destroys signal.
14736
+ {
14737
+ name: "test-writer",
14738
+ description: "Writes tests that follow the project's existing conventions. May create/edit TEST files only \u2014 never production source.",
14739
+ tools: WRITE_TOOLS,
14740
+ // Enforced, not merely requested: the permission gate refuses a write whose
14741
+ // path is not a test file. Without this the allowlist would grant edit_file
14742
+ // for every path and the rule below would be a suggestion the model is free
14743
+ // to rationalise its way past.
14744
+ testFilesOnly: true,
14745
+ source: "builtin",
14746
+ prompt: [
14747
+ "You write tests. You may create and edit TEST files only.",
14748
+ "",
14749
+ "Hard rules \u2014 these are the ways test-writing agents destroy value, so they are non-negotiable:",
14750
+ "- NEVER modify production source to make a test pass. If the code looks wrong, REPORT it and stop.",
14751
+ "- NEVER weaken, delete or skip an existing assertion or test.",
14752
+ "- A test that cannot fail is worse than no test. Every test must be able to fail for one clear reason.",
14753
+ "",
14754
+ "Method:",
14755
+ "1. Read the existing tests FIRST and copy their conventions exactly \u2014 runner, file naming, layout,",
14756
+ " assertion style, fixture/helper patterns. Never introduce a new framework.",
14757
+ "2. Test observable behaviour and the contract, not private internals.",
14758
+ "3. Cover the boring-but-real cases: empty input, null/undefined, unicode and non-BMP characters,",
14759
+ " boundaries, error paths, concurrency where it applies.",
14760
+ "4. No sleeps or wall-clock dependence \u2014 those produce the flaky tests that get deleted later.",
14761
+ "5. RUN the tests you wrote and report the real output. Never claim a test passes without running it."
14762
+ ].join("\n")
14763
+ },
14764
+ // The DevOps gap — answered with a READ-ONLY advisor, not an operator.
14765
+ //
14766
+ // A "DevOps agent" with write/apply access is a genuinely different risk class
14767
+ // from the others here: its mistakes are `kubectl delete`, a bad `terraform
14768
+ // apply`, a broken deploy pipeline — often not revertible and affecting
14769
+ // production rather than a working tree. So this one diagnoses and proposes a
14770
+ // diff; a human applies it. That asymmetry is the whole design.
14771
+ {
14772
+ name: "devops-advisor",
14773
+ 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.",
14774
+ tools: RESEARCH_TOOLS,
14775
+ model: "pro",
14776
+ source: "builtin",
14777
+ prompt: [
14778
+ "You are an infrastructure and delivery advisor. You DIAGNOSE and PROPOSE. You never apply changes.",
14779
+ "",
14780
+ "Hard rules:",
14781
+ "- READ-ONLY, and stricter than the other read-only agents: bash is for INSPECTION only",
14782
+ " (git log/diff, cat, grep, `kubectl get/describe`, `docker images`, `terraform plan`).",
14783
+ " NEVER run anything that mutates infrastructure \u2014 no apply/delete/scale/rollout/restart/push,",
14784
+ " no `terraform apply`, no `helm upgrade`. If a fix needs such a command, WRITE IT OUT for a human.",
14785
+ "- Never print secret values from env files, k8s Secrets or CI variables. Reference them by name.",
14786
+ "",
14787
+ "Method:",
14788
+ "1. Read what actually exists \u2014 workflow files, Dockerfiles, manifests, kustomize overlays, the",
14789
+ ' deploy scripts \u2014 before drawing any conclusion. Never reason from what a stack "usually" looks like.',
14790
+ "2. Follow the real path a change takes to production, and name the step that is broken or missing.",
14791
+ "3. Check the failure modes that bite hardest: CI path filters that skip files a workload actually",
14792
+ " needs, image tags that do not match what is deployed, missing health probes, absent resource",
14793
+ " limits, secrets baked into images, ports/timeouts inconsistent between proxy and app, and",
14794
+ " migrations that must run before the new image is live.",
14795
+ "",
14796
+ "Output: the diagnosis, the evidence (file:line or command output), the proposed change as a diff or",
14797
+ "exact file content, and the command a human should run to apply and verify it."
14798
+ ].join("\n")
14428
14799
  }
14429
14800
  ];
14801
+ var KNOWN_TOOL_NAMES = /* @__PURE__ */ new Set([
14802
+ "read_file",
14803
+ "write_file",
14804
+ "edit_file",
14805
+ "multi_edit",
14806
+ "list_directory",
14807
+ "create_directory",
14808
+ "move_file",
14809
+ "copy_file",
14810
+ "delete_file",
14811
+ "search_files",
14812
+ "glob",
14813
+ "bash",
14814
+ "bash_output",
14815
+ "kill_shell",
14816
+ "notebook_read",
14817
+ "notebook_edit",
14818
+ "todo_write",
14819
+ "todo_read",
14820
+ "memory_write",
14821
+ "memory_read",
14822
+ "use_skill",
14823
+ "fetch_url",
14824
+ "web_search",
14825
+ "generate_image",
14826
+ "stock_photo",
14827
+ "open_in_browser",
14828
+ "task",
14829
+ "get_symbols",
14830
+ "get_workspace_symbols",
14831
+ "find_references",
14832
+ "go_to_definition",
14833
+ "get_hover",
14834
+ "get_diagnostics"
14835
+ ]);
14836
+ var KNOWN_META_KEYS = /* @__PURE__ */ new Set([
14837
+ "name",
14838
+ "description",
14839
+ "tools",
14840
+ "model",
14841
+ "test_files_only",
14842
+ "testfilesonly"
14843
+ ]);
14844
+ var VALID_MODELS = ["turbo", "pro", "ultra"];
14430
14845
  function parseFrontmatter(raw) {
14431
14846
  const m2 = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
14432
14847
  if (!m2)
14433
- return { meta: {}, body: raw.trim() };
14848
+ return { meta: {}, body: raw.trim(), ok: false };
14434
14849
  const meta = {};
14435
14850
  for (const line of m2[1].split(/\r?\n/)) {
14436
14851
  const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
14437
14852
  if (kv)
14438
14853
  meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
14439
14854
  }
14440
- return { meta, body: (m2[2] ?? "").trim() };
14855
+ return { meta, body: (m2[2] ?? "").trim(), ok: true };
14441
14856
  }
14442
14857
  function parseModel(v) {
14443
14858
  const s2 = (v ?? "").toLowerCase();
14444
14859
  return s2 === "turbo" || s2 === "pro" || s2 === "ultra" ? s2 : void 0;
14445
14860
  }
14861
+ function parseBool(v) {
14862
+ return /^(true|yes|1|on)$/i.test((v ?? "").trim());
14863
+ }
14446
14864
  function parseToolList(v) {
14447
14865
  if (!v)
14448
14866
  return void 0;
14449
14867
  const tools = v.replace(/^\[|\]$/g, "").split(/[,\s]+/).map((t2) => t2.trim()).filter(Boolean);
14450
14868
  return tools.length ? tools : void 0;
14451
14869
  }
14452
- function loadDir(dir, source, into) {
14870
+ function loadDir(dir, source2, into, warnings) {
14453
14871
  let entries;
14454
14872
  try {
14455
- entries = fs9.readdirSync(dir).filter((f3) => f3.endsWith(".md"));
14873
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
14456
14874
  } catch {
14457
14875
  return;
14458
14876
  }
14459
- for (const file of entries) {
14877
+ for (const entry of entries) {
14878
+ const full = path6.join(dir, entry.name);
14879
+ if (entry.isDirectory()) {
14880
+ loadDir(full, source2, into, warnings);
14881
+ continue;
14882
+ }
14883
+ if (!entry.name.endsWith(".md"))
14884
+ continue;
14885
+ let raw;
14460
14886
  try {
14461
- const raw = fs9.readFileSync(path6.join(dir, file), "utf-8");
14462
- const { meta, body } = parseFrontmatter(raw);
14463
- const name = (meta.name || path6.basename(file, ".md")).trim();
14464
- if (!name)
14465
- continue;
14466
- if (source !== "project" && into.has(name))
14467
- continue;
14468
- into.set(name, {
14469
- name,
14470
- description: meta.description || `Custom ${name} agent`,
14471
- tools: parseToolList(meta.tools),
14472
- model: parseModel(meta.model),
14473
- prompt: body,
14474
- source
14887
+ raw = fs9.readFileSync(full, "utf-8");
14888
+ } catch (err) {
14889
+ warnings.push({ file: full, agent: path6.basename(entry.name, ".md"), message: `could not be read (${err.message}) \u2014 this agent was skipped` });
14890
+ continue;
14891
+ }
14892
+ const { meta, body, ok } = parseFrontmatter(raw);
14893
+ const name = (meta.name || path6.basename(entry.name, ".md")).trim();
14894
+ if (!name)
14895
+ continue;
14896
+ if (source2 !== "project" && into.has(name))
14897
+ continue;
14898
+ const tools = parseToolList(meta.tools);
14899
+ let effectiveTools = tools;
14900
+ if (!ok) {
14901
+ effectiveTools = READ_ONLY_TOOLS;
14902
+ warnings.push({
14903
+ file: full,
14904
+ agent: name,
14905
+ message: "has no valid YAML frontmatter (a `---` block must be the first thing in the file), so no tool allowlist could be read. Treating it as READ-ONLY. Add frontmatter with a `tools:` line to grant more."
14475
14906
  });
14476
- } catch {
14907
+ } else {
14908
+ if (!meta.description) {
14909
+ warnings.push({
14910
+ file: full,
14911
+ agent: name,
14912
+ message: "has no `description:` \u2014 that text is the ONLY thing the model uses to decide when to delegate to this agent, so it will rarely be picked."
14913
+ });
14914
+ }
14915
+ if (meta.model !== void 0 && parseModel(meta.model) === void 0) {
14916
+ warnings.push({
14917
+ file: full,
14918
+ agent: name,
14919
+ message: `has model: "${meta.model}", which is not valid \u2014 use one of ${VALID_MODELS.join(", ")}, or omit the line to inherit the current session's model.`
14920
+ });
14921
+ }
14922
+ const unknown = (tools ?? []).filter((t2) => !KNOWN_TOOL_NAMES.has(t2) && !t2.includes("__"));
14923
+ if (unknown.length) {
14924
+ warnings.push({
14925
+ file: full,
14926
+ agent: name,
14927
+ message: `lists unknown tool name(s): ${unknown.join(", ")}. An allowlist only grants, so these silently do nothing and the agent cannot use them. Tool names are lower_snake_case (read_file, search_files, glob, bash).`
14928
+ });
14929
+ }
14930
+ const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
14931
+ if (strayKeys.length) {
14932
+ warnings.push({
14933
+ file: full,
14934
+ agent: name,
14935
+ message: `has unrecognised frontmatter key(s): ${strayKeys.join(", ")} \u2014 these are ignored.`
14936
+ });
14937
+ }
14938
+ if (!body) {
14939
+ warnings.push({
14940
+ file: full,
14941
+ agent: name,
14942
+ message: "has an empty body \u2014 the text below the frontmatter IS the agent's system prompt, so it currently has no instructions."
14943
+ });
14944
+ }
14477
14945
  }
14946
+ into.set(name, {
14947
+ name,
14948
+ description: meta.description || `Custom ${name} agent`,
14949
+ tools: effectiveTools,
14950
+ model: parseModel(meta.model),
14951
+ prompt: body,
14952
+ source: source2,
14953
+ // Exposed to user/plugin definitions too — `test_files_only: true` (or
14954
+ // `testFilesOnly`) lets anyone build a test-writing agent that genuinely
14955
+ // cannot touch production source, rather than only the builtin getting
14956
+ // that guarantee.
14957
+ ...parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}
14958
+ });
14478
14959
  }
14479
14960
  }
14480
- function loadAgentTypes2(workDir) {
14961
+ function loadAgentTypes(workDir) {
14962
+ return loadAgentTypesWithWarnings2(workDir).types;
14963
+ }
14964
+ function loadAgentTypesWithWarnings2(workDir) {
14481
14965
  const out = /* @__PURE__ */ new Map();
14482
- loadDir(path6.join(workDir, ".nexrall", "agents"), "project", out);
14483
- loadDir(path6.join(os6.homedir(), ".nexrall", "agents"), "global", out);
14966
+ const warnings = [];
14967
+ loadDir(path6.join(workDir, ".nexrall", "agents"), "project", out, warnings);
14968
+ loadDir(path6.join(os6.homedir(), ".nexrall", "agents"), "global", out, warnings);
14484
14969
  for (const dir of (0, index_1.pluginAssetDirs)(workDir, "agents"))
14485
- loadDir(dir, "plugin", out);
14970
+ loadDir(dir, "plugin", out, warnings);
14486
14971
  for (const agent of BUILTIN_AGENTS) {
14487
14972
  if (!out.has(agent.name))
14488
14973
  out.set(agent.name, agent);
14489
14974
  }
14490
- return [...out.values()];
14975
+ const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i2) => [a.name, i2]));
14976
+ const types3 = [...out.values()].sort((a, b) => {
14977
+ const ai = builtinOrder.get(a.name);
14978
+ const bi = builtinOrder.get(b.name);
14979
+ if (ai !== void 0 && bi !== void 0)
14980
+ return ai - bi;
14981
+ if (ai !== void 0)
14982
+ return -1;
14983
+ if (bi !== void 0)
14984
+ return 1;
14985
+ return a.name.localeCompare(b.name);
14986
+ });
14987
+ return { types: types3, warnings };
14988
+ }
14989
+ function knownToolNames() {
14990
+ return [...KNOWN_TOOL_NAMES].sort();
14991
+ }
14992
+ function builtinAgents() {
14993
+ return BUILTIN_AGENTS;
14491
14994
  }
14492
14995
  function summariseAgents(types3) {
14493
14996
  if (!types3.length)
14494
14997
  return "";
14495
14998
  return types3.map((t2) => {
14496
- const tools = t2.tools ? ` (tools: ${t2.tools.join(", ")})` : "";
14497
- return `- ${t2.name}: ${t2.description}${tools}`;
14999
+ const canWrite = !t2.tools || t2.tools.some((x2) => WRITE_TOOL_HINTS.has(x2));
15000
+ const access = t2.testFilesOnly ? "writes TEST files only" : canWrite ? "can modify files" : "read-only";
15001
+ const model = t2.model ? `, ${t2.model} model` : "";
15002
+ return `- ${t2.name} (${access}${model}): ${t2.description}`;
14498
15003
  }).join("\n");
14499
15004
  }
15005
+ var WRITE_TOOL_HINTS = /* @__PURE__ */ new Set([
15006
+ "write_file",
15007
+ "edit_file",
15008
+ "multi_edit",
15009
+ "notebook_edit",
15010
+ "delete_file",
15011
+ "move_file",
15012
+ "copy_file"
15013
+ ]);
14500
15014
  function findAgentType(types3, name) {
14501
15015
  if (!name)
14502
15016
  return void 0;
@@ -14896,7 +15410,7 @@ var require_loop = __commonJS({
14896
15410
  };
14897
15411
  }();
14898
15412
  Object.defineProperty(exports, "__esModule", { value: true });
14899
- exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
15413
+ exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = void 0;
14900
15414
  exports.resolveMaxIterations = resolveMaxIterations;
14901
15415
  exports.createLimiter = createLimiter;
14902
15416
  exports.extractSubTaskText = extractSubTaskText;
@@ -14905,6 +15419,7 @@ var require_loop = __commonJS({
14905
15419
  exports.contextWindowFor = contextWindowFor2;
14906
15420
  exports.compactionThresholds = compactionThresholds2;
14907
15421
  exports.estimateBodyBytes = estimateBodyBytes2;
15422
+ exports.allowsTestOnlyWrite = allowsTestOnlyWrite;
14908
15423
  exports.findSafeCutIndex = findSafeCutIndex;
14909
15424
  exports.transcriptOf = transcriptOf;
14910
15425
  exports.createLedger = createLedger;
@@ -15172,6 +15687,13 @@ var require_loop = __commonJS({
15172
15687
  var _subTaskCounter = 0;
15173
15688
  var SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0 ? Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) : 10 * 60 * 1e3;
15174
15689
  var SUBTASK_MAX = 48e3;
15690
+ var ToolNotAllowedError = class extends Error {
15691
+ constructor(message) {
15692
+ super(message);
15693
+ this.name = "ToolNotAllowedError";
15694
+ }
15695
+ };
15696
+ exports.ToolNotAllowedError = ToolNotAllowedError;
15175
15697
  function sliceSafeEnd(s2, max) {
15176
15698
  if (s2.length <= max)
15177
15699
  return s2;
@@ -15235,10 +15757,18 @@ ${tail}`;
15235
15757
  return { error: "Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it." };
15236
15758
  }
15237
15759
  const requestedType = typeof input.subagent_type === "string" ? input.subagent_type : "";
15238
- const agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
15760
+ let agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
15761
+ let knownTypes = agentTypes;
15762
+ if (requestedType && !agent) {
15763
+ knownTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
15764
+ agent = (0, agentTypes_1.findAgentType)(knownTypes, requestedType);
15765
+ }
15239
15766
  if (requestedType && !agent) {
15240
- const known = agentTypes.map((a) => a.name).join(", ") || "(none defined)";
15241
- return { error: `Unknown subagent_type "${requestedType}". Available types: ${known}.` };
15767
+ const known = knownTypes.map((a) => a.name).join(", ") || "(none defined)";
15768
+ return {
15769
+ error: `Unknown subagent_type "${requestedType}". Available types: ${known}.
15770
+ If you just created .nexrall/agents/` + requestedType + ".md, make sure the write finished in an EARLIER tool call than this one \u2014 a file written in the same batch may not be on disk yet."
15771
+ };
15242
15772
  }
15243
15773
  const subNexrallMd = agent ? `# Sub-agent role: ${agent.name}
15244
15774
  ${agent.prompt}` + (options.nexrallMd ? `
@@ -15248,8 +15778,12 @@ ${agent.prompt}` + (options.nexrallMd ? `
15248
15778
  ${options.nexrallMd}` : "") : options.nexrallMd;
15249
15779
  const allowed = agent?.tools ? new Set(agent.tools) : null;
15250
15780
  const gatedPermission = async (req) => {
15251
- if (allowed && !allowed.has(req.tool))
15252
- return false;
15781
+ if (allowed && !allowed.has(req.tool)) {
15782
+ throw new ToolNotAllowedError(`The "${agent.name}" sub-agent is not allowed to use \`${req.tool}\` \u2014 it is not in that agent's tool allowlist. This is a restriction of the agent definition, NOT a user decision: do not ask for approval, use one of the tools you do have, or report back that the task needs a different agent.`);
15783
+ }
15784
+ if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input)) {
15785
+ throw new ToolNotAllowedError(`The "${agent.name}" sub-agent may only write to TEST files, so \`${req.tool}\` was refused for this path. Do not try to work around it: if production code must change, say so in your report instead.`);
15786
+ }
15253
15787
  return options.requestPermission(req);
15254
15788
  };
15255
15789
  const subMessages = [
@@ -15394,6 +15928,15 @@ ${partial}` : "",
15394
15928
  return true;
15395
15929
  }
15396
15930
  exports.WRITE_TOOL_NAMES = /* @__PURE__ */ new Set(["write_file", "edit_file", "multi_edit", "delete_file", "move_file", "copy_file", "notebook_edit"]);
15931
+ function allowsTestOnlyWrite(tool, input) {
15932
+ if (!exports.WRITE_TOOL_NAMES.has(tool))
15933
+ return true;
15934
+ const pathKeys = tool === "notebook_edit" ? ["path"] : ["path", "source", "dest", "destination"];
15935
+ const candidates = pathKeys.map((k) => input?.[k]).filter((v) => typeof v === "string" && v.length > 0);
15936
+ if (candidates.length === 0)
15937
+ return false;
15938
+ return candidates.every((p) => (0, testIntegrity_1.isTestFile)(p));
15939
+ }
15397
15940
  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
15941
  function findSafeCutIndex(messages, maxIdx) {
15399
15942
  for (let i2 = Math.min(maxIdx, messages.length - 1); i2 >= 2; i2--) {
@@ -15926,13 +16469,16 @@ Continue the work from here.` }] });
15926
16469
  }
15927
16470
  const description = humanDescription(name, input);
15928
16471
  let permitted;
16472
+ let deniedReason = null;
15929
16473
  try {
15930
16474
  permitted = await options.requestPermission({ tool: name, input, description });
15931
- } catch {
16475
+ } catch (err) {
15932
16476
  permitted = false;
16477
+ if (err instanceof ToolNotAllowedError)
16478
+ deniedReason = err.message;
15933
16479
  }
15934
16480
  if (!permitted) {
15935
- result = { error: "Permission denied by user" };
16481
+ result = { error: deniedReason ?? "Permission denied by user" };
15936
16482
  } else if (name === "task") {
15937
16483
  result = depth === 0 ? await _subTaskLimit(() => runSubTask(input, options, agentTypes)) : await runSubTask(input, options, agentTypes);
15938
16484
  } else {
@@ -17373,6 +17919,175 @@ var require_destructive = __commonJS({
17373
17919
  }
17374
17920
  });
17375
17921
 
17922
+ // ../core/dist/plugins/sources.js
17923
+ var require_sources = __commonJS({
17924
+ "../core/dist/plugins/sources.js"(exports) {
17925
+ "use strict";
17926
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m2, k, k2) {
17927
+ if (k2 === void 0)
17928
+ k2 = k;
17929
+ var desc = Object.getOwnPropertyDescriptor(m2, k);
17930
+ if (!desc || ("get" in desc ? !m2.__esModule : desc.writable || desc.configurable)) {
17931
+ desc = { enumerable: true, get: function() {
17932
+ return m2[k];
17933
+ } };
17934
+ }
17935
+ Object.defineProperty(o, k2, desc);
17936
+ } : function(o, m2, k, k2) {
17937
+ if (k2 === void 0)
17938
+ k2 = k;
17939
+ o[k2] = m2[k];
17940
+ });
17941
+ var __setModuleDefault = exports && exports.__setModuleDefault || (Object.create ? function(o, v) {
17942
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17943
+ } : function(o, v) {
17944
+ o["default"] = v;
17945
+ });
17946
+ var __importStar = exports && exports.__importStar || /* @__PURE__ */ function() {
17947
+ var ownKeys = function(o) {
17948
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
17949
+ var ar = [];
17950
+ for (var k in o2)
17951
+ if (Object.prototype.hasOwnProperty.call(o2, k))
17952
+ ar[ar.length] = k;
17953
+ return ar;
17954
+ };
17955
+ return ownKeys(o);
17956
+ };
17957
+ return function(mod) {
17958
+ if (mod && mod.__esModule)
17959
+ return mod;
17960
+ var result = {};
17961
+ if (mod != null) {
17962
+ for (var k = ownKeys(mod), i2 = 0; i2 < k.length; i2++)
17963
+ if (k[i2] !== "default")
17964
+ __createBinding(result, mod, k[i2]);
17965
+ }
17966
+ __setModuleDefault(result, mod);
17967
+ return result;
17968
+ };
17969
+ }();
17970
+ Object.defineProperty(exports, "__esModule", { value: true });
17971
+ exports.loadDeclaredSources = loadDeclaredSources2;
17972
+ exports.allowedSourcePatterns = allowedSourcePatterns2;
17973
+ exports.isSourceAllowed = isSourceAllowed2;
17974
+ exports.resolveSourceAlias = resolveSourceAlias2;
17975
+ exports.writeDeclaredSource = writeDeclaredSource2;
17976
+ exports.removeDeclaredSource = removeDeclaredSource2;
17977
+ exports.sourcesFileFor = sourcesFileFor2;
17978
+ var fs9 = __importStar(__require("fs"));
17979
+ var path6 = __importStar(__require("path"));
17980
+ var os6 = __importStar(__require("os"));
17981
+ function managedSettingsPath() {
17982
+ if (process.platform === "darwin")
17983
+ return "/Library/Application Support/Nexrall/managed-settings.json";
17984
+ if (process.platform === "win32")
17985
+ return path6.join(process.env.PROGRAMDATA ?? "C:\\ProgramData", "Nexrall", "managed-settings.json");
17986
+ return "/etc/nexrall/managed-settings.json";
17987
+ }
17988
+ function readJson(file) {
17989
+ try {
17990
+ return JSON.parse(fs9.readFileSync(file, "utf-8"));
17991
+ } catch {
17992
+ return null;
17993
+ }
17994
+ }
17995
+ var ALIAS_RE = /^[a-z0-9][\w.-]{0,63}$/i;
17996
+ function readSpec(value) {
17997
+ if (typeof value === "string")
17998
+ return value.trim() || null;
17999
+ if (value && typeof value === "object") {
18000
+ const v = value;
18001
+ const inner = v.source ?? v.repo ?? v.url;
18002
+ if (typeof inner === "string")
18003
+ return inner.trim() || null;
18004
+ if (inner && typeof inner === "object") {
18005
+ const deep = inner.repo ?? inner.url;
18006
+ if (typeof deep === "string")
18007
+ return deep.trim() || null;
18008
+ }
18009
+ }
18010
+ return null;
18011
+ }
18012
+ function collect(file, tier, into) {
18013
+ const obj = readJson(file);
18014
+ const raw = obj?.pluginSources;
18015
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
18016
+ return;
18017
+ for (const [name, value] of Object.entries(raw)) {
18018
+ if (!ALIAS_RE.test(name))
18019
+ continue;
18020
+ const spec = readSpec(value);
18021
+ if (!spec)
18022
+ continue;
18023
+ if (into.has(name))
18024
+ continue;
18025
+ into.set(name, { name, spec, tier, file });
18026
+ }
18027
+ }
18028
+ function loadDeclaredSources2(workDir, opts = {}) {
18029
+ const out = /* @__PURE__ */ new Map();
18030
+ collect(managedSettingsPath(), "managed", out);
18031
+ collect(path6.join(os6.homedir(), ".nexrall", "settings.json"), "user", out);
18032
+ if (opts.includeProject) {
18033
+ collect(path6.join(workDir, ".nexrall", "settings.json"), "project", out);
18034
+ collect(path6.join(workDir, ".nexrall", "settings.local.json"), "project", out);
18035
+ }
18036
+ return [...out.values()];
18037
+ }
18038
+ function allowedSourcePatterns2() {
18039
+ const obj = readJson(managedSettingsPath());
18040
+ const v = obj?.allowedPluginSources;
18041
+ if (!Array.isArray(v))
18042
+ return null;
18043
+ const pats = v.filter((x2) => typeof x2 === "string" && x2.trim().length > 0);
18044
+ return pats.length ? pats : null;
18045
+ }
18046
+ function isSourceAllowed2(spec, patterns = allowedSourcePatterns2()) {
18047
+ if (!patterns)
18048
+ return true;
18049
+ const cleaned = spec.trim().replace(/^https?:\/\/(?:www\.)?github\.com\//i, "").replace(/\.git$/, "");
18050
+ const base = cleaned.split("#")[0].split("/").slice(0, 2).join("/").toLowerCase();
18051
+ return patterns.some((p) => {
18052
+ const pat = p.trim().toLowerCase().replace(/^https?:\/\/(?:www\.)?github\.com\//i, "");
18053
+ if (pat === "*")
18054
+ return true;
18055
+ if (pat.endsWith("/*"))
18056
+ return base.startsWith(pat.slice(0, -1));
18057
+ return base === pat.split("#")[0].split("/").slice(0, 2).join("/");
18058
+ });
18059
+ }
18060
+ function resolveSourceAlias2(alias, workDir, opts = {}) {
18061
+ const want = alias.trim().toLowerCase();
18062
+ return loadDeclaredSources2(workDir, opts).find((s2) => s2.name.toLowerCase() === want) ?? null;
18063
+ }
18064
+ function writeDeclaredSource2(file, name, spec) {
18065
+ if (!ALIAS_RE.test(name)) {
18066
+ throw new Error(`Invalid source name "${name}". Use letters, numbers, dot, dash or underscore.`);
18067
+ }
18068
+ const obj = readJson(file) ?? {};
18069
+ const existing = obj.pluginSources && typeof obj.pluginSources === "object" && !Array.isArray(obj.pluginSources) ? obj.pluginSources : {};
18070
+ obj.pluginSources = { ...existing, [name]: spec };
18071
+ fs9.mkdirSync(path6.dirname(file), { recursive: true });
18072
+ fs9.writeFileSync(file, JSON.stringify(obj, null, 2) + "\n", "utf-8");
18073
+ }
18074
+ function removeDeclaredSource2(file, name) {
18075
+ const obj = readJson(file);
18076
+ if (!obj?.pluginSources || typeof obj.pluginSources !== "object")
18077
+ return false;
18078
+ const sources = obj.pluginSources;
18079
+ if (!(name in sources))
18080
+ return false;
18081
+ delete sources[name];
18082
+ fs9.writeFileSync(file, JSON.stringify(obj, null, 2) + "\n", "utf-8");
18083
+ return true;
18084
+ }
18085
+ function sourcesFileFor2(scope, workDir) {
18086
+ return scope === "user" ? path6.join(os6.homedir(), ".nexrall", "settings.json") : path6.join(workDir, ".nexrall", "settings.json");
18087
+ }
18088
+ }
18089
+ });
18090
+
17376
18091
  // ../core/dist/plugins/installer.js
17377
18092
  var require_installer = __commonJS({
17378
18093
  "../core/dist/plugins/installer.js"(exports) {
@@ -17425,8 +18140,8 @@ var require_installer = __commonJS({
17425
18140
  exports.searchRegistry = searchRegistry2;
17426
18141
  exports.getRegistryPlugin = getRegistryPlugin2;
17427
18142
  exports.reportInstall = reportInstall2;
17428
- exports.parsePluginSource = parsePluginSource;
17429
- exports.inspectPluginDir = inspectPluginDir;
18143
+ exports.parsePluginSource = parsePluginSource2;
18144
+ exports.inspectPluginDir = inspectPluginDir2;
17430
18145
  exports.installPlugin = installPlugin2;
17431
18146
  exports.readReceipt = readReceipt2;
17432
18147
  exports.removePlugin = removePlugin2;
@@ -17436,6 +18151,8 @@ var require_installer = __commonJS({
17436
18151
  var os6 = __importStar(__require("os"));
17437
18152
  var child_process_1 = __require("child_process");
17438
18153
  var client_1 = require_client();
18154
+ var index_1 = require_plugins();
18155
+ var sources_1 = require_sources();
17439
18156
  var RECEIPT_FILE = "_install.json";
17440
18157
  async function searchRegistry2(query, limit = 50) {
17441
18158
  const u = new URL(`${client_1.API_BASE}/api/code/plugins`);
@@ -17465,7 +18182,7 @@ var require_installer = __commonJS({
17465
18182
  fetch(`${client_1.API_BASE}/api/code/plugins/${encodeURIComponent(name)}/installed`, { method: "POST" }).catch(() => {
17466
18183
  });
17467
18184
  }
17468
- function parsePluginSource(spec) {
18185
+ function parsePluginSource2(spec) {
17469
18186
  const raw = spec.trim();
17470
18187
  if (!raw)
17471
18188
  throw new Error("Empty plugin source.");
@@ -17512,29 +18229,45 @@ var require_installer = __commonJS({
17512
18229
  return [];
17513
18230
  }
17514
18231
  }
17515
- function inspectPluginDir(dir) {
18232
+ function inspectPluginDir2(dir) {
17516
18233
  let meta = {};
17517
- try {
17518
- const j = JSON.parse(fs9.readFileSync(path6.join(dir, "plugin.json"), "utf-8"));
17519
- meta = {
17520
- name: typeof j.name === "string" ? j.name : void 0,
17521
- version: typeof j.version === "string" ? j.version : void 0,
17522
- description: typeof j.description === "string" ? j.description : void 0
17523
- };
17524
- } catch {
18234
+ const manifest = (0, index_1.resolvePluginFile)(dir, "manifest");
18235
+ if (manifest) {
18236
+ try {
18237
+ const j = JSON.parse(fs9.readFileSync(manifest, "utf-8"));
18238
+ meta = {
18239
+ name: typeof j.name === "string" ? j.name : void 0,
18240
+ version: typeof j.version === "string" ? j.version : void 0,
18241
+ description: typeof j.description === "string" ? j.description : void 0
18242
+ };
18243
+ } catch {
18244
+ }
17525
18245
  }
17526
18246
  return {
17527
18247
  commands: listMd(path6.join(dir, "commands")),
17528
18248
  agents: listMd(path6.join(dir, "agents")),
17529
- hasHooks: fs9.existsSync(path6.join(dir, "hooks.json")),
17530
- hasMcp: fs9.existsSync(path6.join(dir, "mcp.json")),
18249
+ hasHooks: (0, index_1.resolvePluginFile)(dir, "hooks") !== null,
18250
+ hasMcp: (0, index_1.resolvePluginFile)(dir, "mcp") !== null,
17531
18251
  meta
17532
18252
  };
17533
18253
  }
17534
18254
  function looksLikePlugin(dir) {
17535
- const i2 = inspectPluginDir(dir);
18255
+ const i2 = inspectPluginDir2(dir);
17536
18256
  return i2.commands.length > 0 || i2.agents.length > 0 || i2.hasHooks || i2.hasMcp || i2.meta.name !== void 0;
17537
18257
  }
18258
+ async function resolveCommitSha(repo, ref) {
18259
+ try {
18260
+ const res = await fetch(`https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref || "HEAD")}`, {
18261
+ headers: { Accept: "application/vnd.github.sha", "User-Agent": "nexrall-code" }
18262
+ });
18263
+ if (!res.ok)
18264
+ return null;
18265
+ const sha = (await res.text()).trim();
18266
+ return /^[0-9a-f]{40}$/i.test(sha) ? sha : null;
18267
+ } catch {
18268
+ return null;
18269
+ }
18270
+ }
17538
18271
  async function downloadTarball(repo, ref, destFile) {
17539
18272
  const r2 = ref || "HEAD";
17540
18273
  const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(r2)}`;
@@ -17585,28 +18318,33 @@ var require_installer = __commonJS({
17585
18318
  return scope === "project" ? path6.join(workDir || process.cwd(), ".nexrall", "plugins") : path6.join(os6.homedir(), ".nexrall", "plugins");
17586
18319
  }
17587
18320
  async function installPlugin2(spec, opts = {}) {
17588
- const source = parsePluginSource(spec);
18321
+ const source2 = parsePluginSource2(spec);
17589
18322
  const scope = opts.scope ?? "global";
18323
+ const patterns = (0, sources_1.allowedSourcePatterns)();
18324
+ if (patterns && source2.kind === "github" && !(0, sources_1.isSourceAllowed)(source2.raw, patterns)) {
18325
+ throw new Error(`Blocked by your organisation's policy: plugins may only be installed from ${patterns.join(", ")}.`);
18326
+ }
17590
18327
  const tmpBase = fs9.mkdtempSync(path6.join(os6.tmpdir(), "nexrall-plugin-"));
17591
18328
  try {
17592
18329
  let repoRoot;
17593
- if (source.kind === "local") {
17594
- if (!fs9.existsSync(source.repo) || !fs9.statSync(source.repo).isDirectory()) {
17595
- throw new Error(`Local plugin path not found: ${source.repo}`);
18330
+ if (source2.kind === "local") {
18331
+ if (!fs9.existsSync(source2.repo) || !fs9.statSync(source2.repo).isDirectory()) {
18332
+ throw new Error(`Local plugin path not found: ${source2.repo}`);
17596
18333
  }
17597
- repoRoot = source.repo;
18334
+ repoRoot = source2.repo;
17598
18335
  } else {
17599
18336
  const tarFile = path6.join(tmpBase, "plugin.tar.gz");
17600
- await downloadTarball(source.repo, source.ref, tarFile);
18337
+ await downloadTarball(source2.repo, source2.ref, tarFile);
17601
18338
  repoRoot = extractTarball(tarFile, path6.join(tmpBase, "x"));
17602
18339
  }
17603
- let pluginRoot = source.subdir ? path6.join(repoRoot, source.subdir) : repoRoot;
18340
+ const sha = source2.kind === "github" ? await resolveCommitSha(source2.repo, source2.ref) : null;
18341
+ let pluginRoot = source2.subdir ? path6.join(repoRoot, source2.subdir) : repoRoot;
17604
18342
  pluginRoot = path6.resolve(pluginRoot);
17605
18343
  if (!path6.resolve(pluginRoot).startsWith(path6.resolve(repoRoot))) {
17606
18344
  throw new Error("Plugin subdirectory escapes the repository \u2014 refusing.");
17607
18345
  }
17608
18346
  if (!fs9.existsSync(pluginRoot) || !fs9.statSync(pluginRoot).isDirectory()) {
17609
- throw new Error(`Subdirectory not found in repo: ${source.subdir}`);
18347
+ throw new Error(`Subdirectory not found in repo: ${source2.subdir}`);
17610
18348
  }
17611
18349
  if (!looksLikePlugin(pluginRoot)) {
17612
18350
  const candidates = ["plugin", ".nexrall-plugin"].map((c) => path6.join(pluginRoot, c));
@@ -17617,12 +18355,16 @@ var require_installer = __commonJS({
17617
18355
  throw new Error("No plugin found: expected plugin.json, commands/, agents/, hooks.json or mcp.json at the source root.");
17618
18356
  }
17619
18357
  }
17620
- const inspection = inspectPluginDir(pluginRoot);
17621
- const name = opts.name || inspection.meta.name || (source.subdir ? path6.basename(source.subdir) : void 0) || (source.kind === "github" ? source.repo.split("/")[1] : path6.basename(source.repo));
18358
+ const inspection = inspectPluginDir2(pluginRoot);
18359
+ const name = opts.name || inspection.meta.name || (source2.subdir ? path6.basename(source2.subdir) : void 0) || (source2.kind === "github" ? source2.repo.split("/")[1] : path6.basename(source2.repo));
17622
18360
  if (!/^[\w.-]+$/.test(name))
17623
18361
  throw new Error(`Invalid plugin name "${name}".`);
17624
18362
  if (opts.confirm) {
17625
- const ok = await opts.confirm(inspection, name);
18363
+ const ok = await opts.confirm(inspection, name, {
18364
+ sha: sha ?? void 0,
18365
+ previousSha: opts._previousSha,
18366
+ isUpdate: Boolean(opts._previousSha) || Boolean(opts.force)
18367
+ });
17626
18368
  if (!ok)
17627
18369
  throw new Error("Installation cancelled.");
17628
18370
  }
@@ -17635,14 +18377,15 @@ var require_installer = __commonJS({
17635
18377
  }
17636
18378
  copyPluginDir(pluginRoot, dest);
17637
18379
  const receipt = {
17638
- source: source.raw,
17639
- kind: source.kind,
17640
- ref: source.ref,
17641
- subdir: source.subdir,
17642
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
18380
+ source: source2.raw,
18381
+ kind: source2.kind,
18382
+ ref: source2.ref,
18383
+ subdir: source2.subdir,
18384
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
18385
+ ...sha ? { sha } : {}
17643
18386
  };
17644
18387
  fs9.writeFileSync(path6.join(dest, RECEIPT_FILE), JSON.stringify(receipt, null, 2));
17645
- return { name, dir: dest, scope, inspection };
18388
+ return { name, dir: dest, scope, inspection, sha: sha ?? void 0, previousSha: opts._previousSha };
17646
18389
  } finally {
17647
18390
  fs9.rmSync(tmpBase, { recursive: true, force: true });
17648
18391
  }
@@ -17681,7 +18424,14 @@ var require_installer = __commonJS({
17681
18424
  if (receipt.kind === "local") {
17682
18425
  throw new Error(`Plugin "${name}" was installed from a local path \u2014 re-run 'nex plugin install ${receipt.source} --force'.`);
17683
18426
  }
17684
- return installPlugin2(receipt.source, { scope: s2, workDir: opts.workDir, name, force: true, confirm: opts.confirm });
18427
+ return installPlugin2(receipt.source, {
18428
+ scope: s2,
18429
+ workDir: opts.workDir,
18430
+ name,
18431
+ force: true,
18432
+ confirm: opts.confirm,
18433
+ _previousSha: receipt.sha
18434
+ });
17685
18435
  }
17686
18436
  throw new Error(`Plugin "${name}" is not installed.`);
17687
18437
  }
@@ -17720,6 +18470,7 @@ var require_dist2 = __commonJS({
17720
18470
  __exportStar(require_loop(), exports);
17721
18471
  __exportStar(require_testIntegrity(), exports);
17722
18472
  __exportStar(require_editCompleteness(), exports);
18473
+ __exportStar(require_securityLint(), exports);
17723
18474
  __exportStar(require_crossFile(), exports);
17724
18475
  __exportStar(require_flaky(), exports);
17725
18476
  __exportStar(require_claimEvidence(), exports);
@@ -17735,6 +18486,7 @@ var require_dist2 = __commonJS({
17735
18486
  __exportStar(require_destructive(), exports);
17736
18487
  __exportStar(require_plugins(), exports);
17737
18488
  __exportStar(require_installer(), exports);
18489
+ __exportStar(require_sources(), exports);
17738
18490
  }
17739
18491
  });
17740
18492
 
@@ -20550,23 +21302,23 @@ var require_react_reconciler_production = __commonJS({
20550
21302
  return "\nError generating stack: " + x2.message + "\n" + x2.stack;
20551
21303
  }
20552
21304
  }
20553
- function createCapturedValueAtFiber(value, source) {
21305
+ function createCapturedValueAtFiber(value, source2) {
20554
21306
  if ("object" === typeof value && null !== value) {
20555
21307
  var existing = CapturedStacks.get(value);
20556
21308
  if (void 0 !== existing)
20557
21309
  return existing;
20558
- source = {
21310
+ source2 = {
20559
21311
  value,
20560
- source,
20561
- stack: getStackByFiberInDevAndProd(source)
21312
+ source: source2,
21313
+ stack: getStackByFiberInDevAndProd(source2)
20562
21314
  };
20563
- CapturedStacks.set(value, source);
20564
- return source;
21315
+ CapturedStacks.set(value, source2);
21316
+ return source2;
20565
21317
  }
20566
21318
  return {
20567
21319
  value,
20568
- source,
20569
- stack: getStackByFiberInDevAndProd(source)
21320
+ source: source2,
21321
+ stack: getStackByFiberInDevAndProd(source2)
20570
21322
  };
20571
21323
  }
20572
21324
  function pushTreeFork(workInProgress2, totalChildren) {
@@ -29738,23 +30490,23 @@ var require_react_reconciler_development = __commonJS({
29738
30490
  function describeFunctionComponentFrameWithoutLineNumber(fn) {
29739
30491
  return (fn = fn ? fn.displayName || fn.name : "") ? describeBuiltInComponentFrame(fn) : "";
29740
30492
  }
29741
- function createCapturedValueAtFiber(value, source) {
30493
+ function createCapturedValueAtFiber(value, source2) {
29742
30494
  if ("object" === typeof value && null !== value) {
29743
30495
  var existing = CapturedStacks.get(value);
29744
30496
  if (void 0 !== existing)
29745
30497
  return existing;
29746
- source = {
30498
+ source2 = {
29747
30499
  value,
29748
- source,
29749
- stack: getStackByFiberInDevAndProd(source)
30500
+ source: source2,
30501
+ stack: getStackByFiberInDevAndProd(source2)
29750
30502
  };
29751
- CapturedStacks.set(value, source);
29752
- return source;
30503
+ CapturedStacks.set(value, source2);
30504
+ return source2;
29753
30505
  }
29754
30506
  return {
29755
30507
  value,
29756
- source,
29757
- stack: getStackByFiberInDevAndProd(source)
30508
+ source: source2,
30509
+ stack: getStackByFiberInDevAndProd(source2)
29758
30510
  };
29759
30511
  }
29760
30512
  function pushTreeFork(workInProgress2, totalChildren) {
@@ -42312,9 +43064,9 @@ var require_buffer_util = __commonJS({
42312
43064
  }
42313
43065
  return target;
42314
43066
  }
42315
- function _mask(source, mask, output, offset, length) {
43067
+ function _mask(source2, mask, output, offset, length) {
42316
43068
  for (let i2 = 0; i2 < length; i2++) {
42317
- output[offset + i2] = source[i2] ^ mask[i2 & 3];
43069
+ output[offset + i2] = source2[i2] ^ mask[i2 & 3];
42318
43070
  }
42319
43071
  }
42320
43072
  function _unmask(buffer, mask) {
@@ -42353,11 +43105,11 @@ var require_buffer_util = __commonJS({
42353
43105
  if (!process.env.WS_NO_BUFFER_UTIL) {
42354
43106
  try {
42355
43107
  const bufferUtil = __require("bufferutil");
42356
- module.exports.mask = function(source, mask, output, offset, length) {
43108
+ module.exports.mask = function(source2, mask, output, offset, length) {
42357
43109
  if (length < 48)
42358
- _mask(source, mask, output, offset, length);
43110
+ _mask(source2, mask, output, offset, length);
42359
43111
  else
42360
- bufferUtil.mask(source, mask, output, offset, length);
43112
+ bufferUtil.mask(source2, mask, output, offset, length);
42361
43113
  };
42362
43114
  module.exports.unmask = function(buffer, mask) {
42363
43115
  if (buffer.length < 32)
@@ -49074,16 +49826,16 @@ var require_dist3 = __commonJS({
49074
49826
  }
49075
49827
  function _objectSpread(target) {
49076
49828
  for (var i2 = 1; i2 < arguments.length; i2++) {
49077
- var source = arguments[i2] != null ? arguments[i2] : {};
49829
+ var source2 = arguments[i2] != null ? arguments[i2] : {};
49078
49830
  if (i2 % 2) {
49079
- ownKeys(Object(source), true).forEach(function(key) {
49080
- _defineProperty(target, key, source[key]);
49831
+ ownKeys(Object(source2), true).forEach(function(key) {
49832
+ _defineProperty(target, key, source2[key]);
49081
49833
  });
49082
49834
  } else if (Object.getOwnPropertyDescriptors) {
49083
- Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
49835
+ Object.defineProperties(target, Object.getOwnPropertyDescriptors(source2));
49084
49836
  } else {
49085
- ownKeys(Object(source)).forEach(function(key) {
49086
- Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
49837
+ ownKeys(Object(source2)).forEach(function(key) {
49838
+ Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source2, key));
49087
49839
  });
49088
49840
  }
49089
49841
  }
@@ -53803,6 +54555,16 @@ function trustGrantedByEnv() {
53803
54555
  const v = process.env[TRUST_ENV_VAR];
53804
54556
  return v === "1" || v === "true";
53805
54557
  }
54558
+ function declaresPluginSources(dir) {
54559
+ try {
54560
+ const raw = fs5.readFileSync(path3.join(dir, ".nexrall", "settings.json"), "utf-8");
54561
+ const obj = JSON.parse(raw);
54562
+ const s2 = obj.pluginSources;
54563
+ return Boolean(s2 && typeof s2 === "object" && !Array.isArray(s2) && Object.keys(s2).length > 0);
54564
+ } catch {
54565
+ return false;
54566
+ }
54567
+ }
53806
54568
  function detectTrustSignals(dir) {
53807
54569
  const signals = [];
53808
54570
  const exists = (...p) => {
@@ -53822,7 +54584,9 @@ function detectTrustSignals(dir) {
53822
54584
  signals.push(".nexrall/permissions.json \u2014 pre-approved tool permissions");
53823
54585
  }
53824
54586
  if (exists(".nexrall", "settings.json")) {
53825
- signals.push(".nexrall/settings.json \u2014 project settings and permission rules");
54587
+ signals.push(
54588
+ declaresPluginSources(dir) ? ".nexrall/settings.json \u2014 project settings, and declares plugin sources (aliases for remote plugin repos)" : ".nexrall/settings.json \u2014 project settings and permission rules"
54589
+ );
53826
54590
  }
53827
54591
  if (exists("nexrall.md")) {
53828
54592
  signals.push("nexrall.md \u2014 project instructions added to the agent\u2019s prompt");
@@ -60331,15 +61095,15 @@ var generateLineNumbers = (line, around) => {
60331
61095
  }
60332
61096
  return lineNumbers;
60333
61097
  };
60334
- var codeExcerpt = (source, line, options = {}) => {
61098
+ var codeExcerpt = (source2, line, options = {}) => {
60335
61099
  var _a;
60336
- if (typeof source !== "string") {
61100
+ if (typeof source2 !== "string") {
60337
61101
  throw new TypeError("Source code is missing.");
60338
61102
  }
60339
61103
  if (!line || line < 1) {
60340
61104
  throw new TypeError("Line number must start from `1`.");
60341
61105
  }
60342
- const lines = dist_default3(source).split(/\r?\n/);
61106
+ const lines = dist_default3(source2).split(/\r?\n/);
60343
61107
  if (line > lines.length) {
60344
61108
  return;
60345
61109
  }
@@ -62794,7 +63558,7 @@ var InkReadlineAdapter = class extends EventEmitter3 {
62794
63558
  };
62795
63559
 
62796
63560
  // src/commands/chat.ts
62797
- var CLI_VERSION = "0.5.48";
63561
+ var CLI_VERSION = "0.5.50";
62798
63562
  var MODEL_LABELS = {
62799
63563
  turbo: "Nexrall Turbo",
62800
63564
  pro: "Nexrall Pro",
@@ -63012,15 +63776,23 @@ function formatContextUsage(messages, modelAlias) {
63012
63776
  return lines.join("\n");
63013
63777
  }
63014
63778
  function formatAgentsList(workDir) {
63015
- const types3 = (0, import_code_core3.loadAgentTypes)(workDir);
63779
+ const { types: types3, warnings } = (0, import_code_core3.loadAgentTypesWithWarnings)(workDir);
63016
63780
  if (!types3.length)
63017
63781
  return source_default.dim(" No agent types found (this should not happen \u2014 builtins always register).");
63018
63782
  const lines = types3.map((t2) => {
63019
- const tools = t2.tools ? source_default.dim(` \u2014 tools: ${t2.tools.join(", ")}`) : source_default.dim(" \u2014 all tools");
63783
+ const tools = t2.tools ? source_default.dim(` \u2014 tools: ${t2.tools.join(", ")}`) : source_default.dim(" \u2014 ALL tools (no allowlist)");
63020
63784
  const model = t2.model ? source_default.dim(` \u2014 model: ${t2.model}`) : "";
63021
63785
  const scope = source_default.dim(` (${t2.source})`);
63022
63786
  return " " + source_default.cyan(t2.name.padEnd(16)) + source_default.white(t2.description) + scope + tools + model;
63023
63787
  });
63788
+ if (warnings.length) {
63789
+ lines.push("");
63790
+ lines.push(" " + source_default.yellow.bold(`\u26A0 ${warnings.length} problem${warnings.length > 1 ? "s" : ""} in your agent definitions:`));
63791
+ for (const w of warnings) {
63792
+ lines.push(" " + source_default.yellow(`\u2022 ${w.agent}`) + source_default.dim(` \u2014 ${w.message}`));
63793
+ lines.push(" " + source_default.dim(w.file));
63794
+ }
63795
+ }
63024
63796
  return lines.join("\n");
63025
63797
  }
63026
63798
  async function runTurn(messages, modelAlias, workDir, abortSignal, env3, nexrallMd, mode, effort, checkpointManager, onProgress) {
@@ -64137,11 +64909,23 @@ ${caption}` : `[Attached: ${rel}]`;
64137
64909
  // src/commands/plugin.ts
64138
64910
  var import_prompts = __toESM(require_prompts3(), 1);
64139
64911
  var import_code_core4 = __toESM(require_dist2(), 1);
64140
- function describeInspection(i2, name) {
64912
+ function describeInspection(i2, name, ctx) {
64141
64913
  console.log();
64142
64914
  console.log(source_default.bold(` Plugin: ${name}`) + (i2.meta.version ? source_default.dim(` v${i2.meta.version}`) : ""));
64143
64915
  if (i2.meta.description)
64144
64916
  console.log(source_default.dim(` ${i2.meta.description}`));
64917
+ if (ctx?.sha) {
64918
+ const short = (s2) => s2.slice(0, 7);
64919
+ if (ctx.previousSha && ctx.previousSha !== ctx.sha) {
64920
+ console.log(source_default.dim(" commit: ") + source_default.yellow(`${short(ctx.previousSha)} \u2192 ${short(ctx.sha)}`));
64921
+ } else if (ctx.previousSha === ctx.sha) {
64922
+ console.log(source_default.dim(` commit: ${short(ctx.sha)} (already up to date)`));
64923
+ } else {
64924
+ console.log(source_default.dim(` commit: ${short(ctx.sha)}`));
64925
+ }
64926
+ } else if (ctx?.isUpdate) {
64927
+ console.log(source_default.dim(" commit: unknown (could not reach the GitHub API)"));
64928
+ }
64145
64929
  if (i2.commands.length)
64146
64930
  console.log(` ${source_default.cyan("commands:")} ${i2.commands.map((c) => "/" + c).join(", ")}`);
64147
64931
  if (i2.agents.length)
@@ -64151,10 +64935,16 @@ function describeInspection(i2, name) {
64151
64935
  if (i2.hasMcp)
64152
64936
  console.log(` ${source_default.yellow("mcp:")} yes`);
64153
64937
  }
64154
- async function confirmInstall(i2, name, autoYes) {
64155
- describeInspection(i2, name);
64938
+ async function confirmInstall(i2, name, autoYes, ctx) {
64939
+ describeInspection(i2, name, ctx);
64156
64940
  const dangerous = i2.hasHooks || i2.hasMcp;
64157
64941
  if (dangerous) {
64942
+ if (ctx?.isUpdate) {
64943
+ console.log();
64944
+ console.log(source_default.red.bold(" \u26A0 THIS UPDATE SHIPS CODE EXECUTION"));
64945
+ console.log(source_default.red(" A version of this plugin you already trust is being replaced by one"));
64946
+ console.log(source_default.red(" that runs commands. Review the diff before continuing."));
64947
+ }
64158
64948
  console.log();
64159
64949
  console.log(source_default.red.bold(" \u26A0 SECURITY WARNING"));
64160
64950
  if (i2.hasHooks) {
@@ -64172,6 +64962,12 @@ async function confirmInstall(i2, name, autoYes) {
64172
64962
  if (autoYes && dangerous) {
64173
64963
  console.log(source_default.yellow("\n --yes does not apply to plugins with hooks/MCP; confirmation required."));
64174
64964
  }
64965
+ if (!process.stdin.isTTY) {
64966
+ console.log();
64967
+ console.log(source_default.red(" \u2717 Cannot ask for confirmation: stdin is not a terminal."));
64968
+ 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."));
64969
+ return false;
64970
+ }
64175
64971
  console.log();
64176
64972
  const { ok } = await (0, import_prompts.default)({
64177
64973
  type: "confirm",
@@ -64184,35 +64980,43 @@ async function confirmInstall(i2, name, autoYes) {
64184
64980
  async function pluginInstallCommand(spec, opts) {
64185
64981
  const scope = opts.project ? "project" : "global";
64186
64982
  try {
64187
- let source = spec;
64983
+ let source2 = spec;
64188
64984
  let registryName;
64189
64985
  const isShortName = /^[\w.-]+$/.test(spec.trim()) && !spec.includes("/");
64190
64986
  if (isShortName) {
64987
+ const alias = (0, import_code_core4.resolveSourceAlias)(spec.trim(), process.cwd(), { includeProject: true });
64988
+ if (alias) {
64989
+ source2 = alias.spec;
64990
+ console.log(source_default.dim(` Source: ${alias.name} (${alias.tier}) \u2190 ${alias.spec}`));
64991
+ }
64992
+ }
64993
+ if (isShortName && source2 === spec) {
64191
64994
  const entry = await (0, import_code_core4.getRegistryPlugin)(spec.trim()).catch(() => null);
64192
64995
  if (entry) {
64193
- source = entry.source;
64996
+ source2 = entry.source;
64194
64997
  registryName = entry.name;
64195
64998
  const badge = entry.official ? source_default.magenta(" [official]") : entry.verified ? source_default.green(" [verified]") : "";
64196
64999
  console.log(source_default.dim(` Registry: ${entry.display_name}${badge} \u2190 ${entry.source}`));
64197
65000
  } else {
64198
- console.error(source_default.red(` \u2717 "${spec}" not found in the registry.`));
64199
- console.error(source_default.dim(" Search: nex plugin search <keyword>"));
64200
- console.error(source_default.dim(" Or install from GitHub directly: nex plugin install owner/repo"));
65001
+ console.error(source_default.red(` \u2717 "${spec}" is not a declared source or a registry plugin.`));
65002
+ console.error(source_default.dim(" Search: nex plugin search <keyword>"));
65003
+ console.error(source_default.dim(" From GitHub: nex plugin install owner/repo"));
65004
+ console.error(source_default.dim(" Declare it: nex plugin source add " + spec.trim() + " owner/repo"));
64201
65005
  process.exitCode = 1;
64202
65006
  return;
64203
65007
  }
64204
65008
  }
64205
- const res = await (0, import_code_core4.installPlugin)(source, {
65009
+ const res = await (0, import_code_core4.installPlugin)(source2, {
64206
65010
  scope,
64207
65011
  workDir: process.cwd(),
64208
65012
  name: opts.name ?? registryName,
64209
65013
  force: opts.force,
64210
- confirm: (i2, name) => confirmInstall(i2, name, opts.yes === true)
65014
+ confirm: (i2, name, ctx) => confirmInstall(i2, name, opts.yes === true, ctx)
64211
65015
  });
64212
65016
  if (registryName)
64213
65017
  (0, import_code_core4.reportInstall)(registryName);
64214
65018
  console.log();
64215
- console.log(source_default.green(` \u2713 Installed "${res.name}" (${res.scope}) \u2192 ${res.dir}`));
65019
+ console.log(source_default.green(` \u2713 Installed "${res.name}" (${res.scope}) \u2192 ${res.dir}`) + (res.sha ? source_default.dim(` @ ${res.sha.slice(0, 7)}`) : ""));
64216
65020
  if (res.inspection.commands.length) {
64217
65021
  console.log(source_default.dim(` Try: ${res.inspection.commands.map((c) => "/" + c).join(", ")}`));
64218
65022
  }
@@ -64246,9 +65050,10 @@ async function pluginUpdateCommand(name, opts) {
64246
65050
  try {
64247
65051
  const res = await (0, import_code_core4.updatePlugin)(t2, {
64248
65052
  workDir,
64249
- confirm: (i2, n) => confirmInstall(i2, n, opts.yes === true)
65053
+ confirm: (i2, n, ctx) => confirmInstall(i2, n, opts.yes === true, ctx)
64250
65054
  });
64251
- console.log(source_default.green(` \u2713 Updated "${res.name}"`));
65055
+ const moved = res.sha && res.previousSha && res.sha !== res.previousSha ? source_default.dim(` ${res.previousSha.slice(0, 7)} \u2192 ${res.sha.slice(0, 7)}`) : res.sha && res.previousSha === res.sha ? source_default.dim(" (unchanged)") : "";
65056
+ console.log(source_default.green(` \u2713 Updated "${res.name}"`) + moved);
64252
65057
  } catch (err) {
64253
65058
  console.error(source_default.red(` \u2717 ${t2}: ${err.message}`));
64254
65059
  process.exitCode = 1;
@@ -64330,24 +65135,98 @@ function pluginListCommand() {
64330
65135
  for (const p of plugins) {
64331
65136
  const receipt = (0, import_code_core4.readReceipt)(p.dir);
64332
65137
  const src = receipt ? source_default.dim(` \u2190 ${receipt.source}`) : source_default.dim(" (manual)");
65138
+ const at = receipt?.sha ? source_default.dim(` @ ${receipt.sha.slice(0, 7)}`) : "";
65139
+ const insp = (0, import_code_core4.inspectPluginDir)(p.dir);
65140
+ const runs = insp.hasHooks || insp.hasMcp ? source_default.yellow(` \u26A0 ${[insp.hasHooks && "hooks", insp.hasMcp && "mcp"].filter(Boolean).join("+")}`) : "";
64333
65141
  console.log(
64334
- " " + source_default.cyan(p.name.padEnd(24)) + source_default.dim(`${(p.version ?? "").padEnd(10)}${p.scope.padEnd(9)}`) + src
65142
+ " " + source_default.cyan(p.name.padEnd(24)) + source_default.dim(`${(p.version ?? "").padEnd(10)}${p.scope.padEnd(9)}`) + src + at + runs
64335
65143
  );
64336
65144
  }
64337
65145
  console.log();
64338
65146
  }
65147
+ function pluginSourceListCommand() {
65148
+ const workDir = process.cwd();
65149
+ const sources = (0, import_code_core4.loadDeclaredSources)(workDir, { includeProject: true });
65150
+ const patterns = (0, import_code_core4.allowedSourcePatterns)();
65151
+ if (patterns) {
65152
+ console.log();
65153
+ console.log(source_default.bold(" Organisation policy: ") + source_default.dim(`installs restricted to ${patterns.join(", ")}`));
65154
+ }
65155
+ if (!sources.length) {
65156
+ console.log(source_default.dim("\n No plugin sources declared."));
65157
+ console.log(source_default.dim(" Add one: nex plugin source add <alias> owner/repo"));
65158
+ console.log();
65159
+ return;
65160
+ }
65161
+ console.log();
65162
+ console.log(source_default.bold(" Declared plugin sources:"));
65163
+ for (const s2 of sources) {
65164
+ const tier = s2.tier === "project" ? source_default.yellow(s2.tier.padEnd(9)) : source_default.dim(s2.tier.padEnd(9));
65165
+ const blocked = !(0, import_code_core4.isSourceAllowed)(s2.spec, patterns) ? source_default.red(" \u2717 blocked by policy") : "";
65166
+ console.log(" " + source_default.cyan(s2.name.padEnd(20)) + tier + source_default.white(s2.spec) + blocked);
65167
+ }
65168
+ console.log();
65169
+ console.log(source_default.dim(" Install from one: nex plugin install <alias>"));
65170
+ console.log(source_default.dim(" Declaring a source does not install anything."));
65171
+ console.log();
65172
+ }
65173
+ function pluginSourceAddCommand(name, spec, opts) {
65174
+ try {
65175
+ (0, import_code_core4.parsePluginSource)(spec);
65176
+ const patterns = (0, import_code_core4.allowedSourcePatterns)();
65177
+ if (patterns && !(0, import_code_core4.isSourceAllowed)(spec, patterns)) {
65178
+ console.error(source_default.red(` \u2717 "${spec}" is not permitted by your organisation's policy.`));
65179
+ console.error(source_default.dim(` Allowed: ${patterns.join(", ")}`));
65180
+ process.exitCode = 1;
65181
+ return;
65182
+ }
65183
+ const scope = opts.project ? "project" : "user";
65184
+ const file = (0, import_code_core4.sourcesFileFor)(scope, process.cwd());
65185
+ (0, import_code_core4.writeDeclaredSource)(file, name, spec);
65186
+ console.log();
65187
+ console.log(source_default.green(` \u2713 Declared "${name}" \u2192 ${spec}`) + source_default.dim(` (${scope})`));
65188
+ console.log(source_default.dim(` ${file}`));
65189
+ console.log(source_default.dim(` Nothing installed yet \u2014 run: nex plugin install ${name}`));
65190
+ console.log();
65191
+ } catch (err) {
65192
+ console.error(source_default.red(` \u2717 ${err.message}`));
65193
+ process.exitCode = 1;
65194
+ }
65195
+ }
65196
+ function pluginSourceRemoveCommand(name, opts) {
65197
+ const workDir = process.cwd();
65198
+ const scope = opts.project ? "project" : "user";
65199
+ const file = (0, import_code_core4.sourcesFileFor)(scope, workDir);
65200
+ if ((0, import_code_core4.removeDeclaredSource)(file, name)) {
65201
+ console.log(source_default.green(` \u2713 Removed source "${name}" (${scope})`));
65202
+ console.log(source_default.dim(" Plugins already installed from it are untouched \u2014 remove them with nex plugin remove <name>"));
65203
+ return;
65204
+ }
65205
+ const found = (0, import_code_core4.resolveSourceAlias)(name, workDir, { includeProject: true });
65206
+ if (found) {
65207
+ console.error(source_default.red(` \u2717 "${name}" is declared in the ${found.tier} tier, not ${scope}.`));
65208
+ console.error(source_default.dim(` ${found.file}`));
65209
+ if (found.tier === "project")
65210
+ console.error(source_default.dim(" Retry with --project"));
65211
+ if (found.tier === "managed")
65212
+ console.error(source_default.dim(" Managed sources are set by your organisation and cannot be removed here."));
65213
+ } else {
65214
+ console.error(source_default.red(` \u2717 No source named "${name}".`));
65215
+ }
65216
+ process.exitCode = 1;
65217
+ }
64339
65218
 
64340
65219
  // src/index.ts
64341
65220
  var program2 = new Command();
64342
- program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.48");
65221
+ program2.name("nex").description("Nexrall Code \u2014 AI coding assistant (powered by Nexrall)").version("0.5.50").enablePositionalOptions();
64343
65222
  program2.command("auth").description("Login to your Nexrall account").action(authCommand);
64344
65223
  program2.command("logout").description("Log out of your Nexrall account").action(logoutCommand);
64345
65224
  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) => {
64346
65225
  await updateCommand({ check: opts.check, yes: opts.yes });
64347
65226
  });
64348
65227
  var plugin = program2.command("plugin").description("Manage plugins (install from GitHub, list, update, remove)");
64349
- plugin.command("install <source>").description("Install a plugin: registry name, owner/repo(#ref)(/subdir), GitHub URL, or local path").option("--project", "Install into ./.nexrall/plugins (default: ~/.nexrall/plugins)").option("--name <name>", "Override the installed plugin name").option("-f, --force", "Overwrite if already installed").option("-y, --yes", "Skip confirmation (still prompts for plugins with hooks/MCP)").action(async (source, opts) => {
64350
- await pluginInstallCommand(source, opts);
65228
+ plugin.command("install <source>").description("Install a plugin: registry name, owner/repo(#ref)(/subdir), GitHub URL, or local path").option("--project", "Install into ./.nexrall/plugins (default: ~/.nexrall/plugins)").option("--name <name>", "Override the installed plugin name").option("-f, --force", "Overwrite if already installed").option("-y, --yes", "Skip confirmation (still prompts for plugins with hooks/MCP)").action(async (source2, opts) => {
65229
+ await pluginInstallCommand(source2, opts);
64351
65230
  });
64352
65231
  plugin.command("remove <name>").alias("uninstall").description("Remove an installed plugin").option("--project", "Only look in ./.nexrall/plugins").action((name, opts) => {
64353
65232
  pluginRemoveCommand(name, opts);
@@ -64364,6 +65243,16 @@ plugin.command("search [query]").description("Search the Nexrall plugin registry
64364
65243
  plugin.command("info <name>").description("Show registry details for a plugin").action(async (name) => {
64365
65244
  await pluginInfoCommand(name);
64366
65245
  });
65246
+ var source = plugin.command("source").description("Declare where plugins may be installed from (aliases for plugin repos)");
65247
+ source.command("list").alias("ls").description("List declared plugin sources").action(() => {
65248
+ pluginSourceListCommand();
65249
+ });
65250
+ source.command("add <name> <spec>").description("Declare a source: nex plugin source add acme acme/plugins").option("--project", "Write to ./.nexrall/settings.json so the team shares it (default: ~/.nexrall/settings.json)").action((name, spec, opts) => {
65251
+ pluginSourceAddCommand(name, spec, opts);
65252
+ });
65253
+ source.command("remove <name>").alias("rm").description("Remove a declared source (installed plugins are untouched)").option("--project", "Remove from ./.nexrall/settings.json").action((name, opts) => {
65254
+ pluginSourceRemoveCommand(name, opts);
65255
+ });
64367
65256
  program2.command("sessions").description("List or delete saved chat sessions").option("-d, --delete <id>", "Delete a session by id").option("--clear-all", "Delete ALL saved sessions").action((opts) => {
64368
65257
  if (opts.clearAll) {
64369
65258
  const sessions2 = listSessions();