coder-config 0.46.16 → 0.47.2-beta

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/config-loader.js CHANGED
@@ -28,7 +28,7 @@ const { init, show } = require('./lib/init');
28
28
  const { memoryList, memoryInit, memoryAdd, memorySearch } = require('./lib/memory');
29
29
  const { envList, envSet, envUnset } = require('./lib/env');
30
30
  const { getProjectsRegistryPath, loadProjectsRegistry, saveProjectsRegistry, projectList, projectAdd, projectRemove } = require('./lib/projects');
31
- const { getWorkstreamsPath, loadWorkstreams, saveWorkstreams, workstreamList, workstreamCreate, workstreamUpdate, workstreamDelete, workstreamUse, workstreamActive, workstreamAddProject, workstreamRemoveProject, workstreamInject, workstreamDetect, workstreamGet, getActiveWorkstream, countWorkstreamsForProject, workstreamInstallHook, workstreamInstallHookGemini, workstreamInstallHookCodex, workstreamDeactivate, workstreamCheckPath, getSettingsPath, loadSettings, saveSettings, workstreamAddTrigger, workstreamRemoveTrigger, workstreamSetAutoActivate, setGlobalAutoActivate, shouldAutoActivate, workstreamCheckFolder, workstreamInstallCdHook, workstreamUninstallCdHook, workstreamCdHookStatus, discoverSubProjects, generateRulesFromRepos, generateRulesWithClaude, generateRulesWithAI, getAvailableAITools, findAIBinary, AI_TOOLS } = require('./lib/workstreams');
31
+ const { getWorkstreamsPath, loadWorkstreams, saveWorkstreams, workstreamList, workstreamCreate, workstreamUpdate, workstreamDelete, workstreamUse, workstreamActive, workstreamAddProject, workstreamRemoveProject, workstreamInject, workstreamDetect, workstreamGet, getActiveWorkstream, countWorkstreamsForProject, workstreamInstallHook, workstreamInstallHookGemini, workstreamInstallHookCodex, workstreamDeactivate, workstreamCheckPath, getSettingsPath, loadSettings, saveSettings, workstreamAddTrigger, workstreamRemoveTrigger, workstreamSetAutoActivate, setGlobalAutoActivate, shouldAutoActivate, workstreamCheckFolder, workstreamInstallCdHook, workstreamUninstallCdHook, workstreamCdHookStatus, discoverSubProjects, generateRulesFromRepos, generateRulesWithClaude, generateRulesWithAI, getAvailableAITools, findAIBinary, AI_TOOLS, workstreamSetSandbox } = require('./lib/workstreams');
32
32
  const { getActivityPath, getDefaultActivity, loadActivity, saveActivity, detectProjectRoot, activityLog, activitySummary, generateWorkstreamName, activitySuggestWorkstreams, activityClear } = require('./lib/activity');
33
33
  const { getLoopsPath, loadLoops, saveLoops, loadLoopState, saveLoopState, loadHistory, saveHistory, loopList, loopCreate, loopGet, loopUpdate, loopDelete, loopStart, loopPause, loopResume, loopCancel, loopApprove, loopComplete, loopFail, loopStatus, loopHistory, loopConfig, getActiveLoop, recordIteration, saveClarifications, savePlan, loadClarifications, loadPlan, loopInject, archiveLoop } = require('./lib/loops');
34
34
  const { getSessionStatus, showSessionStatus, flushContext, clearContext, installHooks: sessionInstallHooks, getFlushedContext, installFlushCommand, installAll: sessionInstallAll, SESSION_DIR, FLUSHED_CONTEXT_FILE } = require('./lib/sessions');
@@ -132,12 +132,12 @@ class ClaudeConfigManager {
132
132
  }
133
133
 
134
134
  // Apply
135
- apply(projectDir) { return apply(this.registryPath, projectDir); }
135
+ apply(projectDir) { return apply(this.registryPath, projectDir, this.installDir); }
136
136
  applyForAntigravity(projectDir) { return applyForAntigravity(this.registryPath, projectDir); }
137
137
  applyForGemini(projectDir) { return applyForGemini(this.registryPath, projectDir); }
138
138
  detectInstalledTools() { return detectInstalledTools(); }
139
139
  getToolPaths() { return TOOL_PATHS; }
140
- applyForTools(projectDir, tools) { return applyForTools(this.registryPath, projectDir, tools); }
140
+ applyForTools(projectDir, tools) { return applyForTools(this.registryPath, projectDir, tools, this.installDir); }
141
141
 
142
142
  // MCPs (project)
143
143
  list() { return list(this.registryPath); }
@@ -197,7 +197,7 @@ class ClaudeConfigManager {
197
197
  workstreamInstallHook() { return workstreamInstallHook(); }
198
198
  workstreamInstallHookGemini() { return workstreamInstallHookGemini(); }
199
199
  workstreamInstallHookCodex() { return workstreamInstallHookCodex(); }
200
- workstreamDeactivate() { return workstreamDeactivate(); }
200
+ workstreamDeactivate() { return workstreamDeactivate(this.installDir); }
201
201
  workstreamCheckPath(targetPath, silent) { return workstreamCheckPath(this.installDir, targetPath, silent); }
202
202
  // Workstream folder auto-activation
203
203
  getSettingsPath() { return getSettingsPath(this.installDir); }
@@ -206,6 +206,7 @@ class ClaudeConfigManager {
206
206
  workstreamAddTrigger(idOrName, folderPath) { return workstreamAddTrigger(this.installDir, idOrName, folderPath); }
207
207
  workstreamRemoveTrigger(idOrName, folderPath) { return workstreamRemoveTrigger(this.installDir, idOrName, folderPath); }
208
208
  workstreamSetAutoActivate(idOrName, value) { return workstreamSetAutoActivate(this.installDir, idOrName, value); }
209
+ workstreamSetSandbox(idOrName, value) { return workstreamSetSandbox(this.installDir, idOrName, value); }
209
210
  setGlobalAutoActivate(value) { return setGlobalAutoActivate(this.installDir, value); }
210
211
  shouldAutoActivate(workstream) { return shouldAutoActivate(this.installDir, workstream); }
211
212
  workstreamCheckFolder(folderPath, jsonOutput) { return workstreamCheckFolder(this.installDir, folderPath, jsonOutput); }
package/lib/apply.js CHANGED
@@ -8,11 +8,12 @@ const { execSync } = require('child_process');
8
8
  const { TOOL_PATHS } = require('./constants');
9
9
  const { loadJson, saveJson, loadEnvFile, interpolate, resolveEnvVars } = require('./utils');
10
10
  const { findProjectRoot, findAllConfigs, mergeConfigs, findAllConfigsForTool } = require('./config');
11
+ const { getActiveWorkstream, applySandboxIfEnabled } = require('./workstreams');
11
12
 
12
13
  /**
13
14
  * Generate .mcp.json for a project (with hierarchical config merging)
14
15
  */
15
- function apply(registryPath, projectDir = null) {
16
+ function apply(registryPath, projectDir = null, installDir = null) {
16
17
  const dir = projectDir || findProjectRoot() || process.cwd();
17
18
 
18
19
  const registry = loadJson(registryPath);
@@ -100,6 +101,19 @@ function apply(registryPath, projectDir = null) {
100
101
  console.log(`✓ Generated ${outputPath}`);
101
102
  console.log(` └─ ${count} MCP(s): ${Object.keys(output.mcpServers).join(', ')}`);
102
103
 
104
+ // Generate settings.local.json with additionalDirectories for workstream sandbox scope
105
+ if (installDir) {
106
+ const active = getActiveWorkstream(installDir);
107
+ if (applySandboxIfEnabled(active, dir)) {
108
+ const resolvedDir = path.resolve(dir);
109
+ const count = active.projects.filter(p =>
110
+ path.resolve(p) !== resolvedDir && !resolvedDir.startsWith(path.resolve(p) + path.sep)
111
+ ).length;
112
+ console.log(`✓ Generated .claude/settings.local.json (sandbox scope)`);
113
+ console.log(` └─ ${count} additional director${count === 1 ? 'y' : 'ies'}`);
114
+ }
115
+ }
116
+
103
117
  // Generate settings.json with enabledPlugins if any are configured
104
118
  if (mergedConfig.enabledPlugins && Object.keys(mergedConfig.enabledPlugins).length > 0) {
105
119
  const settingsPath = path.join(dir, '.claude', 'settings.json');
@@ -424,12 +438,12 @@ function detectInstalledTools() {
424
438
  /**
425
439
  * Apply config for multiple tools based on preferences
426
440
  */
427
- function applyForTools(registryPath, projectDir = null, tools = ['claude']) {
441
+ function applyForTools(registryPath, projectDir = null, tools = ['claude'], installDir = null) {
428
442
  const results = {};
429
443
 
430
444
  for (const tool of tools) {
431
445
  if (tool === 'claude') {
432
- results.claude = apply(registryPath, projectDir);
446
+ results.claude = apply(registryPath, projectDir, installDir);
433
447
  } else if (tool === 'gemini') {
434
448
  results.gemini = applyForGemini(registryPath, projectDir);
435
449
  } else if (tool === 'antigravity') {
package/lib/cli.js CHANGED
@@ -194,6 +194,13 @@ function runCli(manager) {
194
194
  process.exit(1);
195
195
  }
196
196
  manager.workstreamRemoveTrigger(args[2], args[3]);
197
+ } else if (args[1] === 'sandbox') {
198
+ if (!args[2]) {
199
+ console.error('Usage: coder-config workstream sandbox <workstream> [on|off]');
200
+ process.exit(1);
201
+ }
202
+ const value = args[3] || 'on';
203
+ manager.workstreamSetSandbox(args[2], value);
197
204
  } else if (args[1] === 'auto-activate') {
198
205
  if (!args[2]) {
199
206
  console.error('Usage: coder-config workstream auto-activate <workstream> [on|off|default]');
@@ -385,7 +392,7 @@ ${chalk.dim('Configuration manager for AI coding tools (Claude Code, Gemini CLI,
385
392
  // Project Commands
386
393
  console.log(box('Project', [
387
394
  cmd('init', 'Initialize project with .claude/mcps.json'),
388
- cmd('apply', 'Generate .mcp.json from config'),
395
+ cmd('apply', 'Generate .mcp.json and sandbox scope'),
389
396
  cmd('show', 'Show current project config'),
390
397
  cmd('list', 'List available MCPs (✓ = active)'),
391
398
  cmd('add <mcp> [mcp...]', 'Add MCP(s) to project'),
@@ -434,6 +441,7 @@ ${chalk.dim('Configuration manager for AI coding tools (Claude Code, Gemini CLI,
434
441
  cmd('workstream use <name>', 'Set active workstream'),
435
442
  cmd('workstream add <ws> <path>', 'Add project to workstream'),
436
443
  cmd('workstream remove <ws> <path>', 'Remove from workstream'),
444
+ cmd('workstream sandbox <ws> [on|off]', 'Toggle sandbox enforcement'),
437
445
  cmd('workstream install-hook', 'Install pre-prompt hook'),
438
446
  ]));
439
447
  console.log();
package/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.46.16';
5
+ const VERSION = '0.47.2-beta';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
@@ -86,7 +86,11 @@ function workstreamList(installDir) {
86
86
  const detected = workstreamDetect(installDir, process.cwd());
87
87
  for (const ws of data.workstreams) {
88
88
  const active = detected && ws.id === detected.id ? '● ' : '○ ';
89
- console.log(`${active}${ws.name}`);
89
+ const badges = [];
90
+ if (ws.sandbox === true) badges.push('[sandbox]');
91
+ if (ws.autoActivate === true) badges.push('[auto]');
92
+ const badgeStr = badges.length > 0 ? ' ' + badges.join(' ') : '';
93
+ console.log(`${active}${ws.name}${badgeStr}`);
90
94
  if (ws.projects && ws.projects.length > 0) {
91
95
  console.log(` Projects: ${ws.projects.map(p => path.basename(p)).join(', ')}`);
92
96
  }
@@ -391,6 +395,9 @@ function workstreamInject(installDir, silent = false) {
391
395
  // Always output the context (for hooks), silent only suppresses "no active" message
392
396
  console.log(output);
393
397
 
398
+ // Generate settings.local.json with additionalDirectories for sandbox scope
399
+ applySandboxIfEnabled(active, process.cwd());
400
+
394
401
  return output;
395
402
  }
396
403
 
@@ -633,10 +640,93 @@ fi
633
640
  return true;
634
641
  }
635
642
 
643
+ /**
644
+ * Apply sandbox settings if the workstream has sandbox enabled.
645
+ * Shared logic used by both workstreamInject() and apply().
646
+ * @param {object} active - Active workstream object
647
+ * @param {string} dir - Target project directory (absolute)
648
+ * @returns {boolean} Whether sandbox settings were written
649
+ */
650
+ function applySandboxIfEnabled(active, dir) {
651
+ if (!active || active.sandbox !== true || !active.projects || active.projects.length === 0) {
652
+ return false;
653
+ }
654
+
655
+ const resolvedDir = path.resolve(dir);
656
+ const otherProjects = active.projects.filter(p => {
657
+ const resolved = path.resolve(p);
658
+ return resolved !== resolvedDir && !resolvedDir.startsWith(resolved + path.sep);
659
+ });
660
+
661
+ if (otherProjects.length > 0) {
662
+ writeWorkstreamSandboxSettings(resolvedDir, otherProjects);
663
+ return true;
664
+ }
665
+ return false;
666
+ }
667
+
668
+ /**
669
+ * Write additionalDirectories to .claude/settings.local.json for sandbox scope
670
+ */
671
+ function writeWorkstreamSandboxSettings(dir, additionalDirectories) {
672
+ const claudeDir = path.join(dir, '.claude');
673
+ const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
674
+
675
+ let existing = {};
676
+ if (fs.existsSync(settingsLocalPath)) {
677
+ try { existing = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); } catch {}
678
+ }
679
+
680
+ const permissions = existing.permissions || {};
681
+ permissions.additionalDirectories = additionalDirectories;
682
+
683
+ if (!fs.existsSync(claudeDir)) {
684
+ fs.mkdirSync(claudeDir, { recursive: true });
685
+ }
686
+ fs.writeFileSync(settingsLocalPath, JSON.stringify({ ...existing, permissions }, null, 2) + '\n');
687
+ }
688
+
689
+ /**
690
+ * Remove additionalDirectories from .claude/settings.local.json
691
+ */
692
+ function removeWorkstreamSandboxSettings(dir) {
693
+ const settingsLocalPath = path.join(dir, '.claude', 'settings.local.json');
694
+ if (!fs.existsSync(settingsLocalPath)) return;
695
+
696
+ let existing = {};
697
+ try { existing = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); } catch { return; }
698
+
699
+ if (existing.permissions && existing.permissions.additionalDirectories) {
700
+ delete existing.permissions.additionalDirectories;
701
+ // Clean up empty permissions object
702
+ if (Object.keys(existing.permissions).length === 0) {
703
+ delete existing.permissions;
704
+ }
705
+ // If nothing left, remove the file
706
+ if (Object.keys(existing).length === 0) {
707
+ fs.unlinkSync(settingsLocalPath);
708
+ } else {
709
+ fs.writeFileSync(settingsLocalPath, JSON.stringify(existing, null, 2) + '\n');
710
+ }
711
+ }
712
+ }
713
+
636
714
  /**
637
715
  * Deactivate workstream (output shell command to unset env var)
638
716
  */
639
- function workstreamDeactivate() {
717
+ function workstreamDeactivate(installDir) {
718
+ // Clean up sandbox settings from all workstream project directories
719
+ if (installDir) {
720
+ const active = getActiveWorkstream(installDir);
721
+ if (active && active.projects) {
722
+ for (const p of active.projects) {
723
+ removeWorkstreamSandboxSettings(path.resolve(p));
724
+ }
725
+ }
726
+ }
727
+ // Also clean CWD in case it's not a listed project
728
+ removeWorkstreamSandboxSettings(process.cwd());
729
+
640
730
  console.log('To deactivate the workstream for this session, run:');
641
731
  console.log(' unset CODER_WORKSTREAM');
642
732
  console.log('\nOr to clear the global active workstream:');
@@ -1308,6 +1398,38 @@ function workstreamRemoveTrigger(installDir, idOrName, folderPath) {
1308
1398
  return ws;
1309
1399
  }
1310
1400
 
1401
+ /**
1402
+ * Set sandbox mode for a workstream
1403
+ * When true: generates additionalDirectories in settings.local.json for OS-level enforcement
1404
+ * When false (default): only injects advisory LLM rules (softer, LLM can override if important)
1405
+ */
1406
+ function workstreamSetSandbox(installDir, idOrName, value) {
1407
+ const data = loadWorkstreams(installDir);
1408
+ const ws = data.workstreams.find(
1409
+ w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
1410
+ );
1411
+
1412
+ if (!ws) {
1413
+ console.error(`Workstream not found: ${idOrName}`);
1414
+ return null;
1415
+ }
1416
+
1417
+ if (value === 'on' || value === true || value === 'true') {
1418
+ ws.sandbox = true;
1419
+ console.log(`✓ Sandbox enabled for ${ws.name} (OS-level directory enforcement)`);
1420
+ } else if (value === 'off' || value === false || value === 'false') {
1421
+ ws.sandbox = false;
1422
+ console.log(`✓ Sandbox disabled for ${ws.name} (advisory LLM rules only)`);
1423
+ } else {
1424
+ console.error(`Invalid value: ${value}. Use "on" or "off".`);
1425
+ return null;
1426
+ }
1427
+
1428
+ ws.updatedAt = new Date().toISOString();
1429
+ saveWorkstreams(installDir, data);
1430
+ return ws;
1431
+ }
1432
+
1311
1433
  /**
1312
1434
  * Set auto-activate for a workstream
1313
1435
  * value: true, false, or null (use global default)
@@ -1641,4 +1763,6 @@ module.exports = {
1641
1763
  workstreamInstallCdHook,
1642
1764
  workstreamUninstallCdHook,
1643
1765
  workstreamCdHookStatus,
1766
+ applySandboxIfEnabled,
1767
+ workstreamSetSandbox,
1644
1768
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.46.16",
3
+ "version": "0.47.2-beta",
4
4
  "description": "Configuration manager for AI coding tools - Claude Code, Gemini CLI, Codex CLI, Antigravity. Manage MCPs, rules, permissions, memory, and workstreams.",
5
5
  "author": "regression.io",
6
6
  "main": "config-loader.js",