delimit-cli 3.11.11 → 3.12.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.12.0] - 2026-03-26
4
+
5
+ ### Added
6
+ - Cross-model hook system: session-start, pre-tool, and pre-commit hooks for Claude Code, Codex, and Gemini CLI
7
+ - `delimit export` and `delimit import` commands for shareable governance config
8
+ - `delimit hook <event>` commands for manual hook invocation
9
+ - `delimit uninstall` removes hooks from all AI tools cleanly
10
+ - Pre-push hooks for catching governance violations before remote push
11
+ - Cursor and Codex adapters for native integration
12
+
13
+ ### Changed
14
+ - "Keep Building." success message displayed on lint/diff/doctor pass
15
+ - Zero-config action improvements for smoother CI integration
16
+
3
17
  ## [3.11.10] - 2026-03-24
4
18
 
5
19
  ### Added
@@ -9,6 +9,7 @@ const chalk = require('chalk');
9
9
  const inquirer = require('inquirer');
10
10
  const DelimitAuthSetup = require('../lib/auth-setup');
11
11
  const DelimitHooksInstaller = require('../lib/hooks-installer');
12
+ const crossModelHooks = require('../lib/cross-model-hooks');
12
13
 
13
14
  const AGENT_URL = `http://127.0.0.1:${process.env.DELIMIT_AGENT_PORT || 7823}`;
14
15
  const program = new Command();
@@ -535,6 +536,36 @@ program
535
536
  changes.push({ target: '~/.delimit/shims/', action: 'Remove CLI shims directory' });
536
537
  }
537
538
 
539
+ // 8. Cross-model governance hooks (LED-202)
540
+ const claudeSettingsPath = path.join(HOME, '.claude', 'settings.json');
541
+ if (fs.existsSync(claudeSettingsPath)) {
542
+ try {
543
+ const cfg = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
544
+ if (cfg.hooks) {
545
+ const hasDelimitHook = Object.values(cfg.hooks).some(arr =>
546
+ Array.isArray(arr) && arr.some(h => h.command && h.command.includes('delimit-cli'))
547
+ );
548
+ if (hasDelimitHook) {
549
+ changes.push({ target: '~/.claude/settings.json', action: 'Remove Delimit governance hooks' });
550
+ }
551
+ }
552
+ } catch (e) {}
553
+ }
554
+ const codexInstructions = path.join(HOME, '.codex', 'instructions.md');
555
+ if (fs.existsSync(codexInstructions)) {
556
+ const content = fs.readFileSync(codexInstructions, 'utf8');
557
+ if (content.includes('delimit:hooks-start')) {
558
+ changes.push({ target: '~/.codex/instructions.md', action: 'Remove Delimit hook instructions' });
559
+ }
560
+ }
561
+ const geminiGovMd = path.join(HOME, '.gemini', 'GEMINI.md');
562
+ if (fs.existsSync(geminiGovMd)) {
563
+ const content = fs.readFileSync(geminiGovMd, 'utf8');
564
+ if (content.includes('Delimit Governance')) {
565
+ changes.push({ target: '~/.gemini/GEMINI.md', action: 'Remove Delimit governance file' });
566
+ }
567
+ }
568
+
538
569
  if (changes.length === 0) {
539
570
  console.log(chalk.green('\nNo Delimit integrations found. Nothing to remove.\n'));
540
571
  return;
@@ -625,6 +656,14 @@ program
625
656
  } catch (e) {}
626
657
  }
627
658
 
659
+ // Remove cross-model governance hooks (LED-202)
660
+ try {
661
+ const removedFrom = crossModelHooks.removeAllHooks();
662
+ if (removedFrom.length > 0) {
663
+ console.log(chalk.green(`✓ Removed governance hooks from: ${removedFrom.join(', ')}`));
664
+ }
665
+ } catch (e) { /* cross-model-hooks module not critical */ }
666
+
628
667
  console.log(chalk.green('\n Delimit has been completely removed.'));
629
668
  console.log(chalk.gray(` Backups saved to: ${backupDir}`));
630
669
  console.log(chalk.gray(' Your data in ~/.delimit/ has been preserved.'));
@@ -701,95 +740,86 @@ program
701
740
  await proxyAITool(tool, args);
702
741
  });
703
742
 
704
- // Hook handler (called by Git hooks)
705
- program
706
- .command('hook <type>')
707
- .description('Internal hook handler')
743
+ // ---------------------------------------------------------------------------
744
+ // LED-202: Cross-model hook commands
745
+ // ---------------------------------------------------------------------------
708
746
 
709
- .action(async (type) => {
710
- await ensureAgent();
711
-
712
- // Gather context
713
- const context = {
714
- command: type,
715
- pwd: process.cwd(),
716
- gitBranch: 'unknown',
717
- files: [],
718
- diff: ''
719
- };
720
-
721
- // Try to get Git info, but don't fail if not in repo
747
+ const hookCmd = program
748
+ .command('hook <event> [tool_name]')
749
+ .description('Governance hook handler (session-start | pre-tool | pre-commit)')
750
+ .action(async (event, toolName) => {
722
751
  try {
723
- context.gitBranch = execSync('git branch --show-current 2>/dev/null').toString().trim() || 'unknown';
724
- } catch (e) {
725
- // Not in a Git repo or Git not available
726
- context.gitBranch = 'unknown';
727
- }
728
-
729
- if (type === 'pre-commit') {
730
- try {
731
- context.files = execSync('git diff --cached --name-only 2>/dev/null').toString().split('\n').filter(f => f);
732
- context.diff = execSync('git diff --cached 2>/dev/null').toString();
733
- } catch (e) {
734
- // Not in a Git repo or no staged changes
735
- context.files = [];
736
- context.diff = '';
737
- }
738
- } else if (type === 'pre-push') {
739
- try {
740
- // Get commits to be pushed
741
- context.files = execSync('git diff --name-only @{upstream}...HEAD 2>/dev/null').toString().split('\n').filter(f => f);
742
- context.diff = execSync('git diff @{upstream}...HEAD 2>/dev/null').toString();
743
- } catch (e) {
744
- // No upstream or not in repo
745
- context.files = [];
746
- context.diff = '';
747
- }
748
- }
749
-
750
- // Query agent for decision
751
- const { data: decision } = await axios.post(`${AGENT_URL}/evaluate`, context);
752
-
753
- // Display decision
754
- if (decision.message) {
755
- const color = decision.action === 'block' ? chalk.red :
756
- decision.action === 'prompt' ? chalk.yellow :
757
- chalk.blue;
758
- console.log(color(decision.message));
759
- }
760
-
761
- // Handle the decision
762
- if (decision.action === 'block') {
763
- if (decision.requiresOverride) {
764
- console.log(chalk.red('Action blocked. Cannot override in enforce mode.'));
765
- process.exit(1);
766
- } else {
767
- const { override } = await inquirer.prompt([{
768
- type: 'confirm',
769
- name: 'override',
770
- message: 'Override and continue?',
771
- default: false
772
- }]);
773
-
774
- if (!override) {
775
- process.exit(1);
776
- }
752
+ switch (event) {
753
+ case 'session-start':
754
+ await crossModelHooks.hookSessionStart();
755
+ break;
756
+ case 'pre-tool':
757
+ await crossModelHooks.hookPreTool(toolName || 'unknown');
758
+ break;
759
+ case 'pre-commit':
760
+ await crossModelHooks.hookPreCommit();
761
+ break;
762
+ default:
763
+ // Legacy: fall back to agent-based hook evaluation
764
+ await ensureAgent();
765
+ const context = {
766
+ command: event,
767
+ pwd: process.cwd(),
768
+ gitBranch: 'unknown',
769
+ files: [],
770
+ diff: ''
771
+ };
772
+ try {
773
+ context.gitBranch = execSync('git branch --show-current 2>/dev/null').toString().trim() || 'unknown';
774
+ } catch (e) { context.gitBranch = 'unknown'; }
775
+
776
+ if (event === 'pre-push') {
777
+ try {
778
+ context.files = execSync('git diff --name-only @{upstream}...HEAD 2>/dev/null').toString().split('\n').filter(f => f);
779
+ context.diff = execSync('git diff @{upstream}...HEAD 2>/dev/null').toString();
780
+ } catch (e) {
781
+ context.files = [];
782
+ context.diff = '';
783
+ }
784
+ }
785
+
786
+ const { data: decision } = await axios.post(`${AGENT_URL}/evaluate`, context);
787
+ if (decision.message) {
788
+ const color = decision.action === 'block' ? chalk.red :
789
+ decision.action === 'prompt' ? chalk.yellow :
790
+ chalk.blue;
791
+ console.log(color(decision.message));
792
+ }
793
+ if (decision.action === 'block') {
794
+ if (decision.requiresOverride) {
795
+ console.log(chalk.red('Action blocked. Cannot override in enforce mode.'));
796
+ process.exit(1);
797
+ } else {
798
+ const { override } = await inquirer.prompt([{
799
+ type: 'confirm',
800
+ name: 'override',
801
+ message: 'Override and continue?',
802
+ default: false
803
+ }]);
804
+ if (!override) process.exit(1);
805
+ }
806
+ } else if (decision.action === 'prompt') {
807
+ const { proceed } = await inquirer.prompt([{
808
+ type: 'confirm',
809
+ name: 'proceed',
810
+ message: 'Continue with this action?',
811
+ default: false
812
+ }]);
813
+ if (!proceed) process.exit(1);
814
+ }
815
+ process.exit(0);
777
816
  }
778
- } else if (decision.action === 'prompt') {
779
- const { proceed } = await inquirer.prompt([{
780
- type: 'confirm',
781
- name: 'proceed',
782
- message: 'Continue with this action?',
783
- default: false
784
- }]);
785
-
786
- if (!proceed) {
787
- process.exit(1);
817
+ } catch (err) {
818
+ // Hooks must never block the AI tool -- fail open
819
+ if (process.env.DELIMIT_DEBUG) {
820
+ process.stderr.write(`[Delimit] Hook error: ${err.message}\n`);
788
821
  }
789
822
  }
790
-
791
- // Action allowed
792
- process.exit(0);
793
823
  });
794
824
 
795
825
  // ═══════════════════════════════════════════════════════════════════════
@@ -1135,6 +1165,7 @@ program
1135
1165
  console.log('');
1136
1166
  if (fail === 0 && warn === 0) {
1137
1167
  console.log(chalk.green.bold(' All checks passed! Ready to lint.\n'));
1168
+ console.log('Keep Building.\n');
1138
1169
  } else if (fail === 0) {
1139
1170
  console.log(chalk.yellow.bold(` ${ok} passed, ${warn} warning(s). Setup looks good.\n`));
1140
1171
  } else {
@@ -1263,6 +1294,10 @@ program
1263
1294
  console.log('');
1264
1295
  }
1265
1296
 
1297
+ if (decision === 'pass') {
1298
+ console.log('Keep Building.\n');
1299
+ }
1300
+
1266
1301
  process.exit(result.exit_code || 0);
1267
1302
  } catch (err) {
1268
1303
  console.error(chalk.red(`Error: ${err.message}`));
@@ -1295,6 +1330,10 @@ program
1295
1330
  console.log(` ${tag} ${c.message}`);
1296
1331
  });
1297
1332
  console.log('');
1333
+
1334
+ if (result.breaking_changes === 0) {
1335
+ console.log('Keep Building.\n');
1336
+ }
1298
1337
  } catch (err) {
1299
1338
  console.error(chalk.red(`Error: ${err.message}`));
1300
1339
  process.exit(1);
@@ -1428,6 +1467,187 @@ program
1428
1467
  console.log(chalk.dim('Tier: pro'));
1429
1468
  });
1430
1469
 
1470
+ // ---------------------------------------------------------------------------
1471
+ // LED-187: Export governance config as shareable JSON
1472
+ // ---------------------------------------------------------------------------
1473
+
1474
+ /**
1475
+ * Build a governance config bundle from the current project directory.
1476
+ * Returns a plain object ready for JSON serialization.
1477
+ */
1478
+ function buildConfigBundle(cwd) {
1479
+ const bundle = {
1480
+ delimit_config_version: 1,
1481
+ created_at: new Date().toISOString(),
1482
+ project: path.basename(cwd),
1483
+ policies: null,
1484
+ workflow: null,
1485
+ };
1486
+
1487
+ // Read delimit.yml or .delimit/policies.yml
1488
+ const candidates = [
1489
+ path.join(cwd, 'delimit.yml'),
1490
+ path.join(cwd, '.delimit.yml'),
1491
+ path.join(cwd, '.delimit', 'policies.yml'),
1492
+ ];
1493
+ for (const p of candidates) {
1494
+ if (fs.existsSync(p)) {
1495
+ const raw = fs.readFileSync(p, 'utf-8');
1496
+ bundle.policies = { path: path.relative(cwd, p), content: raw };
1497
+ break;
1498
+ }
1499
+ }
1500
+
1501
+ // Read GitHub Action workflow if it exists
1502
+ const workflowPath = path.join(cwd, '.github', 'workflows', 'api-governance.yml');
1503
+ if (fs.existsSync(workflowPath)) {
1504
+ bundle.workflow = {
1505
+ path: '.github/workflows/api-governance.yml',
1506
+ content: fs.readFileSync(workflowPath, 'utf-8'),
1507
+ };
1508
+ }
1509
+
1510
+ return bundle;
1511
+ }
1512
+
1513
+ program
1514
+ .command('export')
1515
+ .description('Export governance config as shareable JSON')
1516
+ .option('-o, --output <file>', 'Write to file instead of stdout')
1517
+ .option('--url', 'Generate a delimit.ai/import share URL')
1518
+ .action(async (options) => {
1519
+ const cwd = process.cwd();
1520
+ const bundle = buildConfigBundle(cwd);
1521
+
1522
+ if (!bundle.policies) {
1523
+ console.error(chalk.red('No governance config found. Run "delimit init" first.'));
1524
+ process.exit(1);
1525
+ }
1526
+
1527
+ const json = JSON.stringify(bundle, null, 2);
1528
+
1529
+ if (options.url) {
1530
+ const encoded = Buffer.from(json).toString('base64');
1531
+ const shareUrl = `https://delimit.ai/import?config=${encoded}`;
1532
+ console.log(chalk.green('Share URL:\n'));
1533
+ console.log(shareUrl);
1534
+ return;
1535
+ }
1536
+
1537
+ if (options.output) {
1538
+ fs.writeFileSync(options.output, json);
1539
+ console.log(chalk.green(`Exported config to ${options.output}`));
1540
+ } else {
1541
+ console.log(json);
1542
+ }
1543
+ });
1544
+
1545
+ // ---------------------------------------------------------------------------
1546
+ // LED-187: Import governance config from file, URL, or base64 string
1547
+ // ---------------------------------------------------------------------------
1548
+
1549
+ /**
1550
+ * Parse a config bundle from various sources: file path, base64 string,
1551
+ * or a delimit.ai/import?config=... URL.
1552
+ */
1553
+ function parseConfigSource(source) {
1554
+ // URL form — extract base64 from query param
1555
+ if (source.startsWith('http://') || source.startsWith('https://')) {
1556
+ const url = new URL(source);
1557
+ const encoded = url.searchParams.get('config');
1558
+ if (!encoded) {
1559
+ throw new Error('URL does not contain a config= parameter');
1560
+ }
1561
+ return JSON.parse(Buffer.from(encoded, 'base64').toString('utf-8'));
1562
+ }
1563
+
1564
+ // File path
1565
+ if (fs.existsSync(source)) {
1566
+ return JSON.parse(fs.readFileSync(source, 'utf-8'));
1567
+ }
1568
+
1569
+ // Assume base64
1570
+ try {
1571
+ return JSON.parse(Buffer.from(source, 'base64').toString('utf-8'));
1572
+ } catch {
1573
+ throw new Error('Could not parse source as file path, URL, or base64');
1574
+ }
1575
+ }
1576
+
1577
+ program
1578
+ .command('import <source>')
1579
+ .description('Import governance config from file, URL, or base64 string')
1580
+ .option('--action', 'Also write the GitHub Action workflow')
1581
+ .option('--yes', 'Skip confirmation prompt')
1582
+ .action(async (source, options) => {
1583
+ let bundle;
1584
+ try {
1585
+ bundle = parseConfigSource(source);
1586
+ } catch (err) {
1587
+ console.error(chalk.red(`Failed to parse config: ${err.message}`));
1588
+ process.exit(1);
1589
+ }
1590
+
1591
+ if (!bundle.policies || !bundle.policies.content) {
1592
+ console.error(chalk.red('Invalid config bundle: missing policies'));
1593
+ process.exit(1);
1594
+ }
1595
+
1596
+ const cwd = process.cwd();
1597
+ const policyDest = path.join(cwd, bundle.policies.path || 'delimit.yml');
1598
+
1599
+ // Show what will change
1600
+ console.log(chalk.blue.bold('\nConfig Import Preview\n'));
1601
+ console.log(` Project: ${chalk.bold(bundle.project || 'unknown')}`);
1602
+ console.log(` Created: ${bundle.created_at || 'unknown'}`);
1603
+ console.log(` Policy file: ${chalk.bold(policyDest)}`);
1604
+ if (options.action && bundle.workflow) {
1605
+ console.log(` Workflow: ${chalk.bold(path.join(cwd, bundle.workflow.path))}`);
1606
+ }
1607
+
1608
+ // Show diff if policy file already exists
1609
+ if (fs.existsSync(policyDest)) {
1610
+ const existing = fs.readFileSync(policyDest, 'utf-8');
1611
+ if (existing === bundle.policies.content) {
1612
+ console.log(chalk.yellow('\n No changes -- imported config matches current config.'));
1613
+ return;
1614
+ }
1615
+ console.log(chalk.yellow('\n Policy file already exists and will be overwritten.'));
1616
+ }
1617
+
1618
+ console.log('');
1619
+
1620
+ if (!options.yes) {
1621
+ const { confirm } = await inquirer.prompt([{
1622
+ type: 'confirm',
1623
+ name: 'confirm',
1624
+ message: 'Apply this config?',
1625
+ default: false,
1626
+ }]);
1627
+ if (!confirm) {
1628
+ console.log(chalk.red('Import cancelled'));
1629
+ return;
1630
+ }
1631
+ }
1632
+
1633
+ // Write policy file
1634
+ const policyDir = path.dirname(policyDest);
1635
+ fs.mkdirSync(policyDir, { recursive: true });
1636
+ fs.writeFileSync(policyDest, bundle.policies.content);
1637
+ console.log(chalk.green(` Created ${policyDest}`));
1638
+
1639
+ // Optionally write workflow
1640
+ if (options.action && bundle.workflow && bundle.workflow.content) {
1641
+ const workflowDest = path.join(cwd, bundle.workflow.path);
1642
+ const wfDir = path.dirname(workflowDest);
1643
+ fs.mkdirSync(wfDir, { recursive: true });
1644
+ fs.writeFileSync(workflowDest, bundle.workflow.content);
1645
+ console.log(chalk.green(` Created ${workflowDest}`));
1646
+ }
1647
+
1648
+ console.log(chalk.green('\nConfig imported successfully.'));
1649
+ });
1650
+
1431
1651
  // Version subcommand alias (users type 'delimit version' not 'delimit -V')
1432
1652
  program
1433
1653
  .command('version')
@@ -567,8 +567,56 @@ echo "[Delimit] ${toolName} not found" >&2; exit 127
567
567
  }
568
568
  log('');
569
569
 
570
- // Step 7: Local dashboard API server
571
- step(7, 'Local dashboard API...');
570
+ // Step 7: Install cross-model governance hooks (LED-202)
571
+ step(7, 'Installing AI assistant hooks...');
572
+
573
+ try {
574
+ const crossModelHooks = require('../lib/cross-model-hooks');
575
+ const hookConfig = crossModelHooks.loadHookConfig();
576
+ const detected = crossModelHooks.detectAITools();
577
+
578
+ if (detected.length === 0) {
579
+ log(` ${dim(' No AI assistants detected -- hooks will be installed when tools are found')}`);
580
+ } else {
581
+ log(` ${dim(' Detected: ' + detected.map(t => t.name).join(', '))}`);
582
+
583
+ // Install hooks (auto-accept in non-interactive or prompt if TTY)
584
+ let installHooks = true;
585
+ const inq = (() => { try { return require('inquirer'); } catch { return null; } })();
586
+ if (inq && process.stdin.isTTY) {
587
+ try {
588
+ const answer = await inq.prompt([{
589
+ type: 'confirm',
590
+ name: 'install',
591
+ message: `Install governance hooks for ${detected.map(t => t.name).join(', ')}?`,
592
+ default: true,
593
+ }]);
594
+ installHooks = answer.install;
595
+ } catch {
596
+ installHooks = true;
597
+ }
598
+ }
599
+
600
+ if (installHooks) {
601
+ for (const tool of detected) {
602
+ const result = crossModelHooks.installHooksForTool(tool, hookConfig);
603
+ if (result.changes.length > 0) {
604
+ log(` ${green('✓')} ${tool.name}: ${result.changes.join(', ')}`);
605
+ } else {
606
+ log(` ${dim(' ' + tool.name + ': hooks already installed')}`);
607
+ }
608
+ }
609
+ } else {
610
+ log(` ${dim(' Skipped. Install later: delimit hook install')}`);
611
+ }
612
+ }
613
+ } catch (e) {
614
+ log(` ${dim(' Hook installation skipped: ' + e.message)}`);
615
+ }
616
+ log('');
617
+
618
+ // Step 8: Local dashboard API server
619
+ step(8, 'Local dashboard API...');
572
620
 
573
621
  const localServerPath = path.join(DELIMIT_HOME, 'server', 'ai', 'local_server.py');
574
622
  if (fs.existsSync(localServerPath)) {
@@ -580,8 +628,8 @@ echo "[Delimit] ${toolName} not found" >&2; exit 127
580
628
  }
581
629
  log('');
582
630
 
583
- // Step 8: Post-install config validation (LED-098)
584
- step(8, 'Validating config integrity...');
631
+ // Step 9: Post-install config validation (LED-098)
632
+ step(9, 'Validating config integrity...');
585
633
 
586
634
  let validationIssues = 0;
587
635
  const configFiles = [
@@ -670,7 +718,7 @@ echo "[Delimit] ${toolName} not found" >&2; exit 127
670
718
  log('');
671
719
 
672
720
  // Step 9: Done
673
- step(9, 'Done!');
721
+ step(10, 'Done!');
674
722
  log('');
675
723
  log(` ${green('Delimit is installed.')} Your AI now has persistent memory and governance.`);
676
724
  log('');
@@ -0,0 +1,706 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * LED-202: Cross-Model Hook System
5
+ *
6
+ * Detects installed AI coding assistants (Claude Code, Codex, Gemini CLI)
7
+ * and installs Delimit governance hooks into each one's native config format.
8
+ *
9
+ * Hook commands:
10
+ * delimit hook session-start -- ledger context + gov health
11
+ * delimit hook pre-tool <name> -- lint/test checks before edits
12
+ * delimit hook pre-commit -- repo diagnostics before commits
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { execSync } = require('child_process');
18
+ const os = require('os');
19
+
20
+ // Use process.env.HOME to allow test overrides; fall back to os.homedir()
21
+ function getHome() { return process.env.HOME || os.homedir(); }
22
+ function getDelimitHome() { return path.join(getHome(), '.delimit'); }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Hook configuration (user-overridable via delimit.yml)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ function loadHookConfig() {
29
+ const defaults = {
30
+ session_start: true,
31
+ pre_tool: true,
32
+ pre_commit: true,
33
+ deliberate_on_commit: false,
34
+ show_strategy_items: true,
35
+ };
36
+
37
+ // Check project-level delimit.yml, then global
38
+ const candidates = [
39
+ path.join(process.cwd(), 'delimit.yml'),
40
+ path.join(process.cwd(), '.delimit.yml'),
41
+ path.join(getDelimitHome(), 'delimit.yml'),
42
+ ];
43
+
44
+ for (const candidate of candidates) {
45
+ if (fs.existsSync(candidate)) {
46
+ try {
47
+ const yaml = require('js-yaml');
48
+ const doc = yaml.load(fs.readFileSync(candidate, 'utf-8'));
49
+ if (doc && doc.hooks) {
50
+ return { ...defaults, ...doc.hooks };
51
+ }
52
+ } catch { /* ignore parse errors */ }
53
+ }
54
+ }
55
+ return defaults;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // AI tool detection
60
+ // ---------------------------------------------------------------------------
61
+
62
+ function detectAITools() {
63
+ const detected = [];
64
+
65
+ // Claude Code
66
+ const claudeSettings = path.join(getHome(), '.claude', 'settings.json');
67
+ const claudeSettingsLocal = path.join(getHome(), '.claude', 'settings.local.json');
68
+ let hasClaude = fs.existsSync(claudeSettings) || fs.existsSync(claudeSettingsLocal);
69
+ if (!hasClaude) {
70
+ try {
71
+ execSync('claude --version 2>/dev/null', { stdio: 'pipe' });
72
+ hasClaude = true;
73
+ } catch { /* not installed */ }
74
+ }
75
+ if (hasClaude) {
76
+ detected.push({
77
+ id: 'claude',
78
+ name: 'Claude Code',
79
+ configPath: claudeSettings,
80
+ format: 'claude-hooks',
81
+ });
82
+ }
83
+
84
+ // Codex CLI
85
+ const codexDir = path.join(getHome(), '.codex');
86
+ let hasCodex = fs.existsSync(codexDir);
87
+ if (!hasCodex) {
88
+ try {
89
+ execSync('codex --version 2>/dev/null', { stdio: 'pipe' });
90
+ hasCodex = true;
91
+ } catch { /* not installed */ }
92
+ }
93
+ if (hasCodex) {
94
+ detected.push({
95
+ id: 'codex',
96
+ name: 'Codex CLI',
97
+ configPath: path.join(codexDir, 'config.json'),
98
+ instructionsPath: path.join(codexDir, 'instructions.md'),
99
+ format: 'codex',
100
+ });
101
+ }
102
+
103
+ // Gemini CLI
104
+ const geminiDir = path.join(getHome(), '.gemini');
105
+ let hasGemini = fs.existsSync(geminiDir);
106
+ if (!hasGemini) {
107
+ try {
108
+ execSync('gemini --version 2>/dev/null', { stdio: 'pipe' });
109
+ hasGemini = true;
110
+ } catch { /* not installed */ }
111
+ }
112
+ if (hasGemini) {
113
+ detected.push({
114
+ id: 'gemini',
115
+ name: 'Gemini CLI',
116
+ configPath: path.join(geminiDir, 'settings.json'),
117
+ format: 'gemini-mcp',
118
+ });
119
+ }
120
+
121
+ return detected;
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Hook installers per tool
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Install hooks into Claude Code's ~/.claude/settings.json
130
+ * Claude Code supports native hooks: SessionStart, PreToolUse, PostToolUse, etc.
131
+ */
132
+ function installClaudeHooks(tool, hookConfig) {
133
+ const configPath = tool.configPath;
134
+ const configDir = path.dirname(configPath);
135
+ fs.mkdirSync(configDir, { recursive: true });
136
+
137
+ let config = {};
138
+ if (fs.existsSync(configPath)) {
139
+ try {
140
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
141
+ } catch { config = {}; }
142
+ }
143
+
144
+ if (!config.hooks) {
145
+ config.hooks = {};
146
+ }
147
+
148
+ const npxCmd = 'npx delimit-cli';
149
+ const changes = [];
150
+
151
+ // SessionStart hook
152
+ if (hookConfig.session_start) {
153
+ const sessionHook = {
154
+ type: 'command',
155
+ command: `${npxCmd} hook session-start`,
156
+ };
157
+ if (!config.hooks.SessionStart) {
158
+ config.hooks.SessionStart = [];
159
+ }
160
+ // Check if already installed
161
+ const existing = config.hooks.SessionStart.find(
162
+ h => h.command && h.command.includes('delimit-cli hook session-start')
163
+ );
164
+ if (!existing) {
165
+ config.hooks.SessionStart.push(sessionHook);
166
+ changes.push('SessionStart');
167
+ }
168
+ }
169
+
170
+ // PreToolUse hook for file edits
171
+ if (hookConfig.pre_tool) {
172
+ const preToolHook = {
173
+ type: 'command',
174
+ command: `${npxCmd} hook pre-tool`,
175
+ matcher: 'Edit|Write|Bash',
176
+ };
177
+ if (!config.hooks.PreToolUse) {
178
+ config.hooks.PreToolUse = [];
179
+ }
180
+ const existing = config.hooks.PreToolUse.find(
181
+ h => h.command && h.command.includes('delimit-cli hook pre-tool')
182
+ );
183
+ if (!existing) {
184
+ config.hooks.PreToolUse.push(preToolHook);
185
+ changes.push('PreToolUse');
186
+ }
187
+ }
188
+
189
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
190
+ return changes;
191
+ }
192
+
193
+ /**
194
+ * Install hooks for Codex CLI.
195
+ * Codex uses instructions.md for session-start equivalent and config.json for settings.
196
+ * We add governance instructions and a pre-commit hook reference.
197
+ */
198
+ function installCodexHooks(tool, hookConfig) {
199
+ const changes = [];
200
+ const codexDir = path.dirname(tool.configPath);
201
+ fs.mkdirSync(codexDir, { recursive: true });
202
+
203
+ // Codex instructions.md -- acts as the session-start equivalent
204
+ if (hookConfig.session_start) {
205
+ const instructionsPath = tool.instructionsPath || path.join(codexDir, 'instructions.md');
206
+ const delimitBlock = `<!-- delimit:hooks-start -->
207
+ ## Delimit Governance
208
+
209
+ On every session start, run this command to check governance status:
210
+ \`\`\`
211
+ npx delimit-cli hook session-start
212
+ \`\`\`
213
+
214
+ Before committing changes, run:
215
+ \`\`\`
216
+ npx delimit-cli hook pre-commit
217
+ \`\`\`
218
+
219
+ After editing OpenAPI specs, run:
220
+ \`\`\`
221
+ npx delimit-cli hook pre-tool Edit
222
+ \`\`\`
223
+ <!-- delimit:hooks-end -->`;
224
+
225
+ let content = '';
226
+ if (fs.existsSync(instructionsPath)) {
227
+ content = fs.readFileSync(instructionsPath, 'utf-8');
228
+ }
229
+
230
+ if (content.includes('delimit:hooks-start')) {
231
+ // Replace existing block
232
+ content = content.replace(
233
+ /<!-- delimit:hooks-start -->[\s\S]*?<!-- delimit:hooks-end -->/,
234
+ delimitBlock
235
+ );
236
+ } else {
237
+ content = content ? content + '\n\n' + delimitBlock : delimitBlock;
238
+ }
239
+
240
+ fs.writeFileSync(instructionsPath, content);
241
+ changes.push('instructions.md');
242
+ }
243
+
244
+ // Codex config.json -- add hook commands
245
+ let config = {};
246
+ if (fs.existsSync(tool.configPath)) {
247
+ try {
248
+ config = JSON.parse(fs.readFileSync(tool.configPath, 'utf-8'));
249
+ } catch { config = {}; }
250
+ }
251
+
252
+ if (!config.hooks) {
253
+ config.hooks = {};
254
+ }
255
+
256
+ if (hookConfig.pre_commit && !config.hooks['pre-commit']) {
257
+ config.hooks['pre-commit'] = 'npx delimit-cli hook pre-commit';
258
+ changes.push('pre-commit hook');
259
+ }
260
+
261
+ fs.writeFileSync(tool.configPath, JSON.stringify(config, null, 2));
262
+ return changes;
263
+ }
264
+
265
+ /**
266
+ * Install hooks for Gemini CLI.
267
+ * Gemini CLI uses MCP (already handled by setup) but we add governance
268
+ * instructions to settings.json and a GEMINI.md equivalent.
269
+ */
270
+ function installGeminiHooks(tool, hookConfig) {
271
+ const changes = [];
272
+ const geminiDir = path.dirname(tool.configPath);
273
+ fs.mkdirSync(geminiDir, { recursive: true });
274
+
275
+ // Update settings.json with custom instructions
276
+ let config = {};
277
+ if (fs.existsSync(tool.configPath)) {
278
+ try {
279
+ config = JSON.parse(fs.readFileSync(tool.configPath, 'utf-8'));
280
+ } catch { config = {}; }
281
+ }
282
+
283
+ const govInstructions = [
284
+ 'Delimit governance is active.',
285
+ 'On session start, run: npx delimit-cli hook session-start',
286
+ 'Before committing, run: npx delimit-cli hook pre-commit',
287
+ 'After editing API specs, run: npx delimit-cli hook pre-tool Edit',
288
+ 'Call delimit_ledger_context and delimit_gov_health via MCP when available.',
289
+ ].join(' ');
290
+
291
+ if (!config.customInstructions || !config.customInstructions.includes('delimit-cli hook')) {
292
+ config.customInstructions = govInstructions;
293
+ changes.push('customInstructions');
294
+ }
295
+
296
+ fs.writeFileSync(tool.configPath, JSON.stringify(config, null, 2));
297
+
298
+ // Write a GEMINI.md governance file (equivalent of CLAUDE.md)
299
+ const geminiMd = path.join(geminiDir, 'GEMINI.md');
300
+ if (!fs.existsSync(geminiMd) || !fs.readFileSync(geminiMd, 'utf-8').includes('delimit')) {
301
+ const content = `# Delimit Governance
302
+
303
+ On every session start:
304
+ 1. Run \`npx delimit-cli hook session-start\` to check open tasks and governance status
305
+
306
+ After editing code:
307
+ - After editing API specs: run \`npx delimit-cli hook pre-tool Edit\`
308
+ - After editing tests: run \`npx delimit-cli hook pre-tool Edit\`
309
+
310
+ Before committing:
311
+ - Run \`npx delimit-cli hook pre-commit\` to check for issues
312
+ `;
313
+ fs.writeFileSync(geminiMd, content);
314
+ changes.push('GEMINI.md');
315
+ }
316
+
317
+ return changes;
318
+ }
319
+
320
+ /**
321
+ * Install hooks for a detected tool.
322
+ * Returns { tool, changes } describing what was installed.
323
+ */
324
+ function installHooksForTool(tool, hookConfig) {
325
+ switch (tool.id) {
326
+ case 'claude':
327
+ return { tool, changes: installClaudeHooks(tool, hookConfig) };
328
+ case 'codex':
329
+ return { tool, changes: installCodexHooks(tool, hookConfig) };
330
+ case 'gemini':
331
+ return { tool, changes: installGeminiHooks(tool, hookConfig) };
332
+ default:
333
+ return { tool, changes: [] };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Install hooks for all detected AI tools.
339
+ */
340
+ function installAllHooks(hookConfig) {
341
+ const tools = detectAITools();
342
+ const results = [];
343
+ for (const tool of tools) {
344
+ results.push(installHooksForTool(tool, hookConfig));
345
+ }
346
+ return { tools, results };
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Hook removal (for uninstall)
351
+ // ---------------------------------------------------------------------------
352
+
353
+ function removeClaudeHooks() {
354
+ const configPath = path.join(getHome(), '.claude', 'settings.json');
355
+ if (!fs.existsSync(configPath)) return false;
356
+
357
+ try {
358
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
359
+ if (!config.hooks) return false;
360
+
361
+ let changed = false;
362
+
363
+ for (const event of ['SessionStart', 'PreToolUse', 'PostToolUse']) {
364
+ if (Array.isArray(config.hooks[event])) {
365
+ const before = config.hooks[event].length;
366
+ config.hooks[event] = config.hooks[event].filter(
367
+ h => !(h.command && h.command.includes('delimit-cli'))
368
+ );
369
+ if (config.hooks[event].length === 0) {
370
+ delete config.hooks[event];
371
+ }
372
+ if (config.hooks[event] === undefined || config.hooks[event].length < before) {
373
+ changed = true;
374
+ }
375
+ }
376
+ }
377
+
378
+ if (Object.keys(config.hooks).length === 0) {
379
+ delete config.hooks;
380
+ }
381
+
382
+ if (changed) {
383
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
384
+ }
385
+ return changed;
386
+ } catch {
387
+ return false;
388
+ }
389
+ }
390
+
391
+ function removeCodexHooks() {
392
+ let changed = false;
393
+
394
+ // Remove from instructions.md
395
+ const instructionsPath = path.join(getHome(), '.codex', 'instructions.md');
396
+ if (fs.existsSync(instructionsPath)) {
397
+ let content = fs.readFileSync(instructionsPath, 'utf-8');
398
+ if (content.includes('delimit:hooks-start')) {
399
+ content = content.replace(
400
+ /\n*<!-- delimit:hooks-start -->[\s\S]*?<!-- delimit:hooks-end -->\n*/,
401
+ ''
402
+ );
403
+ fs.writeFileSync(instructionsPath, content);
404
+ changed = true;
405
+ }
406
+ }
407
+
408
+ // Remove hooks from config.json
409
+ const configPath = path.join(getHome(), '.codex', 'config.json');
410
+ if (fs.existsSync(configPath)) {
411
+ try {
412
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
413
+ if (config.hooks) {
414
+ for (const [key, val] of Object.entries(config.hooks)) {
415
+ if (typeof val === 'string' && val.includes('delimit-cli')) {
416
+ delete config.hooks[key];
417
+ changed = true;
418
+ }
419
+ }
420
+ if (Object.keys(config.hooks).length === 0) {
421
+ delete config.hooks;
422
+ }
423
+ if (changed) {
424
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
425
+ }
426
+ }
427
+ } catch { /* ignore */ }
428
+ }
429
+
430
+ return changed;
431
+ }
432
+
433
+ function removeGeminiHooks() {
434
+ let changed = false;
435
+
436
+ // Remove custom instructions referencing delimit
437
+ const configPath = path.join(getHome(), '.gemini', 'settings.json');
438
+ if (fs.existsSync(configPath)) {
439
+ try {
440
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
441
+ if (config.customInstructions && config.customInstructions.includes('delimit-cli hook')) {
442
+ delete config.customInstructions;
443
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
444
+ changed = true;
445
+ }
446
+ } catch { /* ignore */ }
447
+ }
448
+
449
+ // Remove GEMINI.md if it's ours
450
+ const geminiMd = path.join(getHome(), '.gemini', 'GEMINI.md');
451
+ if (fs.existsSync(geminiMd)) {
452
+ const content = fs.readFileSync(geminiMd, 'utf-8');
453
+ if (content.includes('Delimit Governance')) {
454
+ fs.unlinkSync(geminiMd);
455
+ changed = true;
456
+ }
457
+ }
458
+
459
+ return changed;
460
+ }
461
+
462
+ function removeAllHooks() {
463
+ const results = [];
464
+
465
+ if (removeClaudeHooks()) {
466
+ results.push('Claude Code');
467
+ }
468
+ if (removeCodexHooks()) {
469
+ results.push('Codex CLI');
470
+ }
471
+ if (removeGeminiHooks()) {
472
+ results.push('Gemini CLI');
473
+ }
474
+
475
+ return results;
476
+ }
477
+
478
+ // ---------------------------------------------------------------------------
479
+ // Hook execution commands
480
+ // ---------------------------------------------------------------------------
481
+
482
+ /**
483
+ * session-start: Show ledger context and governance health.
484
+ * Output goes to stdout for the AI tool to read.
485
+ */
486
+ async function hookSessionStart() {
487
+ const config = loadHookConfig();
488
+ if (!config.session_start) {
489
+ return;
490
+ }
491
+
492
+ const lines = [];
493
+ lines.push('[Delimit] Governance check');
494
+ lines.push('');
495
+
496
+ // Check for delimit.yml or .delimit.yml
497
+ const cwd = process.cwd();
498
+ const hasPolicy = fs.existsSync(path.join(cwd, 'delimit.yml'))
499
+ || fs.existsSync(path.join(cwd, '.delimit.yml'))
500
+ || fs.existsSync(path.join(cwd, '.delimit', 'policies.yml'));
501
+
502
+ if (hasPolicy) {
503
+ lines.push('[Delimit] Policy file found -- governance active');
504
+ } else {
505
+ lines.push('[Delimit] No policy file found -- run "delimit init" to set up governance');
506
+ }
507
+
508
+ // Check for OpenAPI specs
509
+ const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.json'];
510
+ const foundSpecs = [];
511
+ for (const pattern of specPatterns) {
512
+ const specPath = path.join(cwd, pattern);
513
+ if (fs.existsSync(specPath)) {
514
+ foundSpecs.push(pattern);
515
+ }
516
+ }
517
+ // Also check api/ and specs/ directories
518
+ for (const dir of ['api', 'specs', 'spec']) {
519
+ const dirPath = path.join(cwd, dir);
520
+ if (fs.existsSync(dirPath)) {
521
+ try {
522
+ const files = fs.readdirSync(dirPath);
523
+ for (const f of files) {
524
+ if (/\.(yaml|yml|json)$/.test(f) && /openapi|swagger/i.test(f)) {
525
+ foundSpecs.push(path.join(dir, f));
526
+ }
527
+ }
528
+ } catch { /* ignore */ }
529
+ }
530
+ }
531
+
532
+ if (foundSpecs.length > 0) {
533
+ lines.push(`[Delimit] OpenAPI specs detected: ${foundSpecs.join(', ')}`);
534
+ }
535
+
536
+ // Check ledger
537
+ const ledgerDir = path.join(getDelimitHome(), 'ledger');
538
+ if (fs.existsSync(ledgerDir)) {
539
+ try {
540
+ const ledgerFiles = fs.readdirSync(ledgerDir).filter(f => f.endsWith('.json'));
541
+ let openItems = 0;
542
+ for (const f of ledgerFiles) {
543
+ try {
544
+ const items = JSON.parse(fs.readFileSync(path.join(ledgerDir, f), 'utf-8'));
545
+ if (Array.isArray(items)) {
546
+ openItems += items.filter(i => i.status === 'open' || i.status === 'in_progress').length;
547
+ }
548
+ } catch { /* ignore */ }
549
+ }
550
+ if (openItems > 0) {
551
+ lines.push(`[Delimit] Ledger: ${openItems} open item(s)`);
552
+ } else {
553
+ lines.push('[Delimit] Ledger: no open items');
554
+ }
555
+ } catch {
556
+ lines.push('[Delimit] Ledger: empty');
557
+ }
558
+ }
559
+
560
+ // Git branch info
561
+ try {
562
+ const branch = execSync('git branch --show-current 2>/dev/null', { encoding: 'utf-8' }).trim();
563
+ if (branch) {
564
+ lines.push(`[Delimit] Branch: ${branch}`);
565
+ }
566
+ } catch { /* not in git repo */ }
567
+
568
+ lines.push('');
569
+ process.stdout.write(lines.join('\n') + '\n');
570
+ }
571
+
572
+ /**
573
+ * pre-tool: Check before file edits.
574
+ * If editing an OpenAPI spec, run a quick lint.
575
+ * If editing a test file, note it.
576
+ */
577
+ async function hookPreTool(toolName) {
578
+ const config = loadHookConfig();
579
+ if (!config.pre_tool) {
580
+ return;
581
+ }
582
+
583
+ // The tool name comes from the AI tool (e.g., "Edit", "Write", "Bash")
584
+ // We check the DELIMIT_TOOL_INPUT env or just do lightweight checks
585
+ const cwd = process.cwd();
586
+
587
+ // Check if there are staged OpenAPI spec changes
588
+ try {
589
+ const stagedFiles = execSync('git diff --cached --name-only 2>/dev/null', {
590
+ encoding: 'utf-8',
591
+ timeout: 2000,
592
+ }).split('\n').filter(Boolean);
593
+
594
+ const specFiles = stagedFiles.filter(f =>
595
+ /openapi|swagger/i.test(f) && /\.(yaml|yml|json)$/.test(f)
596
+ );
597
+
598
+ if (specFiles.length > 0) {
599
+ process.stderr.write(`[Delimit] Warning: OpenAPI spec(s) staged for commit: ${specFiles.join(', ')}\n`);
600
+ process.stderr.write('[Delimit] Run "delimit lint" before committing to check for breaking changes.\n');
601
+ }
602
+
603
+ const testFiles = stagedFiles.filter(f =>
604
+ /\.(test|spec)\.(js|ts|py|rb)$/.test(f) || /test_.*\.py$/.test(f)
605
+ );
606
+
607
+ if (testFiles.length > 0) {
608
+ process.stderr.write(`[Delimit] Test files staged: ${testFiles.join(', ')}\n`);
609
+ process.stderr.write('[Delimit] Consider running tests before committing.\n');
610
+ }
611
+ } catch {
612
+ // Not in a git repo or no staged changes -- that is fine
613
+ }
614
+ }
615
+
616
+ /**
617
+ * pre-commit: Run repo diagnostics before committing.
618
+ */
619
+ async function hookPreCommit() {
620
+ const config = loadHookConfig();
621
+ if (!config.pre_commit) {
622
+ return;
623
+ }
624
+
625
+ const cwd = process.cwd();
626
+ const warnings = [];
627
+
628
+ // Check for staged OpenAPI spec changes
629
+ try {
630
+ const stagedFiles = execSync('git diff --cached --name-only 2>/dev/null', {
631
+ encoding: 'utf-8',
632
+ timeout: 2000,
633
+ }).split('\n').filter(Boolean);
634
+
635
+ const specFiles = stagedFiles.filter(f =>
636
+ /openapi|swagger/i.test(f) && /\.(yaml|yml|json)$/.test(f)
637
+ );
638
+
639
+ if (specFiles.length > 0) {
640
+ // Try to find a previous version to diff against
641
+ for (const specFile of specFiles) {
642
+ try {
643
+ // Get the HEAD version
644
+ const oldContent = execSync(`git show HEAD:${specFile} 2>/dev/null`, {
645
+ encoding: 'utf-8',
646
+ timeout: 3000,
647
+ });
648
+ if (oldContent) {
649
+ warnings.push(`[Delimit] OpenAPI spec changed: ${specFile}`);
650
+ warnings.push('[Delimit] Run "delimit diff <old> <new>" to review API changes before committing.');
651
+ }
652
+ } catch {
653
+ // New file, no previous version
654
+ }
655
+ }
656
+ }
657
+
658
+ // Check for secrets patterns in staged files
659
+ const sensitivePatterns = [
660
+ /password\s*[:=]\s*['"][^'"]+['"]/i,
661
+ /api[_-]?key\s*[:=]\s*['"][^'"]+['"]/i,
662
+ /secret\s*[:=]\s*['"][^'"]+['"]/i,
663
+ ];
664
+
665
+ for (const file of stagedFiles) {
666
+ if (/\.(env|key|pem|p12|pfx)$/.test(file)) {
667
+ warnings.push(`[Delimit] WARNING: Potentially sensitive file staged: ${file}`);
668
+ }
669
+ }
670
+ } catch {
671
+ // Not in git repo
672
+ }
673
+
674
+ // Check for policy file
675
+ const hasPolicy = fs.existsSync(path.join(cwd, 'delimit.yml'))
676
+ || fs.existsSync(path.join(cwd, '.delimit.yml'));
677
+
678
+ if (!hasPolicy) {
679
+ warnings.push('[Delimit] No governance policy found. Run "delimit init" to create one.');
680
+ }
681
+
682
+ if (warnings.length > 0) {
683
+ process.stderr.write(warnings.join('\n') + '\n');
684
+ }
685
+ }
686
+
687
+ // ---------------------------------------------------------------------------
688
+ // Exports
689
+ // ---------------------------------------------------------------------------
690
+
691
+ module.exports = {
692
+ detectAITools,
693
+ installHooksForTool,
694
+ installAllHooks,
695
+ installClaudeHooks,
696
+ installCodexHooks,
697
+ installGeminiHooks,
698
+ removeAllHooks,
699
+ removeClaudeHooks,
700
+ removeCodexHooks,
701
+ removeGeminiHooks,
702
+ loadHookConfig,
703
+ hookSessionStart,
704
+ hookPreTool,
705
+ hookPreCommit,
706
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "delimit-cli",
3
3
  "mcpName": "io.github.delimit-ai/delimit-mcp-server",
4
- "version": "3.11.11",
4
+ "version": "3.12.0",
5
5
  "description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
6
6
  "main": "index.js",
7
7
  "files": [
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "scripts": {
22
22
  "postinstall": "echo '\\nRun: npx delimit-cli setup\\n'",
23
- "test": "node --test tests/setup-onboarding.test.js tests/setup-matrix.test.js"
23
+ "test": "node --test tests/setup-onboarding.test.js tests/setup-matrix.test.js tests/config-export-import.test.js tests/cross-model-hooks.test.js"
24
24
  },
25
25
  "keywords": [
26
26
  "openapi",