Skip to content

Commit d6240bc

Browse files
committed
fix: use bunx --bun to avoid node shebang delegation, add min-release-age detection, promote compaction_markers to stable config, respect pinned plugin versions in doctor
1 parent 31241ea commit d6240bc

18 files changed

Lines changed: 241 additions & 79 deletions

File tree

CONFIGURATION.md

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The setup wizard adds this automatically.
1919
If something isn't working, run the doctor to auto-detect and fix common issues:
2020

2121
```bash
22-
bunx @cortexkit/opencode-magic-context@latest doctor
22+
bunx --bun @cortexkit/opencode-magic-context@latest doctor
2323
```
2424

2525
Doctor checks: OpenCode installation, plugin registration, `magic-context.jsonc` existence, conflicts (compaction, DCP, OMO hooks), and TUI sidebar configuration. It auto-fixes what it can.
@@ -71,6 +71,7 @@ Higher-tier models with longer cache windows benefit from a longer TTL. Setting
7171
| `compartment_token_budget` | `number` | `20000` | Token budget for historian input chunks. |
7272
| `historian_timeout_ms` | `number` | `300000` | Timeout per historian call (ms). |
7373
| `history_budget_percentage` | `number` (0–1) | `0.15` | Fraction of usable context reserved for the history block. Triggers compression when exceeded. |
74+
| `compaction_markers` | `boolean` | `true` | Inject compaction boundaries into OpenCode's DB after historian publishes. Reduces transform input size for long sessions. |
7475
| `commit_cluster_trigger` | `object` | See below | Controls the commit-cluster historian trigger. |
7576

7677
### `commit_cluster_trigger`
@@ -268,14 +269,6 @@ It is useful when starting a new session. It's better to choose a fast and cheap
268269

269270
## Experimental Features
270271

271-
### `experimental_compaction_markers`
272-
273-
| Key | Type | Default |
274-
|-----|------|---------|
275-
| `experimental.compaction_markers` | `boolean` | `false` |
276-
277-
When enabled, after historian publishes compartments a compaction boundary is injected into OpenCode's message/part tables. This causes `filterCompacted` to stop at the boundary, dramatically reducing the number of messages processed per transform pass.
278-
279272
### `experimental_user_memories`
280273

281274
| Key | Type | Default |
@@ -332,6 +325,7 @@ When enabled, dreamer analyzes which files each session's agent reads most frequ
332325
"protected_tags": 10,
333326
"auto_drop_tool_age": 50,
334327
"history_budget_percentage": 0.15,
328+
"compaction_markers": true,
335329

336330
"historian": {
337331
"model": "github-copilot/gpt-5.4",
@@ -364,7 +358,6 @@ When enabled, dreamer analyzes which files each session's agent reads most frequ
364358
},
365359

366360
"experimental": {
367-
"compaction_markers": false,
368361
"user_memories": {
369362
"enabled": false,
370363
"promotion_threshold": 3

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ irm https://raw.githubusercontent.com/cortexkit/opencode-magic-context/master/sc
7070

7171
**Or run directly (any OS):**
7272
```bash
73-
bunx @cortexkit/opencode-magic-context@latest setup
73+
bunx --bun @cortexkit/opencode-magic-context@latest setup
7474
```
7575

7676
The wizard will:
@@ -136,21 +136,21 @@ The setup wizard handles this automatically if it detects an oh-my-openagent or
136136
Already installed but something isn't working? Run the doctor to check and auto-fix configuration issues:
137137

138138
```bash
139-
bunx @cortexkit/opencode-magic-context@latest doctor
139+
bunx --bun @cortexkit/opencode-magic-context@latest doctor
140140
```
141141

142142
Doctor checks for conflicts (compaction, DCP, OMO hooks), ensures the TUI sidebar is configured, verifies the plugin is registered, and checks the npm cache — fixing what it can automatically.
143143

144144
Use `--force` to force-clear the plugin cache even when versions match (fixes broken transitive dependencies):
145145

146146
```bash
147-
bunx @cortexkit/opencode-magic-context@latest doctor --force
147+
bunx --bun @cortexkit/opencode-magic-context@latest doctor --force
148148
```
149149

150150
Hit a real bug? Use `--issue` to collect environment, sanitized config, and the last 200 log lines into a ready-to-submit report. It can also open the issue directly via `gh` if you have it installed:
151151

152152
```bash
153-
bunx @cortexkit/opencode-magic-context@latest doctor --issue
153+
bunx --bun @cortexkit/opencode-magic-context@latest doctor --issue
154154
```
155155

156156
---
@@ -312,7 +312,7 @@ The TUI plugin is configured automatically by the setup wizard and the `doctor`
312312

313313
### Startup conflict detection
314314

315-
On startup, Magic Context checks for common configuration problems — OpenCode's built-in compaction being enabled, DCP plugin being active alongside Magic Context, or conflicting oh-my-openagent hooks. When conflicts are detected, it warns the active session with a fix suggestion pointing to `bunx @cortexkit/opencode-magic-context@latest doctor`.
315+
On startup, Magic Context checks for common configuration problems — OpenCode's built-in compaction being enabled, DCP plugin being active alongside Magic Context, or conflicting oh-my-openagent hooks. When conflicts are detected, it warns the active session with a fix suggestion pointing to `bunx --bun @cortexkit/opencode-magic-context@latest doctor`.
316316

317317
---
318318

STRUCTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575

7676
## Key File Locations
7777

78-
**Entry Points:** `src/index.ts`: Register the plugin, hidden agents, hooks, tools, and commands. `src/cli/index.ts`: CLI binary entry for `bunx @cortexkit/opencode-magic-context setup`.
78+
**Entry Points:** `src/index.ts`: Register the plugin, hidden agents, hooks, tools, and commands. `src/cli/index.ts`: CLI binary entry for `bunx --bun @cortexkit/opencode-magic-context setup`.
7979

8080
**Configuration:** `src/config/index.ts`: Load and merge config files; `src/config/schema/magic-context.ts`: define defaults and schema rules.
8181

packages/dashboard/src/components/ConfigEditor/ConfigEditor.tsx

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const FIELD_DEFS: FieldDef[] = [
6565
{ key: "enabled", label: "Enabled", type: "boolean", description: "Enable the magic-context plugin", section: "General" },
6666
{ key: "ctx_reduce_enabled", label: "Agent Controlled Reduction", type: "boolean", description: "Enable agent controlled reductions via ctx_reduce tool. When enabled, agent is prompted and nudged to choose what messages and tool calls to drop periodically. If disabled the system still works via auto drops based on message ages.", section: "General" },
6767
{ key: "drop_tool_structure", label: "Drop Tool Structure", type: "boolean", description: "When enabled, dropped tool calls are fully removed. When disabled, tool input/output is truncated in place so tool structure stays visible.", section: "General", defaultValue: false },
68+
{ key: "compaction_markers", label: "Compaction Markers", type: "boolean", description: "Inject boundary into OpenCode's DB so transform only processes the live tail after historian compaction. Significantly reduces transform input size for long sessions.", section: "General", defaultValue: true },
6869
// Thresholds
6970
// cache_ttl and execute_threshold_percentage are rendered as custom PerModelField components
7071
{ key: "nudge_interval_tokens", label: "Nudge Interval (tokens)", type: "number", description: "Token interval between rolling ctx_reduce nudges.", section: "General" },
@@ -853,12 +854,10 @@ function ConfigForm(props: {
853854
{/* Experimental Features */}
854855
{(() => {
855856
const exp = () => (getNestedValue(formData(), "experimental") as {
856-
compaction_markers?: boolean;
857857
user_memories?: { enabled?: boolean; promotion_threshold?: number };
858858
pin_key_files?: { enabled?: boolean; token_budget?: number; min_reads?: number };
859859
} | undefined) ?? {};
860860

861-
const compactionMarkers = () => exp().compaction_markers ?? false;
862861
const userMemEnabled = () => exp().user_memories?.enabled ?? false;
863862
const userMemThreshold = () => exp().user_memories?.promotion_threshold ?? 3;
864863
const pinEnabled = () => exp().pin_key_files?.enabled ?? false;
@@ -867,9 +866,7 @@ function ConfigForm(props: {
867866

868867
const updateExp = (path: string, value: unknown) => {
869868
const current = exp();
870-
if (path === "compaction_markers") {
871-
handleFieldChange("experimental", { ...current, compaction_markers: value });
872-
} else if (path.startsWith("user_memories.")) {
869+
if (path.startsWith("user_memories.")) {
873870
const um = current.user_memories ?? { enabled: false, promotion_threshold: 3 };
874871
const field = path.split(".")[1];
875872
handleFieldChange("experimental", { ...current, user_memories: { ...um, [field]: value } });
@@ -887,21 +884,8 @@ function ConfigForm(props: {
887884
<span class="config-card-title">Experimental</span>
888885
</div>
889886
<div class="config-card-two-col">
890-
{/* Left column: Compaction Markers + User Memories */}
887+
{/* Left column: User Memories */}
891888
<div class="config-card-content">
892-
<div class="config-field">
893-
<div class="config-field-header">
894-
<label class="config-field-label">Compaction Markers</label>
895-
<span class="config-field-key">experimental.compaction_markers</span>
896-
</div>
897-
<span class="config-field-desc">Inject boundary into OpenCode's DB so transform only processes the live tail</span>
898-
<label class="toggle-switch">
899-
<input type="checkbox" checked={compactionMarkers()} onChange={(e) => updateExp("compaction_markers", e.currentTarget.checked)} />
900-
<span class="toggle-slider" />
901-
<span class="toggle-label">{compactionMarkers() ? "Enabled" : "Disabled"}</span>
902-
</label>
903-
</div>
904-
905889
<div class="config-field">
906890
<div class="config-field-header">
907891
<label class="config-field-label">User Memories</label>
@@ -1084,7 +1068,7 @@ export default function ConfigEditor(props: { models: string[] }) {
10841068
<div class="empty-state">
10851069
<span class="empty-state-icon">⚙️</span>
10861070
<span>No user config found at {userConfig()?.path}</span>
1087-
<span style={{ "font-size": "11px" }}>Run <code>bunx @cortexkit/opencode-magic-context setup</code> to create one</span>
1071+
<span style={{ "font-size": "11px" }}>Run <code>bunx --bun @cortexkit/opencode-magic-context setup</code> to create one</span>
10881072
</div>
10891073
}>
10901074
<ConfigForm

packages/plugin/src/cli/doctor.ts

Lines changed: 155 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ async function clearPluginCache(force = false): Promise<{
7070
// Can't read cached version — proceed with clearing
7171
}
7272

73-
// Compare against our own version — when running via `bunx @cortexkit/opencode-magic-context@latest doctor`,
73+
// Compare against our own version — when running via `bunx --bun @cortexkit/opencode-magic-context@latest doctor`,
7474
// our package.json IS the latest published version. No network call needed.
7575
// Try multiple relative paths to handle both src/ and dist/ build output locations.
7676
const require = createRequire(import.meta.url);
@@ -240,6 +240,40 @@ export async function runDoctor(
240240
log.info(" Run 'setup' to create one with model recommendations");
241241
}
242242

243+
// 3b. Migrate deprecated experimental config keys in magic-context.jsonc
244+
if (existsSync(paths.magicContextConfig)) {
245+
try {
246+
const mcRaw = readFileSync(paths.magicContextConfig, "utf-8");
247+
const mcConfig = parse(mcRaw) as Record<string, unknown>;
248+
let mcChanged = false;
249+
250+
// Migrate experimental.compaction_markers → top-level compaction_markers
251+
const experimental = mcConfig.experimental as Record<string, unknown> | undefined;
252+
if (experimental && "compaction_markers" in experimental) {
253+
if (!("compaction_markers" in mcConfig)) {
254+
// Promote value to top level only if not already set
255+
mcConfig.compaction_markers = experimental.compaction_markers;
256+
}
257+
delete experimental.compaction_markers;
258+
mcChanged = true;
259+
// Clean up empty experimental object
260+
if (Object.keys(experimental).length === 0) {
261+
delete mcConfig.experimental;
262+
}
263+
log.success(
264+
"Migrated experimental.compaction_markers → compaction_markers (now default: true)",
265+
);
266+
fixed++;
267+
}
268+
269+
if (mcChanged) {
270+
writeFileSync(paths.magicContextConfig, `${stringify(mcConfig, null, 2)}\n`);
271+
}
272+
} catch {
273+
log.warn("Could not migrate deprecated config keys in magic-context.jsonc");
274+
}
275+
}
276+
243277
// 4. Check plugin is in opencode.json
244278
if (paths.opencodeConfigFormat !== "none") {
245279
try {
@@ -260,15 +294,27 @@ export async function runDoctor(
260294
if (existingIdx >= 0 && pluginList[existingIdx] === PLUGIN_ENTRY_WITH_VERSION) {
261295
log.success(`Plugin registered in ${configName}`);
262296
} else if (existingIdx >= 0) {
263-
// Upgrade pinned/versionless entry to @latest
264297
const oldEntry = pluginList[existingIdx];
265-
pluginList[existingIdx] = PLUGIN_ENTRY_WITH_VERSION;
266-
config.plugin = pluginList;
267-
writeFileSync(paths.opencodeConfig, `${stringify(config, null, 2)}\n`);
268-
log.success(
269-
`Upgraded plugin entry in ${configName}: ${oldEntry}${PLUGIN_ENTRY_WITH_VERSION}`,
270-
);
271-
fixed++;
298+
const isPinned =
299+
oldEntry !== PLUGIN_NAME &&
300+
oldEntry !== PLUGIN_ENTRY_WITH_VERSION &&
301+
/^@cortexkit\/opencode-magic-context@\d/.test(oldEntry);
302+
303+
if (isPinned && !options.force) {
304+
// Warn but don't change — user intentionally pinned
305+
log.warn(
306+
`Plugin pinned to ${oldEntry} in ${configName} — use 'doctor --force' to upgrade`,
307+
);
308+
} else {
309+
// Upgrade versionless entry to @latest, or --force upgrades pinned
310+
pluginList[existingIdx] = PLUGIN_ENTRY_WITH_VERSION;
311+
config.plugin = pluginList;
312+
writeFileSync(paths.opencodeConfig, `${stringify(config, null, 2)}\n`);
313+
log.success(
314+
`Upgraded plugin entry in ${configName}: ${oldEntry}${PLUGIN_ENTRY_WITH_VERSION}`,
315+
);
316+
fixed++;
317+
}
272318
} else {
273319
// Auto-add plugin entry — preserves comments
274320
pluginList.push(PLUGIN_ENTRY_WITH_VERSION);
@@ -312,13 +358,46 @@ export async function runDoctor(
312358
log.success("Added TUI sidebar plugin to tui.json");
313359
log.warn("Restart OpenCode to see the sidebar");
314360
fixed++;
315-
} else {
316-
// Check if it's already there vs missing tui.json entirely
317-
if (existsSync(paths.tuiConfig)) {
361+
} else if (existsSync(paths.tuiConfig)) {
362+
// Check for pinned version in tui config
363+
try {
364+
const tuiRaw = readFileSync(paths.tuiConfig, "utf-8");
365+
const tuiConfig = parse(tuiRaw) as Record<string, unknown>;
366+
const tuiPlugins = Array.isArray(tuiConfig?.plugin)
367+
? (tuiConfig.plugin as unknown[]).filter((p): p is string => typeof p === "string")
368+
: [];
369+
const tuiIdx = tuiPlugins.findIndex(
370+
(p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`),
371+
);
372+
if (tuiIdx >= 0) {
373+
const tuiEntry = tuiPlugins[tuiIdx];
374+
const tuiPinned =
375+
tuiEntry !== PLUGIN_NAME &&
376+
tuiEntry !== PLUGIN_ENTRY_WITH_VERSION &&
377+
/^@cortexkit\/opencode-magic-context@\d/.test(tuiEntry);
378+
if (tuiPinned && !options.force) {
379+
log.warn(
380+
`TUI plugin pinned to ${tuiEntry} — use 'doctor --force' to upgrade`,
381+
);
382+
} else if (tuiPinned && options.force) {
383+
tuiPlugins[tuiIdx] = PLUGIN_ENTRY_WITH_VERSION;
384+
tuiConfig.plugin = tuiPlugins;
385+
writeFileSync(paths.tuiConfig, `${stringify(tuiConfig, null, 2)}\n`);
386+
log.success(
387+
`Upgraded TUI plugin: ${tuiEntry}${PLUGIN_ENTRY_WITH_VERSION}`,
388+
);
389+
fixed++;
390+
} else {
391+
log.success("TUI sidebar plugin configured");
392+
}
393+
} else {
394+
log.success("TUI sidebar plugin configured");
395+
}
396+
} catch {
318397
log.success("TUI sidebar plugin configured");
319-
} else {
320-
log.success("TUI sidebar plugin configured (tui.json created)");
321398
}
399+
} else {
400+
log.success("TUI sidebar plugin configured (tui.json created)");
322401
}
323402

324403
// 7. Check user memories + dreamer compatibility
@@ -364,7 +443,67 @@ export async function runDoctor(
364443
log.success("Plugin cache clean (no cached version found)");
365444
}
366445

367-
// 9. Show diagnostics info (log file, historian dumps)
446+
// 9. Check for min-release-age / minimumReleaseAge restrictions
447+
// OpenCode uses @npmcli/arborist (npm core) to install plugins, so .npmrc
448+
// restrictions apply. Bun's bunfig.toml is checked too for users who install
449+
// via bunx manually.
450+
{
451+
const ageWarnings: string[] = [];
452+
453+
// Check ~/.npmrc for min-release-age or before
454+
const npmrcPath = join(homedir(), ".npmrc");
455+
if (existsSync(npmrcPath)) {
456+
try {
457+
const npmrc = readFileSync(npmrcPath, "utf-8");
458+
for (const line of npmrc.split("\n")) {
459+
const trimmed = line.trim();
460+
if (trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
461+
const [key] = trimmed.split("=").map((s) => s.trim());
462+
if (key === "min-release-age" || key === "before") {
463+
ageWarnings.push(`~/.npmrc has '${trimmed}'`);
464+
}
465+
}
466+
} catch {
467+
// Can't read .npmrc — skip
468+
}
469+
}
470+
471+
// Check ~/.bunfig.toml for minimumReleaseAge
472+
const bunfigPath = join(homedir(), ".bunfig.toml");
473+
if (existsSync(bunfigPath)) {
474+
try {
475+
const bunfig = readFileSync(bunfigPath, "utf-8");
476+
for (const line of bunfig.split("\n")) {
477+
const trimmed = line.trim();
478+
if (trimmed.startsWith("#")) continue;
479+
if (/minimumReleaseAge\s*=/.test(trimmed)) {
480+
ageWarnings.push(`~/.bunfig.toml has '${trimmed}'`);
481+
}
482+
}
483+
} catch {
484+
// Can't read bunfig — skip
485+
}
486+
}
487+
488+
if (ageWarnings.length > 0) {
489+
log.warn(
490+
"Package manager min-release-age restriction detected — this can prevent OpenCode from installing the latest plugin version",
491+
);
492+
for (const w of ageWarnings) {
493+
log.info(` ${w}`);
494+
}
495+
log.info(
496+
" If the plugin stays on an old version after doctor --force, this is the likely cause.",
497+
);
498+
log.info(
499+
" Workaround: temporarily remove the restriction, restart OpenCode, then re-enable it.",
500+
);
501+
issues++;
502+
}
503+
}
504+
505+
// 10. Show diagnostics info (log file, historian dumps)
506+
368507
const logPath = join(tmpdir(), "magic-context.log");
369508
if (existsSync(logPath)) {
370509
const logStat = statSync(logPath);
@@ -400,7 +539,7 @@ export async function runDoctor(
400539
}
401540
}
402541

403-
// 10. Check OMO config
542+
// 11. Check OMO config
404543
if (paths.omoConfig) {
405544
log.info(`OMO config found: ${paths.omoConfig}`);
406545
}

0 commit comments

Comments
 (0)