@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/dist/cli.js CHANGED
@@ -1,12 +1,82 @@
1
1
  // @bun
2
- // src/worker-client.ts
3
- import { existsSync, readFileSync } from "fs";
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
- import { join } from "path";
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 DEFAULT_WORKER_PORT = 37777;
8
- var SETTINGS_FILE = join(homedir(), ".claude-mem", "settings.json");
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
- return isRecord(parsed) ? parsed : {};
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 parseWorkerEndpoint(env, rawSettings) {
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 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;
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 (!LOOPBACK_HOSTS.has(parseWorkerEndpoint(env, rawSettings).host)) {
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"]) ?? join(home, ".claude-mem");
57
- return join(dir.replace(/^~(?=$|[\\/])/, home), "claude-mem.db");
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 readFileSync(SETTINGS_FILE, "utf8");
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
- const endpoint = parseWorkerEndpoint(process.env, readSettingsRaw());
98
- return `http://${endpoint.host}:${endpoint.port}`;
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 DATABASE_PATH = resolveDatabasePath(process.env, readSettingsRaw(), homedir());
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
- fetch(`${this.BASE_URL}/api/stats`),
111
- fetch(`${this.BASE_URL}/api/processing-status`)
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 fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
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 fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
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 fetch(`${this.BASE_URL}/api/${path}/${id}`);
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 ? { onMouseDown: 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, onMouseDown: onToggle }, [
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 project = String(ctx.location?.directory ?? process.cwd()).split(/[\\/]/).filter(Boolean).findLast(Boolean) ?? "unknown-project";
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 ctx.ui.dialog.alert(detail);
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>;
@@ -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;