@nexrall/code-core 1.4.77 → 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 (39) 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/hooks.d.ts +10 -2
  5. package/dist/agent/hooks.d.ts.map +1 -1
  6. package/dist/agent/hooks.js +9 -3
  7. package/dist/agent/skills.d.ts.map +1 -1
  8. package/dist/agent/skills.js +14 -1
  9. package/dist/index.d.ts +5 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -0
  12. package/dist/mcp/manager.d.ts +8 -0
  13. package/dist/mcp/manager.d.ts.map +1 -1
  14. package/dist/mcp/manager.js +64 -57
  15. package/dist/permissions/destructiveTokens.d.ts +1 -0
  16. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  17. package/dist/permissions/destructiveTokens.js +93 -0
  18. package/dist/permissions/rules.d.ts +12 -0
  19. package/dist/permissions/rules.d.ts.map +1 -1
  20. package/dist/permissions/rules.js +89 -7
  21. package/dist/plugins/configStore.d.ts +43 -0
  22. package/dist/plugins/configStore.d.ts.map +1 -0
  23. package/dist/plugins/configStore.js +253 -0
  24. package/dist/plugins/files.d.ts +10 -0
  25. package/dist/plugins/files.d.ts.map +1 -0
  26. package/dist/plugins/files.js +85 -0
  27. package/dist/plugins/index.d.ts +3 -22
  28. package/dist/plugins/index.d.ts.map +1 -1
  29. package/dist/plugins/index.js +30 -40
  30. package/dist/plugins/resolve.d.ts +18 -0
  31. package/dist/plugins/resolve.d.ts.map +1 -0
  32. package/dist/plugins/resolve.js +152 -0
  33. package/dist/plugins/userConfig.d.ts +90 -0
  34. package/dist/plugins/userConfig.d.ts.map +1 -0
  35. package/dist/plugins/userConfig.js +332 -0
  36. package/dist/plugins/validate.d.ts +14 -0
  37. package/dist/plugins/validate.d.ts.map +1 -0
  38. package/dist/plugins/validate.js +222 -0
  39. package/package.json +1 -1
@@ -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"}
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ // ─── Plugin config values store (S24) ────────────────────────────────────────
3
+ //
4
+ // Where a plugin's userConfig VALUES live:
5
+ //
6
+ // • non-sensitive → `pluginConfigs.<plugin>.<key>` in a settings file
7
+ // (managed → user → project → local → `--settings`, read merged; the LAST
8
+ // tier wins per key, matching every other settings read in the codebase).
9
+ // Written to the tier you choose — default: the scope the plugin is
10
+ // installed in, so a project plugin configures itself in the project.
11
+ // • sensitive → `~/.nexrall/plugin-secrets.json` (0600), keyed the same way.
12
+ // Claude Code uses the OS credential store here; the CLI has no such
13
+ // integration, so this is a dedicated 0600 file instead — stated plainly
14
+ // rather than implied to be a keychain.
15
+ // • nothing set → the manifest default (resolvePluginConfig reports which).
16
+ //
17
+ // Resolution order per key: secrets → settings → default. An INVALID value at
18
+ // a higher precedence does not win: it is reported (resolvePluginConfig's
19
+ // `invalid`) and resolution continues to the next source, so a typo in
20
+ // settings cannot silently break a working default.
21
+ //
22
+ // This module reads/writes the disk; the schema and substitution half is pure
23
+ // (userConfig.ts).
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.readPluginSecrets = readPluginSecrets;
59
+ exports.pluginConfigsFromSettings = pluginConfigsFromSettings;
60
+ exports.resolvePluginConfig = resolvePluginConfig;
61
+ exports.savePluginConfig = savePluginConfig;
62
+ const fs = __importStar(require("fs"));
63
+ const os = __importStar(require("os"));
64
+ const path = __importStar(require("path"));
65
+ const rules_1 = require("../permissions/rules");
66
+ const userConfig_1 = require("./userConfig");
67
+ // NOTE (import graph): this module deliberately does NOT use
68
+ // util/settingsFile's writeSetting — that module imports agent/hooks, and
69
+ // hooks imports plugins/resolve → this file; using it would close a cycle.
70
+ // The atomic write below is the same temp-file+rename shape, minus the
71
+ // ConfigChange observer hook (pluginConfig edits do not fire it yet).
72
+ function secretsPath() {
73
+ return path.join(os.homedir(), '.nexrall', 'plugin-secrets.json');
74
+ }
75
+ /** The settings file a scoped write targets (mirror of util/settingsFile's settingsPath). */
76
+ function tierFile(workDir, scope) {
77
+ return scope === 'project'
78
+ ? path.join(workDir, '.nexrall', 'settings.json')
79
+ : path.join(os.homedir(), '.nexrall', 'settings.json');
80
+ }
81
+ /** Atomic top-level-key write: temp file + rename, so an interrupted write cannot half-JSON a settings file. */
82
+ function writeTierSetting(workDir, scope, key, value) {
83
+ const file = tierFile(workDir, scope);
84
+ let existing = {};
85
+ if (fs.existsSync(file)) {
86
+ try {
87
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
88
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
89
+ existing = parsed;
90
+ else
91
+ return { ok: false, path: file, error: 'the file is valid JSON but not an object' };
92
+ }
93
+ catch (e) {
94
+ return { ok: false, path: file, error: `could not parse it: ${e.message}` };
95
+ }
96
+ }
97
+ const next = { ...existing };
98
+ if (value === undefined)
99
+ delete next[key];
100
+ else
101
+ next[key] = value;
102
+ try {
103
+ fs.mkdirSync(path.dirname(file), { recursive: true });
104
+ const tmp = `${file}.${process.pid}.tmp`;
105
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2) + '\n', 'utf-8');
106
+ fs.renameSync(tmp, file);
107
+ return { ok: true, path: file };
108
+ }
109
+ catch (e) {
110
+ return { ok: false, path: file, error: e.message };
111
+ }
112
+ }
113
+ /** The whole secrets file ({} when absent/unreadable/malformed). */
114
+ function readPluginSecrets() {
115
+ try {
116
+ const j = JSON.parse(fs.readFileSync(secretsPath(), 'utf-8'));
117
+ if (!j || typeof j !== 'object' || Array.isArray(j))
118
+ return {};
119
+ return j;
120
+ }
121
+ catch {
122
+ return {};
123
+ }
124
+ }
125
+ /** Write the secrets file 0600. Best-effort: a read-only home must not crash a save that the settings half may still complete. */
126
+ function writePluginSecrets(map) {
127
+ const file = secretsPath();
128
+ try {
129
+ fs.mkdirSync(path.dirname(file), { recursive: true });
130
+ const tmp = `${file}.${process.pid}.tmp`;
131
+ fs.writeFileSync(tmp, JSON.stringify(map, null, 2) + '\n', { encoding: 'utf-8', mode: 0o600 });
132
+ fs.renameSync(tmp, file);
133
+ return { ok: true };
134
+ }
135
+ catch (e) {
136
+ return { ok: false, error: e.message };
137
+ }
138
+ }
139
+ /**
140
+ * `pluginConfigs` from every settings tier, merged per plugin per key
141
+ * (last tier wins — the same direction loadSettings uses for `raw`).
142
+ */
143
+ function pluginConfigsFromSettings(workDir) {
144
+ const out = {};
145
+ for (const { raw } of (0, rules_1.settingsTiersRaw)(workDir)) {
146
+ const pc = raw.pluginConfigs;
147
+ if (!pc || typeof pc !== 'object' || Array.isArray(pc))
148
+ continue;
149
+ for (const [plugin, bag] of Object.entries(pc)) {
150
+ if (!bag || typeof bag !== 'object' || Array.isArray(bag))
151
+ continue;
152
+ out[plugin] = { ...(out[plugin] ?? {}), ...bag };
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+ /** Resolve every declared option to its effective value. Pure reads. */
158
+ function resolvePluginConfig(workDir, pluginName, options) {
159
+ const secrets = readPluginSecrets()[pluginName] ?? {};
160
+ const fromSettings = pluginConfigsFromSettings(workDir)[pluginName] ?? {};
161
+ const values = {};
162
+ const sources = {};
163
+ const invalid = [];
164
+ for (const option of options) {
165
+ let found = false;
166
+ for (const [source, bag] of [
167
+ ['secret', secrets],
168
+ ['settings', fromSettings],
169
+ ]) {
170
+ if (!(option.key in bag))
171
+ continue;
172
+ const err = (0, userConfig_1.validateUserConfigValue)(option, bag[option.key]);
173
+ if (err) {
174
+ invalid.push({ key: option.key, source, error: err });
175
+ continue; // a broken higher-precedence value must not mask a working lower one
176
+ }
177
+ values[option.key] = bag[option.key];
178
+ sources[option.key] = source;
179
+ found = true;
180
+ break;
181
+ }
182
+ if (found)
183
+ continue;
184
+ if (option.default !== undefined) {
185
+ values[option.key] = option.default;
186
+ sources[option.key] = 'default';
187
+ }
188
+ }
189
+ const missing = options.filter((o) => o.required === true && !(o.key in values)).map((o) => o.key);
190
+ return { values, sources, missing, invalid };
191
+ }
192
+ /**
193
+ * Validate + save values. Unknown keys are errors (a typo must not create a
194
+ * silent no-op entry), as are values that fail their option's validation. The
195
+ * two halves land in different stores (see the file header).
196
+ */
197
+ function savePluginConfig(workDir, pluginName, options, values, opts = {}) {
198
+ const byKey = new Map(options.map((o) => [o.key, o]));
199
+ const errors = [];
200
+ const plain = {};
201
+ const secret = {};
202
+ for (const [key, value] of Object.entries(values)) {
203
+ const option = byKey.get(key);
204
+ if (!option) {
205
+ errors.push(`"${key}" is not a declared option of plugin "${pluginName}"`);
206
+ continue;
207
+ }
208
+ const bad = (0, userConfig_1.validateUserConfigValue)(option, value);
209
+ if (bad) {
210
+ errors.push(`${key}: ${bad}`);
211
+ continue;
212
+ }
213
+ if (option.sensitive)
214
+ secret[key] = value;
215
+ else
216
+ plain[key] = value;
217
+ }
218
+ if (errors.length)
219
+ return { ok: false, errors };
220
+ const result = { ok: true, errors: [] };
221
+ if (Object.keys(secret).length) {
222
+ const map = readPluginSecrets();
223
+ map[pluginName] = { ...(map[pluginName] ?? {}), ...secret };
224
+ const w = writePluginSecrets(map);
225
+ if (!w.ok)
226
+ return { ok: false, errors: [`could not write the secrets file: ${w.error}`] };
227
+ result.wroteSecrets = true;
228
+ }
229
+ if (Object.keys(plain).length) {
230
+ const scope = opts.scope ?? 'user';
231
+ // Read the TARGET tier file's pluginConfigs and merge — the write replaces the
232
+ // whole top-level key, so passing a partial object would drop other plugins.
233
+ const file = tierFile(workDir, scope);
234
+ let tierPc = {};
235
+ try {
236
+ const j = JSON.parse(fs.readFileSync(file, 'utf-8'));
237
+ const pc = j?.pluginConfigs;
238
+ if (pc && typeof pc === 'object' && !Array.isArray(pc)) {
239
+ tierPc = pc;
240
+ }
241
+ }
242
+ catch {
243
+ /* no file yet, or unreadable — start from an empty map; the writer refuses a corrupt file itself */
244
+ }
245
+ const merged = { ...tierPc, [pluginName]: { ...(tierPc[pluginName] ?? {}), ...plain } };
246
+ const w = writeTierSetting(workDir, scope, 'pluginConfigs', merged);
247
+ if (!w.ok)
248
+ return { ok: false, errors: [`could not write settings: ${w.error}`] };
249
+ result.wroteSettings = w.path;
250
+ }
251
+ return result;
252
+ }
253
+ //# sourceMappingURL=configStore.js.map
@@ -0,0 +1,10 @@
1
+ export declare const FILE_CANDIDATES: {
2
+ readonly manifest: readonly ["plugin.json", string];
3
+ readonly hooks: readonly ["hooks.json", string];
4
+ readonly mcp: readonly ["mcp.json", ".mcp.json"];
5
+ };
6
+ /** First existing candidate path for a component, or null. */
7
+ export declare function resolvePluginFile(dir: string, kind: keyof typeof FILE_CANDIDATES): string | null;
8
+ /** The plugin's manifest as a parsed object (null when absent/malformed/not an object). */
9
+ export declare function readPluginManifest(dir: string): Record<string, unknown> | null;
10
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/plugins/files.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,eAAe;;;;CAIlB,CAAC;AAEX,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,eAAe,GAAG,MAAM,GAAG,IAAI,CAUhG;AAED,2FAA2F;AAC3F,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAS9E"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FILE_CANDIDATES = void 0;
37
+ exports.resolvePluginFile = resolvePluginFile;
38
+ exports.readPluginManifest = readPluginManifest;
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ // ─── Component file locations ────────────────────────────────────────────────
42
+ //
43
+ // Extracted from plugins/index.ts (S24) so the modules that PARSE a plugin's
44
+ // files without loading it — userConfig schema reading, `nex plugin validate`,
45
+ // the config store — can resolve paths without importing the loader (which
46
+ // imports them back: a cycle).
47
+ //
48
+ // SECURITY: `hooks` and `mcp` here are code-execution surfaces. Every reader
49
+ // (plugins/index.ts, installer.ts's inspectPluginDir, validate.ts) MUST resolve
50
+ // through this same table - if the loader reads a path the inspector does not
51
+ // check, a plugin can run commands that the install-time warning never
52
+ // mentioned.
53
+ exports.FILE_CANDIDATES = {
54
+ manifest: ['plugin.json', path.join('.claude-plugin', 'plugin.json')],
55
+ hooks: ['hooks.json', path.join('hooks', 'hooks.json')],
56
+ mcp: ['mcp.json', '.mcp.json'],
57
+ };
58
+ /** First existing candidate path for a component, or null. */
59
+ function resolvePluginFile(dir, kind) {
60
+ for (const rel of exports.FILE_CANDIDATES[kind]) {
61
+ const full = path.join(dir, rel);
62
+ try {
63
+ if (fs.statSync(full).isFile())
64
+ return full;
65
+ }
66
+ catch {
67
+ /* try the next candidate */
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /** The plugin's manifest as a parsed object (null when absent/malformed/not an object). */
73
+ function readPluginManifest(dir) {
74
+ const file = resolvePluginFile(dir, 'manifest');
75
+ if (!file)
76
+ return null;
77
+ try {
78
+ const j = JSON.parse(fs.readFileSync(file, 'utf-8'));
79
+ return j && typeof j === 'object' && !Array.isArray(j) ? j : null;
80
+ }
81
+ catch {
82
+ return null;
83
+ }
84
+ }
85
+ //# sourceMappingURL=files.js.map
@@ -1,3 +1,4 @@
1
+ export { FILE_CANDIDATES, resolvePluginFile, readPluginManifest } from './files';
1
2
  export interface PluginInfo {
2
3
  /** Directory name (or plugin.json "name" when present). */
3
4
  name: string;
@@ -13,26 +14,6 @@ export interface PluginInfo {
13
14
  export declare function isPluginDisabled(dir: string): boolean;
14
15
  /** Toggle a plugin on/off. Idempotent — enabling an already-enabled plugin is a no-op. */
15
16
  export declare function setPluginDisabled(dir: string, disabled: boolean): void;
16
- /**
17
- * Where each single-file component may live, in priority order.
18
- *
19
- * Our own layout is checked first so a plugin shipping both is unambiguous, and
20
- * so this stays additive: nothing that worked before changes behaviour.
21
- *
22
- * SECURITY: `hooks` and `mcp` here are code-execution surfaces. Every reader
23
- * below and `inspectPluginDir` in installer.ts MUST resolve through this same
24
- * table — if the loader reads a path the inspector does not check, a plugin can
25
- * run commands that the install-time warning never mentioned. Adding a candidate
26
- * without updating the inspector is exactly that bug, so the two are pinned
27
- * together by a test.
28
- */
29
- export declare const FILE_CANDIDATES: {
30
- readonly manifest: readonly ["plugin.json", string];
31
- readonly hooks: readonly ["hooks.json", string];
32
- readonly mcp: readonly ["mcp.json", ".mcp.json"];
33
- };
34
- /** First existing candidate path for a component, or null. */
35
- export declare function resolvePluginFile(dir: string, kind: keyof typeof FILE_CANDIDATES): string | null;
36
17
  /**
37
18
  * Discover installed plugins (project scope shadows global on name clash).
38
19
  *
@@ -65,7 +46,7 @@ export declare function pluginAssetDirs(workDir: string, kind: 'commands' | 'age
65
46
  * processes their NEXRALL_PLUGIN_DATA (see runHookHandler). Both entry forms are stamped:
66
47
  * the flat one (the entry IS the handler) and the nested one (each item under `hooks`).
67
48
  */
68
- export declare function pluginHooks(workDir: string): Record<string, unknown[]>;
49
+ export declare function pluginHooks(workDir: string, transform?: (entry: unknown, plugin: string) => unknown): Record<string, unknown[]>;
69
50
  /** MCP server maps declared by ENABLED plugins: { serverName → config } (first plugin wins on clash).
70
51
  *
71
52
  * A stdio server from a plugin gets NEXRALL_PLUGIN_DATA in its environment, pointing at the
@@ -73,5 +54,5 @@ export declare function pluginHooks(workDir: string): Record<string, unknown[]>;
73
54
  * this is the only place a server can keep state. A plugin that set the variable itself keeps
74
55
  * its own value: an explicit config value is never overridden here.
75
56
  */
76
- export declare function pluginMcpServers(workDir: string): Record<string, unknown>;
57
+ export declare function pluginMcpServers(workDir: string, transform?: (cfg: unknown, plugin: string) => unknown): Record<string, unknown>;
77
58
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAuCA,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAYD,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAWtE;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe;;;;CAIlB,CAAC;AAEX,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,eAAe,GAAG,MAAM,GAAG,IAAI,CAUhG;AA+CD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAM7G;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,EAAE,CAWjG;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAoBtE;AAeD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkBzE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAoCjF,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAYD,oEAAoE;AACpE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMrD;AAED,0FAA0F;AAC1F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,IAAI,CAWtE;AAsDD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAM7G;AAED,gGAAgG;AAChG,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,EAAE,CAWjG;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,GACtD,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CA4B3B;AAeD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,GACpD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB"}