@nexrall/code-core 1.4.76 → 1.4.78

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 (143) hide show
  1. package/dist/agent/effortCap.d.ts +24 -0
  2. package/dist/agent/effortCap.d.ts.map +1 -0
  3. package/dist/agent/effortCap.js +66 -0
  4. package/dist/agent/goal.d.ts +55 -0
  5. package/dist/agent/goal.d.ts.map +1 -0
  6. package/dist/agent/goal.js +120 -0
  7. package/dist/agent/handoffBrief.d.ts +82 -0
  8. package/dist/agent/handoffBrief.d.ts.map +1 -0
  9. package/dist/agent/handoffBrief.js +121 -0
  10. package/dist/agent/hooks.d.ts +10 -2
  11. package/dist/agent/hooks.d.ts.map +1 -1
  12. package/dist/agent/hooks.js +9 -3
  13. package/dist/agent/modelCatalogue.d.ts +23 -0
  14. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  15. package/dist/agent/modelCatalogue.js +28 -0
  16. package/dist/agent/outputStyles.d.ts +27 -0
  17. package/dist/agent/outputStyles.d.ts.map +1 -0
  18. package/dist/agent/outputStyles.js +174 -0
  19. package/dist/agent/skills.d.ts.map +1 -1
  20. package/dist/agent/skills.js +21 -1
  21. package/dist/api/billingLines.d.ts +11 -0
  22. package/dist/api/billingLines.d.ts.map +1 -0
  23. package/dist/api/billingLines.js +56 -0
  24. package/dist/api/client.d.ts +2 -0
  25. package/dist/api/client.d.ts.map +1 -1
  26. package/dist/api/scheduleLines.d.ts +53 -0
  27. package/dist/api/scheduleLines.d.ts.map +1 -0
  28. package/dist/api/scheduleLines.js +150 -0
  29. package/dist/context/contextFiles.d.ts +81 -0
  30. package/dist/context/contextFiles.d.ts.map +1 -0
  31. package/dist/context/contextFiles.js +310 -0
  32. package/dist/diff/collect.d.ts +25 -0
  33. package/dist/diff/collect.d.ts.map +1 -0
  34. package/dist/diff/collect.js +169 -0
  35. package/dist/diff/diffView.d.ts +57 -0
  36. package/dist/diff/diffView.d.ts.map +1 -0
  37. package/dist/diff/diffView.js +231 -0
  38. package/dist/diff/reviewQueue.d.ts +53 -0
  39. package/dist/diff/reviewQueue.d.ts.map +1 -0
  40. package/dist/diff/reviewQueue.js +349 -0
  41. package/dist/events/recap.d.ts +54 -0
  42. package/dist/events/recap.d.ts.map +1 -0
  43. package/dist/events/recap.js +161 -0
  44. package/dist/events/sessionEvents.d.ts +54 -0
  45. package/dist/events/sessionEvents.d.ts.map +1 -0
  46. package/dist/events/sessionEvents.js +324 -0
  47. package/dist/events/sessionStats.d.ts +89 -0
  48. package/dist/events/sessionStats.d.ts.map +1 -0
  49. package/dist/events/sessionStats.js +264 -0
  50. package/dist/import/claudeImport.d.ts +44 -0
  51. package/dist/import/claudeImport.d.ts.map +1 -0
  52. package/dist/import/claudeImport.js +337 -0
  53. package/dist/index.d.ts +38 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +38 -0
  56. package/dist/mcp/manager.d.ts +8 -0
  57. package/dist/mcp/manager.d.ts.map +1 -1
  58. package/dist/mcp/manager.js +64 -57
  59. package/dist/permissions/destructiveTokens.d.ts +1 -0
  60. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  61. package/dist/permissions/destructiveTokens.js +93 -0
  62. package/dist/permissions/rules.d.ts +12 -0
  63. package/dist/permissions/rules.d.ts.map +1 -1
  64. package/dist/permissions/rules.js +89 -7
  65. package/dist/plugins/configStore.d.ts +43 -0
  66. package/dist/plugins/configStore.d.ts.map +1 -0
  67. package/dist/plugins/configStore.js +253 -0
  68. package/dist/plugins/files.d.ts +10 -0
  69. package/dist/plugins/files.d.ts.map +1 -0
  70. package/dist/plugins/files.js +85 -0
  71. package/dist/plugins/index.d.ts +3 -22
  72. package/dist/plugins/index.d.ts.map +1 -1
  73. package/dist/plugins/index.js +30 -40
  74. package/dist/plugins/resolve.d.ts +18 -0
  75. package/dist/plugins/resolve.d.ts.map +1 -0
  76. package/dist/plugins/resolve.js +152 -0
  77. package/dist/plugins/userConfig.d.ts +90 -0
  78. package/dist/plugins/userConfig.d.ts.map +1 -0
  79. package/dist/plugins/userConfig.js +332 -0
  80. package/dist/plugins/validate.d.ts +14 -0
  81. package/dist/plugins/validate.d.ts.map +1 -0
  82. package/dist/plugins/validate.js +222 -0
  83. package/dist/project/projectCommands.d.ts +33 -0
  84. package/dist/project/projectCommands.d.ts.map +1 -0
  85. package/dist/project/projectCommands.js +246 -0
  86. package/dist/search/conversationTurns.d.ts +53 -0
  87. package/dist/search/conversationTurns.d.ts.map +1 -0
  88. package/dist/search/conversationTurns.js +170 -0
  89. package/dist/search/globalSearch.d.ts +144 -0
  90. package/dist/search/globalSearch.d.ts.map +1 -0
  91. package/dist/search/globalSearch.js +350 -0
  92. package/dist/share/sessionChannel.d.ts +136 -0
  93. package/dist/share/sessionChannel.d.ts.map +1 -0
  94. package/dist/share/sessionChannel.js +373 -0
  95. package/dist/share/sessionShare.d.ts +104 -0
  96. package/dist/share/sessionShare.d.ts.map +1 -0
  97. package/dist/share/sessionShare.js +336 -0
  98. package/dist/ui/configCmd.d.ts +48 -0
  99. package/dist/ui/configCmd.d.ts.map +1 -0
  100. package/dist/ui/configCmd.js +146 -0
  101. package/dist/ui/deployPanel.d.ts +91 -0
  102. package/dist/ui/deployPanel.d.ts.map +1 -0
  103. package/dist/ui/deployPanel.js +442 -0
  104. package/dist/ui/mergeAssistant.d.ts +120 -0
  105. package/dist/ui/mergeAssistant.d.ts.map +1 -0
  106. package/dist/ui/mergeAssistant.js +391 -0
  107. package/dist/ui/notify.d.ts +114 -0
  108. package/dist/ui/notify.d.ts.map +1 -0
  109. package/dist/ui/notify.js +235 -0
  110. package/dist/ui/promptSuggest.d.ts +56 -0
  111. package/dist/ui/promptSuggest.d.ts.map +1 -0
  112. package/dist/ui/promptSuggest.js +160 -0
  113. package/dist/ui/taskGraph.d.ts +44 -0
  114. package/dist/ui/taskGraph.d.ts.map +1 -0
  115. package/dist/ui/taskGraph.js +289 -0
  116. package/dist/util/addDir.d.ts +31 -0
  117. package/dist/util/addDir.d.ts.map +1 -0
  118. package/dist/util/addDir.js +124 -0
  119. package/dist/util/batch.d.ts +16 -0
  120. package/dist/util/batch.d.ts.map +1 -0
  121. package/dist/util/batch.js +92 -0
  122. package/dist/util/cellWidth.d.ts +12 -0
  123. package/dist/util/cellWidth.d.ts.map +1 -0
  124. package/dist/util/cellWidth.js +161 -0
  125. package/dist/util/duration.d.ts +8 -0
  126. package/dist/util/duration.d.ts.map +1 -0
  127. package/dist/util/duration.js +50 -0
  128. package/dist/util/loopJobs.d.ts +67 -0
  129. package/dist/util/loopJobs.d.ts.map +1 -0
  130. package/dist/util/loopJobs.js +169 -0
  131. package/dist/util/settingsFile.d.ts +14 -0
  132. package/dist/util/settingsFile.d.ts.map +1 -0
  133. package/dist/util/settingsFile.js +99 -0
  134. package/dist/util/toolLabel.d.ts +2 -0
  135. package/dist/util/toolLabel.d.ts.map +1 -0
  136. package/dist/util/toolLabel.js +17 -0
  137. package/dist/verify/failureTimeline.d.ts +100 -0
  138. package/dist/verify/failureTimeline.d.ts.map +1 -0
  139. package/dist/verify/failureTimeline.js +253 -0
  140. package/dist/verify/verification.d.ts +75 -0
  141. package/dist/verify/verification.d.ts.map +1 -0
  142. package/dist/verify/verification.js +315 -0
  143. package/package.json +5 -4
@@ -41,6 +41,7 @@ const client_1 = require("./client");
41
41
  const httpClient_1 = require("./httpClient");
42
42
  const sseClient_1 = require("./sseClient");
43
43
  const index_1 = require("../plugins/index");
44
+ const resolve_1 = require("../plugins/resolve");
44
45
  const stats_1 = require("./stats");
45
46
  // ─── McpManager ───────────────────────────────────────────────────────────────
46
47
  class McpManager {
@@ -119,7 +120,8 @@ class McpManager {
119
120
  // Plugins first, then user, then project — later assignments override, so
120
121
  // a user/project server definition beats a plugin's same-name one.
121
122
  try {
122
- Object.assign(merged.mcpServers, (0, index_1.pluginMcpServers)(workDir));
123
+ // resolvePluginMcpServers = pluginMcpServers + `${user_config.KEY}` substitution (S24).
124
+ Object.assign(merged.mcpServers, (0, resolve_1.resolvePluginMcpServers)(workDir));
123
125
  }
124
126
  catch {
125
127
  /* plugin scan is best-effort */
@@ -196,23 +198,6 @@ class McpManager {
196
198
  }
197
199
  return tools;
198
200
  }
199
- /**
200
- * Execute an MCP tool call. Returns null if this tool is not an MCP tool.
201
- *
202
- * `abortSignal` is optional and best-effort: the underlying transports (stdio
203
- * JSON-RPC / HTTP / SSE) have no cancellation message in this client, so an
204
- * abort here does NOT stop the server from finishing its work — it only stops
205
- * THIS CALL from making the caller wait for it. Before this was added, Ctrl+C
206
- * during an MCP tool call was silently swallowed: loop.ts set
207
- * `abortSignal.aborted = true` but nothing downstream of `options.mcpManager
208
- * .callTool(name, input)` ever looked at it, so the user's only way out was
209
- * to wait out the full 60s MCP_TOOL_TIMEOUT_MS (or however long the server
210
- * actually took). Racing the same abortSignal the 60s timeout already races
211
- * against makes Ctrl+C return control immediately instead of up to a minute
212
- * later — matching how the `bash` tool already behaves (its own 200ms abort
213
- * poll in executor.ts), just without the ability to also kill a remote
214
- * server's in-flight work the way `bash` can kill a local child process.
215
- */
216
201
  async callTool(prefixedName, input, abortSignal) {
217
202
  const serverName = this._toolMap.get(prefixedName);
218
203
  if (!serverName)
@@ -222,48 +207,70 @@ class McpManager {
222
207
  return null;
223
208
  // Strip the server prefix to get the real tool name
224
209
  const toolName = prefixedName.slice(serverName.length + 2);
225
- // Enforce a timeout — a hung MCP server would otherwise block the agent loop forever.
210
+ // One attempt's full race — a fresh timeout, abort poll and stat pairing per try,
211
+ // because a promise cannot be awaited twice and S25e's retry below builds new ones.
226
212
  const MCP_TOOL_TIMEOUT_MS = 60000;
227
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${prefixedName}" timed out after ${MCP_TOOL_TIMEOUT_MS / 1000}s`)), MCP_TOOL_TIMEOUT_MS));
228
- // Timing covers the whole path the user waits on (server round-trip AND the
229
- // race against the 60s timeout / abort), which is what "how long did that take"
230
- // means from the outside. The tool name recorded is the PREFIXED one, so the
231
- // stats of two servers with a same-named tool never merge.
232
- const startedAt = Date.now();
233
- this.stats.begin();
234
- const timed = (p) => p.then((v) => {
235
- this.stats.ok(prefixedName, Date.now() - startedAt);
236
- return v;
237
- }, (e) => {
238
- this.stats.fail(prefixedName, Date.now() - startedAt, e.message || String(e));
239
- throw e;
240
- });
241
- // A 401 mid-CALL is different from one at connect: the server is already up and
242
- // some tools work — record it so /mcp flips to needs-auth (with the missing
243
- // scope) and let the enriched message reach the caller (the model sees WHY).
244
- const trackAuth = (p) => p.catch((err) => {
245
- if (err instanceof httpClient_1.McpOAuthRequiredError)
246
- this._needsAuth.set(serverName, err.wwwAuthenticate);
247
- throw err;
248
- });
249
- const racers = [timed(client.callTool(toolName, input)), timed(timeoutPromise)];
250
- if (abortSignal) {
251
- let pollId;
252
- const abortPromise = new Promise((_, reject) => {
253
- pollId = setInterval(() => {
254
- if (abortSignal.aborted) {
255
- clearInterval(pollId);
256
- reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
257
- }
258
- }, 100);
213
+ const once = () => {
214
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${prefixedName}" timed out after ${MCP_TOOL_TIMEOUT_MS / 1000}s`)), MCP_TOOL_TIMEOUT_MS));
215
+ // Timing covers the whole path the user waits on (server round-trip AND the
216
+ // race against the 60s timeout / abort), which is what "how long did that take"
217
+ // means from the outside. The tool name recorded is the PREFIXED one, so the
218
+ // stats of two servers with a same-named tool never merge.
219
+ const startedAt = Date.now();
220
+ this.stats.begin();
221
+ const timed = (p) => p.then((v) => {
222
+ this.stats.ok(prefixedName, Date.now() - startedAt);
223
+ return v;
224
+ }, (e) => {
225
+ this.stats.fail(prefixedName, Date.now() - startedAt, e.message || String(e));
226
+ throw e;
227
+ });
228
+ // A 401 mid-CALL is different from one at connect: the server is already up and
229
+ // some tools work — record it so /mcp flips to needs-auth (with the missing
230
+ // scope) and let the enriched message reach the caller (the model sees WHY).
231
+ const trackAuth = (p) => p.catch((err) => {
232
+ if (err instanceof httpClient_1.McpOAuthRequiredError)
233
+ this._needsAuth.set(serverName, err.wwwAuthenticate);
234
+ throw err;
259
235
  });
260
- racers.push(timed(abortPromise));
261
- // Clearing the poll on whichever branch wins avoids leaving a live interval
262
- // (and the closure it holds) running for the rest of the process lifetime —
263
- // the same leak-prevention pattern client.ts uses for its own abort poll.
264
- return trackAuth(Promise.race(racers).finally(() => clearInterval(pollId)));
236
+ const racers = [timed(client.callTool(toolName, input)), timed(timeoutPromise)];
237
+ if (abortSignal) {
238
+ let pollId;
239
+ const abortPromise = new Promise((_, reject) => {
240
+ pollId = setInterval(() => {
241
+ if (abortSignal.aborted) {
242
+ clearInterval(pollId);
243
+ reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
244
+ }
245
+ }, 100);
246
+ });
247
+ racers.push(timed(abortPromise));
248
+ // Clearing the poll on whichever branch wins avoids leaving a live interval
249
+ // (and the closure it holds) running for the rest of the process lifetime —
250
+ // the same leak-prevention pattern client.ts uses for its own abort poll.
251
+ return trackAuth(Promise.race(racers).finally(() => clearInterval(pollId)));
252
+ }
253
+ return trackAuth(Promise.race(racers));
254
+ };
255
+ try {
256
+ return await once();
257
+ }
258
+ catch (err) {
259
+ const hook = this.onAuthRequired;
260
+ if (!hook || !(err instanceof httpClient_1.McpOAuthRequiredError))
261
+ throw err;
262
+ let retry;
263
+ try {
264
+ retry = await hook(serverName, err.missingScope ?? '');
265
+ }
266
+ catch {
267
+ retry = false; // a broken host hook must not mask the OAuth error
268
+ }
269
+ if (!retry)
270
+ throw err;
271
+ // The host re-authorized (fresh tokens are in): one retry, no loop.
272
+ return await once();
265
273
  }
266
- return trackAuth(Promise.race(racers));
267
274
  }
268
275
  /** Resources across connected, enabled servers. One server failing never hides the others. */
269
276
  async listResources(server) {
@@ -32,4 +32,5 @@ export interface TokenMatch {
32
32
  /** Split `src` into simple commands, each a list of unquoted words. */
33
33
  export declare function tokenize(src: string): string[][];
34
34
  export declare function classifyTokens(tokens: string[], recurse: (command: string) => TokenMatch | null, rawSql: (command: string) => TokenMatch | null): TokenMatch | null;
35
+ export declare function unwrappedViews(command: string): string[];
35
36
  //# sourceMappingURL=destructiveTokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA6DhD;AAsVD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CA+HnB"}
1
+ {"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA6DhD;AAsVD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CA+HnB;AAyBD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAKxD"}
@@ -23,6 +23,7 @@
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
24
  exports.tokenize = tokenize;
25
25
  exports.classifyTokens = classifyTokens;
26
+ exports.unwrappedViews = unwrappedViews;
26
27
  const hit = (category, reason) => ({ category, reason });
27
28
  /** Split `src` into simple commands, each a list of unquoted words. */
28
29
  function tokenize(src) {
@@ -552,4 +553,96 @@ function classifyTokens(tokens, recurse, rawSql) {
552
553
  }
553
554
  return null;
554
555
  }
556
+ // ─── Every spelling of the command, for the permission-rule matcher ─────────
557
+ //
558
+ // A deny/ask rule must not be skippable by dressing the command up. Claude Code
559
+ // fixed exactly this class (2.1.288/289): "a deny or ask rule … missing a
560
+ // command behind an environment variable prefix with an expanded value (e.g.
561
+ // `TZ="$HOME" rm -rf build`)" and "a bare variable assignment came before the
562
+ // command". The destructive guard has always seen through these (tokenize +
563
+ // classifyTokens above); the RULE matcher had not, so `permissions.deny:
564
+ // ["Bash(rm:*)"]` silently did nothing against `FOO=bar rm -rf build`.
565
+ //
566
+ // unwrappedViews returns every "command actually run" spelling hiding in one
567
+ // command line, using this module's tokenizer and the same wrapper tables as
568
+ // classifyTokens, so anything the guard can see, a rule can see too:
569
+ //
570
+ // 'FOO=bar sudo rm -rf build' → 'sudo rm -rf build', 'rm -rf build'
571
+ // 'cd /tmp && FOO=1 rm -rf x' → 'cd /tmp', 'rm -rf x'
572
+ // 'echo $(rm -rf build)' → 'echo $', 'rm -rf build'
573
+ //
574
+ // Deliberately NOT unwrapped (same as classifyTokens's guard scope):
575
+ // ssh / docker exec / kubectl exec — reaching into those needs the remote or
576
+ // container branch, tracked separately.
577
+ //
578
+ // Order: most literal first (wrappers still attached), then each peeled form.
579
+ function unwrappedViews(command) {
580
+ if (typeof command !== 'string' || command.length === 0)
581
+ return [];
582
+ const out = new Set();
583
+ collectViews(command, out, 0);
584
+ return [...out];
585
+ }
586
+ /** Flags of a peelable wrapper that consume a following value (mirror of classifyTokens). */
587
+ const WRAPPER_VALUE_FLAGS = {
588
+ sudo: new Set(['-u', '-g', '-h', '-p', '-C', '-D', '-R', '-T', '-U']),
589
+ doas: new Set(['-u', '-g', '-h', '-p', '-C', '-D', '-R', '-T', '-U']),
590
+ env: new Set(['-u', '-C', '-S']),
591
+ nice: new Set(['-n']),
592
+ ionice: new Set(['-c', '-n', '-p']),
593
+ command: new Set(),
594
+ builtin: new Set(),
595
+ nohup: new Set(),
596
+ exec: new Set(),
597
+ time: new Set(),
598
+ setsid: new Set(),
599
+ caffeinate: new Set(),
600
+ stdbuf: new Set(),
601
+ unbuffer: new Set(),
602
+ };
603
+ /** `command -v rm` (or `-V`, `-pv`) only PRINTS where rm is — never peel it into `rm`. */
604
+ const LOOKUP_FLAG = /^-p?[vV]+$/;
605
+ function collectViews(src, out, depth) {
606
+ if (depth > 3)
607
+ return;
608
+ for (const tokens of tokenize(src)) {
609
+ let t = tokens;
610
+ for (let guard = 0; guard < 12; guard++) {
611
+ while (t.length && (ENV_ASSIGN.test(t[0]) || CONTROL_WORDS.has(t[0])))
612
+ t = t.slice(1);
613
+ if (t.length === 0)
614
+ break;
615
+ out.add(t.join(' '));
616
+ const bin = base(t[0]);
617
+ const args = t.slice(1);
618
+ // bash -c "…" / eval "…" carry the command as a STRING — read it too.
619
+ if (SHELLS.has(bin)) {
620
+ const i = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
621
+ if (i >= 0 && args[i + 1] !== undefined)
622
+ collectViews(args[i + 1], out, depth + 1);
623
+ break;
624
+ }
625
+ if (bin === 'eval') {
626
+ collectViews(args.join(' '), out, depth + 1);
627
+ break;
628
+ }
629
+ if (bin === 'timeout') {
630
+ t = dropOpts(args, new Set(['-s', '-k'])).slice(1); // timeout DURATION cmd
631
+ continue;
632
+ }
633
+ if (bin === 'xargs') {
634
+ t = dropOpts(args, new Set(['-I', '-n', '-P', '-L', '-s', '-d', '-E', '-a', '-J']));
635
+ continue;
636
+ }
637
+ if ((bin === 'command' || bin === 'builtin') && args.some((a) => LOOKUP_FLAG.test(a)))
638
+ break;
639
+ const valueFlags = WRAPPER_VALUE_FLAGS[bin];
640
+ if (valueFlags) {
641
+ t = dropOpts(args, valueFlags);
642
+ continue;
643
+ }
644
+ break;
645
+ }
646
+ }
647
+ }
555
648
  //# sourceMappingURL=destructiveTokens.js.map
@@ -15,6 +15,18 @@ export declare function setSessionRules(r: Partial<PermissionRules>): void;
15
15
  export declare function setSessionSettings(obj: Record<string, unknown> | null): void;
16
16
  export declare function getSessionSettings(): Record<string, unknown> | null;
17
17
  export declare function loadSettings(workDir: string): MergedSettings;
18
+ /**
19
+ * S25b: the raw object of every settings tier, in precedence order (managed →
20
+ * user → project → local → `--settings` session settings). For resolvers where
21
+ * the STRICTEST value must win regardless of tier — `maxEffortLevel` (a cap can
22
+ * only be tightened by a lower-precedence file, never loosened), and anything
23
+ * with the same one-way semantics added later. `loadSettings().raw` cannot serve
24
+ * those: it merges with LAST-wins, which is the opposite direction.
25
+ */
26
+ export declare function settingsTiersRaw(workDir: string): Array<{
27
+ tier: string;
28
+ raw: Record<string, unknown>;
29
+ }>;
18
30
  /** The first ALLOW rule that matches this call, or null. S20's classifier gate
19
31
  * reads this to tell a NARROW pre-approval (wins, no review) from a BROAD one
20
32
  * (suspended while the classifier is on — see isBroadAllowRule). */
@@ -1 +1 @@
1
- {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AAwBA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAQ5C;AAsBD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAEjE;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,CAE5E;AACD,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEnE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA8B5D;AAqJD;;oEAEoE;AACpE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAEf;AAYD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AA0BA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAQ5C;AAsBD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAEjE;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,CAE5E;AACD,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEnE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA8B5D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAcvG;AAsMD;;oEAEoE;AACpE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAEf;AAYD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
@@ -38,12 +38,15 @@ exports.setSessionRules = setSessionRules;
38
38
  exports.setSessionSettings = setSessionSettings;
39
39
  exports.getSessionSettings = getSessionSettings;
40
40
  exports.loadSettings = loadSettings;
41
+ exports.settingsTiersRaw = settingsTiersRaw;
41
42
  exports.matchedAllowRule = matchedAllowRule;
42
43
  exports.isBroadAllowRule = isBroadAllowRule;
43
44
  exports.evaluatePermission = evaluatePermission;
44
45
  const fs = __importStar(require("fs"));
45
46
  const path = __importStar(require("path"));
46
47
  const os = __importStar(require("os"));
48
+ const modePolicy_1 = require("./modePolicy");
49
+ const destructiveTokens_1 = require("./destructiveTokens");
47
50
  function managedSettingsPath() {
48
51
  // Tests (and a sandboxed doctor run) point this at a fixture. Checked first so it
49
52
  // can never be shadowed by a real machine-level file.
@@ -120,6 +123,31 @@ function loadSettings(workDir) {
120
123
  merged.deny.push(...sessionRules.deny);
121
124
  return { permissions: merged, raw };
122
125
  }
126
+ /**
127
+ * S25b: the raw object of every settings tier, in precedence order (managed →
128
+ * user → project → local → `--settings` session settings). For resolvers where
129
+ * the STRICTEST value must win regardless of tier — `maxEffortLevel` (a cap can
130
+ * only be tightened by a lower-precedence file, never loosened), and anything
131
+ * with the same one-way semantics added later. `loadSettings().raw` cannot serve
132
+ * those: it merges with LAST-wins, which is the opposite direction.
133
+ */
134
+ function settingsTiersRaw(workDir) {
135
+ const files = [
136
+ ['managed', managedSettingsPath()],
137
+ ['user', path.join(os.homedir(), '.nexrall', 'settings.json')],
138
+ ['project', path.join(workDir, '.nexrall', 'settings.json')],
139
+ ['local', path.join(workDir, '.nexrall', 'settings.local.json')],
140
+ ];
141
+ const out = [];
142
+ for (const [tier, file] of files) {
143
+ const obj = readJson(file);
144
+ if (obj)
145
+ out.push({ tier, raw: obj });
146
+ }
147
+ if (sessionSettings)
148
+ out.push({ tier: 'session', raw: sessionSettings });
149
+ return out;
150
+ }
123
151
  // ─── Rule matching ──────────────────────────────────────────────────────────
124
152
  function parseRule(rule) {
125
153
  const m = /^([A-Za-z0-9_]+)\((.*)\)$/.exec(rule.trim());
@@ -180,8 +208,21 @@ function matchPattern(pattern, value) {
180
208
  }
181
209
  return value === pattern;
182
210
  }
183
- /** Map a tool's input to the string(s) a rule pattern should match against. */
184
- function matchValues(tool, input, workDir) {
211
+ /** The command line of a bash-family call: `command`, falling back to the `cmd`
212
+ * alias every other reader accepts (destructive guard, CLI handler). */
213
+ function bashCommand(input) {
214
+ const v = input.command ?? input.cmd;
215
+ return typeof v === 'string' ? v : '';
216
+ }
217
+ /** Map a tool's input to the string(s) a rule pattern should match against.
218
+ *
219
+ * S25f: deny/ask matching ALSO sees a path's real identity behind symlinks —
220
+ * Claude Code 2.1.289 fixed exactly this class ("Read deny rules not applying
221
+ * to files @-mentioned, changed, or selected in the IDE through a symlink").
222
+ * The allow path skips those views (`followSymlinks: false`): an exception
223
+ * stays as NARROW as it was written — the same asymmetry the unwrapped bash
224
+ * views in destructiveTokens use. */
225
+ function matchValues(tool, input, workDir, followSymlinks = false) {
185
226
  const str = (v) => (typeof v === 'string' ? v : '');
186
227
  const vals = [];
187
228
  const pushPath = (p) => {
@@ -192,10 +233,36 @@ function matchValues(tool, input, workDir) {
192
233
  vals.push(path.relative(workDir, p));
193
234
  else
194
235
  vals.push(path.resolve(workDir, p));
236
+ if (!followSymlinks)
237
+ return;
238
+ const abs = path.resolve(workDir, p);
239
+ try {
240
+ const real = fs.realpathSync(abs);
241
+ if (real !== abs)
242
+ vals.push(real, path.relative(workDir, real));
243
+ }
244
+ catch {
245
+ // A path being CREATED through a symlinked directory: resolve the parent.
246
+ try {
247
+ const realDir = fs.realpathSync(path.dirname(abs));
248
+ const realAbs = path.join(realDir, path.basename(abs));
249
+ vals.push(realAbs, path.relative(workDir, realAbs));
250
+ }
251
+ catch {
252
+ /* neither exists: nothing to resolve */
253
+ }
254
+ }
195
255
  };
196
256
  switch (tool) {
257
+ // The whole bash family, matching modePolicy.BASH_TOOLS: the executor emits
258
+ // `bash`, and run_command / execute_command are the same tool accepted
259
+ // everywhere else (destructive guard, CLI handler). A `Bash(…)` rule must
260
+ // not silently skip a call that arrives under one of those names — the
261
+ // "looks configured and isn't" failure class the task rules had.
197
262
  case 'bash':
198
- vals.push(str(input.command));
263
+ case 'run_command':
264
+ case 'execute_command':
265
+ vals.push(bashCommand(input));
199
266
  break;
200
267
  case 'move_file':
201
268
  pushPath(str(input.source));
@@ -241,7 +308,7 @@ function ruleMatches(rule, tool, input, workDir, opts) {
241
308
  Read: ['read_file', 'list_directory', 'search_files', 'glob', 'notebook_read'],
242
309
  Edit: ['edit_file', 'multi_edit', 'write_file', 'notebook_edit'],
243
310
  Write: ['write_file', 'write_docx', 'write_xlsx', 'write_pptx'],
244
- Bash: ['bash'],
311
+ Bash: ['bash', 'run_command', 'execute_command'],
245
312
  WebFetch: ['fetch_url'],
246
313
  Image: ['generate_image', 'stock_photo'],
247
314
  };
@@ -255,15 +322,30 @@ function ruleMatches(rule, tool, input, workDir, opts) {
255
322
  // "Bash(npm run test:*)" allow rule straight past the user prompt.
256
323
  // Deny rules stay aggressive (they SHOULD match chained commands), and a
257
324
  // bare "Bash" allow (pattern === null) is an explicit "allow everything".
258
- if (opts?.forAllow && tool === 'bash' && pattern !== null && pattern !== '' && pattern !== '*') {
259
- const cmd = typeof input.command === 'string' ? input.command : '';
325
+ if (opts?.forAllow && modePolicy_1.BASH_TOOLS.has(tool) && pattern !== null && pattern !== '' && pattern !== '*') {
326
+ const cmd = bashCommand(input);
260
327
  // newline / ; / && / || / | / trailing & / command-substitution / backtick
261
328
  if (/[\r\n;`]|&&|\|\||\||&\s*$|\$\(/.test(cmd))
262
329
  return false;
263
330
  }
264
331
  if (pattern === null || pattern === '' || pattern === '*')
265
332
  return true;
266
- return matchValues(tool, input, workDir).some((v) => matchPattern(pattern, v));
333
+ if (matchValues(tool, input, workDir, !opts?.forAllow).some((v) => matchPattern(pattern, v)))
334
+ return true;
335
+ // Deny / ask rules must also see the command BEHIND a costume: env
336
+ // assignments (`TZ="$HOME" rm -rf build` — Claude Code's own fix class,
337
+ // 2.1.288/289), wrappers (`sudo`, `env`, `nice`, `timeout`, `xargs`, shell
338
+ // `-c`, `eval`), compound separators (`;` `&&` `||` `|`) and `$(…)` /
339
+ // backtick bodies. The destructive guard has always read these spellings
340
+ // (destructiveTokens); a user's deny rule must not be the one thing a
341
+ // prefix can skip.
342
+ //
343
+ // ALLOW deliberately stays exact — an exception is only as WIDE as it was
344
+ // written, and env vars can execute code (NODE_OPTIONS, LD_PRELOAD…), so
345
+ // `FOO=x npm run test` must not ride `Bash(npm run test:*)` without asking.
346
+ if (opts?.forAllow || !modePolicy_1.BASH_TOOLS.has(tool))
347
+ return false;
348
+ return (0, destructiveTokens_1.unwrappedViews)(bashCommand(input)).some((v) => matchPattern(pattern, v));
267
349
  }
268
350
  /** The first ALLOW rule that matches this call, or null. S20's classifier gate
269
351
  * reads this to tell a NARROW pre-approval (wins, no review) from a BROAD one
@@ -0,0 +1,43 @@
1
+ import { type UserConfigOption } from './userConfig';
2
+ type SecretMap = Record<string, Record<string, unknown>>;
3
+ /** The whole secrets file ({} when absent/unreadable/malformed). */
4
+ export declare function readPluginSecrets(): SecretMap;
5
+ /**
6
+ * `pluginConfigs` from every settings tier, merged per plugin per key
7
+ * (last tier wins — the same direction loadSettings uses for `raw`).
8
+ */
9
+ export declare function pluginConfigsFromSettings(workDir: string): Record<string, Record<string, unknown>>;
10
+ export interface ResolvedPluginConfig {
11
+ /** key → effective value (valid sources only, default included). */
12
+ values: Record<string, unknown>;
13
+ /** key → where the value came from. */
14
+ sources: Record<string, 'secret' | 'settings' | 'default'>;
15
+ /** required options with no value anywhere (not even a default). */
16
+ missing: string[];
17
+ /** Values that failed validation, with their source — reported, not used. */
18
+ invalid: Array<{
19
+ key: string;
20
+ source: 'secret' | 'settings';
21
+ error: string;
22
+ }>;
23
+ }
24
+ /** Resolve every declared option to its effective value. Pure reads. */
25
+ export declare function resolvePluginConfig(workDir: string, pluginName: string, options: UserConfigOption[]): ResolvedPluginConfig;
26
+ export interface SavePluginConfigResult {
27
+ ok: boolean;
28
+ /** Nothing was written when this is non-empty. */
29
+ errors: string[];
30
+ /** Paths actually written. */
31
+ wroteSettings?: string;
32
+ wroteSecrets?: boolean;
33
+ }
34
+ /**
35
+ * Validate + save values. Unknown keys are errors (a typo must not create a
36
+ * silent no-op entry), as are values that fail their option's validation. The
37
+ * two halves land in different stores (see the file header).
38
+ */
39
+ export declare function savePluginConfig(workDir: string, pluginName: string, options: UserConfigOption[], values: Record<string, unknown>, opts?: {
40
+ scope?: 'user' | 'project';
41
+ }): SavePluginConfigResult;
42
+ export {};
43
+ //# sourceMappingURL=configStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configStore.d.ts","sourceRoot":"","sources":["../../src/plugins/configStore.ts"],"names":[],"mappings":"AA2BA,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAmD9E,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEzD,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,SAAS,CAQ7C;AAgBD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAWlG;AAED,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,CAAC;IAC3D,oEAAoE;IACpE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/E;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,gBAAgB,EAAE,GAC1B,oBAAoB,CA+BtB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,gBAAgB,EAAE,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GACxC,sBAAsB,CAkDxB"}