@chamba/claude-extras 0.1.0 → 0.2.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 (3) hide show
  1. package/README.md +91 -0
  2. package/dist/cli.js +414 -9
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -26,6 +26,97 @@ existing files and preserves any other MCP servers.
26
26
 
27
27
  Then, in Claude Code: `/orq add a health check endpoint`
28
28
 
29
+ ## Configuration: per-agent model + effort
30
+
31
+ chamba lets you pick which **model** and **effort** each **role** uses
32
+ (orchestrator, planner, reviewer, implementer, tester, summarizer, researcher).
33
+
34
+ > **chamba never calls these models.** This config is declarative metadata. For
35
+ > Claude Code it's written into each subagent's frontmatter (`model:` + `effort:`)
36
+ > in `~/.claude/agents/*.md`; Claude Code is what runs the model. Other editors read
37
+ > the same config through the MCP tool `chamba_get_agent_config`. No API keys, ever.
38
+
39
+ ### The recommended defaults (and why)
40
+
41
+ The philosophy: **critical reasoning gets powerful models, mechanical execution gets
42
+ fast/cheap ones.** These ship pre-configured — you only change what you want.
43
+
44
+ | Role | Default model | Effort | Why |
45
+ |---|---|---|---|
46
+ | **orchestrator** | `claude-opus-4-8` | high | The brain: decomposes, plans, decides. Worth the tokens. |
47
+ | **planner** | `claude-opus-4-8` | extreme | Max reasoning when planning is delegated. Invoked rarely. |
48
+ | **reviewer** | `claude-opus-4-7` | high | Critical audit; deep reasoning, doesn't need the very latest model. |
49
+ | **implementer** | `claude-sonnet-4-6` | medium | Executes clear specs; speed matters, medium reasoning is enough. |
50
+ | **tester** | `claude-sonnet-4-6` | medium | Tests over already-implemented code; same profile. |
51
+ | **summarizer** | `claude-haiku-4-5` | low | Summaries are mechanical; a fast, cheap model is perfect. |
52
+ | **researcher** | `claude-opus-4-7` | high | Research + synthesis; high reasoning, doesn't need Opus 4.8. |
53
+
54
+ ### The wizard
55
+
56
+ The first `install` offers an interactive wizard (skipped automatically with
57
+ `--defaults` or in non-TTY/CI environments — defaults apply, install never blocks):
58
+
59
+ ```text
60
+ chamba per-agent config
61
+ Pick which model + effort each role uses. …
62
+
63
+ ? Use the recommended defaults? (No lets you customize each role) (Y/n)
64
+ ```
65
+
66
+ Pick **Yes** to take the table above, or **No** to choose a model + effort for each
67
+ role. Cancelling (Ctrl+C) installs the defaults anyway.
68
+
69
+ ### Reconfigure anytime
70
+
71
+ ```bash
72
+ npx @chamba/claude-extras config show # resolved config + where each value comes from
73
+ npx @chamba/claude-extras config models # list available models
74
+ npx @chamba/claude-extras config set tester claude-haiku-4-5 --effort low
75
+ npx @chamba/claude-extras config apply # regenerate ~/.claude/agents from the config
76
+ npx @chamba/claude-extras config wizard # re-run the wizard
77
+ npx @chamba/claude-extras config reset --yes # back to defaults
78
+ npx @chamba/claude-extras config edit # open ~/.chamba/config.json in $EDITOR
79
+ ```
80
+
81
+ ### Override per project
82
+
83
+ `~/.chamba/config.json` is your global config; a `./.chamba/config.json` in a repo
84
+ overrides it **per role and per field**. Example — use a cheaper reviewer in one repo:
85
+
86
+ ```json
87
+ { "version": 1, "overrides": { "reviewer": { "model": "claude-sonnet-4-6" } } }
88
+ ```
89
+
90
+ Every other role still falls back to your global config, then to the defaults.
91
+
92
+ ### How `effort` maps per provider
93
+
94
+ `effort` is provider-neutral (`low | medium | high | extreme`); chamba translates it:
95
+
96
+ | `effort` | Claude Code | OpenAI (`reasoning_effort`) | Gemini (`thinkingLevel`) | Ollama |
97
+ |---|---|---|---|---|
98
+ | low | low | low | low | n/a (model-defined) |
99
+ | medium | medium | medium | medium | n/a |
100
+ | high | high | high | high | n/a |
101
+ | extreme | **max** | **xhigh** | high | n/a |
102
+
103
+ The subagent frontmatter always uses Claude Code's vocabulary, so `extreme` → `max`.
104
+ If you set a **non-Anthropic** model for a Claude Code subagent, Claude Code can't run
105
+ it, so `model:` is omitted (the subagent inherits the session model) and a comment
106
+ records why — the config still drives every other editor through the MCP tool.
107
+
108
+ ### FAQ
109
+
110
+ - **Why so many different models?** Different roles need different things. Spending
111
+ Opus-tier reasoning on a one-line summary is waste; using Haiku to plan an
112
+ architecture is a false economy. The defaults encode that trade-off.
113
+ - **How do I change one role without re-running the wizard?**
114
+ `config set <role> <model> [--effort <level>]`, then `config apply`.
115
+ - **What if my config gets corrupted?** chamba degrades to the compiled defaults and
116
+ surfaces a warning (`config show` marks the source as `IGNORED`). Nothing breaks.
117
+ - **Why does `extreme` become `max` in Claude Code?** Claude Code's effort scale tops
118
+ out at `max`; `extreme` is chamba's name for "the ceiling".
119
+
29
120
  ## License
30
121
 
31
122
  MIT
package/dist/cli.js CHANGED
@@ -1,11 +1,122 @@
1
1
  // src/cli.ts
2
+ import { homedir as homedir2 } from "os";
3
+ import { fileURLToPath as fileURLToPath2 } from "url";
4
+ import { NodeFilesystem as NodeFilesystem2 } from "@chamba/adapters";
5
+ import { joinPath as joinPath3 } from "@chamba/core";
6
+
7
+ // src/config-cli.ts
8
+ import { spawnSync } from "child_process";
2
9
  import { homedir } from "os";
3
10
  import { fileURLToPath } from "url";
4
11
  import { NodeFilesystem } from "@chamba/adapters";
5
- import { joinPath as joinPath2 } from "@chamba/core";
12
+ import {
13
+ AGENT_ROLES as AGENT_ROLES2,
14
+ ConfigError as ConfigError2,
15
+ joinPath as joinPath2,
16
+ loadConfig as loadConfig2,
17
+ MODEL_CATALOG as MODEL_CATALOG2
18
+ } from "@chamba/core";
19
+ import { confirm as confirm2 } from "@inquirer/prompts";
20
+
21
+ // src/config-store.ts
22
+ import { ConfigError, DEFAULT_CONFIG, dirname, parseChambaConfig } from "@chamba/core";
23
+ var ConfigStore = class {
24
+ constructor(fs, path) {
25
+ this.fs = fs;
26
+ this.path = path;
27
+ }
28
+ fs;
29
+ path;
30
+ /** The current file, or an empty `{ version: 1 }` if it doesn't exist. */
31
+ async read() {
32
+ if (!await this.fs.exists(this.path)) return { version: 1 };
33
+ let raw;
34
+ try {
35
+ raw = JSON.parse(await this.fs.readFile(this.path));
36
+ } catch (e) {
37
+ throw new ConfigError(`invalid JSON in ${this.path}: ${e.message}`);
38
+ }
39
+ const parsed = parseChambaConfig(raw);
40
+ if (!parsed.ok) throw new ConfigError(`invalid config in ${this.path}: ${parsed.error}`);
41
+ return parsed.value;
42
+ }
43
+ async write(config) {
44
+ await this.fs.mkdir(dirname(this.path));
45
+ await this.fs.writeFile(this.path, `${JSON.stringify(config, null, 2)}
46
+ `);
47
+ }
48
+ /** Merge a single role's patch into `overrides` and persist (validated). */
49
+ async setRole(role, patch) {
50
+ const current = await this.read();
51
+ const overrides = { ...current.overrides ?? {} };
52
+ overrides[role] = { ...overrides[role] ?? {}, ...patch };
53
+ const next = { ...current, version: 1, overrides };
54
+ const parsed = parseChambaConfig(next);
55
+ if (!parsed.ok) throw new ConfigError(parsed.error);
56
+ await this.write(parsed.value);
57
+ return parsed.value;
58
+ }
59
+ /** Write the compiled defaults as the full config. */
60
+ async reset() {
61
+ await this.write({ version: 1, defaults: DEFAULT_CONFIG.defaults });
62
+ }
63
+ get filePath() {
64
+ return this.path;
65
+ }
66
+ };
67
+
68
+ // src/installer.ts
69
+ import { joinPath, loadConfig } from "@chamba/core";
70
+
71
+ // src/agent-frontmatter.ts
72
+ import { getModel } from "@chamba/core";
73
+ var AGENT_ROLE_BY_FILE = {
74
+ "implementer.md": "implementer",
75
+ "reviewer.md": "reviewer",
76
+ "tester.md": "tester"
77
+ };
78
+ var CLAUDE_CODE_EFFORT = {
79
+ low: "low",
80
+ medium: "medium",
81
+ high: "high",
82
+ extreme: "max"
83
+ };
84
+ function parseAgentMarkdown(text) {
85
+ const fm = text.match(/^---\n([\s\S]*?)\n---\n?/);
86
+ const meta = {};
87
+ if (fm?.[1]) {
88
+ for (const line of fm[1].split("\n")) {
89
+ const m = line.match(/^(\w+):\s*(.*)$/);
90
+ if (m?.[1]) meta[m[1]] = (m[2] ?? "").trim();
91
+ }
92
+ }
93
+ const body = fm ? text.slice(fm[0].length) : text;
94
+ return {
95
+ name: meta.name ?? "",
96
+ description: meta.description ?? "",
97
+ body: body.trim()
98
+ };
99
+ }
100
+ function renderAgentMarkdown(parsed, cfg) {
101
+ const model = getModel(cfg.model);
102
+ const isAnthropic = model?.provider === "anthropic";
103
+ const effort = CLAUDE_CODE_EFFORT[cfg.effort];
104
+ const lines = ["---", `name: ${parsed.name}`, `description: ${parsed.description}`];
105
+ if (isAnthropic) {
106
+ lines.push(`model: ${cfg.model}`);
107
+ } else {
108
+ lines.push(
109
+ `# requested model '${cfg.model}' is not an Anthropic model; Claude Code uses the session model (inherit)`
110
+ );
111
+ }
112
+ lines.push(`effort: ${effort}`, "---");
113
+ return `${lines.join("\n")}
114
+
115
+ ${parsed.body}
116
+ `;
117
+ }
6
118
 
7
119
  // src/installer.ts
8
- import { joinPath } from "@chamba/core";
9
120
  var CATEGORIES = [
10
121
  { dir: "commands", label: "slash commands" },
11
122
  { dir: "agents", label: "subagents" },
@@ -18,6 +129,7 @@ var Installer = class {
18
129
  this.opts = opts;
19
130
  }
20
131
  opts;
132
+ cachedConfig;
21
133
  /** True if Claude Code seems present (a config dir or file already exists). */
22
134
  async detectClaudeCode() {
23
135
  return await this.opts.fs.exists(this.opts.claudeDir) || await this.opts.fs.exists(this.opts.claudeJsonPath);
@@ -38,7 +150,7 @@ var Installer = class {
38
150
  skipped.push(`${dir}/${name}`);
39
151
  continue;
40
152
  }
41
- const content = await this.opts.fs.readFile(joinPath(this.opts.assetsDir, dir, name));
153
+ const content = await this.materialize(dir, name);
42
154
  await this.opts.fs.writeFile(target, content);
43
155
  installed.push(`${dir}/${name}`);
44
156
  counts[dir] = (counts[dir] ?? 0) + 1;
@@ -62,6 +174,53 @@ var Installer = class {
62
174
  const mcpRemoved = await this.removeMcpServer();
63
175
  return { removed, mcpRemoved };
64
176
  }
177
+ /**
178
+ * Regenerate the subagent files in `~/.claude/agents/` from the current
179
+ * config. Idempotent: a file whose rendered content is unchanged is left
180
+ * untouched. Only role-mapped subagents are (re)generated.
181
+ */
182
+ async applyConfig() {
183
+ const regenerated = [];
184
+ const unchanged = [];
185
+ const targetDir = joinPath(this.opts.claudeDir, "agents");
186
+ await this.opts.fs.mkdir(targetDir);
187
+ for (const name of await this.assetNames("agents")) {
188
+ if (!AGENT_ROLE_BY_FILE[name]) continue;
189
+ const content = await this.materialize("agents", name);
190
+ const target = joinPath(targetDir, name);
191
+ if (await this.readIfExists(target) === content) {
192
+ unchanged.push(`agents/${name}`);
193
+ continue;
194
+ }
195
+ await this.opts.fs.writeFile(target, content);
196
+ regenerated.push(`agents/${name}`);
197
+ }
198
+ return { regenerated, unchanged };
199
+ }
200
+ /** Read an asset and, for role-mapped subagents, inject model + effort. */
201
+ async materialize(dir, name) {
202
+ const raw = await this.opts.fs.readFile(joinPath(this.opts.assetsDir, dir, name));
203
+ if (dir !== "agents") return raw;
204
+ const role = AGENT_ROLE_BY_FILE[name];
205
+ if (!role) return raw;
206
+ const config = await this.resolveConfig();
207
+ return renderAgentMarkdown(parseAgentMarkdown(raw), config[role]);
208
+ }
209
+ async resolveConfig() {
210
+ if (!this.cachedConfig) {
211
+ const { config } = await loadConfig(this.opts.fs, { globalPath: this.opts.globalConfigPath });
212
+ this.cachedConfig = config;
213
+ }
214
+ return this.cachedConfig;
215
+ }
216
+ async readIfExists(path) {
217
+ try {
218
+ if (!await this.opts.fs.exists(path)) return null;
219
+ return await this.opts.fs.readFile(path);
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
65
224
  async assetNames(dir) {
66
225
  try {
67
226
  const entries = await this.opts.fs.readDir(joinPath(this.opts.assetsDir, dir));
@@ -106,15 +265,233 @@ function asRecord(value) {
106
265
  return typeof value === "object" && value !== null ? value : null;
107
266
  }
108
267
 
109
- // src/cli.ts
110
- function buildInstaller() {
268
+ // src/wizard.ts
269
+ import {
270
+ AGENT_ROLES,
271
+ DEFAULT_CONFIG as DEFAULT_CONFIG2,
272
+ EFFORT_LEVELS,
273
+ MODEL_CATALOG,
274
+ ROLE_DESCRIPTIONS
275
+ } from "@chamba/core";
276
+ import { confirm, select } from "@inquirer/prompts";
277
+ function defaultConfigFile() {
278
+ return { version: 1, defaults: DEFAULT_CONFIG2.defaults };
279
+ }
280
+ function buildConfigFromAnswers(answers) {
281
+ const defaults = {};
282
+ for (const role of AGENT_ROLES) {
283
+ const answer = answers.find((a) => a.role === role);
284
+ const base = DEFAULT_CONFIG2.defaults[role];
285
+ defaults[role] = answer ? { model: answer.model, effort: answer.effort, reasoning_priority: base.reasoning_priority } : base;
286
+ }
287
+ return { version: 1, defaults };
288
+ }
289
+ function modelChoices() {
290
+ return MODEL_CATALOG.map((m) => ({
291
+ name: `${m.label} \u2014 ${m.description}`,
292
+ value: m.id
293
+ }));
294
+ }
295
+ function effortChoices() {
296
+ return EFFORT_LEVELS.map((e) => ({ name: e, value: e }));
297
+ }
298
+ function isCancellation(e) {
299
+ return e instanceof Error && e.name === "ExitPromptError";
300
+ }
301
+ async function runWizard(opts = {}) {
302
+ if (opts.nonInteractive) return defaultConfigFile();
303
+ try {
304
+ process.stdout.write(
305
+ "chamba per-agent config\nPick which model + effort each role uses. chamba never calls these models \u2014\nthis only tells your editor's model how to delegate. You can change it later\nwith `npx @chamba/claude-extras config`.\n\n"
306
+ );
307
+ const useDefaults = await confirm({
308
+ message: "Use the recommended defaults? (No lets you customize each role)",
309
+ default: true
310
+ });
311
+ if (useDefaults) return defaultConfigFile();
312
+ const answers = [];
313
+ for (const role of AGENT_ROLES) {
314
+ const model = await select({
315
+ message: `${role} \u2014 ${ROLE_DESCRIPTIONS[role]}`,
316
+ choices: modelChoices(),
317
+ default: DEFAULT_CONFIG2.defaults[role].model
318
+ });
319
+ const effort = await select({
320
+ message: ` effort for ${role}`,
321
+ choices: effortChoices(),
322
+ default: DEFAULT_CONFIG2.defaults[role].effort
323
+ });
324
+ answers.push({ role, model, effort });
325
+ }
326
+ process.stdout.write("\nSummary:\n");
327
+ for (const a of answers) {
328
+ process.stdout.write(` ${a.role.padEnd(13)} ${a.model} \xB7 ${a.effort}
329
+ `);
330
+ }
331
+ const ok = await confirm({ message: "Write this config?", default: true });
332
+ return ok ? buildConfigFromAnswers(answers) : null;
333
+ } catch (e) {
334
+ if (isCancellation(e)) return null;
335
+ throw e;
336
+ }
337
+ }
338
+
339
+ // src/config-cli.ts
340
+ var CONFIG_RELATIVE = ".chamba/config.json";
341
+ function globalConfigPath() {
342
+ return joinPath2(homedir(), CONFIG_RELATIVE);
343
+ }
344
+ function projectConfigPath() {
345
+ return joinPath2(process.cwd(), CONFIG_RELATIVE);
346
+ }
347
+ function formatModels() {
348
+ const lines = ["Available models:"];
349
+ for (const m of MODEL_CATALOG2) {
350
+ const thinking = m.supports_thinking ? "thinking" : "no-thinking";
351
+ lines.push(` ${m.id.padEnd(24)} ${m.provider.padEnd(10)} ${thinking} \u2014 ${m.description}`);
352
+ }
353
+ return lines.join("\n");
354
+ }
355
+ async function formatShow(fs, paths) {
356
+ const { config, sources } = await loadConfig2(fs, paths);
357
+ const lines = ["role model effort priority"];
358
+ for (const role of AGENT_ROLES2) {
359
+ const c = config[role];
360
+ lines.push(
361
+ `${role.padEnd(15)}${c.model.padEnd(26)}${c.effort.padEnd(10)}${c.reasoning_priority}`
362
+ );
363
+ }
364
+ lines.push("");
365
+ for (const s of sources) {
366
+ const where = s.path ? ` (${s.path})` : "";
367
+ const note = s.status === "invalid" ? ` \u2014 IGNORED: ${s.error}` : "";
368
+ lines.push(`source: ${s.kind} [${s.status}]${where}${note}`);
369
+ }
370
+ return lines.join("\n");
371
+ }
372
+ async function cmdSet(store, args) {
373
+ if (!AGENT_ROLES2.includes(args.role)) {
374
+ throw new ConfigError2(`unknown role '${args.role}'; valid roles: ${AGENT_ROLES2.join(", ")}`);
375
+ }
376
+ const patch = { model: args.model };
377
+ if (args.effort !== void 0) patch.effort = args.effort;
378
+ await store.setRole(args.role, patch);
379
+ const effortNote = args.effort ? ` with effort ${args.effort}` : "";
380
+ return `Set ${args.role} \u2192 ${args.model}${effortNote}. Run 'config apply' to regenerate subagents.`;
381
+ }
382
+ function buildInstaller(fs) {
111
383
  const home = homedir();
112
384
  const assetsDir = fileURLToPath(new URL("../assets", import.meta.url));
113
385
  return new Installer({
114
- fs: new NodeFilesystem(),
386
+ fs,
115
387
  assetsDir,
116
388
  claudeDir: joinPath2(home, ".claude"),
117
- claudeJsonPath: joinPath2(home, ".claude.json")
389
+ claudeJsonPath: joinPath2(home, ".claude.json"),
390
+ globalConfigPath: globalConfigPath()
391
+ });
392
+ }
393
+ function parseEffortFlag(rest) {
394
+ const i = rest.indexOf("--effort");
395
+ return i >= 0 ? rest[i + 1] : void 0;
396
+ }
397
+ var USAGE = "Usage: chamba-install config <show|models|set|reset|wizard|apply|edit>\n show resolved config + where each value comes from\n models list the available models\n set <role> <model> [--effort <level>] change one role\n reset [--yes] restore defaults\n wizard [--defaults] (re)run the interactive wizard\n apply regenerate ~/.claude/agents from the config\n edit open the config in $EDITOR\n";
398
+ async function runConfigCommand(args) {
399
+ const [command, ...rest] = args;
400
+ const fs = new NodeFilesystem();
401
+ const store = new ConfigStore(fs, globalConfigPath());
402
+ try {
403
+ switch (command) {
404
+ case "show":
405
+ process.stdout.write(
406
+ `${await formatShow(fs, { globalPath: globalConfigPath(), projectPath: projectConfigPath() })}
407
+ `
408
+ );
409
+ return;
410
+ case "models":
411
+ process.stdout.write(`${formatModels()}
412
+ `);
413
+ return;
414
+ case "set": {
415
+ const [role, model] = rest;
416
+ if (!role || !model) {
417
+ process.stderr.write("Usage: config set <role> <model> [--effort <level>]\n");
418
+ process.exitCode = 1;
419
+ return;
420
+ }
421
+ process.stdout.write(
422
+ `${await cmdSet(store, { role, model, effort: parseEffortFlag(rest) })}
423
+ `
424
+ );
425
+ return;
426
+ }
427
+ case "reset": {
428
+ const confirmed = rest.includes("--yes") || await confirm2({ message: "Reset config to defaults?", default: false });
429
+ if (!confirmed) {
430
+ process.stdout.write("Cancelled.\n");
431
+ return;
432
+ }
433
+ await store.reset();
434
+ process.stdout.write(`Reset config to defaults at ${store.filePath}.
435
+ `);
436
+ return;
437
+ }
438
+ case "wizard": {
439
+ const config = await runWizard({ nonInteractive: rest.includes("--defaults") });
440
+ if (!config) {
441
+ process.stdout.write("Wizard cancelled. Config unchanged.\n");
442
+ return;
443
+ }
444
+ await store.write(config);
445
+ await buildInstaller(fs).applyConfig();
446
+ process.stdout.write(`Wrote ${store.filePath} and regenerated subagents.
447
+ `);
448
+ return;
449
+ }
450
+ case "apply": {
451
+ const result = await buildInstaller(fs).applyConfig();
452
+ process.stdout.write(
453
+ `Applied config: ${result.regenerated.length} regenerated, ${result.unchanged.length} unchanged.
454
+ `
455
+ );
456
+ return;
457
+ }
458
+ case "edit": {
459
+ const editor = process.env.EDITOR;
460
+ if (!await fs.exists(store.filePath)) await store.reset();
461
+ if (!editor) {
462
+ process.stdout.write(`No $EDITOR set. Edit this file directly:
463
+ ${store.filePath}
464
+ `);
465
+ return;
466
+ }
467
+ spawnSync(editor, [store.filePath], { stdio: "inherit" });
468
+ return;
469
+ }
470
+ default:
471
+ process.stderr.write(USAGE);
472
+ process.exitCode = command ? 1 : 0;
473
+ }
474
+ } catch (err) {
475
+ if (err instanceof ConfigError2) {
476
+ process.stderr.write(`config error: ${err.message}
477
+ `);
478
+ process.exitCode = 1;
479
+ return;
480
+ }
481
+ throw err;
482
+ }
483
+ }
484
+
485
+ // src/cli.ts
486
+ function buildInstaller2() {
487
+ const home = homedir2();
488
+ const assetsDir = fileURLToPath2(new URL("../assets", import.meta.url));
489
+ return new Installer({
490
+ fs: new NodeFilesystem2(),
491
+ assetsDir,
492
+ claudeDir: joinPath3(home, ".claude"),
493
+ claudeJsonPath: joinPath3(home, ".claude.json"),
494
+ globalConfigPath: joinPath3(home, ".chamba/config.json")
118
495
  });
119
496
  }
120
497
  function summarize(result) {
@@ -138,28 +515,56 @@ function labelFor(dir) {
138
515
  }
139
516
  async function main() {
140
517
  const [command, ...rest] = process.argv.slice(2);
141
- const installer = buildInstaller();
518
+ const installer = buildInstaller2();
142
519
  if (command === "uninstall") {
143
520
  const result = await installer.uninstall();
144
521
  process.stdout.write(
145
522
  `Removed ${result.removed.length} file(s). ${result.mcpRemoved ? "Removed" : "Did not find"} chamba MCP server in ~/.claude.json
523
+ `
524
+ );
525
+ return;
526
+ }
527
+ if (command === "config") {
528
+ await runConfigCommand(rest);
529
+ return;
530
+ }
531
+ if (command === "apply") {
532
+ const result = await installer.applyConfig();
533
+ process.stdout.write(
534
+ `Applied config: ${result.regenerated.length} subagent(s) regenerated, ${result.unchanged.length} unchanged.
146
535
  `
147
536
  );
148
537
  return;
149
538
  }
150
539
  if (command === void 0 || command === "install") {
151
540
  const force = rest.includes("--force");
541
+ await maybeRunWizard(rest);
152
542
  const result = await installer.install({ force });
153
543
  process.stdout.write(`${summarize(result)}
154
544
  `);
155
545
  return;
156
546
  }
157
547
  process.stderr.write(
158
- `Unknown command "${command}". Usage: chamba-install [install|uninstall] [--force]
548
+ `Unknown command "${command}". Usage: chamba-install [install|uninstall|apply|config <sub>] [--force]
159
549
  `
160
550
  );
161
551
  process.exitCode = 1;
162
552
  }
553
+ async function maybeRunWizard(rest) {
554
+ const fs = new NodeFilesystem2();
555
+ const configPath = joinPath3(homedir2(), ".chamba/config.json");
556
+ if (await fs.exists(configPath)) return;
557
+ const nonInteractive = rest.includes("--defaults") || !process.stdin.isTTY;
558
+ if (nonInteractive) return;
559
+ const config = await runWizard();
560
+ if (!config) {
561
+ process.stdout.write(
562
+ "Wizard skipped \u2014 using recommended defaults. Run `npx @chamba/claude-extras config wizard` anytime.\n"
563
+ );
564
+ return;
565
+ }
566
+ await new ConfigStore(fs, configPath).write(config);
567
+ }
163
568
  main().catch((err) => {
164
569
  process.stderr.write(`chamba-install failed: ${err.message}
165
570
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chamba/claude-extras",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Optional Claude Code extras for chamba: slash commands, subagents and hooks installer",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,8 +30,9 @@
30
30
  "hooks"
31
31
  ],
32
32
  "dependencies": {
33
- "@chamba/adapters": "0.1.0",
34
- "@chamba/core": "0.1.0"
33
+ "@inquirer/prompts": "^7.0.0",
34
+ "@chamba/adapters": "0.2.0",
35
+ "@chamba/core": "0.2.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "^22.0.0",