@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.
- package/README.md +91 -0
- package/dist/cli.js +414 -9
- 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 {
|
|
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.
|
|
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/
|
|
110
|
-
|
|
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
|
|
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 =
|
|
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.
|
|
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
|
-
"@
|
|
34
|
-
"@chamba/
|
|
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",
|