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 +14 -0
- package/bin/delimit-cli.js +304 -84
- package/bin/delimit-setup.js +53 -5
- package/lib/cross-model-hooks.js +706 -0
- package/package.json +2 -2
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
|
package/bin/delimit-cli.js
CHANGED
|
@@ -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
|
-
//
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
.description('Internal hook handler')
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// LED-202: Cross-model hook commands
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
708
746
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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')
|
package/bin/delimit-setup.js
CHANGED
|
@@ -567,8 +567,56 @@ echo "[Delimit] ${toolName} not found" >&2; exit 127
|
|
|
567
567
|
}
|
|
568
568
|
log('');
|
|
569
569
|
|
|
570
|
-
// Step 7:
|
|
571
|
-
step(7, '
|
|
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
|
|
584
|
-
step(
|
|
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(
|
|
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.
|
|
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",
|