@cortexkit/opencode-magic-context 0.5.1 → 0.6.0
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/README.md +18 -2
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli.js +23 -6
- package/dist/config/schema/magic-context.d.ts +21 -0
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts +72 -0
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/runner.d.ts +8 -0
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +8 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -0
- package/dist/features/magic-context/plugin-messages.d.ts +75 -0
- package/dist/features/magic-context/plugin-messages.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-notes.d.ts +51 -5
- package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
- package/dist/features/magic-context/storage.d.ts +1 -2
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +20 -0
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +33 -0
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -0
- package/dist/hooks/magic-context/command-handler.d.ts +4 -0
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +27 -0
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +4 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +5 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +7 -0
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +2 -0
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +4 -0
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +2 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1231 -151
- package/dist/plugin/dream-timer.d.ts +4 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/tui-action-consumer.d.ts +13 -0
- package/dist/plugin/tui-action-consumer.d.ts.map +1 -0
- package/dist/shared/tui-config.d.ts.map +1 -1
- package/dist/tools/ctx-note/constants.d.ts +1 -1
- package/dist/tools/ctx-note/constants.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/types.d.ts +3 -1
- package/dist/tools/ctx-note/types.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +20 -0
- package/dist/tui/data/context-db.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/src/tui/data/context-db.ts +114 -6
- package/src/tui/index.tsx +77 -2
- package/src/tui/slots/sidebar-content.tsx +3 -2
- package/dist/features/magic-context/storage-smart-notes.d.ts +0 -24
- package/dist/features/magic-context/storage-smart-notes.d.ts.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface NormalizeSDKResponseOptions {
|
|
2
|
+
preferResponseOnMissingData?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// Audit note: `as TData` casts are intentional at this boundary. The OpenCode plugin SDK types
|
|
6
|
+
// external responses as `unknown`. Adding Zod validation here would require schema definitions
|
|
7
|
+
// for every SDK response shape, which changes with each OpenCode release. The fallback parameter
|
|
8
|
+
// provides safe degradation when shapes mismatch.
|
|
9
|
+
export function normalizeSDKResponse<TData>(
|
|
10
|
+
response: unknown,
|
|
11
|
+
fallback: TData,
|
|
12
|
+
options?: NormalizeSDKResponseOptions,
|
|
13
|
+
): TData {
|
|
14
|
+
if (response === null || response === undefined) {
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (Array.isArray(response)) {
|
|
19
|
+
return response as TData;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof response === "object" && "data" in response) {
|
|
23
|
+
const data = (response as { data?: unknown }).data;
|
|
24
|
+
if (data !== null && data !== undefined) {
|
|
25
|
+
return data as TData;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options?.preferResponseOnMissingData === true) {
|
|
29
|
+
return response as TData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options?.preferResponseOnMissingData === true) {
|
|
36
|
+
return response as TData;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isOpenCodeAutoCompactionEnabled } from "./opencode-compaction-detector";
|
|
5
|
+
import * as configDir from "./opencode-config-dir";
|
|
6
|
+
|
|
7
|
+
describe("opencode-compaction-detector", () => {
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = join("/tmp", `compaction-detector-test-${Date.now()}`);
|
|
12
|
+
mkdirSync(join(tmpDir, ".opencode"), { recursive: true });
|
|
13
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
14
|
+
spyOn(configDir, "getOpenCodeConfigPaths").mockReturnValue({
|
|
15
|
+
configJson: join(tmpDir, "user-config", "opencode.json"),
|
|
16
|
+
configJsonc: join(tmpDir, "user-config", "opencode.jsonc"),
|
|
17
|
+
} as ReturnType<typeof configDir.getOpenCodeConfigPaths>);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
22
|
+
delete process.env.OPENCODE_DISABLE_AUTOCOMPACT;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("#given no config exists", () => {
|
|
26
|
+
it("#then returns true (default: compaction enabled)", () => {
|
|
27
|
+
const emptyDir = join("/tmp", `compaction-empty-${Date.now()}`);
|
|
28
|
+
mkdirSync(emptyDir, { recursive: true });
|
|
29
|
+
|
|
30
|
+
const result = isOpenCodeAutoCompactionEnabled(emptyDir);
|
|
31
|
+
|
|
32
|
+
expect(result).toBe(true);
|
|
33
|
+
rmSync(emptyDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("#given OPENCODE_DISABLE_AUTOCOMPACT env flag is set", () => {
|
|
38
|
+
it("#then returns false", () => {
|
|
39
|
+
process.env.OPENCODE_DISABLE_AUTOCOMPACT = "1";
|
|
40
|
+
|
|
41
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
42
|
+
|
|
43
|
+
expect(result).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("#given project config has compaction.auto = false", () => {
|
|
48
|
+
it("#when opencode.json #then returns false", () => {
|
|
49
|
+
writeFileSync(
|
|
50
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
51
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
55
|
+
|
|
56
|
+
expect(result).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("#when opencode.jsonc #then returns false", () => {
|
|
60
|
+
writeFileSync(
|
|
61
|
+
join(tmpDir, ".opencode", "opencode.jsonc"),
|
|
62
|
+
`{
|
|
63
|
+
// compaction disabled
|
|
64
|
+
"compaction": { "auto": false }
|
|
65
|
+
}`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
69
|
+
|
|
70
|
+
expect(result).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("#given project config has compaction.auto = true", () => {
|
|
75
|
+
it("#then returns true", () => {
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
78
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
82
|
+
|
|
83
|
+
expect(result).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("#given project config has compaction.prune = true", () => {
|
|
88
|
+
it("#then returns true (conflict enabled)", () => {
|
|
89
|
+
writeFileSync(
|
|
90
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
91
|
+
JSON.stringify({ compaction: { auto: false, prune: true } }),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
95
|
+
|
|
96
|
+
expect(result).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("#given project config has auto/prune both false", () => {
|
|
101
|
+
it("#then returns false", () => {
|
|
102
|
+
writeFileSync(
|
|
103
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
104
|
+
JSON.stringify({ compaction: { auto: false, prune: false } }),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
108
|
+
|
|
109
|
+
expect(result).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("#given project config has only compaction.prune = false", () => {
|
|
114
|
+
it("#then returns false", () => {
|
|
115
|
+
writeFileSync(
|
|
116
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
117
|
+
JSON.stringify({ compaction: { prune: false } }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
121
|
+
|
|
122
|
+
expect(result).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("#given jsonc and json both exist", () => {
|
|
127
|
+
it("#then jsonc takes precedence", () => {
|
|
128
|
+
writeFileSync(
|
|
129
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
130
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
131
|
+
);
|
|
132
|
+
writeFileSync(
|
|
133
|
+
join(tmpDir, ".opencode", "opencode.jsonc"),
|
|
134
|
+
`{ "compaction": { "auto": false } }`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
138
|
+
|
|
139
|
+
expect(result).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("#given config exists but no compaction field", () => {
|
|
144
|
+
it("#then returns true (default)", () => {
|
|
145
|
+
writeFileSync(
|
|
146
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
147
|
+
JSON.stringify({ model: "claude-opus-4-6" }),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
151
|
+
|
|
152
|
+
expect(result).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("#given env flag overrides config", () => {
|
|
157
|
+
it("#then env flag wins even when config has auto: true", () => {
|
|
158
|
+
process.env.OPENCODE_DISABLE_AUTOCOMPACT = "true";
|
|
159
|
+
writeFileSync(
|
|
160
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
161
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
165
|
+
|
|
166
|
+
expect(result).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("#given root-level project config", () => {
|
|
171
|
+
it("#when root opencode.json has compaction.auto = false #then returns false", () => {
|
|
172
|
+
writeFileSync(
|
|
173
|
+
join(tmpDir, "opencode.json"),
|
|
174
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
178
|
+
|
|
179
|
+
expect(result).toBe(false);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("#when root opencode.jsonc has compaction.auto = false #then returns false", () => {
|
|
183
|
+
writeFileSync(join(tmpDir, "opencode.jsonc"), `{ "compaction": { "auto": false } }`);
|
|
184
|
+
|
|
185
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
186
|
+
|
|
187
|
+
expect(result).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("#given .opencode/ overrides root-level config", () => {
|
|
192
|
+
it("#when root says false but .opencode says true #then .opencode wins", () => {
|
|
193
|
+
writeFileSync(
|
|
194
|
+
join(tmpDir, "opencode.json"),
|
|
195
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
196
|
+
);
|
|
197
|
+
writeFileSync(
|
|
198
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
199
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
203
|
+
|
|
204
|
+
expect(result).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("#when root says true but .opencode says false #then .opencode wins", () => {
|
|
208
|
+
writeFileSync(
|
|
209
|
+
join(tmpDir, "opencode.json"),
|
|
210
|
+
JSON.stringify({ compaction: { auto: true } }),
|
|
211
|
+
);
|
|
212
|
+
writeFileSync(
|
|
213
|
+
join(tmpDir, ".opencode", "opencode.json"),
|
|
214
|
+
JSON.stringify({ compaction: { auto: false } }),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const result = isOpenCodeAutoCompactionEnabled(tmpDir);
|
|
218
|
+
|
|
219
|
+
expect(result).toBe(false);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { readJsoncFile } from "./jsonc-parser";
|
|
3
|
+
import { log } from "./logger";
|
|
4
|
+
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
5
|
+
|
|
6
|
+
interface OpenCodeConfig {
|
|
7
|
+
compaction?: {
|
|
8
|
+
auto?: boolean;
|
|
9
|
+
prune?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hasCompactionConflict(
|
|
14
|
+
compaction: OpenCodeConfig["compaction"] | undefined,
|
|
15
|
+
): boolean | undefined {
|
|
16
|
+
if (!compaction) return undefined;
|
|
17
|
+
const hasExplicitSetting = compaction.auto !== undefined || compaction.prune !== undefined;
|
|
18
|
+
if (!hasExplicitSetting) return undefined;
|
|
19
|
+
return compaction.auto === true || compaction.prune === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isOpenCodeAutoCompactionEnabled(directory: string): boolean {
|
|
23
|
+
if (process.env.OPENCODE_DISABLE_AUTOCOMPACT) {
|
|
24
|
+
log(
|
|
25
|
+
"[compaction-detector] OPENCODE_DISABLE_AUTOCOMPACT env flag set — auto compaction disabled",
|
|
26
|
+
);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const projectCompaction = readProjectCompactionConfig(directory);
|
|
31
|
+
if (projectCompaction !== undefined) {
|
|
32
|
+
log("[compaction-detector] project config compaction conflict =", projectCompaction);
|
|
33
|
+
return projectCompaction;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const userCompaction = readUserCompactionConfig(directory);
|
|
37
|
+
if (userCompaction !== undefined) {
|
|
38
|
+
log("[compaction-detector] user config compaction conflict =", userCompaction);
|
|
39
|
+
return userCompaction;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
log("[compaction-detector] no compaction config found — default is enabled");
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readProjectCompactionConfig(directory: string): boolean | undefined {
|
|
47
|
+
// .opencode/ dir config has higher precedence than root-level config in OpenCode's loading order.
|
|
48
|
+
// Check highest precedence first — if .opencode/ sets compaction.auto, that wins.
|
|
49
|
+
const dotOpenCodeJsonc = join(directory, ".opencode", "opencode.jsonc");
|
|
50
|
+
const dotOpenCodeJson = join(directory, ".opencode", "opencode.json");
|
|
51
|
+
const dotOpenCodeConfig =
|
|
52
|
+
readJsoncFile<OpenCodeConfig>(dotOpenCodeJsonc) ??
|
|
53
|
+
readJsoncFile<OpenCodeConfig>(dotOpenCodeJson);
|
|
54
|
+
|
|
55
|
+
const dotOpenCodeCompactionConflict = hasCompactionConflict(dotOpenCodeConfig?.compaction);
|
|
56
|
+
if (dotOpenCodeCompactionConflict !== undefined) {
|
|
57
|
+
return dotOpenCodeCompactionConflict;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Root-level project config (lower precedence than .opencode/)
|
|
61
|
+
const rootJsonc = join(directory, "opencode.jsonc");
|
|
62
|
+
const rootJson = join(directory, "opencode.json");
|
|
63
|
+
const rootConfig =
|
|
64
|
+
readJsoncFile<OpenCodeConfig>(rootJsonc) ?? readJsoncFile<OpenCodeConfig>(rootJson);
|
|
65
|
+
|
|
66
|
+
return hasCompactionConflict(rootConfig?.compaction);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readUserCompactionConfig(_directory: string): boolean | undefined {
|
|
70
|
+
try {
|
|
71
|
+
const paths = getOpenCodeConfigPaths({ binary: "opencode" });
|
|
72
|
+
const config =
|
|
73
|
+
readJsoncFile<OpenCodeConfig>(paths.configJsonc) ??
|
|
74
|
+
readJsoncFile<OpenCodeConfig>(paths.configJson);
|
|
75
|
+
|
|
76
|
+
return hasCompactionConflict(config?.compaction);
|
|
77
|
+
} catch {
|
|
78
|
+
// Intentional: config read is best-effort; missing/unreadable config is not an error
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type OpenCodeBinaryType = "opencode" | "opencode-desktop";
|
|
2
|
+
|
|
3
|
+
export type OpenCodeConfigDirOptions = {
|
|
4
|
+
binary: OpenCodeBinaryType;
|
|
5
|
+
version?: string | null;
|
|
6
|
+
checkExisting?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type OpenCodeConfigPaths = {
|
|
10
|
+
configDir: string;
|
|
11
|
+
configJson: string;
|
|
12
|
+
configJsonc: string;
|
|
13
|
+
packageJson: string;
|
|
14
|
+
omoConfig: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { OpenCodeConfigDirOptions, OpenCodeConfigPaths } from "./opencode-config-dir-types";
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
OpenCodeBinaryType,
|
|
8
|
+
OpenCodeConfigDirOptions,
|
|
9
|
+
OpenCodeConfigPaths,
|
|
10
|
+
} from "./opencode-config-dir-types";
|
|
11
|
+
|
|
12
|
+
function getCliConfigDir(): string {
|
|
13
|
+
const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
14
|
+
if (envConfigDir) {
|
|
15
|
+
return resolve(envConfigDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (process.platform === "win32") {
|
|
19
|
+
return join(homedir(), ".config", "opencode");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "opencode");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getOpenCodeConfigDir(_options: OpenCodeConfigDirOptions): string {
|
|
26
|
+
return getCliConfigDir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getOpenCodeConfigPaths(options: OpenCodeConfigDirOptions): OpenCodeConfigPaths {
|
|
30
|
+
const configDir = getOpenCodeConfigDir(options);
|
|
31
|
+
return {
|
|
32
|
+
configDir,
|
|
33
|
+
configJson: join(configDir, "opencode.json"),
|
|
34
|
+
configJsonc: join(configDir, "opencode.jsonc"),
|
|
35
|
+
packageJson: join(configDir, "package.json"),
|
|
36
|
+
omoConfig: join(configDir, "magic-context.jsonc"),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const SYSTEM_DIRECTIVE_PREFIX = "[SYSTEM DIRECTIVE: MAGIC-CONTEXT";
|
|
2
|
+
|
|
3
|
+
export function isSystemDirective(text: string): boolean {
|
|
4
|
+
return text.trimStart().startsWith(SYSTEM_DIRECTIVE_PREFIX);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function removeSystemReminders(text: string): string {
|
|
8
|
+
return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, "").trim();
|
|
9
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-configure tui.json with magic-context TUI plugin entry.
|
|
3
|
+
* Called from the server plugin at startup so the TUI sidebar loads on next restart.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { parse, stringify } from "comment-json";
|
|
9
|
+
import { log } from "./logger";
|
|
10
|
+
import { getOpenCodeConfigPaths } from "./opencode-config-dir";
|
|
11
|
+
|
|
12
|
+
const PLUGIN_NAME = "@cortexkit/opencode-magic-context";
|
|
13
|
+
const PLUGIN_ENTRY = `${PLUGIN_NAME}@latest`;
|
|
14
|
+
|
|
15
|
+
function resolveTuiConfigPath(): string {
|
|
16
|
+
const configDir = getOpenCodeConfigPaths({ binary: "opencode" }).configDir;
|
|
17
|
+
const jsoncPath = join(configDir, "tui.jsonc");
|
|
18
|
+
const jsonPath = join(configDir, "tui.json");
|
|
19
|
+
|
|
20
|
+
if (existsSync(jsoncPath)) return jsoncPath;
|
|
21
|
+
if (existsSync(jsonPath)) return jsonPath;
|
|
22
|
+
return jsonPath; // default: create tui.json
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ensure tui.json has the magic-context TUI plugin entry.
|
|
27
|
+
* Creates tui.json if it doesn't exist. Silently skips if already present.
|
|
28
|
+
*/
|
|
29
|
+
export function ensureTuiPluginEntry(): boolean {
|
|
30
|
+
try {
|
|
31
|
+
const configPath = resolveTuiConfigPath();
|
|
32
|
+
|
|
33
|
+
let config: Record<string, unknown> = {};
|
|
34
|
+
if (existsSync(configPath)) {
|
|
35
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
36
|
+
config = (parse(raw) as Record<string, unknown>) ?? {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const plugins = Array.isArray(config.plugin)
|
|
40
|
+
? config.plugin.filter((p): p is string => typeof p === "string")
|
|
41
|
+
: [];
|
|
42
|
+
|
|
43
|
+
if (plugins.some((p) => p === PLUGIN_NAME || p.startsWith(`${PLUGIN_NAME}@`))) {
|
|
44
|
+
return false; // Already present
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
plugins.push(PLUGIN_ENTRY);
|
|
48
|
+
config.plugin = plugins;
|
|
49
|
+
|
|
50
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
51
|
+
writeFileSync(configPath, `${stringify(config, null, 2)}\n`);
|
|
52
|
+
log(`[magic-context] added TUI plugin entry to ${configPath}`);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(
|
|
56
|
+
`[magic-context] failed to update tui.json: ${error instanceof Error ? error.message : String(error)}`,
|
|
57
|
+
);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -70,11 +70,11 @@ function getDb(): Database | null {
|
|
|
70
70
|
return cachedDb;
|
|
71
71
|
}
|
|
72
72
|
try {
|
|
73
|
-
// Open
|
|
74
|
-
//
|
|
73
|
+
// Open read-write: WAL-mode DBs need write access to the -shm file,
|
|
74
|
+
// and the TUI writes to plugin_messages for the message bus.
|
|
75
75
|
cachedDb = new Database(targetPath);
|
|
76
76
|
cachedDb.exec("PRAGMA journal_mode = WAL");
|
|
77
|
-
cachedDb.exec("PRAGMA
|
|
77
|
+
cachedDb.exec("PRAGMA busy_timeout = 3000");
|
|
78
78
|
dbPath = targetPath;
|
|
79
79
|
return cachedDb;
|
|
80
80
|
} catch (err) {
|
|
@@ -233,7 +233,7 @@ export function loadSidebarSnapshot(sessionId: string, directory: string): Sideb
|
|
|
233
233
|
try {
|
|
234
234
|
const noteRow = db
|
|
235
235
|
.query<{ count: number }, [string]>(
|
|
236
|
-
`SELECT COUNT(*) as count FROM notes WHERE session_id =
|
|
236
|
+
`SELECT COUNT(*) as count FROM notes WHERE session_id = ? AND type = 'session' AND status = 'active'`,
|
|
237
237
|
)
|
|
238
238
|
.get(sessionId);
|
|
239
239
|
sessionNoteCount = noteRow?.count ?? 0;
|
|
@@ -247,12 +247,12 @@ export function loadSidebarSnapshot(sessionId: string, directory: string): Sideb
|
|
|
247
247
|
try {
|
|
248
248
|
const smartRow = db
|
|
249
249
|
.query<{ count: number }, [string]>(
|
|
250
|
-
`SELECT COUNT(*) as count FROM
|
|
250
|
+
`SELECT COUNT(*) as count FROM notes WHERE project_path = ? AND type = 'smart' AND status = 'ready'`,
|
|
251
251
|
)
|
|
252
252
|
.get(projectIdentity);
|
|
253
253
|
readySmartNoteCount = smartRow?.count ?? 0;
|
|
254
254
|
} catch {
|
|
255
|
-
//
|
|
255
|
+
// notes table may not exist
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
258
|
|
|
@@ -582,3 +582,111 @@ function readMagicContextConfig(directory: string): Record<string, unknown> | nu
|
|
|
582
582
|
}
|
|
583
583
|
return null;
|
|
584
584
|
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get compartment count for a session (used by recomp confirmation dialog).
|
|
588
|
+
*/
|
|
589
|
+
export function getCompartmentCount(sessionId: string): number {
|
|
590
|
+
const db = getDb();
|
|
591
|
+
if (!db) return 0;
|
|
592
|
+
try {
|
|
593
|
+
const row = db
|
|
594
|
+
.prepare("SELECT COUNT(*) as count FROM compartments WHERE session_id = ?")
|
|
595
|
+
.get(sessionId) as { count: number } | null;
|
|
596
|
+
return row?.count ?? 0;
|
|
597
|
+
} catch {
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Consume pending server→TUI messages from the plugin_messages table.
|
|
604
|
+
* Returns consumed messages and marks them as consumed.
|
|
605
|
+
*/
|
|
606
|
+
export interface TuiMessage {
|
|
607
|
+
id: number;
|
|
608
|
+
type: string;
|
|
609
|
+
payload: Record<string, unknown>;
|
|
610
|
+
sessionId: string | null;
|
|
611
|
+
createdAt: number;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function consumeTuiMessages(): TuiMessage[] {
|
|
615
|
+
const db = getDb();
|
|
616
|
+
if (!db) return [];
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
// Check if plugin_messages table exists (migration may not have run yet)
|
|
620
|
+
const tableCheck = db
|
|
621
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_messages'")
|
|
622
|
+
.get();
|
|
623
|
+
if (!tableCheck) return [];
|
|
624
|
+
|
|
625
|
+
const now = Date.now();
|
|
626
|
+
const rows = db
|
|
627
|
+
.prepare(
|
|
628
|
+
"SELECT id, type, payload, session_id, created_at FROM plugin_messages WHERE direction = 'server_to_tui' AND consumed_at IS NULL ORDER BY created_at ASC",
|
|
629
|
+
)
|
|
630
|
+
.all() as Array<{
|
|
631
|
+
id: number;
|
|
632
|
+
type: string;
|
|
633
|
+
payload: string;
|
|
634
|
+
session_id: string | null;
|
|
635
|
+
created_at: number;
|
|
636
|
+
}>;
|
|
637
|
+
|
|
638
|
+
if (rows.length === 0) return [];
|
|
639
|
+
|
|
640
|
+
const ids = rows.map((r) => r.id);
|
|
641
|
+
db.prepare(
|
|
642
|
+
`UPDATE plugin_messages SET consumed_at = ? WHERE id IN (${ids.map(() => "?").join(",")})`,
|
|
643
|
+
).run(now, ...ids);
|
|
644
|
+
|
|
645
|
+
// Cleanup old messages
|
|
646
|
+
db.prepare("DELETE FROM plugin_messages WHERE created_at < ?").run(now - 5 * 60 * 1000);
|
|
647
|
+
|
|
648
|
+
return rows.map((r) => {
|
|
649
|
+
let payload: Record<string, unknown> = {};
|
|
650
|
+
try {
|
|
651
|
+
payload = JSON.parse(r.payload);
|
|
652
|
+
} catch {
|
|
653
|
+
// Intentional: malformed payload treated as empty
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
id: r.id,
|
|
657
|
+
type: r.type,
|
|
658
|
+
payload,
|
|
659
|
+
sessionId: r.session_id,
|
|
660
|
+
createdAt: r.created_at,
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
} catch {
|
|
664
|
+
return [];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Send a message from TUI to server via plugin_messages.
|
|
670
|
+
*/
|
|
671
|
+
export function sendMessageToServer(
|
|
672
|
+
type: string,
|
|
673
|
+
payload: Record<string, unknown>,
|
|
674
|
+
sessionId?: string,
|
|
675
|
+
): boolean {
|
|
676
|
+
const db = getDb();
|
|
677
|
+
if (!db) return false;
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const tableCheck = db
|
|
681
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_messages'")
|
|
682
|
+
.get();
|
|
683
|
+
if (!tableCheck) return false;
|
|
684
|
+
|
|
685
|
+
db.prepare(
|
|
686
|
+
"INSERT INTO plugin_messages (direction, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
687
|
+
).run("tui_to_server", type, JSON.stringify(payload), sessionId ?? null, Date.now());
|
|
688
|
+
return true;
|
|
689
|
+
} catch {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|