delimit-cli 1.0.0 → 2.1.1

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.
Files changed (95) hide show
  1. package/.github/workflows/api-governance.yml +43 -0
  2. package/README.md +70 -113
  3. package/adapters/codex-skill.js +87 -0
  4. package/adapters/cursor-extension.js +190 -0
  5. package/adapters/gemini-action.js +93 -0
  6. package/adapters/openai-function.js +112 -0
  7. package/adapters/xai-plugin.js +151 -0
  8. package/bin/delimit-cli.js +921 -0
  9. package/bin/delimit.js +237 -1
  10. package/delimit.yml +19 -0
  11. package/hooks/evidence-status.sh +12 -0
  12. package/hooks/git/commit-msg +4 -0
  13. package/hooks/git/pre-commit +4 -0
  14. package/hooks/git/pre-push +4 -0
  15. package/hooks/install-hooks.sh +583 -0
  16. package/hooks/message-auth-hook.js +9 -0
  17. package/hooks/message-governance-hook.js +9 -0
  18. package/hooks/models/claude-post.js +4 -0
  19. package/hooks/models/claude-pre.js +4 -0
  20. package/hooks/models/codex-post.js +4 -0
  21. package/hooks/models/codex-pre.js +4 -0
  22. package/hooks/models/cursor-post.js +4 -0
  23. package/hooks/models/cursor-pre.js +4 -0
  24. package/hooks/models/gemini-post.js +4 -0
  25. package/hooks/models/gemini-pre.js +4 -0
  26. package/hooks/models/openai-post.js +4 -0
  27. package/hooks/models/openai-pre.js +4 -0
  28. package/hooks/models/windsurf-post.js +4 -0
  29. package/hooks/models/windsurf-pre.js +4 -0
  30. package/hooks/models/xai-post.js +4 -0
  31. package/hooks/models/xai-pre.js +4 -0
  32. package/hooks/post-bash-hook.js +13 -0
  33. package/hooks/post-mcp-hook.js +13 -0
  34. package/hooks/post-response-hook.js +4 -0
  35. package/hooks/post-tool-hook.js +126 -0
  36. package/hooks/post-write-hook.js +13 -0
  37. package/hooks/pre-bash-hook.js +30 -0
  38. package/hooks/pre-mcp-hook.js +13 -0
  39. package/hooks/pre-read-hook.js +13 -0
  40. package/hooks/pre-search-hook.js +13 -0
  41. package/hooks/pre-submit-hook.js +4 -0
  42. package/hooks/pre-task-hook.js +13 -0
  43. package/hooks/pre-tool-hook.js +121 -0
  44. package/hooks/pre-web-hook.js +13 -0
  45. package/hooks/pre-write-hook.js +31 -0
  46. package/hooks/test-hooks.sh +12 -0
  47. package/hooks/update-delimit.sh +6 -0
  48. package/lib/agent.js +509 -0
  49. package/lib/api-engine.js +156 -0
  50. package/lib/auth-setup.js +891 -0
  51. package/lib/decision-engine.js +474 -0
  52. package/lib/hooks-installer.js +416 -0
  53. package/lib/platform-adapters.js +353 -0
  54. package/lib/proxy-handler.js +114 -0
  55. package/package.json +38 -30
  56. package/scripts/infect.js +128 -0
  57. package/test-decision-engine.js +181 -0
  58. package/test-hook.js +27 -0
  59. package/dist/commands/validate.d.ts +0 -2
  60. package/dist/commands/validate.d.ts.map +0 -1
  61. package/dist/commands/validate.js +0 -106
  62. package/dist/commands/validate.js.map +0 -1
  63. package/dist/index.d.ts +0 -3
  64. package/dist/index.d.ts.map +0 -1
  65. package/dist/index.js +0 -71
  66. package/dist/index.js.map +0 -1
  67. package/dist/types/index.d.ts +0 -39
  68. package/dist/types/index.d.ts.map +0 -1
  69. package/dist/types/index.js +0 -3
  70. package/dist/types/index.js.map +0 -1
  71. package/dist/utils/api.d.ts +0 -3
  72. package/dist/utils/api.d.ts.map +0 -1
  73. package/dist/utils/api.js +0 -64
  74. package/dist/utils/api.js.map +0 -1
  75. package/dist/utils/file.d.ts +0 -7
  76. package/dist/utils/file.d.ts.map +0 -1
  77. package/dist/utils/file.js +0 -69
  78. package/dist/utils/file.js.map +0 -1
  79. package/dist/utils/logger.d.ts +0 -14
  80. package/dist/utils/logger.d.ts.map +0 -1
  81. package/dist/utils/logger.js +0 -28
  82. package/dist/utils/logger.js.map +0 -1
  83. package/dist/utils/masker.d.ts +0 -14
  84. package/dist/utils/masker.d.ts.map +0 -1
  85. package/dist/utils/masker.js +0 -89
  86. package/dist/utils/masker.js.map +0 -1
  87. package/src/commands/validate.ts +0 -150
  88. package/src/index.ts +0 -80
  89. package/src/types/index.ts +0 -41
  90. package/src/utils/api.ts +0 -68
  91. package/src/utils/file.ts +0 -71
  92. package/src/utils/logger.ts +0 -27
  93. package/src/utils/masker.ts +0 -101
  94. package/test-sensitive.yaml +0 -109
  95. package/tsconfig.json +0 -23
@@ -0,0 +1,921 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const axios = require('axios');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { execSync, spawn } = require('child_process');
8
+ const chalk = require('chalk');
9
+ const inquirer = require('inquirer');
10
+ const DelimitAuthSetup = require('../lib/auth-setup');
11
+ const DelimitHooksInstaller = require('../lib/hooks-installer');
12
+
13
+ const AGENT_URL = `http://127.0.0.1:${process.env.DELIMIT_AGENT_PORT || 7823}`;
14
+ const program = new Command();
15
+
16
+ const yaml = require('js-yaml');
17
+
18
+ // Helper to check if agent is running
19
+ async function checkAgent() {
20
+ try {
21
+ await axios.get(`${AGENT_URL}/status`);
22
+ return true;
23
+ } catch (e) {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ // Start agent if not running
29
+ async function ensureAgent() {
30
+ if (!(await checkAgent())) {
31
+ console.log(chalk.yellow('Starting Delimit Agent...'));
32
+ const agentPath = path.join(__dirname, '..', 'lib', 'agent.js');
33
+ spawn('node', [agentPath], {
34
+ detached: true,
35
+ stdio: 'ignore'
36
+ }).unref();
37
+
38
+ // Wait for agent to start
39
+ for (let i = 0; i < 10; i++) {
40
+ await new Promise(r => setTimeout(r, 500));
41
+ if (await checkAgent()) {
42
+ console.log(chalk.green('✓ Agent started'));
43
+ return;
44
+ }
45
+ }
46
+ throw new Error('Failed to start agent');
47
+ }
48
+ }
49
+
50
+ program
51
+ .name('delimit')
52
+ .description('Dynamic AI Governance with seamless mode switching')
53
+ .version('2.0.0');
54
+
55
+ // Install command with modes
56
+ program
57
+ .command('install')
58
+ .description('Install Delimit governance with multi-model hooks')
59
+ .option('--mode <mode>', 'Initial mode: advisory, guarded, enforce', 'advisory')
60
+ .option('--scope <scope>', 'Scope: global, repo', 'global')
61
+ .option('--hooks <hooks>', 'Install hooks for: all, git, ai, mcp', 'all')
62
+ .option('--auth', 'Setup authentication during installation')
63
+ .option('--dry-run', 'Preview changes without applying')
64
+ .action(async (options) => {
65
+ console.log(chalk.blue.bold('\n🔵 Delimit Installation\n'));
66
+
67
+ if (options.dryRun) {
68
+ console.log(chalk.yellow('DRY RUN - No changes will be made\n'));
69
+ }
70
+
71
+ console.log('This will modify:');
72
+ if (options.scope === 'global') {
73
+ console.log(' • Git global hooks');
74
+ console.log(' • Shell PATH (for AI tool interception)');
75
+ console.log(' • Create ~/.delimit configuration');
76
+
77
+ if (options.hooks === 'all' || options.hooks === 'ai') {
78
+ console.log(' • AI model hooks (Claude, Codex, Gemini, etc.)');
79
+ }
80
+ if (options.hooks === 'all' || options.hooks === 'mcp') {
81
+ console.log(' • MCP integration hooks for Claude Code');
82
+ }
83
+ } else {
84
+ console.log(' • Git hooks for current repository');
85
+ console.log(' • Create .delimit.yml in current directory');
86
+ }
87
+
88
+ console.log(`\nInitial mode: ${chalk.bold(options.mode)}`);
89
+ console.log('You can change modes anytime with: delimit mode <mode>\n');
90
+
91
+ if (!options.dryRun) {
92
+ const { confirm } = await inquirer.prompt([{
93
+ type: 'confirm',
94
+ name: 'confirm',
95
+ message: 'Continue with installation?',
96
+ default: false
97
+ }]);
98
+
99
+ if (!confirm) {
100
+ console.log(chalk.red('Installation cancelled'));
101
+ return;
102
+ }
103
+ }
104
+
105
+ if (options.dryRun) {
106
+ console.log(chalk.green('\n✓ Dry run complete'));
107
+ return;
108
+ }
109
+
110
+ // Actual installation
111
+ await installDelimit(options.mode, options.scope, options.hooks);
112
+
113
+ // Prompt for authentication setup
114
+ if (!options.dryRun) {
115
+ const { setupAuth } = await inquirer.prompt([{
116
+ type: 'confirm',
117
+ name: 'setupAuth',
118
+ message: '\nWould you like to setup authentication for GitHub, AI tools, and other services?',
119
+ default: true
120
+ }]);
121
+
122
+ if (setupAuth || options.auth) {
123
+ console.log(chalk.blue.bold('\n🔐 Setting up authentication...\n'));
124
+ const authSetup = new DelimitAuthSetup();
125
+
126
+ // Prompt for which services to configure
127
+ const { authCategories } = await inquirer.prompt([{
128
+ type: 'checkbox',
129
+ name: 'authCategories',
130
+ message: 'Select services to configure:',
131
+ choices: [
132
+ { name: 'GitHub (recommended for governance)', value: 'github', checked: true },
133
+ { name: 'AI Tools (Claude, OpenAI, Gemini)', value: 'ai', checked: true },
134
+ { name: 'Cloud Providers (AWS, GCP, Azure)', value: 'cloud' },
135
+ { name: 'Databases', value: 'databases' },
136
+ { name: 'Container Registries', value: 'registries' },
137
+ { name: 'Package Managers', value: 'packages' },
138
+ { name: 'Monitoring Services', value: 'monitoring' },
139
+ { name: 'Organization Settings', value: 'org' }
140
+ ]
141
+ }]);
142
+
143
+ const setupOptions = {};
144
+ authCategories.forEach(cat => {
145
+ const key = cat === 'ai' ? 'setupAI' :
146
+ cat === 'github' ? 'setupGitHub' :
147
+ `setup${cat.charAt(0).toUpperCase() + cat.slice(1)}`;
148
+ setupOptions[key] = true;
149
+ });
150
+
151
+ await authSetup.setup(setupOptions);
152
+ console.log(chalk.green('\n✅ Authentication setup complete!'));
153
+ }
154
+ }
155
+ });
156
+
157
+ // Mode switching command
158
+ program
159
+ .command('mode [mode]')
160
+ .description('Switch governance mode (advisory, guarded, enforce, auto)')
161
+ .action(async (mode) => {
162
+ await ensureAgent();
163
+
164
+ if (!mode) {
165
+ // Show current mode
166
+ const { data } = await axios.get(`${AGENT_URL}/status`);
167
+ console.log(chalk.blue('Current mode:'), chalk.bold(data.sessionMode));
168
+ return;
169
+ }
170
+
171
+ if (!['advisory', 'guarded', 'enforce', 'auto'].includes(mode)) {
172
+ console.error(chalk.red('Invalid mode. Choose: advisory, guarded, enforce, auto'));
173
+ return;
174
+ }
175
+
176
+ const { data } = await axios.post(`${AGENT_URL}/mode`, { mode });
177
+ console.log(chalk.green(`✓ Mode switched to: ${chalk.bold(data.mode)}`));
178
+ });
179
+
180
+ // Status command
181
+ program
182
+ .command('status')
183
+ .description('Show governance status')
184
+ .option('--verbose', 'Show detailed status')
185
+ .action(async (options) => {
186
+ const agentRunning = await checkAgent();
187
+
188
+ console.log(chalk.blue.bold('\nDelimit Governance Status\n'));
189
+ console.log('Agent:', agentRunning ? chalk.green('✓ Running') : chalk.red('✗ Not running'));
190
+
191
+ if (agentRunning) {
192
+ const { data } = await axios.get(`${AGENT_URL}/status`);
193
+
194
+ // Mode information
195
+ console.log('\n' + chalk.bold('Mode Configuration:'));
196
+ console.log(` Current Mode: ${chalk.bold(data.sessionMode)}`);
197
+ if (data.defaultMode) {
198
+ console.log(` Default Mode: ${data.defaultMode}`);
199
+ }
200
+ if (data.effectiveMode && data.effectiveMode !== data.sessionMode) {
201
+ console.log(` Effective Mode: ${chalk.yellow(data.effectiveMode)} (escalated)`);
202
+ }
203
+
204
+ // Policies
205
+ console.log('\n' + chalk.bold('Policies:'));
206
+ if (data.policiesLoaded.length > 0) {
207
+ data.policiesLoaded.forEach(policy => {
208
+ console.log(` • ${policy}`);
209
+ });
210
+ if (data.totalRules) {
211
+ console.log(` Total Rules: ${data.totalRules}`);
212
+ }
213
+ } else {
214
+ console.log(' No policies loaded');
215
+ }
216
+
217
+ // Recent activity
218
+ console.log('\n' + chalk.bold('Activity:'));
219
+ console.log(` Audit Log Entries: ${data.auditLogSize}`);
220
+ if (data.lastDecision) {
221
+ const timeSince = Date.now() - new Date(data.lastDecision.timestamp);
222
+ const minutes = Math.floor(timeSince / 60000);
223
+ console.log(` Last Decision: ${minutes} minutes ago (${data.lastDecision.action})`);
224
+ }
225
+ console.log(` Uptime: ${Math.floor(data.uptime / 60)} minutes`);
226
+
227
+ // Verbose mode shows recent decisions
228
+ if (options.verbose && data.recentDecisions) {
229
+ console.log('\n' + chalk.bold('Recent Decisions:'));
230
+ data.recentDecisions.forEach(decision => {
231
+ const color = decision.action === 'block' ? chalk.red :
232
+ decision.action === 'prompt' ? chalk.yellow :
233
+ chalk.green;
234
+ console.log(` ${decision.timestamp} | ${color(decision.mode)} | ${decision.rule || 'no rule'}`);
235
+ });
236
+ }
237
+ }
238
+
239
+ // System integration
240
+ console.log('\n' + chalk.bold('System Integration:'));
241
+
242
+ // Git hooks
243
+ try {
244
+ const hooksPath = execSync('git config --global core.hooksPath').toString().trim();
245
+ const hooksActive = hooksPath.includes('.delimit');
246
+ console.log(` Git Hooks: ${hooksActive ? chalk.green('✓ Active') : chalk.yellow('⚠ Not configured')}`);
247
+ } catch (e) {
248
+ console.log(` Git Hooks: ${chalk.red('✗ Not configured')}`);
249
+ }
250
+
251
+ // PATH
252
+ if (process.env.PATH.includes('.delimit/shims')) {
253
+ console.log(` AI Tool Interception: ${chalk.green('✓ Active')}`);
254
+ } else {
255
+ console.log(` AI Tool Interception: ${chalk.gray('Not active')}`);
256
+ }
257
+
258
+ // Policy files
259
+ const policyFiles = [];
260
+ if (fs.existsSync('delimit.yml')) {
261
+ policyFiles.push('project');
262
+ }
263
+ if (fs.existsSync(path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml'))) {
264
+ policyFiles.push('user');
265
+ }
266
+ console.log(` Policy Files: ${policyFiles.length > 0 ? policyFiles.join(', ') : chalk.gray('none')}`);
267
+
268
+ if (options.verbose) {
269
+ console.log('\n' + chalk.gray('Run "delimit doctor" for detailed diagnostics'));
270
+ }
271
+ });
272
+
273
+ // Policy command
274
+ program
275
+ .command('policy')
276
+ .description('Manage governance policies')
277
+ .option('--init', 'Create example policy file')
278
+ .option('--validate', 'Validate policy syntax')
279
+ .action(async (options) => {
280
+ if (options.init) {
281
+ const examplePolicy = `# Delimit Policy Configuration
282
+ # This file defines dynamic governance rules
283
+
284
+ defaultMode: advisory
285
+
286
+ rules:
287
+ - name: "Production Protection"
288
+ mode: enforce
289
+ triggers:
290
+ - gitBranch: [main, master, production]
291
+
292
+ - name: "Payment Code Security"
293
+ mode: enforce
294
+ triggers:
295
+ - path: "**/payment/**"
296
+ - content: ["stripe", "payment", "billing"]
297
+
298
+ - name: "AI-Generated Code Review"
299
+ mode: guarded
300
+ triggers:
301
+ - commitMessage: "Co-authored-by"
302
+
303
+ - name: "Documentation Freedom"
304
+ mode: advisory
305
+ triggers:
306
+ - path: "**/*.md"
307
+ final: true
308
+
309
+ overrides:
310
+ allowEnforceOverride: false
311
+ requireGuardedOverrideReason: true
312
+ `;
313
+
314
+ fs.writeFileSync('delimit.yml', examplePolicy);
315
+ console.log(chalk.green('✓ Created delimit.yml'));
316
+ console.log('Edit this file to customize your governance rules');
317
+ }
318
+
319
+ if (options.validate) {
320
+ // TODO: Implement validation
321
+ console.log(chalk.yellow('Policy validation coming soon'));
322
+ }
323
+ });
324
+
325
+ // Auth command - setup credentials
326
+ program
327
+ .command('auth')
328
+ .description('Setup authentication and credentials for services')
329
+ .option('--all', 'Setup all available services')
330
+ .option('--github', 'Setup GitHub authentication')
331
+ .option('--ai', 'Setup AI tools authentication')
332
+ .option('--cloud', 'Setup cloud provider credentials')
333
+ .option('--databases', 'Setup database credentials')
334
+ .option('--registries', 'Setup container registry credentials')
335
+ .option('--packages', 'Setup package manager credentials')
336
+ .option('--monitoring', 'Setup monitoring service credentials')
337
+ .option('--org', 'Setup organization settings')
338
+ .action(async (options) => {
339
+ console.log(chalk.blue.bold('\n🔐 Delimit Authentication Setup\n'));
340
+
341
+ const authSetup = new DelimitAuthSetup();
342
+
343
+ // Determine what to setup
344
+ const setupOptions = {
345
+ setupAll: options.all,
346
+ setupGitHub: options.github,
347
+ setupAI: options.ai,
348
+ setupCloud: options.cloud,
349
+ setupDatabases: options.databases,
350
+ setupRegistries: options.registries,
351
+ setupPackages: options.packages,
352
+ setupMonitoring: options.monitoring,
353
+ setupOrg: options.org
354
+ };
355
+
356
+ // If no specific options, prompt for what to setup
357
+ if (!Object.values(setupOptions).some(v => v)) {
358
+ const { categories } = await inquirer.prompt([{
359
+ type: 'checkbox',
360
+ name: 'categories',
361
+ message: 'Which services would you like to configure?',
362
+ choices: [
363
+ { name: 'GitHub', value: 'github' },
364
+ { name: 'AI Tools (Claude, OpenAI, Gemini, etc.)', value: 'ai' },
365
+ { name: 'Cloud Providers (AWS, GCP, Azure)', value: 'cloud' },
366
+ { name: 'Databases', value: 'databases' },
367
+ { name: 'Container Registries', value: 'registries' },
368
+ { name: 'Package Managers', value: 'packages' },
369
+ { name: 'Monitoring Services', value: 'monitoring' },
370
+ { name: 'Organization Settings', value: 'org' }
371
+ ]
372
+ }]);
373
+
374
+ categories.forEach(cat => {
375
+ setupOptions[`setup${cat.charAt(0).toUpperCase() + cat.slice(1)}`] = true;
376
+ });
377
+ }
378
+
379
+ await authSetup.setup(setupOptions);
380
+
381
+ console.log(chalk.green.bold('\n✅ Authentication setup complete!\n'));
382
+ console.log('Your credentials have been securely stored in ~/.delimit/credentials/');
383
+ console.log('Run "delimit auth" again anytime to add or update credentials');
384
+ });
385
+
386
+ // Audit command
387
+ program
388
+ .command('audit')
389
+ .description('View governance audit log')
390
+ .option('--tail <n>', 'Show last N entries', '10')
391
+ .action(async (options) => {
392
+ await ensureAgent();
393
+
394
+ const { data } = await axios.get(`${AGENT_URL}/audit`);
395
+ const entries = data.slice(-parseInt(options.tail));
396
+
397
+ if (entries.length === 0) {
398
+ console.log(chalk.yellow('No audit log entries'));
399
+ return;
400
+ }
401
+
402
+ console.log(chalk.blue.bold('\nRecent Governance Decisions:\n'));
403
+ entries.forEach(entry => {
404
+ const color = entry.action === 'block' ? chalk.red :
405
+ entry.action === 'prompt' ? chalk.yellow :
406
+ chalk.green;
407
+
408
+ console.log(`${entry.timestamp} | ${color(entry.mode.toUpperCase())} | ${entry.message}`);
409
+ if (entry.rule) {
410
+ console.log(` Rule: ${entry.rule}`);
411
+ }
412
+ });
413
+ });
414
+
415
+ // Doctor command - diagnose issues
416
+ program
417
+ .command('doctor')
418
+ .description('Diagnose Delimit configuration and issues')
419
+ .action(async () => {
420
+ console.log(chalk.blue.bold('\n🩺 Delimit Doctor\n'));
421
+ const issues = [];
422
+ const warnings = [];
423
+ const info = [];
424
+
425
+ // Check agent status
426
+ const agentRunning = await checkAgent();
427
+ if (!agentRunning) {
428
+ issues.push('Agent is not running. Run "delimit status" to start it.');
429
+ } else {
430
+ info.push('Agent is running and responsive');
431
+ }
432
+
433
+ // Check Git hooks
434
+ try {
435
+ const hooksPath = execSync('git config --global core.hooksPath').toString().trim();
436
+ if (hooksPath.includes('.delimit')) {
437
+ info.push('Git hooks are configured correctly');
438
+
439
+ // Check hook files exist
440
+ const hookFiles = ['pre-commit', 'pre-push'];
441
+ hookFiles.forEach(hook => {
442
+ const hookFile = path.join(hooksPath, hook);
443
+ if (!fs.existsSync(hookFile)) {
444
+ warnings.push(`Missing hook file: ${hook}`);
445
+ }
446
+ });
447
+ } else {
448
+ warnings.push('Git hooks not pointing to Delimit. Run "delimit install" to fix.');
449
+ }
450
+ } catch (e) {
451
+ issues.push('Git hooks not configured. Run "delimit install" to set up.');
452
+ }
453
+
454
+ // Check PATH
455
+ const pathHasDelimit = process.env.PATH.includes('.delimit/shims');
456
+ if (pathHasDelimit) {
457
+ warnings.push('PATH hijacking is active (for AI tool interception)');
458
+ } else {
459
+ info.push('PATH is clean (no AI tool interception)');
460
+ }
461
+
462
+ // Check policy files
463
+ const policies = [];
464
+ if (fs.existsSync('delimit.yml')) {
465
+ policies.push('project');
466
+ // Validate policy
467
+ try {
468
+ const policy = yaml.load(fs.readFileSync('delimit.yml', 'utf8'));
469
+ if (!policy.rules) {
470
+ warnings.push('Project policy has no rules defined');
471
+ }
472
+ } catch (e) {
473
+ issues.push(`Project policy is invalid: ${e.message}`);
474
+ }
475
+ }
476
+
477
+ const userPolicyPath = path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml');
478
+ if (fs.existsSync(userPolicyPath)) {
479
+ policies.push('user');
480
+ }
481
+
482
+ if (policies.length === 0) {
483
+ warnings.push('No policy files found. Run "delimit policy --init" to create one.');
484
+ } else {
485
+ info.push(`Policy files loaded: ${policies.join(', ')}`);
486
+ }
487
+
488
+ // Check audit log
489
+ const auditDir = path.join(process.env.HOME, '.delimit', 'audit');
490
+ if (fs.existsSync(auditDir)) {
491
+ const files = fs.readdirSync(auditDir);
492
+ info.push(`Audit log has ${files.length} day(s) of history`);
493
+ } else {
494
+ warnings.push('No audit logs found yet');
495
+ }
496
+
497
+ // Display results
498
+ if (issues.length > 0) {
499
+ console.log(chalk.red.bold('❌ Issues Found:\n'));
500
+ issues.forEach(issue => console.log(chalk.red(` • ${issue}`)));
501
+ console.log();
502
+ }
503
+
504
+ if (warnings.length > 0) {
505
+ console.log(chalk.yellow.bold('⚠️ Warnings:\n'));
506
+ warnings.forEach(warning => console.log(chalk.yellow(` • ${warning}`)));
507
+ console.log();
508
+ }
509
+
510
+ if (info.length > 0) {
511
+ console.log(chalk.green.bold('✅ Working Correctly:\n'));
512
+ info.forEach(item => console.log(chalk.green(` • ${item}`)));
513
+ console.log();
514
+ }
515
+
516
+ // Overall status
517
+ if (issues.length === 0) {
518
+ console.log(chalk.green.bold('🎉 Delimit is healthy!'));
519
+ } else {
520
+ console.log(chalk.red.bold('🔧 Please fix the issues above'));
521
+ process.exit(1);
522
+ }
523
+ });
524
+
525
+ // Explain-decision command - show governance decision reasoning
526
+ program
527
+ .command('explain-decision [decision-id]')
528
+ .description('Explain a governance decision')
529
+ .action(async (decisionId) => {
530
+ await ensureAgent();
531
+
532
+ try {
533
+ const { data } = await axios.get(`${AGENT_URL}/explain/${decisionId || 'last'}`);
534
+ console.log(data.explanation);
535
+ } catch (e) {
536
+ if (e.response?.status === 404) {
537
+ console.log(chalk.red('No decision found'));
538
+ } else {
539
+ console.log(chalk.red('Error fetching decision explanation'));
540
+ }
541
+ }
542
+ });
543
+
544
+ // Uninstall command
545
+ program
546
+ .command('uninstall')
547
+ .description('Remove Delimit governance')
548
+ .action(async () => {
549
+ const { confirm } = await inquirer.prompt([{
550
+ type: 'confirm',
551
+ name: 'confirm',
552
+ message: 'This will remove all Delimit governance. Continue?',
553
+ default: false
554
+ }]);
555
+
556
+ if (!confirm) return;
557
+
558
+ // Remove Git hooks
559
+ try {
560
+ execSync('git config --global --unset core.hooksPath');
561
+ console.log(chalk.green('✓ Removed Git hooks'));
562
+ } catch (e) {}
563
+
564
+ // Remove from PATH
565
+ const profiles = ['.bashrc', '.zshrc', '.profile'];
566
+ profiles.forEach(profile => {
567
+ const profilePath = path.join(process.env.HOME, profile);
568
+ if (fs.existsSync(profilePath)) {
569
+ let content = fs.readFileSync(profilePath, 'utf8');
570
+ content = content.replace(/# Delimit Governance Layer[\s\S]*?fi\n/g, '');
571
+ fs.writeFileSync(profilePath, content);
572
+ }
573
+ });
574
+ console.log(chalk.green('✓ Removed PATH modifications'));
575
+
576
+ console.log(chalk.yellow('\nRestart your terminal to complete uninstallation'));
577
+ });
578
+
579
+ // Helper function for installation
580
+ async function installDelimit(mode, scope, hooksType = 'all') {
581
+ const HOME = process.env.HOME;
582
+ const DELIMIT_HOME = path.join(HOME, '.delimit');
583
+
584
+ // Create directories
585
+ ['bin', 'hooks', 'shims', 'config', 'audit', 'credentials'].forEach(dir => {
586
+ fs.mkdirSync(path.join(DELIMIT_HOME, dir), { recursive: true });
587
+ });
588
+
589
+ // Install hooks using the hooks installer
590
+ const hooksInstaller = new DelimitHooksInstaller();
591
+
592
+ if (hooksType === 'all' || hooksType === 'git') {
593
+ console.log(chalk.yellow('Installing Git hooks...'));
594
+ await hooksInstaller.installGitHooks();
595
+ console.log(chalk.green('✓ Installed Git hooks'));
596
+ }
597
+
598
+ if (hooksType === 'all' || hooksType === 'ai') {
599
+ console.log(chalk.yellow('Installing AI tool hooks...'));
600
+ await hooksInstaller.installAIHooks();
601
+ console.log(chalk.green('✓ Installed AI tool hooks'));
602
+ }
603
+
604
+ if (hooksType === 'all' || hooksType === 'mcp') {
605
+ console.log(chalk.yellow('Installing MCP hooks...'));
606
+ await hooksInstaller.installMCPHooks();
607
+ console.log(chalk.green('✓ Installed MCP hooks'));
608
+ }
609
+
610
+ // Start agent
611
+ await ensureAgent();
612
+
613
+ // Set initial mode
614
+ await axios.post(`${AGENT_URL}/mode`, { mode });
615
+
616
+ // Create environment file
617
+ const envContent = `#!/bin/sh
618
+ # Delimit Governance Environment Variables
619
+ export DELIMIT_MODE="${mode}"
620
+ export DELIMIT_HOME="${DELIMIT_HOME}"
621
+ export DELIMIT_AGENT_URL="${AGENT_URL}"
622
+ export DELIMIT_ACTIVE=true
623
+ `;
624
+ fs.writeFileSync(path.join(DELIMIT_HOME, 'env'), envContent);
625
+ fs.chmodSync(path.join(DELIMIT_HOME, 'env'), '644');
626
+
627
+ console.log(chalk.green.bold('\n✅ Delimit installed successfully!\n'));
628
+ console.log('Next steps:');
629
+ console.log('1. Create policy file: delimit policy --init');
630
+ console.log('2. Check status: delimit status');
631
+ console.log('3. Switch modes: delimit mode <mode>');
632
+ console.log('4. Setup authentication: delimit auth');
633
+ }
634
+
635
+ // Proxy command for AI tools
636
+ program
637
+ .command('proxy <tool>')
638
+ .allowUnknownOption()
639
+ .description('Proxy AI tool execution with governance')
640
+ .action(async (tool, options) => {
641
+ const { proxyAITool } = require('../lib/proxy-handler');
642
+ // Get all args after the tool name
643
+ const toolIndex = process.argv.indexOf(tool);
644
+ const args = process.argv.slice(toolIndex + 1);
645
+ await proxyAITool(tool, args);
646
+ });
647
+
648
+ // Hook handler (called by Git hooks)
649
+ program
650
+ .command('hook <type>')
651
+ .description('Internal hook handler')
652
+ .action(async (type) => {
653
+ await ensureAgent();
654
+
655
+ // Gather context
656
+ const context = {
657
+ command: type,
658
+ pwd: process.cwd(),
659
+ gitBranch: 'unknown',
660
+ files: [],
661
+ diff: ''
662
+ };
663
+
664
+ // Try to get Git info, but don't fail if not in repo
665
+ try {
666
+ context.gitBranch = execSync('git branch --show-current 2>/dev/null').toString().trim() || 'unknown';
667
+ } catch (e) {
668
+ // Not in a Git repo or Git not available
669
+ context.gitBranch = 'unknown';
670
+ }
671
+
672
+ if (type === 'pre-commit') {
673
+ try {
674
+ context.files = execSync('git diff --cached --name-only 2>/dev/null').toString().split('\n').filter(f => f);
675
+ context.diff = execSync('git diff --cached 2>/dev/null').toString();
676
+ } catch (e) {
677
+ // Not in a Git repo or no staged changes
678
+ context.files = [];
679
+ context.diff = '';
680
+ }
681
+ } else if (type === 'pre-push') {
682
+ try {
683
+ // Get commits to be pushed
684
+ context.files = execSync('git diff --name-only @{upstream}...HEAD 2>/dev/null').toString().split('\n').filter(f => f);
685
+ context.diff = execSync('git diff @{upstream}...HEAD 2>/dev/null').toString();
686
+ } catch (e) {
687
+ // No upstream or not in repo
688
+ context.files = [];
689
+ context.diff = '';
690
+ }
691
+ }
692
+
693
+ // Query agent for decision
694
+ const { data: decision } = await axios.post(`${AGENT_URL}/evaluate`, context);
695
+
696
+ // Display decision
697
+ if (decision.message) {
698
+ const color = decision.action === 'block' ? chalk.red :
699
+ decision.action === 'prompt' ? chalk.yellow :
700
+ chalk.blue;
701
+ console.log(color(decision.message));
702
+ }
703
+
704
+ // Handle the decision
705
+ if (decision.action === 'block') {
706
+ if (decision.requiresOverride) {
707
+ console.log(chalk.red('Action blocked. Cannot override in enforce mode.'));
708
+ process.exit(1);
709
+ } else {
710
+ const { override } = await inquirer.prompt([{
711
+ type: 'confirm',
712
+ name: 'override',
713
+ message: 'Override and continue?',
714
+ default: false
715
+ }]);
716
+
717
+ if (!override) {
718
+ process.exit(1);
719
+ }
720
+ }
721
+ } else if (decision.action === 'prompt') {
722
+ const { proceed } = await inquirer.prompt([{
723
+ type: 'confirm',
724
+ name: 'proceed',
725
+ message: 'Continue with this action?',
726
+ default: false
727
+ }]);
728
+
729
+ if (!proceed) {
730
+ process.exit(1);
731
+ }
732
+ }
733
+
734
+ // Action allowed
735
+ process.exit(0);
736
+ });
737
+
738
+ // ═══════════════════════════════════════════════════════════════════════
739
+ // V1 PUBLIC COMMANDS — API Contract Governance
740
+ // ═══════════════════════════════════════════════════════════════════════
741
+
742
+ const apiEngine = require('../lib/api-engine');
743
+
744
+ // Init command — scaffold .delimit/ config
745
+ program
746
+ .command('init')
747
+ .description('Initialize Delimit API governance in this project')
748
+ .action(async () => {
749
+ const configDir = path.join(process.cwd(), '.delimit');
750
+ const policyFile = path.join(configDir, 'policies.yml');
751
+
752
+ if (fs.existsSync(policyFile)) {
753
+ console.log(chalk.yellow('Already initialized — .delimit/policies.yml exists'));
754
+ return;
755
+ }
756
+
757
+ fs.mkdirSync(configDir, { recursive: true });
758
+
759
+ const template = `# Delimit API Governance Policy
760
+ # https://github.com/delimit-ai/delimit
761
+
762
+ # Override built-in rules (default: false)
763
+ override_defaults: false
764
+
765
+ rules: []
766
+ # Example:
767
+ # - id: protect_v1
768
+ # name: Protect V1 API
769
+ # change_types: [endpoint_removed, method_removed, field_removed]
770
+ # severity: error
771
+ # action: forbid
772
+ # conditions:
773
+ # path_pattern: "^/v1/.*"
774
+ # message: "V1 API is frozen. Make changes in V2."
775
+ `;
776
+ fs.writeFileSync(policyFile, template);
777
+ console.log(chalk.green('Created .delimit/policies.yml'));
778
+ console.log('');
779
+ console.log('Next steps:');
780
+ console.log(` ${chalk.bold('delimit lint')} old.yaml new.yaml — check for breaking changes`);
781
+ console.log(` ${chalk.bold('delimit diff')} old.yaml new.yaml — see all changes`);
782
+ console.log(` ${chalk.bold('delimit explain')} old.yaml new.yaml — human-readable summary`);
783
+ });
784
+
785
+ // Lint command — diff + policy (primary command)
786
+ program
787
+ .command('lint <old_spec> <new_spec>')
788
+ .description('Lint API specs for breaking changes and policy violations')
789
+ .option('-p, --policy <file>', 'Custom policy file')
790
+ .option('--current-version <ver>', 'Current API version for semver bump')
791
+ .option('-n, --name <name>', 'API name for context')
792
+ .option('--json', 'Output raw JSON')
793
+ .action(async (oldSpec, newSpec, options) => {
794
+ try {
795
+ const result = apiEngine.lint(
796
+ path.resolve(oldSpec),
797
+ path.resolve(newSpec),
798
+ { policy: options.policy, version: options.currentVersion, name: options.name }
799
+ );
800
+
801
+ if (options.json) {
802
+ console.log(JSON.stringify(result, null, 2));
803
+ process.exit(result.exit_code || 0);
804
+ return;
805
+ }
806
+
807
+ // Decision banner
808
+ const decision = result.decision;
809
+ const semver = result.semver;
810
+ const banner = decision === 'fail'
811
+ ? chalk.red.bold('FAIL')
812
+ : decision === 'warn'
813
+ ? chalk.yellow.bold('WARN')
814
+ : chalk.green.bold('PASS');
815
+
816
+ const bump = semver ? ` — ${chalk.bold(semver.bump.toUpperCase())}` : '';
817
+ const nextVer = semver && semver.next_version ? ` (${semver.next_version})` : '';
818
+
819
+ console.log(`\n${banner}${bump}${nextVer}\n`);
820
+
821
+ // Summary
822
+ const s = result.summary;
823
+ console.log(` Changes: ${s.total_changes} total, ${s.breaking_changes} breaking`);
824
+ if (s.violations > 0) {
825
+ console.log(` Violations: ${s.errors} error(s), ${s.warnings} warning(s)`);
826
+ }
827
+ console.log('');
828
+
829
+ // Violations
830
+ const violations = result.violations || [];
831
+ if (violations.length > 0) {
832
+ violations.forEach(v => {
833
+ const icon = v.severity === 'error' ? chalk.red('ERR') : chalk.yellow('WRN');
834
+ console.log(` ${icon} ${v.message}`);
835
+ if (v.path) console.log(` ${chalk.gray(v.path)}`);
836
+ });
837
+ console.log('');
838
+ }
839
+
840
+ // Non-breaking changes
841
+ const safe = (result.all_changes || []).filter(c => !c.is_breaking);
842
+ if (safe.length > 0) {
843
+ console.log(chalk.green(' Additions:'));
844
+ safe.forEach(c => console.log(` + ${c.message}`));
845
+ console.log('');
846
+ }
847
+
848
+ process.exit(result.exit_code || 0);
849
+ } catch (err) {
850
+ console.error(chalk.red(`Error: ${err.message}`));
851
+ process.exit(1);
852
+ }
853
+ });
854
+
855
+ // Diff command — pure diff, no policy
856
+ program
857
+ .command('diff <old_spec> <new_spec>')
858
+ .description('Show all changes between two API specs')
859
+ .option('--json', 'Output raw JSON')
860
+ .action(async (oldSpec, newSpec, options) => {
861
+ try {
862
+ const result = apiEngine.diff(
863
+ path.resolve(oldSpec),
864
+ path.resolve(newSpec)
865
+ );
866
+
867
+ if (options.json) {
868
+ console.log(JSON.stringify(result, null, 2));
869
+ return;
870
+ }
871
+
872
+ console.log(`\n ${result.total_changes} change(s), ${result.breaking_changes} breaking\n`);
873
+
874
+ (result.changes || []).forEach(c => {
875
+ const tag = c.is_breaking ? chalk.red('[BREAKING]') : chalk.green('[safe]');
876
+ console.log(` ${tag} ${c.message}`);
877
+ });
878
+ console.log('');
879
+ } catch (err) {
880
+ console.error(chalk.red(`Error: ${err.message}`));
881
+ process.exit(1);
882
+ }
883
+ });
884
+
885
+ // Explain command — human-readable templates
886
+ program
887
+ .command('explain <old_spec> <new_spec>')
888
+ .description('Generate human-readable API change explanation')
889
+ .option('-t, --template <name>', 'Template: developer, team_lead, product, migration, changelog, pr_comment, slack', 'developer')
890
+ .option('--old-version <ver>', 'Old version')
891
+ .option('--new-version <ver>', 'New version')
892
+ .option('-n, --name <name>', 'API name')
893
+ .option('--json', 'Output raw JSON')
894
+ .action(async (oldSpec, newSpec, options) => {
895
+ try {
896
+ const result = apiEngine.explain(
897
+ path.resolve(oldSpec),
898
+ path.resolve(newSpec),
899
+ {
900
+ template: options.template,
901
+ oldVersion: options.oldVersion,
902
+ newVersion: options.newVersion,
903
+ name: options.name,
904
+ }
905
+ );
906
+
907
+ if (options.json) {
908
+ console.log(JSON.stringify(result, null, 2));
909
+ return;
910
+ }
911
+
912
+ console.log('');
913
+ console.log(result.output);
914
+ console.log('');
915
+ } catch (err) {
916
+ console.error(chalk.red(`Error: ${err.message}`));
917
+ process.exit(1);
918
+ }
919
+ });
920
+
921
+ program.parse();