delimit-cli 4.7.4 → 4.7.5

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.
@@ -8,17 +8,14 @@
8
8
  * 3. Installs default agents into ~/.claude/agents/
9
9
  * 4. Prints next steps
10
10
  */
11
-
12
11
  const fs = require('fs');
13
12
  const path = require('path');
14
13
  const { execSync } = require('child_process');
15
14
  const os = require('os');
16
-
17
15
  const DELIMIT_HOME = path.join(os.homedir(), '.delimit');
18
16
  const MCP_CONFIG = path.join(os.homedir(), '.mcp.json');
19
17
  const CLAUDE_DIR = path.join(os.homedir(), '.claude');
20
18
  const AGENTS_DIR = path.join(CLAUDE_DIR, 'agents');
21
-
22
19
  // Colors
23
20
  const green = (s) => `\x1b[32m${s}\x1b[0m`;
24
21
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
@@ -28,12 +25,10 @@ const magenta = (s) => `\x1b[1;35m${s}\x1b[0m`;
28
25
  const orange = (s) => `\x1b[1;33m${s}\x1b[0m`;
29
26
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
30
27
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
31
-
32
28
  function log(msg) { console.log(msg); }
33
29
  function step(n, msg) { log(`\n${blue(`[${n}]`)} ${msg}`); }
34
30
  function pause(ms = 150) { return new Promise(r => setTimeout(r, ms)); }
35
31
  async function logp(msg, ms = 180) { console.log(msg); await pause(ms); }
36
-
37
32
  function findGitDir(startDir) {
38
33
  let dir = startDir;
39
34
  while (dir !== path.dirname(dir)) {
@@ -51,7 +46,6 @@ function findGitDir(startDir) {
51
46
  }
52
47
  return null;
53
48
  }
54
-
55
49
  /**
56
50
  * Recursively find OpenAPI/Swagger spec files, ignoring node_modules.
57
51
  */
@@ -72,7 +66,6 @@ function findSpecFiles(dir, depth = 0) {
72
66
  } catch {}
73
67
  return results;
74
68
  }
75
-
76
69
  async function main() {
77
70
  // Self-update check: ensure we're running the latest version (skip if already re-execed)
78
71
  const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
@@ -87,7 +80,6 @@ async function main() {
87
80
  }
88
81
  } catch { /* offline or timeout — continue with current version */ }
89
82
  }
90
-
91
83
  log('');
92
84
  log(purple(' ____ ________ ______ _____________'));
93
85
  log(purple(' / __ \\/ ____/ / / _/ |/ / _/_ __/'));
@@ -98,10 +90,8 @@ async function main() {
98
90
  const _pkgNow = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
99
91
  log(dim(` v${_pkgNow.version}`));
100
92
  log('');
101
-
102
93
  // Step 1: Check prerequisites
103
94
  step(1, 'Checking prerequisites...');
104
-
105
95
  // Python 3.9+
106
96
  let python = null;
107
97
  for (const cmd of ['python3', 'python']) {
@@ -119,7 +109,6 @@ async function main() {
119
109
  log(` ${yellow('✗')} Python 3.9+ not found. Install Python first.`);
120
110
  process.exit(1);
121
111
  }
122
-
123
112
  // Check if Claude Code is available
124
113
  let hasClaude = false;
125
114
  try {
@@ -129,7 +118,6 @@ async function main() {
129
118
  } catch {
130
119
  log(` ${yellow('!')} Claude Code not detected — MCP config will still be created`);
131
120
  }
132
-
133
121
  // Show what we're about to do and ask for confirmation
134
122
  log('');
135
123
  log(` ${blue('What Delimit will do:')}`);
@@ -145,7 +133,6 @@ async function main() {
145
133
  log('');
146
134
  log(` ${dim('Undo anytime:')} rm -rf ~/.delimit && delimit uninstall`);
147
135
  log('');
148
-
149
136
  const inquirerTop = (() => { try { return require('inquirer'); } catch { return null; } })();
150
137
  if (inquirerTop && process.stdin.isTTY) {
151
138
  try {
@@ -162,17 +149,14 @@ async function main() {
162
149
  } catch {}
163
150
  }
164
151
  log('');
165
-
166
152
  // Step 2: Install Delimit MCP server
167
153
  step(2, 'Installing Delimit MCP server...');
168
-
169
154
  // Create ~/.delimit directory
170
155
  fs.mkdirSync(path.join(DELIMIT_HOME, 'server', 'core', 'zero_spec'), { recursive: true });
171
156
  fs.mkdirSync(path.join(DELIMIT_HOME, 'server', 'tasks'), { recursive: true });
172
157
  fs.mkdirSync(path.join(DELIMIT_HOME, 'deploys'), { recursive: true });
173
158
  fs.mkdirSync(path.join(DELIMIT_HOME, 'ledger'), { recursive: true });
174
159
  fs.mkdirSync(path.join(DELIMIT_HOME, 'evidence'), { recursive: true });
175
-
176
160
  // Copy the gateway core from our bundled copy
177
161
  // Skip if server dirs are symlinks (dev machine using gateway source directly)
178
162
  const serverAiDir = path.join(DELIMIT_HOME, 'server', 'ai');
@@ -193,13 +177,11 @@ async function main() {
193
177
  log(` ${yellow('!')} Could not download. Clone manually: git clone https://github.com/delimit-ai/delimit-gateway.git ~/.delimit/server`);
194
178
  }
195
179
  }
196
-
197
180
  // Copy the MCP server file
198
181
  const serverSource = path.join(__dirname, '..', 'mcp-server.py');
199
182
  if (fs.existsSync(serverSource)) {
200
183
  fs.copyFileSync(serverSource, path.join(DELIMIT_HOME, 'server', 'mcp-server.py'));
201
184
  }
202
-
203
185
  // Download compiled Pro modules (platform-specific)
204
186
  const proDir = path.join(DELIMIT_HOME, 'server', 'ai');
205
187
  const pyVer = (() => { try { return execSync(`${python} -c "import sys; print(f'cp{sys.version_info.major}{sys.version_info.minor}')"`, { encoding: 'utf-8' }).trim(); } catch { return 'cp310'; } })();
@@ -209,7 +191,6 @@ async function main() {
209
191
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
210
192
  const proVersion = pkg.proModuleVersion || '3.8.2';
211
193
  const proUrl = `https://delimit.ai/releases/v${proVersion}/delimit-pro-${artifact}.tar.gz`;
212
-
213
194
  try {
214
195
  const proTarball = path.join(DELIMIT_HOME, 'pro.tar.gz');
215
196
  execSync(`curl -sL "${proUrl}" -o "${proTarball}" --fail`, { stdio: 'pipe', timeout: 30000 });
@@ -219,13 +200,11 @@ async function main() {
219
200
  } catch {
220
201
  log(` ${dim(' Pro modules not available for ${artifact} — free tools work fine')}`);
221
202
  }
222
-
223
203
  // Re-copy gateway source AFTER Pro modules to ensure full files aren't overwritten by stubs
224
204
  // Skip if dev symlinks are in place
225
205
  if (fs.existsSync(gatewaySource) && !isDevSymlink) {
226
206
  copyDir(gatewaySource, path.join(DELIMIT_HOME, 'server'));
227
207
  }
228
-
229
208
  // Remove stale .so binaries that shadow updated .py source files
230
209
  // Python loads .so before .py, so old compiled stubs block new source
231
210
  const aiDir = path.join(DELIMIT_HOME, 'server', 'ai');
@@ -244,7 +223,6 @@ async function main() {
244
223
  }
245
224
  } catch { /* ignore cleanup errors */ }
246
225
  }
247
-
248
226
  // Install Python deps into isolated venv with pinned versions
249
227
  log(` ${dim(' Installing Python dependencies...')}`);
250
228
  const venvDir = path.join(DELIMIT_HOME, 'venv');
@@ -276,7 +254,6 @@ async function main() {
276
254
  log(` ${yellow('!')} pip install failed — run manually: pip install fastmcp pyyaml pydantic packaging pytest`);
277
255
  }
278
256
  }
279
-
280
257
  // LED-1084 week 2: build the content-grounding feature whitelist.
281
258
  // Populates ~/.delimit/content/grounding/features.json with every
282
259
  // @mcp.tool() entry + every CLI subcommand we ship. Drafters then
@@ -299,10 +276,8 @@ async function main() {
299
276
  // which makes the gate strict but doesn't break anything. Customers
300
277
  // can rebuild manually later: `python -m ai.content_grounding.features build`.
301
278
  }
302
-
303
279
  // Step 3: Configure Claude Code MCP
304
280
  step(3, 'Configuring Claude Code MCP...');
305
-
306
281
  const configuredTools = [];
307
282
  let mcpConfig = {};
308
283
  if (fs.existsSync(MCP_CONFIG)) {
@@ -311,14 +286,11 @@ async function main() {
311
286
  } catch {}
312
287
  }
313
288
  if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
314
-
315
289
  const serverPath = path.join(DELIMIT_HOME, 'server', 'ai', 'server.py');
316
290
  const serverPathAlt = path.join(DELIMIT_HOME, 'server', 'mcp-server.py');
317
291
  const actualServer = fs.existsSync(serverPath) ? serverPath : serverPathAlt;
318
-
319
292
  // Always update paths to match this machine's directory structure
320
293
  const delimitMcp = {
321
- type: 'stdio',
322
294
  command: python,
323
295
  args: [actualServer],
324
296
  cwd: path.join(DELIMIT_HOME, 'server'),
@@ -337,7 +309,6 @@ async function main() {
337
309
  await logp(` ${green('✓')} Added delimit to ${MCP_CONFIG}`);
338
310
  configuredTools.push('Claude Code');
339
311
  }
340
-
341
312
  // Auto-approve all Delimit tools in Claude Code settings.json
342
313
  const CLAUDE_SETTINGS = path.join(CLAUDE_DIR, 'settings.json');
343
314
  try {
@@ -356,7 +327,6 @@ async function main() {
356
327
  } catch (e) {
357
328
  log(` ${yellow('!')} Could not set Claude Code permissions: ${e.message}`);
358
329
  }
359
-
360
330
  // Step 3b: Configure Codex MCP (if installed)
361
331
  const CODEX_CONFIG = path.join(os.homedir(), '.codex', 'config.toml');
362
332
  // Create config.toml if .codex dir exists or codex is in PATH
@@ -379,7 +349,6 @@ async function main() {
379
349
  const serverDir = path.join(DELIMIT_HOME, 'server');
380
350
  // approval_policy = "never" means auto-approve all tools from this server (no per-prompt confirmations)
381
351
  const correctEntry = `\n[mcp_servers.delimit]\ncommand = "${python}"\nargs = ["${actualServer}"]\ncwd = "${serverDir}"\napproval_policy = "never"\n\n[mcp_servers.delimit.env]\nPYTHONPATH = "${serverDir}:${path.join(serverDir, 'ai')}"\n`;
382
-
383
352
  // Remove ALL existing delimit MCP entries (prevents duplicates)
384
353
  const existed = toml.includes('mcp_servers.delimit');
385
354
  const lines = toml.split('\n');
@@ -410,7 +379,6 @@ async function main() {
410
379
  log(` ${yellow('!')} Could not configure Codex: ${e.message}`);
411
380
  }
412
381
  }
413
-
414
382
  // Step 3c: Configure Cursor MCP (if installed)
415
383
  const CURSOR_CONFIG = path.join(os.homedir(), '.cursor', 'mcp.json');
416
384
  if (fs.existsSync(path.join(os.homedir(), '.cursor'))) {
@@ -439,7 +407,6 @@ async function main() {
439
407
  log(` ${yellow('!')} Could not configure Cursor: ${e.message}`);
440
408
  }
441
409
  }
442
-
443
410
  // Step 3d: Configure Gemini CLI (if installed)
444
411
  const GEMINI_DIR = path.join(os.homedir(), '.gemini');
445
412
  const GEMINI_CONFIG = path.join(GEMINI_DIR, 'settings.json');
@@ -480,7 +447,6 @@ async function main() {
480
447
  log(` ${yellow('!')} Could not configure Gemini CLI: ${e.message}`);
481
448
  }
482
449
  }
483
-
484
450
  // Step 3e: Configure Antigravity CLI (if installed)
485
451
  const ANTIGRAVITY_DIR = path.join(os.homedir(), '.gemini', 'antigravity-cli');
486
452
  const ANTIGRAVITY_CONFIG = path.join(ANTIGRAVITY_DIR, 'settings.json');
@@ -493,6 +459,7 @@ async function main() {
493
459
  if (!antigravityConfig.mcpServers) antigravityConfig.mcpServers = {};
494
460
  const antigravityExisted = !!antigravityConfig.mcpServers.delimit;
495
461
  antigravityConfig.mcpServers.delimit = {
462
+ type: 'stdio',
496
463
  command: python,
497
464
  args: [actualServer],
498
465
  cwd: path.join(DELIMIT_HOME, 'server'),
@@ -519,7 +486,6 @@ async function main() {
519
486
  log(` ${yellow('!')} Could not configure Antigravity CLI: ${e.message}`);
520
487
  }
521
488
  }
522
-
523
489
  // Checkpoint: MCP is configured, now ask before modifying project files
524
490
  log('');
525
491
  log(` ${green('✓')} MCP server installed and configured`);
@@ -529,7 +495,6 @@ async function main() {
529
495
  log(` • Update CLAUDE.md with Delimit instructions`);
530
496
  log(` • Optional: governance wrapping + hooks`);
531
497
  log('');
532
-
533
498
  const inquirerMid = (() => { try { return require('inquirer'); } catch { return null; } })();
534
499
  if (inquirerMid && process.stdin.isTTY) {
535
500
  try {
@@ -546,12 +511,9 @@ async function main() {
546
511
  }
547
512
  } catch {}
548
513
  }
549
-
550
514
  // Step 4: Install default agents
551
515
  step(4, 'Installing governance agents...');
552
-
553
516
  fs.mkdirSync(AGENTS_DIR, { recursive: true });
554
-
555
517
  const agents = {
556
518
  'lint.md': `---
557
519
  name: lint
@@ -567,9 +529,7 @@ tools:
567
529
  - mcp__delimit__delimit_impact
568
530
  - mcp__delimit__delimit_ledger
569
531
  ---
570
-
571
532
  # Lint Agent
572
-
573
533
  Run API governance checks. Use delimit_lint to compare specs, delimit_policy to check rules, delimit_impact for downstream analysis.
574
534
  `,
575
535
  'engineering.md': `---
@@ -587,9 +547,7 @@ tools:
587
547
  - mcp__delimit__delimit_test_generate
588
548
  - mcp__delimit__delimit_test_coverage
589
549
  ---
590
-
591
550
  # Engineering Agent
592
-
593
551
  Execute code directives. Use delimit_test_coverage to verify coverage targets. Use delimit_lint to check API compatibility after changes.
594
552
  `,
595
553
  'governance.md': `---
@@ -609,13 +567,10 @@ tools:
609
567
  - mcp__delimit__delimit_repo_analyze
610
568
  - mcp__delimit__delimit_repo_config_validate
611
569
  ---
612
-
613
570
  # Governance Agent
614
-
615
571
  Run full governance compliance checks. Verify security, policy compliance, evidence collection, and repo health.
616
572
  `
617
573
  };
618
-
619
574
  let installed = 0;
620
575
  for (const [filename, content] of Object.entries(agents)) {
621
576
  const agentPath = path.join(AGENTS_DIR, filename);
@@ -625,7 +580,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
625
580
  }
626
581
  }
627
582
  await logp(` ${green('✓')} ${installed} agents installed (${Object.keys(agents).length - installed} already existed)`);
628
-
629
583
  // Step 4b: Install Git hooks if inside a git repository
630
584
  const gitDir = findGitDir(process.cwd());
631
585
  if (gitDir) {
@@ -660,10 +614,8 @@ Run full governance compliance checks. Verify security, policy compliance, evide
660
614
  } else {
661
615
  log(` ${dim(' Not inside a git repo — git hooks will be installed on next delimit setup inside a repo')}`);
662
616
  }
663
-
664
617
  // Step 5: Create/update CLAUDE.md and platform instruction files
665
618
  step(5, 'Setting up AI instruction files...');
666
-
667
619
  const claudeMd = path.join(os.homedir(), 'CLAUDE.md');
668
620
  const claudeResult = upsertDelimitSection(claudeMd);
669
621
  if (claudeResult.action === 'created') {
@@ -675,7 +627,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
675
627
  } else {
676
628
  log(` ${dim(' CLAUDE.md already up to date')}`);
677
629
  }
678
-
679
630
  // Codex instructions: AGENTS.md is the file Codex CLI auto-loads
680
631
  // ("from CWD up to the root", per Codex binary spec). The previous
681
632
  // ~/.codex/instructions.md write was dead code — Codex never read it.
@@ -699,7 +650,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
699
650
  log(` ${dim(' AGENTS.md already up to date')}`);
700
651
  }
701
652
  }
702
-
703
653
  // Gemini CLI: GEMINI.md is the auto-loaded instruction file
704
654
  // (~/.gemini/GEMINI.md is the user-global tier per Gemini CLI bundle).
705
655
  // LED-1399: install when Gemini CLI is present so governance triggers
@@ -724,7 +674,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
724
674
  log(` ${dim(' GEMINI.md already up to date')}`);
725
675
  }
726
676
  }
727
-
728
677
  // Antigravity CLI: ANTIGRAVITY.md is the auto-loaded instruction file.
729
678
  // We install it when antigravity/agy is present, or when ~/.gemini/antigravity-cli exists.
730
679
  const antigravityHome = path.join(os.homedir(), '.gemini', 'antigravity-cli');
@@ -746,7 +695,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
746
695
  log(` ${dim(' ANTIGRAVITY.md already up to date')}`);
747
696
  }
748
697
  }
749
-
750
698
  // Cursor rules (legacy .cursorrules + new .cursor/rules/ directory)
751
699
  const cursorRules = path.join(os.homedir(), '.cursorrules');
752
700
  if (fs.existsSync(path.join(os.homedir(), '.cursor'))) {
@@ -765,7 +713,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
765
713
  log(` ${dim(' Could not install .cursor/rules: ' + e.message)}`);
766
714
  }
767
715
  }
768
-
769
716
  // Silent: auto-detect API keys and configure models.json (no output)
770
717
  const modelsPath = path.join(DELIMIT_HOME, 'models.json');
771
718
  if (!fs.existsSync(modelsPath)) {
@@ -787,7 +734,6 @@ Run full governance compliance checks. Verify security, policy compliance, evide
787
734
  fs.writeFileSync(modelsPath, JSON.stringify(models, null, 2));
788
735
  }
789
736
  }
790
-
791
737
  // Step 6: Governance wrapping (shims)
792
738
  step(6, 'Governance wrapping...');
793
739
  log('');
@@ -807,18 +753,14 @@ Run full governance compliance checks. Verify security, policy compliance, evide
807
753
  log(` ${dim('Adds ~/.delimit/shims to your shell PATH.')}`);
808
754
  log(` ${dim('Disable anytime: delimit shims disable')}`);
809
755
  log('');
810
-
811
756
  // Check if shims already installed
812
757
  const shimsDir = path.join(DELIMIT_HOME, 'shims');
813
758
  const shimsInstalled = fs.existsSync(shimsDir) && fs.readdirSync(shimsDir).length > 0;
814
-
815
759
  let enableShims = shimsInstalled; // Auto-yes if already installed (just update)
816
-
817
760
  if (!shimsInstalled) {
818
761
  // First install — prompt
819
762
  const inquirer = (() => { try { return require('inquirer'); } catch { return null; } })();
820
763
  enableShims = true;
821
-
822
764
  if (inquirer && process.stdin.isTTY) {
823
765
  try {
824
766
  const answer = await inquirer.prompt([{
@@ -833,11 +775,9 @@ Run full governance compliance checks. Verify security, policy compliance, evide
833
775
  }
834
776
  }
835
777
  }
836
-
837
778
  if (enableShims) {
838
779
  // Create/update shims with latest template
839
780
  fs.mkdirSync(shimsDir, { recursive: true });
840
-
841
781
  const shimTemplate = (toolName, displayName) => `#!/bin/sh
842
782
  # Delimit Governance Shim for ${displayName}
843
783
  PURPLE='\\033[35m'; MAGENTA='\\033[91m'; ORANGE='\\033[33m'; GREEN='\\033[32m'
@@ -896,7 +836,6 @@ printf " \${MAGENTA}\${BOLD}[Delimit]\${RESET} \${MAGENTA}═══════
896
836
  sleep 0.08
897
837
  printf " \${GREEN}\${BOLD}[Delimit]\${RESET} \${GREEN}✓ Allowed\${RESET}\\n"
898
838
  echo ""
899
-
900
839
  # --- Exit screen: session summary after AI exits ---
901
840
  delimit_exit_screen() {
902
841
  _EXIT_CODE=\$1
@@ -980,7 +919,6 @@ delimit_exit_screen() {
980
919
  printf " \${MAGENTA}\${BOLD}[Delimit]\${RESET} \${MAGENTA}═══════════════════════════════════════════\${RESET}\\n"
981
920
  echo ""
982
921
  }
983
-
984
922
  # Run real binary and show exit screen (replaces exec to allow post-exit code)
985
923
  delimit_run_and_exit() {
986
924
  "\$@"
@@ -988,7 +926,6 @@ delimit_run_and_exit() {
988
926
  [ "$DELIMIT_QUIET" != "true" ] && delimit_exit_screen \$_RC
989
927
  exit \$_RC
990
928
  }
991
-
992
929
  # Find real binary by stripping shim dir from PATH.
993
930
  # We rely on PATH ordering ($HOME/.delimit/shims first) — no rename hack,
994
931
  # no race with npm reinstalls. Previously we used mv tool→tool-real + cp shim,
@@ -1011,7 +948,6 @@ case "${toolName}" in
1011
948
  esac
1012
949
  exit 127
1013
950
  `;
1014
-
1015
951
  for (const [tool, display] of [
1016
952
  ['claude', 'Claude'],
1017
953
  ['codex', 'Codex'],
@@ -1023,7 +959,6 @@ exit 127
1023
959
  fs.writeFileSync(shimPath, shimTemplate(tool, display));
1024
960
  fs.chmodSync(shimPath, '755');
1025
961
  }
1026
-
1027
962
  // Governance is enforced via PATH ordering — $HOME/.delimit/shims
1028
963
  // is prepended to PATH (see below), so `claude`/`codex`/`gemini`
1029
964
  // resolve to our shim first, and the shim then PATH-strips itself
@@ -1035,7 +970,6 @@ exit 127
1035
970
  // symlink), leaving users with "[Delimit] claude not found in PATH"
1036
971
  // when the rename ran but the shim copy failed or the symlink got
1037
972
  // re-created mid-operation. PATH ordering is the durable contract.
1038
-
1039
973
  // Add to PATH in shell rc files (create if missing)
1040
974
  const pathLine = `export PATH="${shimsDir}:$PATH" # Delimit governance wrapping`;
1041
975
  let pathWritten = false;
@@ -1069,13 +1003,11 @@ exit 127
1069
1003
  fs.writeFileSync(profileD, `# Delimit governance wrapping\n${pathLine}\n`);
1070
1004
  }
1071
1005
  } catch { /* not writable, skip */ }
1072
-
1073
1006
  if (shimsInstalled) {
1074
1007
  await logp(` ${green('✓')} Governance shims updated`);
1075
1008
  } else {
1076
1009
  log(` ${green('✓')} Governance wrapping enabled`);
1077
1010
  }
1078
-
1079
1011
  // Check if shims are already in current PATH
1080
1012
  const currentPath = process.env.PATH || '';
1081
1013
  if (!currentPath.includes('.delimit/shims')) {
@@ -1092,20 +1024,16 @@ exit 127
1092
1024
  log(` ${dim(' Skipped. Enable later: delimit shims enable')}`);
1093
1025
  }
1094
1026
  log('');
1095
-
1096
1027
  // Step 7: Install cross-model governance hooks (LED-202)
1097
1028
  step(7, 'Installing AI assistant hooks...');
1098
-
1099
1029
  try {
1100
1030
  const crossModelHooks = require('../lib/cross-model-hooks');
1101
1031
  const hookConfig = crossModelHooks.loadHookConfig();
1102
1032
  const detected = crossModelHooks.detectAITools();
1103
-
1104
1033
  if (detected.length === 0) {
1105
1034
  log(` ${dim(' No AI assistants detected -- hooks will be installed when tools are found')}`);
1106
1035
  } else {
1107
1036
  log(` ${dim(' Detected: ' + detected.map(t => t.name).join(', '))}`);
1108
-
1109
1037
  // Install hooks (auto-accept in non-interactive or prompt if TTY)
1110
1038
  let installHooks = true;
1111
1039
  const inq = (() => { try { return require('inquirer'); } catch { return null; } })();
@@ -1122,7 +1050,6 @@ exit 127
1122
1050
  installHooks = true;
1123
1051
  }
1124
1052
  }
1125
-
1126
1053
  if (installHooks) {
1127
1054
  for (const tool of detected) {
1128
1055
  const result = crossModelHooks.installHooksForTool(tool, hookConfig);
@@ -1140,10 +1067,8 @@ exit 127
1140
1067
  log(` ${dim(' Hook installation skipped: ' + e.message)}`);
1141
1068
  }
1142
1069
  log('');
1143
-
1144
1070
  // Step 8: Local dashboard API server
1145
1071
  step(8, 'Local dashboard API...');
1146
-
1147
1072
  const localServerPath = path.join(DELIMIT_HOME, 'server', 'ai', 'local_server.py');
1148
1073
  if (fs.existsSync(localServerPath)) {
1149
1074
  log(` ${green('✓')} Local API server available on port 7823`);
@@ -1153,10 +1078,8 @@ exit 127
1153
1078
  log(` ${dim(' Local API server not found — dashboard will use cloud sync')}`);
1154
1079
  }
1155
1080
  log('');
1156
-
1157
1081
  // Step 9: Post-install config validation (LED-098)
1158
1082
  step(9, 'Validating config integrity...');
1159
-
1160
1083
  let validationIssues = 0;
1161
1084
  const configFiles = [
1162
1085
  { path: MCP_CONFIG, name: 'Claude Code', format: 'json' },
@@ -1164,7 +1087,6 @@ exit 127
1164
1087
  { path: CURSOR_CONFIG, name: 'Cursor', format: 'json' },
1165
1088
  { path: GEMINI_CONFIG, name: 'Gemini CLI', format: 'json' },
1166
1089
  ];
1167
-
1168
1090
  for (const cfg of configFiles) {
1169
1091
  if (!fs.existsSync(cfg.path)) continue;
1170
1092
  try {
@@ -1178,7 +1100,6 @@ exit 127
1178
1100
  const cmd = delimitEntry.command || '';
1179
1101
  const args = delimitEntry.args || [];
1180
1102
  const serverArg = args[0] || '';
1181
-
1182
1103
  // Check command is python (not arbitrary binary)
1183
1104
  if (!cmd.includes('python') && !cmd.includes('venv')) {
1184
1105
  log(` ${yellow('⚠')} ${cfg.name}: delimit command is not python: ${cmd}`);
@@ -1212,7 +1133,6 @@ exit 127
1212
1133
  validationIssues++;
1213
1134
  }
1214
1135
  }
1215
-
1216
1136
  // Verify server file exists and is our code
1217
1137
  if (fs.existsSync(actualServer)) {
1218
1138
  const serverContent = fs.readFileSync(actualServer, 'utf-8').substring(0, 500);
@@ -1223,7 +1143,6 @@ exit 127
1223
1143
  validationIssues++;
1224
1144
  }
1225
1145
  }
1226
-
1227
1146
  // Check directory permissions
1228
1147
  try {
1229
1148
  const stat = fs.statSync(DELIMIT_HOME);
@@ -1235,24 +1154,20 @@ exit 127
1235
1154
  log(` ${green('✓')} Directory permissions OK`);
1236
1155
  }
1237
1156
  } catch {}
1238
-
1239
1157
  if (validationIssues === 0) {
1240
1158
  log(` ${green('✓')} All config validations passed`);
1241
1159
  } else {
1242
1160
  log(` ${yellow(`⚠ ${validationIssues} issue(s) found — review above`)}`);
1243
1161
  }
1244
1162
  log('');
1245
-
1246
1163
  // Step 10: Auto-detect OpenAPI specs
1247
1164
  step(10, 'Scanning for API specs...');
1248
-
1249
1165
  let detectedSpecs = [];
1250
1166
  try {
1251
1167
  const { minimatch } = (() => { try { return require('minimatch'); } catch { return { minimatch: null }; } })();
1252
1168
  // Simple recursive glob for spec files
1253
1169
  detectedSpecs = findSpecFiles(process.cwd());
1254
1170
  } catch {}
1255
-
1256
1171
  if (detectedSpecs.length > 0) {
1257
1172
  log(` ${green('✓')} Found ${detectedSpecs.length} API spec(s):`);
1258
1173
  detectedSpecs.forEach(s => log(` ${s}`));
@@ -1262,10 +1177,8 @@ exit 127
1262
1177
  log(` ${dim(' No OpenAPI/Swagger specs found in current directory')}`);
1263
1178
  }
1264
1179
  log('');
1265
-
1266
1180
  // Step 11: Social target scanning config
1267
1181
  step(11, 'Configuring social target scanner...');
1268
-
1269
1182
  const socialConfigPath = path.join(DELIMIT_HOME, 'social_target_config.json');
1270
1183
  const socialDefaultConfig = {
1271
1184
  platforms: {
@@ -1281,7 +1194,6 @@ exit 127
1281
1194
  scan_limit: 10,
1282
1195
  min_engagement: { score: 1, comments: 2 },
1283
1196
  };
1284
-
1285
1197
  if (!fs.existsSync(socialConfigPath)) {
1286
1198
  fs.mkdirSync(path.dirname(socialConfigPath), { recursive: true });
1287
1199
  fs.writeFileSync(socialConfigPath, JSON.stringify(socialDefaultConfig, null, 2) + '\n');
@@ -1289,7 +1201,6 @@ exit 127
1289
1201
  } else {
1290
1202
  log(` ${dim(' Config already exists:')} ${socialConfigPath}`);
1291
1203
  }
1292
-
1293
1204
  // Auto-detect available platforms
1294
1205
  const detectedPlatforms = {};
1295
1206
  // HN and Dev.to are always available (public APIs)
@@ -1310,13 +1221,11 @@ exit 127
1310
1221
  } else {
1311
1222
  detectedPlatforms['x'] = 'unavailable (no API key)';
1312
1223
  }
1313
-
1314
1224
  for (const [plat, status] of Object.entries(detectedPlatforms)) {
1315
1225
  const icon = status.startsWith('available') ? green('\u2713') : yellow('\u2717');
1316
1226
  log(` ${icon} ${plat}: ${dim(status)}`);
1317
1227
  }
1318
1228
  log('');
1319
-
1320
1229
  // Step 12: Done
1321
1230
  step(12, 'Done!');
1322
1231
  log('');
@@ -1327,14 +1236,11 @@ exit 127
1327
1236
  for (const t of tools) {
1328
1237
  log(` ${green('✓')} ${t}`);
1329
1238
  }
1330
-
1331
1239
  log('');
1332
-
1333
1240
  // Project scan — show what Delimit already knows (STR-046)
1334
1241
  log(` ${bold('Your project:')}`);
1335
1242
  const cwd = process.cwd();
1336
1243
  let projectFindings = 0;
1337
-
1338
1244
  // Detect framework
1339
1245
  const frameworks = [
1340
1246
  { file: 'package.json', check: 'express', label: 'Express API' },
@@ -1356,7 +1262,6 @@ exit 127
1356
1262
  }
1357
1263
  } catch {}
1358
1264
  }
1359
-
1360
1265
  // Detect OpenAPI specs
1361
1266
  const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.json', 'api.yaml'];
1362
1267
  let specFound = false;
@@ -1371,7 +1276,6 @@ exit 127
1371
1276
  if (!specFound) {
1372
1277
  await logp(` ${dim(' No API spec found — run')} ${blue('delimit-cli lint')} ${dim('to detect one')}`);
1373
1278
  }
1374
-
1375
1279
  // Count tests
1376
1280
  const testDirs = ['tests', 'test', '__tests__', 'spec'];
1377
1281
  for (const td of testDirs) {
@@ -1387,7 +1291,6 @@ exit 127
1387
1291
  } catch {}
1388
1292
  }
1389
1293
  }
1390
-
1391
1294
  // Check git status
1392
1295
  try {
1393
1296
  const gitStatus = execSync('git log --oneline -1 2>/dev/null', { encoding: 'utf-8', timeout: 3000 }).trim();
@@ -1396,7 +1299,6 @@ exit 127
1396
1299
  projectFindings++;
1397
1300
  }
1398
1301
  } catch {}
1399
-
1400
1302
  // Check for existing .delimit context
1401
1303
  const ledgerDir = path.join(os.homedir(), '.delimit', 'ledger');
1402
1304
  if (fs.existsSync(ledgerDir)) {
@@ -1415,7 +1317,6 @@ exit 127
1415
1317
  }
1416
1318
  } catch {}
1417
1319
  }
1418
-
1419
1320
  // Check for sessions
1420
1321
  const sessionsDir = path.join(os.homedir(), '.delimit', 'sessions');
1421
1322
  if (fs.existsSync(sessionsDir)) {
@@ -1427,13 +1328,10 @@ exit 127
1427
1328
  }
1428
1329
  } catch {}
1429
1330
  }
1430
-
1431
1331
  if (projectFindings === 0) {
1432
1332
  log(` ${dim(' New project — run')} ${blue('delimit-cli demo')} ${dim('to see governance in action')}`);
1433
1333
  }
1434
-
1435
1334
  log('');
1436
-
1437
1335
  // Suggested next action based on findings
1438
1336
  log(` ${bold("What's next:")}`);
1439
1337
  if (specFound) {
@@ -1452,7 +1350,6 @@ exit 127
1452
1350
  log('');
1453
1351
  log(` ${bold('Keep Building.')}`);
1454
1352
  log('');
1455
-
1456
1353
  // Show governance banner preview
1457
1354
  const shimFile = path.join(DELIMIT_HOME, 'shims', 'claude');
1458
1355
  if (fs.existsSync(shimFile)) {
@@ -1491,14 +1388,11 @@ exit 127
1491
1388
  log('');
1492
1389
  }
1493
1390
  }
1494
-
1495
1391
  // LED-213: Import canonical template from shared module
1496
1392
  const { getDelimitSection } = require('../lib/delimit-template');
1497
-
1498
1393
  function getClaudeMdContent() {
1499
1394
  return getDelimitSection() + '\n';
1500
1395
  }
1501
-
1502
1396
  /**
1503
1397
  * Upsert the Delimit section in a file using <!-- delimit:start --> / <!-- delimit:end --> markers.
1504
1398
  *
@@ -1517,18 +1411,15 @@ function upsertDelimitSection(filePath) {
1517
1411
  const newSection = getDelimitSection();
1518
1412
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
1519
1413
  const version = pkg.version || '0.0.0';
1520
-
1521
1414
  if (!fs.existsSync(filePath)) {
1522
1415
  fs.writeFileSync(filePath, newSection + '\n');
1523
1416
  return { action: 'created' };
1524
1417
  }
1525
-
1526
1418
  const rawExisting = fs.readFileSync(filePath, 'utf-8');
1527
1419
  // Strip a UTF-8 BOM if present so the start-of-line anchor still matches
1528
1420
  // the very first line of the file. We write back the stripped form to keep
1529
1421
  // serialization deterministic.
1530
1422
  const existing = rawExisting.replace(/^\uFEFF/, '');
1531
-
1532
1423
  // Check if managed markers already exist.
1533
1424
  // Markers MUST be on their own line — anchored with the multiline flag — so
1534
1425
  // that documentation prose that quotes the markers (e.g. inside backticks,
@@ -1542,7 +1433,6 @@ function upsertDelimitSection(filePath) {
1542
1433
  const endMatch = existing.match(endMarkerRe);
1543
1434
  const hasStart = !!startMatch;
1544
1435
  const hasEnd = !!endMatch;
1545
-
1546
1436
  if (hasStart && hasEnd) {
1547
1437
  // Extract current version from the marker (also anchored, allows indent)
1548
1438
  const versionMatch = existing.match(/^[ \t]*<!-- delimit:start v([^ ]+) -->[ \t]*$/m);
@@ -1558,7 +1448,6 @@ function upsertDelimitSection(filePath) {
1558
1448
  fs.writeFileSync(filePath, before + newSection + after);
1559
1449
  return { action: 'updated' };
1560
1450
  }
1561
-
1562
1451
  // No markers present — append the managed section at the bottom.
1563
1452
  // User content at the top is preserved verbatim. Markers get added so future
1564
1453
  // upgrades can update just the managed region.
@@ -1566,7 +1455,6 @@ function upsertDelimitSection(filePath) {
1566
1455
  fs.writeFileSync(filePath, existing + separator + newSection + '\n');
1567
1456
  return { action: 'appended' };
1568
1457
  }
1569
-
1570
1458
  function copyDir(src, dest) {
1571
1459
  fs.mkdirSync(dest, { recursive: true });
1572
1460
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
@@ -1580,7 +1468,6 @@ function copyDir(src, dest) {
1580
1468
  }
1581
1469
  }
1582
1470
  }
1583
-
1584
1471
  main().catch(err => {
1585
1472
  console.error('Setup failed:', err.message);
1586
1473
  process.exit(1);
package/gateway/ai/tui.py CHANGED
@@ -538,6 +538,11 @@ def _channel_color(channel: str) -> str:
538
538
  class ApprovalsPanel(Static):
539
539
  """Pending approvals view -- shows items from drafts.db."""
540
540
 
541
+ BINDINGS = [
542
+ Binding("y", "approve", "Approve", key_display="Y"),
543
+ Binding("n", "reject", "Reject", key_display="N"),
544
+ ]
545
+
541
546
  def compose(self) -> ComposeResult:
542
547
  yield DataTable(id="approvals-table")
543
548
 
@@ -551,7 +556,8 @@ class ApprovalsPanel(Static):
551
556
  def _refresh_data(self) -> None:
552
557
  table = self.query_one("#approvals-table", DataTable)
553
558
  table.clear()
554
- for item in _load_pending_approvals(25):
559
+ self.items = _load_pending_approvals(25)
560
+ for item in self.items:
555
561
  table.add_row(
556
562
  item.get("draft_id", "")[:12],
557
563
  item.get("draft_kind", ""),
@@ -560,6 +566,58 @@ class ApprovalsPanel(Static):
560
566
  item.get("age_str", ""),
561
567
  )
562
568
 
569
+ def action_approve(self) -> None:
570
+ self._handle_action("approve")
571
+
572
+ def action_reject(self) -> None:
573
+ self._handle_action("reject")
574
+
575
+ def _handle_action(self, action: str) -> None:
576
+ table = self.query_one("#approvals-table", DataTable)
577
+ cursor_row = table.cursor_row
578
+ if cursor_row is None or not hasattr(self, "items") or cursor_row >= len(self.items):
579
+ self.app.notify("No draft selected.", severity="warning")
580
+ return
581
+
582
+ item = self.items[cursor_row]
583
+ draft_id = item.get("draft_id")
584
+ current_status = item.get("status")
585
+
586
+ if not draft_id or not current_status:
587
+ self.app.notify("Invalid draft data selected.", severity="error")
588
+ return
589
+
590
+ from .inbox_drafts import transition
591
+ db_path = DELIMIT_HOME / "drafts.db"
592
+ new_status = "approved" if action == "approve" else "cancelled"
593
+
594
+ try:
595
+ success = transition(
596
+ draft_id,
597
+ expected=current_status,
598
+ new=new_status,
599
+ db_path=db_path
600
+ )
601
+ if success:
602
+ self.app.notify(
603
+ f"Draft {draft_id[:12]} {action}d successfully!",
604
+ title="Action Succeeded",
605
+ severity="information"
606
+ )
607
+ self._refresh_data()
608
+ else:
609
+ self.app.notify(
610
+ f"Failed to {action} draft {draft_id[:12]}: state mismatch.",
611
+ title="Action Failed",
612
+ severity="warning"
613
+ )
614
+ except Exception as e:
615
+ self.app.notify(
616
+ f"Error performing {action}: {e}",
617
+ title="System Error",
618
+ severity="error"
619
+ )
620
+
563
621
 
564
622
  class FilesystemPanel(Static):
565
623
  """Filesystem browser -- navigate .delimit/ directory tree."""
@@ -884,6 +942,10 @@ class DelimitOS(App):
884
942
 
885
943
  def action_focus_approvals(self) -> None:
886
944
  self.query_one(TabbedContent).active = "tab-approvals"
945
+ try:
946
+ self.query_one("#approvals-table", DataTable).focus()
947
+ except Exception:
948
+ pass
887
949
 
888
950
  def action_focus_ledger(self) -> None:
889
951
  self.query_one(TabbedContent).active = "tab-ledger"
package/lib/chat-repl.js CHANGED
@@ -140,7 +140,10 @@ class DelimitChatREPL {
140
140
  }
141
141
 
142
142
  const args = [];
143
- if (activeModel.id.startsWith('gemini')) {
143
+ if (activeModel.id === 'antigravity' || activeModel.id === 'agy') {
144
+ args.push('--dangerously-skip-permissions');
145
+ }
146
+ if (activeModel.id.startsWith('gemini')) {
144
147
  const p = this.modelsConfig[activeModel.id];
145
148
  if (p && p.model) {
146
149
  let modelName = p.model;
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": "4.7.4",
4
+ "version": "4.7.5",
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": [