@ephemushroom/opencode-claude-mem 0.7.0 → 0.7.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/README.md +112 -62
- package/dist/capture-status.d.ts +26 -0
- package/dist/cli.js +239 -31
- package/dist/official-sync.d.ts +29 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.js +1299 -0
- package/dist/sidebar-model.d.ts +4 -1
- package/dist/tui.js +195 -31
- package/dist/worker-client.d.ts +28 -1
- package/dist/worker-start.d.ts +46 -0
- package/dist/worker-tools.d.ts +33 -0
- package/package.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,82 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// src/
|
|
3
|
-
import {
|
|
2
|
+
// src/capture-status.ts
|
|
3
|
+
import { createHash, randomUUID } from "crypto";
|
|
4
|
+
import { join, resolve } from "path";
|
|
5
|
+
import {
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
realpathSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
statSync,
|
|
13
|
+
writeFileSync
|
|
14
|
+
} from "fs";
|
|
4
15
|
import { homedir } from "os";
|
|
5
|
-
|
|
16
|
+
function cacheDirectory(env = process.env, home = homedir()) {
|
|
17
|
+
return join(env["XDG_CACHE_HOME"]?.trim() || join(home, ".cache"), "opencode-claude-mem");
|
|
18
|
+
}
|
|
19
|
+
function statusKey(directory) {
|
|
20
|
+
let resolved = resolve(directory);
|
|
21
|
+
try {
|
|
22
|
+
resolved = realpathSync(directory);
|
|
23
|
+
} catch {}
|
|
24
|
+
return createHash("sha256").update(resolved).digest("hex").slice(0, 16);
|
|
25
|
+
}
|
|
26
|
+
function isAlive(pid) {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return error.code === "EPERM";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function parseStatus(file) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const { state, message, pid } = parsed;
|
|
41
|
+
return (state === "ok" || state === "problem") && typeof message === "string" && typeof pid === "number" && Number.isInteger(pid) && pid > 0 ? { state, message, pid } : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function readCaptureStatus(directory, dir = cacheDirectory()) {
|
|
47
|
+
const folder = join(dir, "status");
|
|
48
|
+
const prefix = `${statusKey(directory)}.`;
|
|
49
|
+
let names = [];
|
|
50
|
+
try {
|
|
51
|
+
names = readdirSync(folder).filter((name) => name.startsWith(prefix) && name.endsWith(".json"));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
let best = null;
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
const file = join(folder, name);
|
|
58
|
+
const parsed = parseStatus(file);
|
|
59
|
+
if (!parsed || !isAlive(parsed.pid)) {
|
|
60
|
+
rmSync(file, { force: true });
|
|
61
|
+
} else {
|
|
62
|
+
const own = parsed.pid === process.pid;
|
|
63
|
+
const at = statSync(file, { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
64
|
+
if (!best || own && !best.own || own === best.own && at > best.at) {
|
|
65
|
+
best = { status: { state: parsed.state, message: parsed.message }, own, at };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return best?.status ?? null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/worker-client.ts
|
|
73
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
74
|
+
import { join as join2, resolve as resolve2, win32 } from "path";
|
|
75
|
+
import { createHash as createHash2 } from "crypto";
|
|
76
|
+
import { homedir as homedir2 } from "os";
|
|
6
77
|
var DEFAULT_WORKER_HOST = "127.0.0.1";
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
78
|
+
var WORKER_PORT_BASE = 37700;
|
|
79
|
+
var TOKEN_HEADER = "x-claude-mem-token";
|
|
10
80
|
var PROJECT_COUNTS_SQL = `SELECT
|
|
11
81
|
(SELECT COUNT(*) FROM observations
|
|
12
82
|
WHERE project = ?1 COLLATE NOCASE OR merged_into_project = ?1 COLLATE NOCASE) AS observations,
|
|
@@ -36,25 +106,62 @@ function parseSettings(rawSettings) {
|
|
|
36
106
|
return {};
|
|
37
107
|
}
|
|
38
108
|
try {
|
|
39
|
-
const parsed = JSON.parse(rawSettings);
|
|
40
|
-
|
|
109
|
+
const parsed = JSON.parse(rawSettings.replace(/^\uFEFF/, ""));
|
|
110
|
+
if (!isRecord(parsed)) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
const nested = parsed["env"];
|
|
114
|
+
const source = isRecord(nested) && Object.keys(nested).some((key) => key.startsWith("CLAUDE_MEM_")) ? nested : parsed;
|
|
115
|
+
return Object.fromEntries(Object.entries(source).map(([key, value]) => [
|
|
116
|
+
key,
|
|
117
|
+
typeof value === "number" || typeof value === "boolean" ? String(value) : value
|
|
118
|
+
]));
|
|
41
119
|
} catch {
|
|
42
120
|
return {};
|
|
43
121
|
}
|
|
44
122
|
}
|
|
45
|
-
function
|
|
123
|
+
function dataDirectory(env, home) {
|
|
124
|
+
const dir = readString(env["CLAUDE_MEM_DATA_DIR"]) ?? join2(home, ".claude-mem");
|
|
125
|
+
return dir.replace(/^~(?=$|[\\/])/, home);
|
|
126
|
+
}
|
|
127
|
+
function defaultWorkerPort(dataDir, platform = process.platform, uid = typeof process.getuid === "function" ? process.getuid() : null) {
|
|
128
|
+
if (uid !== null) {
|
|
129
|
+
return WORKER_PORT_BASE + (uid % 100 + 100) % 100;
|
|
130
|
+
}
|
|
131
|
+
const normalized = platform === "win32" ? win32.resolve(dataDir).toLowerCase() : resolve2(dataDir);
|
|
132
|
+
const digest = createHash2("sha256").update(normalized, "utf8").digest();
|
|
133
|
+
return WORKER_PORT_BASE + digest.readUInt32BE(0) % 100;
|
|
134
|
+
}
|
|
135
|
+
function parseWorkerEndpoint(env, rawSettings, fallbackPort = defaultWorkerPort(dataDirectory(env, homedir2()))) {
|
|
46
136
|
const settings = parseSettings(rawSettings);
|
|
47
|
-
const
|
|
48
|
-
const
|
|
137
|
+
const configured = readString(env["CLAUDE_MEM_WORKER_HOST"]) ?? readString(settings["CLAUDE_MEM_WORKER_HOST"]) ?? DEFAULT_WORKER_HOST;
|
|
138
|
+
const host = configured === "localhost" ? DEFAULT_WORKER_HOST : configured;
|
|
139
|
+
const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? fallbackPort;
|
|
49
140
|
return { host, port };
|
|
50
141
|
}
|
|
142
|
+
function isLoopbackHost(host) {
|
|
143
|
+
const name = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
144
|
+
return name === "localhost" || name.endsWith(".localhost") || name === "::1" || /^127(?:\.\d{1,3}){1,3}$/.test(name);
|
|
145
|
+
}
|
|
146
|
+
function workerUrl(endpoint) {
|
|
147
|
+
const host = endpoint.host.includes(":") && !endpoint.host.startsWith("[") ? `[${endpoint.host}]` : endpoint.host;
|
|
148
|
+
return `http://${host}:${endpoint.port}`;
|
|
149
|
+
}
|
|
150
|
+
function tokenHeaders(baseUrl, rawSettings) {
|
|
151
|
+
const token = readString(parseSettings(rawSettings)["CLAUDE_MEM_LOCAL_API_TOKEN"]);
|
|
152
|
+
try {
|
|
153
|
+
return token && isLoopbackHost(new URL(baseUrl).hostname) ? { [TOKEN_HEADER]: token } : {};
|
|
154
|
+
} catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
51
158
|
function resolveDatabasePath(env, rawSettings, home) {
|
|
52
|
-
if (!
|
|
159
|
+
if (!isLoopbackHost(parseWorkerEndpoint(env, rawSettings).host)) {
|
|
53
160
|
return null;
|
|
54
161
|
}
|
|
55
162
|
const settings = parseSettings(rawSettings);
|
|
56
|
-
const dir = readString(env["CLAUDE_MEM_DATA_DIR"]) ?? readString(settings["CLAUDE_MEM_DATA_DIR"]) ??
|
|
57
|
-
return
|
|
163
|
+
const dir = readString(env["CLAUDE_MEM_DATA_DIR"]) ?? readString(settings["CLAUDE_MEM_DATA_DIR"]) ?? join2(home, ".claude-mem");
|
|
164
|
+
return join2(dir.replace(/^~(?=$|[\\/])/, home), "claude-mem.db");
|
|
58
165
|
}
|
|
59
166
|
function readCount(value) {
|
|
60
167
|
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
@@ -88,27 +195,45 @@ function readEpoch(item) {
|
|
|
88
195
|
}
|
|
89
196
|
function readSettingsRaw() {
|
|
90
197
|
try {
|
|
91
|
-
return
|
|
198
|
+
return readFileSync2(join2(dataDirectory(process.env, homedir2()), "settings.json"), "utf8");
|
|
92
199
|
} catch {
|
|
93
200
|
return null;
|
|
94
201
|
}
|
|
95
202
|
}
|
|
203
|
+
function readWorkerSetting(key) {
|
|
204
|
+
return readString(process.env[key]) ?? readString(parseSettings(readSettingsRaw())[key]);
|
|
205
|
+
}
|
|
206
|
+
function getWorkerEndpoint() {
|
|
207
|
+
return parseWorkerEndpoint(process.env, readSettingsRaw());
|
|
208
|
+
}
|
|
96
209
|
function getWorkerBaseUrl() {
|
|
97
|
-
|
|
98
|
-
|
|
210
|
+
return workerUrl(getWorkerEndpoint());
|
|
211
|
+
}
|
|
212
|
+
function workerHeaders(baseUrl = getWorkerBaseUrl()) {
|
|
213
|
+
return tokenHeaders(baseUrl, readSettingsRaw());
|
|
99
214
|
}
|
|
100
215
|
|
|
101
216
|
class WorkerClient {
|
|
102
217
|
static BASE_URL = getWorkerBaseUrl();
|
|
103
|
-
static
|
|
218
|
+
static SUPPORTED = !readWorkerSetting("CLAUDE_MEM_WORKER_URL");
|
|
219
|
+
static DATABASE_PATH = resolveDatabasePath(process.env, readSettingsRaw(), homedir2());
|
|
220
|
+
static get(path) {
|
|
221
|
+
if (!this.SUPPORTED) {
|
|
222
|
+
return Promise.reject(new Error("CLAUDE_MEM_WORKER_URL is not supported"));
|
|
223
|
+
}
|
|
224
|
+
return fetch(`${this.BASE_URL}${path}`, {
|
|
225
|
+
headers: workerHeaders(this.BASE_URL),
|
|
226
|
+
redirect: "error"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
104
229
|
static getProjectCounts(project) {
|
|
105
|
-
return this.DATABASE_PATH ? readProjectCounts(this.DATABASE_PATH, project) : Promise.resolve(null);
|
|
230
|
+
return this.DATABASE_PATH && this.SUPPORTED ? readProjectCounts(this.DATABASE_PATH, project) : Promise.resolve(null);
|
|
106
231
|
}
|
|
107
232
|
static async getStats() {
|
|
108
233
|
try {
|
|
109
234
|
const [statsRes, processingRes] = await Promise.all([
|
|
110
|
-
|
|
111
|
-
|
|
235
|
+
this.get(`/api/stats`),
|
|
236
|
+
this.get(`/api/processing-status`)
|
|
112
237
|
]);
|
|
113
238
|
if (!statsRes.ok) {
|
|
114
239
|
return null;
|
|
@@ -123,7 +248,7 @@ class WorkerClient {
|
|
|
123
248
|
static async getRecentSummaries(project, limit) {
|
|
124
249
|
try {
|
|
125
250
|
const params = new URLSearchParams({ project, limit: String(limit) });
|
|
126
|
-
const response = await
|
|
251
|
+
const response = await this.get(`/api/summaries?${params.toString()}`);
|
|
127
252
|
if (!response.ok) {
|
|
128
253
|
return [];
|
|
129
254
|
}
|
|
@@ -145,7 +270,7 @@ class WorkerClient {
|
|
|
145
270
|
static async getRecentObservations(project, limit) {
|
|
146
271
|
try {
|
|
147
272
|
const params = new URLSearchParams({ project, limit: String(limit) });
|
|
148
|
-
const response = await
|
|
273
|
+
const response = await this.get(`/api/observations?${params.toString()}`);
|
|
149
274
|
if (!response.ok) {
|
|
150
275
|
return [];
|
|
151
276
|
}
|
|
@@ -168,7 +293,7 @@ class WorkerClient {
|
|
|
168
293
|
static async getDetail(kind, id) {
|
|
169
294
|
try {
|
|
170
295
|
const path = kind === "observation" ? "observation" : "session";
|
|
171
|
-
const response = await
|
|
296
|
+
const response = await this.get(`/api/${path}/${id}`);
|
|
172
297
|
if (!response.ok) {
|
|
173
298
|
return null;
|
|
174
299
|
}
|
|
@@ -252,6 +377,9 @@ function summaryLine(view) {
|
|
|
252
377
|
if (!view.healthy) {
|
|
253
378
|
return { label: "(offline)", tone: "err" };
|
|
254
379
|
}
|
|
380
|
+
if (view.capture?.state === "problem") {
|
|
381
|
+
return { label: "(capture off)", tone: "err" };
|
|
382
|
+
}
|
|
255
383
|
if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
|
|
256
384
|
return { label: `(queue ${view.queueDepth ?? 0})`, tone: "warn" };
|
|
257
385
|
}
|
|
@@ -272,18 +400,50 @@ function bulletRow(icon, iconColor, value, theme, labelColor = theme.text) {
|
|
|
272
400
|
label(labelColor, decodeEntities(value))
|
|
273
401
|
]);
|
|
274
402
|
}
|
|
403
|
+
function leftClick(action) {
|
|
404
|
+
let pressed = false;
|
|
405
|
+
return {
|
|
406
|
+
onMouseDown(event) {
|
|
407
|
+
pressed = event.button === 0;
|
|
408
|
+
},
|
|
409
|
+
onMouseDrag() {
|
|
410
|
+
pressed = false;
|
|
411
|
+
},
|
|
412
|
+
onMouseOut() {
|
|
413
|
+
pressed = false;
|
|
414
|
+
},
|
|
415
|
+
onMouseUp(event) {
|
|
416
|
+
const clicked = pressed && event.button === 0;
|
|
417
|
+
pressed = false;
|
|
418
|
+
if (clicked) {
|
|
419
|
+
event.stopPropagation();
|
|
420
|
+
action();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
275
425
|
function recentRow(icon, iconColor, title, age, theme, onSelect) {
|
|
276
|
-
return box({ flexDirection: "column", ...onSelect ?
|
|
426
|
+
return box({ flexDirection: "column", ...onSelect ? leftClick(onSelect) : {} }, [
|
|
277
427
|
bulletRow(icon, iconColor, title, theme),
|
|
278
428
|
...age ? [box({ paddingLeft: 2 }, [label(theme.textMuted, age)])] : []
|
|
279
429
|
]);
|
|
280
430
|
}
|
|
281
431
|
function expandedRows(view, theme, onSelect) {
|
|
432
|
+
const notice = view.capture?.message ? [
|
|
433
|
+
box({ flexDirection: "row", gap: 1 }, [
|
|
434
|
+
text({ fg: theme.warning, flexShrink: 0 }, "!"),
|
|
435
|
+
text({
|
|
436
|
+
fg: view.capture.state === "problem" ? theme.error : theme.warning,
|
|
437
|
+
wrapMode: "word"
|
|
438
|
+
}, view.capture.message)
|
|
439
|
+
])
|
|
440
|
+
] : [];
|
|
282
441
|
if (!view.healthy) {
|
|
283
|
-
return [bulletRow("\u2022", theme.error, "worker offline", theme)];
|
|
442
|
+
return [bulletRow("\u2022", theme.error, "worker offline", theme), ...notice];
|
|
284
443
|
}
|
|
285
444
|
const counts = `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`;
|
|
286
445
|
const rows = [
|
|
446
|
+
...notice,
|
|
287
447
|
bulletRow("\u2022", theme.success, view.scope === "all" ? `all \xB7 ${counts}` : counts, theme)
|
|
288
448
|
];
|
|
289
449
|
if (view.scope === "project") {
|
|
@@ -312,7 +472,7 @@ function expandedRows(view, theme, onSelect) {
|
|
|
312
472
|
}
|
|
313
473
|
function buildMemNodes(view, theme, collapsed, onToggle, onSelect) {
|
|
314
474
|
const summary = summaryLine(view);
|
|
315
|
-
const header = box({ flexDirection: "row", gap: 1,
|
|
475
|
+
const header = box({ flexDirection: "row", gap: 1, ...leftClick(onToggle) }, [
|
|
316
476
|
text({ fg: theme.text, flexShrink: 0 }, collapsed ? "\u25B6" : "\u25BC"),
|
|
317
477
|
text({ fg: theme.info, attributes: 1, flexShrink: 0 }, "Memory"),
|
|
318
478
|
collapsed ? text({ fg: toneColor(summary.tone, theme) }, summary.label) : label(theme.textMuted, view.project)
|
|
@@ -330,14 +490,16 @@ function viewKey(view) {
|
|
|
330
490
|
function readCount2(value) {
|
|
331
491
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
332
492
|
}
|
|
333
|
-
async function readMemView(project, collapsed) {
|
|
493
|
+
async function readMemView(project, collapsed, directory = "") {
|
|
334
494
|
const [stats, counts] = await Promise.all([
|
|
335
495
|
WorkerClient.getStats(),
|
|
336
496
|
WorkerClient.getProjectCounts(project)
|
|
337
497
|
]);
|
|
498
|
+
const capture = directory ? readCaptureStatus(directory) : null;
|
|
338
499
|
if (!stats) {
|
|
339
500
|
return {
|
|
340
501
|
healthy: false,
|
|
502
|
+
capture,
|
|
341
503
|
project,
|
|
342
504
|
scope: "all",
|
|
343
505
|
totalObservations: null,
|
|
@@ -358,6 +520,7 @@ async function readMemView(project, collapsed) {
|
|
|
358
520
|
const totalObservations = readCount2(stats.database?.observations);
|
|
359
521
|
return {
|
|
360
522
|
healthy: true,
|
|
523
|
+
capture,
|
|
361
524
|
project,
|
|
362
525
|
scope: counts ? "project" : "all",
|
|
363
526
|
totalObservations,
|
|
@@ -446,6 +609,7 @@ var REFRESH_EVENTS = ["session.created", "session.execution.succeeded"];
|
|
|
446
609
|
function emptyView(project) {
|
|
447
610
|
return {
|
|
448
611
|
healthy: false,
|
|
612
|
+
capture: null,
|
|
449
613
|
project,
|
|
450
614
|
scope: "all",
|
|
451
615
|
totalObservations: null,
|
|
@@ -466,7 +630,8 @@ var cli_default = define({
|
|
|
466
630
|
if (!solid) {
|
|
467
631
|
return;
|
|
468
632
|
}
|
|
469
|
-
const
|
|
633
|
+
const directory = String(ctx.location?.directory ?? process.cwd());
|
|
634
|
+
const project = directory.split(/[\\/]/).filter(Boolean).findLast(Boolean) ?? "unknown-project";
|
|
470
635
|
const [state, setState] = ctx.storage.memory("claude-mem.sidebar", {
|
|
471
636
|
initial: { collapsed: true, view: emptyView(project) }
|
|
472
637
|
});
|
|
@@ -487,7 +652,7 @@ var cli_default = define({
|
|
|
487
652
|
do {
|
|
488
653
|
refreshPending = false;
|
|
489
654
|
const { collapsed } = state;
|
|
490
|
-
const next = await readMemView(project, collapsed);
|
|
655
|
+
const next = await readMemView(project, collapsed, directory);
|
|
491
656
|
if (disposed) {
|
|
492
657
|
return;
|
|
493
658
|
}
|
|
@@ -531,7 +696,9 @@ var cli_default = define({
|
|
|
531
696
|
});
|
|
532
697
|
return;
|
|
533
698
|
}
|
|
534
|
-
return
|
|
699
|
+
return new Promise((closed) => {
|
|
700
|
+
ctx.ui.dialog.show(() => renderDetail(ctx, solid, detail), closed);
|
|
701
|
+
});
|
|
535
702
|
}).catch(() => {}).finally(() => {
|
|
536
703
|
opening = false;
|
|
537
704
|
});
|
|
@@ -596,6 +763,47 @@ function themeFromResolved(resolved) {
|
|
|
596
763
|
borderSubtle: resolved.border.base
|
|
597
764
|
};
|
|
598
765
|
}
|
|
766
|
+
function renderDetail(ctx, solid, detail) {
|
|
767
|
+
ctx.ui.dialog.set({ size: "xlarge", centered: true });
|
|
768
|
+
const panel = solid.createElement("box");
|
|
769
|
+
solid.setProp(panel, "flexDirection", "column");
|
|
770
|
+
solid.setProp(panel, "padding", 1);
|
|
771
|
+
solid.setProp(panel, "gap", 1);
|
|
772
|
+
const header = solid.createElement("box");
|
|
773
|
+
solid.setProp(header, "flexDirection", "row");
|
|
774
|
+
solid.setProp(header, "gap", 2);
|
|
775
|
+
const title = solid.createElement("text");
|
|
776
|
+
solid.setProp(title, "fg", ctx.theme.text.base);
|
|
777
|
+
solid.setProp(title, "attributes", 1);
|
|
778
|
+
solid.setProp(title, "flexGrow", 1);
|
|
779
|
+
solid.setProp(title, "wrapMode", "none");
|
|
780
|
+
solid.setProp(title, "truncate", true);
|
|
781
|
+
solid.insert(title, detail.title);
|
|
782
|
+
solid.insert(header, title);
|
|
783
|
+
const close = solid.createElement("text");
|
|
784
|
+
solid.setProp(close, "fg", ctx.theme.text.muted);
|
|
785
|
+
solid.setProp(close, "flexShrink", 0);
|
|
786
|
+
solid.setProp(close, "onMouseUp", (event) => {
|
|
787
|
+
if (event.button === 0) {
|
|
788
|
+
ctx.ui.dialog.clear();
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
solid.insert(close, "esc close");
|
|
792
|
+
solid.insert(header, close);
|
|
793
|
+
solid.insert(panel, header);
|
|
794
|
+
const body = solid.createElement("scrollbox");
|
|
795
|
+
solid.setProp(body, "scrollX", false);
|
|
796
|
+
const sizeBody = (height) => solid.setProp(body, "maxHeight", Math.max(1, Math.floor(height * 0.75) - 4));
|
|
797
|
+
sizeBody(ctx.renderer.height);
|
|
798
|
+
solid.onResize((_width, height) => sizeBody(height));
|
|
799
|
+
const message = solid.createElement("text");
|
|
800
|
+
solid.setProp(message, "fg", ctx.theme.text.base);
|
|
801
|
+
solid.setProp(message, "wrapMode", "word");
|
|
802
|
+
solid.insert(message, detail.message);
|
|
803
|
+
solid.insert(body, message);
|
|
804
|
+
solid.insert(panel, body);
|
|
805
|
+
return panel;
|
|
806
|
+
}
|
|
599
807
|
function materializeNode(node, solid) {
|
|
600
808
|
const element = solid.createElement(node.kind);
|
|
601
809
|
for (const [name, value] of Object.entries(node.props)) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface OfficialFiles {
|
|
2
|
+
readonly version: string;
|
|
3
|
+
/** The official OpenCode plugin: default export `{ id, server, setup }`. */
|
|
4
|
+
readonly plugin: string;
|
|
5
|
+
readonly mcpServer: string;
|
|
6
|
+
}
|
|
7
|
+
export type SyncResult = {
|
|
8
|
+
readonly ok: true;
|
|
9
|
+
readonly files: OfficialFiles;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
};
|
|
14
|
+
export interface SyncOptions {
|
|
15
|
+
/** Directory holding one sub-directory per version. */
|
|
16
|
+
readonly root?: string;
|
|
17
|
+
readonly registry?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function officialRoot(): string;
|
|
20
|
+
/** `CLAUDE_MEM_NPM_REGISTRY`, then npm's own registry setting, then the public registry. */
|
|
21
|
+
export declare function npmRegistry(env?: Record<string, string | undefined>): string;
|
|
22
|
+
/**
|
|
23
|
+
* Reads the named regular files out of a gzipped tar. npm tarballs are ustar; a path longer
|
|
24
|
+
* than the name field uses the prefix field, and anything else (pax records, directories)
|
|
25
|
+
* is skipped by its size. No `tar` binary is needed, which Windows may not have on PATH.
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractTarFiles(archive: Uint8Array, members: readonly string[]): Map<string, Uint8Array>;
|
|
28
|
+
/** The official files for `version`, downloading them once when they are not on disk. */
|
|
29
|
+
export declare function syncOfficial(version: string, options?: SyncOptions): Promise<SyncResult>;
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode V2 server plugin: one package entry that brings claude-mem to OpenCode.
|
|
3
|
+
*
|
|
4
|
+
* Capture and context injection stay upstream code: this plugin downloads the official
|
|
5
|
+
* claude-mem OpenCode plugin at the running Worker's version and runs its `setup` with
|
|
6
|
+
* this plugin's context. On top of that it registers the memory tools the official plugin
|
|
7
|
+
* leaves to claude-mem's MCP server, starts the Worker when it is not running, and reports
|
|
8
|
+
* what happened to the sidebar. Loaded from `opencode.json`:
|
|
9
|
+
*
|
|
10
|
+
* ```json
|
|
11
|
+
* { "plugins": ["@ephemushroom/opencode-claude-mem"] }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* `{ "package": "...", "options": { "mcp": true } }` registers claude-mem's MCP server
|
|
15
|
+
* (all of its tools, in a separate process) instead of this plugin's own tools.
|
|
16
|
+
*/
|
|
17
|
+
import { type CaptureStatusWriter } from './capture-status';
|
|
18
|
+
import type { Plugin } from '@opencode/plugin';
|
|
19
|
+
type Cleanup = () => Promise<void> | void;
|
|
20
|
+
/** What a capture pass needs from the host; `Plugin.Context` satisfies it. */
|
|
21
|
+
export interface CaptureContext {
|
|
22
|
+
readonly mcp: Pick<Plugin.Context['mcp'], 'transform'>;
|
|
23
|
+
/** The host's plugins for this checkout; its result is read defensively. */
|
|
24
|
+
readonly plugin: {
|
|
25
|
+
readonly list: () => Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* What the host says about a plugin with the official id: loaded and running, loaded and
|
|
30
|
+
* failed, not among its plugins, or `unknown` while its list is still empty.
|
|
31
|
+
*/
|
|
32
|
+
export type OfficialPluginState = 'active' | 'failed' | 'absent' | 'unknown';
|
|
33
|
+
export declare function officialPluginState(ctx: CaptureContext): Promise<OfficialPluginState>;
|
|
34
|
+
/** One `setup` call: the host runs it once per checkout it serves. */
|
|
35
|
+
export interface CaptureSession {
|
|
36
|
+
readonly ctx: CaptureContext;
|
|
37
|
+
readonly directory: string;
|
|
38
|
+
readonly status: CaptureStatusWriter;
|
|
39
|
+
readonly useMcp: boolean;
|
|
40
|
+
readonly keep: (cleanup: Cleanup) => void;
|
|
41
|
+
readonly disposed: () => boolean;
|
|
42
|
+
/** Something records this checkout's sessions: the copy loaded here, or a standalone one the host confirmed. */
|
|
43
|
+
capturing: boolean;
|
|
44
|
+
mcpRegistered: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True when the official plugin exists as a standalone file, the way
|
|
48
|
+
* `npx claude-mem install --ide opencode` leaves it in the config directory, or a project
|
|
49
|
+
* keeps it under `.opencode` in the checkout or a directory above it. OpenCode normally
|
|
50
|
+
* loads that file by itself, and a second copy here would record every prompt and tool
|
|
51
|
+
* call twice. The file alone proves nothing: whether it runs is the host's to say
|
|
52
|
+
* (`officialPluginState`).
|
|
53
|
+
*/
|
|
54
|
+
export declare function standalonePluginInstalled(env: Record<string, string | undefined>, home: string, directory: string): boolean;
|
|
55
|
+
/** claude-mem's MCP server is a Node script; without Node the host binary runs it as Bun. */
|
|
56
|
+
export declare function mcpServerCommand(script: string, node: string | null, host: string): {
|
|
57
|
+
command: string[];
|
|
58
|
+
environment?: Record<string, string>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* One pass over what is still missing: a running Worker, the official files at exactly its
|
|
62
|
+
* version, capture, and the MCP server when asked for. Resolves true when nothing is left
|
|
63
|
+
* to do. Each step is recorded only after it succeeded, so a later pass never repeats it:
|
|
64
|
+
* a second `setup` of the official plugin would record everything twice. Only the first
|
|
65
|
+
* pass starts the Worker; later ones look at what is there, so a Worker that crashes on
|
|
66
|
+
* start is not launched again every minute.
|
|
67
|
+
*/
|
|
68
|
+
export declare function attemptCapture(session: CaptureSession, first: boolean): Promise<boolean>;
|
|
69
|
+
declare const plugin: Plugin.Plugin;
|
|
70
|
+
export default plugin;
|