@cortexkit/opencode-magic-context 0.5.0 → 0.5.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAWA,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAwHjD"}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAYA,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAwHjD"}
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/cli/setup.ts"],"names":[],"mappings":"AA4JA,wBAAsB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAyPhD"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/cli/setup.ts"],"names":[],"mappings":"AA6JA,wBAAsB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAyPhD"}
package/dist/cli.js CHANGED
@@ -7849,8 +7849,8 @@ var require_src3 = __commonJS((exports, module) => {
7849
7849
  });
7850
7850
 
7851
7851
  // src/cli/doctor.ts
7852
- var import_comment_json3 = __toESM(require_src2(), 1);
7853
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
7852
+ var import_comment_json2 = __toESM(require_src2(), 1);
7853
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
7854
7854
 
7855
7855
  // src/shared/conflict-detector.ts
7856
7856
  import { join as join2 } from "node:path";
@@ -8152,8 +8152,7 @@ function readOmoDisabledHooks(directory) {
8152
8152
  }
8153
8153
 
8154
8154
  // src/shared/conflict-fixer.ts
8155
- var import_comment_json = __toESM(require_src2(), 1);
8156
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
8155
+ import { existsSync as existsSync2, mkdirSync, writeFileSync } from "node:fs";
8157
8156
  import { dirname, join as join3 } from "node:path";
8158
8157
  var CONFLICTING_OMO_HOOKS = [
8159
8158
  "context-window-monitor",
@@ -8179,16 +8178,11 @@ function readConfig(filePath) {
8179
8178
  if (!existsSync2(filePath)) {
8180
8179
  return {};
8181
8180
  }
8182
- try {
8183
- const raw = readFileSync2(filePath, "utf-8");
8184
- return import_comment_json.parse(raw);
8185
- } catch {
8186
- return null;
8187
- }
8181
+ return readJsoncFile(filePath);
8188
8182
  }
8189
8183
  function writeConfig(filePath, config) {
8190
8184
  ensureParentDir(filePath);
8191
- writeFileSync(filePath, `${import_comment_json.stringify(config, null, 2)}
8185
+ writeFileSync(filePath, `${JSON.stringify(config, null, 2)}
8192
8186
  `);
8193
8187
  }
8194
8188
  function resolveUserOpenCodeConfigPath() {
@@ -8318,8 +8312,8 @@ function fixConflicts(directory, conflicts) {
8318
8312
  }
8319
8313
 
8320
8314
  // src/shared/tui-config.ts
8321
- var import_comment_json2 = __toESM(require_src2(), 1);
8322
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
8315
+ var import_comment_json = __toESM(require_src2(), 1);
8316
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
8323
8317
  import { dirname as dirname2, join as join5 } from "node:path";
8324
8318
 
8325
8319
  // src/shared/logger.ts
@@ -8374,6 +8368,7 @@ if (!isTestEnv) {
8374
8368
 
8375
8369
  // src/shared/tui-config.ts
8376
8370
  var PLUGIN_NAME = "@cortexkit/opencode-magic-context";
8371
+ var PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
8377
8372
  function resolveTuiConfigPath() {
8378
8373
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
8379
8374
  const jsoncPath = join5(configDir, "tui.jsonc");
@@ -8389,17 +8384,17 @@ function ensureTuiPluginEntry() {
8389
8384
  const configPath = resolveTuiConfigPath();
8390
8385
  let config = {};
8391
8386
  if (existsSync3(configPath)) {
8392
- const raw = readFileSync3(configPath, "utf-8");
8393
- config = import_comment_json2.parse(raw) ?? {};
8387
+ const raw = readFileSync2(configPath, "utf-8");
8388
+ config = import_comment_json.parse(raw) ?? {};
8394
8389
  }
8395
8390
  const plugins = Array.isArray(config.plugin) ? config.plugin.filter((p) => typeof p === "string") : [];
8396
8391
  if (plugins.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`))) {
8397
8392
  return false;
8398
8393
  }
8399
- plugins.push(PLUGIN_NAME);
8394
+ plugins.push(PLUGIN_ENTRY);
8400
8395
  config.plugin = plugins;
8401
8396
  mkdirSync2(dirname2(configPath), { recursive: true });
8402
- writeFileSync2(configPath, `${import_comment_json2.stringify(config, null, 2)}
8397
+ writeFileSync2(configPath, `${import_comment_json.stringify(config, null, 2)}
8403
8398
  `);
8404
8399
  log(`[magic-context] added TUI plugin entry to ${configPath}`);
8405
8400
  return true;
@@ -9601,6 +9596,7 @@ async function selectOne(message, options) {
9601
9596
 
9602
9597
  // src/cli/doctor.ts
9603
9598
  var PLUGIN_NAME2 = "@cortexkit/opencode-magic-context";
9599
+ var PLUGIN_ENTRY_WITH_VERSION = `${PLUGIN_NAME2}@latest`;
9604
9600
  async function runDoctor() {
9605
9601
  Wt2("Magic Context Doctor");
9606
9602
  let issues = 0;
@@ -9626,17 +9622,17 @@ async function runDoctor() {
9626
9622
  }
9627
9623
  if (paths.opencodeConfigFormat !== "none") {
9628
9624
  try {
9629
- const raw = readFileSync4(paths.opencodeConfig, "utf-8");
9630
- const config = import_comment_json3.parse(raw);
9625
+ const raw = readFileSync3(paths.opencodeConfig, "utf-8");
9626
+ const config = import_comment_json2.parse(raw);
9631
9627
  const plugins = Array.isArray(config?.plugin) ? config.plugin : [];
9632
9628
  const hasPlugin = plugins.some((p) => typeof p === "string" && (p === PLUGIN_NAME2 || p.startsWith(`${PLUGIN_NAME2}@`) || p.includes("opencode-magic-context")));
9633
9629
  const configName = paths.opencodeConfigFormat === "jsonc" ? "opencode.jsonc" : "opencode.json";
9634
9630
  if (hasPlugin) {
9635
9631
  R2.success(`Plugin registered in ${configName}`);
9636
9632
  } else {
9637
- const updatedPlugins = [...plugins, PLUGIN_NAME2];
9633
+ const updatedPlugins = [...plugins, PLUGIN_ENTRY_WITH_VERSION];
9638
9634
  config.plugin = updatedPlugins;
9639
- writeFileSync3(paths.opencodeConfig, `${import_comment_json3.stringify(config, null, 2)}
9635
+ writeFileSync3(paths.opencodeConfig, `${import_comment_json2.stringify(config, null, 2)}
9640
9636
  `);
9641
9637
  R2.success(`Added plugin to ${configName}`);
9642
9638
  fixed++;
@@ -9693,19 +9689,20 @@ async function runDoctor() {
9693
9689
  }
9694
9690
 
9695
9691
  // src/cli/setup.ts
9696
- var import_comment_json4 = __toESM(require_src2(), 1);
9697
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
9692
+ var import_comment_json3 = __toESM(require_src2(), 1);
9693
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
9698
9694
  import { dirname as dirname3 } from "node:path";
9699
9695
  var PLUGIN_NAME3 = "@cortexkit/opencode-magic-context";
9696
+ var PLUGIN_ENTRY2 = "@cortexkit/opencode-magic-context@latest";
9700
9697
  function ensureDir(dir) {
9701
9698
  if (!existsSync6(dir)) {
9702
9699
  mkdirSync3(dir, { recursive: true });
9703
9700
  }
9704
9701
  }
9705
9702
  function readJsonc(path2) {
9706
- const content = readFileSync5(path2, "utf-8");
9703
+ const content = readFileSync4(path2, "utf-8");
9707
9704
  try {
9708
- return import_comment_json4.parse(content);
9705
+ return import_comment_json3.parse(content);
9709
9706
  } catch (err) {
9710
9707
  console.error(` ⚠ Failed to parse ${path2}: ${err instanceof Error ? err.message : err}`);
9711
9708
  return null;
@@ -9715,10 +9712,10 @@ function addPluginToOpenCodeConfig(configPath, format) {
9715
9712
  ensureDir(dirname3(configPath));
9716
9713
  if (format === "none") {
9717
9714
  const config = {
9718
- plugin: [PLUGIN_NAME3],
9715
+ plugin: [PLUGIN_ENTRY2],
9719
9716
  compaction: { auto: false, prune: false }
9720
9717
  };
9721
- writeFileSync4(configPath, `${import_comment_json4.stringify(config, null, 2)}
9718
+ writeFileSync4(configPath, `${import_comment_json3.stringify(config, null, 2)}
9722
9719
  `);
9723
9720
  return;
9724
9721
  }
@@ -9730,20 +9727,20 @@ function addPluginToOpenCodeConfig(configPath, format) {
9730
9727
  const plugins = existing.plugin ?? [];
9731
9728
  const hasPlugin = plugins.some((p) => p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`));
9732
9729
  if (!hasPlugin) {
9733
- plugins.push(PLUGIN_NAME3);
9730
+ plugins.push(PLUGIN_ENTRY2);
9734
9731
  }
9735
9732
  existing.plugin = plugins;
9736
9733
  const compaction = existing.compaction ?? {};
9737
9734
  compaction.auto = false;
9738
9735
  compaction.prune = false;
9739
9736
  existing.compaction = compaction;
9740
- writeFileSync4(configPath, `${import_comment_json4.stringify(existing, null, 2)}
9737
+ writeFileSync4(configPath, `${import_comment_json3.stringify(existing, null, 2)}
9741
9738
  `);
9742
9739
  }
9743
9740
  function addPluginToTuiConfig(configPath, format) {
9744
9741
  ensureDir(dirname3(configPath));
9745
9742
  if (format === "none") {
9746
- writeFileSync4(configPath, `${import_comment_json4.stringify({ plugin: [PLUGIN_NAME3] }, null, 2)}
9743
+ writeFileSync4(configPath, `${import_comment_json3.stringify({ plugin: [PLUGIN_ENTRY2] }, null, 2)}
9747
9744
  `);
9748
9745
  return;
9749
9746
  }
@@ -9755,10 +9752,10 @@ function addPluginToTuiConfig(configPath, format) {
9755
9752
  const plugins = existing.plugin ?? [];
9756
9753
  const hasPlugin = plugins.some((p) => p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`));
9757
9754
  if (!hasPlugin) {
9758
- plugins.push(PLUGIN_NAME3);
9755
+ plugins.push(PLUGIN_ENTRY2);
9759
9756
  }
9760
9757
  existing.plugin = plugins;
9761
- writeFileSync4(configPath, `${import_comment_json4.stringify(existing, null, 2)}
9758
+ writeFileSync4(configPath, `${import_comment_json3.stringify(existing, null, 2)}
9762
9759
  `);
9763
9760
  }
9764
9761
  function writeMagicContextConfig(configPath, options) {
@@ -9799,7 +9796,7 @@ function writeMagicContextConfig(configPath, options) {
9799
9796
  cacheTtl["anthropic/claude-opus-4-6"] = "59m";
9800
9797
  config.cache_ttl = cacheTtl;
9801
9798
  }
9802
- writeFileSync4(configPath, `${import_comment_json4.stringify(config, null, 2)}
9799
+ writeFileSync4(configPath, `${import_comment_json3.stringify(config, null, 2)}
9803
9800
  `);
9804
9801
  }
9805
9802
  async function runSetup() {
@@ -9846,7 +9843,7 @@ async function runSetup() {
9846
9843
  if (shouldRemove) {
9847
9844
  plugins.splice(dcpIndex, 1);
9848
9845
  ocConfig.plugin = plugins;
9849
- writeFileSync4(paths.opencodeConfig, `${import_comment_json4.stringify(ocConfig, null, 2)}
9846
+ writeFileSync4(paths.opencodeConfig, `${import_comment_json3.stringify(ocConfig, null, 2)}
9850
9847
  `);
9851
9848
  R2.success("Removed opencode-dcp from plugin list");
9852
9849
  } else {
package/dist/index.js CHANGED
@@ -8502,7 +8502,7 @@ function ensureTuiPluginEntry() {
8502
8502
  if (plugins.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`))) {
8503
8503
  return false;
8504
8504
  }
8505
- plugins.push(PLUGIN_NAME);
8505
+ plugins.push(PLUGIN_ENTRY);
8506
8506
  config2.plugin = plugins;
8507
8507
  mkdirSync3(dirname(configPath), { recursive: true });
8508
8508
  writeFileSync2(configPath, `${import_comment_json.stringify(config2, null, 2)}
@@ -8514,11 +8514,12 @@ function ensureTuiPluginEntry() {
8514
8514
  return false;
8515
8515
  }
8516
8516
  }
8517
- var import_comment_json, PLUGIN_NAME = "@cortexkit/opencode-magic-context";
8517
+ var import_comment_json, PLUGIN_NAME = "@cortexkit/opencode-magic-context", PLUGIN_ENTRY;
8518
8518
  var init_tui_config = __esm(() => {
8519
8519
  init_logger();
8520
8520
  init_opencode_config_dir();
8521
8521
  import_comment_json = __toESM(require_src2(), 1);
8522
+ PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
8522
8523
  });
8523
8524
 
8524
8525
  // src/agents/dreamer.ts
@@ -1 +1 @@
1
- {"version":3,"file":"conflict-fixer.d.ts","sourceRoot":"","sources":["../../src/shared/conflict-fixer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAmG1D,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,CAAC,WAAW,CAAC,GAAG,MAAM,EAAE,CAiHhG"}
1
+ {"version":3,"file":"conflict-fixer.d.ts","sourceRoot":"","sources":["../../src/shared/conflict-fixer.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAmG1D,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,CAAC,WAAW,CAAC,GAAG,MAAM,EAAE,CAiHhG"}
@@ -1 +1 @@
1
- {"version":3,"file":"tui-config.d.ts","sourceRoot":"","sources":["../../src/shared/tui-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAoBH;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CA+B9C"}
1
+ {"version":3,"file":"tui-config.d.ts","sourceRoot":"","sources":["../../src/shared/tui-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqBH;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CA+B9C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "dist",
30
30
  "src/tui",
31
+ "src/shared",
31
32
  "README.md"
32
33
  ],
33
34
  "scripts": {
@@ -0,0 +1,74 @@
1
+ type MessageTime = { created?: number };
2
+
3
+ type MessageInfo = {
4
+ role?: string;
5
+ time?: MessageTime;
6
+ };
7
+
8
+ type MessagePart = {
9
+ type?: string;
10
+ text?: string;
11
+ };
12
+
13
+ type SessionMessage = {
14
+ info?: MessageInfo;
15
+ parts?: unknown;
16
+ };
17
+
18
+ import { isRecord } from "./record-type-guard";
19
+
20
+ function asSessionMessage(value: unknown): SessionMessage | null {
21
+ if (!isRecord(value)) return null;
22
+ const info = value.info;
23
+ const parts = value.parts;
24
+ return {
25
+ info: isRecord(info)
26
+ ? {
27
+ role: typeof info.role === "string" ? info.role : undefined,
28
+ time: isRecord(info.time)
29
+ ? {
30
+ created:
31
+ typeof info.time.created === "number"
32
+ ? info.time.created
33
+ : undefined,
34
+ }
35
+ : undefined,
36
+ }
37
+ : undefined,
38
+ parts,
39
+ };
40
+ }
41
+
42
+ function getCreatedTime(message: SessionMessage): number {
43
+ return message.info?.time?.created ?? 0;
44
+ }
45
+
46
+ function getTextParts(message: SessionMessage): MessagePart[] {
47
+ if (!Array.isArray(message.parts)) return [];
48
+ return message.parts
49
+ .filter((part): part is Record<string, unknown> => isRecord(part))
50
+ .map((part) => ({
51
+ type: typeof part.type === "string" ? part.type : undefined,
52
+ text: typeof part.text === "string" ? part.text : undefined,
53
+ }))
54
+ .filter((part) => part.type === "text" && Boolean(part.text));
55
+ }
56
+
57
+ export function extractLatestAssistantText(messages: unknown): string | null {
58
+ if (!Array.isArray(messages) || messages.length === 0) return null;
59
+
60
+ const assistantMessages = messages
61
+ .map(asSessionMessage)
62
+ .filter((message): message is SessionMessage => message !== null)
63
+ .filter((message) => message.info?.role === "assistant")
64
+ .sort((a, b) => getCreatedTime(b) - getCreatedTime(a));
65
+
66
+ const latest = assistantMessages[0];
67
+ if (!latest) return null;
68
+
69
+ return (
70
+ getTextParts(latest)
71
+ .map((part) => part.text)
72
+ .join("\n") || null
73
+ );
74
+ }
@@ -0,0 +1,310 @@
1
+ import { join } from "node:path";
2
+ import { readJsoncFile } from "./jsonc-parser";
3
+ import { getOpenCodeConfigPaths } from "./opencode-config-dir";
4
+
5
+ interface OpenCodeConfig {
6
+ compaction?: {
7
+ auto?: boolean;
8
+ prune?: boolean;
9
+ };
10
+ plugin?: string[];
11
+ }
12
+
13
+ interface OmoConfig {
14
+ disabled_hooks?: string[];
15
+ }
16
+
17
+ export interface ConflictResult {
18
+ /** Whether any blocking conflict was found */
19
+ hasConflict: boolean;
20
+ /** Human-readable reasons for each conflict */
21
+ reasons: string[];
22
+ /** Which conflicts were found — used for targeted fixes */
23
+ conflicts: {
24
+ compactionAuto: boolean;
25
+ compactionPrune: boolean;
26
+ dcpPlugin: boolean;
27
+ omoPreemptiveCompaction: boolean;
28
+ omoContextWindowMonitor: boolean;
29
+ omoAnthropicRecovery: boolean;
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Detect all conflicts that would prevent magic-context from working correctly.
35
+ * Checks: OpenCode compaction, DCP plugin, OMO conflicting hooks.
36
+ */
37
+ export function detectConflicts(directory: string): ConflictResult {
38
+ const conflicts: ConflictResult["conflicts"] = {
39
+ compactionAuto: false,
40
+ compactionPrune: false,
41
+ dcpPlugin: false,
42
+ omoPreemptiveCompaction: false,
43
+ omoContextWindowMonitor: false,
44
+ omoAnthropicRecovery: false,
45
+ };
46
+ const reasons: string[] = [];
47
+
48
+ // --- Check OpenCode compaction config ---
49
+ const compactionResult = checkCompaction(directory);
50
+ if (compactionResult.auto) {
51
+ conflicts.compactionAuto = true;
52
+ reasons.push("OpenCode auto-compaction is enabled (compaction.auto=true)");
53
+ }
54
+ if (compactionResult.prune) {
55
+ conflicts.compactionPrune = true;
56
+ reasons.push("OpenCode prune is enabled (compaction.prune=true)");
57
+ }
58
+
59
+ // --- Check for DCP plugin ---
60
+ const dcpFound = checkDcpPlugin(directory);
61
+ if (dcpFound) {
62
+ conflicts.dcpPlugin = true;
63
+ reasons.push(
64
+ "opencode-dcp plugin is installed — it conflicts with Magic Context's context management",
65
+ );
66
+ }
67
+
68
+ // --- Check OMO conflicting hooks ---
69
+ const omoResult = checkOmoHooks(directory);
70
+ if (omoResult.preemptiveCompaction) {
71
+ conflicts.omoPreemptiveCompaction = true;
72
+ reasons.push(
73
+ "oh-my-opencode preemptive-compaction hook is active — it triggers compaction that conflicts with historian",
74
+ );
75
+ }
76
+ if (omoResult.contextWindowMonitor) {
77
+ conflicts.omoContextWindowMonitor = true;
78
+ reasons.push(
79
+ "oh-my-opencode context-window-monitor hook is active — it injects usage warnings that overlap with Magic Context nudges",
80
+ );
81
+ }
82
+ if (omoResult.anthropicRecovery) {
83
+ conflicts.omoAnthropicRecovery = true;
84
+ reasons.push(
85
+ "oh-my-opencode anthropic-context-window-limit-recovery hook is active — it triggers emergency compaction that bypasses historian",
86
+ );
87
+ }
88
+
89
+ return {
90
+ hasConflict: reasons.length > 0,
91
+ reasons,
92
+ conflicts,
93
+ };
94
+ }
95
+
96
+ // --- Compaction detection (extracted from opencode-compaction-detector.ts) ---
97
+
98
+ function checkCompaction(directory: string): { auto: boolean; prune: boolean } {
99
+ if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
100
+ return { auto: false, prune: false };
101
+ }
102
+
103
+ // Check project-level config first (higher precedence)
104
+ const projectResult = readProjectCompaction(directory);
105
+ if (projectResult.resolved) return projectResult;
106
+
107
+ // Fall back to user-level config
108
+ const userResult = readUserCompaction();
109
+ if (userResult.resolved) return userResult;
110
+
111
+ // Default: OpenCode has compaction enabled by default
112
+ return { auto: true, prune: false };
113
+ }
114
+
115
+ function readProjectCompaction(directory: string): {
116
+ auto: boolean;
117
+ prune: boolean;
118
+ resolved: boolean;
119
+ } {
120
+ // .opencode/ config has higher precedence
121
+ const dotOcJsonc = join(directory, ".opencode", "opencode.jsonc");
122
+ const dotOcJson = join(directory, ".opencode", "opencode.json");
123
+ const dotOcConfig =
124
+ readJsoncFile<OpenCodeConfig>(dotOcJsonc) ?? readJsoncFile<OpenCodeConfig>(dotOcJson);
125
+
126
+ if (dotOcConfig?.compaction) {
127
+ const c = dotOcConfig.compaction;
128
+ if (c.auto !== undefined || c.prune !== undefined) {
129
+ return { auto: c.auto === true, prune: c.prune === true, resolved: true };
130
+ }
131
+ }
132
+
133
+ // Root-level project config
134
+ const rootJsonc = join(directory, "opencode.jsonc");
135
+ const rootJson = join(directory, "opencode.json");
136
+ const rootConfig =
137
+ readJsoncFile<OpenCodeConfig>(rootJsonc) ?? readJsoncFile<OpenCodeConfig>(rootJson);
138
+
139
+ if (rootConfig?.compaction) {
140
+ const c = rootConfig.compaction;
141
+ if (c.auto !== undefined || c.prune !== undefined) {
142
+ return { auto: c.auto === true, prune: c.prune === true, resolved: true };
143
+ }
144
+ }
145
+
146
+ return { auto: false, prune: false, resolved: false };
147
+ }
148
+
149
+ function readUserCompaction(): { auto: boolean; prune: boolean; resolved: boolean } {
150
+ try {
151
+ const paths = getOpenCodeConfigPaths({ binary: "opencode" });
152
+ const config =
153
+ readJsoncFile<OpenCodeConfig>(paths.configJsonc) ??
154
+ readJsoncFile<OpenCodeConfig>(paths.configJson);
155
+
156
+ if (config?.compaction) {
157
+ const c = config.compaction;
158
+ if (c.auto !== undefined || c.prune !== undefined) {
159
+ return { auto: c.auto === true, prune: c.prune === true, resolved: true };
160
+ }
161
+ }
162
+ } catch {
163
+ // Intentional: config read is best-effort
164
+ }
165
+ return { auto: false, prune: false, resolved: false };
166
+ }
167
+
168
+ // --- DCP detection ---
169
+
170
+ function checkDcpPlugin(directory: string): boolean {
171
+ const plugins = collectPluginEntries(directory);
172
+ return plugins.some((p) => p.includes("opencode-dcp"));
173
+ }
174
+
175
+ function collectPluginEntries(directory: string): string[] {
176
+ const plugins: string[] = [];
177
+
178
+ // Project-level configs
179
+ for (const configPath of [
180
+ join(directory, ".opencode", "opencode.jsonc"),
181
+ join(directory, ".opencode", "opencode.json"),
182
+ join(directory, "opencode.jsonc"),
183
+ join(directory, "opencode.json"),
184
+ ]) {
185
+ const config = readJsoncFile<OpenCodeConfig>(configPath);
186
+ if (config?.plugin) {
187
+ plugins.push(...config.plugin);
188
+ }
189
+ }
190
+
191
+ // User-level config
192
+ try {
193
+ const paths = getOpenCodeConfigPaths({ binary: "opencode" });
194
+ for (const configPath of [paths.configJsonc, paths.configJson]) {
195
+ const config = readJsoncFile<OpenCodeConfig>(configPath);
196
+ if (config?.plugin) {
197
+ plugins.push(...config.plugin);
198
+ }
199
+ }
200
+ } catch {
201
+ // best-effort
202
+ }
203
+
204
+ return plugins;
205
+ }
206
+
207
+ // --- OMO hook detection ---
208
+
209
+ function checkOmoHooks(directory: string): {
210
+ preemptiveCompaction: boolean;
211
+ contextWindowMonitor: boolean;
212
+ anthropicRecovery: boolean;
213
+ } {
214
+ const result = {
215
+ preemptiveCompaction: false,
216
+ contextWindowMonitor: false,
217
+ anthropicRecovery: false,
218
+ };
219
+
220
+ // First check if OMO is even installed
221
+ const plugins = collectPluginEntries(directory);
222
+ const hasOmo = plugins.some(
223
+ (p) =>
224
+ p.includes("oh-my-opencode") ||
225
+ p.includes("oh-my-openagent") ||
226
+ p.includes("@code-yeongyu/"),
227
+ );
228
+ if (!hasOmo) return result;
229
+
230
+ // Read OMO config to check disabled_hooks
231
+ const disabledHooks = readOmoDisabledHooks(directory);
232
+
233
+ // Hooks are ACTIVE unless explicitly in disabled_hooks
234
+ result.preemptiveCompaction = !disabledHooks.has("preemptive-compaction");
235
+ result.contextWindowMonitor = !disabledHooks.has("context-window-monitor");
236
+ result.anthropicRecovery = !disabledHooks.has("anthropic-context-window-limit-recovery");
237
+
238
+ return result;
239
+ }
240
+
241
+ function readOmoDisabledHooks(directory: string): Set<string> {
242
+ const disabled = new Set<string>();
243
+
244
+ // Check both old and new OMO config names
245
+ const configNames = [
246
+ "oh-my-opencode.jsonc",
247
+ "oh-my-opencode.json",
248
+ "oh-my-openagent.jsonc",
249
+ "oh-my-openagent.json",
250
+ ];
251
+
252
+ try {
253
+ const paths = getOpenCodeConfigPaths({ binary: "opencode" });
254
+ for (const name of configNames) {
255
+ const configPath = join(paths.configDir, name);
256
+ const config = readJsoncFile<OmoConfig>(configPath);
257
+ if (config?.disabled_hooks) {
258
+ for (const hook of config.disabled_hooks) {
259
+ disabled.add(hook);
260
+ }
261
+ }
262
+ }
263
+ } catch {
264
+ // best-effort
265
+ }
266
+
267
+ // Also check project-level OMO configs
268
+ for (const name of configNames) {
269
+ const config = readJsoncFile<OmoConfig>(join(directory, name));
270
+ if (config?.disabled_hooks) {
271
+ for (const hook of config.disabled_hooks) {
272
+ disabled.add(hook);
273
+ }
274
+ }
275
+ }
276
+
277
+ return disabled;
278
+ }
279
+
280
+ /**
281
+ * Generate a user-facing summary of conflicts for display in dialogs/notifications.
282
+ */
283
+ export function formatConflictSummary(result: ConflictResult): string {
284
+ if (!result.hasConflict) return "";
285
+
286
+ const lines = [
287
+ "⚠️ Magic Context is disabled due to conflicting configuration:\n",
288
+ ...result.reasons.map((r) => ` • ${r}`),
289
+ "",
290
+ "Run `bunx @cortexkit/opencode-magic-context doctor` to fix,",
291
+ "or resolve these conflicts manually in your OpenCode config.",
292
+ ];
293
+ return lines.join("\n");
294
+ }
295
+
296
+ /**
297
+ * Generate a short conflict summary for ignored message display.
298
+ */
299
+ export function formatConflictShort(result: ConflictResult): string {
300
+ if (!result.hasConflict) return "";
301
+
302
+ const lines = [
303
+ "⚠️ Magic Context is disabled due to conflicting configuration:",
304
+ "",
305
+ ...result.reasons.map((r) => `• ${r}`),
306
+ "",
307
+ "Fix: run `bunx @cortexkit/opencode-magic-context doctor`",
308
+ ];
309
+ return lines.join("\n");
310
+ }