@cortexkit/opencode-magic-context 0.5.1 → 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
@@ -8368,6 +8368,7 @@ if (!isTestEnv) {
8368
8368
 
8369
8369
  // src/shared/tui-config.ts
8370
8370
  var PLUGIN_NAME = "@cortexkit/opencode-magic-context";
8371
+ var PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
8371
8372
  function resolveTuiConfigPath() {
8372
8373
  const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
8373
8374
  const jsoncPath = join5(configDir, "tui.jsonc");
@@ -8390,7 +8391,7 @@ function ensureTuiPluginEntry() {
8390
8391
  if (plugins.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`))) {
8391
8392
  return false;
8392
8393
  }
8393
- plugins.push(PLUGIN_NAME);
8394
+ plugins.push(PLUGIN_ENTRY);
8394
8395
  config.plugin = plugins;
8395
8396
  mkdirSync2(dirname2(configPath), { recursive: true });
8396
8397
  writeFileSync2(configPath, `${import_comment_json.stringify(config, null, 2)}
@@ -9595,6 +9596,7 @@ async function selectOne(message, options) {
9595
9596
 
9596
9597
  // src/cli/doctor.ts
9597
9598
  var PLUGIN_NAME2 = "@cortexkit/opencode-magic-context";
9599
+ var PLUGIN_ENTRY_WITH_VERSION = `${PLUGIN_NAME2}@latest`;
9598
9600
  async function runDoctor() {
9599
9601
  Wt2("Magic Context Doctor");
9600
9602
  let issues = 0;
@@ -9628,7 +9630,7 @@ async function runDoctor() {
9628
9630
  if (hasPlugin) {
9629
9631
  R2.success(`Plugin registered in ${configName}`);
9630
9632
  } else {
9631
- const updatedPlugins = [...plugins, PLUGIN_NAME2];
9633
+ const updatedPlugins = [...plugins, PLUGIN_ENTRY_WITH_VERSION];
9632
9634
  config.plugin = updatedPlugins;
9633
9635
  writeFileSync3(paths.opencodeConfig, `${import_comment_json2.stringify(config, null, 2)}
9634
9636
  `);
@@ -9691,6 +9693,7 @@ var import_comment_json3 = __toESM(require_src2(), 1);
9691
9693
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
9692
9694
  import { dirname as dirname3 } from "node:path";
9693
9695
  var PLUGIN_NAME3 = "@cortexkit/opencode-magic-context";
9696
+ var PLUGIN_ENTRY2 = "@cortexkit/opencode-magic-context@latest";
9694
9697
  function ensureDir(dir) {
9695
9698
  if (!existsSync6(dir)) {
9696
9699
  mkdirSync3(dir, { recursive: true });
@@ -9709,7 +9712,7 @@ function addPluginToOpenCodeConfig(configPath, format) {
9709
9712
  ensureDir(dirname3(configPath));
9710
9713
  if (format === "none") {
9711
9714
  const config = {
9712
- plugin: [PLUGIN_NAME3],
9715
+ plugin: [PLUGIN_ENTRY2],
9713
9716
  compaction: { auto: false, prune: false }
9714
9717
  };
9715
9718
  writeFileSync4(configPath, `${import_comment_json3.stringify(config, null, 2)}
@@ -9724,7 +9727,7 @@ function addPluginToOpenCodeConfig(configPath, format) {
9724
9727
  const plugins = existing.plugin ?? [];
9725
9728
  const hasPlugin = plugins.some((p) => p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`));
9726
9729
  if (!hasPlugin) {
9727
- plugins.push(PLUGIN_NAME3);
9730
+ plugins.push(PLUGIN_ENTRY2);
9728
9731
  }
9729
9732
  existing.plugin = plugins;
9730
9733
  const compaction = existing.compaction ?? {};
@@ -9737,7 +9740,7 @@ function addPluginToOpenCodeConfig(configPath, format) {
9737
9740
  function addPluginToTuiConfig(configPath, format) {
9738
9741
  ensureDir(dirname3(configPath));
9739
9742
  if (format === "none") {
9740
- writeFileSync4(configPath, `${import_comment_json3.stringify({ plugin: [PLUGIN_NAME3] }, null, 2)}
9743
+ writeFileSync4(configPath, `${import_comment_json3.stringify({ plugin: [PLUGIN_ENTRY2] }, null, 2)}
9741
9744
  `);
9742
9745
  return;
9743
9746
  }
@@ -9749,7 +9752,7 @@ function addPluginToTuiConfig(configPath, format) {
9749
9752
  const plugins = existing.plugin ?? [];
9750
9753
  const hasPlugin = plugins.some((p) => p === PLUGIN_NAME3 || p.startsWith(`${PLUGIN_NAME3}@`));
9751
9754
  if (!hasPlugin) {
9752
- plugins.push(PLUGIN_NAME3);
9755
+ plugins.push(PLUGIN_ENTRY2);
9753
9756
  }
9754
9757
  existing.plugin = plugins;
9755
9758
  writeFileSync4(configPath, `${import_comment_json3.stringify(existing, null, 2)}
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":"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.1",
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
+ }