@ephemushroom/opencode-claude-mem 0.3.2 → 0.4.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 +43 -18
- package/dist/index.js +12451 -4
- package/dist/tui.d.ts +32 -0
- package/dist/tui.js +488 -0
- package/dist/worker-client.d.ts +31 -0
- package/package.json +7 -5
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface MemSidebarView {
|
|
2
|
+
readonly healthy: boolean;
|
|
3
|
+
readonly project: string;
|
|
4
|
+
readonly observations: number | null;
|
|
5
|
+
readonly sessions: number | null;
|
|
6
|
+
readonly summaries: number | null;
|
|
7
|
+
readonly queueDepth: number | null;
|
|
8
|
+
readonly isProcessing: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface ViewNode {
|
|
11
|
+
readonly kind: 'box' | 'text';
|
|
12
|
+
readonly props: Record<string, unknown>;
|
|
13
|
+
readonly children?: readonly ViewNode[];
|
|
14
|
+
readonly text?: string;
|
|
15
|
+
}
|
|
16
|
+
interface Theme {
|
|
17
|
+
readonly text: string;
|
|
18
|
+
readonly textMuted: string;
|
|
19
|
+
readonly info: string;
|
|
20
|
+
readonly success: string;
|
|
21
|
+
readonly warning: string;
|
|
22
|
+
readonly error: string;
|
|
23
|
+
readonly borderSubtle: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function buildMemNodes(view: MemSidebarView, theme: Theme): ViewNode[];
|
|
26
|
+
export declare function viewKey(view: MemSidebarView): string;
|
|
27
|
+
export declare function readMemView(project: string): Promise<MemSidebarView>;
|
|
28
|
+
declare const tuiModule: {
|
|
29
|
+
id: string;
|
|
30
|
+
tui: (api: any) => Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
export default tuiModule;
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/worker-client.ts
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
var HEALTH_TIMEOUT_MS = 1000;
|
|
10
|
+
var AUTOSTART_POLL_INTERVAL_MS = 500;
|
|
11
|
+
var AUTOSTART_POLL_MAX_ATTEMPTS = 16;
|
|
12
|
+
var DEFAULT_WORKER_HOST = "127.0.0.1";
|
|
13
|
+
var DEFAULT_WORKER_PORT = 37777;
|
|
14
|
+
var PLATFORM_SOURCE = "opencode";
|
|
15
|
+
var SETTINGS_FILE = join(homedir(), ".claude-mem", "settings.json");
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function readString(value) {
|
|
20
|
+
if (typeof value !== "string") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
return trimmed || null;
|
|
25
|
+
}
|
|
26
|
+
function readPort(value) {
|
|
27
|
+
const text = readString(value);
|
|
28
|
+
if (!text || !/^\d+$/.test(text)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const port = Number(text);
|
|
32
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null;
|
|
33
|
+
}
|
|
34
|
+
function parseSettings(rawSettings) {
|
|
35
|
+
if (!rawSettings) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(rawSettings);
|
|
40
|
+
return isRecord(parsed) ? parsed : {};
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parseWorkerEndpoint(env, rawSettings) {
|
|
46
|
+
const settings = parseSettings(rawSettings);
|
|
47
|
+
const host = readString(env["CLAUDE_MEM_WORKER_HOST"]) ?? readString(settings["CLAUDE_MEM_WORKER_HOST"]) ?? DEFAULT_WORKER_HOST;
|
|
48
|
+
const port = readPort(env["CLAUDE_MEM_WORKER_PORT"]) ?? readPort(settings["CLAUDE_MEM_WORKER_PORT"]) ?? DEFAULT_WORKER_PORT;
|
|
49
|
+
return { host, port };
|
|
50
|
+
}
|
|
51
|
+
function readSettingsRaw() {
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(SETTINGS_FILE, "utf8");
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getWorkerBaseUrl() {
|
|
59
|
+
const endpoint = parseWorkerEndpoint(process.env, readSettingsRaw());
|
|
60
|
+
return `http://${endpoint.host}:${endpoint.port}`;
|
|
61
|
+
}
|
|
62
|
+
function extractTextContent(value) {
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
return value.trim() || null;
|
|
65
|
+
}
|
|
66
|
+
if (!isRecord(value)) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const { content } = value;
|
|
70
|
+
if (typeof content === "string") {
|
|
71
|
+
return content.trim() || null;
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(content)) {
|
|
74
|
+
const text2 = content.map((part) => {
|
|
75
|
+
if (!isRecord(part) || part["type"] !== "text" || typeof part["text"] !== "string") {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
return part["text"];
|
|
79
|
+
}).filter(Boolean).join(`
|
|
80
|
+
`).trim();
|
|
81
|
+
return text2 || null;
|
|
82
|
+
}
|
|
83
|
+
const text = JSON.stringify(value, null, 2);
|
|
84
|
+
return text === "{}" || text === "null" ? null : text;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class WorkerClient {
|
|
88
|
+
static BASE_URL = getWorkerBaseUrl();
|
|
89
|
+
static autoStartPromise = null;
|
|
90
|
+
static async isHealthy() {
|
|
91
|
+
try {
|
|
92
|
+
const controller = new AbortController;
|
|
93
|
+
const timeoutId = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
94
|
+
const response = await fetch(`${this.BASE_URL}/api/health`, {
|
|
95
|
+
signal: controller.signal
|
|
96
|
+
});
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
return response.ok;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
static async ensureRunning() {
|
|
104
|
+
const result = await this.tryAutoStart();
|
|
105
|
+
return result === "already-running" || result === "started";
|
|
106
|
+
}
|
|
107
|
+
static tryAutoStart() {
|
|
108
|
+
if (!this.autoStartPromise) {
|
|
109
|
+
this.autoStartPromise = this.runAutoStart().catch(() => "spawn-failed");
|
|
110
|
+
}
|
|
111
|
+
return this.autoStartPromise;
|
|
112
|
+
}
|
|
113
|
+
static async runAutoStart() {
|
|
114
|
+
if (await this.isHealthy()) {
|
|
115
|
+
return "already-running";
|
|
116
|
+
}
|
|
117
|
+
const bunPath = this.findBun();
|
|
118
|
+
if (!bunPath) {
|
|
119
|
+
return "no-bun";
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const child = spawn(bunPath, ["x", "claude-mem", "start"], {
|
|
123
|
+
detached: true,
|
|
124
|
+
stdio: "ignore",
|
|
125
|
+
windowsHide: true,
|
|
126
|
+
env: process.env
|
|
127
|
+
});
|
|
128
|
+
child.on("error", () => {});
|
|
129
|
+
child.unref();
|
|
130
|
+
} catch {
|
|
131
|
+
return "spawn-failed";
|
|
132
|
+
}
|
|
133
|
+
for (let i = 0;i < AUTOSTART_POLL_MAX_ATTEMPTS; i++) {
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, AUTOSTART_POLL_INTERVAL_MS));
|
|
135
|
+
if (await this.isHealthy()) {
|
|
136
|
+
return "started";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return "timeout";
|
|
140
|
+
}
|
|
141
|
+
static findBun() {
|
|
142
|
+
const bunGlobal = globalThis.Bun;
|
|
143
|
+
if (bunGlobal && typeof bunGlobal.which === "function") {
|
|
144
|
+
try {
|
|
145
|
+
const found = bunGlobal.which("bun");
|
|
146
|
+
if (typeof found === "string" && found) {
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
const path = process.env.PATH || "";
|
|
152
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
153
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
154
|
+
const slash = process.platform === "win32" ? "\\" : "/";
|
|
155
|
+
const dirs = path.split(sep).filter(Boolean);
|
|
156
|
+
for (const dir of dirs) {
|
|
157
|
+
const trailing = dir.endsWith("/") || dir.endsWith("\\") ? "" : slash;
|
|
158
|
+
for (const ext of exts) {
|
|
159
|
+
const candidate = `${dir}${trailing}bun${ext}`;
|
|
160
|
+
try {
|
|
161
|
+
if (existsSync(candidate)) {
|
|
162
|
+
return candidate;
|
|
163
|
+
}
|
|
164
|
+
} catch {}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
static async sessionInit(contentSessionId, project, prompt) {
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetch(`${this.BASE_URL}/api/sessions/init`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/json" },
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
contentSessionId,
|
|
176
|
+
project,
|
|
177
|
+
prompt,
|
|
178
|
+
platformSource: PLATFORM_SOURCE
|
|
179
|
+
})
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return await response.json();
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
static async sendObservation(contentSessionId, toolName, toolInput, toolResponse, cwd) {
|
|
190
|
+
try {
|
|
191
|
+
await fetch(`${this.BASE_URL}/api/sessions/observations`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "Content-Type": "application/json" },
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
contentSessionId,
|
|
196
|
+
tool_name: toolName,
|
|
197
|
+
tool_input: toolInput,
|
|
198
|
+
tool_response: toolResponse,
|
|
199
|
+
cwd,
|
|
200
|
+
platformSource: PLATFORM_SOURCE
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
static async summarize(contentSessionId, lastUserMessage, lastAssistantMessage) {
|
|
206
|
+
try {
|
|
207
|
+
await fetch(`${this.BASE_URL}/api/sessions/summarize`, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers: { "Content-Type": "application/json" },
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
contentSessionId,
|
|
212
|
+
last_user_message: lastUserMessage,
|
|
213
|
+
last_assistant_message: lastAssistantMessage,
|
|
214
|
+
platformSource: PLATFORM_SOURCE
|
|
215
|
+
})
|
|
216
|
+
});
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
static async completeSession(contentSessionId) {
|
|
220
|
+
try {
|
|
221
|
+
await fetch(`${this.BASE_URL}/api/sessions/complete`, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: { "Content-Type": "application/json" },
|
|
224
|
+
body: JSON.stringify({ contentSessionId, platformSource: PLATFORM_SOURCE })
|
|
225
|
+
});
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
static async getContext(project) {
|
|
229
|
+
try {
|
|
230
|
+
const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`);
|
|
231
|
+
if (!response.ok) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const contentType = response.headers.get("content-type") || "";
|
|
235
|
+
if (contentType.includes("application/json")) {
|
|
236
|
+
return extractTextContent(await response.json());
|
|
237
|
+
}
|
|
238
|
+
const text = await response.text();
|
|
239
|
+
return text.trim() || null;
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
static async timeline(options) {
|
|
245
|
+
try {
|
|
246
|
+
const params = new URLSearchParams({ project: options.project });
|
|
247
|
+
if (options.anchor !== undefined) {
|
|
248
|
+
params.set("anchor", String(options.anchor));
|
|
249
|
+
}
|
|
250
|
+
if (options.query) {
|
|
251
|
+
params.set("query", options.query);
|
|
252
|
+
}
|
|
253
|
+
if (options.depthBefore !== undefined) {
|
|
254
|
+
params.set("depth_before", String(options.depthBefore));
|
|
255
|
+
}
|
|
256
|
+
if (options.depthAfter !== undefined) {
|
|
257
|
+
params.set("depth_after", String(options.depthAfter));
|
|
258
|
+
}
|
|
259
|
+
const response = await fetch(`${this.BASE_URL}/api/timeline?${params.toString()}`);
|
|
260
|
+
if (!response.ok) {
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
const contentType = response.headers.get("content-type") || "";
|
|
264
|
+
if (contentType.includes("application/json")) {
|
|
265
|
+
return extractTextContent(await response.json()) || "";
|
|
266
|
+
}
|
|
267
|
+
const text = await response.text();
|
|
268
|
+
return text.trim();
|
|
269
|
+
} catch {
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
static async getObservations(ids, project) {
|
|
274
|
+
try {
|
|
275
|
+
const body = { ids };
|
|
276
|
+
if (project) {
|
|
277
|
+
body["project"] = project;
|
|
278
|
+
}
|
|
279
|
+
const response = await fetch(`${this.BASE_URL}/api/observations/batch`, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { "Content-Type": "application/json" },
|
|
282
|
+
body: JSON.stringify(body)
|
|
283
|
+
});
|
|
284
|
+
if (!response.ok) {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
const contentType = response.headers.get("content-type") || "";
|
|
288
|
+
if (contentType.includes("application/json")) {
|
|
289
|
+
const payload = await response.json();
|
|
290
|
+
if (Array.isArray(payload)) {
|
|
291
|
+
return payload.length ? JSON.stringify(payload, null, 2) : "";
|
|
292
|
+
}
|
|
293
|
+
return extractTextContent(payload) || "";
|
|
294
|
+
}
|
|
295
|
+
const text = await response.text();
|
|
296
|
+
return text.trim();
|
|
297
|
+
} catch {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
static async getStats() {
|
|
302
|
+
try {
|
|
303
|
+
const [statsRes, processingRes] = await Promise.all([
|
|
304
|
+
fetch(`${this.BASE_URL}/api/stats`),
|
|
305
|
+
fetch(`${this.BASE_URL}/api/processing-status`)
|
|
306
|
+
]);
|
|
307
|
+
if (!statsRes.ok) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const stats = await statsRes.json();
|
|
311
|
+
const processing = processingRes.ok ? await processingRes.json() : undefined;
|
|
312
|
+
return { database: stats.database, processing };
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
static async search(query, project, limit) {
|
|
318
|
+
try {
|
|
319
|
+
const params = new URLSearchParams({ query, project });
|
|
320
|
+
if (limit !== undefined) {
|
|
321
|
+
params.set("limit", String(limit));
|
|
322
|
+
}
|
|
323
|
+
const response = await fetch(`${this.BASE_URL}/api/search?${params.toString()}`);
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
327
|
+
const contentType = response.headers.get("content-type") || "";
|
|
328
|
+
if (contentType.includes("application/json")) {
|
|
329
|
+
return extractTextContent(await response.json()) || "";
|
|
330
|
+
}
|
|
331
|
+
const text = await response.text();
|
|
332
|
+
return text.trim();
|
|
333
|
+
} catch {
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/tui.ts
|
|
340
|
+
var POLL_INTERVAL_MS = 5000;
|
|
341
|
+
var LABEL_MAX = 28;
|
|
342
|
+
function box(props, children) {
|
|
343
|
+
return { kind: "box", props, children };
|
|
344
|
+
}
|
|
345
|
+
function text(props, value) {
|
|
346
|
+
return { kind: "text", props, text: value };
|
|
347
|
+
}
|
|
348
|
+
function truncate(value) {
|
|
349
|
+
return value.length <= LABEL_MAX ? value : `${value.slice(0, LABEL_MAX - 3)}...`;
|
|
350
|
+
}
|
|
351
|
+
function formatCount(value) {
|
|
352
|
+
if (value === null) {
|
|
353
|
+
return "-";
|
|
354
|
+
}
|
|
355
|
+
return value >= 1e4 ? `${(value / 1000).toFixed(1)}k` : String(value);
|
|
356
|
+
}
|
|
357
|
+
function buildMemNodes(view, theme) {
|
|
358
|
+
const rows = view.healthy ? [
|
|
359
|
+
text({ fg: theme.success }, "worker online"),
|
|
360
|
+
text({ fg: theme.text }, `project ${truncate(view.project)}`),
|
|
361
|
+
text({ fg: theme.text }, `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)}`),
|
|
362
|
+
text({ fg: theme.textMuted }, `sessions ${formatCount(view.sessions)}`),
|
|
363
|
+
...view.isProcessing || (view.queueDepth ?? 0) > 0 ? [text({ fg: theme.warning }, `processing \xB7 queue ${view.queueDepth ?? 0}`)] : []
|
|
364
|
+
] : [text({ fg: theme.error }, "worker offline")];
|
|
365
|
+
return [
|
|
366
|
+
box({
|
|
367
|
+
borderStyle: "single",
|
|
368
|
+
borderColor: theme.borderSubtle,
|
|
369
|
+
flexDirection: "column",
|
|
370
|
+
padding: 1
|
|
371
|
+
}, [text({ fg: theme.info }, "Memory"), ...rows])
|
|
372
|
+
];
|
|
373
|
+
}
|
|
374
|
+
function viewKey(view) {
|
|
375
|
+
return JSON.stringify(view);
|
|
376
|
+
}
|
|
377
|
+
function readCount(value) {
|
|
378
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
379
|
+
}
|
|
380
|
+
async function readMemView(project) {
|
|
381
|
+
const stats = await WorkerClient.getStats();
|
|
382
|
+
if (!stats) {
|
|
383
|
+
return {
|
|
384
|
+
healthy: false,
|
|
385
|
+
project,
|
|
386
|
+
observations: null,
|
|
387
|
+
sessions: null,
|
|
388
|
+
summaries: null,
|
|
389
|
+
queueDepth: null,
|
|
390
|
+
isProcessing: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
healthy: true,
|
|
395
|
+
project,
|
|
396
|
+
observations: readCount(stats.database?.observations),
|
|
397
|
+
sessions: readCount(stats.database?.sessions),
|
|
398
|
+
summaries: readCount(stats.database?.summaries),
|
|
399
|
+
queueDepth: readCount(stats.processing?.queueDepth),
|
|
400
|
+
isProcessing: stats.processing?.isProcessing === true
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function materializeNode(node, solid) {
|
|
404
|
+
const element = solid.createElement(node.kind);
|
|
405
|
+
for (const [name, value] of Object.entries(node.props)) {
|
|
406
|
+
solid.setProp(element, name, value);
|
|
407
|
+
}
|
|
408
|
+
if (node.kind === "text") {
|
|
409
|
+
solid.insert(element, node.text ?? "");
|
|
410
|
+
}
|
|
411
|
+
for (const child of node.children ?? []) {
|
|
412
|
+
solid.insert(element, materializeNode(child, solid));
|
|
413
|
+
}
|
|
414
|
+
return element;
|
|
415
|
+
}
|
|
416
|
+
function materialize(nodes, solid) {
|
|
417
|
+
const root = solid.createElement("box");
|
|
418
|
+
solid.setProp(root, "flexDirection", "column");
|
|
419
|
+
for (const node of nodes) {
|
|
420
|
+
solid.insert(root, materializeNode(node, solid));
|
|
421
|
+
}
|
|
422
|
+
return root;
|
|
423
|
+
}
|
|
424
|
+
function projectNameFromDirectory(directory) {
|
|
425
|
+
return directory.split(/[\\/]/).findLast(Boolean) || "unknown-project";
|
|
426
|
+
}
|
|
427
|
+
var tuiModule = {
|
|
428
|
+
id: "opencode-claude-mem:tui",
|
|
429
|
+
tui: async (api) => {
|
|
430
|
+
const solid = await import("@opentui/solid").catch(() => null);
|
|
431
|
+
if (!solid) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const project = projectNameFromDirectory(String(api.state?.path?.directory ?? ""));
|
|
435
|
+
let currentView = await readMemView(project);
|
|
436
|
+
let currentKey = viewKey(currentView);
|
|
437
|
+
let disposed = false;
|
|
438
|
+
let inFlight = false;
|
|
439
|
+
let timer = null;
|
|
440
|
+
api.slots.register({
|
|
441
|
+
order: 910,
|
|
442
|
+
slots: {
|
|
443
|
+
sidebar_content: () => materialize(buildMemNodes(currentView, api.theme.current), solid)
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
api.renderer.requestRender();
|
|
447
|
+
const schedule = () => {
|
|
448
|
+
timer = setTimeout(tick, POLL_INTERVAL_MS);
|
|
449
|
+
};
|
|
450
|
+
const tick = async () => {
|
|
451
|
+
if (disposed || inFlight) {
|
|
452
|
+
if (!disposed) {
|
|
453
|
+
schedule();
|
|
454
|
+
}
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
inFlight = true;
|
|
458
|
+
try {
|
|
459
|
+
const nextView = await readMemView(project);
|
|
460
|
+
const nextKey = viewKey(nextView);
|
|
461
|
+
if (nextKey !== currentKey) {
|
|
462
|
+
currentView = nextView;
|
|
463
|
+
currentKey = nextKey;
|
|
464
|
+
api.renderer.requestRender();
|
|
465
|
+
}
|
|
466
|
+
} catch {} finally {
|
|
467
|
+
inFlight = false;
|
|
468
|
+
if (!disposed) {
|
|
469
|
+
schedule();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
schedule();
|
|
474
|
+
api.lifecycle.onDispose(() => {
|
|
475
|
+
disposed = true;
|
|
476
|
+
if (timer) {
|
|
477
|
+
clearTimeout(timer);
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
var tui_default = tuiModule;
|
|
483
|
+
export {
|
|
484
|
+
viewKey,
|
|
485
|
+
readMemView,
|
|
486
|
+
tui_default as default,
|
|
487
|
+
buildMemNodes
|
|
488
|
+
};
|
package/dist/worker-client.d.ts
CHANGED
|
@@ -64,5 +64,36 @@ export declare class WorkerClient {
|
|
|
64
64
|
* Uses /api/context/inject which returns rich markdown with observations + session summaries.
|
|
65
65
|
*/
|
|
66
66
|
static getContext(project: string): Promise<string | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Timeline: get chronological context around an observation anchor or query.
|
|
69
|
+
* Mirrors the upstream MCP `timeline` tool (GET /api/timeline).
|
|
70
|
+
*/
|
|
71
|
+
static timeline(options: {
|
|
72
|
+
project: string;
|
|
73
|
+
anchor?: number;
|
|
74
|
+
query?: string;
|
|
75
|
+
depthBefore?: number;
|
|
76
|
+
depthAfter?: number;
|
|
77
|
+
}): Promise<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Fetch full observation details by IDs.
|
|
80
|
+
* Mirrors the upstream MCP `get_observations` tool (POST /api/observations/batch).
|
|
81
|
+
*/
|
|
82
|
+
static getObservations(ids: number[], project?: string): Promise<string>;
|
|
83
|
+
/**
|
|
84
|
+
* Fetch worker + database stats plus processing status for the sidebar view.
|
|
85
|
+
* Combines GET /api/stats and GET /api/processing-status; null when offline.
|
|
86
|
+
*/
|
|
87
|
+
static getStats(): Promise<{
|
|
88
|
+
database?: {
|
|
89
|
+
observations?: number;
|
|
90
|
+
sessions?: number;
|
|
91
|
+
summaries?: number;
|
|
92
|
+
};
|
|
93
|
+
processing?: {
|
|
94
|
+
isProcessing?: boolean;
|
|
95
|
+
queueDepth?: number;
|
|
96
|
+
};
|
|
97
|
+
} | null>;
|
|
67
98
|
static search(query: string, project: string, limit?: number): Promise<string>;
|
|
68
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ephemushroom/opencode-claude-mem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OpenCode plugin for Claude-Mem persistent memory system",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -16,10 +16,14 @@
|
|
|
16
16
|
"./server": {
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./tui": {
|
|
21
|
+
"types": "./dist/tui.d.ts",
|
|
22
|
+
"import": "./dist/tui.js"
|
|
19
23
|
}
|
|
20
24
|
},
|
|
21
25
|
"scripts": {
|
|
22
|
-
"build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm --external @
|
|
26
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target bun --format esm && bun build src/tui.ts --outfile dist/tui.js --target bun --format esm --external @opentui/solid && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
23
27
|
"dev": "tsc --watch",
|
|
24
28
|
"clean": "rm -rf dist",
|
|
25
29
|
"lint": "oxlint",
|
|
@@ -48,10 +52,8 @@
|
|
|
48
52
|
"publishConfig": {
|
|
49
53
|
"access": "public"
|
|
50
54
|
},
|
|
51
|
-
"dependencies": {
|
|
52
|
-
"@opencode-ai/plugin": "^1.3.0"
|
|
53
|
-
},
|
|
54
55
|
"devDependencies": {
|
|
56
|
+
"@opencode-ai/plugin": "^1.3.0",
|
|
55
57
|
"@types/bun": "latest",
|
|
56
58
|
"oxfmt": "^0.33.0",
|
|
57
59
|
"oxlint": "^1.48.0",
|