coder-config 0.46.16 → 0.47.1-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,7 +132,7 @@ 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(); }
@@ -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, writeWorkstreamSandboxSettings } = 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,23 @@ 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 (active && active.projects && active.projects.length > 0 && active.sandbox === true) {
108
+ const otherProjects = active.projects.filter(p => {
109
+ const resolved = path.resolve(p);
110
+ return resolved !== path.resolve(dir) && !dir.startsWith(resolved + path.sep);
111
+ });
112
+
113
+ if (otherProjects.length > 0) {
114
+ writeWorkstreamSandboxSettings(dir, otherProjects);
115
+ console.log(`✓ Generated .claude/settings.local.json (sandbox scope)`);
116
+ console.log(` └─ ${otherProjects.length} additional director${otherProjects.length === 1 ? 'y' : 'ies'}`);
117
+ }
118
+ }
119
+ }
120
+
103
121
  // Generate settings.json with enabledPlugins if any are configured
104
122
  if (mergedConfig.enabledPlugins && Object.keys(mergedConfig.enabledPlugins).length > 0) {
105
123
  const settingsPath = path.join(dir, '.claude', 'settings.json');
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.1-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,20 @@ 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
+ // Only when sandbox mode is explicitly enabled (default: off)
400
+ if (active.sandbox === true && active.projects && active.projects.length > 0) {
401
+ const cwd = process.cwd();
402
+ const otherProjects = active.projects.filter(p => {
403
+ const resolved = path.resolve(p);
404
+ return resolved !== path.resolve(cwd) && !cwd.startsWith(resolved + path.sep);
405
+ });
406
+
407
+ if (otherProjects.length > 0) {
408
+ writeWorkstreamSandboxSettings(cwd, otherProjects);
409
+ }
410
+ }
411
+
394
412
  return output;
395
413
  }
396
414
 
@@ -633,10 +651,59 @@ fi
633
651
  return true;
634
652
  }
635
653
 
654
+ /**
655
+ * Write additionalDirectories to .claude/settings.local.json for sandbox scope
656
+ */
657
+ function writeWorkstreamSandboxSettings(dir, additionalDirectories) {
658
+ const claudeDir = path.join(dir, '.claude');
659
+ const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
660
+
661
+ let existing = {};
662
+ if (fs.existsSync(settingsLocalPath)) {
663
+ try { existing = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); } catch {}
664
+ }
665
+
666
+ const permissions = existing.permissions || {};
667
+ permissions.additionalDirectories = additionalDirectories;
668
+
669
+ if (!fs.existsSync(claudeDir)) {
670
+ fs.mkdirSync(claudeDir, { recursive: true });
671
+ }
672
+ fs.writeFileSync(settingsLocalPath, JSON.stringify({ ...existing, permissions }, null, 2) + '\n');
673
+ }
674
+
675
+ /**
676
+ * Remove additionalDirectories from .claude/settings.local.json
677
+ */
678
+ function removeWorkstreamSandboxSettings(dir) {
679
+ const settingsLocalPath = path.join(dir, '.claude', 'settings.local.json');
680
+ if (!fs.existsSync(settingsLocalPath)) return;
681
+
682
+ let existing = {};
683
+ try { existing = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8')); } catch { return; }
684
+
685
+ if (existing.permissions && existing.permissions.additionalDirectories) {
686
+ delete existing.permissions.additionalDirectories;
687
+ // Clean up empty permissions object
688
+ if (Object.keys(existing.permissions).length === 0) {
689
+ delete existing.permissions;
690
+ }
691
+ // If nothing left, remove the file
692
+ if (Object.keys(existing).length === 0) {
693
+ fs.unlinkSync(settingsLocalPath);
694
+ } else {
695
+ fs.writeFileSync(settingsLocalPath, JSON.stringify(existing, null, 2) + '\n');
696
+ }
697
+ }
698
+ }
699
+
636
700
  /**
637
701
  * Deactivate workstream (output shell command to unset env var)
638
702
  */
639
703
  function workstreamDeactivate() {
704
+ // Clean up sandbox settings from CWD
705
+ removeWorkstreamSandboxSettings(process.cwd());
706
+
640
707
  console.log('To deactivate the workstream for this session, run:');
641
708
  console.log(' unset CODER_WORKSTREAM');
642
709
  console.log('\nOr to clear the global active workstream:');
@@ -1308,6 +1375,35 @@ function workstreamRemoveTrigger(installDir, idOrName, folderPath) {
1308
1375
  return ws;
1309
1376
  }
1310
1377
 
1378
+ /**
1379
+ * Set sandbox mode for a workstream
1380
+ * When true (default): generates additionalDirectories in settings.local.json for OS-level enforcement
1381
+ * When false: only injects advisory LLM rules (softer, LLM can override if important)
1382
+ */
1383
+ function workstreamSetSandbox(installDir, idOrName, value) {
1384
+ const data = loadWorkstreams(installDir);
1385
+ const ws = data.workstreams.find(
1386
+ w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
1387
+ );
1388
+
1389
+ if (!ws) {
1390
+ console.error(`Workstream not found: ${idOrName}`);
1391
+ return null;
1392
+ }
1393
+
1394
+ if (value === 'on' || value === true || value === 'true') {
1395
+ ws.sandbox = true;
1396
+ console.log(`✓ Sandbox enabled for ${ws.name} (OS-level directory enforcement)`);
1397
+ } else if (value === 'off' || value === false || value === 'false') {
1398
+ ws.sandbox = false;
1399
+ console.log(`✓ Sandbox disabled for ${ws.name} (advisory LLM rules only)`);
1400
+ }
1401
+
1402
+ ws.updatedAt = new Date().toISOString();
1403
+ saveWorkstreams(installDir, data);
1404
+ return ws;
1405
+ }
1406
+
1311
1407
  /**
1312
1408
  * Set auto-activate for a workstream
1313
1409
  * value: true, false, or null (use global default)
@@ -1641,4 +1737,7 @@ module.exports = {
1641
1737
  workstreamInstallCdHook,
1642
1738
  workstreamUninstallCdHook,
1643
1739
  workstreamCdHookStatus,
1740
+ writeWorkstreamSandboxSettings,
1741
+ removeWorkstreamSandboxSettings,
1742
+ workstreamSetSandbox,
1644
1743
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.46.16",
3
+ "version": "0.47.1-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",