delimit-cli 4.1.48 → 4.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.1.49] - 2026-04-09
4
+
5
+ ### Fixed (full preservation audit follow-up to 4.1.48)
6
+ - **Project `.claude/settings.json` hooks clobber** — `installClaudeHooks` was replacing the project-level `.claude/settings.json` hooks object with the merged-with-global config, propagating global hooks into every project file and wiping any project-local hooks the user had set. Now merges only Delimit-owned hook groups (entries whose command contains `delimit`) into existing project hooks; project-specific user hooks survive.
7
+ - **Gemini `general.defaultApprovalMode` clobber** — `delimit-cli setup` was force-setting Gemini's `defaultApprovalMode` to `auto_edit` on every run, overwriting whatever the user had chosen (e.g. `manual`). Now only sets it when missing.
8
+ - **`~/.claude.json` MCP hooks replacement** — `lib/hooks-installer.js` (opt-in via `delimit-cli hooks install`) replaced `preCommand` / `postCommand` / `authentication` / `audit` keys on every install. Now only fills in missing keys, preserving any user-chosen MCP hook commands.
9
+
10
+ ### Added
11
+ - **`tests/setup-no-clobber.test.js`** — dedicated regression suite that runs setup helpers against synthetic fresh-user HOME directories with pre-populated user customizations (project hooks, Gemini approval mode, custom MCP hook commands) and asserts none get clobbered. 5 tests, all passing.
12
+
13
+ ### Audit results
14
+ - Audited every `fs.writeFileSync` in `bin/delimit-setup.js`, `lib/cross-model-hooks.js`, `lib/hooks-installer.js`, `adapters/cursor-rules.js`, and `scripts/postinstall.js`.
15
+ - All remaining writes are either delimit-owned (shims, hook scripts, generated `delimit.md`), guarded by `!fs.existsSync` (models.json, social_target_config.json, codex empty file), or surgical merges that preserve user content (`.mcp.json` mcpServers, `.claude/settings.json` allowList, `.codex/config.toml` mcp_servers.delimit block, `.cursor/mcp.json` mcpServers, rc-file PATH append).
16
+ - The full preservation contract is now: `delimit-cli setup` may safely run on any user machine, including via the shim auto-update flow, without destroying user state. New installs and upgrades are equivalent for everything except delimit-owned files.
17
+
18
+ ### Tests
19
+ - 129/129 passing (was 124).
20
+
3
21
  ## [4.1.48] - 2026-04-09
4
22
 
5
23
  ### Fixed
@@ -430,9 +430,12 @@ async function main() {
430
430
  cwd: path.join(DELIMIT_HOME, 'server'),
431
431
  env: { PYTHONPATH: path.join(DELIMIT_HOME, 'server') }
432
432
  };
433
- // Auto-approve all tools — users should not be prompted for every Delimit call
433
+ // Auto-approve all tools — users should not be prompted for every Delimit call.
434
+ // Only set if missing — never clobber the user's chosen approval mode on upgrade.
434
435
  if (!geminiConfig.general) geminiConfig.general = {};
435
- geminiConfig.general.defaultApprovalMode = 'auto_edit';
436
+ if (!geminiConfig.general.defaultApprovalMode) {
437
+ geminiConfig.general.defaultApprovalMode = 'auto_edit';
438
+ }
436
439
  fs.writeFileSync(GEMINI_CONFIG, JSON.stringify(geminiConfig, null, 2));
437
440
  if (geminiExisted) {
438
441
  await logp(` ${green('✓')} Updated Delimit paths in Gemini CLI config`);
@@ -577,14 +577,38 @@ echo ""
577
577
  const configJson = JSON.stringify(config, null, 2);
578
578
  for (const target of writeTargets) {
579
579
  try {
580
- // For project settings, merge hooks into existing config
581
- if (target !== configPath && fs.existsSync(target)) {
582
- const existing = JSON.parse(fs.readFileSync(target, 'utf-8'));
583
- existing.hooks = config.hooks;
584
- fs.writeFileSync(target, JSON.stringify(existing, null, 2));
585
- } else {
580
+ if (target === configPath) {
581
+ // Global ~/.claude/settings.json: write the merged config we built
586
582
  fs.writeFileSync(target, configJson);
583
+ continue;
584
+ }
585
+
586
+ // Project settings (.claude/settings.json in cwd): merge ONLY the
587
+ // Delimit-added hook entries into existing project hooks. Never
588
+ // overwrite the project's own hook entries with global ones.
589
+ // Previous behavior (`existing.hooks = config.hooks`) propagated
590
+ // every global hook into project files, wiping project-local hooks
591
+ // and leaking unrelated user customizations across repos.
592
+ let existing = {};
593
+ if (fs.existsSync(target)) {
594
+ try { existing = JSON.parse(fs.readFileSync(target, 'utf-8')); } catch { existing = {}; }
595
+ }
596
+ if (!existing.hooks) existing.hooks = {};
597
+
598
+ for (const [event, groups] of Object.entries(config.hooks || {})) {
599
+ if (!Array.isArray(groups)) continue;
600
+ if (!existing.hooks[event]) existing.hooks[event] = [];
601
+ for (const group of groups) {
602
+ const cmds = (group.hooks || []).map(h => h.command || '');
603
+ // Only propagate Delimit-owned hook groups to project files
604
+ if (!cmds.some(c => c.includes('delimit'))) continue;
605
+ const alreadyHas = existing.hooks[event].some(eg =>
606
+ (eg.hooks || []).some(h => cmds.includes(h.command))
607
+ );
608
+ if (!alreadyHas) existing.hooks[event].push(group);
609
+ }
587
610
  }
611
+ fs.writeFileSync(target, JSON.stringify(existing, null, 2));
588
612
  } catch {}
589
613
  }
590
614
  return changes;
@@ -185,29 +185,37 @@ class DelimitHooksInstaller {
185
185
 
186
186
  async configureClaudeCode() {
187
187
  const claudeConfigPath = path.join(process.env.HOME, '.claude.json');
188
-
188
+
189
189
  if (fs.existsSync(claudeConfigPath)) {
190
190
  try {
191
191
  const config = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));
192
-
193
- // Add Delimit governance hooks
192
+
193
+ // Preserve any existing hooks the user has set. Only fill in
194
+ // Delimit MCP hooks if those specific keys are missing —
195
+ // never overwrite a user-chosen preCommand/postCommand.
194
196
  if (!config.hooks) {
195
197
  config.hooks = {};
196
198
  }
197
-
198
- config.hooks.preCommand = path.join(this.mcpHooksDir, 'pre-mcp-call');
199
- config.hooks.postCommand = path.join(this.mcpHooksDir, 'post-mcp-call');
200
- config.hooks.authentication = path.join(this.mcpHooksDir, 'mcp-auth');
201
- config.hooks.audit = path.join(this.mcpHooksDir, 'mcp-audit');
202
-
203
- // Add Delimit governance settings
199
+ const delimitHooks = {
200
+ preCommand: path.join(this.mcpHooksDir, 'pre-mcp-call'),
201
+ postCommand: path.join(this.mcpHooksDir, 'post-mcp-call'),
202
+ authentication: path.join(this.mcpHooksDir, 'mcp-auth'),
203
+ audit: path.join(this.mcpHooksDir, 'mcp-audit'),
204
+ };
205
+ for (const [key, value] of Object.entries(delimitHooks)) {
206
+ if (!config.hooks[key]) {
207
+ config.hooks[key] = value;
208
+ }
209
+ }
210
+
211
+ // Add Delimit governance settings (own namespace, safe to set)
204
212
  config.delimitGovernance = {
205
213
  enabled: true,
206
214
  agent: 'http://127.0.0.1:7823',
207
215
  mode: 'auto',
208
216
  hooks: this.mcpHooks.map(h => path.join(this.mcpHooksDir, h))
209
217
  };
210
-
218
+
211
219
  fs.writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
212
220
  console.log(chalk.green(' ✓ Claude Code configuration updated'));
213
221
  } catch (e) {
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.1.48",
4
+ "version": "4.1.49",
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": [
@@ -35,7 +35,7 @@
35
35
  "postinstall": "node scripts/postinstall.js",
36
36
  "sync-gateway": "bash scripts/sync-gateway.sh",
37
37
  "prepublishOnly": "bash scripts/publish-ci-guard.sh && npm run sync-gateway && bash scripts/security-check.sh",
38
- "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 tests/golden-path.test.js tests/v420-features.test.js"
38
+ "test": "node --test tests/setup-onboarding.test.js tests/setup-matrix.test.js tests/setup-no-clobber.test.js tests/config-export-import.test.js tests/cross-model-hooks.test.js tests/golden-path.test.js tests/v420-features.test.js"
39
39
  },
40
40
  "keywords": [
41
41
  "openapi",