open-mem 0.12.0 → 0.13.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/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.13.0] - 2026-02-23
9
+
10
+ ### Added
11
+ - **`npx open-mem` CLI installer** — one-command plugin setup for OpenCode. Automatically finds or creates the config file and adds `open-mem` to the plugin array. Supports `--global`, `--uninstall`, `--dry-run`, `--force`, and `--version` flags. JSONC-aware (preserves comments in existing config files). Cleans OpenCode plugin cache on uninstall.
12
+ - AI provider detection in installer — shows which providers are configured after install.
13
+
14
+ ### Changed
15
+ - Documentation updated — README Quick Start and Getting Started guide now recommend `npx open-mem` as the primary installation method, with manual `bun add` as alternative.
16
+
8
17
  ## [0.12.0] - 2026-02-16
9
18
 
10
19
  ### Changed
package/README.md CHANGED
@@ -31,11 +31,19 @@ You use tools, open-mem captures the outputs, AI compresses them into structured
31
31
 
32
32
  ## Quick start
33
33
 
34
+ ```bash
35
+ npx open-mem
36
+ ```
37
+
38
+ That's it. This adds `open-mem` to your OpenCode plugin config automatically. It starts capturing from your next session.
39
+
40
+ Or install manually:
41
+
34
42
  ```bash
35
43
  bun add open-mem
36
44
  ```
37
45
 
38
- Add it to your OpenCode config (`~/.config/opencode/opencode.json`):
46
+ Then add to your OpenCode config (`~/.config/opencode/opencode.json` or `.opencode/opencode.json`):
39
47
 
40
48
  ```json
41
49
  {
@@ -43,8 +51,6 @@ Add it to your OpenCode config (`~/.config/opencode/opencode.json`):
43
51
  }
44
52
  ```
45
53
 
46
- That's it. open-mem starts capturing from your next session.
47
-
48
54
  ### AI compression (optional)
49
55
 
50
56
  By default, open-mem uses a basic metadata extractor. For semantic compression, add an AI provider:
package/bin/cli.mjs ADDED
@@ -0,0 +1,650 @@
1
+ #!/usr/bin/env node
2
+
3
+ // open-mem — Plugin Installer for OpenCode
4
+ // Usage: npx open-mem [--global] [--force] [--uninstall] [--dry-run] [--help] [--version]
5
+
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import readline from "node:readline/promises";
10
+
11
+ // ── colour helpers (disabled when piped) ────────────────────────────
12
+ const isTTY = process.stdout.isTTY;
13
+ const RED = isTTY ? "\x1b[0;31m" : "";
14
+ const GREEN = isTTY ? "\x1b[0;32m" : "";
15
+ const YELLOW = isTTY ? "\x1b[0;33m" : "";
16
+ const BLUE = isTTY ? "\x1b[0;34m" : "";
17
+ const DIM = isTTY ? "\x1b[2m" : "";
18
+ const BOLD = isTTY ? "\x1b[1m" : "";
19
+ const RESET = isTTY ? "\x1b[0m" : "";
20
+
21
+ const info = (msg) => process.stdout.write(`${BLUE}[info]${RESET} ${msg}\n`);
22
+ const ok = (msg) => process.stdout.write(`${GREEN}[ok]${RESET} ${msg}\n`);
23
+ const warn = (msg) => process.stdout.write(`${YELLOW}[warn]${RESET} ${msg}\n`);
24
+ const err = (msg) => process.stderr.write(`${RED}[error]${RESET} ${msg}\n`);
25
+
26
+ // ── constants ───────────────────────────────────────────────────────
27
+ const PLUGIN_ENTRY = "open-mem@latest";
28
+ const PKG_NAME = "open-mem";
29
+ const DOCS_URL = "https://github.com/clopca/open-mem";
30
+
31
+ // ── CLI flags ───────────────────────────────────────────────────────
32
+ const args = process.argv.slice(2);
33
+ const flagGlobal = args.includes("--global");
34
+ const flagUninstall = args.includes("--uninstall");
35
+ const flagDryRun = args.includes("--dry-run");
36
+ const flagForce = args.includes("--force");
37
+ const flagHelp = args.includes("--help") || args.includes("-h");
38
+ const flagVersion = args.includes("--version") || args.includes("-v");
39
+
40
+ // ── unknown flag validation ─────────────────────────────────────────
41
+ const KNOWN_FLAGS = new Set([
42
+ "--global",
43
+ "--uninstall",
44
+ "--dry-run",
45
+ "--force",
46
+ "--help",
47
+ "-h",
48
+ "--version",
49
+ "-v",
50
+ ]);
51
+ const unknown = args.filter((a) => a.startsWith("-") && !KNOWN_FLAGS.has(a));
52
+ if (unknown.length > 0) {
53
+ err(`Unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}`);
54
+ info(`Run ${BOLD}npx open-mem --help${RESET} for usage.`);
55
+ process.exit(1);
56
+ }
57
+
58
+ // ── version helper ──────────────────────────────────────────────────
59
+
60
+ function getVersion() {
61
+ try {
62
+ const pkgPath = path.join(
63
+ path.dirname(new URL(import.meta.url).pathname),
64
+ "..",
65
+ "package.json",
66
+ );
67
+ return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ // ── --version flag ──────────────────────────────────────────────────
74
+ if (flagVersion) {
75
+ const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "package.json");
76
+ try {
77
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
78
+ process.stdout.write(`${pkg.name} v${pkg.version}\n`);
79
+ } catch {
80
+ process.stdout.write(`open-mem (version unknown)\n`);
81
+ }
82
+ process.exit(0);
83
+ }
84
+
85
+ // ── help ────────────────────────────────────────────────────────────
86
+ if (flagHelp) {
87
+ process.stdout.write(`
88
+ ${BOLD}open-mem${RESET} — Persistent memory plugin for OpenCode
89
+
90
+ ${BOLD}USAGE${RESET}
91
+ npx open-mem [flags]
92
+
93
+ ${BOLD}FLAGS${RESET}
94
+ ${DIM}(none)${RESET} Add open-mem to local .opencode/opencode.json
95
+ --global Target ~/.config/opencode/opencode.json instead
96
+ --uninstall Remove open-mem from all discovered config files
97
+ --dry-run Preview changes without writing anything
98
+ --force Skip confirmation prompts
99
+ --help, -h Show this help
100
+ --version, -v Show version
101
+
102
+ ${BOLD}EXAMPLES${RESET}
103
+ npx open-mem ${DIM}# install locally${RESET}
104
+ npx open-mem --global ${DIM}# install globally${RESET}
105
+ npx open-mem --uninstall ${DIM}# remove from all configs${RESET}
106
+
107
+ ${BOLD}DOCS${RESET}
108
+ ${DOCS_URL}
109
+ `);
110
+ process.exit(0);
111
+ }
112
+
113
+ // ── JSONC helpers ───────────────────────────────────────────────────
114
+
115
+ /** Strip line and block comments from JSONC so JSON.parse can handle it. */
116
+ function stripJsonComments(text) {
117
+ let result = "";
118
+ let i = 0;
119
+ let inString = false;
120
+ let stringChar = "";
121
+
122
+ while (i < text.length) {
123
+ // inside a JSON string — pass through, handling escapes
124
+ if (inString) {
125
+ if (text[i] === "\\") {
126
+ result += text[i] + (text[i + 1] ?? "");
127
+ i += 2;
128
+ continue;
129
+ }
130
+ if (text[i] === stringChar) inString = false;
131
+ result += text[i];
132
+ i++;
133
+ continue;
134
+ }
135
+
136
+ // string start
137
+ if (text[i] === '"' || text[i] === "'") {
138
+ inString = true;
139
+ stringChar = text[i];
140
+ result += text[i];
141
+ i++;
142
+ continue;
143
+ }
144
+
145
+ // line comment
146
+ if (text[i] === "/" && text[i + 1] === "/") {
147
+ while (i < text.length && text[i] !== "\n") i++;
148
+ continue;
149
+ }
150
+
151
+ // block comment
152
+ if (text[i] === "/" && text[i + 1] === "*") {
153
+ i += 2;
154
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
155
+ i += 2; // skip closing */
156
+ continue;
157
+ }
158
+
159
+ result += text[i];
160
+ i++;
161
+ }
162
+ return result;
163
+ }
164
+
165
+ // ── config discovery ────────────────────────────────────────────────
166
+
167
+ /** Walk up from `start` looking for an OpenCode config file. */
168
+ function findUp(start) {
169
+ const candidates = [
170
+ ".opencode/opencode.jsonc",
171
+ ".opencode/opencode.json",
172
+ "opencode.jsonc",
173
+ "opencode.json",
174
+ ];
175
+
176
+ let dir = path.resolve(start);
177
+ const root = path.parse(dir).root;
178
+
179
+ while (true) {
180
+ for (const c of candidates) {
181
+ const full = path.join(dir, c);
182
+ if (fs.existsSync(full)) return full;
183
+ }
184
+ const parent = path.dirname(dir);
185
+ if (parent === dir || dir === root) break;
186
+ dir = parent;
187
+ }
188
+ return null;
189
+ }
190
+
191
+ /** Determine the target config path for install. */
192
+ function getTargetPath() {
193
+ if (flagGlobal) {
194
+ const globalDir = path.join(os.homedir(), ".config", "opencode");
195
+ // prefer existing file
196
+ for (const name of ["opencode.jsonc", "opencode.json"]) {
197
+ const p = path.join(globalDir, name);
198
+ if (fs.existsSync(p)) return p;
199
+ }
200
+ return path.join(globalDir, "opencode.json");
201
+ }
202
+
203
+ // local: walk up from cwd
204
+ const found = findUp(process.cwd());
205
+ if (found) return found;
206
+
207
+ // fallback: create in .opencode/
208
+ return path.join(process.cwd(), ".opencode", "opencode.json");
209
+ }
210
+
211
+ /** Find ALL config files that contain open-mem (for uninstall). */
212
+ function findAllConfigsWithPlugin() {
213
+ const configs = [];
214
+ const seen = new Set();
215
+
216
+ const candidates = [
217
+ ".opencode/opencode.jsonc",
218
+ ".opencode/opencode.json",
219
+ "opencode.jsonc",
220
+ "opencode.json",
221
+ ];
222
+
223
+ const tryAdd = (p) => {
224
+ const resolved = path.resolve(p);
225
+ if (seen.has(resolved)) return;
226
+ seen.add(resolved);
227
+ if (!fs.existsSync(resolved)) return;
228
+ try {
229
+ const config = readConfig(resolved);
230
+ if (config && config.data && findPluginEntry(config.data) !== -1) {
231
+ configs.push(resolved);
232
+ }
233
+ } catch {
234
+ /* skip */
235
+ }
236
+ };
237
+
238
+ // Walk up from cwd, checking all candidates at each level
239
+ let dir = path.resolve(process.cwd());
240
+ const root = path.parse(dir).root;
241
+ while (true) {
242
+ for (const c of candidates) tryAdd(path.join(dir, c));
243
+ const parent = path.dirname(dir);
244
+ if (parent === dir || dir === root) break;
245
+ dir = parent;
246
+ }
247
+
248
+ // Global locations
249
+ const globalDir = path.join(os.homedir(), ".config", "opencode");
250
+ tryAdd(path.join(globalDir, "opencode.jsonc"));
251
+ tryAdd(path.join(globalDir, "opencode.json"));
252
+
253
+ return configs;
254
+ }
255
+
256
+ // ── read / write helpers ────────────────────────────────────────────
257
+
258
+ function readConfig(filePath) {
259
+ if (!fs.existsSync(filePath)) return null;
260
+ const raw = fs.readFileSync(filePath, "utf8");
261
+ try {
262
+ return { raw, data: JSON.parse(stripJsonComments(raw)) };
263
+ } catch {
264
+ return { raw, data: null };
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Write a brand-new minimal config with the plugin entry.
270
+ * Used when no config file exists at all.
271
+ */
272
+ function writeNewConfig(filePath) {
273
+ const content = JSON.stringify({ plugin: [PLUGIN_ENTRY] }, null, 2) + "\n";
274
+ if (flagDryRun) {
275
+ info(`Would create ${BOLD}${filePath}${RESET} with:`);
276
+ process.stdout.write(DIM + content + RESET);
277
+ return true;
278
+ }
279
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
280
+ fs.writeFileSync(filePath, content, "utf8");
281
+ ok(`Created ${BOLD}${filePath}${RESET}`);
282
+ return true;
283
+ }
284
+
285
+ /**
286
+ * Add the plugin entry to an existing config, preserving JSONC comments.
287
+ * Strategy: regex-based insertion so comments survive.
288
+ */
289
+ function addPluginEntry(filePath, raw, data) {
290
+ // already present? (handles any version suffix)
291
+ if (data && findPluginEntry(data) !== -1) {
292
+ ok(`${BOLD}${PKG_NAME}${RESET} already in ${filePath}`);
293
+ return false;
294
+ }
295
+
296
+ let updated;
297
+
298
+ // case 1: "plugin": [...] exists — append our entry
299
+ const pluginArrayRe = /("plugin"\s*:\s*\[)([\s\S]*?)(\])/;
300
+ const m = raw.match(pluginArrayRe);
301
+ if (m) {
302
+ const inside = m[2].trim();
303
+ if (inside.length === 0) {
304
+ // empty array
305
+ updated = raw.replace(pluginArrayRe, `$1"${PLUGIN_ENTRY}"$3`);
306
+ } else {
307
+ // non-empty — add after last entry
308
+ updated = raw.replace(pluginArrayRe, (_, open, entries, close) => {
309
+ const trimmed = entries.trimEnd();
310
+ const needsComma = trimmed.length > 0 && !trimmed.endsWith(",");
311
+ return `${open}${entries.trimEnd()}${needsComma ? "," : ""} "${PLUGIN_ENTRY}"${close}`;
312
+ });
313
+ }
314
+ } else {
315
+ // case 2: no plugin key — inject after the opening {
316
+ const idx = raw.indexOf("{");
317
+ if (idx === -1) {
318
+ err(`Cannot parse ${filePath} — not a JSON object`);
319
+ return false;
320
+ }
321
+ const before = raw.slice(0, idx + 1);
322
+ const after = raw.slice(idx + 1);
323
+ // detect indent
324
+ const indentMatch = after.match(/\n(\s+)/);
325
+ const indent = indentMatch ? indentMatch[1] : " ";
326
+ updated = `${before}\n${indent}"plugin": ["${PLUGIN_ENTRY}"],${after}`;
327
+ }
328
+
329
+ if (flagDryRun) {
330
+ info(`Would update ${BOLD}${filePath}${RESET}`);
331
+ process.stdout.write(DIM + updated + RESET);
332
+ return true;
333
+ }
334
+
335
+ fs.writeFileSync(filePath, updated, "utf8");
336
+ ok(`Added ${BOLD}${PLUGIN_ENTRY}${RESET} to ${filePath}`);
337
+ return true;
338
+ }
339
+
340
+ /** Remove the plugin entry from a config file, preserving JSONC comments. */
341
+ function removePluginEntry(filePath, raw) {
342
+ if (!raw.includes(PKG_NAME)) return false;
343
+
344
+ // Remove the entry (with or without @latest, quotes, surrounding commas)
345
+ let updated = raw;
346
+
347
+ // Pattern: "open-mem" or "open-mem@latest" or "open-mem@<version>" as array element
348
+ // Handle trailing comma, leading comma, or standalone
349
+ // NOTE: no 'g' flag — avoids lastIndex issues with test() + replace()
350
+ const patterns = [
351
+ // entry with trailing comma and optional whitespace
352
+ new RegExp(`\\s*"${PKG_NAME}(?:@[^"]*)?"\\s*,`),
353
+ // entry with leading comma — also trim trailing whitespace after comma removal
354
+ new RegExp(`,\\s*"${PKG_NAME}(?:@[^"]*)?"\\s*`),
355
+ // standalone entry (only element)
356
+ new RegExp(`"${PKG_NAME}(?:@[^"]*)?"`),
357
+ ];
358
+
359
+ for (const pat of patterns) {
360
+ if (pat.test(updated)) {
361
+ updated = updated.replace(pat, "");
362
+ break;
363
+ }
364
+ }
365
+
366
+ if (updated === raw) return false;
367
+
368
+ if (flagDryRun) {
369
+ info(`Would update ${BOLD}${filePath}${RESET}`);
370
+ return true;
371
+ }
372
+
373
+ fs.writeFileSync(filePath, updated, "utf8");
374
+ ok(`Removed ${BOLD}${PKG_NAME}${RESET} from ${filePath}`);
375
+ return true;
376
+ }
377
+
378
+ /** Find plugin entry in parsed config data. */
379
+ function findPluginEntry(data) {
380
+ if (!data || !Array.isArray(data.plugin)) return -1;
381
+ return data.plugin.findIndex(
382
+ (e) => typeof e === "string" && (e === PKG_NAME || e.startsWith(PKG_NAME + "@")),
383
+ );
384
+ }
385
+
386
+ // ── node_modules cleanup ────────────────────────────────────────────
387
+
388
+ function findStaleNodeModules(start) {
389
+ const results = [];
390
+ let dir = path.resolve(start);
391
+ const root = path.parse(dir).root;
392
+
393
+ while (true) {
394
+ const candidate = path.join(dir, "node_modules", PKG_NAME);
395
+ if (fs.existsSync(candidate)) results.push(candidate);
396
+ const parent = path.dirname(dir);
397
+ if (parent === dir || dir === root) break;
398
+ dir = parent;
399
+ }
400
+ return results;
401
+ }
402
+
403
+ function cleanupNodeModules() {
404
+ const stale = findStaleNodeModules(process.cwd());
405
+ if (stale.length === 0) return;
406
+
407
+ for (const p of stale) {
408
+ if (flagDryRun) {
409
+ info(`Would remove ${BOLD}${p}${RESET}`);
410
+ continue;
411
+ }
412
+ try {
413
+ fs.rmSync(p, { recursive: true, force: true });
414
+ ok(`Removed ${BOLD}${p}${RESET}`);
415
+ } catch (e) {
416
+ warn(`Could not remove ${p}: ${e.message}`);
417
+ }
418
+ }
419
+
420
+ // also remove from package.json dependencies if present
421
+ const pkgPath = path.join(process.cwd(), "package.json");
422
+ if (fs.existsSync(pkgPath)) {
423
+ try {
424
+ const raw = fs.readFileSync(pkgPath, "utf8");
425
+ if (raw.includes(`"${PKG_NAME}"`)) {
426
+ const pkg = JSON.parse(raw);
427
+ let changed = false;
428
+ for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
429
+ if (pkg[key] && pkg[key][PKG_NAME]) {
430
+ delete pkg[key][PKG_NAME];
431
+ changed = true;
432
+ }
433
+ }
434
+ if (changed) {
435
+ if (flagDryRun) {
436
+ info(`Would remove ${BOLD}${PKG_NAME}${RESET} from ${pkgPath}`);
437
+ } else {
438
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
439
+ ok(`Removed ${BOLD}${PKG_NAME}${RESET} from ${pkgPath}`);
440
+ }
441
+ }
442
+ }
443
+ } catch {
444
+ /* skip */
445
+ }
446
+ }
447
+ }
448
+
449
+ // ── confirmation prompt ─────────────────────────────────────────────
450
+
451
+ async function confirm(question) {
452
+ if (flagForce) return true;
453
+ if (!isTTY) return true; // non-interactive — assume yes
454
+
455
+ const rl = readline.createInterface({
456
+ input: process.stdin,
457
+ output: process.stdout,
458
+ });
459
+
460
+ try {
461
+ const answer = await rl.question(`${question} ${DIM}[Y/n]${RESET} `);
462
+ return !answer || answer.toLowerCase().startsWith("y");
463
+ } finally {
464
+ rl.close();
465
+ }
466
+ }
467
+
468
+ // ── install flow ────────────────────────────────────────────────────
469
+
470
+ async function install() {
471
+ const target = getTargetPath();
472
+ info(`Target: ${BOLD}${target}${RESET}`);
473
+
474
+ const config = readConfig(target);
475
+
476
+ // no file yet — create one
477
+ if (!config) {
478
+ if (!(await confirm(`Create ${target}?`))) {
479
+ info("Aborted.");
480
+ return;
481
+ }
482
+ writeNewConfig(target);
483
+ printNextSteps();
484
+ return;
485
+ }
486
+
487
+ // file exists but can't be parsed
488
+ if (!config.data) {
489
+ err(`Could not parse ${target} — fix the JSON/JSONC syntax first.`);
490
+ process.exit(1);
491
+ }
492
+
493
+ // already has the entry?
494
+ if (findPluginEntry(config.data) !== -1) {
495
+ ok(`${BOLD}${PLUGIN_ENTRY}${RESET} is already in ${target}`);
496
+ printNextSteps();
497
+ return;
498
+ }
499
+
500
+ if (!(await confirm(`Add ${PLUGIN_ENTRY} to ${target}?`))) {
501
+ info("Aborted.");
502
+ return;
503
+ }
504
+
505
+ addPluginEntry(target, config.raw, config.data);
506
+ printNextSteps();
507
+ }
508
+
509
+ // ── uninstall flow ──────────────────────────────────────────────────
510
+
511
+ async function uninstall() {
512
+ const configs = findAllConfigsWithPlugin();
513
+
514
+ if (configs.length === 0) {
515
+ info(`No config files found containing ${BOLD}${PKG_NAME}${RESET}.`);
516
+ } else {
517
+ info(`Found ${configs.length} config(s) with ${BOLD}${PKG_NAME}${RESET}:`);
518
+ for (const c of configs) info(` ${c}`);
519
+
520
+ if (await confirm("Remove open-mem from these configs?")) {
521
+ for (const c of configs) {
522
+ const raw = fs.readFileSync(c, "utf8");
523
+ removePluginEntry(c, raw);
524
+ }
525
+ }
526
+ }
527
+
528
+ // cleanup node_modules
529
+ const stale = findStaleNodeModules(process.cwd());
530
+ if (stale.length > 0) {
531
+ info(`Found ${stale.length} node_modules installation(s):`);
532
+ for (const s of stale) info(` ${s}`);
533
+
534
+ if (await confirm("Remove these?")) {
535
+ cleanupNodeModules();
536
+ }
537
+ }
538
+
539
+ // cleanup OpenCode cache
540
+ const cacheDir = path.join(os.homedir(), ".cache", "opencode", "node_modules", PKG_NAME);
541
+ if (fs.existsSync(cacheDir)) {
542
+ info(`Found cached plugin at ${BOLD}${cacheDir}${RESET}`);
543
+ if (await confirm("Remove cached plugin?")) {
544
+ if (flagDryRun) {
545
+ info(`Would remove ${BOLD}${cacheDir}${RESET}`);
546
+ } else {
547
+ try {
548
+ fs.rmSync(cacheDir, { recursive: true, force: true });
549
+ ok(`Removed ${BOLD}${cacheDir}${RESET}`);
550
+ } catch (e) {
551
+ warn(`Could not remove cache: ${e.message}`);
552
+ }
553
+ }
554
+ }
555
+ }
556
+
557
+ // cleanup OpenCode cache package.json dependency
558
+ const cachePkgPath = path.join(os.homedir(), ".cache", "opencode", "package.json");
559
+ if (fs.existsSync(cachePkgPath)) {
560
+ try {
561
+ const raw = fs.readFileSync(cachePkgPath, "utf8");
562
+ if (raw.includes(`"${PKG_NAME}"`)) {
563
+ const pkg = JSON.parse(raw);
564
+ let changed = false;
565
+ for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
566
+ if (pkg[key] && pkg[key][PKG_NAME]) {
567
+ delete pkg[key][PKG_NAME];
568
+ changed = true;
569
+ }
570
+ }
571
+ if (changed) {
572
+ if (flagDryRun) {
573
+ info(`Would remove ${BOLD}${PKG_NAME}${RESET} from ${cachePkgPath}`);
574
+ } else {
575
+ fs.writeFileSync(cachePkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
576
+ ok(`Removed ${BOLD}${PKG_NAME}${RESET} from ${cachePkgPath}`);
577
+ }
578
+ }
579
+ }
580
+ } catch {
581
+ /* skip */
582
+ }
583
+ }
584
+
585
+ ok("Uninstall complete.");
586
+ }
587
+
588
+ // ── AI provider detection ───────────────────────────────────────────
589
+
590
+ function detectAIProviders() {
591
+ const providers = [
592
+ { key: "GOOGLE_GENERATIVE_AI_API_KEY", name: "Google Gemini" },
593
+ { key: "ANTHROPIC_API_KEY", name: "Anthropic" },
594
+ { key: "AWS_ACCESS_KEY_ID", name: "AWS Bedrock" },
595
+ { key: "OPENAI_API_KEY", name: "OpenAI" },
596
+ { key: "OPENROUTER_API_KEY", name: "OpenRouter" },
597
+ ];
598
+ return providers.filter((p) => process.env[p.key]);
599
+ }
600
+
601
+ // ── next steps ──────────────────────────────────────────────────────
602
+
603
+ function printNextSteps() {
604
+ const detected = detectAIProviders();
605
+
606
+ process.stdout.write(`
607
+ ${GREEN}✓${RESET} ${BOLD}open-mem${RESET} is configured!
608
+
609
+ ${BOLD}Next steps:${RESET}
610
+ 1. Start OpenCode — open-mem loads automatically
611
+ 2. Use ${BOLD}mem-find${RESET}, ${BOLD}mem-create${RESET}, ${BOLD}mem-history${RESET} tools in your sessions
612
+ 3. Observations are captured and compressed automatically
613
+
614
+ `);
615
+
616
+ if (detected.length > 0) {
617
+ const names = detected.map((p) => p.name).join(", ");
618
+ process.stdout.write(`${BOLD}AI compression:${RESET} ${GREEN}✓${RESET} ${names} detected\n`);
619
+ } else {
620
+ process.stdout.write(
621
+ `${BOLD}Optional — enable AI compression:${RESET}\n` +
622
+ ` ${DIM}export GOOGLE_GENERATIVE_AI_API_KEY=...${RESET}\n` +
623
+ ` ${DIM}# Also supports: Anthropic, AWS Bedrock, OpenAI, OpenRouter${RESET}\n`,
624
+ );
625
+ }
626
+
627
+ process.stdout.write(`\n${BOLD}Docs:${RESET} ${DOCS_URL}\n`);
628
+ }
629
+
630
+ // ── main ────────────────────────────────────────────────────────────
631
+
632
+ async function main() {
633
+ const version = getVersion();
634
+ process.stdout.write(
635
+ `\n${BOLD}open-mem${RESET}${version ? ` ${DIM}v${version}${RESET}` : ""} ${DIM}— Persistent memory plugin for OpenCode${RESET}\n\n`,
636
+ );
637
+
638
+ if (flagDryRun) info(`${YELLOW}Dry-run mode${RESET} — no files will be modified.\n`);
639
+
640
+ if (flagUninstall) {
641
+ await uninstall();
642
+ } else {
643
+ await install();
644
+ }
645
+ }
646
+
647
+ main().catch((e) => {
648
+ err(e.message);
649
+ process.exit(1);
650
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-mem",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Persistent memory plugin for OpenCode — captures, compresses, and recalls context across coding sessions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,6 +13,7 @@
13
13
  }
14
14
  },
15
15
  "bin": {
16
+ "open-mem": "./bin/cli.mjs",
16
17
  "open-mem-mcp": "./dist/mcp.js",
17
18
  "open-mem-daemon": "./dist/daemon.js",
18
19
  "open-mem-maintenance": "./dist/maintenance.js",
@@ -22,6 +23,7 @@
22
23
  },
23
24
  "files": [
24
25
  "dist",
26
+ "bin",
25
27
  "README.md",
26
28
  "LICENSE",
27
29
  "CHANGELOG.md"