@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/sidebar-model.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { type CaptureStatus } from './capture-status';
|
|
1
2
|
export declare const POLL_INTERVAL_MS = 5000;
|
|
2
3
|
export declare const OBSERVATION_TYPE_ICONS: Record<string, string>;
|
|
3
4
|
export interface MemSidebarView {
|
|
4
5
|
readonly healthy: boolean;
|
|
6
|
+
/** What this package's server part reported; null when it is not loaded. */
|
|
7
|
+
readonly capture: CaptureStatus | null;
|
|
5
8
|
readonly project: string;
|
|
6
9
|
/** `project` when counts come from the Worker database; `all` for the global fallback. */
|
|
7
10
|
readonly scope: 'project' | 'all';
|
|
@@ -65,7 +68,7 @@ export declare function bulletRow(icon: string, iconColor: unknown, value: strin
|
|
|
65
68
|
export declare function expandedRows(view: MemSidebarView, theme: Theme, onSelect?: RecentItemHandler): ViewNode[];
|
|
66
69
|
export declare function buildMemNodes(view: MemSidebarView, theme: Theme, collapsed: boolean, onToggle: () => void, onSelect?: RecentItemHandler): ViewNode[];
|
|
67
70
|
export declare function viewKey(view: MemSidebarView): string;
|
|
68
|
-
export declare function readMemView(project: string, collapsed: boolean): Promise<MemSidebarView>;
|
|
71
|
+
export declare function readMemView(project: string, collapsed: boolean, directory?: string): Promise<MemSidebarView>;
|
|
69
72
|
export interface MemDetail {
|
|
70
73
|
readonly title: string;
|
|
71
74
|
readonly message: string;
|
package/dist/tui.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,
|
|
@@ -428,8 +591,9 @@ var tuiModule = {
|
|
|
428
591
|
}
|
|
429
592
|
];
|
|
430
593
|
};
|
|
431
|
-
const
|
|
432
|
-
const
|
|
594
|
+
const directory = String(api.state?.path?.directory ?? "");
|
|
595
|
+
const project = projectNameFromDirectory(directory);
|
|
596
|
+
const initialView = await readMemView(project, true, directory);
|
|
433
597
|
const [collapsed, setCollapsed] = signal(true);
|
|
434
598
|
const [view, setView] = signal(initialView);
|
|
435
599
|
let currentKey = viewKey(initialView);
|
|
@@ -437,7 +601,7 @@ var tuiModule = {
|
|
|
437
601
|
let inFlight = false;
|
|
438
602
|
let timer = null;
|
|
439
603
|
const refresh = async () => {
|
|
440
|
-
const nextView = await readMemView(project, collapsed());
|
|
604
|
+
const nextView = await readMemView(project, collapsed(), directory);
|
|
441
605
|
const nextKey = viewKey(nextView);
|
|
442
606
|
if (nextKey !== currentKey) {
|
|
443
607
|
currentKey = nextKey;
|
package/dist/worker-client.d.ts
CHANGED
|
@@ -7,7 +7,26 @@ export interface ProjectCounts {
|
|
|
7
7
|
readonly summaries: number;
|
|
8
8
|
readonly sessions: number;
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
type JsonRecord = Record<string, unknown>;
|
|
11
|
+
export declare function isRecord(value: unknown): value is JsonRecord;
|
|
12
|
+
/** claude-mem's data directory, which holds its settings.json: the environment, else ~/.claude-mem. */
|
|
13
|
+
export declare function dataDirectory(env: Record<string, string | undefined>, home: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* The port a Worker uses when none is configured: 37700 plus the user id's last two
|
|
16
|
+
* digits, or on Windows (no user id) a number derived from the data directory.
|
|
17
|
+
*/
|
|
18
|
+
export declare function defaultWorkerPort(dataDir: string, platform?: NodeJS.Platform, uid?: number | null): number;
|
|
19
|
+
export declare function parseWorkerEndpoint(env: Record<string, string | undefined>, rawSettings: string | null, fallbackPort?: number): WorkerEndpoint;
|
|
20
|
+
/** True for this machine: localhost, *.localhost, ::1 and 127.0.0.0/8. */
|
|
21
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
22
|
+
/** An IPv6 host needs brackets inside a URL. */
|
|
23
|
+
export declare function workerUrl(endpoint: WorkerEndpoint): string;
|
|
24
|
+
/**
|
|
25
|
+
* The header a Worker with `CLAUDE_MEM_LOCAL_API_TOKEN` requires on everything but its
|
|
26
|
+
* health check. The token is read from the settings file only, and is sent only to a
|
|
27
|
+
* Worker on this machine.
|
|
28
|
+
*/
|
|
29
|
+
export declare function tokenHeaders(baseUrl: string, rawSettings: string | null): Record<string, string>;
|
|
11
30
|
/**
|
|
12
31
|
* The Worker's SQLite database: CLAUDE_MEM_DATA_DIR from env, then settings, then
|
|
13
32
|
* ~/.claude-mem. Null when the Worker is not on this machine, because a local file would
|
|
@@ -16,10 +35,17 @@ export declare function parseWorkerEndpoint(env: Record<string, string | undefin
|
|
|
16
35
|
export declare function resolveDatabasePath(env: Record<string, string | undefined>, rawSettings: string | null, home: string): string | null;
|
|
17
36
|
/** Read-only per-project counts; null on a missing, locked or unexpected database. */
|
|
18
37
|
export declare function readProjectCounts(path: string, project: string): Promise<ProjectCounts | null>;
|
|
38
|
+
/** A claude-mem setting: the environment first, then the data directory's settings.json. */
|
|
39
|
+
export declare function readWorkerSetting(key: string): string | null;
|
|
40
|
+
export declare function getWorkerEndpoint(): WorkerEndpoint;
|
|
19
41
|
export declare function getWorkerBaseUrl(): string;
|
|
42
|
+
/** Headers for a request to `baseUrl`: the local token when that Worker needs one. */
|
|
43
|
+
export declare function workerHeaders(baseUrl?: string): Record<string, string>;
|
|
20
44
|
export declare class WorkerClient {
|
|
21
45
|
private static readonly BASE_URL;
|
|
46
|
+
private static readonly SUPPORTED;
|
|
22
47
|
private static readonly DATABASE_PATH;
|
|
48
|
+
private static get;
|
|
23
49
|
/**
|
|
24
50
|
* Per-project counts, read from the Worker's database in read-only mode. The Worker's
|
|
25
51
|
* HTTP API only reports global totals. Null when the database is remote, missing,
|
|
@@ -64,3 +90,4 @@ export declare class WorkerClient {
|
|
|
64
90
|
*/
|
|
65
91
|
static getDetail(kind: 'summary' | 'observation', id: number): Promise<Record<string, unknown> | null>;
|
|
66
92
|
}
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type WorkerState =
|
|
2
|
+
/** `version` is null when the Worker answered without a readable version. */
|
|
3
|
+
{
|
|
4
|
+
readonly status: 'running';
|
|
5
|
+
readonly version: string | null;
|
|
6
|
+
}
|
|
7
|
+
/** No launcher: claude-mem is not installed on this machine. */
|
|
8
|
+
| {
|
|
9
|
+
readonly status: 'missing';
|
|
10
|
+
}
|
|
11
|
+
/** Not running, and this machine is told not to start it (or it runs elsewhere). */
|
|
12
|
+
| {
|
|
13
|
+
readonly status: 'disabled';
|
|
14
|
+
}
|
|
15
|
+
/** The launcher ran, and the port still does not answer. */
|
|
16
|
+
| {
|
|
17
|
+
readonly status: 'failed';
|
|
18
|
+
};
|
|
19
|
+
export interface WorkerStartDeps {
|
|
20
|
+
/** Null only when the connection was refused, meaning no Worker holds the port. */
|
|
21
|
+
readonly probe: () => Promise<{
|
|
22
|
+
version: string | null;
|
|
23
|
+
} | null>;
|
|
24
|
+
readonly disabled: () => boolean;
|
|
25
|
+
readonly launcher: () => string | null;
|
|
26
|
+
/** Resolves false when the launcher could not be spawned. */
|
|
27
|
+
readonly start: (launcher: string) => Promise<boolean>;
|
|
28
|
+
readonly waitMs: number;
|
|
29
|
+
readonly pollMs: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* True for a refused connection, however the runtime reports it: Bun sets
|
|
33
|
+
* `code: 'ConnectionRefused'`, Node keeps ECONNREFUSED on `cause` or nested `errors`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isConnectionRefused(error: unknown, seen?: Set<unknown>): boolean;
|
|
36
|
+
/** Where `npx claude-mem install` places the launcher (claude-mem's install-launcher.ts). */
|
|
37
|
+
export declare function launcherPath(platform: NodeJS.Platform, env: Record<string, string | undefined>, home: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* claude-mem's own switches for "do not start a Worker here": an explicit Worker URL,
|
|
40
|
+
* `CLAUDE_MEM_WORKER_AUTOSTART=false`, or a Worker host that is another machine.
|
|
41
|
+
*/
|
|
42
|
+
export declare function autostartDisabled(read: (key: string) => string | null, host?: string): boolean;
|
|
43
|
+
/** The Worker as it is now, without starting anything. */
|
|
44
|
+
export declare function workerState(deps?: WorkerStartDeps): Promise<WorkerState>;
|
|
45
|
+
/** One start at a time per process; every caller during it gets the same answer. */
|
|
46
|
+
export declare function ensureWorker(deps?: WorkerStartDeps): Promise<WorkerState>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One namespace for every tool: a direct tool call sees `claude_mem_<name>`, Code Mode
|
|
3
|
+
* sees `tools.claude_mem.<name>` under a single catalog entry with this description.
|
|
4
|
+
*/
|
|
5
|
+
export declare const TOOL_NAMESPACE: {
|
|
6
|
+
readonly name: "claude_mem";
|
|
7
|
+
readonly description: "claude-mem memory beyond search. work_state_write / work_state_read are your canonical to-do list and working state for this project: use them instead of any built-in to-do tool. save_memory records a durable note. dumb_search is a one-call lookup; get_tool_uses fetches raw tool I/O; the corpus tools build and query knowledge corpora.";
|
|
8
|
+
};
|
|
9
|
+
export interface WorkerTool {
|
|
10
|
+
/** Without the namespace: `save_memory`, not `claude_mem_save_memory`. */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly input: Record<string, unknown>;
|
|
14
|
+
readonly options: {
|
|
15
|
+
readonly namespace: string;
|
|
16
|
+
readonly pinned?: true;
|
|
17
|
+
};
|
|
18
|
+
/** `context.signal` aborts when the session stops; the request is dropped with it. */
|
|
19
|
+
readonly execute: (input: unknown, context?: {
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
}) => Promise<{
|
|
22
|
+
content: string;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkerToolOptions {
|
|
26
|
+
/** The session's checkout: the Worker derives the project from it. */
|
|
27
|
+
readonly directory: string;
|
|
28
|
+
readonly baseUrl?: string;
|
|
29
|
+
/** Starts the Worker after a refused connection; resolves true when it answers. */
|
|
30
|
+
readonly startWorker?: () => Promise<boolean>;
|
|
31
|
+
readonly corpusIdleTimeoutMs?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function createWorkerTools(options: WorkerToolOptions): WorkerTool[];
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ephemushroom/opencode-claude-mem",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.2",
|
|
4
|
+
"description": "Claude-Mem for OpenCode: capture through the official claude-mem plugin, memory tools and the Memory sidebar in one package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
8
8
|
],
|
|
9
9
|
"exports": {
|
|
10
|
+
"./server": {
|
|
11
|
+
"types": "./dist/server.d.ts",
|
|
12
|
+
"import": "./dist/server.js"
|
|
13
|
+
},
|
|
10
14
|
"./tui": {
|
|
11
15
|
"types": "./dist/package-tui.d.ts",
|
|
12
16
|
"import": "./dist/package-tui.js"
|
|
@@ -17,7 +21,7 @@
|
|
|
17
21
|
}
|
|
18
22
|
},
|
|
19
23
|
"scripts": {
|
|
20
|
-
"build": "bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid --external solid-js && bun build src/cli.ts --outfile dist/cli.js --target bun --format esm --external @opentui/solid --external solid-js && bun src/build-tui-bridge.ts && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
24
|
+
"build": "bun build src/server.ts --outfile dist/server.js --target bun --format esm && bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid --external solid-js && bun build src/cli.ts --outfile dist/cli.js --target bun --format esm --external @opentui/solid --external solid-js && bun src/build-tui-bridge.ts && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
21
25
|
"dev": "tsc --watch",
|
|
22
26
|
"clean": "rm -rf dist",
|
|
23
27
|
"lint": "oxlint",
|