@nemus-cli/nemus 0.5.0 → 0.8.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.
- package/CHANGELOG.md +34 -0
- package/README.md +33 -0
- package/dist/commands/config.js +113 -0
- package/dist/commands/reflect.js +141 -20
- package/dist/program.js +2 -0
- package/dist/utils/config-schema.js +88 -0
- package/dist/utils/config.js +4 -2
- package/dist/utils/reflect.js +170 -1
- package/package.json +1 -1
- package/src/commands/config.ts +112 -0
- package/src/commands/reflect.ts +156 -20
- package/src/program.ts +2 -0
- package/src/utils/config-schema.test.ts +111 -0
- package/src/utils/config-schema.ts +112 -0
- package/src/utils/config.ts +4 -1
- package/src/utils/reflect.test.ts +129 -2
- package/src/utils/reflect.ts +201 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`nemus reflect history`** and **`nemus reflect show [id]`** to review saved
|
|
15
|
+
reports (under `~/.nemus/reflect/`) without re-running the judge. `show`
|
|
16
|
+
defaults to the latest, accepts an id or id-prefix, and supports
|
|
17
|
+
`--json` / `--markdown` / `--group-by`.
|
|
18
|
+
- **`nemus reflect --group-by kind|priority`** (default `priority`) to control
|
|
19
|
+
how recommendations are grouped in both the human and Markdown output.
|
|
20
|
+
|
|
21
|
+
## [0.7.0] - 2026-09-01
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`nemus reflect --markdown`** — render the reflection report as clean,
|
|
26
|
+
severity-grouped Markdown to stdout (pipe into a file or an issue:
|
|
27
|
+
`nemus reflect --markdown > reflection.md`). Fenced example snippets are
|
|
28
|
+
escaped so they can't break out of their own code block.
|
|
29
|
+
- The human `reflect` report now prints a **severity summary line**
|
|
30
|
+
(e.g. `2 high · 1 medium`).
|
|
31
|
+
|
|
32
|
+
## [0.6.0] - 2026-09-01
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **`nemus config` command** for non-interactive configuration:
|
|
37
|
+
`config get [key]`, `set <key> <value>`, `unset <key>`, `list` (alias `ls`),
|
|
38
|
+
and `path`. Values are validated and coerced per field (booleans accept
|
|
39
|
+
`true/false/yes/no/on/off/1/0`; enums like `cloneProtocol` are checked), an
|
|
40
|
+
unknown key or invalid value exits non-zero with a clear message, and
|
|
41
|
+
`get`/`list` support `--json`. Complements the interactive `configure` wizard
|
|
42
|
+
and pairs well with `--quiet` for scripting.
|
|
43
|
+
|
|
10
44
|
## [0.5.0] - 2026-09-01
|
|
11
45
|
|
|
12
46
|
### Added
|
package/README.md
CHANGED
|
@@ -243,6 +243,23 @@ The workspace-scoped ones (`status`/`doctor`/`analyze-deps`) need an explicit
|
|
|
243
243
|
workspace name with `--json` (they never prompt). On failure, `--json` prints a
|
|
244
244
|
parseable `{ "ok": false, "error": … }` to stdout and exits non-zero.
|
|
245
245
|
|
|
246
|
+
### Configuration
|
|
247
|
+
|
|
248
|
+
Run `nemus configure` for the interactive wizard, or manage settings
|
|
249
|
+
non-interactively (handy for scripts and dotfiles):
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
nemus config list # all keys, values, and descriptions
|
|
253
|
+
nemus config get cloneProtocol # print one value (raw, for scripts)
|
|
254
|
+
nemus config set cloneProtocol https # validated + coerced per key
|
|
255
|
+
nemus config set autoReportBugs yes # booleans accept true/false/yes/no/on/off/1/0
|
|
256
|
+
nemus config unset githubOrg # reset a key to its default
|
|
257
|
+
nemus config path # print the config file location
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
`get`/`list` also accept `--json`. An unknown key or an invalid value exits
|
|
261
|
+
non-zero with a clear message (e.g. `cloneProtocol must be one of: ssh, https`).
|
|
262
|
+
|
|
246
263
|
### Global flags
|
|
247
264
|
|
|
248
265
|
- `--no-color` — disable ANSI color. Nemus also honors the standard
|
|
@@ -294,9 +311,25 @@ nemus snapshot restore <id> # (sr)
|
|
|
294
311
|
nemus reflect # (retro) analyze your last 10 workspaces' sessions
|
|
295
312
|
nemus reflect --limit 5 # narrow the window
|
|
296
313
|
nemus reflect --json # structured report for tooling
|
|
314
|
+
nemus reflect --markdown # Markdown report (grouped by severity) to paste/save
|
|
315
|
+
nemus reflect --group-by kind # group recommendations by kind instead of priority
|
|
297
316
|
nemus reflect --dry-run # show what the judge sees, without calling the agent
|
|
298
317
|
```
|
|
299
318
|
|
|
319
|
+
`--markdown` writes a clean, severity-grouped report to stdout — pipe it into a
|
|
320
|
+
file or an issue: `nemus reflect --markdown > reflection.md`. `--group-by
|
|
321
|
+
kind|priority` (default `priority`) controls how recommendations are grouped in
|
|
322
|
+
both the human and Markdown output.
|
|
323
|
+
|
|
324
|
+
Every run is saved under `~/.nemus/reflect/`. Review past reports without
|
|
325
|
+
re-running the judge:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
nemus reflect history # list saved reports, newest first (--json)
|
|
329
|
+
nemus reflect show # print the latest saved report
|
|
330
|
+
nemus reflect show <id> # a specific one (--markdown / --json / --group-by)
|
|
331
|
+
```
|
|
332
|
+
|
|
300
333
|
`reflect` reads your recent agent **session transcripts** (Claude + pi), distills
|
|
301
334
|
the prompts you sent, the failures the agent hit, and the tools it used, then asks
|
|
302
335
|
**your own configured agent** (LLM-as-a-judge — no extra API key) to recommend
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerConfigCommand = registerConfigCommand;
|
|
4
|
+
const config_1 = require("../utils/config");
|
|
5
|
+
const config_schema_1 = require("../utils/config-schema");
|
|
6
|
+
const output_1 = require("../utils/output");
|
|
7
|
+
const logger_1 = require("../utils/logger");
|
|
8
|
+
const colors_1 = require("../utils/colors");
|
|
9
|
+
/**
|
|
10
|
+
* Non-interactive config management: `nemus config get/set/unset/list/path`.
|
|
11
|
+
* Complements the interactive `configure` wizard and is script-friendly —
|
|
12
|
+
* `get`/`list` write DATA to stdout (raw value, or JSON with --json), logs go to
|
|
13
|
+
* stderr. Values are validated/coerced against config-schema.ts.
|
|
14
|
+
*/
|
|
15
|
+
function registerConfigCommand(parent) {
|
|
16
|
+
const config = parent.command('config').description('Get or set Nemus configuration');
|
|
17
|
+
config
|
|
18
|
+
.command('get')
|
|
19
|
+
.description('Print a config value (or all values with no key)')
|
|
20
|
+
.argument('[key]', 'Config key')
|
|
21
|
+
.option('--json', 'Output as JSON')
|
|
22
|
+
.action((key, opts) => {
|
|
23
|
+
const cfg = (0, config_1.getUserConfig)();
|
|
24
|
+
if (key === undefined) {
|
|
25
|
+
printAll(cfg, opts.json);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!(0, config_schema_1.isConfigKey)(key)) {
|
|
29
|
+
if (opts.json)
|
|
30
|
+
(0, output_1.outputJsonError)(`Unknown config key "${key}"`);
|
|
31
|
+
else
|
|
32
|
+
(0, logger_1.logError)(`Unknown config key "${key}". Run "nemus config list" to see valid keys.`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const value = cfg[key];
|
|
37
|
+
if (opts.json)
|
|
38
|
+
(0, output_1.outputJson)({ key, value });
|
|
39
|
+
else
|
|
40
|
+
process.stdout.write((0, config_schema_1.formatConfigValue)(value) + '\n');
|
|
41
|
+
});
|
|
42
|
+
config
|
|
43
|
+
.command('set')
|
|
44
|
+
.description('Set a config value')
|
|
45
|
+
.argument('<key>', 'Config key')
|
|
46
|
+
.argument('<value>', 'New value')
|
|
47
|
+
.option('--json', 'Output as JSON')
|
|
48
|
+
.action((key, value, opts) => {
|
|
49
|
+
const result = (0, config_schema_1.applyConfigSet)((0, config_1.getUserConfig)(), key, value);
|
|
50
|
+
if (!result.ok) {
|
|
51
|
+
if (opts.json)
|
|
52
|
+
(0, output_1.outputJsonError)(result.error);
|
|
53
|
+
else
|
|
54
|
+
(0, logger_1.logError)(result.error);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
(0, config_1.saveUserConfig)(result.next);
|
|
59
|
+
if (opts.json)
|
|
60
|
+
(0, output_1.outputJson)({ ok: true, key, value: result.value });
|
|
61
|
+
else
|
|
62
|
+
(0, logger_1.logSuccess)(`Set ${(0, colors_1.colorize)(key, 'cyan')} = ${(0, config_schema_1.formatConfigValue)(result.value)}`);
|
|
63
|
+
});
|
|
64
|
+
config
|
|
65
|
+
.command('unset')
|
|
66
|
+
.description('Reset a config value to its default')
|
|
67
|
+
.argument('<key>', 'Config key')
|
|
68
|
+
.option('--json', 'Output as JSON')
|
|
69
|
+
.action((key, opts) => {
|
|
70
|
+
const result = (0, config_schema_1.applyConfigUnset)((0, config_1.getUserConfig)(), key);
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
if (opts.json)
|
|
73
|
+
(0, output_1.outputJsonError)(result.error);
|
|
74
|
+
else
|
|
75
|
+
(0, logger_1.logError)(result.error);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
(0, config_1.saveUserConfig)(result.next);
|
|
80
|
+
if (opts.json)
|
|
81
|
+
(0, output_1.outputJson)({ ok: true, key, value: result.value });
|
|
82
|
+
else
|
|
83
|
+
(0, logger_1.logSuccess)(`Reset ${(0, colors_1.colorize)(key, 'cyan')} to default (${(0, config_schema_1.formatConfigValue)(result.value)})`);
|
|
84
|
+
});
|
|
85
|
+
config
|
|
86
|
+
.command('list')
|
|
87
|
+
.alias('ls')
|
|
88
|
+
.description('List all config keys and current values')
|
|
89
|
+
.option('--json', 'Output as JSON')
|
|
90
|
+
.action((opts) => printAll((0, config_1.getUserConfig)(), opts.json));
|
|
91
|
+
config
|
|
92
|
+
.command('path')
|
|
93
|
+
.description('Print the path to the config file')
|
|
94
|
+
.action(() => {
|
|
95
|
+
process.stdout.write(config_1.CONFIG_PATH + '\n');
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function printAll(cfg, json) {
|
|
99
|
+
if (json) {
|
|
100
|
+
const values = {};
|
|
101
|
+
for (const key of config_schema_1.CONFIG_KEYS)
|
|
102
|
+
values[key] = cfg[key];
|
|
103
|
+
(0, output_1.outputJson)({ path: config_1.CONFIG_PATH, values });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const width = Math.max(...config_schema_1.CONFIG_KEYS.map((k) => k.length));
|
|
107
|
+
console.log((0, colors_1.colorize)('Nemus configuration', 'bright') + (0, colors_1.colorize)(` (${config_1.CONFIG_PATH})`, 'dim'));
|
|
108
|
+
for (const key of config_schema_1.CONFIG_KEYS) {
|
|
109
|
+
const val = (0, config_schema_1.formatConfigValue)(cfg[key]);
|
|
110
|
+
const shown = val === '' ? (0, colors_1.colorize)('(empty)', 'dim') : val;
|
|
111
|
+
console.log(` ${key.padEnd(width)} ${shown} ${(0, colors_1.colorize)(config_schema_1.CONFIG_SCHEMA[key].describe, 'dim')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/commands/reflect.js
CHANGED
|
@@ -11,22 +11,55 @@ function registerReflectCommand(parent) {
|
|
|
11
11
|
parent
|
|
12
12
|
.command('reflect')
|
|
13
13
|
.alias('retro')
|
|
14
|
-
.description('Analyze
|
|
14
|
+
.description('Analyze recent sessions for improvements, or review saved reports ("history"/"show")')
|
|
15
|
+
.argument('[subcommand]', '"history" or "show" — omit to run a new analysis')
|
|
16
|
+
.argument('[id]', 'report id when using "show" (default: latest)')
|
|
15
17
|
.option('-n, --limit <n>', 'How many recent workspaces to analyze', '10')
|
|
16
18
|
.option('-w, --workspace <name>', 'Analyze a single workspace by name (ignores --limit)')
|
|
17
19
|
.option('--model <model>', 'Judge model override (agent-native pattern/id)')
|
|
18
20
|
.option('--thinking <level>', 'Judge thinking level for pi: off|minimal|low|medium|high|xhigh|max')
|
|
19
21
|
.option('--json', 'Output the report as JSON')
|
|
22
|
+
.option('--markdown', 'Output the report as Markdown (paste into an issue/PR)')
|
|
23
|
+
.option('--group-by <how>', 'Group recommendations by: priority | kind', 'priority')
|
|
20
24
|
.option('--no-save', 'Do not save the report to ~/.nemus/reflect/')
|
|
21
25
|
.option('--dry-run', 'Print the assembled corpus + judge prompt without calling the agent')
|
|
22
|
-
.
|
|
23
|
-
|
|
26
|
+
.addHelpText('after', '\nSaved reports:\n nemus reflect history List saved reports (newest first)\n nemus reflect show [id] Print a saved report (default: latest)\n')
|
|
27
|
+
// history/show are positional (not commander subcommands) on purpose: as
|
|
28
|
+
// subcommands they would share --json/--markdown/--group-by with this parent
|
|
29
|
+
// command, and the only commander fix (enablePositionalOptions on the root)
|
|
30
|
+
// breaks global flags placed after a subcommand (e.g. `nemus list --quiet`).
|
|
31
|
+
.action(async (subcommand, id, opts) => {
|
|
32
|
+
if (subcommand === 'history')
|
|
33
|
+
return handleHistory(opts);
|
|
34
|
+
if (subcommand === 'show')
|
|
35
|
+
return handleShow(id, opts);
|
|
36
|
+
if (subcommand !== undefined) {
|
|
37
|
+
const msg = `Unknown reflect subcommand "${subcommand}" (expected "history" or "show").`;
|
|
38
|
+
if (opts.json)
|
|
39
|
+
(0, output_1.outputJsonError)(msg);
|
|
40
|
+
else
|
|
41
|
+
(0, logger_1.logError)(msg);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return handleReflect(opts);
|
|
24
45
|
});
|
|
25
46
|
}
|
|
47
|
+
/** Validate --group-by; returns the value or exits non-zero with a clear error. */
|
|
48
|
+
function resolveGroupBy(raw, json) {
|
|
49
|
+
if (raw === undefined || raw === 'priority' || raw === 'kind')
|
|
50
|
+
return (raw ?? 'priority');
|
|
51
|
+
const msg = `--group-by must be "priority" or "kind"; got "${raw}"`;
|
|
52
|
+
if (json)
|
|
53
|
+
(0, output_1.outputJsonError)(msg);
|
|
54
|
+
else
|
|
55
|
+
(0, logger_1.logError)(msg);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
26
58
|
async function handleReflect(opts) {
|
|
27
59
|
const limit = Math.max(1, Number.parseInt(opts.limit ?? '10', 10) || 10);
|
|
60
|
+
const groupBy = resolveGroupBy(opts.groupBy, opts.json);
|
|
28
61
|
try {
|
|
29
|
-
const showProgress = !opts.json && !opts.dryRun;
|
|
62
|
+
const showProgress = !opts.json && !opts.markdown && !opts.dryRun;
|
|
30
63
|
if (showProgress) {
|
|
31
64
|
(0, logger_1.logStep)(opts.workspace
|
|
32
65
|
? `Analyzing workspace ${(0, colors_1.colorize)(opts.workspace, 'cyan')}…`
|
|
@@ -66,7 +99,7 @@ async function handleReflect(opts) {
|
|
|
66
99
|
const timeoutMs = Number.parseInt(process.env.NEMUS_JUDGE_TIMEOUT_MS ?? '', 10) || undefined;
|
|
67
100
|
const model = opts.model ?? process.env.NEMUS_JUDGE_MODEL ?? undefined;
|
|
68
101
|
const thinking = opts.thinking ?? process.env.NEMUS_JUDGE_THINKING ?? undefined;
|
|
69
|
-
const stopSpinner = opts.json
|
|
102
|
+
const stopSpinner = opts.json || opts.markdown
|
|
70
103
|
? () => { }
|
|
71
104
|
: startSpinner(`Judging ${withSessions} session(s) with your configured agent (this can take a minute)…`);
|
|
72
105
|
let parsed;
|
|
@@ -95,7 +128,20 @@ async function handleReflect(opts) {
|
|
|
95
128
|
(0, output_1.outputJson)({ analyzed: withSessions, workspaces: corpus.workspaces.length, ...report, savedTo });
|
|
96
129
|
return;
|
|
97
130
|
}
|
|
98
|
-
|
|
131
|
+
if (opts.markdown) {
|
|
132
|
+
// DATA channel: markdown to stdout, nothing else (a 'Saved report' note
|
|
133
|
+
// would corrupt a redirected .md file), so surface the path on stderr.
|
|
134
|
+
process.stdout.write((0, reflect_1.renderReportMarkdown)(report, {
|
|
135
|
+
analyzed: withSessions,
|
|
136
|
+
workspaces: corpus.workspaces.length,
|
|
137
|
+
workspace: opts.workspace,
|
|
138
|
+
generatedAt: new Date().toISOString(),
|
|
139
|
+
}, groupBy));
|
|
140
|
+
if (savedTo)
|
|
141
|
+
(0, logger_1.logInfo)(`Saved report to ${(0, colors_1.colorize)(savedTo, 'dim')}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
printReport(report, corpus.workspaces.length, withSessions, groupBy);
|
|
99
145
|
if (savedTo)
|
|
100
146
|
(0, logger_1.logInfo)(`Saved report to ${(0, colors_1.colorize)(savedTo, 'dim')}`);
|
|
101
147
|
}
|
|
@@ -164,7 +210,7 @@ function priorityBadge(p) {
|
|
|
164
210
|
return (0, colors_1.colorize)('● med', 'yellow');
|
|
165
211
|
return (0, colors_1.colorize)('● low', 'gray');
|
|
166
212
|
}
|
|
167
|
-
function printReport(report, workspaces, analyzed) {
|
|
213
|
+
function printReport(report, workspaces, analyzed, groupBy = 'priority') {
|
|
168
214
|
console.log('');
|
|
169
215
|
console.log((0, colors_1.colorize)(' Reflection', 'bright') + (0, colors_1.colorize)(` (${analyzed} sessions across ${workspaces} workspaces)`, 'dim'));
|
|
170
216
|
console.log((0, colors_1.colorize)(' ' + '─'.repeat(56), 'dim'));
|
|
@@ -175,19 +221,94 @@ function printReport(report, workspaces, analyzed) {
|
|
|
175
221
|
console.log('\n ' + (0, colors_1.colorize)('No specific recommendations — looks solid.', 'green') + '\n');
|
|
176
222
|
return;
|
|
177
223
|
}
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
console.log(`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
224
|
+
console.log('\n ' + (0, colors_1.colorize)((0, reflect_1.severitySummary)(report.recommendations), 'dim'));
|
|
225
|
+
for (const group of (0, reflect_1.groupRecommendations)(report.recommendations, groupBy)) {
|
|
226
|
+
console.log('\n ' + (0, colors_1.colorize)(group.heading, 'bright'));
|
|
227
|
+
for (const r of group.recs) {
|
|
228
|
+
const target = r.target ? (0, colors_1.colorize)(` [${r.target}]`, 'cyan') : '';
|
|
229
|
+
// Under a kind heading show the priority badge; under a priority heading
|
|
230
|
+
// show the kind label (the heading conveys the other axis).
|
|
231
|
+
const lead = groupBy === 'kind' ? priorityBadge(r.priority) : (0, colors_1.colorize)(KIND_LABEL[r.kind], 'bright');
|
|
232
|
+
console.log(` ${lead} ${r.title}${target}`);
|
|
233
|
+
if (r.detail)
|
|
234
|
+
console.log(` ${r.detail.replace(/\n/g, '\n ')}`);
|
|
235
|
+
if (r.example) {
|
|
236
|
+
console.log((0, colors_1.colorize)(' example:', 'dim'));
|
|
237
|
+
console.log((0, colors_1.colorize)(r.example.replace(/^/gm, ' '), 'dim'));
|
|
238
|
+
}
|
|
190
239
|
}
|
|
191
|
-
console.log('');
|
|
192
240
|
}
|
|
241
|
+
console.log('');
|
|
242
|
+
}
|
|
243
|
+
async function handleHistory(opts) {
|
|
244
|
+
const reports = await (0, reflect_1.listSavedReports)();
|
|
245
|
+
if (opts.json) {
|
|
246
|
+
(0, output_1.outputJson)({
|
|
247
|
+
count: reports.length,
|
|
248
|
+
reports: reports.map((r) => ({
|
|
249
|
+
id: r.id,
|
|
250
|
+
generatedAt: r.generatedAt,
|
|
251
|
+
analyzed: r.analyzed,
|
|
252
|
+
workspaces: r.workspaces,
|
|
253
|
+
workspace: r.workspace,
|
|
254
|
+
recommendations: r.report.recommendations.length,
|
|
255
|
+
severity: (0, reflect_1.severitySummary)(r.report.recommendations),
|
|
256
|
+
})),
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (reports.length === 0) {
|
|
261
|
+
(0, logger_1.logInfo)('No saved reflection reports yet. Run `nemus reflect` to create one.');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log((0, colors_1.colorize)(' Saved reflections', 'bright') + (0, colors_1.colorize)(` (${reports.length})`, 'dim'));
|
|
266
|
+
console.log((0, colors_1.colorize)(' ' + '─'.repeat(56), 'dim'));
|
|
267
|
+
for (const r of reports) {
|
|
268
|
+
const when = r.generatedAt ? new Date(r.generatedAt).toLocaleString() : r.id;
|
|
269
|
+
const scope = r.workspace ? (0, colors_1.colorize)(` ${r.workspace}`, 'cyan') : (0, colors_1.colorize)(` ${r.analyzed} sessions`, 'dim');
|
|
270
|
+
const sev = r.report.recommendations.length
|
|
271
|
+
? (0, colors_1.colorize)(` ${(0, reflect_1.severitySummary)(r.report.recommendations)}`, 'dim')
|
|
272
|
+
: (0, colors_1.colorize)(' no recs', 'green');
|
|
273
|
+
console.log(` ${(0, colors_1.colorize)(r.id, 'bright')}${scope}${sev}`);
|
|
274
|
+
console.log((0, colors_1.colorize)(` ${when}`, 'dim'));
|
|
275
|
+
}
|
|
276
|
+
console.log('');
|
|
277
|
+
console.log((0, colors_1.colorize)(' nemus reflect show <id> (or `latest`)', 'dim'));
|
|
278
|
+
}
|
|
279
|
+
async function handleShow(id, opts) {
|
|
280
|
+
const groupBy = resolveGroupBy(opts.groupBy, opts.json);
|
|
281
|
+
const matches = (0, reflect_1.findSavedMatches)(await (0, reflect_1.listSavedReports)(), id);
|
|
282
|
+
const saved = matches[0];
|
|
283
|
+
if (!saved) {
|
|
284
|
+
const msg = id && id !== 'latest'
|
|
285
|
+
? `No saved report matching "${id}". Run "nemus reflect history" to list them.`
|
|
286
|
+
: 'No saved reflection reports yet. Run "nemus reflect" to create one.';
|
|
287
|
+
if (opts.json)
|
|
288
|
+
(0, output_1.outputJsonError)(msg);
|
|
289
|
+
else
|
|
290
|
+
(0, logger_1.logError)(msg);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
// An id-prefix that matches several reports resolves to the newest — say so
|
|
294
|
+
// (stderr only, so --json/--markdown stdout stays clean) rather than quietly
|
|
295
|
+
// showing a possibly-unintended report. An exact id / "latest" never multi-matches.
|
|
296
|
+
if (matches.length > 1 && !opts.json) {
|
|
297
|
+
(0, logger_1.logWarning)(`"${id}" matched ${matches.length} reports; showing the newest (${saved.id}). Use a longer id to disambiguate.`);
|
|
298
|
+
}
|
|
299
|
+
const meta = {
|
|
300
|
+
analyzed: saved.analyzed,
|
|
301
|
+
workspaces: saved.workspaces,
|
|
302
|
+
workspace: saved.workspace,
|
|
303
|
+
generatedAt: saved.generatedAt,
|
|
304
|
+
};
|
|
305
|
+
if (opts.json) {
|
|
306
|
+
(0, output_1.outputJson)({ id: saved.id, ...meta, ...saved.report });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (opts.markdown) {
|
|
310
|
+
process.stdout.write((0, reflect_1.renderReportMarkdown)(saved.report, meta, groupBy));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
printReport(saved.report, saved.workspaces, saved.analyzed, groupBy);
|
|
193
314
|
}
|
package/dist/program.js
CHANGED
|
@@ -84,6 +84,7 @@ const archive_1 = require("./commands/archive");
|
|
|
84
84
|
const sessions_1 = require("./commands/sessions");
|
|
85
85
|
const generate_docs_1 = require("./commands/generate-docs");
|
|
86
86
|
const configure_1 = require("./commands/configure");
|
|
87
|
+
const config_1 = require("./commands/config");
|
|
87
88
|
const configure_claude_1 = require("./commands/configure-claude");
|
|
88
89
|
const ghq_status_1 = require("./commands/ghq-status");
|
|
89
90
|
const save_context_1 = require("./commands/save-context");
|
|
@@ -109,6 +110,7 @@ const reflect_1 = require("./commands/reflect");
|
|
|
109
110
|
(0, sessions_1.registerSessionsCommand)(exports.program);
|
|
110
111
|
(0, generate_docs_1.registerGenerateDocsCommand)(exports.program);
|
|
111
112
|
(0, configure_1.registerConfigureCommand)(exports.program);
|
|
113
|
+
(0, config_1.registerConfigCommand)(exports.program);
|
|
112
114
|
(0, configure_claude_1.registerConfigureClaudeCommand)(exports.program);
|
|
113
115
|
(0, ghq_status_1.registerGhqStatusCommand)(exports.program);
|
|
114
116
|
(0, save_context_1.registerSaveContextCommand)(exports.program);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONFIG_KEYS = exports.CONFIG_SCHEMA = void 0;
|
|
4
|
+
exports.isConfigKey = isConfigKey;
|
|
5
|
+
exports.parseConfigValue = parseConfigValue;
|
|
6
|
+
exports.applyConfigSet = applyConfigSet;
|
|
7
|
+
exports.applyConfigUnset = applyConfigUnset;
|
|
8
|
+
exports.formatConfigValue = formatConfigValue;
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const AGENT_VALUES = ['claude', 'pi', 'opencode', 'codex', 'gemini'];
|
|
11
|
+
exports.CONFIG_SCHEMA = {
|
|
12
|
+
workspacesDir: { type: 'string', describe: 'Directory where workspaces are created' },
|
|
13
|
+
githubOrg: { type: 'string', allowEmpty: true, describe: 'Default GitHub org for repo lookups' },
|
|
14
|
+
cloneProtocol: { type: 'enum', values: ['ssh', 'https'], describe: 'Protocol used to clone repos' },
|
|
15
|
+
aiAgent: {
|
|
16
|
+
type: 'enum',
|
|
17
|
+
values: [...AGENT_VALUES, 'both', 'auto'],
|
|
18
|
+
describe: 'AI agent(s) to integrate with',
|
|
19
|
+
},
|
|
20
|
+
primaryAgent: {
|
|
21
|
+
type: 'enum',
|
|
22
|
+
values: [...AGENT_VALUES, 'auto'],
|
|
23
|
+
describe: 'Agent launched when opening a workspace',
|
|
24
|
+
},
|
|
25
|
+
autoLaunchClaude: { type: 'boolean', describe: 'Auto-launch the agent after creating a workspace' },
|
|
26
|
+
generateClaudeContext: { type: 'boolean', describe: 'Generate agent context files (AGENTS.md)' },
|
|
27
|
+
installMcp: { type: 'boolean', describe: 'Install the MCP server during configure' },
|
|
28
|
+
piWorkspaceInputStatus: { type: 'boolean', describe: "Show workspace status in Pi's input area" },
|
|
29
|
+
claudeWorkspaceStatusLine: { type: 'boolean', describe: "Show workspace table in Claude's status line" },
|
|
30
|
+
autoReportBugs: { type: 'boolean', describe: 'Auto-file a GitHub issue when a command crashes' },
|
|
31
|
+
};
|
|
32
|
+
exports.CONFIG_KEYS = Object.keys(exports.CONFIG_SCHEMA).sort();
|
|
33
|
+
const TRUE_WORDS = new Set(['true', '1', 'yes', 'on', 'y']);
|
|
34
|
+
const FALSE_WORDS = new Set(['false', '0', 'no', 'off', 'n']);
|
|
35
|
+
/** True if `key` is a writable config key. */
|
|
36
|
+
function isConfigKey(key) {
|
|
37
|
+
return Object.prototype.hasOwnProperty.call(exports.CONFIG_SCHEMA, key);
|
|
38
|
+
}
|
|
39
|
+
/** Validate + coerce a raw string for `key` into the field's typed value. */
|
|
40
|
+
function parseConfigValue(key, raw) {
|
|
41
|
+
const spec = exports.CONFIG_SCHEMA[key];
|
|
42
|
+
if (spec.type === 'boolean') {
|
|
43
|
+
const v = raw.trim().toLowerCase();
|
|
44
|
+
if (TRUE_WORDS.has(v))
|
|
45
|
+
return { ok: true, value: true };
|
|
46
|
+
if (FALSE_WORDS.has(v))
|
|
47
|
+
return { ok: true, value: false };
|
|
48
|
+
return { ok: false, error: `${key} expects a boolean (true/false); got "${raw}"` };
|
|
49
|
+
}
|
|
50
|
+
if (spec.type === 'enum') {
|
|
51
|
+
// Enum values are all lowercase, so normalize input like booleans do —
|
|
52
|
+
// `HTTPS` / ` https ` should resolve to the canonical value, not fail.
|
|
53
|
+
const v = raw.trim().toLowerCase();
|
|
54
|
+
if (spec.values.includes(v)) {
|
|
55
|
+
return { ok: true, value: v };
|
|
56
|
+
}
|
|
57
|
+
return { ok: false, error: `${key} must be one of: ${spec.values.join(', ')}; got "${raw}"` };
|
|
58
|
+
}
|
|
59
|
+
// string: trim surrounding whitespace (a stray space in a path/org is almost
|
|
60
|
+
// always a mistake), but preserve case.
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
if (!spec.allowEmpty && trimmed === '') {
|
|
63
|
+
return { ok: false, error: `${key} cannot be empty` };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, value: trimmed };
|
|
66
|
+
}
|
|
67
|
+
/** Apply a `set` to a config object, returning a NEW config or an error. Pure. */
|
|
68
|
+
function applyConfigSet(current, key, raw) {
|
|
69
|
+
if (!isConfigKey(key)) {
|
|
70
|
+
return { ok: false, error: `Unknown config key "${key}". Run "nemus config list" to see valid keys.` };
|
|
71
|
+
}
|
|
72
|
+
const parsed = parseConfigValue(key, raw);
|
|
73
|
+
if (!parsed.ok)
|
|
74
|
+
return parsed;
|
|
75
|
+
return { ok: true, next: { ...current, [key]: parsed.value }, value: parsed.value };
|
|
76
|
+
}
|
|
77
|
+
/** Reset a key to its default value, returning a NEW config or an error. Pure. */
|
|
78
|
+
function applyConfigUnset(current, key) {
|
|
79
|
+
if (!isConfigKey(key)) {
|
|
80
|
+
return { ok: false, error: `Unknown config key "${key}". Run "nemus config list" to see valid keys.` };
|
|
81
|
+
}
|
|
82
|
+
const value = config_1.CONFIG_DEFAULTS[key];
|
|
83
|
+
return { ok: true, next: { ...current, [key]: value }, value };
|
|
84
|
+
}
|
|
85
|
+
/** Render a config value for plain (scriptable) stdout output. */
|
|
86
|
+
function formatConfigValue(value) {
|
|
87
|
+
return typeof value === 'boolean' ? String(value) : String(value ?? '');
|
|
88
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.config = exports.CLONE_MAX_BUFFER = exports.CLONE_TIMEOUT_MS = exports.CONFIG_PATH = exports.META_FILENAME = exports.SUITES_FILE = exports.HISTORY_FILE = exports.CACHE_DIR = exports.WORKSPACES_DIR = void 0;
|
|
36
|
+
exports.config = exports.CLONE_MAX_BUFFER = exports.CLONE_TIMEOUT_MS = exports.CONFIG_PATH = exports.META_FILENAME = exports.SUITES_FILE = exports.HISTORY_FILE = exports.CACHE_DIR = exports.WORKSPACES_DIR = exports.CONFIG_DEFAULTS = void 0;
|
|
37
37
|
exports.getUserConfig = getUserConfig;
|
|
38
38
|
exports.getPackageVersion = getPackageVersion;
|
|
39
39
|
exports.getCloneUrl = getCloneUrl;
|
|
@@ -77,7 +77,7 @@ const CACHE_DIR_RESOLVED = process.env.NEMUS_CACHE_DIR || process.env.WORKSPACE_
|
|
|
77
77
|
migrateLegacyCacheDir(CACHE_DIR_RESOLVED);
|
|
78
78
|
// Config file lives inside the cache dir.
|
|
79
79
|
const CONFIG_FILE = path.join(CACHE_DIR_RESOLVED, 'config.json');
|
|
80
|
-
|
|
80
|
+
exports.CONFIG_DEFAULTS = {
|
|
81
81
|
workspacesDir: path.join(HOME_DIR, 'workspaces'),
|
|
82
82
|
githubOrg: '',
|
|
83
83
|
autoLaunchClaude: true,
|
|
@@ -90,6 +90,8 @@ const DEFAULTS = {
|
|
|
90
90
|
claudeWorkspaceStatusLine: true,
|
|
91
91
|
autoReportBugs: false,
|
|
92
92
|
};
|
|
93
|
+
// Internal alias retained for the many references below.
|
|
94
|
+
const DEFAULTS = exports.CONFIG_DEFAULTS;
|
|
93
95
|
function loadConfigFileSync() {
|
|
94
96
|
try {
|
|
95
97
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|