@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.
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli.js +31 -34
- package/dist/index.js +3 -2
- package/dist/shared/conflict-fixer.d.ts.map +1 -1
- package/dist/shared/tui-config.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/shared/assistant-message-extractor.ts +74 -0
- package/src/shared/conflict-detector.ts +310 -0
- package/src/shared/conflict-fixer.ts +216 -0
- package/src/shared/data-path.ts +10 -0
- package/src/shared/error-message.ts +3 -0
- package/src/shared/format-bytes.ts +5 -0
- package/src/shared/index.ts +4 -0
- package/src/shared/internal-initiator-marker.ts +1 -0
- package/src/shared/jsonc-parser.ts +138 -0
- package/src/shared/logger.ts +63 -0
- package/src/shared/model-requirements.ts +84 -0
- package/src/shared/model-suggestion-retry.ts +160 -0
- package/src/shared/normalize-sdk-response.ts +40 -0
- package/src/shared/opencode-compaction-detector.test.ts +222 -0
- package/src/shared/opencode-compaction-detector.ts +81 -0
- package/src/shared/opencode-config-dir-types.ts +15 -0
- package/src/shared/opencode-config-dir.ts +38 -0
- package/src/shared/record-type-guard.ts +3 -0
- package/src/shared/system-directive.ts +9 -0
- package/src/shared/tui-config.ts +60 -0
package/dist/cli/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAYA,wBAAsB,SAAS,IAAI,OAAO,CAAC,MAAM,CAAC,CAwHjD"}
|
package/dist/cli/setup.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/cli/setup.ts"],"names":[],"mappings":"
|
|
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
|
|
7853
|
-
import { existsSync as existsSync5, readFileSync as
|
|
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
|
-
|
|
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
|
-
|
|
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, `${
|
|
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
|
|
8322
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as
|
|
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 =
|
|
8393
|
-
config =
|
|
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(
|
|
8394
|
+
plugins.push(PLUGIN_ENTRY);
|
|
8400
8395
|
config.plugin = plugins;
|
|
8401
8396
|
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
8402
|
-
writeFileSync2(configPath, `${
|
|
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 =
|
|
9630
|
-
const config =
|
|
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,
|
|
9633
|
+
const updatedPlugins = [...plugins, PLUGIN_ENTRY_WITH_VERSION];
|
|
9638
9634
|
config.plugin = updatedPlugins;
|
|
9639
|
-
writeFileSync3(paths.opencodeConfig, `${
|
|
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
|
|
9697
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
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 =
|
|
9703
|
+
const content = readFileSync4(path2, "utf-8");
|
|
9707
9704
|
try {
|
|
9708
|
-
return
|
|
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: [
|
|
9715
|
+
plugin: [PLUGIN_ENTRY2],
|
|
9719
9716
|
compaction: { auto: false, prune: false }
|
|
9720
9717
|
};
|
|
9721
|
-
writeFileSync4(configPath, `${
|
|
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(
|
|
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, `${
|
|
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, `${
|
|
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(
|
|
9755
|
+
plugins.push(PLUGIN_ENTRY2);
|
|
9759
9756
|
}
|
|
9760
9757
|
existing.plugin = plugins;
|
|
9761
|
-
writeFileSync4(configPath, `${
|
|
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, `${
|
|
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, `${
|
|
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(
|
|
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":"
|
|
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;
|
|
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.
|
|
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
|
+
}
|