coder-config 0.42.13 → 0.42.14

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 } = 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 } = 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, loopStatus, loopHistory, loopConfig, getActiveLoop, recordIteration, saveClarifications, savePlan, loadClarifications, loadPlan, loopInject, archiveLoop } = require('./lib/loops');
34
34
  const { runCli } = require('./lib/cli');
@@ -148,6 +148,19 @@ class ClaudeConfigManager {
148
148
  workstreamInstallHookCodex() { return workstreamInstallHookCodex(); }
149
149
  workstreamDeactivate() { return workstreamDeactivate(); }
150
150
  workstreamCheckPath(targetPath, silent) { return workstreamCheckPath(this.installDir, targetPath, silent); }
151
+ // Workstream folder auto-activation
152
+ getSettingsPath() { return getSettingsPath(this.installDir); }
153
+ loadSettings() { return loadSettings(this.installDir); }
154
+ saveSettings(settings) { return saveSettings(this.installDir, settings); }
155
+ workstreamAddTrigger(idOrName, folderPath) { return workstreamAddTrigger(this.installDir, idOrName, folderPath); }
156
+ workstreamRemoveTrigger(idOrName, folderPath) { return workstreamRemoveTrigger(this.installDir, idOrName, folderPath); }
157
+ workstreamSetAutoActivate(idOrName, value) { return workstreamSetAutoActivate(this.installDir, idOrName, value); }
158
+ setGlobalAutoActivate(value) { return setGlobalAutoActivate(this.installDir, value); }
159
+ shouldAutoActivate(workstream) { return shouldAutoActivate(this.installDir, workstream); }
160
+ workstreamCheckFolder(folderPath, jsonOutput) { return workstreamCheckFolder(this.installDir, folderPath, jsonOutput); }
161
+ workstreamInstallCdHook() { return workstreamInstallCdHook(); }
162
+ workstreamUninstallCdHook() { return workstreamUninstallCdHook(); }
163
+ workstreamCdHookStatus() { return workstreamCdHookStatus(); }
151
164
 
152
165
  // Loops (Ralph Loop)
153
166
  getLoopsPath() { return getLoopsPath(this.installDir); }
package/lib/cli.js CHANGED
@@ -144,6 +144,38 @@ function runCli(manager) {
144
144
  const silent = args.includes('--silent') || args.includes('-s');
145
145
  const isValid = manager.workstreamCheckPath(targetPath, silent);
146
146
  process.exit(isValid ? 0 : 1);
147
+ } else if (args[1] === 'add-trigger') {
148
+ if (!args[2] || !args[3]) {
149
+ console.error('Usage: coder-config workstream add-trigger <workstream> <folder>');
150
+ process.exit(1);
151
+ }
152
+ manager.workstreamAddTrigger(args[2], args[3]);
153
+ } else if (args[1] === 'remove-trigger') {
154
+ if (!args[2] || !args[3]) {
155
+ console.error('Usage: coder-config workstream remove-trigger <workstream> <folder>');
156
+ process.exit(1);
157
+ }
158
+ manager.workstreamRemoveTrigger(args[2], args[3]);
159
+ } else if (args[1] === 'auto-activate') {
160
+ if (!args[2]) {
161
+ console.error('Usage: coder-config workstream auto-activate <workstream> [on|off|default]');
162
+ process.exit(1);
163
+ }
164
+ const value = args[3] || 'on';
165
+ manager.workstreamSetAutoActivate(args[2], value);
166
+ } else if (args[1] === 'check-folder') {
167
+ const folderPath = args[2] || process.cwd();
168
+ const jsonOutput = args.includes('--json') || args.includes('-j');
169
+ manager.workstreamCheckFolder(folderPath, jsonOutput);
170
+ } else if (args[1] === 'install-cd-hook') {
171
+ manager.workstreamInstallCdHook();
172
+ } else if (args[1] === 'uninstall-cd-hook') {
173
+ manager.workstreamUninstallCdHook();
174
+ } else if (args[1] === 'cd-hook-status') {
175
+ const status = manager.workstreamCdHookStatus();
176
+ console.log(`CD hook: ${status.installed ? 'installed' : 'not installed'}`);
177
+ console.log(`Shell: ${status.shell}`);
178
+ console.log(`RC file: ${status.rcFile}`);
147
179
  } else {
148
180
  manager.workstreamList();
149
181
  }
@@ -286,6 +318,15 @@ Workstream Commands:
286
318
  workstream install-hook --codex Install for Codex CLI
287
319
  workstream install-hook --all Install for all supported tools
288
320
 
321
+ Folder Auto-Activation:
322
+ workstream add-trigger <ws> <folder> Add trigger folder
323
+ workstream remove-trigger <ws> <folder> Remove trigger folder
324
+ workstream auto-activate <ws> [on|off|default] Set auto-activate
325
+ workstream check-folder [path] [--json] Check folder for matches
326
+ workstream install-cd-hook Install cd hook for auto-activation
327
+ workstream uninstall-cd-hook Remove cd hook
328
+ workstream cd-hook-status Check if cd hook is installed
329
+
289
330
  Per-session activation (enables parallel work):
290
331
  export CODER_WORKSTREAM=<name-or-id>
291
332
 
package/lib/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Constants and tool path configurations
3
3
  */
4
4
 
5
- const VERSION = '0.42.13';
5
+ const VERSION = '0.42.14';
6
6
 
7
7
  // Tool-specific path configurations
8
8
  const TOOL_PATHS = {
@@ -875,6 +875,371 @@ function extractFirstParagraph(content) {
875
875
  return result.join(' ').replace(/\s+/g, ' ').trim();
876
876
  }
877
877
 
878
+ /**
879
+ * Get global settings path
880
+ */
881
+ function getSettingsPath(installDir) {
882
+ return path.join(installDir, 'settings.json');
883
+ }
884
+
885
+ /**
886
+ * Load global settings
887
+ */
888
+ function loadSettings(installDir) {
889
+ const settingsPath = getSettingsPath(installDir);
890
+ if (fs.existsSync(settingsPath)) {
891
+ try {
892
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
893
+ } catch (e) {
894
+ return { workstreamAutoActivate: true };
895
+ }
896
+ }
897
+ return { workstreamAutoActivate: true };
898
+ }
899
+
900
+ /**
901
+ * Save global settings
902
+ */
903
+ function saveSettings(installDir, settings) {
904
+ const settingsPath = getSettingsPath(installDir);
905
+ const dir = path.dirname(settingsPath);
906
+ if (!fs.existsSync(dir)) {
907
+ fs.mkdirSync(dir, { recursive: true });
908
+ }
909
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
910
+ }
911
+
912
+ /**
913
+ * Add trigger folder to workstream
914
+ */
915
+ function workstreamAddTrigger(installDir, idOrName, folderPath) {
916
+ const data = loadWorkstreams(installDir);
917
+ const ws = data.workstreams.find(
918
+ w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
919
+ );
920
+
921
+ if (!ws) {
922
+ console.error(`Workstream not found: ${idOrName}`);
923
+ return null;
924
+ }
925
+
926
+ const absPath = path.resolve(folderPath.replace(/^~/, process.env.HOME || ''));
927
+
928
+ if (!ws.triggerFolders) {
929
+ ws.triggerFolders = [];
930
+ }
931
+
932
+ if (!ws.triggerFolders.includes(absPath)) {
933
+ ws.triggerFolders.push(absPath);
934
+ ws.updatedAt = new Date().toISOString();
935
+ saveWorkstreams(installDir, data);
936
+ console.log(`✓ Added trigger folder ${path.basename(absPath)} to ${ws.name}`);
937
+ } else {
938
+ console.log(`Trigger folder already in workstream: ${path.basename(absPath)}`);
939
+ }
940
+
941
+ return ws;
942
+ }
943
+
944
+ /**
945
+ * Remove trigger folder from workstream
946
+ */
947
+ function workstreamRemoveTrigger(installDir, idOrName, folderPath) {
948
+ const data = loadWorkstreams(installDir);
949
+ const ws = data.workstreams.find(
950
+ w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
951
+ );
952
+
953
+ if (!ws) {
954
+ console.error(`Workstream not found: ${idOrName}`);
955
+ return null;
956
+ }
957
+
958
+ const absPath = path.resolve(folderPath.replace(/^~/, process.env.HOME || ''));
959
+
960
+ if (!ws.triggerFolders) {
961
+ ws.triggerFolders = [];
962
+ }
963
+
964
+ const idx = ws.triggerFolders.indexOf(absPath);
965
+
966
+ if (idx !== -1) {
967
+ ws.triggerFolders.splice(idx, 1);
968
+ ws.updatedAt = new Date().toISOString();
969
+ saveWorkstreams(installDir, data);
970
+ console.log(`✓ Removed trigger folder ${path.basename(absPath)} from ${ws.name}`);
971
+ } else {
972
+ console.log(`Trigger folder not in workstream: ${path.basename(absPath)}`);
973
+ }
974
+
975
+ return ws;
976
+ }
977
+
978
+ /**
979
+ * Set auto-activate for a workstream
980
+ * value: true, false, or null (use global default)
981
+ */
982
+ function workstreamSetAutoActivate(installDir, idOrName, value) {
983
+ const data = loadWorkstreams(installDir);
984
+ const ws = data.workstreams.find(
985
+ w => w.id === idOrName || w.name.toLowerCase() === idOrName.toLowerCase()
986
+ );
987
+
988
+ if (!ws) {
989
+ console.error(`Workstream not found: ${idOrName}`);
990
+ return null;
991
+ }
992
+
993
+ if (value === 'on' || value === true) {
994
+ ws.autoActivate = true;
995
+ console.log(`✓ Auto-activate enabled for ${ws.name}`);
996
+ } else if (value === 'off' || value === false) {
997
+ ws.autoActivate = false;
998
+ console.log(`✓ Auto-activate disabled for ${ws.name}`);
999
+ } else if (value === 'default' || value === null) {
1000
+ delete ws.autoActivate;
1001
+ console.log(`✓ Auto-activate set to global default for ${ws.name}`);
1002
+ }
1003
+
1004
+ ws.updatedAt = new Date().toISOString();
1005
+ saveWorkstreams(installDir, data);
1006
+ return ws;
1007
+ }
1008
+
1009
+ /**
1010
+ * Set global auto-activate setting
1011
+ */
1012
+ function setGlobalAutoActivate(installDir, value) {
1013
+ const settings = loadSettings(installDir);
1014
+ settings.workstreamAutoActivate = value === true || value === 'true' || value === 'on';
1015
+ saveSettings(installDir, settings);
1016
+ console.log(`✓ Global workstream auto-activate: ${settings.workstreamAutoActivate ? 'on' : 'off'}`);
1017
+ return settings;
1018
+ }
1019
+
1020
+ /**
1021
+ * Check if a workstream should auto-activate based on its setting and global default
1022
+ */
1023
+ function shouldAutoActivate(installDir, workstream) {
1024
+ if (workstream.autoActivate === true) return true;
1025
+ if (workstream.autoActivate === false) return false;
1026
+ // Use global default
1027
+ const settings = loadSettings(installDir);
1028
+ return settings.workstreamAutoActivate !== false;
1029
+ }
1030
+
1031
+ /**
1032
+ * Check folder for matching workstreams
1033
+ * Returns { count, current, matches: [{id, name, autoActivate}] }
1034
+ */
1035
+ function workstreamCheckFolder(installDir, folderPath = process.cwd(), jsonOutput = false) {
1036
+ const data = loadWorkstreams(installDir);
1037
+ const absPath = path.resolve(folderPath.replace(/^~/, process.env.HOME || ''));
1038
+
1039
+ // Find all workstreams that match this folder
1040
+ const matches = data.workstreams.filter(ws => {
1041
+ // Check if folder is within any project
1042
+ const inProject = (ws.projects || []).some(p =>
1043
+ absPath === p || absPath.startsWith(p + path.sep) || p.startsWith(absPath + path.sep)
1044
+ );
1045
+ // Check if folder is within any trigger folder
1046
+ const inTrigger = (ws.triggerFolders || []).some(t =>
1047
+ absPath === t || absPath.startsWith(t + path.sep) || t.startsWith(absPath + path.sep)
1048
+ );
1049
+ return inProject || inTrigger;
1050
+ });
1051
+
1052
+ // Filter to only those that should auto-activate
1053
+ const autoActivateMatches = matches.filter(ws => shouldAutoActivate(installDir, ws));
1054
+
1055
+ // Check if current workstream is already one of the matches
1056
+ const currentWs = getActiveWorkstream(installDir);
1057
+ const currentIsMatch = currentWs && autoActivateMatches.some(m => m.id === currentWs.id);
1058
+
1059
+ const result = {
1060
+ count: autoActivateMatches.length,
1061
+ current: currentIsMatch,
1062
+ matches: autoActivateMatches.map(ws => ({
1063
+ id: ws.id,
1064
+ name: ws.name,
1065
+ autoActivate: ws.autoActivate
1066
+ }))
1067
+ };
1068
+
1069
+ if (jsonOutput) {
1070
+ console.log(JSON.stringify(result));
1071
+ } else {
1072
+ if (result.count === 0) {
1073
+ console.log('No matching workstreams for this folder');
1074
+ } else if (result.current) {
1075
+ console.log(`Current workstream "${currentWs.name}" matches this folder`);
1076
+ } else if (result.count === 1) {
1077
+ console.log(`Matching workstream: ${result.matches[0].name}`);
1078
+ } else {
1079
+ console.log(`${result.count} matching workstreams:`);
1080
+ result.matches.forEach((m, i) => {
1081
+ console.log(` ${i + 1}) ${m.name}`);
1082
+ });
1083
+ }
1084
+ }
1085
+
1086
+ return result;
1087
+ }
1088
+
1089
+ /**
1090
+ * Install the cd hook for automatic workstream activation
1091
+ */
1092
+ function workstreamInstallCdHook() {
1093
+ const shell = process.env.SHELL || '/bin/zsh';
1094
+ const isZsh = shell.includes('zsh');
1095
+ const rcFile = isZsh
1096
+ ? path.join(process.env.HOME || '', '.zshrc')
1097
+ : path.join(process.env.HOME || '', '.bashrc');
1098
+
1099
+ const hookMarker = '# coder-config workstream cd hook';
1100
+ const hookCode = `
1101
+ ${hookMarker}
1102
+ coder_workstream_cd() {
1103
+ builtin cd "$@" || return $?
1104
+
1105
+ # Check for matching workstreams
1106
+ local result
1107
+ result=$(coder-config workstream check-folder "$PWD" --json 2>/dev/null)
1108
+
1109
+ [ -z "$result" ] && return 0
1110
+
1111
+ local count current
1112
+ count=$(echo "$result" | grep -o '"count":[0-9]*' | cut -d: -f2)
1113
+ current=$(echo "$result" | grep -o '"current":true' || echo "")
1114
+
1115
+ # Already on matching workstream
1116
+ [ -n "$current" ] && return 0
1117
+
1118
+ [ "$count" = "0" ] && return 0
1119
+
1120
+ if [ "$count" = "1" ]; then
1121
+ # Single match - auto-activate
1122
+ local name id
1123
+ name=$(echo "$result" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
1124
+ id=$(echo "$result" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
1125
+ export CODER_WORKSTREAM="$id"
1126
+ echo "📂 Workstream: $name"
1127
+ elif [ "$count" -gt 1 ]; then
1128
+ # Multiple matches - prompt
1129
+ echo "Multiple workstreams match this folder:"
1130
+ local i=1
1131
+ echo "$result" | grep -o '"name":"[^"]*"' | cut -d'"' -f4 | while read -r name; do
1132
+ echo " $i) $name"
1133
+ i=$((i + 1))
1134
+ done
1135
+ echo " 0) Skip"
1136
+ ${isZsh ? 'read "choice?Choose [0-$count]: "' : 'read -p "Choose [0-$count]: " choice'}
1137
+ if [ "$choice" -gt 0 ] 2>/dev/null; then
1138
+ local id name
1139
+ id=$(echo "$result" | grep -o '"id":"[^"]*"' | sed -n "${choice}p" | cut -d'"' -f4)
1140
+ name=$(echo "$result" | grep -o '"name":"[^"]*"' | sed -n "${choice}p" | cut -d'"' -f4)
1141
+ if [ -n "$id" ]; then
1142
+ export CODER_WORKSTREAM="$id"
1143
+ echo "📂 Workstream: $name"
1144
+ fi
1145
+ fi
1146
+ fi
1147
+ }
1148
+ alias cd='coder_workstream_cd'
1149
+ # end coder-config workstream cd hook
1150
+ `;
1151
+
1152
+ // Check if hook already installed
1153
+ if (fs.existsSync(rcFile)) {
1154
+ const content = fs.readFileSync(rcFile, 'utf8');
1155
+ if (content.includes(hookMarker)) {
1156
+ console.log('✓ Workstream cd hook already installed');
1157
+ console.log(` Location: ${rcFile}`);
1158
+ return true;
1159
+ }
1160
+ }
1161
+
1162
+ // Append hook to rc file
1163
+ fs.appendFileSync(rcFile, hookCode);
1164
+ console.log(`✓ Installed workstream cd hook`);
1165
+ console.log(` Location: ${rcFile}`);
1166
+ console.log('');
1167
+ console.log('Restart your shell or run:');
1168
+ console.log(` source ${rcFile}`);
1169
+
1170
+ return true;
1171
+ }
1172
+
1173
+ /**
1174
+ * Uninstall the cd hook
1175
+ */
1176
+ function workstreamUninstallCdHook() {
1177
+ const shell = process.env.SHELL || '/bin/zsh';
1178
+ const isZsh = shell.includes('zsh');
1179
+ const rcFile = isZsh
1180
+ ? path.join(process.env.HOME || '', '.zshrc')
1181
+ : path.join(process.env.HOME || '', '.bashrc');
1182
+
1183
+ if (!fs.existsSync(rcFile)) {
1184
+ console.log('Shell config file not found');
1185
+ return false;
1186
+ }
1187
+
1188
+ const content = fs.readFileSync(rcFile, 'utf8');
1189
+ const hookMarker = '# coder-config workstream cd hook';
1190
+ const endMarker = '# end coder-config workstream cd hook';
1191
+
1192
+ if (!content.includes(hookMarker)) {
1193
+ console.log('Workstream cd hook not installed');
1194
+ return false;
1195
+ }
1196
+
1197
+ // Remove the hook block
1198
+ const startIdx = content.indexOf(hookMarker);
1199
+ const endIdx = content.indexOf(endMarker);
1200
+
1201
+ if (startIdx === -1 || endIdx === -1) {
1202
+ console.error('Could not find hook boundaries');
1203
+ return false;
1204
+ }
1205
+
1206
+ // Find the newline before the marker (to remove leading blank line)
1207
+ let removeStart = startIdx;
1208
+ if (startIdx > 0 && content[startIdx - 1] === '\n') {
1209
+ removeStart = startIdx - 1;
1210
+ }
1211
+
1212
+ const newContent = content.slice(0, removeStart) + content.slice(endIdx + endMarker.length);
1213
+ fs.writeFileSync(rcFile, newContent);
1214
+
1215
+ console.log(`✓ Uninstalled workstream cd hook from ${rcFile}`);
1216
+ console.log('');
1217
+ console.log('Restart your shell or run:');
1218
+ console.log(` source ${rcFile}`);
1219
+
1220
+ return true;
1221
+ }
1222
+
1223
+ /**
1224
+ * Check if cd hook is installed
1225
+ */
1226
+ function workstreamCdHookStatus() {
1227
+ const shell = process.env.SHELL || '/bin/zsh';
1228
+ const isZsh = shell.includes('zsh');
1229
+ const rcFile = isZsh
1230
+ ? path.join(process.env.HOME || '', '.zshrc')
1231
+ : path.join(process.env.HOME || '', '.bashrc');
1232
+
1233
+ if (!fs.existsSync(rcFile)) {
1234
+ return { installed: false, shell: isZsh ? 'zsh' : 'bash', rcFile };
1235
+ }
1236
+
1237
+ const content = fs.readFileSync(rcFile, 'utf8');
1238
+ const installed = content.includes('# coder-config workstream cd hook');
1239
+
1240
+ return { installed, shell: isZsh ? 'zsh' : 'bash', rcFile };
1241
+ }
1242
+
878
1243
  module.exports = {
879
1244
  getWorkstreamsPath,
880
1245
  loadWorkstreams,
@@ -899,4 +1264,17 @@ module.exports = {
899
1264
  workstreamCheckPath,
900
1265
  generateRulesFromRepos,
901
1266
  generateRulesWithClaude,
1267
+ // New folder auto-activation functions
1268
+ getSettingsPath,
1269
+ loadSettings,
1270
+ saveSettings,
1271
+ workstreamAddTrigger,
1272
+ workstreamRemoveTrigger,
1273
+ workstreamSetAutoActivate,
1274
+ setGlobalAutoActivate,
1275
+ shouldAutoActivate,
1276
+ workstreamCheckFolder,
1277
+ workstreamInstallCdHook,
1278
+ workstreamUninstallCdHook,
1279
+ workstreamCdHookStatus,
902
1280
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-config",
3
- "version": "0.42.13",
3
+ "version": "0.42.14",
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",