@nemus-cli/nemus 0.9.0 → 0.11.0

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.
Files changed (74) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +23 -5
  3. package/bin/workspace.js +12 -0
  4. package/dist/commands/analyze-deps.js +5 -12
  5. package/dist/commands/archive.js +13 -18
  6. package/dist/commands/branch/create.js +5 -12
  7. package/dist/commands/branch/switch.js +14 -25
  8. package/dist/commands/cache/manager.js +13 -24
  9. package/dist/commands/cleanup.js +14 -26
  10. package/dist/commands/configure-claude.js +4 -8
  11. package/dist/commands/configure.js +38 -32
  12. package/dist/commands/dashboard/session-picker.js +19 -26
  13. package/dist/commands/dashboard/workspace-picker.js +19 -26
  14. package/dist/commands/delete.js +15 -30
  15. package/dist/commands/ghq-status.js +7 -12
  16. package/dist/commands/go.js +18 -27
  17. package/dist/commands/history.js +6 -13
  18. package/dist/commands/list.js +20 -29
  19. package/dist/commands/prune.js +196 -0
  20. package/dist/commands/remove-repo.js +21 -29
  21. package/dist/commands/save-context.js +5 -10
  22. package/dist/commands/sessions.js +19 -25
  23. package/dist/commands/suite/create.js +27 -53
  24. package/dist/commands/suite/delete.js +13 -25
  25. package/dist/commands/suite/export.js +20 -36
  26. package/dist/commands/suite/import.js +9 -20
  27. package/dist/commands/suite/use.js +9 -17
  28. package/dist/program.js +2 -0
  29. package/dist/utils/prompt.js +26 -0
  30. package/dist/utils/prompts.js +86 -118
  31. package/dist/utils/prune.js +70 -0
  32. package/package.json +3 -6
  33. package/scripts/release-notes.mjs +54 -0
  34. package/skills/config.md +17 -0
  35. package/skills/nemus/SKILL.md +7 -2
  36. package/skills/nemus/references/completion.md +22 -0
  37. package/skills/nemus/references/config.md +32 -0
  38. package/skills/nemus/references/prune.md +44 -0
  39. package/skills/nemus/references/reflect.md +43 -0
  40. package/skills/nemus/references/save-context.md +25 -0
  41. package/skills/prune-workspaces.md +23 -0
  42. package/skills/reflect.md +21 -0
  43. package/src/commands/analyze-deps.ts +5 -9
  44. package/src/commands/archive.ts +5 -7
  45. package/src/commands/branch/create.ts +5 -9
  46. package/src/commands/branch/switch.ts +14 -22
  47. package/src/commands/cache/manager.ts +13 -21
  48. package/src/commands/cleanup.ts +14 -23
  49. package/src/commands/configure-claude.ts +4 -5
  50. package/src/commands/configure.ts +35 -29
  51. package/src/commands/dashboard/session-picker.ts +6 -11
  52. package/src/commands/dashboard/workspace-picker.ts +6 -11
  53. package/src/commands/delete.test.ts +36 -42
  54. package/src/commands/delete.ts +15 -27
  55. package/src/commands/ghq-status.ts +5 -7
  56. package/src/commands/go.ts +18 -25
  57. package/src/commands/history.ts +6 -10
  58. package/src/commands/list.test.ts +19 -26
  59. package/src/commands/list.ts +24 -31
  60. package/src/commands/prune.ts +183 -0
  61. package/src/commands/remove-repo.ts +11 -17
  62. package/src/commands/save-context.ts +3 -5
  63. package/src/commands/sessions.ts +6 -10
  64. package/src/commands/suite/create.ts +28 -50
  65. package/src/commands/suite/delete.ts +14 -23
  66. package/src/commands/suite/export.ts +20 -33
  67. package/src/commands/suite/import.ts +9 -17
  68. package/src/commands/suite/use.ts +9 -14
  69. package/src/program.ts +2 -0
  70. package/src/utils/prompt.ts +16 -0
  71. package/src/utils/prompts.test.ts +70 -41
  72. package/src/utils/prompts.ts +98 -128
  73. package/src/utils/prune.test.ts +121 -0
  74. package/src/utils/prune.ts +109 -0
@@ -32,41 +32,31 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
35
  Object.defineProperty(exports, "__esModule", { value: true });
39
36
  exports.promptMultiWorkspaceSelection = exports.promptWorkspaceSelection = exports.confirmWorkspaceCreation = exports.promptWorkspaceName = exports.promptRepositorySelection = exports.promptInstanceSuffix = void 0;
40
- const inquirer_1 = __importDefault(require("inquirer"));
41
- const inquirer_autocomplete_prompt_1 = __importDefault(require("inquirer-autocomplete-prompt"));
37
+ const prompt_1 = require("./prompt");
42
38
  const fuzzy = __importStar(require("fuzzy"));
43
39
  const validation_1 = require("./validation");
44
40
  const colors_1 = require("./colors");
45
- // Register autocomplete prompt type
46
- inquirer_1.default.registerPrompt('autocomplete', inquirer_autocomplete_prompt_1.default);
47
41
  const promptInstanceSuffix = async (repoName, existingDirectoryNames) => {
48
- const { suffix } = await inquirer_1.default.prompt([
49
- {
50
- type: 'input',
51
- name: 'suffix',
52
- message: `"${repoName}" already exists. Enter a suffix for this instance:`,
53
- validate: (input) => {
54
- if (!input || input.trim().length === 0) {
55
- return 'Suffix cannot be empty';
56
- }
57
- const trimmed = input.trim();
58
- // Validate characters (alphanumeric, hyphens, underscores)
59
- if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
60
- return 'Suffix can only contain letters, numbers, hyphens, and underscores';
61
- }
62
- const candidateName = `${repoName}-${trimmed}`;
63
- if (existingDirectoryNames.includes(candidateName)) {
64
- return `"${candidateName}" already exists. Choose a different suffix`;
65
- }
66
- return true;
67
- },
42
+ const suffix = await (0, prompt_1.input)({
43
+ message: `"${repoName}" already exists. Enter a suffix for this instance:`,
44
+ validate: (input) => {
45
+ if (!input || input.trim().length === 0) {
46
+ return 'Suffix cannot be empty';
47
+ }
48
+ const trimmed = input.trim();
49
+ // Validate characters (alphanumeric, hyphens, underscores)
50
+ if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
51
+ return 'Suffix can only contain letters, numbers, hyphens, and underscores';
52
+ }
53
+ const candidateName = `${repoName}-${trimmed}`;
54
+ if (existingDirectoryNames.includes(candidateName)) {
55
+ return `"${candidateName}" already exists. Choose a different suffix`;
56
+ }
57
+ return true;
68
58
  },
69
- ]);
59
+ });
70
60
  return suffix.trim();
71
61
  };
72
62
  exports.promptInstanceSuffix = promptInstanceSuffix;
@@ -80,38 +70,34 @@ const promptRepositorySelection = async (repos, existingDirectoryNames = []) =>
80
70
  console.log('Type "done" when finished selecting repositories.\n');
81
71
  while (true) {
82
72
  try {
83
- const { repoName } = await inquirer_1.default.prompt([
84
- {
85
- type: 'autocomplete',
86
- name: 'repoName',
87
- message: `Search and select repository (${(0, colors_1.colorize)(String(selectedEntries.length), 'cyan')} selected):`,
88
- source: async (_answersSoFar, input) => {
89
- const searchInput = input || '';
90
- // Always include done option
91
- const doneOption = { name: (0, colors_1.colorize)('done - Finish selection', 'green'), value: 'done' };
92
- // If no input or "done" typed, show done + top repos
93
- if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
94
- return [
95
- doneOption,
96
- ...repos.slice(0, 15).map(repo => ({
97
- name: `${repo.name}${repo.description ? ` - ${(0, colors_1.colorize)(repo.description, 'gray')}` : ''}`,
98
- value: repo.name,
99
- }))
100
- ];
101
- }
102
- // Perform fuzzy search
103
- const results = fuzzy.filter(searchInput, repos, {
104
- extract: (repo) => `${repo.name} ${repo.description || ''}`,
105
- });
106
- const suggestions = results.slice(0, 15).map(result => ({
107
- name: `${result.original.name}${result.original.description ? ` - ${(0, colors_1.colorize)(result.original.description, 'gray')}` : ''}`,
108
- value: result.original.name,
109
- }));
110
- return [doneOption, ...suggestions];
111
- },
112
- pageSize: 16,
73
+ const repoName = await (0, prompt_1.search)({
74
+ message: `Search and select repository (${(0, colors_1.colorize)(String(selectedEntries.length), 'cyan')} selected):`,
75
+ pageSize: 16,
76
+ source: async (term) => {
77
+ const searchInput = term || '';
78
+ // Always include done option
79
+ const doneOption = { name: (0, colors_1.colorize)('done - Finish selection', 'green'), value: 'done' };
80
+ // If no input or "done" typed, show done + top repos
81
+ if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
82
+ return [
83
+ doneOption,
84
+ ...repos.slice(0, 15).map(repo => ({
85
+ name: `${repo.name}${repo.description ? ` - ${(0, colors_1.colorize)(repo.description, 'gray')}` : ''}`,
86
+ value: repo.name,
87
+ }))
88
+ ];
89
+ }
90
+ // Perform fuzzy search
91
+ const results = fuzzy.filter(searchInput, repos, {
92
+ extract: (repo) => `${repo.name} ${repo.description || ''}`,
93
+ });
94
+ const suggestions = results.slice(0, 15).map(result => ({
95
+ name: `${result.original.name}${result.original.description ? ` - ${(0, colors_1.colorize)(result.original.description, 'gray')}` : ''}`,
96
+ value: result.original.name,
97
+ }));
98
+ return [doneOption, ...suggestions];
113
99
  },
114
- ]);
100
+ });
115
101
  if (repoName === 'done') {
116
102
  if (selectedEntries.length === 0) {
117
103
  console.log((0, colors_1.colorize)('\nYou must select at least one repository\n', 'yellow'));
@@ -153,22 +139,17 @@ const promptRepositorySelection = async (repos, existingDirectoryNames = []) =>
153
139
  };
154
140
  exports.promptRepositorySelection = promptRepositorySelection;
155
141
  const promptWorkspaceName = async () => {
156
- const { workspaceName } = await inquirer_1.default.prompt([
157
- {
158
- type: 'input',
159
- name: 'workspaceName',
160
- message: 'Enter workspace name:',
161
- validate: (input) => {
162
- const validationResult = (0, validation_1.validateWorkspaceName)(input);
163
- if (validationResult !== true) {
164
- return validationResult;
165
- }
166
- return true;
167
- },
168
- filter: (input) => (0, validation_1.sanitizeWorkspaceName)(input),
142
+ const raw = await (0, prompt_1.input)({
143
+ message: 'Enter workspace name:',
144
+ validate: (value) => {
145
+ const validationResult = (0, validation_1.validateWorkspaceName)((0, validation_1.sanitizeWorkspaceName)(value));
146
+ if (validationResult !== true) {
147
+ return validationResult;
148
+ }
149
+ return true;
169
150
  },
170
- ]);
171
- return workspaceName;
151
+ });
152
+ return (0, validation_1.sanitizeWorkspaceName)(raw);
172
153
  };
173
154
  exports.promptWorkspaceName = promptWorkspaceName;
174
155
  const confirmWorkspaceCreation = async (workspaceName, repoCount, workspacePath) => {
@@ -179,14 +160,10 @@ const confirmWorkspaceCreation = async (workspaceName, repoCount, workspacePath)
179
160
  console.log(`Location: ${(0, colors_1.colorize)(workspacePath, 'gray')}`);
180
161
  console.log(`Repositories: ${(0, colors_1.colorize)(String(repoCount), 'yellow')}`);
181
162
  console.log('='.repeat(60) + '\n');
182
- const { confirmed } = await inquirer_1.default.prompt([
183
- {
184
- type: 'confirm',
185
- name: 'confirmed',
186
- message: 'Create workspace with these settings?',
187
- default: true,
188
- },
189
- ]);
163
+ const confirmed = await (0, prompt_1.confirm)({
164
+ message: 'Create workspace with these settings?',
165
+ default: true,
166
+ });
190
167
  return confirmed;
191
168
  };
192
169
  exports.confirmWorkspaceCreation = confirmWorkspaceCreation;
@@ -199,17 +176,12 @@ const promptWorkspaceSelection = async (workspaces) => {
199
176
  ? `${ws.name} (${ws.metadata.repositories.length} repos)`
200
177
  : ws.name,
201
178
  value: ws.name,
202
- short: ws.name,
203
179
  }));
204
- const { workspaceName } = await inquirer_1.default.prompt([
205
- {
206
- type: 'list',
207
- name: 'workspaceName',
208
- message: 'Select workspace to update:',
209
- choices,
210
- pageSize: 15,
211
- },
212
- ]);
180
+ const workspaceName = await (0, prompt_1.select)({
181
+ message: 'Select workspace to update:',
182
+ choices,
183
+ pageSize: 15,
184
+ });
213
185
  return workspaceName;
214
186
  };
215
187
  exports.promptWorkspaceSelection = promptWorkspaceSelection;
@@ -226,33 +198,29 @@ const promptMultiWorkspaceSelection = async (workspaces) => {
226
198
  console.log((0, colors_1.colorize)('All workspaces selected.', 'yellow'));
227
199
  break;
228
200
  }
229
- const { workspaceName } = await inquirer_1.default.prompt([
230
- {
231
- type: 'autocomplete',
232
- name: 'workspaceName',
233
- message: `Search and select workspace (${(0, colors_1.colorize)(String(selectedNames.length), 'cyan')} selected):`,
234
- source: async (_answersSoFar, input) => {
235
- const searchInput = input || '';
236
- const doneOption = { name: (0, colors_1.colorize)('done - Finish selection', 'green'), value: 'done' };
237
- if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
238
- return [
239
- doneOption,
240
- ...availableNames.slice(0, 15).map(name => ({
241
- name,
242
- value: name,
243
- }))
244
- ];
245
- }
246
- const results = fuzzy.filter(searchInput, availableNames);
247
- const suggestions = results.slice(0, 15).map(result => ({
248
- name: result.original,
249
- value: result.original,
250
- }));
251
- return [doneOption, ...suggestions];
252
- },
253
- pageSize: 16,
201
+ const workspaceName = await (0, prompt_1.search)({
202
+ message: `Search and select workspace (${(0, colors_1.colorize)(String(selectedNames.length), 'cyan')} selected):`,
203
+ pageSize: 16,
204
+ source: async (term) => {
205
+ const searchInput = term || '';
206
+ const doneOption = { name: (0, colors_1.colorize)('done - Finish selection', 'green'), value: 'done' };
207
+ if (!searchInput || searchInput.toLowerCase().startsWith('done')) {
208
+ return [
209
+ doneOption,
210
+ ...availableNames.slice(0, 15).map(name => ({
211
+ name,
212
+ value: name,
213
+ }))
214
+ ];
215
+ }
216
+ const results = fuzzy.filter(searchInput, availableNames);
217
+ const suggestions = results.slice(0, 15).map(result => ({
218
+ name: result.original,
219
+ value: result.original,
220
+ }));
221
+ return [doneOption, ...suggestions];
254
222
  },
255
- ]);
223
+ });
256
224
  if (workspaceName === 'done') {
257
225
  if (selectedNames.length === 0) {
258
226
  console.log((0, colors_1.colorize)('\nYou must select at least one workspace\n', 'yellow'));
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toCandidate = toCandidate;
4
+ exports.isStale = isStale;
5
+ exports.protectionReason = protectionReason;
6
+ exports.planPrune = planPrune;
7
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
8
+ /** Build a dated candidate for a workspace. `now` is injected for testability. */
9
+ function toCandidate(ws, now) {
10
+ const referenceAt = ws.lastActiveAt > 0 ? ws.lastActiveAt : ws.createdAt;
11
+ const undatable = !(referenceAt > 0);
12
+ const ageDays = undatable ? 0 : Math.floor((now - referenceAt) / MS_PER_DAY);
13
+ return {
14
+ name: ws.name,
15
+ path: ws.path,
16
+ repoDirNames: ws.repoDirNames,
17
+ referenceAt,
18
+ fromSession: ws.lastActiveAt > 0,
19
+ ageDays,
20
+ undatable,
21
+ };
22
+ }
23
+ /**
24
+ * A workspace is a prune candidate when it is datable and its age meets the
25
+ * threshold. Undatable workspaces are never auto-selected — we won't delete
26
+ * something we can't put a date on. A future `referenceAt` (clock skew) yields
27
+ * a negative age and is therefore not stale.
28
+ */
29
+ function isStale(c, days) {
30
+ return !c.undatable && c.ageDays >= days;
31
+ }
32
+ /**
33
+ * Why a stale workspace should be held back from deletion, or null if it's safe.
34
+ * Unsafe = any repo has uncommitted changes (`!clean`) or unpushed commits
35
+ * (`ahead > 0`). With `includeDirty`, nothing is held back. An empty workspace
36
+ * (no repos) is always safe.
37
+ */
38
+ function protectionReason(statuses, includeDirty) {
39
+ if (includeDirty)
40
+ return null;
41
+ const dirty = statuses.filter((s) => !s.clean).length;
42
+ const unpushed = statuses.filter((s) => s.ahead > 0).length;
43
+ if (dirty === 0 && unpushed === 0)
44
+ return null;
45
+ const parts = [];
46
+ if (dirty > 0)
47
+ parts.push(`${dirty} repo${dirty === 1 ? '' : 's'} with uncommitted changes`);
48
+ if (unpushed > 0)
49
+ parts.push(`${unpushed} repo${unpushed === 1 ? '' : 's'} with unpushed commits`);
50
+ return parts.join(', ');
51
+ }
52
+ /**
53
+ * Partition stale candidates into prunable vs. protected, given a resolver that
54
+ * returns each workspace's per-repo git status. The resolver is only invoked
55
+ * for workspaces that actually have repos, so empty stale workspaces cost no git
56
+ * calls. Injecting the resolver keeps this function pure and unit-testable.
57
+ */
58
+ async function planPrune(staleCandidates, getStatuses, includeDirty) {
59
+ const prunable = [];
60
+ const protectedList = [];
61
+ for (const c of staleCandidates) {
62
+ const statuses = c.repoDirNames.length > 0 ? await getStatuses(c) : [];
63
+ const reason = protectionReason(statuses, includeDirty);
64
+ if (reason)
65
+ protectedList.push({ candidate: c, reason });
66
+ else
67
+ prunable.push(c);
68
+ }
69
+ return { prunable, protected: protectedList };
70
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nemus-cli/nemus",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "workspaces": [
5
5
  "packages/*"
6
6
  ],
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "homepage": "https://github.com/me-public/nemus#readme",
31
31
  "engines": {
32
- "node": ">=22.0.0",
32
+ "node": ">=22.13.0",
33
33
  "npm": ">=9.0.0"
34
34
  },
35
35
  "publishConfig": {
@@ -67,21 +67,18 @@
67
67
  "postinstall": "node scripts/postinstall.js"
68
68
  },
69
69
  "dependencies": {
70
+ "@inquirer/prompts": "^8.7.0",
70
71
  "@modelcontextprotocol/sdk": "^1.26.0",
71
72
  "cli-progress": "^3.12.0",
72
73
  "commander": "^15.0.0",
73
74
  "fuzzy": "^0.1.3",
74
75
  "ink": "^7.1.1",
75
- "inquirer": "^8.0.0",
76
- "inquirer-autocomplete-prompt": "^2.0.0",
77
76
  "markdown-table": "^3.0.3",
78
77
  "react": "^19.2.8",
79
78
  "zod": "^4.3.6"
80
79
  },
81
80
  "devDependencies": {
82
81
  "@types/cli-progress": "^3.11.0",
83
- "@types/inquirer": "^9.0.3",
84
- "@types/inquirer-autocomplete-prompt": "^2.0.0",
85
82
  "@types/node": "^26.3.0",
86
83
  "@types/react": "^19.2.2",
87
84
  "ts-node": "^10.9.1",
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // Emit GitHub Release notes for a given version by extracting that version's
3
+ // section from CHANGELOG.md (newest-first "Keep a Changelog" format), prepended
4
+ // with an install snippet and followed by a compare link to the previous tag.
5
+ //
6
+ // Usage: node scripts/release-notes.mjs <version> [repo]
7
+ // repo defaults to $GITHUB_REPOSITORY or "me-public/nemus".
8
+ //
9
+ // If the version has no CHANGELOG section, prints a minimal fallback (so a
10
+ // release is never blocked on a missing entry). Dependency-free; prints to
11
+ // stdout so the workflow can redirect it into `gh release --notes-file`.
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ const version = (process.argv[2] || '').trim();
15
+ const repo = (process.argv[3] || process.env.GITHUB_REPOSITORY || 'me-public/nemus').trim();
16
+ if (!version) {
17
+ process.stderr.write('usage: release-notes.mjs <version> [repo]\n');
18
+ process.exit(2);
19
+ }
20
+
21
+ const install = `\`\`\`bash\nnpm install -g @nemus-cli/nemus@${version}\n\`\`\``;
22
+
23
+ let body = '';
24
+ let prev = null;
25
+ try {
26
+ const changelog = readFileSync(new URL('../CHANGELOG.md', import.meta.url), 'utf8');
27
+ const lines = changelog.split('\n');
28
+ // Collect version headers in file order (newest first) with their line index.
29
+ const heads = [];
30
+ lines.forEach((line, i) => {
31
+ const m = line.match(/^## \[(\d+\.\d+\.\d+)\]/);
32
+ if (m) heads.push({ version: m[1], line: i });
33
+ });
34
+ const idx = heads.findIndex((h) => h.version === version);
35
+ if (idx !== -1) {
36
+ const start = heads[idx].line + 1;
37
+ const end = idx + 1 < heads.length ? heads[idx + 1].line : lines.length;
38
+ body = lines.slice(start, end).join('\n').trim();
39
+ // Newest-first: the NEXT header in the file is the previous release.
40
+ prev = idx + 1 < heads.length ? heads[idx + 1].version : null;
41
+ }
42
+ } catch {
43
+ // fall through to fallback
44
+ }
45
+
46
+ const compare = prev
47
+ ? `[\`v${prev}...v${version}\`](https://github.com/${repo}/compare/v${prev}...v${version})`
48
+ : `[\`v${version}\`](https://github.com/${repo}/releases/tag/v${version})`;
49
+
50
+ const parts = [`## Nemus v${version}`, '', install];
51
+ if (body) parts.push('', body);
52
+ parts.push('', '---', '', `**Full diff:** ${compare} · [Full changelog](https://github.com/${repo}/blob/main/CHANGELOG.md)`);
53
+
54
+ process.stdout.write(parts.join('\n') + '\n');
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: config
3
+ description: Read and write Nemus configuration non-interactively (get/set/unset/list/path/edit)
4
+ ---
5
+
6
+ Non-interactive configuration in `~/.nemus/config.json` (no wizard):
7
+ ```bash
8
+ nemus config list # all keys + resolved values (alias: ls; --json)
9
+ nemus config get <key> # print one value (--json)
10
+ nemus config set <key> <value> # set + validate + persist
11
+ nemus config unset <key> # revert a key to its default
12
+ nemus config path # print the config file path
13
+ ```
14
+
15
+ - Values are validated/coerced per key: booleans accept `true/false/yes/no/on/off/1/0`; enums (e.g. `cloneProtocol=ssh|https`) are checked. Invalid key/value exits non-zero.
16
+ - Common keys: `workspacesDir`, `githubOrg`, `cloneProtocol`, `aiAgent`, `primaryAgent`. Run `nemus config list` to see all.
17
+ - For a guided first-time setup, use `nemus configure` instead.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: nemus
3
3
  description: Manage multi-repo development workspaces — create, sync, branch, diff, clone, archive, analyze dependencies, run commands across repos, check status, clean up, delete, list org repos, search repos, list suites, remove repo, cache, suite management, sessions, history, generate docs, configure, MCP server, AI prompt. Use when working with workspaces, repos, branches, git operations across multiple repos, or the `nemus` CLI tool. NEVER use `git clone` directly — always use `nemus update` to add repos and `nemus create` to create workspaces.
4
- bashPattern: "\\bnemus\\s+(create|list|update|delete|sync|status|diff|run|go|doctor|analyze-deps|history|cleanup|remove-repo|archive|sessions|generate-docs|configure|configure-claude|ghq-status|tui|dashboard|dash|branch|suite|cache|mcp|--)\\b"
4
+ bashPattern: "\\bnemus\\s+(create|list|update|delete|prune|sync|status|diff|run|go|doctor|analyze-deps|reflect|retro|history|cleanup|remove-repo|archive|sessions|save-context|ctx|generate-docs|configure|config|configure-claude|ghq-status|completion|tui|dashboard|dash|branch|suite|cache|mcp|--)\\b"
5
5
  ---
6
6
 
7
7
  # Nemus
@@ -33,6 +33,7 @@ Global flags: `-f/--force-refresh` (skip repo cache), `-y/--yes` (skip prompts),
33
33
  | Create new workspace | [create-workspace](references/create-workspace.md) | `nemus create` | `c` |
34
34
  | Add repos to workspace | [update-workspace](references/update-workspace.md) | `nemus update` | `u` |
35
35
  | Delete workspace permanently | [delete-workspace](references/delete-workspace.md) | `nemus delete` | `d` |
36
+ | Prune inactive workspaces (safe) | [prune](references/prune.md) | `nemus prune` | — |
36
37
  | Archive / unarchive workspace | [archive-workspace](references/archive-workspace.md) | `nemus archive` | `a` |
37
38
  | List all workspaces | [list-workspaces](references/list-workspaces.md) | `nemus list` | `l` |
38
39
  | Navigate to workspace | [go](references/go.md) | `nemus go [name]` | — |
@@ -88,14 +89,18 @@ Global flags: `-f/--force-refresh` (skip repo cache), `-y/--yes` (skip prompts),
88
89
  | Resume Claude session | [sessions](references/sessions.md) | `nemus sessions` | `ses` |
89
90
  | Agent management dashboard | [dashboard](references/dashboard.md) | `nemus dashboard` | `dash` |
90
91
  | View operation history | [history](references/history.md) | `nemus history` | `h` |
92
+ | Save progress/context to a workspace | [save-context](references/save-context.md) | `nemus save-context` | `ctx` |
93
+ | Retrospective on recent sessions | [reflect](references/reflect.md) | `nemus reflect` | `retro` |
91
94
 
92
95
  ### Configuration & MCP
93
96
 
94
97
  | Intent | Reference | CLI |
95
98
  |---|---|---|
96
- | Configure Nemus | [configure](references/configure.md) | `nemus configure` |
99
+ | Configure Nemus (interactive wizard) | [configure](references/configure.md) | `nemus configure` |
100
+ | Get/set config non-interactively | [config](references/config.md) | `nemus config get\|set\|list\|edit` |
97
101
  | Configure Claude integration | [configure-claude](references/configure-claude.md) | `nemus configure-claude` |
98
102
  | Check ghq status | [ghq-status](references/ghq-status.md) | `nemus ghq-status` |
103
+ | Shell completions (bash/zsh/fish) | [completion](references/completion.md) | `nemus completion <shell>` |
99
104
  | Install / manage MCP server | [mcp](references/mcp.md) | `nemus mcp install\|status\|upgrade\|uninstall` |
100
105
 
101
106
  ### AI Assistant & TUI
@@ -0,0 +1,22 @@
1
+ # Shell Completions
2
+
3
+ `nemus completion <shell>` prints a completion script for `bash`, `zsh`, or
4
+ `fish`. It completes subcommands and, for workspace-scoped commands, live
5
+ workspace names (the script calls back into `nemus completion --workspaces`, so
6
+ completions stay fresh without regenerating). Registered for both `nemus` and
7
+ `nem`.
8
+
9
+ Install (pick your shell):
10
+ ```bash
11
+ # bash
12
+ nemus completion bash > /etc/bash_completion.d/nemus # or: >> ~/.bashrc
13
+
14
+ # zsh — save on your $fpath as _nemus
15
+ nemus completion zsh > "${fpath[1]}/_nemus"
16
+
17
+ # fish
18
+ nemus completion fish > ~/.config/fish/completions/nemus.fish
19
+ ```
20
+
21
+ Then restart the shell (or `source` the file). Requires a shell argument —
22
+ one of `bash|zsh|fish`.
@@ -0,0 +1,32 @@
1
+ # Config — non-interactive configuration
2
+
3
+ `nemus config` reads and writes `~/.nemus/config.json` without the interactive
4
+ `nemus configure` wizard. Ideal for scripting and for setting one value.
5
+
6
+ ```bash
7
+ nemus config list # show all keys + resolved values (alias: ls)
8
+ nemus config list --json # machine-readable
9
+ nemus config get <key> # print one value (--json for structured)
10
+ nemus config set <key> <value> # set + validate + persist
11
+ nemus config unset <key> # remove a key (revert to default)
12
+ nemus config path # print the config file path
13
+ nemus config edit # open the file in $VISUAL/$EDITOR (needs a TTY)
14
+ ```
15
+
16
+ Values are validated and coerced per key: booleans accept
17
+ `true/false/yes/no/on/off/1/0`; enums (e.g. `cloneProtocol` = `ssh|https`) are
18
+ checked. An unknown key or invalid value exits non-zero with a clear message.
19
+
20
+ Common keys: `workspacesDir`, `githubOrg`, `cloneProtocol`, `aiAgent`,
21
+ `primaryAgent`, `autoLaunchClaude`, `generateClaudeContext`, `installMcp`.
22
+ Run `nemus config list` to see them all.
23
+
24
+ ```bash
25
+ # examples
26
+ nemus config set githubOrg acme
27
+ nemus config set cloneProtocol https
28
+ nemus config get workspacesDir --json
29
+ ```
30
+
31
+ Pairs well with `--quiet` for scripts. For a guided first-time setup, use
32
+ `nemus configure` instead.
@@ -0,0 +1,44 @@
1
+ # Prune Inactive Workspaces
2
+
3
+ Bulk-delete workspaces with no recent activity. **Safe by default** — it holds
4
+ back any workspace with uncommitted or unpushed work.
5
+
6
+ 1. **Always preview first.** Show the user exactly what would be deleted (and
7
+ what is protected) before removing anything:
8
+ ```bash
9
+ nemus prune --days <n> --dry-run
10
+ ```
11
+ A workspace is *stale* when its most recent agent session — or, if it has no
12
+ session, its `createdAt` — is older than `--days` (default 30). Workspaces
13
+ with no date at all are never selected.
14
+
15
+ 2. **Review the two lists.** `prune` prints:
16
+ - **Protected** — stale workspaces skipped because a repo has uncommitted
17
+ changes or unpushed commits (with the reason). These are NOT deleted.
18
+ - **Prunable** — stale workspaces that are safe to remove.
19
+
20
+ 3. **Confirm with the user, then prune.** This permanently deletes the
21
+ workspace directories and every cloned repo inside them:
22
+ ```bash
23
+ nemus prune --days <n> # prompts for confirmation (default: No)
24
+ nemus prune --days <n> --yes # non-interactive (only when the user is sure)
25
+ ```
26
+
27
+ 4. **`--json`** gives a machine-readable plan (`{ prunable, protected }`) and,
28
+ like `--dry-run`, never deletes.
29
+
30
+ Deletions go through the same validated path as `nemus delete` (name allowlist +
31
+ path pinned inside the workspaces directory).
32
+
33
+ ## Flags
34
+
35
+ | Flag | Short | Description |
36
+ |---|---|---|
37
+ | `--days <n>` | `-d` | Stale after N days of inactivity (default 30) |
38
+ | `--dry-run` | | Show the plan without deleting anything |
39
+ | `--json` | | Output the plan as JSON (never deletes) |
40
+ | `--yes` | `-y` | Skip the confirmation prompt |
41
+ | `--include-dirty` | | Also prune workspaces with uncommitted/unpushed work (overrides the safety guard — use with care) |
42
+
43
+ > **Only** pass `--include-dirty` when the user has explicitly accepted losing
44
+ > uncommitted/unpushed work in the protected workspaces.
@@ -0,0 +1,43 @@
1
+ # Reflect — retrospective on recent sessions
2
+
3
+ `nemus reflect` (alias `retro`) reads your recent workspaces' agent session
4
+ transcripts and asks *your own* configured agent (claude/pi/opencode — no API key
5
+ of Nemus's) to recommend concrete setup improvements: skills to add, missing
6
+ `AGENTS.md`/context rules, missing connectivity/smoke tests, and prompt/workflow
7
+ habits — each with a priority and an example.
8
+
9
+ ```bash
10
+ nemus reflect # analyze the most recent workspaces (default 10)
11
+ nemus reflect --limit 20 # widen the window
12
+ nemus reflect --workspace <name> # analyze a single workspace
13
+ ```
14
+
15
+ Output / sharing:
16
+ ```bash
17
+ nemus reflect --json # structured report ({ ok:false, error } on failure)
18
+ nemus reflect --markdown > reflection.md # paste into an issue/PR
19
+ nemus reflect --group-by kind # group recommendations by kind (default: priority)
20
+ ```
21
+
22
+ Review saved reports (each run is saved under `~/.nemus/reflect/` unless
23
+ `--no-save`):
24
+ ```bash
25
+ nemus reflect history # list saved reports
26
+ nemus reflect show [id] # show one (id or id-prefix; defaults to latest)
27
+ ```
28
+
29
+ ## Flags
30
+
31
+ | Flag | Description |
32
+ |---|---|
33
+ | `--limit <n>` / `-n` | How many recent workspaces to analyze (default 10) |
34
+ | `--workspace <name>` / `-w` | Analyze a single workspace (ignores `--limit`) |
35
+ | `--json` | Structured JSON report to stdout |
36
+ | `--markdown` | Markdown report to stdout |
37
+ | `--group-by <how>` | `priority` (default) or `kind` |
38
+ | `--no-save` | Don't save the report to `~/.nemus/reflect/` |
39
+ | `--model` / `--thinking` | Judge model / pi thinking-level overrides |
40
+ | `--dry-run` | Print the assembled corpus + judge prompt without calling the agent |
41
+
42
+ Read-only and safe — it analyzes transcripts and prints advice; it changes no
43
+ repos. Use it to coach setup, not to modify anything.
@@ -0,0 +1,25 @@
1
+ # Save Context
2
+
3
+ `nemus save-context` (alias `ctx`) writes a progress summary to the workspace's
4
+ `CONTEXT.md`, so work survives `/clear` or a new session. Read `CONTEXT.md` back
5
+ at the start of a session to resume. The workspace defaults to the current
6
+ directory; pass `-w` to target another.
7
+
8
+ ```bash
9
+ nemus save-context -m "…" # save to the current workspace
10
+ nemus save-context -w <name> -m "…" # target a specific workspace
11
+ nemus save-context # interactive: prompts for the summary
12
+ nemus save-context -f notes.md --append # read from a file, append (don't replace)
13
+ ```
14
+
15
+ Use it to capture: what was done, what's in progress, key decisions, and the
16
+ next steps — before a context reset or when handing off.
17
+
18
+ ## Flags
19
+
20
+ | Flag | Short | Description |
21
+ |---|---|---|
22
+ | `--workspace <name>` | `-w` | Workspace name (default: current directory) |
23
+ | `--message <text>` | `-m` | Summary text to save (skips the prompt) |
24
+ | `--file <path>` | `-f` | Read the summary from a file |
25
+ | `--append` | | Append to existing context instead of replacing |