enigma-cli 1.1.0 → 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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
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.0",
6
+ "cliVersion": "1.1.2",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
package/dist/enigma.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { dirname as dirname4, join as join9 } from "path";
4
+ import { dirname as dirname4, join as join10 } from "path";
5
5
  import { fileURLToPath as fileURLToPath4 } from "url";
6
- import * as p4 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,15 +1251,113 @@ 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
 
1261
+ // src/update.ts
1262
+ import { homedir as homedir5 } from "os";
1263
+ import { join as join9 } from "path";
1264
+ import { writeFileSync as writeFileSync6 } from "fs";
1265
+ import { spawn, spawnSync } from "child_process";
1266
+ import * as p5 from "@clack/prompts";
1267
+ var REGISTRY_URL = "https://registry.npmjs.org/enigma-cli/latest";
1268
+ var UPDATE_COMMAND = "npm i -g enigma-cli@latest";
1269
+ var CACHE_FILE = join9(homedir5(), ".enigma-update-check.json");
1270
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1271
+ var CHILD_SCRIPT = [
1272
+ "const fs = require('fs');",
1273
+ "const ctrl = new AbortController();",
1274
+ "const t = setTimeout(() => ctrl.abort(), 5000);",
1275
+ "fetch(process.env.E_URL, { signal: ctrl.signal, headers: { 'user-agent': 'enigma-cli-update-check' } })",
1276
+ " .then((r) => (r.ok ? r.json() : null))",
1277
+ " .then((d) => { if (d && d.version) fs.writeFileSync(process.env.E_FILE, JSON.stringify({ latest: d.version, checkedAt: Date.now() })); })",
1278
+ " .catch(() => {})",
1279
+ " .finally(() => clearTimeout(t));"
1280
+ ].join("\n");
1281
+ function parseVersion(version) {
1282
+ const core = String(version).trim().replace(/^v/, "").split("-")[0];
1283
+ const [major, minor, patch] = core.split(".").map((n) => parseInt(n, 10) || 0);
1284
+ return [major || 0, minor || 0, patch || 0];
1285
+ }
1286
+ function isNewer(latest, current) {
1287
+ const a = parseVersion(latest);
1288
+ const b = parseVersion(current);
1289
+ for (let i = 0; i < 3; i++) {
1290
+ if (a[i] > b[i]) return true;
1291
+ if (a[i] < b[i]) return false;
1292
+ }
1293
+ return false;
1294
+ }
1295
+ function paint(text, code) {
1296
+ return process.stdout.isTTY && !process.env.NO_COLOR ? `\x1B[${code}m${text}\x1B[0m` : text;
1297
+ }
1298
+ function renderUpdateBox(current, latest) {
1299
+ const lines = [
1300
+ { text: `Update available ${current} -> ${latest}`, color: "1;33" },
1301
+ { text: `Run ${UPDATE_COMMAND} to update`, color: "36" }
1302
+ ];
1303
+ const width = Math.max(...lines.map((l) => l.text.length));
1304
+ const border = `+${"-".repeat(width + 4)}+`;
1305
+ const blank = `|${" ".repeat(width + 4)}|`;
1306
+ const rows = lines.map((l) => `|${paint(` ${l.text}${" ".repeat(width - l.text.length)} `, l.color)}|`);
1307
+ return [border, blank, ...rows, blank, border].join("\n");
1308
+ }
1309
+ function readCache() {
1310
+ return readJson(CACHE_FILE);
1311
+ }
1312
+ function scheduleUpdateCheck() {
1313
+ try {
1314
+ const cache = readCache();
1315
+ if (cache && Date.now() - cache.checkedAt < CHECK_INTERVAL_MS) return;
1316
+ writeFileSync6(CACHE_FILE, JSON.stringify({ latest: cache?.latest ?? null, checkedAt: Date.now() }));
1317
+ const child = spawn(process.execPath, ["-e", CHILD_SCRIPT], {
1318
+ detached: true,
1319
+ stdio: "ignore",
1320
+ windowsHide: true,
1321
+ env: { ...process.env, E_URL: REGISTRY_URL, E_FILE: CACHE_FILE }
1322
+ });
1323
+ child.unref();
1324
+ } catch {
1325
+ }
1326
+ }
1327
+ function runUpdate() {
1328
+ try {
1329
+ const result = spawnSync("npm", ["i", "-g", "enigma-cli@latest"], {
1330
+ stdio: "inherit",
1331
+ shell: process.platform === "win32"
1332
+ });
1333
+ if (result.status === 0) console.log("Updated. Re-run your command to use the new version.");
1334
+ else console.log(`Update did not complete. Run '${UPDATE_COMMAND}' manually.`);
1335
+ } catch {
1336
+ console.log(`Could not run the update. Run '${UPDATE_COMMAND}' manually.`);
1337
+ }
1338
+ }
1339
+ async function notifyUpdate(current, interactive) {
1340
+ if (!process.stdout.isTTY || process.env.CI) return;
1341
+ try {
1342
+ if (!readConfig().config.updateNotifier) return;
1343
+ scheduleUpdateCheck();
1344
+ const cache = readCache();
1345
+ const latest = cache?.latest ? String(cache.latest).replace(/[^\w.+-]/g, "") : "";
1346
+ if (!latest || !isNewer(latest, current)) return;
1347
+ process.stdout.write(`
1348
+ ${renderUpdateBox(current, latest)}
1349
+ `);
1350
+ if (!interactive) return;
1351
+ const ok = await p5.confirm({ message: `Update now with ${UPDATE_COMMAND}?`, initialValue: true });
1352
+ if (p5.isCancel(ok) || !ok) return;
1353
+ runUpdate();
1354
+ } catch {
1355
+ }
1356
+ }
1357
+
995
1358
  // src/cli.ts
996
1359
  var __dirname3 = dirname4(fileURLToPath4(import.meta.url));
997
- var PKG = readJson(join9(__dirname3, "..", "package.json")) || {};
1360
+ var PKG = readJson(join10(__dirname3, "..", "package.json")) || {};
998
1361
  var COMMANDS = /* @__PURE__ */ new Set(["install", "security", "guard", "seal", "check", "config", "help", "version"]);
999
1362
  function parseArgs(argv) {
1000
1363
  const opts = {
@@ -1103,15 +1466,19 @@ Usage:
1103
1466
  enigma [command] [options]
1104
1467
 
1105
1468
  Commands:
1106
- (none) Interactive menu: pick which features to set up
1469
+ (none) Interactive hub: configure settings or set up features
1107
1470
  install Install/update agent skills (Claude Code, Codex, opencode)
1108
1471
  security Set up git security hooks in the current repo
1109
1472
  guard [--all] Run the commit guard (staged files, or --all for every tracked file)
1110
- 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)
1111
1475
  seal Maintenance: (re)compute skill content hashes
1112
1476
  check Integrity gate: verify skills are well-formed and sealed
1113
1477
  help, version
1114
1478
 
1479
+ Config keys: commit-emoji, update-notifier, claude-attribution,
1480
+ bypass-claude, bypass-codex, bypass-opencode
1481
+
1115
1482
  Install options:
1116
1483
  -g, --global Install at user level
1117
1484
  -l, --local Install into the current project
@@ -1142,12 +1509,16 @@ Examples:
1142
1509
  }
1143
1510
  async function run(argv) {
1144
1511
  const opts = parseArgs(argv);
1512
+ const interactive = Boolean(process.stdout.isTTY) && !opts.yes;
1513
+ const version = PKG.version || "0.0.0";
1145
1514
  if (opts.help || opts.command === "help") {
1146
1515
  printHelp();
1516
+ await notifyUpdate(version, interactive);
1147
1517
  return;
1148
1518
  }
1149
1519
  if (opts.version || opts.command === "version") {
1150
- console.log(PKG.version || "0.0.0");
1520
+ console.log(version);
1521
+ await notifyUpdate(version, interactive);
1151
1522
  return;
1152
1523
  }
1153
1524
  if (opts.command === "seal") return sealSources();
@@ -1156,44 +1527,45 @@ async function run(argv) {
1156
1527
  process.exit(runGuardCli(opts.all));
1157
1528
  }
1158
1529
  if (opts.command === "config") {
1159
- process.exit(runConfigCli(opts.positionals, opts.scope));
1530
+ process.exit(await runConfigCli(opts.positionals, opts.scope, interactive));
1160
1531
  }
1161
- const interactive = Boolean(process.stdout.isTTY) && !opts.yes;
1162
1532
  if (opts.command === "install") {
1163
- p4.intro("enigma - install agent skills");
1533
+ p6.intro("enigma - install agent skills");
1164
1534
  await installSkills(opts, interactive);
1165
- p4.outro("Done.");
1535
+ p6.outro("Done.");
1536
+ await notifyUpdate(version, interactive);
1166
1537
  return;
1167
1538
  }
1168
1539
  if (opts.command === "security") {
1169
- p4.intro("enigma - git security hooks");
1540
+ p6.intro("enigma - git security hooks");
1170
1541
  const done = await setupGitHooks(opts, interactive);
1171
- p4.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);
1548
+ await notifyUpdate(version, interactive);
1172
1549
  return;
1173
1550
  }
1174
- p4.intro("enigma");
1175
- let features;
1176
- if (interactive) {
1177
- const r = await p4.multiselect({
1178
- 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?",
1179
1555
  options: [
1180
- { value: "skills", label: "Agent skills", hint: "Claude Code, Codex, opencode" },
1181
- { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" }
1182
- ],
1183
- initialValues: ["skills"],
1184
- 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
+ ]
1185
1561
  });
1186
- if (p4.isCancel(r)) {
1187
- p4.cancel("Aborted.");
1188
- return;
1189
- }
1190
- features = r;
1191
- } else {
1192
- 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);
1193
1566
  }
1194
- if (features.includes("skills")) await installSkills(opts, interactive);
1195
- if (features.includes("security")) await setupGitHooks(opts, interactive);
1196
- p4.outro("Done.");
1567
+ p6.outro("Done.");
1568
+ await notifyUpdate(version, interactive);
1197
1569
  }
1198
1570
 
1199
1571
  // src/bin/enigma.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.1.0",
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": {