@ephemushroom/opencode-claude-mem 0.7.0 → 0.7.1

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 CHANGED
@@ -18,10 +18,17 @@ The **Memory sidebar** for [OpenCode](https://opencode.ai), backed by
18
18
  Click an item for details ← opens the full summary or observation
19
19
  ```
20
20
 
21
- From **v0.7.0** this package is sidebar only. Memory capture, context injection, search
22
- and the MCP tools come from the **official claude-mem OpenCode plugin**, which tracks the
23
- Worker's API in the same release. Earlier versions shipped their own capture adapter; it is
24
- removed so the two can never run side by side.
21
+ One package brings claude-mem to OpenCode 2, installed and updated like any other plugin:
22
+
23
+ - **Capture and context injection** run the **official claude-mem OpenCode plugin**. This
24
+ package downloads it at the running Worker's version (from the `claude-mem` npm release,
25
+ checked against the registry's sha512) and loads it, so capture always matches the
26
+ Worker's API and no file has to be copied by hand.
27
+ - **Memory tools** beyond the official plugin's search: `save_memory`, `work_state_write`,
28
+ `work_state_read`, `dumb_search`, `get_tool_uses` and the six corpus tools, served straight
29
+ from the Worker's HTTP API. No MCP server is needed.
30
+ - **The Worker is started** when nothing answers on its port.
31
+ - **The Memory sidebar**, below.
25
32
 
26
33
  - Counts are for the current project (the directory name), matching the Worker's own
27
34
  project filter: case-insensitive, merged projects included. They are read from the Worker's
@@ -38,61 +45,81 @@ removed so the two can never run side by side.
38
45
  - Recent rows are fetched only while expanded; collapsed, it polls stats every 5s.
39
46
  - Read-only and fail-open: an offline Worker shows the offline state, never blocks the TUI.
40
47
 
41
- Tested with OpenCode **2.2.7** (`@opencode/plugin` 2.2.7, `@opentui/solid` 0.8.1). The V1
42
- sidebar contract is kept on the same `./tui` export.
48
+ Verified on macOS with OpenCode **2.3.0** (built against `@opencode/plugin` 2.2.7,
49
+ `@opentui/solid` 0.8.1). The Windows paths follow claude-mem's own launcher location but
50
+ have not been run on Windows yet.
43
51
 
44
52
  ## Setup (OpenCode 2)
45
53
 
46
- 1. **Install Claude-Mem** in Claude Code and make sure the Worker is healthy:
54
+ 1. **Install claude-mem** once per machine. This installs the Worker and its launcher:
47
55
 
48
56
  ```bash
49
- curl -s http://127.0.0.1:37777/api/health
57
+ npx claude-mem install
50
58
  ```
51
59
 
52
- 2. **Install the official server plugin.** OpenCode 2 auto-loads single files from
53
- `~/.config/opencode/plugins/`; the file ships in the `claude-mem` npm package:
60
+ 2. **Add the plugin.** This installs the package and lists it in
61
+ `~/.config/opencode/opencode.json`:
54
62
 
55
63
  ```bash
56
- mkdir -p ~/.config/opencode/plugins
57
- cd "$(mktemp -d)" && npm pack claude-mem@latest --silent \
58
- && tar -xzf claude-mem-*.tgz package/dist/opencode-plugin/index.js \
59
- && cp package/dist/opencode-plugin/index.js ~/.config/opencode/plugins/claude-mem.js
64
+ opencode plugin add @ephemushroom/opencode-claude-mem
60
65
  ```
61
66
 
62
- Claude-Mem's auto-update upgrades the Worker, not this copy: repeat this step after a
63
- Worker upgrade.
64
-
65
- 3. **Register the MCP server** (search, timeline, observations, `save_memory`,
66
- `work_state_*`) in `~/.config/opencode/opencode.json`, under V2's `mcp.servers`:
67
+ 3. **Add the sidebar** to `~/.config/opencode/cli.json`:
67
68
 
68
69
  ```json
69
- {
70
- "mcp": {
71
- "servers": {
72
- "claude-mem": {
73
- "type": "local",
74
- "command": [
75
- "/opt/homebrew/bin/node",
76
- "/Users/<you>/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs"
77
- ]
78
- }
79
- }
80
- }
81
- }
70
+ { "plugins": ["@ephemushroom/opencode-claude-mem"] }
82
71
  ```
83
72
 
84
- Point at the marketplace path, not a versioned `plugins/cache/.../<version>/` path, so
85
- Worker upgrades keep it valid. Prefer this over `npx claude-mem install --ide=opencode`,
86
- which writes the V1 `mcp` shape.
73
+ Restart OpenCode. The first start downloads the official files (about 10 MB) in the
74
+ background; the memory tools work at once, and capture begins when the download finishes.
87
75
 
88
- 4. **Add the sidebar** to `~/.config/opencode/cli.json`:
76
+ ### Updating
89
77
 
90
- ```json
91
- { "plugins": ["@ephemushroom/opencode-claude-mem"] }
92
- ```
78
+ ```bash
79
+ opencode plugin update # this package: server part and sidebar
80
+ ```
81
+
82
+ OpenCode does not update plugins by itself. The official claude-mem files need no command:
83
+ after a Worker upgrade, the next OpenCode start fetches the matching version.
84
+
85
+ ### Options
86
+
87
+ ```json
88
+ {
89
+ "plugins": [{ "package": "@ephemushroom/opencode-claude-mem", "options": { "mcp": true } }]
90
+ }
91
+ ```
92
+
93
+ - `mcp: true` registers claude-mem's own MCP server (every tool it has, including the
94
+ `smart_*` code-outline tools) in a separate process, instead of this package's tools.
95
+ A `claude-mem` entry you wrote under `mcp.servers` is left as it is.
96
+
97
+ claude-mem's settings apply as usual (its `settings.json`, in `~/.claude-mem` or
98
+ `CLAUDE_MEM_DATA_DIR`, or the environment):
93
99
 
94
- 5. **Remove this package from `opencode.json` `plugins`** if an older version put it there.
95
- Both server plugins use the id `claude-mem`; keeping both captures every tool twice.
100
+ - `CLAUDE_MEM_WORKER_HOST` / `CLAUDE_MEM_WORKER_PORT` select the Worker. Without a port,
101
+ claude-mem's own default applies (37700 plus the last two digits of the user id).
102
+ - `CLAUDE_MEM_LOCAL_API_TOKEN` is sent to a Worker on this machine that requires it. This
103
+ package's own requests refuse redirects; the official plugin's capture requests are
104
+ claude-mem's code and follow them (as of 15.18.3).
105
+ - `CLAUDE_MEM_WORKER_AUTOSTART=false` keeps this package from starting the Worker.
106
+ - `CLAUDE_MEM_WORK_STATE_ENABLED=false` removes the two work-state tools.
107
+ - `CLAUDE_MEM_WORKER_URL` is not supported: the official OpenCode plugin addresses the
108
+ Worker by host and port only, so the sidebar reports it and nothing is captured.
109
+
110
+ ### Tools
111
+
112
+ OpenCode lists them as the `claude_mem` namespace (`tools.claude_mem.save_memory(...)` in
113
+ Code Mode, `claude_mem_save_memory` as a direct tool call). The official plugin adds
114
+ `claude_mem_search`, `claude_mem_timeline` and `claude_mem_get_observations`.
115
+
116
+ | Tool | Purpose |
117
+ |---|---|
118
+ | `save_memory` | Save a durable note |
119
+ | `work_state_write` / `work_state_read` | Per-project to-do lists and working state, shown at session start |
120
+ | `dumb_search` | One-call lookup by title overlap |
121
+ | `get_tool_uses` | Raw tool input/output for selected ids |
122
+ | `build_corpus`, `list_corpora`, `prime_corpus`, `query_corpus`, `rebuild_corpus`, `reprime_corpus` | Knowledge corpora |
96
123
 
97
124
  ### Share memory across Claude Code and OpenCode
98
125
 
@@ -106,52 +133,75 @@ add this to `~/.claude-mem/settings.json` (it also applies to Claude Code's sess
106
133
  ### Verify
107
134
 
108
135
  ```bash
109
- opencode plugin list # claude-mem local ~/.config/opencode/plugins/claude-mem.js
110
- opencode mcp list # ✓ claude-mem connected
136
+ opencode plugin list # opencode-claude-mem 0.7.1 @ephemushroom/opencode-claude-mem
111
137
  ```
112
138
 
113
139
  Open a session: the sidebar shows `▶ Memory (online, …)`, and new observations carry
114
140
  canonical tool names (`Read`, `Edit`, `Bash`) with `files_modified` filled in.
115
141
 
116
- ## Migrating from v0.6.x
142
+ ### Where files go
117
143
 
118
- v0.6.x registered `@ephemushroom/opencode-claude-mem` as a **server** plugin. Its capture,
119
- `mem-*` tools, file-history enrichment and semantic injection are gone:
144
+ `~/.cache/opencode-claude-mem/` (or `$XDG_CACHE_HOME/opencode-claude-mem/`): the official
145
+ files under `official/<version>/` (about 1.5 MB per claude-mem version; earlier versions are
146
+ kept, because another OpenCode process may still run from them) and `status/`, which carries
147
+ the server part's state to the sidebar. Deleting the directory while OpenCode is closed is
148
+ safe.
120
149
 
121
- | v0.6.x | Now |
122
- |---|---|
123
- | `mem-search` / `mem-timeline` / `mem-get-observations` | `claude_mem_search` / `claude_mem_timeline` / `claude_mem_get_observations` (official plugin) and the MCP `search` / `timeline` / `get_observations` |
124
- | `mem-save` | MCP `save_memory` |
125
- | Read-file history appended to `read` output | Not available |
126
- | Opt-in semantic injection | Not available |
150
+ ## Migrating
151
+
152
+ **From v0.7.0's manual setup.** Remove what the manual steps installed, then follow Setup:
153
+
154
+ - delete `~/.config/opencode/plugins/claude-mem.js`. While that file exists it keeps
155
+ capturing, this package does not load a second copy, and that file is not kept up to date;
156
+ - delete the `claude-mem` entry under `mcp.servers` in `opencode.json`, unless you want
157
+ the MCP server; in that case use `options.mcp` instead.
158
+
159
+ **From v0.6.x.** Its own capture adapter is gone; capture is the official plugin's.
160
+ `mem-search` / `mem-timeline` / `mem-get-observations` are now `claude_mem_search` /
161
+ `claude_mem_timeline` / `claude_mem_get_observations`, and `mem-save` is `save_memory`.
162
+ Read-file history and opt-in semantic injection are not available.
127
163
 
128
- Follow steps 2–5 above, then keep the bare name in `cli.json` (and `tui.json` for V1).
164
+ **OpenCode 1.** The server part needs OpenCode 2. On OpenCode 1 use
165
+ `npx claude-mem install --ide opencode`; the sidebar still loads from `tui.json`.
129
166
 
130
167
  ## Troubleshooting
131
168
 
132
- - **`(offline)`**: the Worker is not running. Start Claude Code, or `bunx claude-mem start`.
169
+ - **`(offline)`**: the Worker is not running and could not be started. Run
170
+ `npx claude-mem start`; if that command is missing, `npx claude-mem install`.
171
+ - **`(capture off)`**: nothing records this OpenCode's sessions. Expand the sidebar: the
172
+ first row says why (claude-mem not installed, the Worker did not start, the download
173
+ failed, or a `plugins/claude-mem.js` file that OpenCode has not loaded) and what to do.
174
+ The package looks again every minute.
133
175
  - **Sidebar missing**: check `~/.config/opencode/cli.json` lists the package; the sidebar
134
176
  needs OpenCode's `@opentui/solid` runtime and is skipped silently without it.
135
- - **Duplicate observations**: two capture integrations are active. Remove this package from
136
- `opencode.json` `plugins`, and disable any Claude Code compatibility bridge for claude-mem.
137
- - **MCP not listed in an open session**: run `/reload`; already-open locations may keep the
138
- previous MCP set until they reload.
177
+ - **Duplicate observations**: two capture integrations are active. Remove
178
+ `~/.config/opencode/plugins/claude-mem.js` or this package, and disable any Claude Code
179
+ compatibility bridge for claude-mem. In Code Mode one call is recorded as `execute` plus
180
+ the tool it ran; that is one call, not a duplicate.
181
+ - **Behind a registry mirror**: the download uses `CLAUDE_MEM_NPM_REGISTRY`, then
182
+ `npm_config_registry`, then `https://registry.npmjs.org`. The mirror must be HTTPS (or on
183
+ this machine): it names both the tarball and its checksum, so it is trusted like the
184
+ registry itself.
139
185
 
140
186
  ## Development
141
187
 
142
188
  ```bash
143
189
  bun install
144
- bun run build # sidebar adapters + TUI bridge + declarations
190
+ bun run build # server part + sidebar adapters + TUI bridge + declarations
145
191
  bun test # build first: export tests import dist artifacts
146
192
  bun run typecheck && bun run lint && bun run fmt:check
193
+ bun src/test-fixtures/server-runtime.ts # installed opencode: download, capture, tools
194
+ bun src/test-fixtures/server-runtime.ts mcp # options.mcp registers the MCP server
195
+ bun src/test-fixtures/server-runtime.ts start # no Worker: the launcher is run (macOS/Linux)
196
+ bun src/test-fixtures/server-runtime.ts standalone # plugins/claude-mem.js present: one capture
147
197
  bun src/test-fixtures/tui-runtime.ts # installed opencode: real PTY, render + mouse toggle
148
198
  bun src/test-fixtures/tui-runtime.ts offline # offline state renders and toggles
149
199
  bun --conditions=browser src/test-fixtures/cli-driver.ts render # OpenTUI component driver
150
200
  ```
151
201
 
152
- The PTY check packs the current build, serves it from a temporary local registry, and runs
153
- the installed `opencode` in an isolated home with a fake Worker. It never touches your config
154
- or memory database and never calls a model. Set `MEM_TUI_EVIDENCE_DIR` to keep the captured
202
+ The runtime checks pack the current build, serve it from a temporary local registry, and run
203
+ the installed `opencode` in an isolated home with a fake Worker and model. They never touch
204
+ your config or memory database. The server check downloads the real `claude-mem` release. Set `MEM_TUI_EVIDENCE_DIR` to keep the captured
155
205
  collapsed, expanded, detail-dialog and re-collapsed frames.
156
206
 
157
207
  ## License
@@ -0,0 +1,26 @@
1
+ export interface CaptureStatus {
2
+ /** `problem`: nothing is capturing this checkout's sessions. */
3
+ readonly state: 'ok' | 'problem';
4
+ /** Empty when there is nothing to tell the user. */
5
+ readonly message: string;
6
+ }
7
+ /** Holds the official claude-mem files this package downloads, and the status files. */
8
+ export declare function cacheDirectory(env?: Record<string, string | undefined>, home?: string): string;
9
+ /** Names the status files of one checkout, however its path was spelled. */
10
+ export declare function statusKey(directory: string): string;
11
+ /**
12
+ * The newest status a running server part wrote for this checkout, preferring one from
13
+ * this process. Null when none is running: a status left by a package that was since
14
+ * removed from `opencode.json`, or by a process that exited, must not stay on screen.
15
+ */
16
+ export declare function readCaptureStatus(directory: string, dir?: string): CaptureStatus | null;
17
+ export interface CaptureStatusWriter {
18
+ readonly write: (status: CaptureStatus) => void;
19
+ /** Called when the server part is unloaded for the checkout while the host keeps running. */
20
+ readonly clear: () => void;
21
+ }
22
+ /**
23
+ * The status file of one `setup` call. Each has its own, so two servers on one checkout,
24
+ * or a reload that sets up before the old instance is disposed, never remove each other's.
25
+ */
26
+ export declare function captureStatusWriter(directory: string, dir?: string): CaptureStatusWriter;
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
  }
@@ -279,11 +407,21 @@ function recentRow(icon, iconColor, title, age, theme, onSelect) {
279
407
  ]);
280
408
  }
281
409
  function expandedRows(view, theme, onSelect) {
410
+ const notice = view.capture?.message ? [
411
+ box({ flexDirection: "row", gap: 1 }, [
412
+ text({ fg: theme.warning, flexShrink: 0 }, "!"),
413
+ text({
414
+ fg: view.capture.state === "problem" ? theme.error : theme.warning,
415
+ wrapMode: "word"
416
+ }, view.capture.message)
417
+ ])
418
+ ] : [];
282
419
  if (!view.healthy) {
283
- return [bulletRow("\u2022", theme.error, "worker offline", theme)];
420
+ return [bulletRow("\u2022", theme.error, "worker offline", theme), ...notice];
284
421
  }
285
422
  const counts = `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`;
286
423
  const rows = [
424
+ ...notice,
287
425
  bulletRow("\u2022", theme.success, view.scope === "all" ? `all \xB7 ${counts}` : counts, theme)
288
426
  ];
289
427
  if (view.scope === "project") {
@@ -330,14 +468,16 @@ function viewKey(view) {
330
468
  function readCount2(value) {
331
469
  return typeof value === "number" && Number.isFinite(value) ? value : null;
332
470
  }
333
- async function readMemView(project, collapsed) {
471
+ async function readMemView(project, collapsed, directory = "") {
334
472
  const [stats, counts] = await Promise.all([
335
473
  WorkerClient.getStats(),
336
474
  WorkerClient.getProjectCounts(project)
337
475
  ]);
476
+ const capture = directory ? readCaptureStatus(directory) : null;
338
477
  if (!stats) {
339
478
  return {
340
479
  healthy: false,
480
+ capture,
341
481
  project,
342
482
  scope: "all",
343
483
  totalObservations: null,
@@ -358,6 +498,7 @@ async function readMemView(project, collapsed) {
358
498
  const totalObservations = readCount2(stats.database?.observations);
359
499
  return {
360
500
  healthy: true,
501
+ capture,
361
502
  project,
362
503
  scope: counts ? "project" : "all",
363
504
  totalObservations,
@@ -446,6 +587,7 @@ var REFRESH_EVENTS = ["session.created", "session.execution.succeeded"];
446
587
  function emptyView(project) {
447
588
  return {
448
589
  healthy: false,
590
+ capture: null,
449
591
  project,
450
592
  scope: "all",
451
593
  totalObservations: null,
@@ -466,7 +608,8 @@ var cli_default = define({
466
608
  if (!solid) {
467
609
  return;
468
610
  }
469
- const project = String(ctx.location?.directory ?? process.cwd()).split(/[\\/]/).filter(Boolean).findLast(Boolean) ?? "unknown-project";
611
+ const directory = String(ctx.location?.directory ?? process.cwd());
612
+ const project = directory.split(/[\\/]/).filter(Boolean).findLast(Boolean) ?? "unknown-project";
470
613
  const [state, setState] = ctx.storage.memory("claude-mem.sidebar", {
471
614
  initial: { collapsed: true, view: emptyView(project) }
472
615
  });
@@ -487,7 +630,7 @@ var cli_default = define({
487
630
  do {
488
631
  refreshPending = false;
489
632
  const { collapsed } = state;
490
- const next = await readMemView(project, collapsed);
633
+ const next = await readMemView(project, collapsed, directory);
491
634
  if (disposed) {
492
635
  return;
493
636
  }
@@ -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>;