@orchid-labs/pluxx 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -5170,12 +5170,39 @@ function buildUserConfigEnvMap(entries) {
5170
5170
  }
5171
5171
  return env;
5172
5172
  }
5173
- function buildUserConfigValueMap(entries) {
5173
+ function buildInstalledUserConfigPayload(entries, options = {}) {
5174
+ const preserveSecretReferences = options.preserveSecretReferences === true;
5174
5175
  const values = {};
5176
+ const env = {};
5177
+ const envRefs = {};
5178
+ const secretKeys = /* @__PURE__ */ new Set();
5179
+ const secretEnv = /* @__PURE__ */ new Set();
5180
+ let hasSecret = false;
5175
5181
  for (const entry of entries) {
5182
+ const envVar = entry.envVar;
5183
+ const isSecret = entry.field.type === "secret";
5184
+ if (isSecret) {
5185
+ hasSecret = true;
5186
+ if (preserveSecretReferences) {
5187
+ secretKeys.add(entry.field.key);
5188
+ if (envVar) secretEnv.add(envVar);
5189
+ }
5190
+ }
5191
+ if (preserveSecretReferences && isSecret) {
5192
+ if (envVar) envRefs[envVar] = envVar;
5193
+ continue;
5194
+ }
5176
5195
  values[entry.field.key] = entry.value;
5196
+ if (envVar) env[envVar] = String(entry.value);
5177
5197
  }
5178
- return values;
5198
+ return {
5199
+ ...Object.keys(values).length > 0 ? { values } : {},
5200
+ ...Object.keys(env).length > 0 ? { env } : {},
5201
+ ...Object.keys(envRefs).length > 0 ? { envRefs } : {},
5202
+ ...hasSecret ? { secretStorage: preserveSecretReferences ? "env-ref" : "materialized" } : {},
5203
+ ...preserveSecretReferences && secretKeys.size > 0 ? { secretKeys: [...secretKeys].sort() } : {},
5204
+ ...preserveSecretReferences && secretEnv.size > 0 ? { secretEnv: [...secretEnv].sort() } : {}
5205
+ };
5179
5206
  }
5180
5207
 
5181
5208
  // src/mcp-native-overrides.ts
@@ -5386,9 +5413,9 @@ var Generator = class {
5386
5413
  for (const configPath of this.config.passthrough ?? []) {
5387
5414
  const src = this.resolveConfigPath(configPath, "passthrough");
5388
5415
  if (!existsSync4(src)) continue;
5389
- const basename10 = src.split("/").filter(Boolean).pop();
5390
- if (!basename10) continue;
5391
- this.copyDir(configPath, `${basename10}/`, "passthrough");
5416
+ const basename11 = src.split("/").filter(Boolean).pop();
5417
+ if (!basename11) continue;
5418
+ this.copyDir(configPath, `${basename11}/`, "passthrough");
5392
5419
  }
5393
5420
  }
5394
5421
  /** Build canonical MCP server configs for target-specific output shaping. */
@@ -5494,6 +5521,18 @@ var CURSOR_SUPPORTED_HOOK_EVENTS = [
5494
5521
  "beforeTabFileRead",
5495
5522
  "afterTabFileEdit"
5496
5523
  ];
5524
+ var CODEX_SUPPORTED_HOOK_EVENTS = [
5525
+ "SessionStart",
5526
+ "SubagentStart",
5527
+ "PreToolUse",
5528
+ "PermissionRequest",
5529
+ "PostToolUse",
5530
+ "PreCompact",
5531
+ "PostCompact",
5532
+ "UserPromptSubmit",
5533
+ "SubagentStop",
5534
+ "Stop"
5535
+ ];
5497
5536
  var PASCAL_CASE_HOOK_ALIASES = {
5498
5537
  beforeSubmitPrompt: "UserPromptSubmit"
5499
5538
  };
@@ -5540,8 +5579,8 @@ var HOOK_PLATFORM_REGISTRY = {
5540
5579
  },
5541
5580
  codex: {
5542
5581
  supportedTypes: ["command"],
5543
- supportedEvents: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
5544
- unsupportedEventReason: "Codex currently documents only SessionStart, PreToolUse, PermissionRequest, PostToolUse, UserPromptSubmit, and Stop for hook configuration.",
5582
+ supportedEvents: CODEX_SUPPORTED_HOOK_EVENTS,
5583
+ unsupportedEventReason: `Codex currently documents only ${CODEX_SUPPORTED_HOOK_EVENTS.join(", ")} for hook configuration.`,
5545
5584
  fields: {
5546
5585
  prompt: { mode: "drop" },
5547
5586
  matcher: {
@@ -5838,8 +5877,16 @@ function loadUserEnv(pluginRoot) {
5838
5877
  const env = parsed && typeof parsed === "object" && parsed.env && typeof parsed.env === "object"
5839
5878
  ? parsed.env
5840
5879
  : {}
5880
+ const envRefs = parsed && typeof parsed === "object" && parsed.envRefs && typeof parsed.envRefs === "object"
5881
+ ? parsed.envRefs
5882
+ : {}
5841
5883
  return Object.fromEntries(
5842
- Object.entries(env).filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)),
5884
+ [
5885
+ ...Object.entries(env),
5886
+ ...Object.entries(envRefs)
5887
+ .filter(([, value]) => typeof value === "string" && value in process.env)
5888
+ .map(([key, value]) => [key, process.env[value]]),
5889
+ ].filter(([key, value]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && typeof value === "string"),
5843
5890
  )
5844
5891
  } catch {
5845
5892
  return {}
@@ -6077,7 +6124,7 @@ function getEnabledRuntimeReadinessBindings(capability, plan) {
6077
6124
  });
6078
6125
  }
6079
6126
  var NAMED_PROMPT_TARGET_NOTE = "Named `skills` / `commands` readiness targets currently translate through prompt-entry gating with best-effort matching because the core four do not share one exact per-skill or per-command runtime interception surface.";
6080
- var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, and Pluxx still emits `.codex/readiness.generated.json` plus `.codex/hooks.generated.json` as debugging companions because plugin-bundled hook activation still depends on `[features].plugin_hooks = true`. The general `hooks = true` flag covers non-plugin hook config and defaults on, while `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback.";
6127
+ var CODEX_EXTERNAL_NOTE = "Codex readiness now bundles translated hooks in the plugin, and Pluxx still emits `.codex/readiness.generated.json` plus `.codex/hooks.generated.json` as debugging companions because hook activation still depends on `[features].hooks = true`, enabled plugin state, review, trust, and runtime support. `codex_hooks` is deprecated and should not be treated as the current hook feature key.";
6081
6128
  function getRuntimeReadinessNamedPromptTargetNote() {
6082
6129
  return NAMED_PROMPT_TARGET_NOTE;
6083
6130
  }
@@ -6151,18 +6198,18 @@ function getRuntimeReadinessCapability(platform, pluginRootVar = "PLUGIN_ROOT")
6151
6198
  {
6152
6199
  gate: "session-start",
6153
6200
  event: "SessionStart",
6154
- command: "node ./.codex/pluxx-readiness.mjs session-start"
6201
+ command: `node "\${${pluginRootVar}}/.codex/pluxx-readiness.mjs" session-start`
6155
6202
  },
6156
6203
  {
6157
6204
  gate: "mcp-gate",
6158
6205
  event: "PreToolUse",
6159
6206
  matcher: "MCP",
6160
- command: "node ./.codex/pluxx-readiness.mjs mcp-gate"
6207
+ command: `node "\${${pluginRootVar}}/.codex/pluxx-readiness.mjs" mcp-gate`
6161
6208
  },
6162
6209
  {
6163
6210
  gate: "prompt-gate",
6164
6211
  event: "UserPromptSubmit",
6165
- command: "node ./.codex/pluxx-readiness.mjs prompt-gate"
6212
+ command: `node "\${${pluginRootVar}}/.codex/pluxx-readiness.mjs" prompt-gate`
6166
6213
  }
6167
6214
  ],
6168
6215
  notes: CODEX_EXTERNAL_NOTE
@@ -6378,6 +6425,9 @@ function shellSingleQuote(value) {
6378
6425
  return `'${value.replace(/'/g, `'"'"'`)}'`;
6379
6426
  }
6380
6427
  function buildHookCommandWrapperScript(command2, pluginRootVar, envFileVar) {
6428
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(pluginRootVar)) {
6429
+ throw new Error(`Invalid plugin root environment variable name: ${pluginRootVar}`);
6430
+ }
6381
6431
  const serializedCommand = shellSingleQuote(command2);
6382
6432
  const exportLoader = [
6383
6433
  'import { readFileSync } from "node:fs"',
@@ -6390,10 +6440,19 @@ function buildHookCommandWrapperScript(command2, pluginRootVar, envFileVar) {
6390
6440
  'const env = payload && typeof payload === "object" && payload.env && typeof payload.env === "object"',
6391
6441
  " ? payload.env",
6392
6442
  " : {}",
6443
+ 'const envRefs = payload && typeof payload === "object" && payload.envRefs && typeof payload.envRefs === "object"',
6444
+ " ? payload.envRefs",
6445
+ " : {}",
6393
6446
  "",
6394
6447
  "for (const [key, value] of Object.entries(env)) {",
6395
6448
  " if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue",
6396
6449
  " process.stdout.write(`export ${key}=${shellSingleQuote(value)}\\0`)",
6450
+ "}",
6451
+ "for (const [key, envVar] of Object.entries(envRefs)) {",
6452
+ " if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue",
6453
+ ' if (typeof envVar !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(envVar)) continue',
6454
+ " if (!(envVar in process.env)) continue",
6455
+ " process.stdout.write(`export ${key}=${shellSingleQuote(process.env[envVar])}\\0`)",
6397
6456
  "}"
6398
6457
  ].join("\n");
6399
6458
  const maybeAppendEnvFile = envFileVar ? [
@@ -6406,6 +6465,8 @@ function buildHookCommandWrapperScript(command2, pluginRootVar, envFileVar) {
6406
6465
  "set -euo pipefail",
6407
6466
  "",
6408
6467
  `PLUXX_PLUGIN_ROOT="\${${pluginRootVar}:-$(cd "$(dirname "$0")/.." && pwd)}"`,
6468
+ `export ${pluginRootVar}="$PLUXX_PLUGIN_ROOT"`,
6469
+ 'export PLUGIN_ROOT="$PLUXX_PLUGIN_ROOT"',
6409
6470
  'PLUXX_USER_CONFIG_PATH="$PLUXX_PLUGIN_ROOT/.pluxx-user.json"',
6410
6471
  "",
6411
6472
  'if [ -f "$PLUXX_USER_CONFIG_PATH" ]; then',
@@ -9870,7 +9931,7 @@ function toQuotedTomlKey(value) {
9870
9931
  // src/codex-hooks-feature.ts
9871
9932
  var RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
9872
9933
  var ALTERNATE_CODEX_HOOKS_FEATURE_FLAG = "codex_hooks";
9873
- var PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG = "plugin_hooks";
9934
+ var PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG = "hooks";
9874
9935
  function getCodexHooksFeatureState(features) {
9875
9936
  if (!features) {
9876
9937
  return {
@@ -10145,7 +10206,7 @@ var CodexGenerator = class extends Generator {
10145
10206
  }
10146
10207
  async generateHooksCompanion() {
10147
10208
  const readinessPlan = getRuntimeReadinessPlan(this.config.readiness);
10148
- const readinessCapability = getRuntimeReadinessCapability("codex");
10209
+ const readinessCapability = getRuntimeReadinessCapability("codex", "CODEX_PLUGIN_ROOT");
10149
10210
  if (!this.config.hooks && !readinessPlan.hasReadiness) return;
10150
10211
  const hooks = {};
10151
10212
  const unsupported = [];
@@ -10193,12 +10254,12 @@ var CodexGenerator = class extends Generator {
10193
10254
  const relativePath = `hooks/pluxx-hook-command-${nextWrapperIndex}.sh`;
10194
10255
  await this.writeFile(
10195
10256
  relativePath,
10196
- buildHookCommandWrapperScript(entry.command.replace("${PLUGIN_ROOT}", "."), "CODEX_PLUGIN_ROOT")
10257
+ buildHookCommandWrapperScript(entry.command.replace("${PLUGIN_ROOT}", "${PLUXX_PLUGIN_ROOT}"), "CODEX_PLUGIN_ROOT")
10197
10258
  );
10198
10259
  const matcher = typeof entry.matcher === "string" && isHookFieldPreserved("codex", "matcher", codexEvent) ? entry.matcher : void 0;
10199
10260
  mappedEntries.push(this.buildCodexCommandHookGroup(
10200
10261
  codexEvent,
10201
- `bash ./${relativePath}`,
10262
+ `bash "\${CODEX_PLUGIN_ROOT}/${relativePath}"`,
10202
10263
  matcher,
10203
10264
  entry.timeout
10204
10265
  ));
@@ -10220,14 +10281,14 @@ var CodexGenerator = class extends Generator {
10220
10281
  pluginBundleFeatureFlag: PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG,
10221
10282
  generalFeatureFlag: RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG,
10222
10283
  deprecatedGeneralFeatureFlag: ALTERNATE_CODEX_HOOKS_FEATURE_FLAG,
10223
- note: "Codex plugin-bundled hook configuration is bundled at hooks/hooks.json in the plugin and currently requires `[features].plugin_hooks = true`. The general `[features].hooks` flag covers non-plugin hook config and defaults on, while `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback. This companion mirror preserves the translated native event names, matcher groups, and command handlers while highlighting any dropped events or fields.",
10284
+ note: "Codex plugin-bundled hook configuration is bundled at hooks/hooks.json in the plugin and currently requires `[features].hooks = true`, enabled plugin state, review, trust, and runtime support. `codex_hooks` is deprecated and should not be treated as the current hook feature key. This companion mirror preserves the translated native event names, matcher groups, and command handlers while highlighting any dropped events or fields.",
10224
10285
  hooks,
10225
10286
  ...unsupported.length > 0 ? { unsupported } : {}
10226
10287
  });
10227
10288
  }
10228
10289
  async generateReadinessCompanion() {
10229
10290
  const readinessPlan = getRuntimeReadinessPlan(this.config.readiness);
10230
- const readinessCapability = getRuntimeReadinessCapability("codex");
10291
+ const readinessCapability = getRuntimeReadinessCapability("codex", "CODEX_PLUGIN_ROOT");
10231
10292
  if (!readinessPlan.hasReadiness || !this.config.readiness) return;
10232
10293
  const translatedHooks = Object.fromEntries(
10233
10294
  getEnabledRuntimeReadinessBindings(readinessCapability, readinessPlan).map((binding) => [
@@ -10429,7 +10490,7 @@ var OpenCodeGenerator = class extends Generator {
10429
10490
  `const isMcpTool = (tool: string): boolean =>`,
10430
10491
  ` tool === "mcp" || tool.startsWith("mcp.") || tool.startsWith("mcp_")`,
10431
10492
  "",
10432
- `const loadUserConfig = (directory: string): { values?: Record<string, string | number | boolean>; env?: Record<string, string> } => {`,
10493
+ `const loadUserConfig = (directory: string): { values?: Record<string, string | number | boolean>; env?: Record<string, string>; envRefs?: Record<string, string> } => {`,
10433
10494
  ` const filepath = resolve(directory, ".pluxx-user.json")`,
10434
10495
  ` if (!existsSync(filepath)) return {}`,
10435
10496
  ` try {`,
@@ -10439,28 +10500,30 @@ var OpenCodeGenerator = class extends Generator {
10439
10500
  ` }`,
10440
10501
  `}`,
10441
10502
  "",
10442
- `const resolveRuntimeValue = (name: string, userEnv: Record<string, string>): string | undefined =>`,
10443
- ` userEnv[name] ?? process.env[name]`,
10503
+ `const resolveRuntimeValue = (name: string, userEnv: Record<string, string>, userEnvRefs: Record<string, string>): string | undefined =>`,
10504
+ ` userEnv[name] ?? (userEnvRefs[name] ? process.env[userEnvRefs[name]] : undefined) ?? process.env[name]`,
10444
10505
  "",
10445
- `const materializeEnv = (input: Record<string, string> | undefined, userEnv: Record<string, string>): Record<string, string> | undefined => {`,
10506
+ `const materializeEnv = (input: Record<string, string> | undefined, userEnv: Record<string, string>, userEnvRefs: Record<string, string>): Record<string, string> | undefined => {`,
10446
10507
  ` if (!input) return undefined`,
10447
10508
  ` const output: Record<string, string> = {}`,
10448
10509
  ` for (const [key, value] of Object.entries(input)) {`,
10449
- ` output[key] = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => resolveRuntimeValue(name, userEnv) ?? \`\\\${\${name}}\`)`,
10510
+ ` output[key] = value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => resolveRuntimeValue(name, userEnv, userEnvRefs) ?? \`\\\${\${name}}\`)`,
10450
10511
  ` }`,
10451
10512
  ` return output`,
10452
10513
  `}`,
10453
10514
  "",
10454
10515
  `const buildMcpConfig = (directory: string): NonNullable<Config["mcp"]> => {`,
10455
10516
  ` const config: NonNullable<Config["mcp"]> = {}`,
10456
- ` const userEnv = loadUserConfig(directory).env ?? {}`,
10517
+ ` const userConfig = loadUserConfig(directory)`,
10518
+ ` const userEnv = userConfig.env ?? {}`,
10519
+ ` const userEnvRefs = userConfig.envRefs ?? {}`,
10457
10520
  "",
10458
10521
  ` for (const [name, definition] of Object.entries(MCP_DEFINITIONS)) {`,
10459
10522
  ` if (definition.transport === "stdio" && definition.command) {`,
10460
10523
  ` config[name] = {`,
10461
10524
  ` type: "local",`,
10462
10525
  ` command: [definition.command, ...(definition.args ?? [])],`,
10463
- ` ...(definition.env ? { environment: materializeEnv(definition.env, userEnv) } : {}),`,
10526
+ ` ...(definition.env ? { environment: materializeEnv(definition.env, userEnv, userEnvRefs) } : {}),`,
10464
10527
  ` }`,
10465
10528
  ` continue`,
10466
10529
  ` }`,
@@ -10477,12 +10540,12 @@ var OpenCodeGenerator = class extends Generator {
10477
10540
  ` }`,
10478
10541
  "",
10479
10542
  ` if (definition.auth?.type === "bearer" && definition.auth.envVar) {`,
10480
- ` const token = resolveRuntimeValue(definition.auth.envVar, userEnv)`,
10543
+ ` const token = resolveRuntimeValue(definition.auth.envVar, userEnv, userEnvRefs)`,
10481
10544
  ` if (token) remote.headers = { Authorization: \`Bearer \${token}\` }`,
10482
10545
  ` }`,
10483
10546
  "",
10484
10547
  ` if (definition.auth?.type === "header" && definition.auth.envVar && definition.auth.headerName && definition.auth.headerTemplate) {`,
10485
- ` const value = resolveRuntimeValue(definition.auth.envVar, userEnv)`,
10548
+ ` const value = resolveRuntimeValue(definition.auth.envVar, userEnv, userEnvRefs)`,
10486
10549
  ` if (value) {`,
10487
10550
  ` remote.headers = {`,
10488
10551
  ` ...(remote.headers ?? {}),`,
@@ -10512,10 +10575,15 @@ var OpenCodeGenerator = class extends Generator {
10512
10575
  ` const shellSingleQuote = (input: string): string => \`'\${String(input ?? "").replace(/'/g, \`'"'"'\`)}'\``,
10513
10576
  "",
10514
10577
  ` const buildHookShellCommand = (rawCommand: string): string => {`,
10515
- ` const userEnv = loadUserConfig(directory).env ?? {}`,
10578
+ ` const userConfig = loadUserConfig(directory)`,
10579
+ ` const userEnv = userConfig.env ?? {}`,
10580
+ ` const userEnvRefs = userConfig.envRefs ?? {}`,
10516
10581
  ` const exports = Object.entries(userEnv)`,
10517
10582
  ` .filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))`,
10518
10583
  ` .map(([key, value]) => \`export \${key}=\${shellSingleQuote(String(value))}\`)`,
10584
+ ` .concat(Object.entries(userEnvRefs)`,
10585
+ ` .filter(([key, envVar]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) && typeof envVar === "string" && envVar in process.env)`,
10586
+ ` .map(([key, envVar]) => \`export \${key}=\${shellSingleQuote(String(process.env[envVar]))}\`))`,
10519
10587
  ` .join("; ")`,
10520
10588
  ` const command = rawCommand.replaceAll("\${PLUGIN_ROOT}", directory)`,
10521
10589
  ` return exports ? \`\${exports}; \${command}\` : command`,
@@ -11313,6 +11381,7 @@ import { existsSync as existsSync18, lstatSync as lstatSync2, readdirSync as rea
11313
11381
  import { resolve as resolve14, relative as relative8, basename as basename5, dirname as dirname4 } from "path";
11314
11382
 
11315
11383
  // src/validation/platform-rules.ts
11384
+ var CORE_FOUR_PLATFORMS = ["claude-code", "cursor", "codex", "opencode"];
11316
11385
  var STANDARD_SKILL_FRONTMATTER = [
11317
11386
  "name",
11318
11387
  "description",
@@ -11441,7 +11510,7 @@ var PLATFORM_LIMIT_POLICIES = {
11441
11510
  },
11442
11511
  hooksFeatureFlag: {
11443
11512
  kind: "hard",
11444
- notes: "Hook support depends on the Codex hooks feature flag/runtime support."
11513
+ notes: "Codex hook support depends on the canonical hooks feature flag plus enabled-plugin, trust/review, and runtime support."
11445
11514
  }
11446
11515
  },
11447
11516
  "cursor": {
@@ -11641,8 +11710,8 @@ var PLATFORM_VALIDATION_RULES = {
11641
11710
  hooks: {
11642
11711
  supported: true,
11643
11712
  files: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
11644
- eventNames: ["SessionStart", "PreToolUse", "PermissionRequest", "PostToolUse", "UserPromptSubmit", "Stop"],
11645
- notes: "Codex documents both project or user hook config and plugin-bundled hooks. Plugin-bundled hooks live at `hooks/hooks.json` in the plugin and require `[features].plugin_hooks = true`; the general `[features].hooks = true` flag covers non-plugin hook config and defaults on. `codex_hooks` is deprecated and should not be treated as a plugin-bundled hook fallback."
11713
+ eventNames: [...CODEX_SUPPORTED_HOOK_EVENTS],
11714
+ notes: "Codex documents both project/user hook config and plugin-bundled hooks. Plugin-bundled hooks live at `hooks/hooks.json` in the plugin and require the canonical `[features].hooks = true` flag, enabled plugin state, user review, trust, and runtime support. `codex_hooks` is deprecated and should not be treated as the current hook feature key."
11646
11715
  },
11647
11716
  instructions: {
11648
11717
  files: ["AGENTS.md", "AGENTS.override.md"],
@@ -12081,7 +12150,7 @@ var CORE_FOUR_PRIMITIVE_CAPABILITIES = {
12081
12150
  hooks: {
12082
12151
  mode: "translate",
12083
12152
  nativeSurfaces: ["hooks/hooks.json", ".codex/hooks.json", "~/.codex/hooks.json"],
12084
- notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin with the documented `[features].plugin_hooks = true` plugin gate, and still tracks the broader project/user hook config paths where `[features].hooks` is the general flag and `codex_hooks` is deprecated."
12153
+ notes: "Hooks are native. Pluxx bundles translated Codex hooks in the plugin with the documented `[features].hooks = true` feature key, and still tracks broader project/user hook config paths where `codex_hooks` is deprecated. Plugin-bundled execution still also depends on enabled plugin state, review, trust, and runtime support."
12085
12154
  },
12086
12155
  permissions: {
12087
12156
  mode: "translate",
@@ -12152,7 +12221,6 @@ function getCoreFourPrimitiveCapabilities(platform) {
12152
12221
  }
12153
12222
 
12154
12223
  // src/cli/primitive-summary.ts
12155
- var CORE_FOUR_ORDER = ["claude-code", "cursor", "codex", "opencode"];
12156
12224
  var TARGET_LABELS = {
12157
12225
  "claude-code": "claude",
12158
12226
  cursor: "cursor",
@@ -12166,7 +12234,7 @@ var MODE_LABELS = {
12166
12234
  drop: "drop"
12167
12235
  };
12168
12236
  function buildPrimitiveTranslationSummary(config, targets = config.targets) {
12169
- const selectedTargets = CORE_FOUR_ORDER.filter((target) => targets.includes(target));
12237
+ const selectedTargets = CORE_FOUR_PLATFORMS.filter((target) => targets.includes(target));
12170
12238
  if (selectedTargets.length === 0) return void 0;
12171
12239
  const configuredBuckets = getConfiguredCompilerBuckets({
12172
12240
  ...config,
@@ -13356,7 +13424,7 @@ function lintCodexHooksExternalConfig(config, issues) {
13356
13424
  pushIssue(issues, {
13357
13425
  level: "warning",
13358
13426
  code: "codex-hooks-external-config",
13359
- message: `Pluxx now bundles Codex hooks at \`hooks/hooks.json\`, and Codex plugin-bundled hook loading requires \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`. The general \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` flag covers non-plugin hook config and defaults on; \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as a plugin-bundled hook fallback. If bundled hooks do not activate, enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\`, reload Codex, and retest in a trusted interactive session.`,
13427
+ message: `Pluxx now bundles Codex hooks at \`hooks/hooks.json\`, and current Codex docs use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` as the canonical hook feature key. \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as the current hook feature key. If bundled hooks do not activate, enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\`, reload Codex, and retest in a trusted interactive session.`,
13360
13428
  file: "pluxx.config.ts",
13361
13429
  platform: "Codex"
13362
13430
  });
@@ -13365,7 +13433,7 @@ function lintCodexHooksExternalConfig(config, issues) {
13365
13433
  pushIssue(issues, {
13366
13434
  level: "warning",
13367
13435
  code: "codex-hooks-legacy-feature-flag",
13368
- message: `\`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated for general Codex hook config and should not be treated as a plugin-bundled hook fallback. Keep it only as a compatibility fallback for non-plugin hook wiring, prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` there, and use \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` for plugin-bundled hooks.`,
13436
+ message: `\`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated for Codex hook config. Prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`.`,
13369
13437
  file: "pluxx.config.ts",
13370
13438
  platform: "Codex"
13371
13439
  });
@@ -13374,7 +13442,7 @@ function lintCodexHooksExternalConfig(config, issues) {
13374
13442
  pushIssue(issues, {
13375
13443
  level: "warning",
13376
13444
  code: "codex-hooks-general-feature-flag-only",
13377
- message: `This Codex config enables only general hook flags. Plugin-bundled hooks at \`hooks/hooks.json\` still need \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`; \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` and \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` alone are not sufficient.`,
13445
+ message: `This Codex config enables only deprecated hook flags. Plugin-bundled hooks at \`hooks/hooks.json\` should use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`.`,
13378
13446
  file: "pluxx.config.ts",
13379
13447
  platform: "Codex"
13380
13448
  });
@@ -17987,10 +18055,10 @@ ${additions.map((block) => `- ${block.replace(/\n/g, "\n ")}`).join("\n")}
17987
18055
  import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
17988
18056
  import { accessSync, constants, existsSync as existsSync24, lstatSync as lstatSync3, readFileSync as readFileSync14, readdirSync as readdirSync10, realpathSync } from "fs";
17989
18057
  import { homedir as homedir2 } from "os";
17990
- import { basename as basename6, dirname as dirname7, resolve as resolve20 } from "path";
18058
+ import { basename as basename7, dirname as dirname7, relative as relative12, resolve as resolve20 } from "path";
17991
18059
 
17992
18060
  // src/cli/install.ts
17993
- import { resolve as resolve19, dirname as dirname6 } from "path";
18061
+ import { resolve as resolve19, dirname as dirname6, basename as basename6, relative as relative11 } from "path";
17994
18062
  import { existsSync as existsSync23, symlinkSync, mkdirSync as mkdirSync4, rmSync as rmSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync4, cpSync as cpSync3, readdirSync as readdirSync9 } from "fs";
17995
18063
  import { spawnSync } from "child_process";
17996
18064
  import * as readline2 from "readline";
@@ -18474,19 +18542,25 @@ function createOpenCodeCopiedInstall(target, pluginName) {
18474
18542
  writeOpenCodeEntryFile(target.pluginDir, pluginName);
18475
18543
  syncOpenCodeSkills(target.pluginDir, pluginName);
18476
18544
  }
18477
- function materializeTemplateValue(value, env) {
18478
- return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => env[name] ?? `\${${name}}`);
18545
+ function materializeTemplateValue(value, env, secretEnvVars = /* @__PURE__ */ new Set()) {
18546
+ return value.replace(
18547
+ /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
18548
+ (_match, name) => secretEnvVars.has(name) ? `\${${name}}` : env[name] ?? `\${${name}}`
18549
+ );
18479
18550
  }
18480
- function materializeEnvRecord(input, env) {
18551
+ function materializeEnvRecord(input, env, secretEnvVars = /* @__PURE__ */ new Set()) {
18481
18552
  const output = {};
18482
18553
  for (const [key, value] of Object.entries(input ?? {})) {
18483
- output[key] = materializeTemplateValue(value, env);
18554
+ output[key] = materializeTemplateValue(value, env, secretEnvVars);
18484
18555
  }
18485
18556
  return output;
18486
18557
  }
18487
18558
  function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
18488
18559
  if (!config.mcp) return;
18489
18560
  const env = buildUserConfigEnvMap(entries);
18561
+ const secretEnvVars = new Set(
18562
+ entries.filter((entry) => entry.field.type === "secret" && entry.envVar).map((entry) => entry.envVar)
18563
+ );
18490
18564
  if (platform === "claude-code" || platform === "cursor") {
18491
18565
  const filepath = resolve19(pluginDir, platform === "claude-code" ? ".mcp.json" : "mcp.json");
18492
18566
  if (!existsSync23(filepath)) return;
@@ -18531,7 +18605,7 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
18531
18605
  mcpServers[name] = {
18532
18606
  command: materializeInstalledPluginOwnedStdioPathForPlatform(server.command, platform, pluginDir),
18533
18607
  args: (server.args ?? []).map((value) => materializeInstalledPluginOwnedStdioPathForPlatform(value, platform, pluginDir)),
18534
- env: materializeEnvRecord(server.env, env)
18608
+ env: materializeEnvRecord(server.env, env, secretEnvVars)
18535
18609
  };
18536
18610
  continue;
18537
18611
  }
@@ -18541,49 +18615,46 @@ function patchInstalledMcpConfig(pluginDir, platform, config, entries) {
18541
18615
  const nativeOverride = getNativeCodexMcpEntryOverride(config, name);
18542
18616
  if (nativeOverride) {
18543
18617
  if (typeof nativeOverride.bearer_token_env_var === "string") {
18544
- const envVar = nativeOverride.bearer_token_env_var;
18545
- if (env[envVar]) {
18546
- entry.bearer_token_env_var = envVar;
18547
- }
18618
+ entry.bearer_token_env_var = nativeOverride.bearer_token_env_var;
18548
18619
  }
18549
18620
  if (nativeOverride.auth) {
18550
18621
  entry.auth = nativeOverride.auth;
18551
18622
  }
18552
18623
  if (nativeOverride.env_http_headers && typeof nativeOverride.env_http_headers === "object") {
18553
- entry.http_headers = Object.fromEntries(
18554
- Object.entries(nativeOverride.env_http_headers).filter(([, value]) => typeof value === "string").map(([headerName, envVar]) => [
18555
- headerName,
18556
- env[envVar] ?? `\${${envVar}}`
18557
- ])
18624
+ entry.env_http_headers = Object.fromEntries(
18625
+ Object.entries(nativeOverride.env_http_headers).filter(([, value]) => typeof value === "string").map(([headerName, envVar]) => [headerName, envVar])
18558
18626
  );
18559
18627
  }
18560
18628
  if (nativeOverride.http_headers && typeof nativeOverride.http_headers === "object") {
18561
18629
  entry.http_headers = materializeEnvRecord(
18562
18630
  nativeOverride.http_headers,
18563
- env
18631
+ env,
18632
+ secretEnvVars
18564
18633
  );
18565
18634
  }
18566
- } else if (server.auth?.type === "bearer" && server.auth.envVar && env[server.auth.envVar]) {
18567
- entry.http_headers = {
18568
- Authorization: `Bearer ${env[server.auth.envVar]}`
18569
- };
18570
- } else if (server.auth?.type === "header" && server.auth.envVar && env[server.auth.envVar]) {
18571
- entry.http_headers = {
18572
- [server.auth.headerName]: server.auth.headerTemplate.replace("${value}", env[server.auth.envVar])
18573
- };
18635
+ } else if (server.auth?.type === "bearer" && server.auth.envVar) {
18636
+ entry.bearer_token_env_var = server.auth.envVar;
18637
+ } else if (server.auth?.type === "header" && server.auth.envVar) {
18638
+ const isBearerAuthorizationHeader = server.auth.headerName === "Authorization" && server.auth.headerTemplate === "Bearer ${value}";
18639
+ if (isBearerAuthorizationHeader) {
18640
+ entry.bearer_token_env_var = server.auth.envVar;
18641
+ } else if (server.auth.headerTemplate === "${value}") {
18642
+ entry.env_http_headers = {
18643
+ [server.auth.headerName]: server.auth.envVar
18644
+ };
18645
+ }
18574
18646
  }
18575
18647
  mcpServers[name] = entry;
18576
18648
  }
18577
18649
  writeFileSync4(filepath, JSON.stringify({ mcpServers }, null, 2) + "\n");
18578
18650
  }
18579
18651
  }
18580
- function writeInstalledUserConfig(pluginDir, entries) {
18652
+ function writeInstalledUserConfig(pluginDir, entries, platform) {
18581
18653
  if (entries.length === 0) return;
18582
18654
  const filepath = resolve19(pluginDir, ".pluxx-user.json");
18583
- const payload = {
18584
- values: buildUserConfigValueMap(entries),
18585
- env: buildUserConfigEnvMap(entries)
18586
- };
18655
+ const payload = buildInstalledUserConfigPayload(entries, {
18656
+ preserveSecretReferences: platform === "codex"
18657
+ });
18587
18658
  writeFileSync4(filepath, JSON.stringify(payload, null, 2) + "\n");
18588
18659
  }
18589
18660
  function disableInstalledEnvValidation(pluginDir, entries) {
@@ -18597,7 +18668,7 @@ function disableInstalledEnvValidation(pluginDir, entries) {
18597
18668
  }
18598
18669
  function materializeInstalledPlugin(pluginDir, platform, config, entries) {
18599
18670
  if (entries.length > 0) {
18600
- writeInstalledUserConfig(pluginDir, entries);
18671
+ writeInstalledUserConfig(pluginDir, entries, platform);
18601
18672
  disableInstalledEnvValidation(pluginDir, entries);
18602
18673
  }
18603
18674
  patchInstalledMcpConfig(pluginDir, platform, config, entries);
@@ -18676,7 +18747,7 @@ function resolveBundleReference(rootDir, value) {
18676
18747
  if (isRelativeBundlePath(value)) {
18677
18748
  return resolve19(rootDir, value);
18678
18749
  }
18679
- const pluginRootMatch = value.match(/^\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/](.+)$/);
18750
+ const pluginRootMatch = value.match(/^\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|CODEX_PLUGIN_ROOT|OPENCODE_PLUGIN_ROOT|PLUGIN_ROOT|PLUXX_PLUGIN_ROOT)\}[\\/](.+)$/);
18680
18751
  if (pluginRootMatch) {
18681
18752
  return resolve19(rootDir, pluginRootMatch[1]);
18682
18753
  }
@@ -18719,9 +18790,43 @@ function collectHookCommandStrings(value, commands) {
18719
18790
  }
18720
18791
  }
18721
18792
  function extractBundleCommandTargets(command2) {
18722
- const matches = command2.match(/\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|PLUGIN_ROOT)\}[\\/][^\s"'`;$|&<>]+|\.\.?[\\/][^\s"'`;$|&<>]+/g);
18793
+ const matches = command2.match(/\$\{(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|CODEX_PLUGIN_ROOT|OPENCODE_PLUGIN_ROOT|PLUGIN_ROOT|PLUXX_PLUGIN_ROOT)\}[\\/][^\s"'`;$|&<>]+|\.\.?[\\/][^\s"'`;$|&<>]+/g);
18723
18794
  return matches ?? [];
18724
18795
  }
18796
+ function extractRelativeBundleCommandTargets(command2) {
18797
+ return extractBundleCommandTargets(command2).filter(isRelativeBundlePath);
18798
+ }
18799
+ function commandChangesToKnownPluginRoot(command2) {
18800
+ return /\bcd\s+["']?\$\{?(?:CLAUDE_PLUGIN_ROOT|CURSOR_PLUGIN_ROOT|CODEX_PLUGIN_ROOT|OPENCODE_PLUGIN_ROOT|PLUGIN_ROOT|PLUXX_PLUGIN_ROOT)\}?/.test(command2);
18801
+ }
18802
+ function formatBundleRelativePath(rootDir, filePath) {
18803
+ const relativePath = relative11(rootDir, filePath);
18804
+ return relativePath && !relativePath.startsWith("..") ? relativePath : filePath;
18805
+ }
18806
+ function findHookWrapperPaths(rootDir, command2) {
18807
+ return extractBundleCommandTargets(command2).map((target) => resolveBundleReference(rootDir, target)).filter((target) => {
18808
+ if (!target) return false;
18809
+ return existsSync23(target) && basename6(target).startsWith("pluxx-hook-command-") && target.endsWith(".sh");
18810
+ });
18811
+ }
18812
+ function findCodexCwdUnsafeHookCommands(rootDir, commands) {
18813
+ const issues = /* @__PURE__ */ new Set();
18814
+ for (const command2 of commands) {
18815
+ const relativeTargets = extractRelativeBundleCommandTargets(command2);
18816
+ if (relativeTargets.length > 0 && !commandChangesToKnownPluginRoot(command2)) {
18817
+ issues.add(`${command2} uses cwd-relative bundle target(s): ${relativeTargets.join(", ")}`);
18818
+ }
18819
+ for (const wrapperPath of findHookWrapperPaths(rootDir, command2)) {
18820
+ const wrapper = readFileSync13(wrapperPath, "utf-8");
18821
+ const hookCommand = wrapper.match(/^PLUXX_HOOK_COMMAND=(.*)$/m)?.[1] ?? "";
18822
+ const wrapperRelativeTargets = extractRelativeBundleCommandTargets(hookCommand);
18823
+ if (wrapperRelativeTargets.length > 0 && !commandChangesToKnownPluginRoot(wrapper)) {
18824
+ issues.add(`${formatBundleRelativePath(rootDir, wrapperPath)} evaluates cwd-relative bundle target(s): ${wrapperRelativeTargets.join(", ")}`);
18825
+ }
18826
+ }
18827
+ }
18828
+ return [...issues].sort();
18829
+ }
18725
18830
  function resolveInstalledHooksReference(rootDir, platform, manifest) {
18726
18831
  const manifestReference = typeof manifest.hooks === "string" ? manifest.hooks : void 0;
18727
18832
  if (manifestReference) {
@@ -18759,6 +18864,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18759
18864
  return {
18760
18865
  missingManifestPaths: [],
18761
18866
  missingHookTargets: [],
18867
+ cwdUnsafeHookCommands: [],
18762
18868
  invalidRuntimeScripts: []
18763
18869
  };
18764
18870
  }
@@ -18768,6 +18874,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18768
18874
  manifestIssue: `missing plugin manifest at ${manifestPath}`,
18769
18875
  missingManifestPaths: [],
18770
18876
  missingHookTargets: [],
18877
+ cwdUnsafeHookCommands: [],
18771
18878
  invalidRuntimeScripts: []
18772
18879
  };
18773
18880
  }
@@ -18779,6 +18886,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18779
18886
  manifestIssue: `plugin manifest at ${manifestPath} is not parseable: ${error instanceof Error ? error.message : String(error)}`,
18780
18887
  missingManifestPaths: [],
18781
18888
  missingHookTargets: [],
18889
+ cwdUnsafeHookCommands: [],
18782
18890
  invalidRuntimeScripts: []
18783
18891
  };
18784
18892
  }
@@ -18793,6 +18901,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18793
18901
  ...manifestIssue ? { manifestIssue } : {},
18794
18902
  missingManifestPaths,
18795
18903
  missingHookTargets: [],
18904
+ cwdUnsafeHookCommands: [],
18796
18905
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
18797
18906
  };
18798
18907
  }
@@ -18801,6 +18910,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18801
18910
  ...manifestIssue ? { manifestIssue } : {},
18802
18911
  missingManifestPaths,
18803
18912
  missingHookTargets: [],
18913
+ cwdUnsafeHookCommands: [],
18804
18914
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
18805
18915
  };
18806
18916
  }
@@ -18814,10 +18924,12 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18814
18924
  return resolved !== void 0 && !existsSync23(resolved);
18815
18925
  })
18816
18926
  )].sort();
18927
+ const cwdUnsafeHookCommands = platform === "codex" ? findCodexCwdUnsafeHookCommands(rootDir, commands) : [];
18817
18928
  return {
18818
18929
  ...manifestIssue ? { manifestIssue } : {},
18819
18930
  missingManifestPaths,
18820
18931
  missingHookTargets,
18932
+ cwdUnsafeHookCommands,
18821
18933
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
18822
18934
  };
18823
18935
  } catch (error) {
@@ -18826,6 +18938,7 @@ function findInstalledBundleIntegrityIssues(rootDir, platform) {
18826
18938
  hookConfigIssue: `hooks config at ${hooksReference} is not parseable: ${error instanceof Error ? error.message : String(error)}`,
18827
18939
  missingManifestPaths,
18828
18940
  missingHookTargets: [],
18941
+ cwdUnsafeHookCommands: [],
18829
18942
  invalidRuntimeScripts: findInstalledRuntimeScriptIssues(rootDir, manifest)
18830
18943
  };
18831
18944
  }
@@ -18875,6 +18988,9 @@ function assertInstalledBundleIntegrity(rootDir, platform, label) {
18875
18988
  if (issues.missingHookTargets.length > 0) {
18876
18989
  details.push(`hook targets missing: ${issues.missingHookTargets.join(", ")}`);
18877
18990
  }
18991
+ if (issues.cwdUnsafeHookCommands.length > 0) {
18992
+ details.push(`hook commands require plugin-root cwd: ${issues.cwdUnsafeHookCommands.join("; ")}`);
18993
+ }
18878
18994
  if (issues.invalidRuntimeScripts.length > 0) {
18879
18995
  details.push(`runtime script issues: ${issues.invalidRuntimeScripts.join(", ")}`);
18880
18996
  }
@@ -19119,6 +19235,13 @@ var LOW_INFO_DESCRIPTION_PATTERNS = [
19119
19235
  var MATERIALIZED_ENV_MARKER = "materialized required config";
19120
19236
  var MIN_NODE_MAJOR = 18;
19121
19237
  var STDIO_LAUNCH_SMOKE_TIMEOUT_MS = 1200;
19238
+ var MAX_SECRET_SCAN_FILE_BYTES = 512 * 1024;
19239
+ var LEGACY_SECRET_IDENTIFIER_PATTERN = /(^|[-_])(api[-_]?key|secret|token|password|credential)([-_]|$)/i;
19240
+ var TEST_SECRET_SENTINELS = [
19241
+ { pattern: /\bshh-secret\b/i, label: "test secret sentinel" },
19242
+ { pattern: /\bsecret-key\b/i, label: "test secret sentinel" },
19243
+ { pattern: /\bliteral-secret-value-that-should-not-copy\b/i, label: "test secret sentinel" }
19244
+ ];
19122
19245
  var PRIMITIVE_MODE_LEVEL = {
19123
19246
  preserve: "success",
19124
19247
  translate: "info",
@@ -19131,6 +19254,106 @@ function renderInstalledPluginRoot(value, rootDir) {
19131
19254
  function addCheck2(checks, check) {
19132
19255
  checks.push(check);
19133
19256
  }
19257
+ function looksLegacySecretIdentifier(value) {
19258
+ return LEGACY_SECRET_IDENTIFIER_PATTERN.test(
19259
+ value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[._/\s]+/g, "-")
19260
+ );
19261
+ }
19262
+ function walkInstalledBundleFiles(rootDir, currentDir = rootDir) {
19263
+ const files = [];
19264
+ for (const entry of readdirSync10(currentDir, { withFileTypes: true })) {
19265
+ const absolutePath = resolve20(currentDir, entry.name);
19266
+ if (entry.isDirectory()) {
19267
+ files.push(...walkInstalledBundleFiles(rootDir, absolutePath));
19268
+ continue;
19269
+ }
19270
+ if (!entry.isFile()) continue;
19271
+ files.push(relative12(rootDir, absolutePath).replace(/\\/g, "/"));
19272
+ }
19273
+ return files.sort();
19274
+ }
19275
+ function readInstalledTextFile(rootDir, relativePath) {
19276
+ const absolutePath = resolve20(rootDir, relativePath);
19277
+ const content = readFileSync14(absolutePath);
19278
+ if (content.length > MAX_SECRET_SCAN_FILE_BYTES) return null;
19279
+ if (content.includes(0)) return null;
19280
+ return content.toString("utf-8");
19281
+ }
19282
+ function collectInstalledPlaintextSecretCandidates(rootDir) {
19283
+ const userConfigPath = resolve20(rootDir, ".pluxx-user.json");
19284
+ if (!existsSync24(userConfigPath)) return [];
19285
+ try {
19286
+ const payload = JSON.parse(readFileSync14(userConfigPath, "utf-8"));
19287
+ if (payload.secretStorage === "materialized") return [];
19288
+ const secretKeys = new Set(Array.isArray(payload.secretKeys) ? payload.secretKeys.filter((value) => typeof value === "string") : []);
19289
+ const secretEnv = new Set(Array.isArray(payload.secretEnv) ? payload.secretEnv.filter((value) => typeof value === "string") : []);
19290
+ const hasSecretMetadata = secretKeys.size > 0 || secretEnv.size > 0;
19291
+ const candidateLabels = /* @__PURE__ */ new Map();
19292
+ const recordCandidate = (rawValue, label) => {
19293
+ if (typeof rawValue !== "string") return;
19294
+ const value = rawValue.trim();
19295
+ if (!value) return;
19296
+ if (extractEnvReference(value)) return;
19297
+ if (isPlaceholderSecretValue(value)) return;
19298
+ const labels = candidateLabels.get(value) ?? /* @__PURE__ */ new Set();
19299
+ labels.add(label);
19300
+ candidateLabels.set(value, labels);
19301
+ };
19302
+ for (const [key, value] of Object.entries(payload.values ?? {})) {
19303
+ if (hasSecretMetadata ? !secretKeys.has(key) : !looksLegacySecretIdentifier(key)) continue;
19304
+ recordCandidate(value, `userConfig "${key}"`);
19305
+ }
19306
+ for (const [key, value] of Object.entries(payload.env ?? {})) {
19307
+ if (hasSecretMetadata ? !secretEnv.has(key) : !looksLegacySecretIdentifier(key)) continue;
19308
+ recordCandidate(value, `env "${key}"`);
19309
+ }
19310
+ return [...candidateLabels.entries()].map(([value, labels]) => ({
19311
+ value,
19312
+ labels: [...labels].sort()
19313
+ }));
19314
+ } catch {
19315
+ return [];
19316
+ }
19317
+ }
19318
+ function checkInstalledPlaintextSecrets(checks, rootDir) {
19319
+ const literalCandidates = collectInstalledPlaintextSecretCandidates(rootDir);
19320
+ const findings = /* @__PURE__ */ new Map();
19321
+ for (const relativePath of walkInstalledBundleFiles(rootDir)) {
19322
+ const content = readInstalledTextFile(rootDir, relativePath);
19323
+ if (!content) continue;
19324
+ const labels = /* @__PURE__ */ new Set();
19325
+ for (const candidate of literalCandidates) {
19326
+ if (!content.includes(candidate.value)) continue;
19327
+ for (const label of candidate.labels) labels.add(label);
19328
+ }
19329
+ for (const sentinel of TEST_SECRET_SENTINELS) {
19330
+ if (sentinel.pattern.test(content)) {
19331
+ labels.add(sentinel.label);
19332
+ }
19333
+ }
19334
+ if (labels.size > 0) {
19335
+ findings.set(relativePath, labels);
19336
+ }
19337
+ }
19338
+ if (findings.size === 0) {
19339
+ addCheck2(checks, {
19340
+ level: "success",
19341
+ code: "consumer-plaintext-secret-absent",
19342
+ title: "Installed bundle does not expose known plaintext secret material",
19343
+ detail: "No known prompted secret values or maintained test-secret sentinels were detected in scanned installed text files.",
19344
+ fix: "No action needed."
19345
+ });
19346
+ return;
19347
+ }
19348
+ const detail = [...findings.entries()].map(([path, labels]) => `${path} (${[...labels].sort().join(", ")})`).join("; ");
19349
+ addCheck2(checks, {
19350
+ level: "error",
19351
+ code: "consumer-plaintext-secret-leak",
19352
+ title: "Installed bundle contains plaintext secret material",
19353
+ detail: `Detected plaintext secret material in installed files: ${detail}.`,
19354
+ fix: "Rotate the affected credential, remove the leaking install, reinstall after the Pluxx secret-reference fix, and rerun pluxx doctor --consumer."
19355
+ });
19356
+ }
19134
19357
  function summarizeChecks2(checks) {
19135
19358
  const visibleChecks = checks.filter((check) => !check.code.startsWith("primitive-"));
19136
19359
  const errors = visibleChecks.filter((check) => check.level === "error").length;
@@ -19755,7 +19978,7 @@ function readCodexHooksFeatureFlag(filePath) {
19755
19978
  inFeaturesTable = sectionMatch[1].trim() === "features";
19756
19979
  continue;
19757
19980
  }
19758
- const dottedFeatureMatch = line.match(/^features\.(plugin_hooks|hooks|codex_hooks)\s*=\s*(.+)$/);
19981
+ const dottedFeatureMatch = line.match(/^features\.(hooks|codex_hooks)\s*=\s*(.+)$/);
19759
19982
  if (dottedFeatureMatch) {
19760
19983
  assignFeatureFlag(dottedFeatureMatch[1], dottedFeatureMatch[2]);
19761
19984
  continue;
@@ -19772,7 +19995,7 @@ function readCodexHooksFeatureFlag(filePath) {
19772
19995
  continue;
19773
19996
  }
19774
19997
  if (!inFeaturesTable) continue;
19775
- const featureMatch = line.match(/^(plugin_hooks|hooks|codex_hooks)\s*=\s*(.+)$/);
19998
+ const featureMatch = line.match(/^(hooks|codex_hooks)\s*=\s*(.+)$/);
19776
19999
  if (!featureMatch) continue;
19777
20000
  assignFeatureFlag(featureMatch[1], featureMatch[2]);
19778
20001
  }
@@ -19961,7 +20184,7 @@ function listClaudeSettingsCandidates(projectRoot) {
19961
20184
  });
19962
20185
  }
19963
20186
  function readClaudeDisableAllHooks(filePath) {
19964
- const parsed = readJsonFile2(dirname7(filePath), basename6(filePath));
20187
+ const parsed = readJsonFile2(dirname7(filePath), basename7(filePath));
19965
20188
  return typeof parsed.disableAllHooks === "boolean" ? parsed.disableAllHooks : void 0;
19966
20189
  }
19967
20190
  function probeClaudeDisableAllHooks(projectRoot) {
@@ -20304,6 +20527,9 @@ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
20304
20527
  if (issues.missingHookTargets.length > 0) {
20305
20528
  details.push(`hook commands reference missing bundle target${issues.missingHookTargets.length === 1 ? "" : "s"}: ${issues.missingHookTargets.join(", ")}`);
20306
20529
  }
20530
+ if (issues.cwdUnsafeHookCommands.length > 0) {
20531
+ details.push(`Codex hook command${issues.cwdUnsafeHookCommands.length === 1 ? " depends" : "s depend"} on plugin-root cwd: ${issues.cwdUnsafeHookCommands.join("; ")}`);
20532
+ }
20307
20533
  if (issues.invalidRuntimeScripts.length > 0) {
20308
20534
  details.push(`runtime startup still depends on installer-owned validation: ${issues.invalidRuntimeScripts.join(", ")}`);
20309
20535
  }
@@ -20323,7 +20549,7 @@ function checkInstalledBundleIntegrity(checks, rootDir, layout) {
20323
20549
  code: "consumer-bundle-integrity-invalid",
20324
20550
  title: "Installed bundle integrity is broken",
20325
20551
  detail: details.join("; "),
20326
- fix: "Reinstall the plugin or rebuild the bundle so every manifest path, hook config, and hook target is valid inside the installed plugin.",
20552
+ fix: "Reinstall the plugin or rebuild the bundle so every manifest path, hook config, and hook command resolves from the installed plugin root.",
20327
20553
  path: layout.manifestPath
20328
20554
  });
20329
20555
  }
@@ -20388,31 +20614,31 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20388
20614
  const enabledProbes = probes.filter((probe) => probe.enabled);
20389
20615
  const checkedPaths = probes.map((probe) => `${probe.scope} ${probe.exists ? "config" : "path"}: ${probe.path}${probe.exists ? "" : " (missing)"}`).join("; ");
20390
20616
  const describeEnabledFlags = (probe) => {
20391
- const enabledFlags = [];
20392
- if (probe.pluginBundledEnabled) enabledFlags.push(PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG);
20393
- if (probe.recommendedEnabled) enabledFlags.push(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
20394
- if (probe.alternateEnabled) enabledFlags.push(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
20395
- return enabledFlags.join(" + ");
20617
+ const enabledFlags = /* @__PURE__ */ new Set();
20618
+ if (probe.pluginBundledEnabled) enabledFlags.add(PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG);
20619
+ if (probe.recommendedEnabled) enabledFlags.add(RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG);
20620
+ if (probe.alternateEnabled) enabledFlags.add(ALTERNATE_CODEX_HOOKS_FEATURE_FLAG);
20621
+ return [...enabledFlags].join(" + ");
20396
20622
  };
20397
20623
  const generalOnlyProbes = probes.filter((probe) => !probe.pluginBundledEnabled && (probe.recommendedEnabled || probe.alternateEnabled));
20398
20624
  if (generalOnlyProbes.length > 0) {
20399
20625
  addCheck2(checks, {
20400
20626
  level: "warning",
20401
20627
  code: "consumer-codex-plugin-hooks-feature-flag-general-only",
20402
- title: "Codex config enables only general hook flags, not the plugin-bundled hook gate",
20403
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but ${generalOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")} enables only general hook flags (${generalOnlyProbes.map((probe) => describeEnabledFlags(probe)).join(" and ")}). Those general flags do not activate plugin-bundled hooks by themselves.`,
20404
- fix: `Enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, reload Codex, and rerun pluxx verify-install.`,
20628
+ title: "Codex config enables only deprecated hook flags",
20629
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but ${generalOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")} enables only deprecated hook flags (${generalOnlyProbes.map((probe) => describeEnabledFlags(probe)).join(" and ")}). Current Codex docs use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` as the canonical feature key.`,
20630
+ fix: `Enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the active Codex config, reload Codex, and rerun pluxx verify-install.`,
20405
20631
  path: generalOnlyProbes[0].path
20406
20632
  });
20407
20633
  }
20408
- const legacyOnlyProbes = probes.filter((probe) => probe.alternateEnabled && !probe.recommendedEnabled);
20634
+ const legacyOnlyProbes = probes.filter((probe) => !probe.pluginBundledEnabled && probe.alternateEnabled && !probe.recommendedEnabled);
20409
20635
  if (legacyOnlyProbes.length > 0) {
20410
20636
  addCheck2(checks, {
20411
20637
  level: "warning",
20412
20638
  code: "consumer-codex-hooks-feature-flag-legacy-only",
20413
20639
  title: "Codex config still uses the deprecated general hook compatibility flag",
20414
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the checked Codex config enables only the deprecated general hook flag \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` in ${legacyOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")}. That flag is not the plugin-bundled hook gate.`,
20415
- fix: `Prefer \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` only for non-plugin hook config if needed, and use \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` for plugin-bundled hooks. Reload Codex and rerun pluxx verify-install after updating the active config.`,
20640
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the checked Codex config enables only the deprecated hook flag \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG} = true\` in ${legacyOnlyProbes.map((probe) => `${probe.scope} config ${probe.path}`).join(" and ")}. Current Codex docs use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` as the canonical feature key.`,
20641
+ fix: `Use \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\`, reload Codex, and rerun pluxx verify-install after updating the active config.`,
20416
20642
  path: legacyOnlyProbes[0].path
20417
20643
  });
20418
20644
  }
@@ -20420,8 +20646,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20420
20646
  addCheck2(checks, {
20421
20647
  level: "success",
20422
20648
  code: "consumer-codex-hooks-feature-flag-enabled",
20423
- title: "Codex plugin-bundled hook feature flag found for this install",
20424
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the plugin hook gate was found in ${enabledProbes.map((probe) => `${probe.scope} config ${probe.path} (${describeEnabledFlags(probe)})`).join(" and ")}. Treat that as a prerequisite, not proof of live hook execution.`,
20649
+ title: "Codex hook feature flag found for this install",
20650
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, and the canonical hook feature key was found in ${enabledProbes.map((probe) => `${probe.scope} config ${probe.path} (${describeEnabledFlags(probe)})`).join(" and ")}. Treat that as a prerequisite, not proof of live hook execution.`,
20425
20651
  fix: "No action needed.",
20426
20652
  path: enabledProbes[0].path
20427
20653
  });
@@ -20432,8 +20658,8 @@ function checkInstalledCodexHooksFeatureFlag(checks, rootDir, layout, options) {
20432
20658
  level: "warning",
20433
20659
  code: "consumer-codex-hooks-feature-flag-missing",
20434
20660
  title: "Codex plugin-bundled hook activation is missing its known feature-gate prerequisite",
20435
- detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` was not found in the checked Codex config layers. The general \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG}\` flag covers non-plugin hook config and defaults on, while \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as a plugin-bundled hook fallback. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""}`,
20436
- fix: `Enable \`${PLUGIN_BUNDLED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the relevant project or user Codex config, reload Codex, and rerun pluxx verify-install.`,
20661
+ detail: `This installed Codex bundle declares plugin-bundled hooks at ${hooksReference}, but \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` was not found in the checked Codex config layers. \`${ALTERNATE_CODEX_HOOKS_FEATURE_FLAG}\` is deprecated and should not be treated as the current hook feature key. Checked ${checkedPaths}.${parseErrors.length > 0 ? ` Unparseable config: ${parseErrors.join("; ")}.` : ""}`,
20662
+ fix: `Enable \`${RECOMMENDED_CODEX_HOOKS_FEATURE_FLAG} = true\` under \`[features]\` in the relevant project or user Codex config, reload Codex, and rerun pluxx verify-install.`,
20437
20663
  path: probes.find((probe) => probe.exists)?.path ?? hooksReference
20438
20664
  });
20439
20665
  }
@@ -20796,7 +21022,7 @@ function isLikelyLocalRuntimePath3(value) {
20796
21022
  function isLikelyOpenCodeInstallPath(rootDir) {
20797
21023
  const parent = dirname7(rootDir);
20798
21024
  const grandparent = dirname7(parent);
20799
- return basename6(parent) === "plugins" && basename6(grandparent) === "opencode";
21025
+ return basename7(parent) === "plugins" && basename7(grandparent) === "opencode";
20800
21026
  }
20801
21027
  function checkInstalledOpenCodeHostBridge(checks, rootDir) {
20802
21028
  if (!isLikelyOpenCodeInstallPath(rootDir)) {
@@ -20810,7 +21036,7 @@ function checkInstalledOpenCodeHostBridge(checks, rootDir) {
20810
21036
  });
20811
21037
  return;
20812
21038
  }
20813
- const pluginName = basename6(rootDir);
21039
+ const pluginName = basename7(rootDir);
20814
21040
  const entryPath = `${rootDir}.ts`;
20815
21041
  const entryRelativePath = `${pluginName}.ts`;
20816
21042
  if (!existsSync24(entryPath)) {
@@ -20851,7 +21077,7 @@ function checkInstalledOpenCodeSkills(checks, rootDir) {
20851
21077
  if (!isLikelyOpenCodeInstallPath(rootDir)) {
20852
21078
  return;
20853
21079
  }
20854
- const pluginName = basename6(rootDir);
21080
+ const pluginName = basename7(rootDir);
20855
21081
  const sourceSkillsDir = resolve20(rootDir, "skills");
20856
21082
  if (!existsSync24(sourceSkillsDir)) {
20857
21083
  addCheck2(checks, {
@@ -20967,6 +21193,7 @@ async function doctorConsumer(rootDir = process.cwd(), options = {}) {
20967
21193
  checkInstalledCodexPermissionCompanion(checks, rootDir, layout, options);
20968
21194
  checkInstalledPermissionHook(checks, rootDir, layout);
20969
21195
  checkInstalledUserConfig(checks, rootDir);
21196
+ checkInstalledPlaintextSecrets(checks, rootDir);
20970
21197
  checkInstalledEnvValidation(checks, rootDir);
20971
21198
  checkInstalledRuntimeScriptRoles(checks, rootDir);
20972
21199
  await checkInstalledMcpConfig(checks, rootDir, layout);
@@ -21079,7 +21306,7 @@ function printDoctorReport(report) {
21079
21306
 
21080
21307
  // src/cli/dev.ts
21081
21308
  import { watch } from "fs";
21082
- import { relative as relative11, resolve as resolve21 } from "path";
21309
+ import { relative as relative13, resolve as resolve21 } from "path";
21083
21310
  var WATCH_PATTERNS = [
21084
21311
  /^pluxx\.config\.(ts|js|json)$/,
21085
21312
  /^skills\//,
@@ -21105,7 +21332,7 @@ async function runDev(args2) {
21105
21332
  let pendingFile = null;
21106
21333
  const watcher = watch(rootDir, { recursive: true }, (_event, filename) => {
21107
21334
  if (!filename) return;
21108
- const rel = relative11(rootDir, resolve21(rootDir, filename));
21335
+ const rel = relative13(rootDir, resolve21(rootDir, filename));
21109
21336
  if (rel.startsWith("dist/") || rel.startsWith(".") || rel.includes("node_modules")) {
21110
21337
  return;
21111
21338
  }
@@ -21160,7 +21387,7 @@ async function runBuild(rootDir, targets) {
21160
21387
  }
21161
21388
 
21162
21389
  // src/cli/migrate.ts
21163
- import { basename as basename7, dirname as dirname8, relative as relative12, resolve as resolve22 } from "path";
21390
+ import { basename as basename8, dirname as dirname8, relative as relative14, resolve as resolve22 } from "path";
21164
21391
  import { existsSync as existsSync25, readdirSync as readdirSync11, mkdirSync as mkdirSync5, cpSync as cpSync4, readFileSync as readFileSync15, writeFileSync as writeFileSync5 } from "fs";
21165
21392
  function detectPlatform(pluginDir) {
21166
21393
  const checks = [
@@ -21525,7 +21752,7 @@ function parseCursorRuleFile(pluginDir, filePath) {
21525
21752
  const content = readFileSync15(filePath, "utf-8");
21526
21753
  const { hasFrontmatter, frontmatterLines, body } = splitMarkdownFrontmatter5(content);
21527
21754
  if (!hasFrontmatter) return null;
21528
- const description = parseTopLevelStringFrontmatter(frontmatterLines, "description") ?? titleCaseFromDirName(basename7(filePath, ".mdc"));
21755
+ const description = parseTopLevelStringFrontmatter(frontmatterLines, "description") ?? titleCaseFromDirName(basename8(filePath, ".mdc"));
21529
21756
  const globs = parseTopLevelStringOrStringArrayFrontmatter(frontmatterLines, "globs");
21530
21757
  const alwaysApply = parseTopLevelBooleanFrontmatter(frontmatterLines, "alwaysApply");
21531
21758
  return {
@@ -21533,7 +21760,7 @@ function parseCursorRuleFile(pluginDir, filePath) {
21533
21760
  ...globs !== void 0 ? { globs } : {},
21534
21761
  ...alwaysApply !== void 0 ? { alwaysApply } : {},
21535
21762
  ...body.trim() ? { content: body.trim() } : {},
21536
- path: `./${relative12(pluginDir, filePath).replace(/\\/g, "/")}`
21763
+ path: `./${relative14(pluginDir, filePath).replace(/\\/g, "/")}`
21537
21764
  };
21538
21765
  }
21539
21766
  function collectFiles(dir, predicate) {
@@ -21578,7 +21805,7 @@ function findInstructionSources(pluginDir, detection) {
21578
21805
  const extraPaths = /* @__PURE__ */ new Set();
21579
21806
  const platformOverrides = {};
21580
21807
  if (detection.platform === "cursor") {
21581
- const nestedAgents = collectFiles(pluginDir, (entry) => entry === "AGENTS.md").map((path) => `./${relative12(pluginDir, path).replace(/\\/g, "/")}`).filter((path) => path !== primary);
21808
+ const nestedAgents = collectFiles(pluginDir, (entry) => entry === "AGENTS.md").map((path) => `./${relative14(pluginDir, path).replace(/\\/g, "/")}`).filter((path) => path !== primary);
21582
21809
  if (nestedAgents.length > 0) {
21583
21810
  platformOverrides.cursor = {
21584
21811
  ...platformOverrides.cursor ?? {},
@@ -22241,7 +22468,7 @@ function hasTopLevelFrontmatterKey(frontmatterLines, key) {
22241
22468
  function normalizeMigratedOpenCodeAgentFile(agentPath) {
22242
22469
  const original = readFileSync15(agentPath, "utf-8");
22243
22470
  const parsed = splitMarkdownFrontmatter5(original);
22244
- const fileStem = toKebabCase2(basename7(agentPath, ".md")) || "agent";
22471
+ const fileStem = toKebabCase2(basename8(agentPath, ".md")) || "agent";
22245
22472
  const fallbackDescription = buildFallbackAgentDescription(fileStem);
22246
22473
  if (!parsed.hasFrontmatter) {
22247
22474
  const rewritten2 = [
@@ -22297,7 +22524,7 @@ function normalizeMigratedOpenCodeAgents(destDir) {
22297
22524
  const normalized = [];
22298
22525
  for (const filePath of walkMarkdownFiles3(destDir)) {
22299
22526
  if (normalizeMigratedOpenCodeAgentFile(filePath)) {
22300
- normalized.push(relative12(destDir, filePath).replace(/\\/g, "/"));
22527
+ normalized.push(relative14(destDir, filePath).replace(/\\/g, "/"));
22301
22528
  }
22302
22529
  }
22303
22530
  return normalized.sort();
@@ -24135,7 +24362,7 @@ ${c2}
24135
24362
  } }).prompt();
24136
24363
 
24137
24364
  // src/cli/index.ts
24138
- import { basename as basename9, resolve as resolve29 } from "path";
24365
+ import { basename as basename10, resolve as resolve29 } from "path";
24139
24366
  import { mkdir as mkdir4, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
24140
24367
  import { tmpdir as tmpdir5 } from "os";
24141
24368
  import { spawn as spawn6, spawnSync as spawnSync4 } from "child_process";
@@ -24444,8 +24671,15 @@ echo
24444
24671
  echo "Installed __DISPLAY_NAME__ across ${installerTargets.join(", ")}."
24445
24672
  `.replaceAll("__REPO__", "REPO_PLACEHOLDER").replaceAll("__DISPLAY_NAME__", "DISPLAY_PLACEHOLDER");
24446
24673
  }
24674
+ function collectInstallerUserConfigEntries(config, platforms) {
24675
+ const baseEntries = collectUserConfigEntries(config, platforms);
24676
+ return [
24677
+ ...baseEntries,
24678
+ ...collectNativeMcpAuthUserConfigEntries(config, platforms, baseEntries)
24679
+ ];
24680
+ }
24447
24681
  function renderInstallerUserConfigSnippet(config, platform, installDirVariable) {
24448
- const entries = collectUserConfigEntries(config, [platform]).map((entry) => ({
24682
+ const entries = collectInstallerUserConfigEntries(config, [platform]).map((entry) => ({
24449
24683
  key: entry.key,
24450
24684
  title: entry.title,
24451
24685
  type: entry.type ?? "string",
@@ -24453,6 +24687,7 @@ function renderInstallerUserConfigSnippet(config, platform, installDirVariable)
24453
24687
  envVar: entry.envVar ?? defaultUserConfigEnvVar(entry.key)
24454
24688
  }));
24455
24689
  if (entries.length === 0) return "";
24690
+ const preserveSecretReferences = platform === "codex";
24456
24691
  const promptLines = entries.map((entry) => {
24457
24692
  const functionName = entry.type === "secret" ? "pluxx_prompt_secret_config" : "pluxx_prompt_text_config";
24458
24693
  return `${functionName} ${JSON.stringify(entry.key)} ${JSON.stringify(entry.envVar)} ${JSON.stringify(entry.title)} ${entry.required ? "1" : "0"}`;
@@ -24463,6 +24698,8 @@ ${JSON.stringify(entries)}
24463
24698
  PLUXX_USER_CONFIG_JSON
24464
24699
  )"
24465
24700
  PLUXX_REUSED_USER_CONFIG=0
24701
+ PLUXX_PRESERVE_SECRET_REFS="${preserveSecretReferences ? "1" : "0"}"
24702
+ export PLUXX_PRESERVE_SECRET_REFS
24466
24703
 
24467
24704
  pluxx_is_placeholder_secret() {
24468
24705
  case "$1" in
@@ -24489,12 +24726,14 @@ const fs = require('fs')
24489
24726
  const filepath = process.env.PLUXX_SAVED_USER_CONFIG_PATH
24490
24727
  const key = process.env.PLUXX_SAVED_CONFIG_KEY
24491
24728
  const envVar = process.env.PLUXX_SAVED_CONFIG_ENV_VAR
24729
+ const preserveSecretRefs = process.env.PLUXX_PRESERVE_SECRET_REFS === '1'
24492
24730
 
24493
24731
  try {
24494
24732
  const payload = JSON.parse(fs.readFileSync(filepath, 'utf8'))
24495
24733
  const candidates = [
24496
24734
  payload && payload.env && envVar ? payload.env[envVar] : undefined,
24497
24735
  payload && payload.values && key ? payload.values[key] : undefined,
24736
+ preserveSecretRefs && payload && payload.envRefs && envVar && payload.envRefs[envVar] === envVar ? envVar : undefined,
24498
24737
  ]
24499
24738
 
24500
24739
  for (const candidate of candidates) {
@@ -24590,21 +24829,53 @@ const path = require('path')
24590
24829
 
24591
24830
  const installDir = process.env.PLUXX_INSTALL_DIR
24592
24831
  const spec = JSON.parse(process.env.PLUXX_USER_CONFIG_SPEC || '[]')
24832
+ const preserveSecretReferences = ${preserveSecretReferences ? "true" : "false"}
24593
24833
 
24594
24834
  if (installDir && spec.length > 0) {
24595
24835
  const env = {}
24596
24836
  const values = {}
24837
+ const envRefs = {}
24838
+ const secretKeys = []
24839
+ const secretEnv = []
24840
+ let hasSecret = false
24841
+ const secretEnvVars = new Set(
24842
+ spec
24843
+ .filter((entry) => entry && entry.type === 'secret' && typeof entry.envVar === 'string' && entry.envVar !== '')
24844
+ .map((entry) => entry.envVar),
24845
+ )
24597
24846
 
24598
24847
  for (const entry of spec) {
24848
+ if (entry.type === 'secret') {
24849
+ hasSecret = true
24850
+ if (preserveSecretReferences) {
24851
+ if (entry.key) secretKeys.push(entry.key)
24852
+ if (entry.envVar) secretEnv.push(entry.envVar)
24853
+ }
24854
+ }
24599
24855
  const value = process.env[entry.envVar]
24600
24856
  if (value === undefined || value === '') continue
24857
+ if (preserveSecretReferences && entry.type === 'secret') {
24858
+ envRefs[entry.envVar] = entry.envVar
24859
+ continue
24860
+ }
24601
24861
  values[entry.key] = value
24602
24862
  env[entry.envVar] = value
24603
24863
  }
24604
24864
 
24605
24865
  fs.writeFileSync(
24606
24866
  path.join(installDir, '.pluxx-user.json'),
24607
- JSON.stringify({ values, env }, null, 2) + '\\n',
24867
+ JSON.stringify(
24868
+ {
24869
+ ...(Object.keys(values).length > 0 ? { values } : {}),
24870
+ ...(Object.keys(env).length > 0 ? { env } : {}),
24871
+ ...(Object.keys(envRefs).length > 0 ? { envRefs } : {}),
24872
+ ...(hasSecret ? { secretStorage: preserveSecretReferences ? 'env-ref' : 'materialized' } : {}),
24873
+ ...(preserveSecretReferences && secretKeys.length > 0 ? { secretKeys: [...new Set(secretKeys)].sort() } : {}),
24874
+ ...(preserveSecretReferences && secretEnv.length > 0 ? { secretEnv: [...new Set(secretEnv)].sort() } : {}),
24875
+ },
24876
+ null,
24877
+ 2,
24878
+ ) + '\\n',
24608
24879
  )
24609
24880
 
24610
24881
  const envScriptPath = path.join(installDir, 'scripts/check-env.sh')
@@ -24617,7 +24888,10 @@ if (installDir && spec.length > 0) {
24617
24888
 
24618
24889
  const materialize = (value) =>
24619
24890
  typeof value === 'string'
24620
- ? value.replace(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g, (_match, name) => env[name] || '${" + name + "}')
24891
+ ? value.replace(
24892
+ /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g,
24893
+ (_match, name) => (preserveSecretReferences && secretEnvVars.has(name) ? '${" + name + "}' : (env[name] || '${" + name + "}')),
24894
+ )
24621
24895
  : value
24622
24896
 
24623
24897
  const materializeRecord = (record) => {
@@ -24641,7 +24915,7 @@ if (installDir && spec.length > 0) {
24641
24915
  server.env = materializeRecord(server.env)
24642
24916
  }
24643
24917
 
24644
- if (server.bearer_token_env_var && env[server.bearer_token_env_var]) {
24918
+ if (!preserveSecretReferences && server.bearer_token_env_var && env[server.bearer_token_env_var]) {
24645
24919
  server.http_headers = {
24646
24920
  ...(server.http_headers || {}),
24647
24921
  Authorization: 'Bearer ' + env[server.bearer_token_env_var],
@@ -24649,7 +24923,7 @@ if (installDir && spec.length > 0) {
24649
24923
  delete server.bearer_token_env_var
24650
24924
  }
24651
24925
 
24652
- if (server.env_http_headers && typeof server.env_http_headers === 'object') {
24926
+ if (!preserveSecretReferences && server.env_http_headers && typeof server.env_http_headers === 'object') {
24653
24927
  server.http_headers = {
24654
24928
  ...(server.http_headers || {}),
24655
24929
  }
@@ -24674,7 +24948,7 @@ NODE
24674
24948
  `;
24675
24949
  }
24676
24950
  function hasInstallerUserConfig(config, platform) {
24677
- return collectUserConfigEntries(config, [platform]).length > 0;
24951
+ return collectInstallerUserConfigEntries(config, [platform]).length > 0;
24678
24952
  }
24679
24953
  function renderInstallerSavedUserConfigCaptureSnippet(config, platform, installDirVariable) {
24680
24954
  if (!hasInstallerUserConfig(config, platform)) return "";
@@ -24840,13 +25114,13 @@ for (const line of lines) {
24840
25114
  continue
24841
25115
  }
24842
25116
  if (tableName === '') {
24843
- const dottedMatch = trimmed.match(/^features\\.plugin_hooks\\s*=\\s*(.+)$/)
25117
+ const dottedMatch = trimmed.match(/^features\\.hooks\\s*=\\s*(.+)$/)
24844
25118
  if (dottedMatch && isTomlTrue(dottedMatch[1])) process.exit(0)
24845
25119
  const inlineMatch = trimmed.match(/^features\\s*=\\s*(.+)$/)
24846
- if (inlineMatch && /\\bplugin_hooks\\s*=\\s*true\\b/i.test(inlineMatch[1])) process.exit(0)
25120
+ if (inlineMatch && /\\bhooks\\s*=\\s*true\\b/i.test(inlineMatch[1])) process.exit(0)
24847
25121
  }
24848
25122
  if (tableName !== 'features') continue
24849
- const match = trimmed.match(/^plugin_hooks\\s*=\\s*(.+)$/)
25123
+ const match = trimmed.match(/^hooks\\s*=\\s*(.+)$/)
24850
25124
  if (match && isTomlTrue(match[1])) process.exit(0)
24851
25125
  }
24852
25126
  process.exit(1)
@@ -24865,7 +25139,7 @@ NODE
24865
25139
  *)
24866
25140
  if [[ -r /dev/tty ]]; then
24867
25141
  echo "This Codex plugin bundle includes startup hooks." >/dev/tty
24868
- echo "Codex requires [features].plugin_hooks = true before plugin-bundled hooks can run." >/dev/tty
25142
+ echo "Codex requires [features].hooks = true before plugin-bundled hooks can run." >/dev/tty
24869
25143
  read -r -p "Enable Codex plugin-bundled hooks in $CODEX_CONFIG_PATH now? [Y/n] " PLUXX_CODEX_HOOKS_REPLY </dev/tty
24870
25144
  case "$PLUXX_CODEX_HOOKS_REPLY" in
24871
25145
  n|N|no|NO)
@@ -24937,7 +25211,7 @@ for (let index = 0; index < lines.length; index += 1) {
24937
25211
  if (/^features\\.[A-Za-z0-9_-]+\\s*=/.test(trimmed) && firstTopLevelFeaturesDotted < 0) {
24938
25212
  firstTopLevelFeaturesDotted = index
24939
25213
  }
24940
- if (/^features\\.plugin_hooks\\s*=/.test(trimmed)) {
25214
+ if (/^features\\.hooks\\s*=/.test(trimmed)) {
24941
25215
  topLevelPluginHooksDotted = index
24942
25216
  }
24943
25217
  if (/^features\\s*=\\s*\\{/.test(trimmed)) {
@@ -24956,28 +25230,28 @@ if (start >= 0) {
24956
25230
 
24957
25231
  let updated = false
24958
25232
  for (let index = start + 1; index < end; index += 1) {
24959
- if (/^plugin_hooks\\s*=/.test(stripTomlComment(lines[index]).trim())) {
24960
- lines[index] = 'plugin_hooks = true'
25233
+ if (/^hooks\\s*=/.test(stripTomlComment(lines[index]).trim())) {
25234
+ lines[index] = 'hooks = true'
24961
25235
  updated = true
24962
25236
  }
24963
25237
  }
24964
- if (!updated) lines.splice(start + 1, 0, 'plugin_hooks = true')
25238
+ if (!updated) lines.splice(start + 1, 0, 'hooks = true')
24965
25239
  } else if (topLevelPluginHooksDotted >= 0) {
24966
- lines[topLevelPluginHooksDotted] = 'features.plugin_hooks = true'
25240
+ lines[topLevelPluginHooksDotted] = 'features.hooks = true'
24967
25241
  } else if (firstTopLevelFeaturesDotted >= 0) {
24968
- lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.plugin_hooks = true')
25242
+ lines.splice(firstTopLevelFeaturesDotted + 1, 0, 'features.hooks = true')
24969
25243
  } else if (topLevelInlineFeatures >= 0 && lines[topLevelInlineFeatures].includes('}')) {
24970
- if (/\\bplugin_hooks\\s*=/.test(lines[topLevelInlineFeatures])) {
25244
+ if (/\\bhooks\\s*=/.test(lines[topLevelInlineFeatures])) {
24971
25245
  lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(
24972
- /\\bplugin_hooks\\s*=\\s*(true|false)\\b/i,
24973
- 'plugin_hooks = true',
25246
+ /\\bhooks\\s*=\\s*(true|false)\\b/i,
25247
+ 'hooks = true',
24974
25248
  )
24975
25249
  } else {
24976
- lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ', plugin_hooks = true }')
25250
+ lines[topLevelInlineFeatures] = lines[topLevelInlineFeatures].replace(/}/, ', hooks = true }')
24977
25251
  }
24978
25252
  } else {
24979
25253
  if (lines.length > 0 && lines[lines.length - 1] !== '') lines.push('')
24980
- lines.push('[features]', 'plugin_hooks = true')
25254
+ lines.push('[features]', 'hooks = true')
24981
25255
  }
24982
25256
 
24983
25257
  fs.mkdirSync(path.dirname(filepath), { recursive: true })
@@ -24988,7 +25262,7 @@ NODE
24988
25262
  else
24989
25263
  echo "Codex plugin-bundled hooks are not enabled. Startup hooks from this plugin will not run until you add this to $CODEX_CONFIG_PATH:" >&2
24990
25264
  echo "[features]" >&2
24991
- echo "plugin_hooks = true" >&2
25265
+ echo "hooks = true" >&2
24992
25266
  echo "Then restart or refresh Codex before relying on plugin startup hooks." >&2
24993
25267
  echo "Set PLUXX_CODEX_ENABLE_PLUGIN_HOOKS=1 before running this installer to enable it noninteractively." >&2
24994
25268
  fi
@@ -25854,10 +26128,10 @@ import { resolve as resolve27 } from "path";
25854
26128
  import { existsSync as existsSync28, readFileSync as readFileSync19 } from "fs";
25855
26129
  import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
25856
26130
  import { tmpdir as tmpdir4 } from "os";
25857
- import { basename as basename8, resolve as resolve26 } from "path";
26131
+ import { basename as basename9, resolve as resolve26 } from "path";
25858
26132
  import { spawn as spawn4 } from "child_process";
25859
26133
  async function executeCodexExecCommand(command2, options) {
25860
- if (basename8(command2[0]) !== "codex" || command2[1] !== "exec") {
26134
+ if (basename9(command2[0]) !== "codex" || command2[1] !== "exec") {
25861
26135
  throw new Error("Codex exec runner requires a command beginning with `codex exec`.");
25862
26136
  }
25863
26137
  const outputDir = await mkdtemp2(resolve26(tmpdir4(), options.outputDirPrefix ?? "pluxx-codex-exec-"));
@@ -26182,6 +26456,7 @@ async function executeBehavioralCommand(platform, command2, cwd, timeoutMs) {
26182
26456
  };
26183
26457
  }
26184
26458
  return await new Promise((resolvePromise, reject) => {
26459
+ const startedAt = Date.now();
26185
26460
  const child = spawn5(command2[0], command2.slice(1), {
26186
26461
  cwd,
26187
26462
  detached: process.platform !== "win32",
@@ -26190,15 +26465,30 @@ async function executeBehavioralCommand(platform, command2, cwd, timeoutMs) {
26190
26465
  });
26191
26466
  const stdoutChunks = [];
26192
26467
  const stderrChunks = [];
26468
+ let settled = false;
26469
+ let timedOut = false;
26470
+ const timeout = setTimeout(() => {
26471
+ timedOut = true;
26472
+ signalBehavioralProcess(child, "SIGKILL");
26473
+ }, timeoutMs);
26193
26474
  child.stdout?.on("data", (chunk) => stdoutChunks.push(Buffer.from(chunk)));
26194
26475
  child.stderr?.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
26195
- child.on("error", reject);
26476
+ child.on("error", (error) => {
26477
+ if (settled) return;
26478
+ settled = true;
26479
+ clearTimeout(timeout);
26480
+ reject(error);
26481
+ });
26196
26482
  child.on("close", (code) => {
26483
+ if (settled) return;
26484
+ settled = true;
26485
+ clearTimeout(timeout);
26486
+ const exceededDeadline = timedOut || Date.now() - startedAt > timeoutMs;
26197
26487
  const stdout = Buffer.concat(stdoutChunks).toString("utf-8");
26198
26488
  const stderr = Buffer.concat(stderrChunks).toString("utf-8");
26199
26489
  resolvePromise({
26200
- exitCode: code ?? 1,
26201
- response: stdout.trim() || stderr.trim()
26490
+ exitCode: exceededDeadline ? 124 : code ?? 1,
26491
+ response: exceededDeadline ? `behavioral runner timed out after ${timeoutMs}ms` : stdout.trim() || stderr.trim()
26202
26492
  });
26203
26493
  });
26204
26494
  });
@@ -26240,6 +26530,22 @@ async function commandSucceeds2(command2) {
26240
26530
  child.on("error", () => resolvePromise(false));
26241
26531
  });
26242
26532
  }
26533
+ function signalBehavioralProcess(child, signal) {
26534
+ if (child.exitCode != null || child.signalCode != null) {
26535
+ return;
26536
+ }
26537
+ if (process.platform !== "win32" && typeof child.pid === "number") {
26538
+ try {
26539
+ process.kill(-child.pid, signal);
26540
+ return;
26541
+ } catch {
26542
+ }
26543
+ }
26544
+ try {
26545
+ child.kill(signal);
26546
+ } catch {
26547
+ }
26548
+ }
26243
26549
  function truncate2(value, length) {
26244
26550
  if (value.length <= length) return value;
26245
26551
  return `${value.slice(0, Math.max(0, length - 3))}...`;
@@ -26261,7 +26567,7 @@ function shellQuote2(value) {
26261
26567
  // src/cli/discover-installed-mcp.ts
26262
26568
  import { existsSync as existsSync30, readFileSync as readFileSync21, realpathSync as realpathSync3 } from "fs";
26263
26569
  import { homedir as homedir3 } from "os";
26264
- import { isAbsolute as isAbsolute2, relative as relative13, resolve as resolve28 } from "path";
26570
+ import { isAbsolute as isAbsolute2, relative as relative15, resolve as resolve28 } from "path";
26265
26571
  var INSTALLED_MCP_HOSTS = ["claude-code", "cursor", "codex", "opencode"];
26266
26572
  function discoverInstalledMcpServers(options = {}) {
26267
26573
  const rootDir = options.rootDir ?? process.cwd();
@@ -26400,13 +26706,13 @@ function buildClaudeProjectSourceScope(projectPath, rootDir, homeDir) {
26400
26706
  return `projects/absolute/${normalizeScopePath(projectPath)}`;
26401
26707
  }
26402
26708
  function buildRelativeScopePath(baseDir, targetPath) {
26403
- const relativePath = relative13(baseDir, targetPath);
26709
+ const relativePath = relative15(baseDir, targetPath);
26404
26710
  if (!relativePath.startsWith("..") && !isAbsolute2(relativePath)) {
26405
26711
  return normalizeScopePath(relativePath);
26406
26712
  }
26407
26713
  if (!existsSync30(baseDir) || !existsSync30(targetPath)) return void 0;
26408
26714
  try {
26409
- const realRelativePath = relative13(realpathSync3(baseDir), realpathSync3(targetPath));
26715
+ const realRelativePath = relative15(realpathSync3(baseDir), realpathSync3(targetPath));
26410
26716
  if (realRelativePath.startsWith("..") || isAbsolute2(realRelativePath)) return void 0;
26411
26717
  return normalizeScopePath(realRelativePath);
26412
26718
  } catch {
@@ -26606,7 +26912,7 @@ function buildInstalledMcpSourceLabel(discovered, rootDir, homeDir) {
26606
26912
  return discovered.sourceScope ? `${base}:${discovered.sourceScope}` : base;
26607
26913
  }
26608
26914
  function buildRelativeSelectorLabel(prefix, baseDir, sourcePath) {
26609
- const relativePath = relative13(baseDir, sourcePath);
26915
+ const relativePath = relative15(baseDir, sourcePath);
26610
26916
  if (relativePath === "") return `${prefix}:.`;
26611
26917
  if (relativePath.startsWith("..") || isAbsolute2(relativePath)) return void 0;
26612
26918
  return `${prefix}:${relativePath.replace(/\\/g, "/")}`;
@@ -27530,7 +27836,7 @@ async function runInit() {
27530
27836
  if (!runtime.isInteractive) {
27531
27837
  throw new Error("pluxx init requires an interactive terminal unless you use `pluxx init --from-mcp ... --yes`.");
27532
27838
  }
27533
- const dirName = positionalName ? toKebabCase3(positionalName) : basename9(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
27839
+ const dirName = positionalName ? toKebabCase3(positionalName) : basename10(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
27534
27840
  console.log("");
27535
27841
  console.log(" pluxx init \u2014 Create a new plugin");
27536
27842
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");