codemem 0.20.3 → 0.20.5-alpha.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.
@@ -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.3";
18
+ const PINNED_BACKEND_VERSION = "0.20.5-alpha.1";
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":"AAcA,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;AAiMD,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;AAmLD,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;AA+OD,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AA8BH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8HpC,eAAO,MAAM,WAAW,SAE+B,CAAC"}
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAAA;;GAEG;AA8BH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwHpC,eAAO,MAAM,WAAW,SAE+B,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
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, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
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";
@@ -1020,6 +1020,14 @@ async function waitForProcessExit(pid, timeoutMs = 5e3) {
1020
1020
  await new Promise((resolve) => setTimeout(resolve, 100));
1021
1021
  }
1022
1022
  }
1023
+ async function waitForPortRelease(host, port, timeoutMs = 1e4) {
1024
+ const deadline = Date.now() + timeoutMs;
1025
+ while (Date.now() < deadline) {
1026
+ if (!await isPortOpen(host, port)) return true;
1027
+ await new Promise((resolve) => setTimeout(resolve, 200));
1028
+ }
1029
+ return false;
1030
+ }
1023
1031
  async function stopExistingViewer(dbPath, target) {
1024
1032
  const pidPath = pidFilePath(dbPath);
1025
1033
  const record = readViewerPidRecord(dbPath);
@@ -1112,7 +1120,7 @@ async function startBackgroundViewer(invocation) {
1112
1120
  p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
1113
1121
  }
1114
1122
  async function startForegroundViewer(invocation) {
1115
- const { createApp, closeStore, getStore } = await import("@codemem/server");
1123
+ const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
1116
1124
  const { serve } = await import("@hono/node-server");
1117
1125
  if (invocation.dbPath) process.env.CODEMEM_DB = invocation.dbPath;
1118
1126
  if (await isPortOpen(invocation.host, invocation.port)) {
@@ -1137,15 +1145,16 @@ async function startForegroundViewer(invocation) {
1137
1145
  sweeper.start();
1138
1146
  const syncAbort = new AbortController();
1139
1147
  let syncRunning = false;
1140
- const config = readCodememConfigFile();
1141
- if (config.sync_enabled === true || process.env.CODEMEM_SYNC_ENABLED?.toLowerCase() === "true" || process.env.CODEMEM_SYNC_ENABLED === "1") {
1148
+ const syncConfig = readCoordinatorSyncConfig(readCodememConfigFile());
1149
+ const syncEnabled = syncConfig.syncEnabled;
1150
+ if (syncEnabled) {
1142
1151
  syncRunning = true;
1143
- const syncIntervalS = typeof config.sync_interval_s === "number" ? config.sync_interval_s : 120;
1152
+ const syncIntervalS = syncConfig.syncIntervalS;
1144
1153
  runSyncDaemon({
1145
1154
  dbPath: resolveDbPath(invocation.dbPath ?? void 0),
1146
1155
  intervalS: syncIntervalS,
1147
- host: invocation.host,
1148
- port: invocation.port,
1156
+ host: syncConfig.syncHost,
1157
+ port: syncConfig.syncPort,
1149
1158
  signal: syncAbort.signal
1150
1159
  }).catch((err) => {
1151
1160
  const msg = err instanceof Error ? err.message : String(err);
@@ -1154,13 +1163,30 @@ async function startForegroundViewer(invocation) {
1154
1163
  syncRunning = false;
1155
1164
  });
1156
1165
  }
1157
- const app = createApp({
1166
+ const appOpts = {
1158
1167
  storeFactory: () => store,
1159
1168
  sweeper,
1160
1169
  observer
1161
- });
1170
+ };
1171
+ const app = createApp(appOpts);
1162
1172
  const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
1163
1173
  const pidPath = pidFilePath(dbPath);
1174
+ let syncServer = null;
1175
+ let syncListenerReady = false;
1176
+ if (syncEnabled) {
1177
+ syncServer = serve({
1178
+ fetch: createSyncApp(appOpts).fetch,
1179
+ hostname: syncConfig.syncHost,
1180
+ port: syncConfig.syncPort
1181
+ }, (info) => {
1182
+ syncListenerReady = true;
1183
+ p.log.step(`Sync protocol listening on http://${info.address}:${info.port}`);
1184
+ });
1185
+ syncServer.on("error", (err) => {
1186
+ if (!syncListenerReady && err.code === "EADDRINUSE") p.log.warn(`Sync port ${syncConfig.syncPort} already in use; peer sync protocol unavailable`);
1187
+ else p.log.warn(`Sync listener error: ${err.message}`);
1188
+ });
1189
+ }
1164
1190
  const server = serve({
1165
1191
  fetch: app.fetch,
1166
1192
  hostname: invocation.host,
@@ -1186,13 +1212,21 @@ async function startForegroundViewer(invocation) {
1186
1212
  p.outro("shutting down");
1187
1213
  syncAbort.abort();
1188
1214
  await sweeper.stop();
1189
- server.close(() => {
1190
- try {
1191
- rmSync(pidPath);
1192
- } catch {}
1193
- closeStore();
1194
- process.exit(0);
1195
- });
1215
+ await new Promise((resolve) => {
1216
+ let remaining = syncServer ? 2 : 1;
1217
+ const done = () => {
1218
+ if (--remaining === 0) resolve();
1219
+ };
1220
+ syncServer?.close(done);
1221
+ server.close(done);
1222
+ }).catch(() => {});
1223
+ try {
1224
+ rmSync(pidPath);
1225
+ } catch {}
1226
+ closeStore();
1227
+ process.exit(0);
1228
+ };
1229
+ const forceShutdown = () => {
1196
1230
  setTimeout(() => {
1197
1231
  try {
1198
1232
  rmSync(pidPath);
@@ -1202,9 +1236,11 @@ async function startForegroundViewer(invocation) {
1202
1236
  }, 5e3).unref();
1203
1237
  };
1204
1238
  process.on("SIGINT", () => {
1239
+ forceShutdown();
1205
1240
  shutdown();
1206
1241
  });
1207
1242
  process.on("SIGTERM", () => {
1243
+ forceShutdown();
1208
1244
  shutdown();
1209
1245
  });
1210
1246
  }
@@ -1222,6 +1258,7 @@ async function runServeInvocation(invocation) {
1222
1258
  p.outro("done");
1223
1259
  return;
1224
1260
  }
1261
+ if (!await waitForPortRelease(invocation.host, invocation.port)) p.log.warn(`Port ${invocation.port} still in use after stop — restart may fail`);
1225
1262
  } else if (invocation.mode === "stop") {
1226
1263
  p.intro("codemem viewer");
1227
1264
  p.outro("No background viewer found");
@@ -1285,9 +1322,9 @@ function writeJsonConfig(path, data) {
1285
1322
  * Replaces Python's install_plugin_cmd + install_mcp_cmd.
1286
1323
  *
1287
1324
  * What it does:
1288
- * 1. Copies the plugin file to ~/.config/opencode/plugin/codemem.js
1289
- * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.json
1290
- * 3. Copies the compat lib to ~/.config/opencode/lib/compat.js
1325
+ * 1. Adds "codemem" to the plugin array in ~/.config/opencode/opencode.jsonc
1326
+ * 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.jsonc
1327
+ * 3. For Claude Code: installs MCP config and guides marketplace plugin install
1291
1328
  *
1292
1329
  * Designed to be safe to run repeatedly (idempotent unless --force).
1293
1330
  */
@@ -1297,57 +1334,93 @@ function opencodeConfigDir() {
1297
1334
  function claudeConfigDir() {
1298
1335
  return join(homedir(), ".claude");
1299
1336
  }
1300
- /**
1301
- * Find the plugin source file — walk up from this module's location
1302
- * to find the .opencode/plugins/codemem.js in the package tree.
1303
- */
1304
- function findPluginSource() {
1305
- let dir = dirname(import.meta.url.replace("file://", ""));
1306
- for (let i = 0; i < 6; i++) {
1307
- const candidate = join(dir, ".opencode", "plugins", "codemem.js");
1308
- if (existsSync(candidate)) return candidate;
1309
- const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "plugins", "codemem.js");
1310
- if (existsSync(nmCandidate)) return nmCandidate;
1311
- const parent = dirname(dir);
1312
- if (parent === dir) break;
1313
- dir = parent;
1337
+ /** The npm package name used in the OpenCode plugin array. */
1338
+ var OPENCODE_PLUGIN_SPEC = "codemem";
1339
+ /** Remove legacy copied plugin JS file from ~/.config/opencode/plugins/codemem.js */
1340
+ function migrateLegacyOpencodePlugin() {
1341
+ const legacyPlugin = join(opencodeConfigDir(), "plugins", "codemem.js");
1342
+ const legacyCompat = join(opencodeConfigDir(), "lib", "compat.js");
1343
+ if (existsSync(legacyPlugin)) try {
1344
+ rmSync(legacyPlugin);
1345
+ p.log.step("Removed legacy copied plugin: ~/.config/opencode/plugins/codemem.js");
1346
+ } catch {
1347
+ p.log.warn("Could not remove legacy plugin file — remove manually if needed");
1314
1348
  }
1315
- return null;
1349
+ if (existsSync(legacyCompat)) try {
1350
+ rmSync(legacyCompat);
1351
+ p.log.step("Removed legacy compat lib: ~/.config/opencode/lib/compat.js");
1352
+ } catch {}
1316
1353
  }
1317
- function findCompatSource() {
1318
- let dir = dirname(import.meta.url.replace("file://", ""));
1319
- for (let i = 0; i < 6; i++) {
1320
- const candidate = join(dir, ".opencode", "lib", "compat.js");
1321
- if (existsSync(candidate)) return candidate;
1322
- const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "lib", "compat.js");
1323
- if (existsSync(nmCandidate)) return nmCandidate;
1324
- const legacyCandidate = join(dir, "node_modules", "@kunickiaj", "codemem", ".opencode", "lib", "compat.js");
1325
- if (existsSync(legacyCandidate)) return legacyCandidate;
1326
- const parent = dirname(dir);
1327
- if (parent === dir) break;
1328
- dir = parent;
1354
+ /** Detect and upgrade legacy uvx/uv-based MCP entries in OpenCode config. */
1355
+ function migrateLegacyOpencodeMcp(config) {
1356
+ const mcpConfig = config.mcp;
1357
+ if (!mcpConfig || typeof mcpConfig !== "object") return false;
1358
+ const entry = mcpConfig.codemem;
1359
+ if (!entry || typeof entry !== "object") return false;
1360
+ const command = entry.command;
1361
+ if (Array.isArray(command) && command.some((arg) => typeof arg === "string" && (arg === "uvx" || arg === "uv")) || typeof command === "string" && (command === "uvx" || command === "uv")) {
1362
+ p.log.step("Upgrading legacy uvx MCP entry to npx");
1363
+ mcpConfig.codemem = {
1364
+ type: "local",
1365
+ command: [
1366
+ "npx",
1367
+ "codemem",
1368
+ "mcp"
1369
+ ],
1370
+ enabled: true
1371
+ };
1372
+ return true;
1329
1373
  }
1330
- return null;
1374
+ return false;
1375
+ }
1376
+ /** Detect and upgrade legacy uvx-based MCP entries in Claude settings. */
1377
+ function migrateLegacyClaudeMcp(settings) {
1378
+ const mcpServers = settings.mcpServers;
1379
+ if (!mcpServers || typeof mcpServers !== "object") return false;
1380
+ const entry = mcpServers.codemem;
1381
+ if (!entry || typeof entry !== "object") return false;
1382
+ const command = entry.command;
1383
+ const args = entry.args;
1384
+ if (typeof command === "string" && (command === "uvx" || command === "uv") || Array.isArray(args) && args.some((arg) => typeof arg === "string" && (arg.startsWith("codemem==") || arg === "uvx"))) {
1385
+ p.log.step("Upgrading legacy uvx Claude MCP entry to npx");
1386
+ mcpServers.codemem = {
1387
+ command: "npx",
1388
+ args: [
1389
+ "-y",
1390
+ "codemem",
1391
+ "mcp"
1392
+ ]
1393
+ };
1394
+ return true;
1395
+ }
1396
+ return false;
1331
1397
  }
1332
1398
  function installPlugin(force) {
1333
- const source = findPluginSource();
1334
- if (!source) {
1335
- p.log.error("Plugin file not found in package tree");
1399
+ migrateLegacyOpencodePlugin();
1400
+ const configPath = resolveOpencodeConfigPath(opencodeConfigDir());
1401
+ let config;
1402
+ try {
1403
+ config = loadJsoncConfig(configPath);
1404
+ } catch (err) {
1405
+ p.log.error(`Failed to parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
1336
1406
  return false;
1337
1407
  }
1338
- const destDir = join(opencodeConfigDir(), "plugins");
1339
- const dest = join(destDir, "codemem.js");
1340
- if (existsSync(dest) && !force) p.log.info(`Plugin already installed at ${dest}`);
1341
- else {
1342
- mkdirSync(destDir, { recursive: true });
1343
- copyFileSync(source, dest);
1344
- p.log.success(`Plugin installed: ${dest}`);
1408
+ let plugins = config.plugin;
1409
+ if (!Array.isArray(plugins)) plugins = [];
1410
+ const hasCodemem = plugins.some((entry) => typeof entry === "string" && (entry === OPENCODE_PLUGIN_SPEC || entry.startsWith(`${OPENCODE_PLUGIN_SPEC}@`)));
1411
+ if (hasCodemem && !force) {
1412
+ p.log.info(`Plugin "${OPENCODE_PLUGIN_SPEC}" already in plugin array`);
1413
+ return true;
1345
1414
  }
1346
- const compatSource = findCompatSource();
1347
- if (compatSource) {
1348
- const compatDir = join(opencodeConfigDir(), "lib");
1349
- mkdirSync(compatDir, { recursive: true });
1350
- copyFileSync(compatSource, join(compatDir, "compat.js"));
1415
+ if (hasCodemem && force) plugins = plugins.filter((entry) => typeof entry !== "string" || entry !== OPENCODE_PLUGIN_SPEC && !entry.startsWith(`${OPENCODE_PLUGIN_SPEC}@`));
1416
+ plugins.push(OPENCODE_PLUGIN_SPEC);
1417
+ config.plugin = plugins;
1418
+ try {
1419
+ writeJsonConfig(configPath, config);
1420
+ p.log.success(`Plugin "${OPENCODE_PLUGIN_SPEC}" added to ${configPath}`);
1421
+ } catch (err) {
1422
+ p.log.error(`Failed to write ${configPath}: ${err instanceof Error ? err.message : String(err)}`);
1423
+ return false;
1351
1424
  }
1352
1425
  return true;
1353
1426
  }
@@ -1362,20 +1435,23 @@ function installMcp(force) {
1362
1435
  }
1363
1436
  let mcpConfig = config.mcp;
1364
1437
  if (mcpConfig == null || typeof mcpConfig !== "object" || Array.isArray(mcpConfig)) mcpConfig = {};
1365
- if ("codemem" in mcpConfig && !force) {
1438
+ const migrated = migrateLegacyOpencodeMcp(config);
1439
+ if ("codemem" in mcpConfig && !force && !migrated) {
1366
1440
  p.log.info(`MCP entry already exists in ${configPath}`);
1367
1441
  return true;
1368
1442
  }
1369
- mcpConfig.codemem = {
1370
- type: "local",
1371
- command: [
1372
- "npx",
1373
- "codemem",
1374
- "mcp"
1375
- ],
1376
- enabled: true
1377
- };
1378
- config.mcp = mcpConfig;
1443
+ if (!migrated) {
1444
+ mcpConfig.codemem = {
1445
+ type: "local",
1446
+ command: [
1447
+ "npx",
1448
+ "codemem",
1449
+ "mcp"
1450
+ ],
1451
+ enabled: true
1452
+ };
1453
+ config.mcp = mcpConfig;
1454
+ }
1379
1455
  try {
1380
1456
  writeJsonConfig(configPath, config);
1381
1457
  p.log.success(`MCP entry installed: ${configPath}`);
@@ -1385,6 +1461,12 @@ function installMcp(force) {
1385
1461
  }
1386
1462
  return true;
1387
1463
  }
1464
+ function isClaudeHooksPluginInstalled() {
1465
+ const pluginDir = join(claudeConfigDir(), "plugins", "codemem");
1466
+ if (existsSync(pluginDir)) return true;
1467
+ if (existsSync(join(pluginDir, "hooks", "hooks.json"))) return true;
1468
+ return false;
1469
+ }
1388
1470
  function installClaudeMcp(force) {
1389
1471
  const settingsPath = join(claudeConfigDir(), "settings.json");
1390
1472
  let settings;
@@ -1395,22 +1477,36 @@ function installClaudeMcp(force) {
1395
1477
  }
1396
1478
  let mcpServers = settings.mcpServers;
1397
1479
  if (mcpServers == null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) mcpServers = {};
1398
- if ("codemem" in mcpServers && !force) {
1399
- p.log.info(`Claude MCP entry already exists in ${settingsPath}`);
1400
- return true;
1401
- }
1402
- mcpServers.codemem = {
1403
- command: "npx",
1404
- args: ["codemem", "mcp"]
1405
- };
1406
- settings.mcpServers = mcpServers;
1407
- try {
1408
- writeJsonConfig(settingsPath, settings);
1409
- p.log.success(`Claude MCP entry installed: ${settingsPath}`);
1410
- } catch (err) {
1411
- p.log.error(`Failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
1412
- return false;
1480
+ const migrated = migrateLegacyClaudeMcp(settings);
1481
+ if ("codemem" in mcpServers && !force && !migrated) p.log.info(`Claude MCP entry already exists in ${settingsPath}`);
1482
+ else {
1483
+ if (!migrated) {
1484
+ mcpServers.codemem = {
1485
+ command: "npx",
1486
+ args: [
1487
+ "-y",
1488
+ "codemem",
1489
+ "mcp"
1490
+ ]
1491
+ };
1492
+ settings.mcpServers = mcpServers;
1493
+ }
1494
+ try {
1495
+ writeJsonConfig(settingsPath, settings);
1496
+ p.log.success(`Claude MCP entry installed: ${settingsPath}`);
1497
+ } catch (err) {
1498
+ p.log.error(`Failed to write ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
1499
+ return false;
1500
+ }
1413
1501
  }
1502
+ if (!isClaudeHooksPluginInstalled() || force) {
1503
+ p.log.info("To install the Claude Code hooks plugin, run in Claude Code:");
1504
+ p.log.info(" /plugin marketplace add kunickiaj/codemem");
1505
+ p.log.info(" /plugin install codemem");
1506
+ p.log.info("");
1507
+ p.log.info("To update an existing install:");
1508
+ p.log.info(" /plugin marketplace update codemem-marketplace");
1509
+ } else p.log.info("Claude Code hooks plugin appears to be installed");
1414
1510
  return true;
1415
1511
  }
1416
1512
  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) => {
@@ -1572,22 +1668,11 @@ function parseStoredAddressEndpoint(value) {
1572
1668
  async function runServeLifecycle(action, opts) {
1573
1669
  if (opts.user === false || opts.system === true) p.log.warn("TS sync lifecycle currently manages the local viewer process, not separate user/system services.");
1574
1670
  if (action === "start") {
1575
- const config = readCodememConfigFile();
1576
- if (config.sync_enabled !== true) {
1671
+ if (readCodememConfigFile().sync_enabled !== true) {
1577
1672
  p.log.error("Sync is disabled. Run `codemem sync enable` first.");
1578
1673
  process.exitCode = 1;
1579
1674
  return;
1580
1675
  }
1581
- const configuredHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
1582
- const configuredPort = typeof config.sync_port === "number" ? String(config.sync_port) : "7337";
1583
- opts.host ??= configuredHost;
1584
- opts.port ??= configuredPort;
1585
- } else if (action === "restart") {
1586
- const config = readCodememConfigFile();
1587
- const configuredHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
1588
- const configuredPort = typeof config.sync_port === "number" ? String(config.sync_port) : "7337";
1589
- opts.host ??= configuredHost;
1590
- opts.port ??= configuredPort;
1591
1676
  }
1592
1677
  const args = buildServeLifecycleArgs(action, opts, process.argv[1] ?? "", process.execArgv);
1593
1678
  await new Promise((resolve, reject) => {