enigma-cli 1.1.1 → 1.1.2

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.
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Backend/API architecture: controller-service-repository layering, API and request optimization, server-side caching (Redis), and Zod boundary validation.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "c442bc9e39a7710cb709ef2abb8d15ecd8aa16ed4f5c8af92b7af6877401cba4"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.1.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Ciphera code style conventions (formatting, naming, imports, comments, code-level anti-patterns; TypeScript-first, language-agnostic).",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "f8602bb79fbbe063ab39fbd59d0b7844a22c3a1583fcd11c1b4f98a2fe8ddc86"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Pre-delivery self-review gate, prioritized review dimensions, and change-quality criteria.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "3d3bbe0602d5bbb4afe37648fe3c2fa39376b1bcbac5d8c441f01fad1e866ed0"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.4.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Core engineering execution policy and harness orchestration (highest-authority rules).",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "c9c69c59516794311cb7b306ed4d4ad971824de3689a39c2b86c7669c73f2e8b"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Senior database architecture policy: query optimization, anti-duplication/normalization, scalability, and RGPD/GDPR encryption.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "c4617ee8d1a57d9621c81bef3093e94de91f79eec0cc0ead41f6d18dd443e623"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Reproduce-isolate-fix debugging methodology with root-cause discipline and regression verification.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "14b0064c8b33a0dc85e51464b05005cf5801c756b1101789a6924b9548420f6b"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Dependency and supply-chain security: lockfiles and reproducible installs, version pinning, vulnerability auditing, vetting/minimizing packages, vendoring, and SBOM/provenance.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Frontend architecture: reusable components, abstraction thresholds, state management, and optimistic UI with rollback.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "b0355b0e15f9f528d32adf19f0722d2727cd64d6b3544307ecc7a3141338f023"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.2.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Git & contribution policy (senior engineering standards).",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "ada4b7eb5bb7e013429e23703c271c0f34b0d76327c059efa148ea2794f96178"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Application and AI-agent security: secrets, authn/authz (least privilege), OWASP Top 10, transport/crypto baseline, secure logging, and agent/MCP/tool-use safety.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "9971e9d9127397d0152e89d24aad3191e2935e55a8483db7fd15f5d4d7a60e7a"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Test strategy, coverage gates, deterministic tests, mocking discipline, and regression-first bug fixing.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "d19fa8ec7985ed231478be504d3c80360897f555d0bc0624bea19c091f459fb0"
8
8
  }
@@ -3,6 +3,6 @@
3
3
  "version": "1.0.0",
4
4
  "provider": "FJRG2007/enigma",
5
5
  "description": "Strict frontend + backend schema validation, schema consistency, and safe client-facing error handling.",
6
- "cliVersion": "1.1.1",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
package/dist/enigma.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { dirname as dirname4, join as join10 } from "path";
5
5
  import { fileURLToPath as fileURLToPath4 } from "url";
6
- import * as p5 from "@clack/prompts";
6
+ import * as p6 from "@clack/prompts";
7
7
 
8
8
  // src/util.ts
9
9
  import { existsSync, statSync, readFileSync } from "fs";
@@ -257,6 +257,54 @@ function disableClaudeAttribution(scope) {
257
257
  writeFileSync2(path, JSON.stringify(next, null, 2) + "\n");
258
258
  return true;
259
259
  }
260
+ function getClaudeAttribution(scope) {
261
+ const current = readJson(claudeSettingsPath(scope)) || {};
262
+ const attribution = current.attribution;
263
+ const disabled = Boolean(attribution) && attribution.commit === "" && attribution.pr === "" && current.includeCoAuthoredBy === false;
264
+ return !disabled;
265
+ }
266
+ function setClaudeAttribution(scope, enabled) {
267
+ if (!enabled) return disableClaudeAttribution(scope);
268
+ const path = claudeSettingsPath(scope);
269
+ const current = readJson(path) || {};
270
+ const attribution = typeof current.attribution === "object" && current.attribution !== null ? { ...current.attribution } : {};
271
+ let changed = false;
272
+ if (attribution.commit === "") {
273
+ delete attribution.commit;
274
+ changed = true;
275
+ }
276
+ if (attribution.pr === "") {
277
+ delete attribution.pr;
278
+ changed = true;
279
+ }
280
+ if (current.includeCoAuthoredBy === false) changed = true;
281
+ if (!changed) return false;
282
+ const next = { ...current };
283
+ if (Object.keys(attribution).length) next.attribution = attribution;
284
+ else delete next.attribution;
285
+ delete next.includeCoAuthoredBy;
286
+ writeClaudeSettings(path, next);
287
+ return true;
288
+ }
289
+ function getClaudeBypass(scope) {
290
+ const current = readJson(claudeSettingsPath(scope)) || {};
291
+ const permissions = current.permissions;
292
+ return Boolean(permissions) && permissions.defaultMode === "bypassPermissions";
293
+ }
294
+ function setClaudeBypass(scope, on, dryRun) {
295
+ if (on) return enableClaudeBypass(scope, dryRun);
296
+ const path = claudeSettingsPath(scope);
297
+ const current = readJson(path) || {};
298
+ const permissions = typeof current.permissions === "object" && current.permissions !== null ? { ...current.permissions } : {};
299
+ if (permissions.defaultMode !== "bypassPermissions") return { path, changed: false };
300
+ if (dryRun) return { path, changed: true };
301
+ delete permissions.defaultMode;
302
+ const next = { ...current };
303
+ if (Object.keys(permissions).length) next.permissions = permissions;
304
+ else delete next.permissions;
305
+ writeClaudeSettings(path, next);
306
+ return { path, changed: true };
307
+ }
260
308
  function enableClaudeBypass(scope, dryRun) {
261
309
  const path = claudeSettingsPath(scope);
262
310
  const current = readJson(path) || {};
@@ -264,10 +312,13 @@ function enableClaudeBypass(scope, dryRun) {
264
312
  if (permissions.defaultMode === "bypassPermissions") return { path, changed: false };
265
313
  if (dryRun) return { path, changed: true };
266
314
  const next = { ...current, permissions: { ...permissions, defaultMode: "bypassPermissions" } };
315
+ writeClaudeSettings(path, next);
316
+ return { path, changed: true };
317
+ }
318
+ function writeClaudeSettings(path, data) {
267
319
  const dir = join4(path, "..");
268
320
  if (!isDir(dir)) mkdirSync2(dir, { recursive: true });
269
- writeFileSync2(path, JSON.stringify(next, null, 2) + "\n");
270
- return { path, changed: true };
321
+ writeFileSync2(path, JSON.stringify(data, null, 2) + "\n");
271
322
  }
272
323
 
273
324
  // src/permissions.ts
@@ -323,6 +374,74 @@ function enableFor(name, scope, dryRun) {
323
374
  return null;
324
375
  }
325
376
  }
377
+ function getBypass(name, scope) {
378
+ switch (name) {
379
+ case "claude":
380
+ return getClaudeBypass(scope);
381
+ case "codex":
382
+ return getCodexBypass();
383
+ case "opencode":
384
+ return getOpencodeBypass(scope);
385
+ default:
386
+ return false;
387
+ }
388
+ }
389
+ function setBypass(name, scope, on, dryRun) {
390
+ switch (name) {
391
+ case "claude":
392
+ return setClaudeBypass(scope, on, dryRun);
393
+ case "codex":
394
+ return on ? enableCodexBypass(dryRun) : disableCodexBypass(dryRun);
395
+ case "opencode":
396
+ return on ? enableOpencodeBypass(scope, dryRun) : disableOpencodeBypass(scope, dryRun);
397
+ default:
398
+ return null;
399
+ }
400
+ }
401
+ function getCodexBypass() {
402
+ const path = join5(homedir3(), ".codex", "config.toml");
403
+ const content = existsSync4(path) ? readFileSync2(path, "utf8") : "";
404
+ return getTomlTopLevelKey(content, "approval_policy") === '"never"';
405
+ }
406
+ function disableCodexBypass(dryRun) {
407
+ const path = join5(homedir3(), ".codex", "config.toml");
408
+ const before = existsSync4(path) ? readFileSync2(path, "utf8") : "";
409
+ let after = removeTomlTopLevelKey(before, "approval_policy");
410
+ after = removeTomlTopLevelKey(after, "sandbox_mode");
411
+ const changed = after !== before;
412
+ if (changed && !dryRun) writeFileSync3(path, after);
413
+ return { path, changed };
414
+ }
415
+ function getOpencodeBypass(scope) {
416
+ const path = opencodeConfigPath(scope);
417
+ const perm = (readJson(path) || {}).permission;
418
+ return perm === "allow" || typeof perm === "object" && perm !== null && perm["*"] === "allow";
419
+ }
420
+ function disableOpencodeBypass(scope, dryRun) {
421
+ const path = opencodeConfigPath(scope);
422
+ const current = readJson(path) || {};
423
+ const perm = current.permission;
424
+ if (!getOpencodeBypass(scope)) return { path, changed: false };
425
+ if (dryRun) return { path, changed: true };
426
+ const next = { ...current };
427
+ if (typeof perm === "object" && perm !== null) {
428
+ const rest = {};
429
+ for (const k of Object.keys(perm)) {
430
+ if (k !== "*") rest[k] = perm[k];
431
+ }
432
+ if (Object.keys(rest).length) next.permission = rest;
433
+ else delete next.permission;
434
+ } else {
435
+ delete next.permission;
436
+ }
437
+ const dir = join5(path, "..");
438
+ if (!isDir(dir)) mkdirSync3(dir, { recursive: true });
439
+ writeFileSync3(path, JSON.stringify(next, null, 2) + "\n");
440
+ return { path, changed: true };
441
+ }
442
+ function opencodeConfigPath(scope) {
443
+ return scope === "global" ? join5(homedir3(), ".config", "opencode", "opencode.json") : join5(process.cwd(), "opencode.json");
444
+ }
326
445
  function enableCodexBypass(dryRun) {
327
446
  const path = join5(homedir3(), ".codex", "config.toml");
328
447
  const before = existsSync4(path) ? readFileSync2(path, "utf8") : "";
@@ -337,7 +456,7 @@ function enableCodexBypass(dryRun) {
337
456
  return { path, changed };
338
457
  }
339
458
  function enableOpencodeBypass(scope, dryRun) {
340
- const path = scope === "global" ? join5(homedir3(), ".config", "opencode", "opencode.json") : join5(process.cwd(), "opencode.json");
459
+ const path = opencodeConfigPath(scope);
341
460
  const current = readJson(path) || {};
342
461
  const perm = current.permission;
343
462
  const alreadyAllowAll = perm === "allow" || typeof perm === "object" && perm !== null && perm["*"] === "allow";
@@ -372,6 +491,27 @@ function setTomlTopLevelKey(content, key, tomlValue) {
372
491
  lines.splice(insertAt, 0, ...followsTable ? [assign, ""] : [assign]);
373
492
  return normalizeTrailingNewline(lines.join("\n"));
374
493
  }
494
+ function getTomlTopLevelKey(content, key) {
495
+ const lines = content.split("\n");
496
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
497
+ const scanEnd = firstTable === -1 ? lines.length : firstTable;
498
+ const keyRe = new RegExp(`^\\s*${key}\\s*=\\s*(.+?)\\s*$`);
499
+ for (let i = 0; i < scanEnd; i++) {
500
+ const m = keyRe.exec(lines[i]);
501
+ if (m) return m[1];
502
+ }
503
+ return null;
504
+ }
505
+ function removeTomlTopLevelKey(content, key) {
506
+ if (content.trim() === "") return content;
507
+ const lines = content.split("\n");
508
+ const firstTable = lines.findIndex((l) => /^\s*\[/.test(l));
509
+ const scanEnd = firstTable === -1 ? lines.length : firstTable;
510
+ const keyRe = new RegExp(`^\\s*${key}\\s*=`);
511
+ const kept = lines.filter((l, i) => !(i < scanEnd && keyRe.test(l)));
512
+ if (kept.length === lines.length) return content;
513
+ return normalizeTrailingNewline(kept.join("\n"));
514
+ }
375
515
  function normalizeTrailingNewline(s) {
376
516
  return `${s.replace(/\s+$/, "")}
377
517
  `;
@@ -920,14 +1060,15 @@ if (isGuardEntry && fileURLToPath3(import.meta.url) === guardEntry) {
920
1060
  process.exit(runGuardCli(process.argv.includes("--all")));
921
1061
  }
922
1062
 
1063
+ // src/settings.ts
1064
+ import * as p4 from "@clack/prompts";
1065
+
923
1066
  // src/config.ts
924
1067
  import { homedir as homedir4 } from "os";
925
1068
  import { join as join8 } from "path";
926
1069
  import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
927
1070
  var CONFIG_FILE = ".enigma.json";
928
- var CONFIG_DEFAULTS = { commitEmoji: true };
929
- var BOOLEAN_KEYS = ["commitEmoji"];
930
- var CLI_KEYS = { "commit-emoji": "commitEmoji" };
1071
+ var CONFIG_DEFAULTS = { commitEmoji: true, updateNotifier: true };
931
1072
  function configPath(scope) {
932
1073
  return scope === "global" ? join8(homedir4(), CONFIG_FILE) : join8(process.cwd(), CONFIG_FILE);
933
1074
  }
@@ -944,13 +1085,7 @@ function readConfig() {
944
1085
  }
945
1086
  return { config, sources };
946
1087
  }
947
- function parseBool(value) {
948
- const v = value.toLowerCase();
949
- if (["on", "true", "yes", "1", "enable", "enabled"].includes(v)) return true;
950
- if (["off", "false", "no", "0", "disable", "disabled"].includes(v)) return false;
951
- return null;
952
- }
953
- function setValue(scope, key, value) {
1088
+ function setEnigmaToggle(key, value, scope) {
954
1089
  const path = configPath(scope);
955
1090
  const current = readJson(path) || {};
956
1091
  const next = { ...current, [key]: value };
@@ -959,26 +1094,156 @@ function setValue(scope, key, value) {
959
1094
  writeFileSync5(path, JSON.stringify(next, null, 2) + "\n");
960
1095
  return path;
961
1096
  }
962
- function runConfigCli(positionals, scope) {
1097
+
1098
+ // src/settings.ts
1099
+ function enigmaToggle(key, field, label, hint) {
1100
+ return {
1101
+ key,
1102
+ label,
1103
+ hint,
1104
+ read: () => readConfig().config[field],
1105
+ write: (value, scope) => ({ path: setEnigmaToggle(field, value, scope), changed: true })
1106
+ };
1107
+ }
1108
+ var CATEGORIES = [
1109
+ {
1110
+ title: "General",
1111
+ blurb: "enigma runtime toggles (.enigma.json)",
1112
+ settings: [
1113
+ enigmaToggle("commit-emoji", "commitEmoji", "Commit subject emoji", "leading gitmoji on commit subjects"),
1114
+ enigmaToggle("update-notifier", "updateNotifier", "Update notifications", "notify when a newer enigma-cli is published")
1115
+ ]
1116
+ },
1117
+ {
1118
+ title: "Git & attribution",
1119
+ blurb: "how the coding agent attributes its work in git",
1120
+ settings: [
1121
+ {
1122
+ key: "claude-attribution",
1123
+ label: "Claude commit attribution",
1124
+ hint: "let Claude Code commit as its own contributor (Co-Authored-By / PR footer); enigma default: off",
1125
+ read: (scope) => getClaudeAttribution(scope),
1126
+ write: (value, scope) => ({ changed: setClaudeAttribution(scope, value) })
1127
+ }
1128
+ ]
1129
+ },
1130
+ {
1131
+ title: "Permissions",
1132
+ blurb: "approval-prompt bypass per agent (security trade-off)",
1133
+ settings: BYPASS_SUPPORTED.map((name) => ({
1134
+ key: `bypass-${name}`,
1135
+ label: `${AGENTS[name]?.label || name} approval bypass`,
1136
+ hint: name === "codex" ? "skip approval prompts (global ~/.codex only)" : "skip per-action approval prompts",
1137
+ globalOnly: name === "codex",
1138
+ read: (scope) => getBypass(name, scope),
1139
+ write: (value, scope) => setBypass(name, scope, value, false) || { changed: false }
1140
+ }))
1141
+ }
1142
+ ];
1143
+ var ALL_SETTINGS = CATEGORIES.flatMap((c) => c.settings);
1144
+ function valueLabel(on) {
1145
+ return on ? "on" : "off";
1146
+ }
1147
+ function parseBool(value) {
1148
+ const v = value.toLowerCase();
1149
+ if (["on", "true", "yes", "1", "enable", "enabled"].includes(v)) return true;
1150
+ if (["off", "false", "no", "0", "disable", "disabled"].includes(v)) return false;
1151
+ return null;
1152
+ }
1153
+ async function runSettingsMenu() {
1154
+ for (; ; ) {
1155
+ const choice = await p4.select({
1156
+ message: "Settings - choose a category",
1157
+ options: [
1158
+ ...CATEGORIES.map((c) => ({ value: c.title, label: c.title, hint: c.blurb })),
1159
+ { value: "__back", label: "< Back" }
1160
+ ]
1161
+ });
1162
+ if (p4.isCancel(choice) || choice === "__back") return;
1163
+ await runCategory(CATEGORIES.find((c) => c.title === choice));
1164
+ }
1165
+ }
1166
+ async function runCategory(category) {
1167
+ for (; ; ) {
1168
+ const choice = await p4.select({
1169
+ message: `${category.title} - ${category.blurb}`,
1170
+ options: [
1171
+ ...category.settings.map((s) => ({
1172
+ value: s.key,
1173
+ label: `${s.label}: ${valueLabel(s.read("global"))}`,
1174
+ hint: s.hint
1175
+ })),
1176
+ { value: "__back", label: "< Back" }
1177
+ ]
1178
+ });
1179
+ if (p4.isCancel(choice) || choice === "__back") return;
1180
+ await editSetting(category.settings.find((s) => s.key === choice));
1181
+ }
1182
+ }
1183
+ async function editSetting(setting) {
1184
+ let scope = "global";
1185
+ if (!setting.globalOnly) {
1186
+ const picked2 = await p4.select({
1187
+ message: `Apply "${setting.label}" to which scope?`,
1188
+ options: [
1189
+ { value: "global", label: "Global", hint: "all projects (~)" },
1190
+ { value: "local", label: "This project", hint: "current directory" }
1191
+ ],
1192
+ initialValue: "global"
1193
+ });
1194
+ if (p4.isCancel(picked2)) return;
1195
+ scope = picked2;
1196
+ }
1197
+ const current = setting.read(scope);
1198
+ const picked = await p4.select({
1199
+ message: `${setting.label} (${scope})`,
1200
+ options: [{ value: "on", label: "On" }, { value: "off", label: "Off" }],
1201
+ initialValue: current ? "on" : "off"
1202
+ });
1203
+ if (p4.isCancel(picked)) return;
1204
+ const value = picked === "on";
1205
+ if (value === current) {
1206
+ p4.log.info(`${setting.label}: unchanged (${valueLabel(current)}).`);
1207
+ return;
1208
+ }
1209
+ const result = setting.write(value, scope);
1210
+ if (result.changed) {
1211
+ const where = result.path ? ` -> ${result.path}` : "";
1212
+ p4.log.success(`${setting.label}: ${valueLabel(value)} (${scope})${where}.`);
1213
+ } else {
1214
+ p4.log.info(`${setting.label}: already ${valueLabel(value)} (${scope}).`);
1215
+ }
1216
+ }
1217
+ function printEffective() {
1218
+ console.log("Effective enigma settings:\n");
1219
+ for (const category of CATEGORIES) {
1220
+ console.log(`${category.title}:`);
1221
+ for (const s of category.settings) console.log(` ${s.key}: ${valueLabel(s.read("global"))}`);
1222
+ console.log("");
1223
+ }
1224
+ const { sources } = readConfig();
1225
+ console.log(sources.length ? `.enigma.json sources: ${sources.join(", ")}` : ".enigma.json: built-in defaults (no file found)");
1226
+ console.log("Agent settings (attribution, bypass) reflect each agent's own config at the global scope.");
1227
+ }
1228
+ async function runConfigCli(positionals, scope, interactive) {
963
1229
  const [rawKey, rawValue] = positionals;
964
1230
  if (!rawKey) {
965
- const { config, sources } = readConfig();
966
- console.log("Effective enigma config:");
967
- for (const k of BOOLEAN_KEYS) {
968
- const cliKey = Object.keys(CLI_KEYS).find((c) => CLI_KEYS[c] === k) || k;
969
- console.log(` ${cliKey}: ${config[k]}`);
1231
+ if (interactive) {
1232
+ p4.intro("enigma config");
1233
+ await runSettingsMenu();
1234
+ p4.outro("Done.");
1235
+ } else {
1236
+ printEffective();
970
1237
  }
971
- console.log(sources.length ? `
972
- From: ${sources.join(", ")}` : "\nFrom: built-in defaults (no .enigma.json found)");
973
1238
  return 0;
974
1239
  }
975
- const key = CLI_KEYS[rawKey];
976
- if (!key) {
977
- console.error(`Unknown config key: ${rawKey}. Known keys: ${Object.keys(CLI_KEYS).join(", ")}.`);
1240
+ const setting = ALL_SETTINGS.find((s) => s.key === rawKey);
1241
+ if (!setting) {
1242
+ console.error(`Unknown config key: ${rawKey}. Known keys: ${ALL_SETTINGS.map((s) => s.key).join(", ")}.`);
978
1243
  return 1;
979
1244
  }
980
1245
  if (rawValue === void 0) {
981
- console.error(`Missing value for '${rawKey}'. Usage: enigma config ${rawKey} <on|off>`);
1246
+ console.error(`Missing value for '${rawKey}'. Usage: enigma config ${rawKey} <on|off> [-g|-l]`);
982
1247
  return 1;
983
1248
  }
984
1249
  const value = parseBool(rawValue);
@@ -986,9 +1251,10 @@ From: ${sources.join(", ")}` : "\nFrom: built-in defaults (no .enigma.json found
986
1251
  console.error(`Invalid value '${rawValue}' for '${rawKey}'. Use on or off.`);
987
1252
  return 1;
988
1253
  }
989
- const target = scope || "global";
990
- const path = setValue(target, key, value);
991
- console.log(`Set ${rawKey} = ${value ? "on" : "off"} (${target}) in ${path}.`);
1254
+ const target = setting.globalOnly ? "global" : scope || "global";
1255
+ const result = setting.write(value, target);
1256
+ const where = result.path ? ` in ${result.path}` : "";
1257
+ console.log(`Set ${rawKey} = ${valueLabel(value)} (${target})${where}.`);
992
1258
  return 0;
993
1259
  }
994
1260
 
@@ -997,7 +1263,7 @@ import { homedir as homedir5 } from "os";
997
1263
  import { join as join9 } from "path";
998
1264
  import { writeFileSync as writeFileSync6 } from "fs";
999
1265
  import { spawn, spawnSync } from "child_process";
1000
- import * as p4 from "@clack/prompts";
1266
+ import * as p5 from "@clack/prompts";
1001
1267
  var REGISTRY_URL = "https://registry.npmjs.org/enigma-cli/latest";
1002
1268
  var UPDATE_COMMAND = "npm i -g enigma-cli@latest";
1003
1269
  var CACHE_FILE = join9(homedir5(), ".enigma-update-check.json");
@@ -1073,6 +1339,7 @@ function runUpdate() {
1073
1339
  async function notifyUpdate(current, interactive) {
1074
1340
  if (!process.stdout.isTTY || process.env.CI) return;
1075
1341
  try {
1342
+ if (!readConfig().config.updateNotifier) return;
1076
1343
  scheduleUpdateCheck();
1077
1344
  const cache = readCache();
1078
1345
  const latest = cache?.latest ? String(cache.latest).replace(/[^\w.+-]/g, "") : "";
@@ -1081,8 +1348,8 @@ async function notifyUpdate(current, interactive) {
1081
1348
  ${renderUpdateBox(current, latest)}
1082
1349
  `);
1083
1350
  if (!interactive) return;
1084
- const ok = await p4.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
1085
- if (p4.isCancel(ok) || !ok) return;
1351
+ const ok = await p5.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
1352
+ if (p5.isCancel(ok) || !ok) return;
1086
1353
  runUpdate();
1087
1354
  } catch {
1088
1355
  }
@@ -1199,15 +1466,19 @@ Usage:
1199
1466
  enigma [command] [options]
1200
1467
 
1201
1468
  Commands:
1202
- (none) Interactive menu: pick which features to set up
1469
+ (none) Interactive hub: configure settings or set up features
1203
1470
  install Install/update agent skills (Claude Code, Codex, opencode)
1204
1471
  security Set up git security hooks in the current repo
1205
1472
  guard [--all] Run the commit guard (staged files, or --all for every tracked file)
1206
- config [key val] Show/set runtime toggles (e.g. config commit-emoji off)
1473
+ config [key val] Configure settings: no args opens the interactive menu;
1474
+ 'config <key> <on|off> [-g|-l]' sets one (e.g. config claude-attribution on)
1207
1475
  seal Maintenance: (re)compute skill content hashes
1208
1476
  check Integrity gate: verify skills are well-formed and sealed
1209
1477
  help, version
1210
1478
 
1479
+ Config keys: commit-emoji, update-notifier, claude-attribution,
1480
+ bypass-claude, bypass-codex, bypass-opencode
1481
+
1211
1482
  Install options:
1212
1483
  -g, --global Install at user level
1213
1484
  -l, --local Install into the current project
@@ -1256,45 +1527,44 @@ async function run(argv) {
1256
1527
  process.exit(runGuardCli(opts.all));
1257
1528
  }
1258
1529
  if (opts.command === "config") {
1259
- process.exit(runConfigCli(opts.positionals, opts.scope));
1530
+ process.exit(await runConfigCli(opts.positionals, opts.scope, interactive));
1260
1531
  }
1261
1532
  if (opts.command === "install") {
1262
- p5.intro("enigma - install agent skills");
1533
+ p6.intro("enigma - install agent skills");
1263
1534
  await installSkills(opts, interactive);
1264
- p5.outro("Done.");
1535
+ p6.outro("Done.");
1265
1536
  await notifyUpdate(version, interactive);
1266
1537
  return;
1267
1538
  }
1268
1539
  if (opts.command === "security") {
1269
- p5.intro("enigma - git security hooks");
1540
+ p6.intro("enigma - git security hooks");
1270
1541
  const done = await setupGitHooks(opts, interactive);
1271
- p5.outro(done ? "Git hooks configured." : "No changes made.");
1542
+ p6.outro(done ? "Git hooks configured." : "No changes made.");
1543
+ await notifyUpdate(version, interactive);
1544
+ return;
1545
+ }
1546
+ if (!interactive) {
1547
+ await installSkills(opts, interactive);
1272
1548
  await notifyUpdate(version, interactive);
1273
1549
  return;
1274
1550
  }
1275
- p5.intro("enigma");
1276
- let features;
1277
- if (interactive) {
1278
- const r = await p5.multiselect({
1279
- message: "What do you want to set up?",
1551
+ p6.intro("enigma");
1552
+ for (; ; ) {
1553
+ const action = await p6.select({
1554
+ message: "What would you like to do?",
1280
1555
  options: [
1281
- { value: "skills", label: "Agent skills", hint: "Claude Code, Codex, opencode" },
1282
- { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" }
1283
- ],
1284
- initialValues: ["skills"],
1285
- required: true
1556
+ { value: "config", label: "Configure settings", hint: "emoji, attribution, permission bypass, ..." },
1557
+ { value: "skills", label: "Install agent skills", hint: "Claude Code, Codex, opencode" },
1558
+ { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" },
1559
+ { value: "exit", label: "Exit" }
1560
+ ]
1286
1561
  });
1287
- if (p5.isCancel(r)) {
1288
- p5.cancel("Aborted.");
1289
- return;
1290
- }
1291
- features = r;
1292
- } else {
1293
- features = ["skills"];
1562
+ if (p6.isCancel(action) || action === "exit") break;
1563
+ if (action === "config") await runSettingsMenu();
1564
+ else if (action === "skills") await installSkills(opts, interactive);
1565
+ else if (action === "security") await setupGitHooks(opts, interactive);
1294
1566
  }
1295
- if (features.includes("skills")) await installSkills(opts, interactive);
1296
- if (features.includes("security")) await setupGitHooks(opts, interactive);
1297
- p5.outro("Done.");
1567
+ p6.outro("Done.");
1298
1568
  await notifyUpdate(version, interactive);
1299
1569
  }
1300
1570
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Everything you need to work with a coding agent: install shared policy skills for Claude Code, OpenAI Codex and opencode, and set up portable git security hooks.",
5
5
  "type": "module",
6
6
  "bin": {