@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 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
+ }
@@ -11,22 +11,55 @@ function registerReflectCommand(parent) {
11
11
  parent
12
12
  .command('reflect')
13
13
  .alias('retro')
14
- .description('Analyze your recent workspace sessions and suggest skill/prompt/context improvements (LLM-as-a-judge)')
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
- .action(async (opts) => {
23
- await handleReflect(opts);
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
- printReport(report, corpus.workspaces.length, withSessions);
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
- // High priority first.
179
- const order = { high: 0, medium: 1, low: 2 };
180
- const recs = [...report.recommendations].sort((a, b) => order[a.priority] - order[b.priority]);
181
- console.log('');
182
- for (const r of recs) {
183
- const target = r.target ? (0, colors_1.colorize)(` [${r.target}]`, 'cyan') : '';
184
- console.log(` ${priorityBadge(r.priority)} ${(0, colors_1.colorize)(KIND_LABEL[r.kind], 'bright')} ${r.title}${target}`);
185
- if (r.detail)
186
- console.log(` ${r.detail.replace(/\n/g, '\n ')}`);
187
- if (r.example) {
188
- console.log((0, colors_1.colorize)(' example:', 'dim'));
189
- console.log((0, colors_1.colorize)(r.example.replace(/^/gm, ' '), 'dim'));
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
+ }
@@ -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
- const DEFAULTS = {
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');