claudeup 6.3.2 → 6.5.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/package.json +4 -4
- package/src/__tests__/catalog-notice.test.ts +3 -3
- package/src/__tests__/cli-live.test.ts +9 -2
- package/src/__tests__/cli-update-view.test.ts +2 -2
- package/src/__tests__/footer-hints.test.ts +40 -0
- package/src/__tests__/gap-fill-versions.test.ts +24 -24
- package/src/__tests__/gitignore-prerun.test.ts +6 -13
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/manifest.test.ts +134 -0
- package/src/__tests__/marketplace-badge.test.ts +1 -1
- package/src/__tests__/marketplaces.test.ts +0 -1
- package/src/__tests__/model-visuals.test.tsx +793 -0
- package/src/__tests__/models-adapter.test.ts +317 -0
- package/src/__tests__/models-cli.test.ts +173 -0
- package/src/__tests__/models-core.test.ts +640 -0
- package/src/__tests__/models-manager.test.ts +497 -0
- package/src/__tests__/models-screen-state.test.ts +259 -0
- package/src/__tests__/moved-marketplace.test.ts +7 -8
- package/src/__tests__/plugin-contents.test.ts +1 -1
- package/src/__tests__/profile-adopt.test.ts +1 -1
- package/src/__tests__/profile-materializer.test.ts +48 -2
- package/src/__tests__/resolver.test.ts +43 -6
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/symlink-manager.test.ts +65 -1
- package/src/__tests__/tabbar-layout.test.ts +40 -2
- package/src/__tests__/theme-adaptive-colors.test.ts +48 -1
- package/src/__tests__/version-snapshot.test.ts +4 -4
- package/src/cli/doctor.ts +90 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/models.ts +214 -0
- package/src/cli/router.ts +12 -0
- package/src/data/gitignore-defaults.ts +4 -1
- package/src/data/gitignore-reasons.ts +0 -4
- package/src/data/marketplaces.ts +1 -15
- package/src/data/models-presets.ts +270 -0
- package/src/data/predefined-profiles.ts +12 -21
- package/src/data/settings-catalog.ts +11 -4
- package/src/main.tsx +51 -82
- package/src/services/hook-registration.ts +218 -0
- package/src/services/manifest.ts +84 -0
- package/src/services/models-core.ts +628 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-manager.ts +2 -3
- package/src/services/profile-materializer.ts +17 -0
- package/src/services/resolver.ts +13 -2
- package/src/services/settings-file.ts +69 -0
- package/src/services/styles-manager.ts +23 -45
- package/src/services/symlink-manager.ts +57 -11
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +21 -0
- package/src/types/index.ts +14 -0
- package/src/ui/App.tsx +15 -3
- package/src/ui/adapters/modelsAdapter.ts +170 -0
- package/src/ui/adapters/pluginsAdapter.ts +1 -1
- package/src/ui/components/TabBar.tsx +9 -4
- package/src/ui/components/layout/FooterHints.tsx +20 -3
- package/src/ui/components/layout/ScreenLayout.tsx +87 -7
- package/src/ui/components/primitives/MetaText.tsx +27 -1
- package/src/ui/renderers/modelRenderers.tsx +1004 -0
- package/src/ui/renderers/modelVisuals.tsx +853 -0
- package/src/ui/renderers/skillRenderers.tsx +13 -3
- package/src/ui/renderers/styleRenderers.tsx +7 -3
- package/src/ui/screens/ModelsScreen.tsx +478 -0
- package/src/ui/screens/PluginsScreen.tsx +1 -1
- package/src/ui/screens/StylesScreen.tsx +8 -13
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.ts +94 -0
- package/src/ui/state/types.ts +65 -2
- package/src/ui/theme-mode.ts +116 -0
- package/src/ui/theme.ts +26 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `claudeup models list | use <preset> | status | off`
|
|
3
|
+
*
|
|
4
|
+
* The user-facing half of per-subagent model routing. Everything it prints is
|
|
5
|
+
* derived from `services/models-manager.ts`; this file owns wording and exit
|
|
6
|
+
* codes only.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes: 1 for an unknown preset or a config that fails validation, 0
|
|
9
|
+
* otherwise. An unknown preset LISTS the valid names rather than near-matching
|
|
10
|
+
* one — `fable-lead` and `fable-advisor` differ by which model leads, and
|
|
11
|
+
* silently picking the closer string would route every subagent differently
|
|
12
|
+
* from what was asked.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
BUILT_IN_PRESETS,
|
|
17
|
+
DEFAULT_PRESET,
|
|
18
|
+
presetLabel,
|
|
19
|
+
presetNames,
|
|
20
|
+
} from "../data/models-presets.js";
|
|
21
|
+
import {
|
|
22
|
+
GRADES,
|
|
23
|
+
type ModelsConfig,
|
|
24
|
+
type ModelsState,
|
|
25
|
+
} from "../services/models-core.js";
|
|
26
|
+
import {
|
|
27
|
+
applyModelPreset,
|
|
28
|
+
clearModels,
|
|
29
|
+
configPath,
|
|
30
|
+
readModelsConfig,
|
|
31
|
+
readModelsStatus,
|
|
32
|
+
} from "../services/models-manager.js";
|
|
33
|
+
import { ensureManifest } from "./bootstrap.js";
|
|
34
|
+
|
|
35
|
+
const HEADLINE: Record<ModelsState, string> = {
|
|
36
|
+
off: "off — no .claude/models.json, subagents inherit the session model",
|
|
37
|
+
on: "on",
|
|
38
|
+
stale: "stale — settings no longer match the config",
|
|
39
|
+
invalid: "invalid — nothing is routed",
|
|
40
|
+
unhooked: "unhooked — the config is there, but nothing runs it",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** `opus (medium)`, or `opus` when no effort is set. */
|
|
44
|
+
function describeSpec(spec: { model: string; effort?: string }): string {
|
|
45
|
+
return spec.effort ? `${spec.model} (${spec.effort})` : spec.model;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A preset headed by what a person would call it, with the id beside it.
|
|
50
|
+
*
|
|
51
|
+
* Both, deliberately: the label is what you choose between ("Opus with Fable help" says which
|
|
52
|
+
* model leads and which helps), and the id is what you TYPE into `models use` and what lands
|
|
53
|
+
* in the committed config. Printing only the label would leave the command unguessable.
|
|
54
|
+
*/
|
|
55
|
+
function printPreset(preset: ModelsConfig, active: boolean): void {
|
|
56
|
+
const mark = active ? "●" : " ";
|
|
57
|
+
const isDefault = preset.preset === DEFAULT_PRESET ? " (default)" : "";
|
|
58
|
+
console.log(
|
|
59
|
+
`${mark} ${presetLabel(preset.preset)} — ${preset.preset}${isDefault}`,
|
|
60
|
+
);
|
|
61
|
+
console.log(` main ${describeSpec(preset.main)}`);
|
|
62
|
+
for (const grade of GRADES) {
|
|
63
|
+
console.log(
|
|
64
|
+
` ${grade.padEnd(10)} ${describeSpec(preset.grades[grade])}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runList(projectPath: string): Promise<number> {
|
|
70
|
+
const { config } = await readModelsConfig(projectPath);
|
|
71
|
+
console.log("\nModel tiers:\n");
|
|
72
|
+
for (const preset of BUILT_IN_PRESETS) {
|
|
73
|
+
printPreset(preset, config?.preset === preset.preset);
|
|
74
|
+
console.log();
|
|
75
|
+
}
|
|
76
|
+
if (config && !presetNames().includes(config.preset)) {
|
|
77
|
+
console.log(`● ${config.preset} (this project's own, not a built-in)\n`);
|
|
78
|
+
}
|
|
79
|
+
console.log(
|
|
80
|
+
"Tiers name what a subagent is FOR: `smart` takes the judgement calls, `cheap`",
|
|
81
|
+
);
|
|
82
|
+
console.log(
|
|
83
|
+
"the searching. Which model serves each is the preset's call. Apply one with\n`claudeup models use <name>`.\n",
|
|
84
|
+
);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function runStatus(projectPath: string): Promise<number> {
|
|
89
|
+
const status = await readModelsStatus(projectPath);
|
|
90
|
+
console.log(`\nModel tiers: ${HEADLINE[status.state]}`);
|
|
91
|
+
if (status.preset)
|
|
92
|
+
console.log(
|
|
93
|
+
`Preset: ${presetLabel(status.preset)} (${status.preset})`,
|
|
94
|
+
);
|
|
95
|
+
console.log(`Config: ${configPath(projectPath)}`);
|
|
96
|
+
|
|
97
|
+
if (status.drift.length > 0) {
|
|
98
|
+
console.log(status.state === "invalid" ? "\nErrors:" : "\nDrift:");
|
|
99
|
+
for (const line of status.drift) console.log(` ✗ ${line}`);
|
|
100
|
+
if (status.state === "stale") {
|
|
101
|
+
console.log(
|
|
102
|
+
"\n Fix: claudeup models use <preset> (or claudeup doctor --fix)",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (status.state === "unhooked")
|
|
106
|
+
console.log("\n Fix: claudeup doctor --fix");
|
|
107
|
+
}
|
|
108
|
+
for (const warning of status.warnings) console.log(`\n⚠ ${warning}`);
|
|
109
|
+
console.log();
|
|
110
|
+
return status.state === "invalid" ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runUse(projectPath: string, name?: string): Promise<number> {
|
|
114
|
+
if (!name) {
|
|
115
|
+
console.error("Usage: claudeup models use <preset>");
|
|
116
|
+
console.error(`Available: ${presetNames().join(", ")}`);
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
if (!presetNames().includes(name)) {
|
|
120
|
+
// Never near-match. See the file header.
|
|
121
|
+
console.error(`Unknown preset "${name}".`);
|
|
122
|
+
console.error(`Available: ${presetNames().join(", ")}`);
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let result: Awaited<ReturnType<typeof applyModelPreset>>;
|
|
127
|
+
try {
|
|
128
|
+
result = await applyModelPreset({
|
|
129
|
+
projectPath,
|
|
130
|
+
preset: name,
|
|
131
|
+
ensureManifest: (p) => ensureManifest(p),
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n✓ Applied preset "${name}".`);
|
|
139
|
+
console.log(` ${configPath(projectPath)}`);
|
|
140
|
+
console.log(` ${result.settingsPath}`);
|
|
141
|
+
for (const [key, value] of Object.entries(result.wrote)) {
|
|
142
|
+
console.log(` ${key} = ${JSON.stringify(value)}`);
|
|
143
|
+
}
|
|
144
|
+
console.log(
|
|
145
|
+
` hook: ${result.hook === "registered" ? "registered at user scope" : "already registered"}`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (result.recordedInManifest && result.profile) {
|
|
149
|
+
console.log(
|
|
150
|
+
`\nRecorded in .claude/profiles.json under profiles.${result.profile}.`,
|
|
151
|
+
);
|
|
152
|
+
console.log("Commit .claude/profiles.json to share this with the team.");
|
|
153
|
+
} else {
|
|
154
|
+
console.log(
|
|
155
|
+
"\nNo active profile — written straight to .claude/models.json, which is",
|
|
156
|
+
);
|
|
157
|
+
console.log(
|
|
158
|
+
"generated and gitignored. Run `claudeup install` to adopt a profile if the",
|
|
159
|
+
);
|
|
160
|
+
console.log("team should share this routing.");
|
|
161
|
+
}
|
|
162
|
+
console.log();
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function runOff(projectPath: string): Promise<number> {
|
|
167
|
+
await clearModels(projectPath);
|
|
168
|
+
console.log(
|
|
169
|
+
"\n✓ Model tiers off. Subagents inherit the session model again.",
|
|
170
|
+
);
|
|
171
|
+
console.log(
|
|
172
|
+
" The hook stays registered — it is a no-op without a models.json.\n",
|
|
173
|
+
);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function printUsage(): void {
|
|
178
|
+
console.log(`Usage: claudeup models <command>
|
|
179
|
+
|
|
180
|
+
list Show the built-in tier presets; ● marks the active one
|
|
181
|
+
use <preset> Apply a preset to this project and register the hook
|
|
182
|
+
status What is routed right now, and any drift
|
|
183
|
+
off Turn model tiers off (removes the config, keeps the hook)
|
|
184
|
+
|
|
185
|
+
Available presets: ${presetNames().join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `projectPath` defaults to the cwd, which is what the router passes. It is a
|
|
190
|
+
* parameter so tests can drive a temp project without moving the process —
|
|
191
|
+
* `cli/profile.ts` takes the same shape for the same reason.
|
|
192
|
+
*/
|
|
193
|
+
export async function runModelsCommand(
|
|
194
|
+
args: string[],
|
|
195
|
+
projectPath: string = process.cwd(),
|
|
196
|
+
): Promise<number> {
|
|
197
|
+
const [command, ...rest] = args;
|
|
198
|
+
|
|
199
|
+
switch (command) {
|
|
200
|
+
case undefined:
|
|
201
|
+
case "status":
|
|
202
|
+
return runStatus(projectPath);
|
|
203
|
+
case "list":
|
|
204
|
+
return runList(projectPath);
|
|
205
|
+
case "use":
|
|
206
|
+
return runUse(projectPath, rest[0]);
|
|
207
|
+
case "off":
|
|
208
|
+
return runOff(projectPath);
|
|
209
|
+
default:
|
|
210
|
+
console.error(`Unknown models command "${command}".`);
|
|
211
|
+
printUsage();
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/cli/router.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { parseThemeWord } from "../ui/theme-resolve.js";
|
|
|
12
12
|
import { runClaudeCommand } from "./claude.js";
|
|
13
13
|
import { runDoctorCommand } from "./doctor.js";
|
|
14
14
|
import { runInstallCommand } from "./install.js";
|
|
15
|
+
import { runModelsCommand } from "./models.js";
|
|
15
16
|
import { runProfileCommand } from "./profile.js";
|
|
16
17
|
import { runUpdateCommand } from "./update.js";
|
|
17
18
|
import { runUpgradeCommand } from "./upgrade.js";
|
|
@@ -125,6 +126,8 @@ export async function route(
|
|
|
125
126
|
return HANDLED(await runInstallCommand(rest));
|
|
126
127
|
case "profile":
|
|
127
128
|
return HANDLED(await runProfileCommand(rest));
|
|
129
|
+
case "models":
|
|
130
|
+
return HANDLED(await runModelsCommand(rest));
|
|
128
131
|
case "doctor":
|
|
129
132
|
return HANDLED(await runDoctorCommand(rest));
|
|
130
133
|
default:
|
|
@@ -180,6 +183,15 @@ without one is offered adoption on first run. See docs/team-configuration.md.
|
|
|
180
183
|
doctor Check binary deps, profile symlinks, and conventions
|
|
181
184
|
--fix Apply the repairs it can make itself
|
|
182
185
|
|
|
186
|
+
Per-subagent model routing — give each subagent its own model and effort. The
|
|
187
|
+
routing lives on the active profile (committed in .claude/profiles.json) and is
|
|
188
|
+
applied by a PreToolUse hook claudeup registers at user scope.
|
|
189
|
+
models list Show the built-in presets; ● marks the active one
|
|
190
|
+
models use <p> Apply a preset, record it on the active profile, and
|
|
191
|
+
register the hook
|
|
192
|
+
models status What is routed now, plus any drift from the config
|
|
193
|
+
models off Stop routing; subagents inherit the session model again
|
|
194
|
+
|
|
183
195
|
Other commands:
|
|
184
196
|
claude [args...] Check for plugin updates (1h cache), then run claude
|
|
185
197
|
-f, --force Force update check (bypass 1h cache)
|
|
@@ -14,10 +14,13 @@ export const BUILTIN_DEFAULTS: GitignoreManifest = {
|
|
|
14
14
|
".claude/.statusline-worktree-*",
|
|
15
15
|
// Generated, per-developer profile build output (like node_modules).
|
|
16
16
|
".claude/_profiles/",
|
|
17
|
+
// Generated from profiles.json (a symlink into _profiles/ when a profile
|
|
18
|
+
// is active). The routing itself IS committed — in profiles.json, under
|
|
19
|
+
// the profile that declares it.
|
|
20
|
+
".claude/models.json",
|
|
17
21
|
"ai-docs/sessions/",
|
|
18
22
|
".mnemex/",
|
|
19
23
|
".claudemem/",
|
|
20
|
-
"gtd/sessions/",
|
|
21
24
|
".agents/",
|
|
22
25
|
],
|
|
23
26
|
track: [
|
|
@@ -29,10 +29,6 @@ const RULE_REASONS = new Map<string, string>([
|
|
|
29
29
|
key("ignore", ".claudemem/"),
|
|
30
30
|
"Local claudemem indexes are generated from the repo and should not be committed.",
|
|
31
31
|
],
|
|
32
|
-
[
|
|
33
|
-
key("ignore", "gtd/sessions/"),
|
|
34
|
-
"Local GTD session artifacts are personal workflow state.",
|
|
35
|
-
],
|
|
36
32
|
[
|
|
37
33
|
key("ignore", ".agents/"),
|
|
38
34
|
"Local agent workspaces and runtime artifacts should stay out of team history.",
|
package/src/data/marketplaces.ts
CHANGED
|
@@ -47,25 +47,11 @@ export const defaultMarketplaces: Marketplace[] = [
|
|
|
47
47
|
source: "github",
|
|
48
48
|
repo: "MadAppGang/magus-marketing",
|
|
49
49
|
},
|
|
50
|
-
description:
|
|
51
|
-
"SEO, cold email outreach, AI image generation, and video editing plugins",
|
|
50
|
+
description: "AI image generation and video editing plugins",
|
|
52
51
|
official: false,
|
|
53
52
|
owned: true, // Ours: always listed even when uninstalled, sorted to the top
|
|
54
53
|
// Not featured: a niche channel, so keep it collapsed by default.
|
|
55
54
|
},
|
|
56
|
-
{
|
|
57
|
-
name: "magus-alpha",
|
|
58
|
-
displayName: "Magus Alpha",
|
|
59
|
-
source: {
|
|
60
|
-
source: "github",
|
|
61
|
-
repo: "MadAppGang/magus-alpha",
|
|
62
|
-
},
|
|
63
|
-
description:
|
|
64
|
-
"Experimental plugins with evolving interfaces — may change or be withdrawn without notice",
|
|
65
|
-
official: false,
|
|
66
|
-
owned: true, // Ours: always listed even when uninstalled, sorted to the top
|
|
67
|
-
// Not featured: experimental, so keep it collapsed by default.
|
|
68
|
-
},
|
|
69
55
|
{
|
|
70
56
|
name: "claude-plugins-official",
|
|
71
57
|
displayName: "Anthropic Official",
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in presets and the shared agent → tier table.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written constants rather than a glob over data files, for the same reason
|
|
5
|
+
* `src/data/styles/index.ts` hand-lists its embedded presets: a glob cannot resolve at
|
|
6
|
+
* `bun build --compile` time, and this module has to survive into the compiled binary — the
|
|
7
|
+
* hook path reads a project's own `models.json`, but `claudeup models list` and the TUI read
|
|
8
|
+
* these.
|
|
9
|
+
*
|
|
10
|
+
* Tiers name what a subagent is FOR, on one axis: `smart`, `normal`, `cheap`, plus `main` for
|
|
11
|
+
* the orchestrator. Which model serves each is the PRESET's call, and `smart` is not always
|
|
12
|
+
* its largest spend: `fable-advisor` sends the judgement calls to a different model (fable at
|
|
13
|
+
* `medium`) rather than a harder-working one. What holds everywhere is the other half: the
|
|
14
|
+
* thread dispatching the work is never strictly the biggest spend in the preset.
|
|
15
|
+
*/
|
|
16
|
+
import type { Grade, ModelsConfig } from "../services/models-core.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Which tier each subagent gets, shared by every preset.
|
|
20
|
+
*
|
|
21
|
+
* Keys are `subagent_type` strings exactly as the Agent tool receives them: bare names for
|
|
22
|
+
* Claude Code's built-ins, `plugin:name` for plugin agents. A name absent from this table
|
|
23
|
+
* falls to the preset's `fallback`, so a newly installed plugin routes sensibly on day one
|
|
24
|
+
* instead of silently inheriting the orchestrator's model.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_AGENT_GRADES: Record<string, Grade> = {
|
|
27
|
+
// `smart` is for JUDGEMENT — work where the agent decides something a person would
|
|
28
|
+
// argue about, and where being wrong is expensive to undo.
|
|
29
|
+
Plan: "smart",
|
|
30
|
+
"dev:architect": "smart",
|
|
31
|
+
"dev:reviewer": "smart",
|
|
32
|
+
"dev:debugger": "smart",
|
|
33
|
+
"multimodel:deep-analyst": "smart",
|
|
34
|
+
|
|
35
|
+
// `normal`: transforms material it is GIVEN. Real work, bounded judgement.
|
|
36
|
+
//
|
|
37
|
+
// `spec-writer` reads an interview log and writes it up as a spec — the decisions were
|
|
38
|
+
// made during the interview, and this turns answers into structure. `synthesizer` merges
|
|
39
|
+
// reviews it is handed into one report against thresholds it is handed. Neither decides
|
|
40
|
+
// what is true; both were on `smart` because "writes an important document" got confused
|
|
41
|
+
// with "makes an expensive judgement".
|
|
42
|
+
"dev:spec-writer": "normal",
|
|
43
|
+
"dev:synthesizer": "normal",
|
|
44
|
+
|
|
45
|
+
// Reading a lot is not analysing.
|
|
46
|
+
//
|
|
47
|
+
// `detective` is read-only: it locates implementations, traces a path and reports
|
|
48
|
+
// file:line. That is high-volume retrieval with light inference — the same shape as
|
|
49
|
+
// `Explore`, with more structure in the output. It sat on `smart` because "investigates"
|
|
50
|
+
// sounds like thinking; what it actually does is look.
|
|
51
|
+
"code-analysis:detective": "cheap",
|
|
52
|
+
|
|
53
|
+
// Search, transcription and bookkeeping: bounded, mechanical, high-volume.
|
|
54
|
+
Explore: "cheap",
|
|
55
|
+
"dev:scribe": "cheap",
|
|
56
|
+
"dev:stack-detector": "cheap",
|
|
57
|
+
"dev:docs": "cheap",
|
|
58
|
+
"terminal:tui-navigator": "cheap",
|
|
59
|
+
"statusline-setup": "cheap",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The three workflows the `dev` plugin actually runs, and the agents each one dispatches.
|
|
64
|
+
*
|
|
65
|
+
* A single "17 agents" bar counts every agent claudeup has ever heard of, weighted equally —
|
|
66
|
+
* which is not how a session spends anything. Nobody runs all seventeen; they run ONE of
|
|
67
|
+
* these, and what it costs depends on which handful it reaches for. `/dev:investigate`
|
|
68
|
+
* touching two cheap agents and `/dev:dev` touching five across three tiers are different
|
|
69
|
+
* answers to "what will this preset cost me", and the lump sum shows neither.
|
|
70
|
+
*
|
|
71
|
+
* Read out of `plugins/dev/commands/{dev,debug,investigate}.md` — agents only. Those files
|
|
72
|
+
* also name skills (`dev:context-detection`, `code-analysis:investigate`) which are not
|
|
73
|
+
* dispatched through the Agent tool and so are not routed at all.
|
|
74
|
+
*/
|
|
75
|
+
export const WORKFLOWS: { name: string; agents: string[] }[] = [
|
|
76
|
+
{
|
|
77
|
+
name: "dev",
|
|
78
|
+
agents: [
|
|
79
|
+
"dev:stack-detector",
|
|
80
|
+
"dev:architect",
|
|
81
|
+
"dev:developer",
|
|
82
|
+
"dev:test-architect",
|
|
83
|
+
"dev:reviewer",
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "debug",
|
|
88
|
+
agents: [
|
|
89
|
+
"dev:stack-detector",
|
|
90
|
+
"code-analysis:detective",
|
|
91
|
+
"dev:debugger",
|
|
92
|
+
"dev:developer",
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "investigate",
|
|
97
|
+
agents: ["code-analysis:detective", "dev:researcher"],
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Agents whose model is a CAPABILITY choice, not a spend choice.
|
|
103
|
+
*
|
|
104
|
+
* The three tiers answer "how much model", and that is the wrong question for an agent that
|
|
105
|
+
* reads images. `designer:design-review` compares a rendered screen against a reference and
|
|
106
|
+
* `designer:ui` audits a screenshot for usability: what they need is the model that SEES
|
|
107
|
+
* best, which is not a point on a cheap-to-smart line and does not move when the preset
|
|
108
|
+
* changes.
|
|
109
|
+
*
|
|
110
|
+
* They are deliberately absent from the tier table above and from this map until the model
|
|
111
|
+
* is chosen from the live catalogue rather than from memory. Absent means they fall to the
|
|
112
|
+
* preset's `fallback`, which is a defensible default and an honest one; pinning them to a
|
|
113
|
+
* model id guessed here is neither.
|
|
114
|
+
*/
|
|
115
|
+
export const VISION_AGENTS = ["designer:design-review", "designer:ui"] as const;
|
|
116
|
+
|
|
117
|
+
function preset(
|
|
118
|
+
name: string,
|
|
119
|
+
main: ModelsConfig["main"],
|
|
120
|
+
grades: ModelsConfig["grades"],
|
|
121
|
+
): ModelsConfig {
|
|
122
|
+
return {
|
|
123
|
+
version: 1,
|
|
124
|
+
preset: name,
|
|
125
|
+
main,
|
|
126
|
+
grades,
|
|
127
|
+
agents: { ...DEFAULT_AGENT_GRADES },
|
|
128
|
+
fallback: "normal",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* What each preset is CALLED on screen.
|
|
134
|
+
*
|
|
135
|
+
* The id (`fable-advisor`) is the machine name: it is what `models use` takes, what lands in
|
|
136
|
+
* the committed config, and what must never change. The label is what a person reads, and it
|
|
137
|
+
* says which model leads and which one helps — the thing you actually choose between. Kept
|
|
138
|
+
* beside the presets rather than in the UI so the CLI and the TUI cannot disagree.
|
|
139
|
+
*/
|
|
140
|
+
export const PRESET_LABELS: Record<string, string> = {
|
|
141
|
+
"fable-advisor": "Opus with Fable help",
|
|
142
|
+
"fable-lead": "Fable main",
|
|
143
|
+
"fable-top": "Fable top",
|
|
144
|
+
"opus-lead": "Opus main",
|
|
145
|
+
"sonnet-economy": "Sonnet",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** The screen name for a preset id, falling back to the id for a hand-edited config. */
|
|
149
|
+
export function presetLabel(id: string): string {
|
|
150
|
+
return PRESET_LABELS[id] ?? id;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The shipped presets.
|
|
155
|
+
*
|
|
156
|
+
* Effort is set per seat and reaches subagents through `modelSettings` — measured by
|
|
157
|
+
* `benches/agent-model-routing/` (AMR-1), which found alias keys honoured and a routed
|
|
158
|
+
* subagent running at its model's effort while the main thread kept its own.
|
|
159
|
+
*
|
|
160
|
+
* ## Effort belongs to the MODEL, so a preset gets one value per model
|
|
161
|
+
*
|
|
162
|
+
* `modelSettings` is keyed by model. Two seats naming the same model therefore cannot carry
|
|
163
|
+
* different efforts, and the validator rejects that rather than letting one silently win.
|
|
164
|
+
* That is the only cross-seat rule here: **within one preset, a model appears at exactly one
|
|
165
|
+
* effort.** Which effort is a per-preset choice — `fable-lead` runs fable at `medium` and
|
|
166
|
+
* `fable-top` runs it at `xhigh`, and that difference is the whole distinction between them.
|
|
167
|
+
*
|
|
168
|
+
* Presets never collide with each other, because only one is ever applied to a project.
|
|
169
|
+
*
|
|
170
|
+
* ## The levelling these presets apply
|
|
171
|
+
*
|
|
172
|
+
* Each one fixes every model it names at a single effort, which is what makes it writable:
|
|
173
|
+
*
|
|
174
|
+
* opus xhigh wherever it appears
|
|
175
|
+
* fable medium in `fable-lead`, xhigh in `fable-top`
|
|
176
|
+
* sonnet high in the `cheap` seat, xhigh when it holds the whole preset
|
|
177
|
+
* haiku not used
|
|
178
|
+
*
|
|
179
|
+
* A tier therefore differs from its neighbour by MODEL, not by a throttle on one model. That
|
|
180
|
+
* is not a style choice: a ramp like "fable xhigh on `smart`, fable low on `cheap`" is exactly
|
|
181
|
+
* what the `modelSettings` keying cannot express.
|
|
182
|
+
*/
|
|
183
|
+
export const BUILT_IN_PRESETS: ModelsConfig[] = [
|
|
184
|
+
// The default: opus runs everything except the judgement calls, which go to fable.
|
|
185
|
+
//
|
|
186
|
+
// Opus takes `main` and `normal` at `xhigh`, fable takes `smart` at its ordinary `medium`,
|
|
187
|
+
// and sonnet picks up the cheap seat at `high`. The id says what fable DOES here — it
|
|
188
|
+
// advises on the hard work rather than running the thread.
|
|
189
|
+
preset(
|
|
190
|
+
"fable-advisor",
|
|
191
|
+
{ model: "opus", effort: "xhigh" },
|
|
192
|
+
{
|
|
193
|
+
smart: { model: "fable", effort: "medium" },
|
|
194
|
+
normal: { model: "opus", effort: "xhigh" },
|
|
195
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
196
|
+
},
|
|
197
|
+
),
|
|
198
|
+
|
|
199
|
+
// Fable runs everything it can, at its ordinary `medium`, with sonnet on the cheap seat.
|
|
200
|
+
//
|
|
201
|
+
// "Fable on the main orchestrator" — the request, literally, extended to the tiers it
|
|
202
|
+
// dispatches. Paired with `fable-top` below: the same model on `main` and `smart`, one
|
|
203
|
+
// effort apart.
|
|
204
|
+
preset(
|
|
205
|
+
"fable-lead",
|
|
206
|
+
{ model: "fable", effort: "medium" },
|
|
207
|
+
{
|
|
208
|
+
smart: { model: "fable", effort: "medium" },
|
|
209
|
+
normal: { model: "fable", effort: "medium" },
|
|
210
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
211
|
+
},
|
|
212
|
+
),
|
|
213
|
+
|
|
214
|
+
// Fable at full stretch on the thread and the judgement calls; opus does the building.
|
|
215
|
+
//
|
|
216
|
+
// `fable-lead` runs fable at `medium` in three seats; this runs it at `xhigh` in two and
|
|
217
|
+
// hands `normal` — the highest-volume tier, since it is also the `fallback` — to opus.
|
|
218
|
+
// The difference between the two presets is fable's effort, which is why it has to BE two
|
|
219
|
+
// presets rather than one with a ramp.
|
|
220
|
+
preset(
|
|
221
|
+
"fable-top",
|
|
222
|
+
{ model: "fable", effort: "xhigh" },
|
|
223
|
+
{
|
|
224
|
+
smart: { model: "fable", effort: "xhigh" },
|
|
225
|
+
normal: { model: "opus", effort: "xhigh" },
|
|
226
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
227
|
+
},
|
|
228
|
+
),
|
|
229
|
+
|
|
230
|
+
// Opus everywhere it is reached at all, at `xhigh`; sonnet only on the cheap seat.
|
|
231
|
+
//
|
|
232
|
+
// `main`, `smart` and `normal` are one spend decision wearing three names. That is legal
|
|
233
|
+
// precisely because they agree on effort, and it is the point of the preset rather than a
|
|
234
|
+
// compromise: the only thing NOT on opus is the searching and transcription.
|
|
235
|
+
preset(
|
|
236
|
+
"opus-lead",
|
|
237
|
+
{ model: "opus", effort: "xhigh" },
|
|
238
|
+
{
|
|
239
|
+
smart: { model: "opus", effort: "xhigh" },
|
|
240
|
+
normal: { model: "opus", effort: "xhigh" },
|
|
241
|
+
cheap: { model: "sonnet", effort: "high" },
|
|
242
|
+
},
|
|
243
|
+
),
|
|
244
|
+
|
|
245
|
+
// Sonnet, at `xhigh`, in all four seats.
|
|
246
|
+
//
|
|
247
|
+
// This preset deliberately has no tier differentiation: its saving is the model, and the
|
|
248
|
+
// effort is not throttled on top of it. Every routed agent runs sonnet at `xhigh`,
|
|
249
|
+
// including `cheap` — so the hook still pins the model (a session started on opus does
|
|
250
|
+
// not leak into its subagents) while the tier table itself makes no distinction.
|
|
251
|
+
preset(
|
|
252
|
+
"sonnet-economy",
|
|
253
|
+
{ model: "sonnet", effort: "xhigh" },
|
|
254
|
+
{
|
|
255
|
+
smart: { model: "sonnet", effort: "xhigh" },
|
|
256
|
+
normal: { model: "sonnet", effort: "xhigh" },
|
|
257
|
+
cheap: { model: "sonnet", effort: "xhigh" },
|
|
258
|
+
},
|
|
259
|
+
),
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
export const DEFAULT_PRESET = "fable-advisor";
|
|
263
|
+
|
|
264
|
+
export function findPreset(name: string): ModelsConfig | undefined {
|
|
265
|
+
return BUILT_IN_PRESETS.find((p) => p.preset === name);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function presetNames(): string[] {
|
|
269
|
+
return BUILT_IN_PRESETS.map((p) => p.preset);
|
|
270
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ModelsConfig } from "../services/models-core.js";
|
|
2
|
+
|
|
1
3
|
export interface PredefinedProfile {
|
|
2
4
|
id: string;
|
|
3
5
|
name: string;
|
|
@@ -7,6 +9,13 @@ export interface PredefinedProfile {
|
|
|
7
9
|
anthropicPlugins: string[];
|
|
8
10
|
skills: string[];
|
|
9
11
|
settings: Record<string, unknown>;
|
|
12
|
+
/**
|
|
13
|
+
* Per-subagent model routing a profile inherits via `extends`. None of the
|
|
14
|
+
* profiles below declare one — routing is chosen per project by `claudeup
|
|
15
|
+
* models use`, not bundled with a plugin set — but the field is here so the
|
|
16
|
+
* inheritance path in `resolveExtends` is real rather than notional.
|
|
17
|
+
*/
|
|
18
|
+
models?: ModelsConfig;
|
|
10
19
|
}
|
|
11
20
|
|
|
12
21
|
export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
@@ -48,7 +57,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
48
57
|
"browser-use",
|
|
49
58
|
"designer",
|
|
50
59
|
"terminal",
|
|
51
|
-
"kanban",
|
|
52
60
|
],
|
|
53
61
|
anthropicPlugins: [
|
|
54
62
|
"claude-code-setup",
|
|
@@ -121,14 +129,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
121
129
|
name: "Backend Forge",
|
|
122
130
|
description: "API development, debugging, code quality, data workflows",
|
|
123
131
|
icon: "★",
|
|
124
|
-
magusPlugins: [
|
|
125
|
-
"dev",
|
|
126
|
-
"code-analysis",
|
|
127
|
-
"terminal",
|
|
128
|
-
"setup",
|
|
129
|
-
"multimodel",
|
|
130
|
-
"gtd",
|
|
131
|
-
],
|
|
132
|
+
magusPlugins: ["dev", "code-analysis", "terminal", "setup", "multimodel"],
|
|
132
133
|
anthropicPlugins: [
|
|
133
134
|
"feature-dev",
|
|
134
135
|
"code-review",
|
|
@@ -162,7 +163,6 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
162
163
|
"terminal",
|
|
163
164
|
"setup",
|
|
164
165
|
"multimodel",
|
|
165
|
-
"gtd",
|
|
166
166
|
"browser-use",
|
|
167
167
|
],
|
|
168
168
|
anthropicPlugins: [
|
|
@@ -190,15 +190,13 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
190
190
|
{
|
|
191
191
|
id: "growth-marketer",
|
|
192
192
|
name: "Growth Marketer",
|
|
193
|
-
description:
|
|
194
|
-
"SEO, website audits, content production, marketing automation",
|
|
193
|
+
description: "Website audits, content production, marketing automation",
|
|
195
194
|
icon: "★",
|
|
196
195
|
magusPlugins: [
|
|
197
196
|
"dev",
|
|
198
197
|
"code-analysis",
|
|
199
198
|
"terminal",
|
|
200
199
|
"setup",
|
|
201
|
-
"seo@magus-marketing",
|
|
202
200
|
"browser-use",
|
|
203
201
|
"image-generate@magus-marketing",
|
|
204
202
|
"video-editing@magus-marketing",
|
|
@@ -228,14 +226,7 @@ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
|
|
|
228
226
|
description:
|
|
229
227
|
"Planning, code review, coordination, and broad repo visibility",
|
|
230
228
|
icon: "★",
|
|
231
|
-
magusPlugins: [
|
|
232
|
-
"dev",
|
|
233
|
-
"code-analysis",
|
|
234
|
-
"terminal",
|
|
235
|
-
"setup",
|
|
236
|
-
"multimodel",
|
|
237
|
-
"gtd",
|
|
238
|
-
],
|
|
229
|
+
magusPlugins: ["dev", "code-analysis", "terminal", "setup", "multimodel"],
|
|
239
230
|
anthropicPlugins: [
|
|
240
231
|
"code-review",
|
|
241
232
|
"pr-review-toolkit",
|
|
@@ -66,6 +66,7 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
66
66
|
{ label: "Low", value: "low" },
|
|
67
67
|
{ label: "Medium", value: "medium" },
|
|
68
68
|
{ label: "High", value: "high" },
|
|
69
|
+
{ label: "Extra high", value: "xhigh" },
|
|
69
70
|
],
|
|
70
71
|
storage: { type: "setting", key: "effortLevel" },
|
|
71
72
|
},
|
|
@@ -135,14 +136,20 @@ export const SETTINGS_CATALOG: SettingDefinition[] = [
|
|
|
135
136
|
{
|
|
136
137
|
id: "model",
|
|
137
138
|
name: "Default Model",
|
|
138
|
-
|
|
139
|
+
// Aliases, never pinned ids. A pinned id in a shipped list goes stale the
|
|
140
|
+
// day Anthropic moves the alias, and goes stale SILENTLY — the setting
|
|
141
|
+
// keeps naming a model that is a release behind, and nothing reports it.
|
|
142
|
+
// The alias always names whatever that tier currently is.
|
|
143
|
+
description:
|
|
144
|
+
"Override the default model for Claude Code. Aliases track the current model for each tier",
|
|
139
145
|
category: "models",
|
|
140
146
|
type: "select",
|
|
141
147
|
options: [
|
|
142
148
|
{ label: "Default", value: "" },
|
|
143
|
-
{ label: "
|
|
144
|
-
{ label: "
|
|
145
|
-
{ label: "Haiku", value: "
|
|
149
|
+
{ label: "Opus", value: "opus" },
|
|
150
|
+
{ label: "Sonnet", value: "sonnet" },
|
|
151
|
+
{ label: "Haiku", value: "haiku" },
|
|
152
|
+
{ label: "Fable", value: "fable" },
|
|
146
153
|
],
|
|
147
154
|
storage: { type: "setting", key: "model" },
|
|
148
155
|
},
|