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.
package/.opencode/lib/compat.js
CHANGED
|
@@ -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.
|
|
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;
|
|
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"}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* Replaces Python's install_plugin_cmd + install_mcp_cmd.
|
|
5
5
|
*
|
|
6
6
|
* What it does:
|
|
7
|
-
* 1.
|
|
8
|
-
* 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.
|
|
9
|
-
* 3.
|
|
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;
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
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))
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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 (
|
|
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
|
-
},
|
|
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.
|
|
1317
|
-
* 2. Adds/updates the MCP entry in ~/.config/opencode/opencode.
|
|
1318
|
-
* 3.
|
|
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
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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) => {
|