enigma-cli 1.1.3 → 1.1.4

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.3",
6
+ "cliVersion": "1.1.4",
7
7
  "sha": "c442bc9e39a7710cb709ef2abb8d15ecd8aa16ed4f5c8af92b7af6877401cba4"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ciphera-style-policy
3
- description: Ciphera code style conventions - mandatory formatting and language idioms for source code (TypeScript-first, applies to every language): American-English naming, double quotes, string interpolation, length-sorted imports, 4-space indentation, comment/JSDoc format, compact single-line blocks, and code-level anti-patterns (barrel files, external CDN/hosting dependencies). Use whenever writing, refactoring, or reviewing source code.
3
+ description: Ciphera code style conventions - mandatory formatting and language idioms for source code (TypeScript-first, applies to every language) - American-English naming, double quotes, string interpolation, length-sorted imports, 4-space indentation, comment/JSDoc format, compact single-line blocks, and code-level anti-patterns (barrel files, external CDN/hosting dependencies). Use whenever writing, refactoring, or reviewing source code.
4
4
  ---
5
5
 
6
6
  # Ciphera Code Style Policy
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ciphera-style-policy",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
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.3",
7
- "sha": "f8602bb79fbbe063ab39fbd59d0b7844a22c3a1583fcd11c1b4f98a2fe8ddc86"
6
+ "cliVersion": "1.1.4",
7
+ "sha": "74f638aec13e8c93257fe1ad604c28b07e9a7c456796a4ceefcc99217d9e7039"
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
7
7
  "sha": "6375d835c2aef2c9bd31ce116444dc3d796f510f9970a213aa3ac4696d7e21b9"
8
8
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: frontend-policy
3
- description: Frontend architecture - reusable components, abstraction thresholds, state management, client-side caching (localStorage/sessionStorage to avoid redundant server calls and survive rate limits), and optimistic UI with rollback. Use when building or changing UI components, client state, data fetching/caching, or any frontend structure.
3
+ description: Frontend architecture - reusable components, abstraction thresholds, state management, client-side caching (localStorage/sessionStorage to avoid redundant server calls and survive rate limits), optimistic UI with rollback, and periodic React code-health audits (react-doctor). Use when building or changing UI components, client state, data fetching/caching, auditing React code, or any frontend structure.
4
4
  ---
5
5
 
6
6
  # Frontend Architecture Policy
@@ -114,4 +114,18 @@ Cache on the client to avoid redundant server round-trips and to keep the app us
114
114
 
115
115
  - Use semantic markup and accessible interactive elements by default.
116
116
  - Handle loading, empty, and error states explicitly for every async view.
117
- - Validate user input in real time per validation-policy; never rely on the UI as the only validation layer.
117
+ - Validate user input in real time per validation-policy; never rely on the UI as the only validation layer.
118
+
119
+ ---
120
+
121
+ ## React Code Health Audit (react-doctor)
122
+
123
+ - Periodically audit React code with React Doctor, a fast static analyzer that scores the codebase across performance, security, correctness, accessibility, bundle size, and architecture (60+ rules, framework-aware: Next.js, Vite, React Native, Expo, ...). It is purpose-built to catch the bad React that agents tend to write.
124
+ - Run it from the project root; no install needed:
125
+ - `npx -y react-doctor@latest .`
126
+ - When to run it:
127
+ - Occasionally during React work, and as a final sanity check before committing a non-trivial React change.
128
+ - After large refactors, or when touching performance-sensitive components.
129
+ - It is an advisory audit, not a gate: read the findings, fix the high-value issues (real performance, correctness, or accessibility problems), and skip noise that does not apply. Never block delivery on the score alone.
130
+ - Review anything it proposes to auto-fix as a normal diff before keeping it; do not apply changes blindly (treat tool output as untrusted per security-policy).
131
+ - It runs locally and analyzes read-only by default. Rules for wiring it into CI as a deterministic gate live in dependency-policy and testing-policy.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "frontend-policy",
3
- "version": "1.0.0",
3
+ "version": "1.1.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.3",
7
- "sha": "b0355b0e15f9f528d32adf19f0722d2727cd64d6b3544307ecc7a3141338f023"
6
+ "cliVersion": "1.1.4",
7
+ "sha": "33fa1e9f667ef26203a3d6c892121efe12b0cddb706c195492fa97e080fba115"
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
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.3",
6
+ "cliVersion": "1.1.4",
7
7
  "sha": "a33622a2f810ee4cea39824cb1a7ca34b355a917d4224025df50d77dd74f0b3a"
8
8
  }
package/dist/enigma.js CHANGED
@@ -526,29 +526,113 @@ var init_settings_registry = __esm({
526
526
  // src/tui/settings.ts
527
527
  var settings_exports = {};
528
528
  __export(settings_exports, {
529
+ buildApp: () => buildApp,
530
+ runHomeTui: () => runHomeTui,
529
531
  runSettingsTui: () => runSettingsTui
530
532
  });
533
+ async function runHomeTui(runAction) {
534
+ await runTui("home", runAction);
535
+ }
531
536
  async function runSettingsTui() {
537
+ await runTui("settings");
538
+ }
539
+ async function runTui(initialView, runAction) {
532
540
  if (!process.stdout.isTTY) return;
533
541
  const React = (await import("react")).default;
534
542
  const ink = await import("ink");
535
- const { render, useApp, useInput } = ink;
543
+ const { render } = ink;
544
+ const h = React.createElement;
545
+ const restore = () => {
546
+ try {
547
+ process.stdout.write(LEAVE_ALT);
548
+ } catch {
549
+ }
550
+ };
551
+ for (; ; ) {
552
+ const state = { leave: "quit" };
553
+ const App = buildApp(React, ink, { initialView, onLeave: (a) => {
554
+ state.leave = a;
555
+ } });
556
+ process.stdout.write(ENTER_ALT);
557
+ process.on("exit", restore);
558
+ try {
559
+ const app = render(h(App), { exitOnCtrlC: true });
560
+ await app.waitUntilExit();
561
+ } finally {
562
+ process.removeListener("exit", restore);
563
+ restore();
564
+ }
565
+ if (runAction && (state.leave === "skills" || state.leave === "security")) {
566
+ await runAction(state.leave);
567
+ continue;
568
+ }
569
+ break;
570
+ }
571
+ }
572
+ function buildApp(React, ink, opts) {
573
+ const { useApp, useInput, useStdout } = ink;
536
574
  const Box = ink.Box;
537
575
  const Text = ink.Text;
538
- const { useState } = React;
576
+ const { useState, useEffect } = React;
539
577
  const h = React.createElement;
540
- function App() {
578
+ const canGoHome = opts.initialView === "home";
579
+ return function App() {
541
580
  const { exit } = useApp();
581
+ const { stdout } = useStdout();
582
+ const [size, setSize] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
583
+ const [view, setView] = useState(opts.initialView);
584
+ const [homeIndex, setHomeIndex] = useState(0);
542
585
  const [scope, setScope] = useState("global");
543
586
  const [focusSettings, setFocusSettings] = useState(false);
544
587
  const [catIndex, setCatIndex] = useState(0);
545
588
  const [setIndex, setSetIndex] = useState(0);
546
- const [, bump] = useState(0);
547
- const category = CATEGORIES[catIndex];
548
- const settings = category.settings;
549
- useInput((input, key) => {
550
- if (input === "q" || key.escape || key.ctrl && input === "c") {
589
+ const [, redraw] = useState(0);
590
+ useEffect(() => {
591
+ const onResize = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
592
+ stdout.on("resize", onResize);
593
+ return () => {
594
+ stdout.off("resize", onResize);
595
+ };
596
+ }, [stdout]);
597
+ const back = () => {
598
+ if (canGoHome) setView("home");
599
+ else {
600
+ opts.onLeave("quit");
551
601
  exit();
602
+ }
603
+ };
604
+ useInput((input, key) => {
605
+ if (view === "home") {
606
+ if (input === "q" || key.escape) {
607
+ opts.onLeave("quit");
608
+ exit();
609
+ return;
610
+ }
611
+ if (key.upArrow || input === "k") {
612
+ setHomeIndex((i) => Math.max(0, i - 1));
613
+ return;
614
+ }
615
+ if (key.downArrow || input === "j") {
616
+ setHomeIndex((i) => Math.min(HOME_ITEMS.length - 1, i + 1));
617
+ return;
618
+ }
619
+ if (key.return || input === " ") {
620
+ const item = HOME_ITEMS[homeIndex];
621
+ if (item.key === "settings") {
622
+ setView("settings");
623
+ setFocusSettings(false);
624
+ } else if (item.key === "exit") {
625
+ opts.onLeave("quit");
626
+ exit();
627
+ } else {
628
+ opts.onLeave(item.key);
629
+ exit();
630
+ }
631
+ }
632
+ return;
633
+ }
634
+ if (input === "q" || key.escape) {
635
+ back();
552
636
  return;
553
637
  }
554
638
  if (input === "g") {
@@ -576,7 +660,7 @@ async function runSettingsTui() {
576
660
  return;
577
661
  }
578
662
  if (key.downArrow || input === "j") {
579
- if (focusSettings) setSetIndex((i) => Math.min(settings.length - 1, i + 1));
663
+ if (focusSettings) setSetIndex((i) => Math.min(CATEGORIES[catIndex].settings.length - 1, i + 1));
580
664
  else {
581
665
  setCatIndex((i) => Math.min(CATEGORIES.length - 1, i + 1));
582
666
  setSetIndex(0);
@@ -588,64 +672,115 @@ async function runSettingsTui() {
588
672
  setFocusSettings(true);
589
673
  return;
590
674
  }
591
- const setting = settings[setIndex];
675
+ const setting = CATEGORIES[catIndex].settings[setIndex];
592
676
  setting.write(!setting.read(scope), scope);
593
- bump((n) => n + 1);
677
+ redraw((n) => n + 1);
594
678
  }
595
679
  });
596
- const header = h(
680
+ const titleBar = (right2) => h(
597
681
  Box,
598
- { marginBottom: 1 },
599
- h(Text, { bold: true, color: "cyan" }, "enigma settings"),
600
- h(Text, { dimColor: true }, " scope: "),
601
- h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
602
- h(Text, { dimColor: true }, " (g to change)")
682
+ { width: size.columns, paddingX: 1, justifyContent: "space-between" },
683
+ h(Text, { bold: true, color: "cyan" }, "enigma"),
684
+ right2
603
685
  );
604
- const left = h(Box, {
605
- flexDirection: "column",
606
- borderStyle: "round",
607
- borderColor: focusSettings ? "gray" : "cyan",
608
- paddingX: 1,
609
- width: 26,
610
- marginRight: 1
611
- }, CATEGORIES.map((c, i) => h(Text, {
612
- key: c.title,
613
- color: i === catIndex ? "cyan" : void 0,
614
- inverse: !focusSettings && i === catIndex
615
- }, `${i === catIndex ? ">" : " "} ${c.title}`)));
616
- const right = h(Box, {
617
- flexDirection: "column",
618
- borderStyle: "round",
619
- borderColor: focusSettings ? "cyan" : "gray",
620
- paddingX: 1,
621
- flexGrow: 1
622
- }, [
623
- h(Text, { key: "__blurb", dimColor: true }, category.blurb),
624
- ...settings.map((s, i) => {
625
- const on = s.read(scope);
626
- const selected = focusSettings && i === setIndex;
627
- return h(
628
- Box,
629
- { key: s.key, justifyContent: "space-between" },
630
- h(Text, { inverse: selected }, `${selected ? ">" : " "} ${s.label}${s.globalOnly ? " (global)" : ""}`),
631
- h(Text, { bold: true, color: on ? "green" : "gray" }, ` ${valueLabel(on)}`)
632
- );
633
- })
634
- ]);
635
- const footer = h(Box, { marginTop: 1 }, h(
686
+ const body = view === "home" ? renderHome(h, Box, Text, homeIndex) : renderSettings(h, Box, Text, { size, scope, focusSettings, catIndex, setIndex });
687
+ const footer = h(Box, { width: size.columns, paddingX: 1 }, h(
636
688
  Text,
637
689
  { dimColor: true },
638
- "up/down move - tab/left/right switch pane - enter/space toggle - g scope - q quit"
690
+ view === "home" ? "up/down move enter select q quit" : `up/down move tab/left/right switch enter toggle g scope ${canGoHome ? "esc back" : "q quit"}`
639
691
  ));
640
- return h(Box, { flexDirection: "column" }, header, h(Box, {}, left, right), footer);
641
- }
642
- const app = render(h(App));
643
- await app.waitUntilExit();
692
+ const right = view === "home" ? h(Text, { dimColor: true }, "main menu") : h(
693
+ Box,
694
+ {},
695
+ h(Text, { dimColor: true }, "scope "),
696
+ h(Text, { bold: true, color: scope === "global" ? "green" : "yellow" }, scope),
697
+ h(Text, { dimColor: true }, " (g to change)")
698
+ );
699
+ return h(
700
+ Box,
701
+ { width: size.columns, height: size.rows, flexDirection: "column" },
702
+ titleBar(right),
703
+ h(Box, { flexGrow: 1 }, body),
704
+ footer
705
+ );
706
+ };
644
707
  }
708
+ function renderHome(h, Box, Text, homeIndex) {
709
+ const items = HOME_ITEMS.map((item, i) => h(
710
+ Box,
711
+ { key: item.key, flexDirection: "column" },
712
+ h(Text, { inverse: i === homeIndex, bold: i === homeIndex }, ` ${item.label} `),
713
+ i === homeIndex && item.hint ? h(Text, { key: "h", dimColor: true }, ` ${item.hint}`) : null
714
+ ));
715
+ return h(Box, {
716
+ flexDirection: "column",
717
+ borderStyle: "round",
718
+ borderColor: "cyan",
719
+ paddingX: 1,
720
+ flexGrow: 1
721
+ }, [
722
+ h(Text, { key: "__t", bold: true, color: "cyan" }, "What would you like to do?"),
723
+ h(Box, { key: "__sp", marginTop: 1, flexDirection: "column" }, items)
724
+ ]);
725
+ }
726
+ function renderSettings(h, Box, Text, s) {
727
+ const category = CATEGORIES[s.catIndex];
728
+ const settings = category.settings;
729
+ const focused = settings[s.setIndex];
730
+ const sidebarWidth = Math.min(28, Math.max(18, Math.floor(s.size.columns * 0.3)));
731
+ const sidebar = h(Box, {
732
+ flexDirection: "column",
733
+ borderStyle: "round",
734
+ borderColor: s.focusSettings ? "gray" : "cyan",
735
+ paddingX: 1,
736
+ width: sidebarWidth,
737
+ marginRight: 1
738
+ }, [
739
+ h(Text, { key: "__t", bold: true, dimColor: true }, "CATEGORIES"),
740
+ ...CATEGORIES.map((c, i) => h(Text, {
741
+ key: c.title,
742
+ inverse: !s.focusSettings && i === s.catIndex,
743
+ color: i === s.catIndex ? "cyan" : void 0
744
+ }, ` ${c.title} `))
745
+ ]);
746
+ const rows = settings.map((setting, i) => {
747
+ const on = setting.read(s.scope);
748
+ const selected = s.focusSettings && i === s.setIndex;
749
+ return h(
750
+ Box,
751
+ { key: setting.key, justifyContent: "space-between" },
752
+ h(Text, { inverse: selected, bold: selected }, ` ${setting.label}${setting.globalOnly ? " (global)" : ""} `),
753
+ h(Text, { bold: true, color: on ? "green" : "gray" }, `${valueLabel(on)} `)
754
+ );
755
+ });
756
+ const panel = h(Box, {
757
+ flexDirection: "column",
758
+ borderStyle: "round",
759
+ borderColor: s.focusSettings ? "cyan" : "gray",
760
+ paddingX: 1,
761
+ flexGrow: 1
762
+ }, [
763
+ h(Text, { key: "__b", bold: true, color: "cyan" }, category.title),
764
+ h(Text, { key: "__bl", dimColor: true }, category.blurb),
765
+ h(Box, { key: "__rows", marginTop: 1, flexDirection: "column" }, rows),
766
+ h(Box, { key: "__grow", flexGrow: 1 }),
767
+ h(Text, { key: "__hint", dimColor: true, wrap: "truncate-end" }, focused.hint)
768
+ ]);
769
+ return h(Box, { flexGrow: 1 }, sidebar, panel);
770
+ }
771
+ var ENTER_ALT, LEAVE_ALT, HOME_ITEMS;
645
772
  var init_settings = __esm({
646
773
  "src/tui/settings.ts"() {
647
774
  "use strict";
648
775
  init_settings_registry();
776
+ ENTER_ALT = "\x1B[?1049h\x1B[?25l";
777
+ LEAVE_ALT = "\x1B[?25h\x1B[?1049l";
778
+ HOME_ITEMS = [
779
+ { key: "settings", label: "Configure settings", hint: "emoji, attribution, permission bypass, ..." },
780
+ { key: "skills", label: "Install agent skills", hint: "Claude Code, Codex, opencode" },
781
+ { key: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" },
782
+ { key: "exit", label: "Exit", hint: "leave enigma" }
783
+ ];
649
784
  }
650
785
  });
651
786
 
@@ -1671,25 +1806,18 @@ async function run(argv) {
1671
1806
  await notifyUpdate(version, interactive);
1672
1807
  return;
1673
1808
  }
1674
- p5.intro("enigma");
1675
- for (; ; ) {
1676
- const action = await p5.select({
1677
- message: "What would you like to do?",
1678
- options: [
1679
- { value: "config", label: "Configure settings", hint: "emoji, attribution, permission bypass, ..." },
1680
- { value: "skills", label: "Install agent skills", hint: "Claude Code, Codex, opencode" },
1681
- { value: "security", label: "Git security hooks", hint: "block secrets, .env, node_modules on commit" },
1682
- { value: "exit", label: "Exit" }
1683
- ]
1684
- });
1685
- if (p5.isCancel(action) || action === "exit") break;
1686
- if (action === "config") {
1687
- const { runSettingsTui: runSettingsTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
1688
- await runSettingsTui2();
1689
- } else if (action === "skills") await installSkills(opts, interactive);
1690
- else if (action === "security") await setupGitHooks(opts, interactive);
1691
- }
1692
- p5.outro("Done.");
1809
+ const { runHomeTui: runHomeTui2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
1810
+ await runHomeTui2(async (action) => {
1811
+ if (action === "skills") {
1812
+ p5.intro("enigma - install agent skills");
1813
+ await installSkills(opts, interactive);
1814
+ p5.outro("Done.");
1815
+ } else if (action === "security") {
1816
+ p5.intro("enigma - git security hooks");
1817
+ const done = await setupGitHooks(opts, interactive);
1818
+ p5.outro(done ? "Git hooks configured." : "No changes made.");
1819
+ }
1820
+ });
1693
1821
  await notifyUpdate(version, interactive);
1694
1822
  }
1695
1823
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enigma-cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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": {