codemem 0.20.4 → 0.20.5-alpha.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.
@@ -1,5 +1,5 @@
1
1
  export const parseSemver = (value) => {
2
- const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
2
+ const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-.*)?$/);
3
3
  if (!match) return null;
4
4
  return [Number(match[1]), Number(match[2]), Number(match[3])];
5
5
  };
@@ -15,7 +15,7 @@ import {
15
15
 
16
16
  const TRUTHY_VALUES = ["1", "true", "yes"];
17
17
  const DISABLED_VALUES = ["0", "false", "off"];
18
- const PINNED_BACKEND_VERSION = "0.20.4";
18
+ const PINNED_BACKEND_VERSION = "0.20.5-alpha.2";
19
19
 
20
20
  const normalizeEnvValue = (value) => (value || "").toLowerCase();
21
21
  const envHasValue = (value, truthyValues) =>
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAEN,KAAK,uBAAuB,EAG5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAGf;AA0KD,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,uBAAuB,EACnC,QAAQ,GAAE,MAAM,EAAqB,GACnC,MAAM,EAAE,CAgBV;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS9D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpF;AA0OD,eAAO,MAAM,YAAY,SAuBtB,CAAC"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAEN,KAAK,uBAAuB,EAG5B,MAAM,uBAAuB,CAAC;AAQ/B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKhE;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASjD;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ9D;AAED,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,WAAW,EAAE,MAAM,GAAG,IAAI,GACxB,MAAM,GAAG,IAAI,CAGf;AAqLD,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,uBAAuB,EACnC,QAAQ,GAAE,MAAM,EAAqB,GACnC,MAAM,EAAE,CAgBV;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS9D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAUpF;AAkRD,eAAO,MAAM,YAAY,SAuBtB,CAAC"}
@@ -4,9 +4,9 @@
4
4
  * Replaces Python's install_plugin_cmd + install_mcp_cmd.
5
5
  *
6
6
  * What it does:
7
- * 1. Copies the plugin file to ~/.config/opencode/plugin/codemem.js
8
- * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.json
9
- * 3. Copies the compat lib to ~/.config/opencode/lib/compat.js
7
+ * 1. Adds "codemem" to the plugin array in ~/.config/opencode/opencode.jsonc
8
+ * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.jsonc
9
+ * 3. For Claude Code: installs MCP config and guides marketplace plugin install
10
10
  *
11
11
  * Designed to be safe to run repeatedly (idempotent unless --force).
12
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmKpC,eAAO,MAAM,YAAY,SA6BtB,CAAC"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyQpC,eAAO,MAAM,YAAY,SA6BtB,CAAC"}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, VERSION, backfillTagsText, backfillVectors, buildRawEventEnvelopeFromHook, connect, coordinatorCreateInviteAction, coordinatorImportInviteAction, coordinatorListJoinRequestsAction, coordinatorReviewJoinRequestAction, createCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command } from "commander";
4
4
  import omelette from "omelette";
5
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { styleText } from "node:util";
7
7
  import * as p from "@clack/prompts";
8
8
  import { homedir, networkInterfaces } from "node:os";
@@ -1013,24 +1013,41 @@ async function isPortOpen(host, port) {
1013
1013
  socket.once("error", () => done(false));
1014
1014
  });
1015
1015
  }
1016
- async function waitForProcessExit(pid, timeoutMs = 5e3) {
1016
+ async function waitForProcessExit(pid, timeoutMs = 3e4) {
1017
1017
  const deadline = Date.now() + timeoutMs;
1018
1018
  while (Date.now() < deadline) {
1019
- if (!isProcessRunning(pid)) return;
1019
+ if (!isProcessRunning(pid)) return true;
1020
1020
  await new Promise((resolve) => setTimeout(resolve, 100));
1021
1021
  }
1022
+ return !isProcessRunning(pid);
1023
+ }
1024
+ async function waitForPortRelease(host, port, timeoutMs = 1e4) {
1025
+ const deadline = Date.now() + timeoutMs;
1026
+ while (Date.now() < deadline) {
1027
+ if (!await isPortOpen(host, port)) return true;
1028
+ await new Promise((resolve) => setTimeout(resolve, 200));
1029
+ }
1030
+ return false;
1022
1031
  }
1023
1032
  async function stopExistingViewer(dbPath, target) {
1033
+ const terminatePid = async (pid) => {
1034
+ try {
1035
+ process.kill(pid, "SIGTERM");
1036
+ return await waitForProcessExit(pid);
1037
+ } catch {
1038
+ return true;
1039
+ }
1040
+ };
1024
1041
  const pidPath = pidFilePath(dbPath);
1025
1042
  const record = readViewerPidRecord(dbPath);
1026
1043
  const viewerPidFromStats = await lookupViewerPidFromStats(target.host, target.port);
1027
1044
  const listenerPid = lookupListeningPid(target.host, target.port);
1028
1045
  const viewerPid = pickViewerPidCandidate(viewerPidFromStats, listenerPid);
1029
1046
  if (viewerPid && isTrustedViewerPid(viewerPid, target, listenerPid)) {
1030
- try {
1031
- process.kill(viewerPid, "SIGTERM");
1032
- await waitForProcessExit(viewerPid);
1033
- } catch {}
1047
+ if (!await terminatePid(viewerPid)) return {
1048
+ stopped: false,
1049
+ pid: viewerPid
1050
+ };
1034
1051
  try {
1035
1052
  rmSync(pidPath);
1036
1053
  } catch {}
@@ -1043,10 +1060,12 @@ async function stopExistingViewer(dbPath, target) {
1043
1060
  stopped: false,
1044
1061
  pid: null
1045
1062
  };
1046
- if (await respondsLikeCodememViewer(record)) try {
1047
- process.kill(record.pid, "SIGTERM");
1048
- await waitForProcessExit(record.pid);
1049
- } catch {}
1063
+ if (await respondsLikeCodememViewer(record)) {
1064
+ if (!await terminatePid(record.pid)) return {
1065
+ stopped: false,
1066
+ pid: record.pid
1067
+ };
1068
+ }
1050
1069
  try {
1051
1070
  rmSync(pidPath);
1052
1071
  } catch {}
@@ -1136,29 +1155,17 @@ async function startForegroundViewer(invocation) {
1136
1155
  const sweeper = new RawEventSweeper(store, { observer });
1137
1156
  sweeper.start();
1138
1157
  const syncAbort = new AbortController();
1139
- let syncRunning = false;
1140
1158
  const syncConfig = readCoordinatorSyncConfig(readCodememConfigFile());
1141
1159
  const syncEnabled = syncConfig.syncEnabled;
1142
- if (syncEnabled) {
1143
- syncRunning = true;
1144
- const syncIntervalS = syncConfig.syncIntervalS;
1145
- runSyncDaemon({
1146
- dbPath: resolveDbPath(invocation.dbPath ?? void 0),
1147
- intervalS: syncIntervalS,
1148
- host: syncConfig.syncHost,
1149
- port: syncConfig.syncPort,
1150
- signal: syncAbort.signal
1151
- }).catch((err) => {
1152
- const msg = err instanceof Error ? err.message : String(err);
1153
- p.log.error(`Sync daemon failed: ${msg}`);
1154
- }).finally(() => {
1155
- syncRunning = false;
1156
- });
1157
- }
1160
+ const syncRuntimeStatus = {
1161
+ phase: syncEnabled ? "starting" : "disabled",
1162
+ detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
1163
+ };
1158
1164
  const appOpts = {
1159
1165
  storeFactory: () => store,
1160
1166
  sweeper,
1161
- observer
1167
+ observer,
1168
+ getSyncRuntimeStatus: () => syncRuntimeStatus
1162
1169
  };
1163
1170
  const app = createApp(appOpts);
1164
1171
  const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
@@ -1193,7 +1200,40 @@ async function startForegroundViewer(invocation) {
1193
1200
  p.log.success(`Listening on http://${info.address}:${info.port}`);
1194
1201
  p.log.info(`Database: ${dbPath}`);
1195
1202
  p.log.step("Raw event sweeper started");
1196
- if (syncRunning) p.log.step("Sync daemon started");
1203
+ if (syncEnabled) {
1204
+ const syncStartDelayMs = 3e3;
1205
+ p.log.step(`Sync daemon will start in background (${syncStartDelayMs / 1e3}s delay)`);
1206
+ setTimeout(() => {
1207
+ syncRuntimeStatus.phase = "starting";
1208
+ syncRuntimeStatus.detail = "Starting sync in background";
1209
+ runSyncDaemon({
1210
+ dbPath: resolveDbPath(invocation.dbPath ?? void 0),
1211
+ intervalS: syncConfig.syncIntervalS,
1212
+ host: syncConfig.syncHost,
1213
+ port: syncConfig.syncPort,
1214
+ signal: syncAbort.signal,
1215
+ onPhaseChange: (phase) => {
1216
+ if (phase === "running") {
1217
+ syncRuntimeStatus.phase = null;
1218
+ syncRuntimeStatus.detail = null;
1219
+ } else {
1220
+ syncRuntimeStatus.phase = phase;
1221
+ syncRuntimeStatus.detail = phase === "starting" ? "Running initial sync in background" : "Stopping sync daemon";
1222
+ }
1223
+ }
1224
+ }).catch((err) => {
1225
+ const msg = err instanceof Error ? err.message : String(err);
1226
+ syncRuntimeStatus.phase = "error";
1227
+ syncRuntimeStatus.detail = msg;
1228
+ p.log.error(`Sync daemon failed: ${msg}`);
1229
+ }).finally(() => {
1230
+ if (syncRuntimeStatus.phase !== "error") {
1231
+ syncRuntimeStatus.phase = syncAbort.signal.aborted ? "stopping" : null;
1232
+ syncRuntimeStatus.detail = syncAbort.signal.aborted ? "Sync stopped" : null;
1233
+ }
1234
+ });
1235
+ }, syncStartDelayMs).unref();
1236
+ }
1197
1237
  });
1198
1238
  server.on("error", (err) => {
1199
1239
  if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
@@ -1225,7 +1265,7 @@ async function startForegroundViewer(invocation) {
1225
1265
  } catch {}
1226
1266
  closeStore();
1227
1267
  process.exit(1);
1228
- }, 5e3).unref();
1268
+ }, 3e4).unref();
1229
1269
  };
1230
1270
  process.on("SIGINT", () => {
1231
1271
  forceShutdown();
@@ -1250,6 +1290,12 @@ async function runServeInvocation(invocation) {
1250
1290
  p.outro("done");
1251
1291
  return;
1252
1292
  }
1293
+ if (!await waitForPortRelease(invocation.host, invocation.port)) p.log.warn(`Port ${invocation.port} still in use after stop — restart may fail`);
1294
+ } else if (result.pid) {
1295
+ p.intro("codemem viewer");
1296
+ p.log.error(`Viewer is still shutting down (pid ${result.pid})`);
1297
+ process.exitCode = 1;
1298
+ return;
1253
1299
  } else if (invocation.mode === "stop") {
1254
1300
  p.intro("codemem viewer");
1255
1301
  p.outro("No background viewer found");
@@ -1313,9 +1359,9 @@ function writeJsonConfig(path, data) {
1313
1359
  * Replaces Python's install_plugin_cmd + install_mcp_cmd.
1314
1360
  *
1315
1361
  * What it does:
1316
- * 1. Copies the plugin file to ~/.config/opencode/plugin/codemem.js
1317
- * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.json
1318
- * 3. Copies the compat lib to ~/.config/opencode/lib/compat.js
1362
+ * 1. Adds "codemem" to the plugin array in ~/.config/opencode/opencode.jsonc
1363
+ * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.jsonc
1364
+ * 3. For Claude Code: installs MCP config and guides marketplace plugin install
1319
1365
  *
1320
1366
  * Designed to be safe to run repeatedly (idempotent unless --force).
1321
1367
  */
@@ -1325,57 +1371,93 @@ function opencodeConfigDir() {
1325
1371
  function claudeConfigDir() {
1326
1372
  return join(homedir(), ".claude");
1327
1373
  }
1328
- /**
1329
- * Find the plugin source file — walk up from this module's location
1330
- * to find the .opencode/plugins/codemem.js in the package tree.
1331
- */
1332
- function findPluginSource() {
1333
- let dir = dirname(import.meta.url.replace("file://", ""));
1334
- for (let i = 0; i < 6; i++) {
1335
- const candidate = join(dir, ".opencode", "plugins", "codemem.js");
1336
- if (existsSync(candidate)) return candidate;
1337
- const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "plugins", "codemem.js");
1338
- if (existsSync(nmCandidate)) return nmCandidate;
1339
- const parent = dirname(dir);
1340
- if (parent === dir) break;
1341
- dir = parent;
1374
+ /** The npm package name used in the OpenCode plugin array. */
1375
+ var OPENCODE_PLUGIN_SPEC = "codemem";
1376
+ /** Remove legacy copied plugin JS file from ~/.config/opencode/plugins/codemem.js */
1377
+ function migrateLegacyOpencodePlugin() {
1378
+ const legacyPlugin = join(opencodeConfigDir(), "plugins", "codemem.js");
1379
+ const legacyCompat = join(opencodeConfigDir(), "lib", "compat.js");
1380
+ if (existsSync(legacyPlugin)) try {
1381
+ rmSync(legacyPlugin);
1382
+ p.log.step("Removed legacy copied plugin: ~/.config/opencode/plugins/codemem.js");
1383
+ } catch {
1384
+ p.log.warn("Could not remove legacy plugin file — remove manually if needed");
1342
1385
  }
1343
- return null;
1386
+ if (existsSync(legacyCompat)) try {
1387
+ rmSync(legacyCompat);
1388
+ p.log.step("Removed legacy compat lib: ~/.config/opencode/lib/compat.js");
1389
+ } catch {}
1344
1390
  }
1345
- function findCompatSource() {
1346
- let dir = dirname(import.meta.url.replace("file://", ""));
1347
- for (let i = 0; i < 6; i++) {
1348
- const candidate = join(dir, ".opencode", "lib", "compat.js");
1349
- if (existsSync(candidate)) return candidate;
1350
- const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "lib", "compat.js");
1351
- if (existsSync(nmCandidate)) return nmCandidate;
1352
- const legacyCandidate = join(dir, "node_modules", "@kunickiaj", "codemem", ".opencode", "lib", "compat.js");
1353
- if (existsSync(legacyCandidate)) return legacyCandidate;
1354
- const parent = dirname(dir);
1355
- if (parent === dir) break;
1356
- dir = parent;
1391
+ /** Detect and upgrade legacy uvx/uv-based MCP entries in OpenCode config. */
1392
+ function migrateLegacyOpencodeMcp(config) {
1393
+ const mcpConfig = config.mcp;
1394
+ if (!mcpConfig || typeof mcpConfig !== "object") return false;
1395
+ const entry = mcpConfig.codemem;
1396
+ if (!entry || typeof entry !== "object") return false;
1397
+ const command = entry.command;
1398
+ if (Array.isArray(command) && command.some((arg) => typeof arg === "string" && (arg === "uvx" || arg === "uv")) || typeof command === "string" && (command === "uvx" || command === "uv")) {
1399
+ p.log.step("Upgrading legacy uvx MCP entry to npx");
1400
+ mcpConfig.codemem = {
1401
+ type: "local",
1402
+ command: [
1403
+ "npx",
1404
+ "codemem",
1405
+ "mcp"
1406
+ ],
1407
+ enabled: true
1408
+ };
1409
+ return true;
1357
1410
  }
1358
- return null;
1411
+ return false;
1412
+ }
1413
+ /** Detect and upgrade legacy uvx-based MCP entries in Claude settings. */
1414
+ function migrateLegacyClaudeMcp(settings) {
1415
+ const mcpServers = settings.mcpServers;
1416
+ if (!mcpServers || typeof mcpServers !== "object") return false;
1417
+ const entry = mcpServers.codemem;
1418
+ if (!entry || typeof entry !== "object") return false;
1419
+ const command = entry.command;
1420
+ const args = entry.args;
1421
+ if (typeof command === "string" && (command === "uvx" || command === "uv") || Array.isArray(args) && args.some((arg) => typeof arg === "string" && (arg.startsWith("codemem==") || arg === "uvx"))) {
1422
+ p.log.step("Upgrading legacy uvx Claude MCP entry to npx");
1423
+ mcpServers.codemem = {
1424
+ command: "npx",
1425
+ args: [
1426
+ "-y",
1427
+ "codemem",
1428
+ "mcp"
1429
+ ]
1430
+ };
1431
+ return true;
1432
+ }
1433
+ return false;
1359
1434
  }
1360
1435
  function installPlugin(force) {
1361
- const source = findPluginSource();
1362
- if (!source) {
1363
- p.log.error("Plugin file not found in package tree");
1436
+ migrateLegacyOpencodePlugin();
1437
+ const configPath = resolveOpencodeConfigPath(opencodeConfigDir());
1438
+ let config;
1439
+ try {
1440
+ config = loadJsoncConfig(configPath);
1441
+ } catch (err) {
1442
+ p.log.error(`Failed to parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
1364
1443
  return false;
1365
1444
  }
1366
- const destDir = join(opencodeConfigDir(), "plugins");
1367
- const dest = join(destDir, "codemem.js");
1368
- if (existsSync(dest) && !force) p.log.info(`Plugin already installed at ${dest}`);
1369
- else {
1370
- mkdirSync(destDir, { recursive: true });
1371
- copyFileSync(source, dest);
1372
- p.log.success(`Plugin installed: ${dest}`);
1445
+ let plugins = config.plugin;
1446
+ if (!Array.isArray(plugins)) plugins = [];
1447
+ const hasCodemem = plugins.some((entry) => typeof entry === "string" && (entry === OPENCODE_PLUGIN_SPEC || entry.startsWith(`${OPENCODE_PLUGIN_SPEC}@`)));
1448
+ if (hasCodemem && !force) {
1449
+ p.log.info(`Plugin "${OPENCODE_PLUGIN_SPEC}" already in plugin array`);
1450
+ return true;
1373
1451
  }
1374
- const compatSource = findCompatSource();
1375
- if (compatSource) {
1376
- const compatDir = join(opencodeConfigDir(), "lib");
1377
- mkdirSync(compatDir, { recursive: true });
1378
- copyFileSync(compatSource, join(compatDir, "compat.js"));
1452
+ if (hasCodemem && force) plugins = plugins.filter((entry) => typeof entry !== "string" || entry !== OPENCODE_PLUGIN_SPEC && !entry.startsWith(`${OPENCODE_PLUGIN_SPEC}@`));
1453
+ plugins.push(OPENCODE_PLUGIN_SPEC);
1454
+ config.plugin = plugins;
1455
+ try {
1456
+ writeJsonConfig(configPath, config);
1457
+ p.log.success(`Plugin "${OPENCODE_PLUGIN_SPEC}" added to ${configPath}`);
1458
+ } catch (err) {
1459
+ p.log.error(`Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
1460
+ return false;
1379
1461
  }
1380
1462
  return true;
1381
1463
  }
@@ -1390,20 +1472,23 @@ function installMcp(force) {
1390
1472
  }
1391
1473
  let mcpConfig = config.mcp;
1392
1474
  if (mcpConfig == null || typeof mcpConfig !== "object" || Array.isArray(mcpConfig)) mcpConfig = {};
1393
- if ("codemem" in mcpConfig && !force) {
1475
+ const migrated = migrateLegacyOpencodeMcp(config);
1476
+ if ("codemem" in mcpConfig && !force && !migrated) {
1394
1477
  p.log.info(`MCP entry already exists in ${configPath}`);
1395
1478
  return true;
1396
1479
  }
1397
- mcpConfig.codemem = {
1398
- type: "local",
1399
- command: [
1400
- "npx",
1401
- "codemem",
1402
- "mcp"
1403
- ],
1404
- enabled: true
1405
- };
1406
- config.mcp = mcpConfig;
1480
+ if (!migrated) {
1481
+ mcpConfig.codemem = {
1482
+ type: "local",
1483
+ command: [
1484
+ "npx",
1485
+ "codemem",
1486
+ "mcp"
1487
+ ],
1488
+ enabled: true
1489
+ };
1490
+ config.mcp = mcpConfig;
1491
+ }
1407
1492
  try {
1408
1493
  writeJsonConfig(configPath, config);
1409
1494
  p.log.success(`MCP entry installed: ${configPath}`);
@@ -1413,6 +1498,12 @@ function installMcp(force) {
1413
1498
  }
1414
1499
  return true;
1415
1500
  }
1501
+ function isClaudeHooksPluginInstalled() {
1502
+ const pluginDir = join(claudeConfigDir(), "plugins", "codemem");
1503
+ if (existsSync(pluginDir)) return true;
1504
+ if (existsSync(join(pluginDir, "hooks", "hooks.json"))) return true;
1505
+ return false;
1506
+ }
1416
1507
  function installClaudeMcp(force) {
1417
1508
  const settingsPath = join(claudeConfigDir(), "settings.json");
1418
1509
  let settings;
@@ -1423,22 +1514,36 @@ function installClaudeMcp(force) {
1423
1514
  }
1424
1515
  let mcpServers = settings.mcpServers;
1425
1516
  if (mcpServers == null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) mcpServers = {};
1426
- if ("codemem" in mcpServers && !force) {
1427
- p.log.info(`Claude MCP entry already exists in ${settingsPath}`);
1428
- return true;
1429
- }
1430
- mcpServers.codemem = {
1431
- command: "npx",
1432
- args: ["codemem", "mcp"]
1433
- };
1434
- settings.mcpServers = mcpServers;
1435
- try {
1436
- writeJsonConfig(settingsPath, settings);
1437
- p.log.success(`Claude MCP entry installed: ${settingsPath}`);
1438
- } catch (err) {
1439
- p.log.error(`Failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
1440
- return false;
1517
+ const migrated = migrateLegacyClaudeMcp(settings);
1518
+ if ("codemem" in mcpServers && !force && !migrated) p.log.info(`Claude MCP entry already exists in ${settingsPath}`);
1519
+ else {
1520
+ if (!migrated) {
1521
+ mcpServers.codemem = {
1522
+ command: "npx",
1523
+ args: [
1524
+ "-y",
1525
+ "codemem",
1526
+ "mcp"
1527
+ ]
1528
+ };
1529
+ settings.mcpServers = mcpServers;
1530
+ }
1531
+ try {
1532
+ writeJsonConfig(settingsPath, settings);
1533
+ p.log.success(`Claude MCP entry installed: ${settingsPath}`);
1534
+ } catch (err) {
1535
+ p.log.error(`Failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
1536
+ return false;
1537
+ }
1441
1538
  }
1539
+ if (!isClaudeHooksPluginInstalled() || force) {
1540
+ p.log.info("To install the Claude Code hooks plugin, run in Claude Code:");
1541
+ p.log.info(" /plugin marketplace add kunickiaj/codemem");
1542
+ p.log.info(" /plugin install codemem");
1543
+ p.log.info("");
1544
+ p.log.info("To update an existing install:");
1545
+ p.log.info(" /plugin marketplace update codemem-marketplace");
1546
+ } else p.log.info("Claude Code hooks plugin appears to be installed");
1442
1547
  return true;
1443
1548
  }
1444
1549
  var setupCommand = new Command("setup").configureHelp(helpStyle).description("Install codemem plugin + MCP config for OpenCode and Claude Code").option("--force", "overwrite existing installations").option("--opencode-only", "only install for OpenCode").option("--claude-only", "only install for Claude Code").action((opts) => {