Skip to content

Commit 7c22c88

Browse files
committed
fix: setup wizard config merging, model selection, and Claude Max TTL
- Proper JSONC parser (state machine) handles comments inside strings, trailing commas — no more silent parse failures wiping configs - All config writers merge into existing files instead of overwriting - Parse failures skip the file with a warning instead of writing {} - Model selection follows actual fallback chain order with github-copilot per-request models recommended first (correct dotted model names) - Removed '(free with Copilot)' noise from copilot model labels - Added Claude Max/Pro question — sets cache_ttl to 59m for Anthropic - Detect oh-my-openagent.json(c) in addition to oh-my-opencode.json(c) - Removed stale claude-sonnet-4-5 from historian options
1 parent bc20ae3 commit 7c22c88

File tree

2 files changed

+107
-50
lines changed

2 files changed

+107
-50
lines changed

src/cli/opencode-helpers.ts

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -70,73 +70,57 @@ export function buildModelSelection(
7070
const result: { label: string; value: string; recommended?: boolean }[] = [];
7171
const added = new Set<string>();
7272

73-
const addIfAvailable = (pattern: string, recommended = false) => {
73+
const addIfAvailable = (pattern: string, hint?: string) => {
7474
const matches = allModels.filter((m) => m === pattern || m.endsWith(`/${pattern}`));
7575
for (const m of matches) {
7676
if (!added.has(m)) {
7777
added.add(m);
7878
result.push({
79-
label: m,
79+
label: hint ? `${m}${hint}` : m,
8080
value: m,
81-
recommended: recommended && result.length === 0,
81+
recommended: result.length === 0,
8282
});
8383
}
8484
}
8585
};
8686

8787
if (role === "historian") {
88-
// Per-request models first — cheap, fast, good at summarization
89-
addIfAvailable("claude-sonnet-4-6", true);
90-
addIfAvailable("claude-sonnet-4-5");
91-
addIfAvailable("gemini-3.1-pro");
92-
addIfAvailable("gpt-5.4");
93-
addIfAvailable("glm-5");
94-
addIfAvailable("minimax-m2.7");
95-
96-
// Copilot-backed models (free for copilot users)
97-
for (const m of allModels.filter((m) => m.startsWith("github-copilot/"))) {
98-
if (!added.has(m)) {
99-
added.add(m);
100-
result.push({ label: `${m} (free with Copilot)`, value: m });
101-
}
102-
}
88+
// Follow the actual fallback chain order.
89+
// Per-request providers first (github-copilot) — better for historian's
90+
// single long prompt/request pattern vs token-based billing.
91+
addIfAvailable("github-copilot/claude-sonnet-4.6", "per-request billing");
92+
addIfAvailable("anthropic/claude-sonnet-4-6");
93+
addIfAvailable("github-copilot/gpt-5.4", "per-request billing");
94+
addIfAvailable("openai/gpt-5.4");
95+
addIfAvailable("github-copilot/gemini-3.1-pro-preview", "per-request billing");
96+
addIfAvailable("opencode-go/minimax-m2.7");
97+
addIfAvailable("opencode-go/glm-5");
10398
} else if (role === "dreamer") {
10499
// Local/cheap models first — dreamer runs overnight
105100
for (const m of allModels.filter((m) => m.startsWith("ollama/"))) {
106101
if (!added.has(m)) {
107102
added.add(m);
108-
result.push({ label: `${m} (local)`, value: m, recommended: result.length === 0 });
103+
result.push({ label: `${m} local`, value: m, recommended: result.length === 0 });
109104
}
110105
}
111106

112-
addIfAvailable("claude-sonnet-4-6", result.length === 0);
113-
addIfAvailable("gemini-3-flash");
114-
addIfAvailable("glm-5");
115-
addIfAvailable("minimax-m2.7");
116-
addIfAvailable("gpt-5.4-mini");
117-
118-
for (const m of allModels.filter((m) => m.startsWith("github-copilot/"))) {
119-
if (!added.has(m)) {
120-
added.add(m);
121-
result.push({ label: `${m} (free with Copilot)`, value: m });
122-
}
123-
}
107+
addIfAvailable("github-copilot/claude-sonnet-4.6", "per-request billing");
108+
addIfAvailable("anthropic/claude-sonnet-4-6");
109+
addIfAvailable("github-copilot/gemini-3-flash-preview", "per-request billing");
110+
addIfAvailable("opencode-go/glm-5");
111+
addIfAvailable("opencode-go/minimax-m2.7");
124112
} else if (role === "sidekick") {
125113
// Fast models first
126114
for (const m of allModels.filter((m) => m.startsWith("cerebras/"))) {
127115
if (!added.has(m)) {
128116
added.add(m);
129-
result.push({
130-
label: `${m} (fastest)`,
131-
value: m,
132-
recommended: result.length === 0,
133-
});
117+
result.push({ label: m, value: m, recommended: result.length === 0 });
134118
}
135119
}
136120

137-
addIfAvailable("gpt-5-nano");
138-
addIfAvailable("gemini-3-flash");
139-
addIfAvailable("gpt-5.4-mini");
121+
addIfAvailable("opencode/gpt-5-nano");
122+
addIfAvailable("github-copilot/gemini-3-flash-preview");
123+
addIfAvailable("github-copilot/gpt-5-mini");
140124
}
141125

142126
return result;

src/cli/setup.ts

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,62 @@ function ensureDir(dir: string): void {
1919
}
2020
}
2121

22-
function stripJsoncComments(text: string): string {
23-
return text.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
22+
function stripJsoncToJson(text: string): string {
23+
let result = "";
24+
let i = 0;
25+
let inString = false;
26+
let escaped = false;
27+
28+
while (i < text.length) {
29+
const ch = text[i];
30+
31+
if (escaped) {
32+
result += ch;
33+
escaped = false;
34+
i++;
35+
continue;
36+
}
37+
38+
if (inString) {
39+
if (ch === "\\") escaped = true;
40+
else if (ch === '"') inString = false;
41+
result += ch;
42+
i++;
43+
continue;
44+
}
45+
46+
// Line comment
47+
if (ch === "/" && text[i + 1] === "/") {
48+
while (i < text.length && text[i] !== "\n") i++;
49+
continue;
50+
}
51+
52+
// Block comment
53+
if (ch === "/" && text[i + 1] === "*") {
54+
i += 2;
55+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
56+
i += 2;
57+
continue;
58+
}
59+
60+
if (ch === '"') inString = true;
61+
result += ch;
62+
i++;
63+
}
64+
65+
// Strip trailing commas before } or ]
66+
return result.replace(/,(\s*[}\]])/g, "$1");
2467
}
2568

26-
function readJsonc(path: string): Record<string, unknown> {
69+
function readJsonc(path: string): Record<string, unknown> | null {
2770
const content = readFileSync(path, "utf-8");
2871
try {
29-
return JSON.parse(stripJsoncComments(content));
30-
} catch {
31-
return {};
72+
return JSON.parse(stripJsoncToJson(content));
73+
} catch (err) {
74+
console.error(` ⚠ Failed to parse ${path}: ${err instanceof Error ? err.message : err}`);
75+
return null;
3276
}
3377
}
34-
3578
// ─── Config Manipulators ──────────────────────────────────
3679

3780
function addPluginToOpenCodeConfig(configPath: string, format: "json" | "jsonc" | "none"): void {
@@ -48,6 +91,10 @@ function addPluginToOpenCodeConfig(configPath: string, format: "json" | "jsonc"
4891

4992
// Read existing config, merge our changes, preserve everything else
5093
const existing = readJsonc(configPath);
94+
if (!existing) {
95+
log.warn(`Could not parse ${configPath} — skipping to avoid data loss`);
96+
return;
97+
}
5198

5299
// Add plugin if not present
53100
const plugins = (existing.plugin as string[]) ?? [];
@@ -74,11 +121,12 @@ function writeMagicContextConfig(
74121
dreamerModel: string | null;
75122
sidekickEnabled: boolean;
76123
sidekickModel: string | null;
124+
claudeMax: boolean;
77125
},
78126
): void {
79127
// Read existing config to preserve user's other settings
80-
const config: Record<string, unknown> = existsSync(configPath) ? readJsonc(configPath) : {};
81-
128+
const config: Record<string, unknown> =
129+
(existsSync(configPath) ? readJsonc(configPath) : null) ?? {};
82130
if (options.historianModel) {
83131
const historian = (config.historian as Record<string, unknown>) ?? {};
84132
historian.model = options.historianModel;
@@ -107,13 +155,23 @@ function writeMagicContextConfig(
107155
config.sidekick = sidekick;
108156
}
109157

158+
if (options.claudeMax) {
159+
const cacheTtl = (config.cache_ttl as Record<string, string>) ?? {};
160+
if (!cacheTtl.default) cacheTtl.default = "5m";
161+
cacheTtl["anthropic/claude-sonnet-4-6"] = "59m";
162+
cacheTtl["anthropic/claude-opus-4-6"] = "59m";
163+
config.cache_ttl = cacheTtl;
164+
}
165+
110166
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
111167
}
112-
113168
function disableOmoHooks(omoConfigPath: string): void {
114169
const config = readJsonc(omoConfigPath);
170+
if (!config) {
171+
log.warn(`Could not parse ${omoConfigPath} — skipping to avoid data loss`);
172+
return;
173+
}
115174
const disabledHooks = (config.disabled_hooks as string[]) ?? [];
116-
117175
const hooksToDisable = [
118176
"context-window-monitor",
119177
"preemptive-compaction",
@@ -233,13 +291,28 @@ export async function runSetup(): Promise<number> {
233291
log.info("Using built-in fallback chain for sidekick");
234292
}
235293

294+
// ─── Claude Max subscription ────────────────────────
295+
const hasAnthropic = allModels.some((m) => m.startsWith("anthropic/"));
296+
let claudeMax = false;
297+
if (hasAnthropic) {
298+
log.message(
299+
"Claude Max/Pro subscribers get extended prompt caching (up to 1 hour).\n" +
300+
"This lets Magic Context defer context operations much longer, saving money.",
301+
);
302+
claudeMax = await confirm("Do you have a Claude Max or Pro subscription?", false);
303+
if (claudeMax) {
304+
log.success("Cache TTL set to 59m for Anthropic models");
305+
}
306+
}
307+
236308
// Write magic-context config
237309
writeMagicContextConfig(paths.magicContextConfig, {
238310
historianModel,
239311
dreamerEnabled,
240312
dreamerModel,
241313
sidekickEnabled,
242314
sidekickModel,
315+
claudeMax,
243316
});
244317
log.success(`Config written to ${paths.magicContextConfig}`);
245318

0 commit comments

Comments
 (0)