memroot 0.1.0-alpha.1 → 0.1.0-alpha.3

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.
Files changed (36) hide show
  1. package/README.md +26 -2
  2. package/dist/index.js +1929 -470
  3. package/dist/plugins/claude/.claude-plugin/plugin.json +2 -2
  4. package/dist/plugins/claude/README.md +8 -4
  5. package/dist/plugins/claude/hooks/hooks.json +10 -0
  6. package/dist/plugins/claude/scripts/capture-worker.mjs +19294 -0
  7. package/dist/plugins/claude/scripts/session-end.mjs +469 -0
  8. package/dist/plugins/claude/scripts/session-start.mjs +372 -53
  9. package/dist/plugins/claude/scripts/status.mjs +152 -12
  10. package/dist/plugins/claude/skills/memroot-memory/SKILL.md +48 -0
  11. package/dist/plugins/claude/skills/memroot-memory/references/save.md +76 -0
  12. package/dist/plugins/claude/skills/memroot-status/SKILL.md +19 -0
  13. package/dist/plugins/codex/.codex-plugin/plugin.json +4 -4
  14. package/dist/plugins/codex/README.md +13 -9
  15. package/dist/plugins/codex/hooks/hooks.json +11 -0
  16. package/dist/plugins/codex/scripts/capture-worker.mjs +19294 -0
  17. package/dist/plugins/codex/scripts/session-end.mjs +469 -0
  18. package/dist/plugins/codex/scripts/session-start.mjs +372 -53
  19. package/dist/plugins/codex/scripts/status.mjs +149 -9
  20. package/dist/plugins/codex/skills/memroot-memory/SKILL.md +48 -0
  21. package/dist/plugins/codex/skills/memroot-memory/references/save.md +76 -0
  22. package/dist/plugins/codex/skills/memroot-status/SKILL.md +20 -0
  23. package/dist/plugins/grok/.claude-plugin/plugin.json +2 -2
  24. package/dist/plugins/grok/README.md +10 -4
  25. package/dist/plugins/grok/hooks/hooks.json +24 -1
  26. package/dist/plugins/grok/scripts/capture-worker.mjs +19294 -0
  27. package/dist/plugins/grok/scripts/session-end.mjs +469 -0
  28. package/dist/plugins/grok/scripts/session-reminder.mjs +275 -0
  29. package/dist/plugins/grok/scripts/status.mjs +152 -12
  30. package/dist/plugins/grok/skills/memroot-memory/SKILL.md +48 -0
  31. package/dist/plugins/grok/skills/memroot-memory/references/save.md +76 -0
  32. package/dist/plugins/grok/skills/memroot-status/SKILL.md +20 -0
  33. package/package.json +6 -3
  34. package/dist/plugins/claude/skills/status/SKILL.md +0 -18
  35. package/dist/plugins/codex/skills/status/SKILL.md +0 -22
  36. package/dist/plugins/grok/skills/status/SKILL.md +0 -18
package/dist/index.js CHANGED
@@ -1129,359 +1129,245 @@ var i = `${styleText("gray", S_BAR)} `;
1129
1129
 
1130
1130
  // src/auth.ts
1131
1131
  import { spawn } from "node:child_process";
1132
- import { createHash as createHash2, randomBytes } from "node:crypto";
1133
- import { createServer, request as httpRequest } from "node:http";
1134
- import { request as httpsRequest } from "node:https";
1132
+ import { createHash as createHash2, randomBytes as randomBytes3 } from "node:crypto";
1133
+ import { createServer, request as httpRequest2 } from "node:http";
1134
+ import { request as httpsRequest2 } from "node:https";
1135
1135
 
1136
- // src/codex-mcp.ts
1137
- import { spawnSync as spawnSync2 } from "node:child_process";
1136
+ // src/capture/status.ts
1137
+ import { readFileSync as readFileSync3 } from "node:fs";
1138
+ import { homedir as homedir3 } from "node:os";
1139
+ import { isAbsolute as isAbsolute4, join as join9 } from "node:path";
1138
1140
 
1139
- // src/native.ts
1141
+ // src/credentials.ts
1140
1142
  import { spawnSync } from "node:child_process";
1141
- import { accessSync, constants, realpathSync, statSync } from "node:fs";
1142
- import { delimiter, isAbsolute, join } from "node:path";
1143
- import { stripVTControlCharacters as stripVTControlCharacters2 } from "node:util";
1144
- var agents = ["claude", "codex", "grok"];
1145
- var labels = {
1146
- claude: "Claude Code",
1147
- codex: "Codex",
1148
- grok: "Grok"
1143
+ import {
1144
+ chmod as chmod2,
1145
+ lstat as lstat3,
1146
+ readFile as readFile3,
1147
+ rename as rename3,
1148
+ rm as rm3,
1149
+ writeFile as writeFile3
1150
+ } from "node:fs/promises";
1151
+ import { join as join4 } from "node:path";
1152
+
1153
+ // src/config.ts
1154
+ import { randomUUID as randomUUID2 } from "node:crypto";
1155
+ import {
1156
+ chmod,
1157
+ lstat as lstat2,
1158
+ readFile as readFile2,
1159
+ rename as rename2,
1160
+ rm as rm2,
1161
+ writeFile as writeFile2
1162
+ } from "node:fs/promises";
1163
+ import { join as join3 } from "node:path";
1164
+
1165
+ // src/api-base.ts
1166
+ function isLoopbackHostname(hostname) {
1167
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
1168
+ }
1169
+ function isAllowedApiBase(value) {
1170
+ try {
1171
+ const url = new URL(value);
1172
+ if (url.username || url.password || url.search || url.hash) return false;
1173
+ if (url.protocol === "https:") return true;
1174
+ return url.protocol === "http:" && isLoopbackHostname(url.hostname);
1175
+ } catch {
1176
+ return false;
1177
+ }
1178
+ }
1179
+
1180
+ // src/capture/paths.ts
1181
+ import { randomBytes } from "node:crypto";
1182
+ import {
1183
+ chmodSync,
1184
+ closeSync,
1185
+ constants,
1186
+ fstatSync,
1187
+ lstatSync,
1188
+ mkdirSync,
1189
+ openSync,
1190
+ readdirSync,
1191
+ readFileSync,
1192
+ renameSync,
1193
+ rmSync,
1194
+ writeFileSync
1195
+ } from "node:fs";
1196
+ import { homedir } from "node:os";
1197
+ import { dirname, isAbsolute, join, normalize } from "node:path";
1198
+ var UnsafePathError = class extends Error {
1199
+ constructor(code = "UNSAFE_PATH") {
1200
+ super(`${code}: refusing an unexpected file type or symbolic link.`);
1201
+ this.code = code;
1202
+ }
1203
+ code;
1149
1204
  };
1150
- function executable(agent) {
1151
- for (const directory of (process.env.PATH ?? "").split(delimiter)) {
1152
- if (!isAbsolute(directory)) continue;
1153
- for (const extension of [""]) {
1154
- const path = join(directory, agent + extension);
1155
- try {
1156
- accessSync(path, constants.X_OK);
1157
- if (statSync(path).isFile()) return path;
1158
- } catch {
1159
- }
1205
+ function absoluteEnv(name) {
1206
+ const value = process.env[name];
1207
+ if (!value) return void 0;
1208
+ if (!isAbsolute(value))
1209
+ throw new Error(`INVALID_STATE_DIRECTORY: ${name} must be absolute.`);
1210
+ return normalize(value);
1211
+ }
1212
+ function stateBoundary() {
1213
+ const explicit = absoluteEnv("MEMROOT_STATE_DIR");
1214
+ if (explicit) return { boundary: explicit, dir: explicit };
1215
+ const base = absoluteEnv("XDG_STATE_HOME") ?? join(homedir(), ".local", "state");
1216
+ return { boundary: base, dir: join(base, "memroot", "capture") };
1217
+ }
1218
+ function stateDir() {
1219
+ return stateBoundary().dir;
1220
+ }
1221
+ function memrootConfigDir() {
1222
+ const override = process.env.MEMROOT_TEST_CONFIG_DIR;
1223
+ if (process.env.NODE_ENV === "test" && override && isAbsolute(override))
1224
+ return normalize(override);
1225
+ return join(homedir(), ".memroot");
1226
+ }
1227
+ function consentPath() {
1228
+ return join(memrootConfigDir(), "capture.json");
1229
+ }
1230
+ function assertRealDirectory(path) {
1231
+ const info = lstatSync(path);
1232
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new UnsafePathError();
1233
+ }
1234
+ function ensurePrivateDir(path, boundary, checkRoot = false) {
1235
+ const target = normalize(path);
1236
+ const root = normalize(boundary);
1237
+ if (target !== root && !target.startsWith(`${root}/`))
1238
+ throw new UnsafePathError();
1239
+ mkdirSync(target, { recursive: true, mode: 448 });
1240
+ let current = target;
1241
+ for (; ; ) {
1242
+ if (current === root) {
1243
+ if (checkRoot) assertRealDirectory(current);
1244
+ break;
1160
1245
  }
1246
+ assertRealDirectory(current);
1247
+ current = dirname(current);
1161
1248
  }
1162
- return void 0;
1249
+ const info = lstatSync(target);
1250
+ if ((info.mode & 511) !== 448) chmodSync(target, 448);
1163
1251
  }
1164
- function native(path, agent, args) {
1165
- const result = spawnSync(path, args, {
1166
- encoding: "utf8",
1167
- timeout: 6e4,
1168
- maxBuffer: 4 * 1024 * 1024,
1169
- shell: false
1170
- });
1171
- if (result.error || result.status !== 0) {
1172
- throw new Error(
1173
- `NATIVE_COMMAND_FAILED: ${agent} ${args.slice(0, 3).join(" ")} failed (${result.status ?? result.error?.code ?? "signal"}). Check '${agent} plugin --help', update the agent CLI, and retry. Its plugin manager may have partially registered Memroot; no success receipt was written.`
1174
- );
1252
+ function stateSubdir(...parts) {
1253
+ const { boundary, dir } = stateBoundary();
1254
+ const path = join(dir, ...parts);
1255
+ ensurePrivateDir(path, boundary, boundary === dir);
1256
+ return path;
1257
+ }
1258
+ function readPrivateFile(path, maxBytes = 16 * 1024 * 1024) {
1259
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1260
+ try {
1261
+ const info = fstatSync(fd);
1262
+ if (!info.isFile()) throw new UnsafePathError();
1263
+ if (info.size > maxBytes) throw new UnsafePathError("FILE_TOO_LARGE");
1264
+ return readFileSync(fd, "utf8");
1265
+ } finally {
1266
+ closeSync(fd);
1175
1267
  }
1176
- return result.stdout;
1177
1268
  }
1178
- function installCommands(agent, marketplace) {
1179
- if (agent === "claude")
1180
- return [
1181
- ["plugin", "marketplace", "add", marketplace, "--scope", "user"],
1182
- ["plugin", "install", "memroot@memroot", "--scope", "user"]
1183
- ];
1184
- if (agent === "codex")
1185
- return [
1186
- ["plugin", "marketplace", "add", marketplace, "--json"],
1187
- ["plugin", "add", "memroot@memroot", "--json"]
1188
- ];
1189
- return [
1190
- ["plugin", "install", join(marketplace, "plugins", "memroot"), "--trust"]
1191
- ];
1269
+ function readPrivateFileIfExists(path, maxBytes) {
1270
+ try {
1271
+ return readPrivateFile(path, maxBytes);
1272
+ } catch (error) {
1273
+ if (error.code === "ENOENT") return void 0;
1274
+ if (error.code === "ELOOP")
1275
+ throw new UnsafePathError();
1276
+ throw error;
1277
+ }
1192
1278
  }
1193
- function preflight(path, agent) {
1194
- native(path, agent, ["plugin", "--help"]);
1195
- if (agent !== "grok")
1196
- native(path, agent, ["plugin", "marketplace", "add", "--help"]);
1197
- native(path, agent, [
1198
- "plugin",
1199
- agent === "codex" ? "add" : "install",
1200
- "--help"
1201
- ]);
1279
+ function writePrivateFileAtomic(path, content) {
1280
+ const temporary = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
1281
+ try {
1282
+ writeFileSync(temporary, content, { flag: "wx", mode: 384 });
1283
+ renameSync(temporary, path);
1284
+ } finally {
1285
+ rmSync(temporary, { force: true });
1286
+ }
1202
1287
  }
1203
- function rows(output, field) {
1204
- let value;
1288
+ function removeFile(path) {
1289
+ rmSync(path, { force: true });
1290
+ }
1291
+ function listFiles(path) {
1205
1292
  try {
1206
- value = JSON.parse(output);
1207
- } catch {
1208
- throw new Error(
1209
- "NATIVE_STATUS_UNSUPPORTED: Agent plugin status is not valid JSON. Update the agent CLI and retry."
1210
- );
1293
+ assertRealDirectory(path);
1294
+ return readdirSync(path).sort();
1295
+ } catch (error) {
1296
+ if (error.code === "ENOENT") return [];
1297
+ throw error;
1211
1298
  }
1212
- if (field)
1213
- value = value && typeof value === "object" ? value[field] : void 0;
1214
- if (!Array.isArray(value) || value.some((row) => !row || typeof row !== "object" || Array.isArray(row)))
1215
- throw new Error(
1216
- "NATIVE_STATUS_UNSUPPORTED: Unrecognized agent plugin status; refusing to change it."
1217
- );
1218
- return value;
1219
1299
  }
1220
- function samePath(left, right) {
1221
- if (typeof left !== "string") return false;
1300
+ function pathExists(path) {
1222
1301
  try {
1223
- return realpathSync(left) === realpathSync(right);
1302
+ lstatSync(path);
1303
+ return true;
1224
1304
  } catch {
1225
- return left === right;
1305
+ return false;
1226
1306
  }
1227
1307
  }
1228
- function inspectNative(path, agent, marketplace, previousMarketplace) {
1229
- if (agent === "grok") {
1230
- const output = stripVTControlCharacters2(
1231
- native(path, agent, ["plugin", "list"])
1232
- );
1233
- const lines = output.split("\n").filter((line) => line.trim());
1234
- const empty = output.trim() === "No plugins installed. Run `grok plugin install --help` to get started.";
1235
- if (!empty && (!lines.length || lines.some(
1236
- (line) => !/^\s*\S+:\s+[^[]+\[.+\](?:\s+\(disabled\))?\s*$/.test(line)
1237
- ))) {
1238
- throw new Error(
1239
- "NATIVE_STATUS_UNSUPPORTED: Unrecognized Grok plugin list format. Update Grok and inspect its native plugin manager before retrying."
1240
- );
1241
- }
1242
- const matching = output.split("\n").filter((line) => /:\s*memroot(?:\s|$)/.test(line));
1243
- for (const line of matching) {
1244
- const source = line.match(/\[local: (.*)\]/)?.[1];
1245
- if (!samePath(source, join(marketplace, "plugins", "memroot")) && !(previousMarketplace && samePath(source, join(previousMarketplace, "plugins", "memroot"))))
1246
- throw new Error(
1247
- "PLUGIN_COLLISION: Grok already has a different Memroot plugin. Resolve it with the native plugin manager first."
1248
- );
1249
- if (/disabled/i.test(line))
1250
- throw new Error(
1251
- "PLUGIN_DISABLED: Memroot is disabled in Grok. Enable it in the native plugin manager when ready."
1252
- );
1253
- }
1254
- return {
1255
- registered: false,
1256
- currentMarketplace: false,
1257
- installed: matching.length > 0
1258
- };
1259
- }
1260
- const markets = rows(
1261
- native(path, agent, ["plugin", "marketplace", "list", "--json"]),
1262
- agent === "codex" ? "marketplaces" : void 0
1263
- );
1264
- const plugins = rows(
1265
- native(path, agent, ["plugin", "list", "--json"]),
1266
- agent === "codex" ? "installed" : void 0
1267
- );
1268
- const matchingMarkets = markets.filter((row) => row.name === "memroot");
1269
- if (matchingMarkets.length > 1)
1308
+
1309
+ // src/storage.ts
1310
+ import { createHash, randomUUID } from "node:crypto";
1311
+ import {
1312
+ cp,
1313
+ lstat,
1314
+ mkdir,
1315
+ readdir,
1316
+ readFile,
1317
+ rename,
1318
+ rm,
1319
+ writeFile
1320
+ } from "node:fs/promises";
1321
+ import { homedir as homedir2 } from "node:os";
1322
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2 } from "node:path";
1323
+ import { fileURLToPath } from "node:url";
1324
+ var version = "0.1.0-alpha.3";
1325
+ function dataDirectory() {
1326
+ const base = process.platform === "win32" ? process.env.LOCALAPPDATA ?? join2(homedir2(), "AppData", "Local") : process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share");
1327
+ if (!isAbsolute2(base))
1270
1328
  throw new Error(
1271
- "MARKETPLACE_COLLISION: Multiple Memroot marketplaces were reported; refusing to change them."
1329
+ "INVALID_DATA_DIRECTORY: XDG_DATA_HOME or LOCALAPPDATA must be absolute."
1272
1330
  );
1273
- for (const market of matchingMarkets) {
1274
- const source = agent === "codex" ? market.root : market.path;
1275
- if (!samePath(source, marketplace) && !(previousMarketplace && samePath(source, previousMarketplace)))
1276
- throw new Error(
1277
- "MARKETPLACE_COLLISION: memroot is already registered from a different source. Resolve it with the native plugin manager first."
1278
- );
1331
+ return join2(base, "memroot");
1332
+ }
1333
+ async function exists(path) {
1334
+ try {
1335
+ await lstat(path);
1336
+ return true;
1337
+ } catch (error) {
1338
+ if (error.code === "ENOENT") return false;
1339
+ throw error;
1279
1340
  }
1280
- const matchingPlugins = plugins.filter(
1281
- (row) => row.id === "memroot@memroot" || row.pluginId === "memroot@memroot" || row.name === "memroot"
1282
- );
1283
- if (matchingPlugins.length && !matchingMarkets.length)
1341
+ }
1342
+ async function safeDirectory(path) {
1343
+ const boundary = dataDirectory();
1344
+ if (path !== boundary && path.startsWith(`${boundary}/`))
1345
+ await safeDirectory(dirname2(path));
1346
+ await mkdir(path, { recursive: true, mode: 448 });
1347
+ const info = await lstat(path);
1348
+ if (!info.isDirectory() || info.isSymbolicLink())
1349
+ throw new Error(`UNSAFE_PATH: Expected a real directory at ${path}.`);
1350
+ }
1351
+ async function fileEntries(root, prefix = "") {
1352
+ const directory = await lstat(join2(root, prefix));
1353
+ if (!directory.isDirectory() || directory.isSymbolicLink())
1284
1354
  throw new Error(
1285
- "PLUGIN_COLLISION: An existing Memroot plugin has no matching managed marketplace. Resolve it with the native plugin manager first."
1355
+ "INTEGRITY_ERROR: Managed plugin directory is not a real directory."
1286
1356
  );
1287
- for (const plugin of matchingPlugins) {
1288
- if (plugin.enabled === false)
1289
- throw new Error(
1290
- `PLUGIN_DISABLED: Memroot is disabled in ${labels[agent]}. Enable it in the native plugin manager when ready; setup preserves your choice.`
1291
- );
1292
- if (plugin.enabled !== true || agent === "claude" && plugin.scope !== "user")
1357
+ const result = [];
1358
+ for (const name of (await readdir(join2(root, prefix))).sort()) {
1359
+ const path = join2(root, prefix, name);
1360
+ const info = await lstat(path);
1361
+ const key = join2(prefix, name);
1362
+ if (info.isSymbolicLink())
1293
1363
  throw new Error(
1294
- "PLUGIN_COLLISION: Existing Memroot plugin status or scope cannot be safely managed. Check the native plugin manager."
1364
+ "INTEGRITY_ERROR: Plugin assets must not contain symbolic links."
1295
1365
  );
1366
+ if (info.isDirectory()) result.push(...await fileEntries(root, key));
1367
+ else if (info.isFile()) result.push([key, await readFile(path)]);
1368
+ else throw new Error("INTEGRITY_ERROR: Unsupported plugin asset.");
1296
1369
  }
1297
- const currentMarketplace = matchingMarkets.some(
1298
- (market) => samePath(agent === "codex" ? market.root : market.path, marketplace)
1299
- );
1300
- if (agent === "codex" && matchingMarkets.length > 0 && !currentMarketplace && plugins.some(
1301
- (plugin) => plugin.marketplaceName === "memroot" && plugin.pluginId !== "memroot@memroot"
1302
- ))
1303
- throw new Error(
1304
- "MARKETPLACE_COLLISION: The previous Memroot marketplace contains another installed plugin; refusing to remove it."
1305
- );
1306
- return {
1307
- registered: matchingMarkets.length > 0,
1308
- currentMarketplace,
1309
- installed: matchingPlugins.length > 0
1310
- };
1311
- }
1312
-
1313
- // src/codex-mcp.ts
1314
- var codexMcpUrl = "https://api.memroot.dev/mcp";
1315
- function command(path, args, interactive = false) {
1316
- const result = spawnSync2(path, ["mcp", ...args], {
1317
- shell: false,
1318
- encoding: "utf8",
1319
- timeout: interactive ? 3e5 : 3e4,
1320
- maxBuffer: 1024 * 1024,
1321
- // Let Codex present browser authentication directly; never capture credentials.
1322
- stdio: interactive ? "inherit" : "pipe"
1323
- });
1324
- if (result.error || result.status !== 0)
1325
- throw new Error(
1326
- "MCP_COMMAND_FAILED: Codex MCP command did not complete. Your configuration is preserved; retry memroot setup --agent codex --yes."
1327
- );
1328
- return result.stdout ?? "";
1329
- }
1330
- function inspectCodexMcp(path) {
1331
- let rows2;
1332
- try {
1333
- rows2 = JSON.parse(command(path, ["list", "--json"]));
1334
- } catch {
1335
- throw new Error(
1336
- "MCP_STATUS_UNAVAILABLE: Cannot inspect Codex MCP state. Retry later; no new authorization was requested."
1337
- );
1338
- }
1339
- if (!Array.isArray(rows2) || rows2.some((row2) => !row2 || typeof row2 !== "object"))
1340
- throw new Error(
1341
- "MCP_STATUS_UNAVAILABLE: Unsupported Codex MCP status format."
1342
- );
1343
- const matching = rows2.filter((row2) => row2.name === "memroot");
1344
- if (!matching.length)
1345
- return { configured: false, authorized: false, verified: false };
1346
- const row = matching[0];
1347
- if (matching.length !== 1 || row.enabled !== true || row.transport?.type !== "streamable_http" || row.transport?.url !== codexMcpUrl || row.transport?.bearer_token_env_var || row.transport?.http_headers || row.transport?.env_http_headers || row.transport?.http_headers_helper)
1348
- throw new Error(
1349
- "MCP_CONFLICT: Existing memroot MCP configuration differs from production OAuth or is disabled. Inspect it in Codex; setup preserves your settings."
1350
- );
1351
- if (row.auth_status !== "oauth" && row.auth_status !== "not_logged_in")
1352
- throw new Error(
1353
- "MCP_AUTH_UNKNOWN: Codex could not establish OAuth status. Resolve it in Codex before retrying; no replacement grant was requested."
1354
- );
1355
- return {
1356
- configured: true,
1357
- authorized: row.auth_status === "oauth",
1358
- verified: false
1359
- };
1360
- }
1361
- function codexMcpPreflight(path) {
1362
- for (const [args, flags] of [
1363
- [["list", "--help"], ["--json"]],
1364
- [
1365
- ["add", "--help"],
1366
- ["--url", "--oauth-resource"]
1367
- ],
1368
- [["login", "--help"], []]
1369
- ]) {
1370
- const help2 = command(path, [...args]);
1371
- if (flags.some((flag) => !help2.includes(flag)))
1372
- throw new Error(
1373
- "MCP_UNSUPPORTED: Update Codex to a version supporting native remote OAuth MCP setup."
1374
- );
1375
- }
1376
- }
1377
- async function ensureCodexMcp(options) {
1378
- if (options.dryRun)
1379
- return { configured: false, authorized: false, verified: false };
1380
- const path = executable("codex");
1381
- if (!path)
1382
- throw new Error("AGENT_NOT_FOUND: Install Codex before linking Memroot.");
1383
- codexMcpPreflight(path);
1384
- let state = inspectCodexMcp(path);
1385
- if (!state.configured) {
1386
- command(
1387
- path,
1388
- ["add", "memroot", "--url", codexMcpUrl, "--oauth-resource", codexMcpUrl],
1389
- true
1390
- );
1391
- state = inspectCodexMcp(path);
1392
- if (!state.authorized)
1393
- throw new Error(
1394
- "MCP_AUTH_PENDING: Codex MCP is configured; authorization has not completed. Retry setup to resume."
1395
- );
1396
- } else if (!state.authorized) {
1397
- command(path, ["login", "memroot"], true);
1398
- state = inspectCodexMcp(path);
1399
- }
1400
- if (!state.authorized)
1401
- throw new Error(
1402
- "MCP_AUTH_PENDING: Codex has not confirmed OAuth authorization."
1403
- );
1404
- return state;
1405
- }
1406
- function describeCodexMcp(state) {
1407
- return `Codex MCP: ${state.configured ? "configured" : "not configured"}; OAuth: ${state.authorized ? "authorized" : "not authorized"}; memory verification: not verified. Ask Codex to call connection_status, then save and retrieve a test memory.`;
1408
- }
1409
-
1410
- // src/config.ts
1411
- import { randomUUID as randomUUID2 } from "node:crypto";
1412
- import {
1413
- chmod,
1414
- lstat as lstat2,
1415
- readFile as readFile2,
1416
- rename as rename2,
1417
- rm as rm2,
1418
- writeFile as writeFile2
1419
- } from "node:fs/promises";
1420
- import { homedir as homedir2 } from "node:os";
1421
- import { join as join3 } from "node:path";
1422
-
1423
- // src/storage.ts
1424
- import { createHash, randomUUID } from "node:crypto";
1425
- import {
1426
- cp,
1427
- lstat,
1428
- mkdir,
1429
- readdir,
1430
- readFile,
1431
- rename,
1432
- rm,
1433
- writeFile
1434
- } from "node:fs/promises";
1435
- import { homedir } from "node:os";
1436
- import { dirname, isAbsolute as isAbsolute2, join as join2 } from "node:path";
1437
- import { fileURLToPath } from "node:url";
1438
- var version = "0.1.0-alpha.1";
1439
- function dataDirectory() {
1440
- const base = process.platform === "win32" ? process.env.LOCALAPPDATA ?? join2(homedir(), "AppData", "Local") : process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
1441
- if (!isAbsolute2(base))
1442
- throw new Error(
1443
- "INVALID_DATA_DIRECTORY: XDG_DATA_HOME or LOCALAPPDATA must be absolute."
1444
- );
1445
- return join2(base, "memroot");
1446
- }
1447
- async function exists(path) {
1448
- try {
1449
- await lstat(path);
1450
- return true;
1451
- } catch (error) {
1452
- if (error.code === "ENOENT") return false;
1453
- throw error;
1454
- }
1455
- }
1456
- async function safeDirectory(path) {
1457
- const boundary = dataDirectory();
1458
- if (path !== boundary && path.startsWith(`${boundary}/`))
1459
- await safeDirectory(dirname(path));
1460
- await mkdir(path, { recursive: true, mode: 448 });
1461
- const info = await lstat(path);
1462
- if (!info.isDirectory() || info.isSymbolicLink())
1463
- throw new Error(`UNSAFE_PATH: Expected a real directory at ${path}.`);
1464
- }
1465
- async function fileEntries(root, prefix = "") {
1466
- const directory = await lstat(join2(root, prefix));
1467
- if (!directory.isDirectory() || directory.isSymbolicLink())
1468
- throw new Error(
1469
- "INTEGRITY_ERROR: Managed plugin directory is not a real directory."
1470
- );
1471
- const result = [];
1472
- for (const name of (await readdir(join2(root, prefix))).sort()) {
1473
- const path = join2(root, prefix, name);
1474
- const info = await lstat(path);
1475
- const key = join2(prefix, name);
1476
- if (info.isSymbolicLink())
1477
- throw new Error(
1478
- "INTEGRITY_ERROR: Plugin assets must not contain symbolic links."
1479
- );
1480
- if (info.isDirectory()) result.push(...await fileEntries(root, key));
1481
- else if (info.isFile()) result.push([key, await readFile(path)]);
1482
- else throw new Error("INTEGRITY_ERROR: Unsupported plugin asset.");
1483
- }
1484
- return result;
1370
+ return result;
1485
1371
  }
1486
1372
  async function integrity(root) {
1487
1373
  const hash = createHash("sha256");
@@ -1573,7 +1459,7 @@ async function stage(data, agent) {
1573
1459
  });
1574
1460
  if (agent !== "grok") {
1575
1461
  const metadata = marketplaceMetadata(agent);
1576
- await mkdir(dirname(join2(temporary, metadata.path)), { recursive: true });
1462
+ await mkdir(dirname2(join2(temporary, metadata.path)), { recursive: true });
1577
1463
  await writeFile(
1578
1464
  join2(temporary, metadata.path),
1579
1465
  `${JSON.stringify(metadata.value, null, 2)}
@@ -1622,24 +1508,11 @@ async function acquireLock(data) {
1622
1508
 
1623
1509
  // src/config.ts
1624
1510
  function configDirectory() {
1625
- return join3(homedir2(), ".memroot");
1511
+ return memrootConfigDir();
1626
1512
  }
1627
1513
  function configPath() {
1628
1514
  return join3(configDirectory(), "config.json");
1629
1515
  }
1630
- function isLoopbackHostname(hostname) {
1631
- return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
1632
- }
1633
- function isAllowedApiBase(value) {
1634
- try {
1635
- const url = new URL(value);
1636
- if (url.username || url.password || url.search || url.hash) return false;
1637
- if (url.protocol === "https:") return true;
1638
- return url.protocol === "http:" && isLoopbackHostname(url.hostname);
1639
- } catch {
1640
- return false;
1641
- }
1642
- }
1643
1516
  function optionalString(value) {
1644
1517
  return typeof value === "string" && value.length > 0 ? value : void 0;
1645
1518
  }
@@ -1707,6 +1580,17 @@ async function readConfig() {
1707
1580
  if (clientIdApiBase) config.clientIdApiBase = clientIdApiBase;
1708
1581
  const userId = optionalString(record.userId);
1709
1582
  if (userId) config.userId = userId;
1583
+ const mcp = record.mcpRegistered;
1584
+ if (mcp && typeof mcp === "object" && !Array.isArray(mcp)) {
1585
+ const registered = {};
1586
+ if (mcp.claude === true)
1587
+ registered.claude = true;
1588
+ if (mcp.codex === true)
1589
+ registered.codex = true;
1590
+ if (mcp.grok === true) registered.grok = true;
1591
+ if (registered.claude || registered.codex || registered.grok)
1592
+ config.mcpRegistered = registered;
1593
+ }
1710
1594
  return config;
1711
1595
  }
1712
1596
  async function writeConfig(config) {
@@ -1728,16 +1612,6 @@ async function writeConfig(config) {
1728
1612
  }
1729
1613
 
1730
1614
  // src/credentials.ts
1731
- import { spawnSync as spawnSync3 } from "node:child_process";
1732
- import {
1733
- chmod as chmod2,
1734
- lstat as lstat3,
1735
- readFile as readFile3,
1736
- rename as rename3,
1737
- rm as rm3,
1738
- writeFile as writeFile3
1739
- } from "node:fs/promises";
1740
- import { join as join4 } from "node:path";
1741
1615
  var SERVICE = "memroot";
1742
1616
  var TOOL_TIMEOUT_MS = 15e3;
1743
1617
  function credentialStoreKind() {
@@ -1756,7 +1630,7 @@ function unavailable(tool) {
1756
1630
  );
1757
1631
  }
1758
1632
  function run(tool, args, input, allowNotFound = false) {
1759
- const result = spawnSync3(tool, args, {
1633
+ const result = spawnSync(tool, args, {
1760
1634
  encoding: "utf8",
1761
1635
  input,
1762
1636
  timeout: TOOL_TIMEOUT_MS,
@@ -1864,55 +1738,1170 @@ async function readSecret(account) {
1864
1738
  void 0,
1865
1739
  true
1866
1740
  );
1867
- if (result2.status !== 0) {
1868
- if (/could not be found|item not found/i.test(result2.stderr))
1869
- return void 0;
1741
+ if (result2.status !== 0) {
1742
+ if (/could not be found|item not found/i.test(result2.stderr))
1743
+ return void 0;
1744
+ throw new Error(
1745
+ `AUTH_KEYCHAIN_FAILED: security find-generic-password failed (${result2.status ?? "signal"}).`
1746
+ );
1747
+ }
1748
+ const secret2 = result2.stdout.replace(/\n$/, "");
1749
+ return secret2.length ? secret2 : void 0;
1750
+ }
1751
+ const result = run(
1752
+ "secret-tool",
1753
+ ["lookup", "service", SERVICE, "account", account],
1754
+ void 0,
1755
+ true
1756
+ );
1757
+ if (result.status !== 0) return void 0;
1758
+ const secret = result.stdout.replace(/\n$/, "");
1759
+ return secret.length ? secret : void 0;
1760
+ }
1761
+ async function deleteSecret(account) {
1762
+ const kind = credentialStoreKind();
1763
+ if (kind === "test-file") {
1764
+ const entries = await readTestStore();
1765
+ if (account in entries) {
1766
+ delete entries[account];
1767
+ await writeTestStore(entries);
1768
+ }
1769
+ return;
1770
+ }
1771
+ if (kind === "macos-keychain") {
1772
+ const result = run(
1773
+ "security",
1774
+ ["delete-generic-password", "-s", SERVICE, "-a", account],
1775
+ void 0,
1776
+ true
1777
+ );
1778
+ if (result.status !== 0 && !/could not be found|item not found/i.test(result.stderr))
1779
+ throw new Error(
1780
+ `AUTH_KEYCHAIN_FAILED: security delete-generic-password failed (${result.status ?? "signal"}).`
1781
+ );
1782
+ return;
1783
+ }
1784
+ run(
1785
+ "secret-tool",
1786
+ ["clear", "service", SERVICE, "account", account],
1787
+ void 0,
1788
+ true
1789
+ );
1790
+ }
1791
+
1792
+ // src/capture/consent.ts
1793
+ import { dirname as dirname3 } from "node:path";
1794
+ var CAPTURE_AGENTS = ["claude", "codex", "grok"];
1795
+ var CONSENT_TEXT_VERSION = "capture-consent-v2";
1796
+ var DEFAULT_MAX_ITEMS_PER_SESSION = 3;
1797
+ var DEFAULT_MAX_ITEMS_PER_DAY = 10;
1798
+ var KEYS = [
1799
+ "schemaVersion",
1800
+ "enabled",
1801
+ "enabledAt",
1802
+ "consentTextVersion",
1803
+ "apiBase",
1804
+ "agents",
1805
+ "crossAgentExtraction",
1806
+ "includeHeadlessSessions",
1807
+ "maxItemsPerSession",
1808
+ "maxItemsPerDay",
1809
+ "models"
1810
+ ];
1811
+ var MODEL = /^[A-Za-z0-9._:/@+-]{1,128}$/;
1812
+ function isInt(value, min, max) {
1813
+ return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
1814
+ }
1815
+ function parseConsent(value) {
1816
+ if (!value || typeof value !== "object" || Array.isArray(value))
1817
+ return void 0;
1818
+ const record = value;
1819
+ const keys = Object.keys(record);
1820
+ if (keys.some((key) => !KEYS.includes(key)) || keys.length !== KEYS.length)
1821
+ return void 0;
1822
+ const agents2 = record.agents;
1823
+ const models = record.models;
1824
+ if (record.schemaVersion !== 1 || typeof record.enabled !== "boolean" || typeof record.enabledAt !== "string" || Number.isNaN(Date.parse(record.enabledAt)) || record.consentTextVersion !== CONSENT_TEXT_VERSION || typeof record.apiBase !== "string" || !isAllowedApiBase(record.apiBase) || record.apiBase.endsWith("/") || !Array.isArray(agents2) || agents2.length === 0 || agents2.length > CAPTURE_AGENTS.length || new Set(agents2).size !== agents2.length || agents2.some((agent) => !CAPTURE_AGENTS.includes(agent)) || typeof record.crossAgentExtraction !== "boolean" || typeof record.includeHeadlessSessions !== "boolean" || !isInt(record.maxItemsPerSession, 1, 5) || !isInt(record.maxItemsPerDay, 1, 20) || !models || typeof models !== "object" || Array.isArray(models))
1825
+ return void 0;
1826
+ const modelRecord = models;
1827
+ const modelKeys = Object.keys(modelRecord);
1828
+ if (modelKeys.length !== CAPTURE_AGENTS.length || modelKeys.some((key) => !CAPTURE_AGENTS.includes(key)) || Object.values(modelRecord).some(
1829
+ (model) => model !== null && (typeof model !== "string" || !MODEL.test(model))
1830
+ ))
1831
+ return void 0;
1832
+ return record;
1833
+ }
1834
+ function readConsent() {
1835
+ let raw;
1836
+ try {
1837
+ raw = readPrivateFileIfExists(consentPath(), 64 * 1024);
1838
+ } catch {
1839
+ return { kind: "invalid" };
1840
+ }
1841
+ if (raw === void 0) return { kind: "absent" };
1842
+ try {
1843
+ const consent = parseConsent(JSON.parse(raw));
1844
+ return consent ? { kind: "ok", consent } : { kind: "invalid" };
1845
+ } catch {
1846
+ return { kind: "invalid" };
1847
+ }
1848
+ }
1849
+ function writeConsent(consent) {
1850
+ if (!parseConsent(consent))
1851
+ throw new Error("INVALID_CONSENT: Refusing to write invalid consent.");
1852
+ ensurePrivateDir(memrootConfigDir(), dirname3(memrootConfigDir()), false);
1853
+ writePrivateFileAtomic(
1854
+ consentPath(),
1855
+ `${JSON.stringify(consent, null, 2)}
1856
+ `
1857
+ );
1858
+ }
1859
+
1860
+ // src/capture/extract/run.ts
1861
+ import { accessSync, constants as constants2, statSync } from "node:fs";
1862
+ import { delimiter, isAbsolute as isAbsolute3, join as join5 } from "node:path";
1863
+ var MAX_OUTPUT_BYTES = 256 * 1024;
1864
+ var MAX_STDERR_BYTES = 8 * 1024;
1865
+ function findExecutable(name) {
1866
+ for (const directory of (process.env.PATH ?? "").split(delimiter)) {
1867
+ if (!isAbsolute3(directory)) continue;
1868
+ const path = join5(directory, name);
1869
+ try {
1870
+ accessSync(path, constants2.X_OK);
1871
+ if (statSync(path).isFile()) return path;
1872
+ } catch {
1873
+ }
1874
+ }
1875
+ return void 0;
1876
+ }
1877
+
1878
+ // src/capture/queue/job.ts
1879
+ import { join as join6 } from "node:path";
1880
+ var JOB_STATES = [
1881
+ "pending",
1882
+ "extracted",
1883
+ "quota_blocked",
1884
+ "auth_required",
1885
+ "server_unsupported",
1886
+ "runner_unavailable"
1887
+ ];
1888
+ var JOB_ID = /^(claude|codex|grok)-[a-f0-9]{32}$/;
1889
+ var HEX64 = /^[a-f0-9]{64}$/;
1890
+ function queueDir() {
1891
+ return stateSubdir("queue");
1892
+ }
1893
+ function jobPath(jobId) {
1894
+ return join6(queueDir(), `${jobId}.json`);
1895
+ }
1896
+ function parseJob(value) {
1897
+ if (!value || typeof value !== "object" || Array.isArray(value))
1898
+ return void 0;
1899
+ const job = value;
1900
+ const optionalTime = (field) => field === null || typeof field === "string" && !Number.isNaN(Date.parse(field));
1901
+ if (job.schemaVersion !== 1 || typeof job.jobId !== "string" || !JOB_ID.test(job.jobId) || !CAPTURE_AGENTS.includes(job.agent) || !job.jobId.startsWith(`${job.agent}-`) || typeof job.sessionId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(job.sessionId) || typeof job.sessionRef !== "string" || !HEX64.test(job.sessionRef) || job.jobId !== `${job.agent}-${job.sessionRef.slice(0, 32)}` || !(job.transcriptPath === null || typeof job.transcriptPath === "string") || typeof job.cwd !== "string" || !job.cwd.startsWith("/") || typeof job.reason !== "string" || !optionalTime(job.endedAt) || job.endedAt === null || !optionalTime(job.createdAt) || job.createdAt === null || !JOB_STATES.includes(job.state) || typeof job.attempts !== "number" || !Number.isInteger(job.attempts) || job.attempts < 0 || !optionalTime(job.nextAttemptAt) || !(job.lastError === null || typeof job.lastError === "string"))
1902
+ return void 0;
1903
+ return job;
1904
+ }
1905
+ function readJob(jobId) {
1906
+ const raw = readPrivateFileIfExists(jobPath(jobId), 64 * 1024);
1907
+ if (raw === void 0) return void 0;
1908
+ try {
1909
+ return parseJob(JSON.parse(raw));
1910
+ } catch {
1911
+ return void 0;
1912
+ }
1913
+ }
1914
+ function deleteJob(jobId) {
1915
+ removeFile(jobPath(jobId));
1916
+ }
1917
+ function listJobIds() {
1918
+ return listFiles(queueDir()).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -5)).filter((id) => JOB_ID.test(id));
1919
+ }
1920
+
1921
+ // src/capture/queue/ledger.ts
1922
+ import { join as join7 } from "node:path";
1923
+ var MAX_TITLE_HASHES = 200;
1924
+ var UPLOAD_RETENTION_MS = 7 * 24 * 60 * 60 * 1e3;
1925
+ var HEX642 = /^[a-f0-9]{64}$/;
1926
+ function empty() {
1927
+ return {
1928
+ schemaVersion: 1,
1929
+ cursors: {},
1930
+ uploads: [],
1931
+ titleHashes: [],
1932
+ project: null,
1933
+ lastRun: null
1934
+ };
1935
+ }
1936
+ function ledgerPath() {
1937
+ return join7(stateSubdir(), "ledger.json");
1938
+ }
1939
+ function sanitize(value) {
1940
+ const ledger = empty();
1941
+ if (!value || typeof value !== "object" || Array.isArray(value))
1942
+ return ledger;
1943
+ const record = value;
1944
+ if (record.schemaVersion !== 1) return ledger;
1945
+ if (record.cursors && typeof record.cursors === "object")
1946
+ for (const [key, entry] of Object.entries(
1947
+ record.cursors
1948
+ )) {
1949
+ const cursor3 = entry;
1950
+ if (HEX642.test(key) && typeof cursor3?.turns === "number" && Number.isInteger(cursor3.turns) && cursor3.turns >= 0 && typeof cursor3.updatedAt === "string")
1951
+ ledger.cursors[key] = {
1952
+ turns: cursor3.turns,
1953
+ updatedAt: cursor3.updatedAt
1954
+ };
1955
+ }
1956
+ if (Array.isArray(record.uploads))
1957
+ for (const entry of record.uploads) {
1958
+ const upload = entry;
1959
+ if (typeof upload?.at === "string" && !Number.isNaN(Date.parse(upload.at)) && typeof upload.titleHash === "string" && HEX642.test(upload.titleHash))
1960
+ ledger.uploads.push({
1961
+ at: upload.at,
1962
+ titleHash: upload.titleHash,
1963
+ memoryId: typeof upload.memoryId === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(upload.memoryId) ? upload.memoryId : null
1964
+ });
1965
+ }
1966
+ if (Array.isArray(record.titleHashes))
1967
+ ledger.titleHashes = record.titleHashes.filter(
1968
+ (hash) => typeof hash === "string" && HEX642.test(hash)
1969
+ ).slice(-MAX_TITLE_HASHES);
1970
+ const project = record.project;
1971
+ if (project && typeof project.apiBase === "string" && typeof project.projectId === "string" && /^[A-Za-z0-9-]{1,128}$/.test(project.projectId))
1972
+ ledger.project = { apiBase: project.apiBase, projectId: project.projectId };
1973
+ const lastRun = record.lastRun;
1974
+ if (lastRun && typeof lastRun.startedAt === "string" && typeof lastRun.finishedAt === "string" && lastRun.outcomes && typeof lastRun.outcomes === "object") {
1975
+ const outcomes = {};
1976
+ for (const [key, count] of Object.entries(lastRun.outcomes))
1977
+ if (/^[a-z_]{1,40}$/.test(key) && typeof count === "number")
1978
+ outcomes[key] = count;
1979
+ ledger.lastRun = {
1980
+ startedAt: lastRun.startedAt,
1981
+ finishedAt: lastRun.finishedAt,
1982
+ outcomes
1983
+ };
1984
+ }
1985
+ return ledger;
1986
+ }
1987
+ function readLedger() {
1988
+ try {
1989
+ const raw = readPrivateFileIfExists(ledgerPath(), 4 * 1024 * 1024);
1990
+ return raw === void 0 ? empty() : sanitize(JSON.parse(raw));
1991
+ } catch {
1992
+ return empty();
1993
+ }
1994
+ }
1995
+ function uploadsInLastDay(ledger, now = Date.now()) {
1996
+ return ledger.uploads.filter(
1997
+ (upload) => now - Date.parse(upload.at) < 24 * 60 * 60 * 1e3
1998
+ ).length;
1999
+ }
2000
+
2001
+ // src/capture/upload/http.ts
2002
+ import { request as httpRequest } from "node:http";
2003
+ import { request as httpsRequest } from "node:https";
2004
+ var HTTP_TIMEOUT_MS = 3e4;
2005
+ var MAX_RESPONSE_BYTES = 1024 * 1024;
2006
+ var TransportError = class extends Error {
2007
+ constructor(code) {
2008
+ super(code);
2009
+ this.code = code;
2010
+ }
2011
+ code;
2012
+ };
2013
+ function httpPost(target, headers, payload, timeoutMs = HTTP_TIMEOUT_MS) {
2014
+ const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
2015
+ return new Promise((resolve, reject) => {
2016
+ const req = transport(
2017
+ target,
2018
+ {
2019
+ method: "POST",
2020
+ agent: false,
2021
+ timeout: timeoutMs,
2022
+ headers: { ...headers, "content-length": Buffer.byteLength(payload) }
2023
+ },
2024
+ (response) => {
2025
+ const chunks = [];
2026
+ let size = 0;
2027
+ response.on("data", (chunk) => {
2028
+ size += chunk.length;
2029
+ if (size > MAX_RESPONSE_BYTES) {
2030
+ req.destroy(new TransportError("response_too_large"));
2031
+ return;
2032
+ }
2033
+ chunks.push(chunk);
2034
+ });
2035
+ response.on(
2036
+ "end",
2037
+ () => resolve({
2038
+ status: response.statusCode ?? 0,
2039
+ headers: response.headers,
2040
+ body: Buffer.concat(chunks).toString("utf8")
2041
+ })
2042
+ );
2043
+ response.on("error", () => reject(new TransportError("network_error")));
2044
+ }
2045
+ );
2046
+ req.on("timeout", () => req.destroy(new TransportError("timeout")));
2047
+ req.on(
2048
+ "error",
2049
+ (error) => reject(
2050
+ error instanceof TransportError ? error : new TransportError("network_error")
2051
+ )
2052
+ );
2053
+ req.end(payload);
2054
+ });
2055
+ }
2056
+
2057
+ // src/capture/queue/lock.ts
2058
+ import { randomBytes as randomBytes2 } from "node:crypto";
2059
+ import {
2060
+ lstatSync as lstatSync2,
2061
+ mkdirSync as mkdirSync2,
2062
+ readFileSync as readFileSync2,
2063
+ renameSync as renameSync2,
2064
+ rmSync as rmSync2,
2065
+ writeFileSync as writeFileSync2
2066
+ } from "node:fs";
2067
+ import { join as join8 } from "node:path";
2068
+ function lockPath(name) {
2069
+ return join8(stateSubdir("locks"), name);
2070
+ }
2071
+ function processAlive(pid) {
2072
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2073
+ try {
2074
+ process.kill(pid, 0);
2075
+ return true;
2076
+ } catch (error) {
2077
+ return error.code === "EPERM";
2078
+ }
2079
+ }
2080
+ function readOwner(path) {
2081
+ try {
2082
+ const value = JSON.parse(
2083
+ readFileSync2(join8(path, "owner.json"), "utf8")
2084
+ );
2085
+ const createdAt = typeof value.createdAt === "string" ? Date.parse(value.createdAt) : NaN;
2086
+ if (typeof value.pid !== "number" || Number.isNaN(createdAt))
2087
+ return void 0;
2088
+ const heartbeat = typeof value.heartbeatAt === "string" ? Date.parse(value.heartbeatAt) : NaN;
2089
+ return {
2090
+ pid: value.pid,
2091
+ createdAt,
2092
+ heartbeatAt: Number.isNaN(heartbeat) ? createdAt : heartbeat
2093
+ };
2094
+ } catch {
2095
+ return void 0;
2096
+ }
2097
+ }
2098
+ function isStale(path, staleMs) {
2099
+ let info;
2100
+ try {
2101
+ info = lstatSync2(path);
2102
+ } catch {
2103
+ return false;
2104
+ }
2105
+ if (info.isSymbolicLink() || !info.isDirectory()) return true;
2106
+ const owner = readOwner(path);
2107
+ if (!owner) return Date.now() - info.mtimeMs > staleMs;
2108
+ if (!processAlive(owner.pid)) return true;
2109
+ return Date.now() - owner.heartbeatAt > staleMs;
2110
+ }
2111
+ var held = /* @__PURE__ */ new Map();
2112
+ function tryMkdir(path) {
2113
+ try {
2114
+ mkdirSync2(path, { mode: 448 });
2115
+ } catch (error) {
2116
+ if (error.code === "EEXIST") return false;
2117
+ throw error;
2118
+ }
2119
+ const token = randomBytes2(8).toString("hex");
2120
+ held.set(path, { ino: lstatSync2(path).ino, token });
2121
+ writeOwner(path, (/* @__PURE__ */ new Date()).toISOString(), token);
2122
+ return true;
2123
+ }
2124
+ function writeOwner(path, createdAt, token) {
2125
+ const temporary = join8(path, `owner.${process.pid}.${token}.tmp`);
2126
+ writeFileSync2(
2127
+ temporary,
2128
+ JSON.stringify({
2129
+ pid: process.pid,
2130
+ createdAt,
2131
+ heartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
2132
+ token
2133
+ }),
2134
+ { mode: 384 }
2135
+ );
2136
+ renameSync2(temporary, join8(path, "owner.json"));
2137
+ }
2138
+ function readToken(path) {
2139
+ try {
2140
+ const value = JSON.parse(
2141
+ readFileSync2(join8(path, "owner.json"), "utf8")
2142
+ );
2143
+ return typeof value.token === "string" ? value.token : void 0;
2144
+ } catch {
2145
+ return void 0;
2146
+ }
2147
+ }
2148
+ function stillOurs(path) {
2149
+ const mine = held.get(path);
2150
+ if (!mine) return false;
2151
+ try {
2152
+ return lstatSync2(path).ino === mine.ino && readToken(path) === mine.token;
2153
+ } catch {
2154
+ return false;
2155
+ }
2156
+ }
2157
+ function clearStale(path, staleMs) {
2158
+ const tombstone = `${path}.stale-${process.pid}-${randomBytes2(4).toString("hex")}`;
2159
+ try {
2160
+ renameSync2(path, tombstone);
2161
+ } catch {
2162
+ return;
2163
+ }
2164
+ if (!isStale(tombstone, staleMs)) {
2165
+ try {
2166
+ renameSync2(tombstone, path);
2167
+ return;
2168
+ } catch {
2169
+ }
2170
+ }
2171
+ rmSync2(tombstone, { recursive: true, force: true });
2172
+ }
2173
+ async function acquireLock2(name, options) {
2174
+ const path = lockPath(name);
2175
+ const deadline = Date.now() + (options.waitMs ?? 0);
2176
+ let cleanups = 0;
2177
+ for (; ; ) {
2178
+ if (tryMkdir(path)) {
2179
+ let released = false;
2180
+ return () => {
2181
+ if (released) return;
2182
+ released = true;
2183
+ if (stillOurs(path)) rmSync2(path, { recursive: true, force: true });
2184
+ held.delete(path);
2185
+ };
2186
+ }
2187
+ if (cleanups < 3 && isStale(path, options.staleMs)) {
2188
+ cleanups++;
2189
+ clearStale(path, options.staleMs);
2190
+ continue;
2191
+ }
2192
+ if (Date.now() >= deadline) return void 0;
2193
+ await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? 100));
2194
+ }
2195
+ }
2196
+
2197
+ // src/capture/upload/token-broker.ts
2198
+ var MCP_CLIENT_ID = "memroot-cli";
2199
+ var REFRESH_SKEW_MS = 3e4;
2200
+ var AuthRequiredError = class extends Error {
2201
+ constructor() {
2202
+ super("AUTH_REQUIRED");
2203
+ }
2204
+ };
2205
+ function mcpAccount(apiBase) {
2206
+ return `${apiBase}#mcp`;
2207
+ }
2208
+ function mcpResource(apiBase) {
2209
+ return `${apiBase}/mcp`;
2210
+ }
2211
+ var TokenBroker = class {
2212
+ constructor(apiBase) {
2213
+ this.apiBase = apiBase;
2214
+ }
2215
+ apiBase;
2216
+ cached;
2217
+ invalidate() {
2218
+ this.cached = void 0;
2219
+ }
2220
+ async accessToken() {
2221
+ if (this.cached && this.cached.expiresAt - Date.now() > REFRESH_SKEW_MS)
2222
+ return this.cached.token;
2223
+ const release = await acquireLock2("auth.lock", {
2224
+ staleMs: 6e4,
2225
+ waitMs: 2e4,
2226
+ pollMs: 100
2227
+ });
2228
+ if (!release) throw new TransportError("auth_lock_busy");
2229
+ try {
2230
+ const refreshToken = await readSecret(mcpAccount(this.apiBase));
2231
+ if (!refreshToken) throw new AuthRequiredError();
2232
+ const response = await httpPost(
2233
+ new URL(`${this.apiBase}/oauth/token`),
2234
+ {
2235
+ "content-type": "application/x-www-form-urlencoded",
2236
+ accept: "application/json"
2237
+ },
2238
+ new URLSearchParams({
2239
+ grant_type: "refresh_token",
2240
+ refresh_token: refreshToken,
2241
+ client_id: MCP_CLIENT_ID,
2242
+ resource: mcpResource(this.apiBase)
2243
+ }).toString()
2244
+ );
2245
+ let body = {};
2246
+ try {
2247
+ const parsed = JSON.parse(response.body || "{}");
2248
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
2249
+ body = parsed;
2250
+ } catch {
2251
+ body = {};
2252
+ }
2253
+ if (response.status >= 400) {
2254
+ if (response.status === 400 && (body.error === "invalid_grant" || body.error === "invalid_client" || body.error === "invalid_target"))
2255
+ throw new AuthRequiredError();
2256
+ throw new TransportError(`token_http_${response.status}`);
2257
+ }
2258
+ if (typeof body.access_token !== "string" || !body.access_token)
2259
+ throw new TransportError("token_response_invalid");
2260
+ if (typeof body.refresh_token === "string" && body.refresh_token && body.refresh_token !== refreshToken)
2261
+ await storeSecret(mcpAccount(this.apiBase), body.refresh_token);
2262
+ const lifetime = typeof body.expires_in === "number" && body.expires_in > 0 ? body.expires_in : 300;
2263
+ this.cached = {
2264
+ token: body.access_token,
2265
+ expiresAt: Date.now() + lifetime * 1e3
2266
+ };
2267
+ return body.access_token;
2268
+ } finally {
2269
+ release();
2270
+ }
2271
+ }
2272
+ };
2273
+
2274
+ // src/capture/upload/mcp-client.ts
2275
+ var MCP_PROTOCOL_VERSION = "2025-11-25";
2276
+ var McpInputRejected = class extends Error {
2277
+ constructor(rules) {
2278
+ super("MCP_INPUT_REJECTED");
2279
+ this.rules = rules;
2280
+ }
2281
+ rules;
2282
+ };
2283
+ var McpToolError = class extends Error {
2284
+ constructor(code) {
2285
+ super(`MCP_TOOL_ERROR:${code}`);
2286
+ this.code = code;
2287
+ }
2288
+ code;
2289
+ };
2290
+ var McpRateLimited = class extends Error {
2291
+ constructor(retryAfterMs) {
2292
+ super("MCP_RATE_LIMITED");
2293
+ this.retryAfterMs = retryAfterMs;
2294
+ }
2295
+ retryAfterMs;
2296
+ };
2297
+ var MIN_RETRY_MS = 6e4;
2298
+ var MAX_RETRY_MS = 36e5;
2299
+ function retryAfter(value) {
2300
+ const header = Array.isArray(value) ? value[0] : value;
2301
+ const seconds = header ? Number(header) : Number.NaN;
2302
+ let ms = MIN_RETRY_MS;
2303
+ if (Number.isFinite(seconds) && seconds >= 0) ms = seconds * 1e3;
2304
+ else {
2305
+ const date = header ? Date.parse(header) : Number.NaN;
2306
+ if (Number.isFinite(date)) ms = date - Date.now();
2307
+ }
2308
+ return Math.min(Math.max(ms, MIN_RETRY_MS), MAX_RETRY_MS);
2309
+ }
2310
+ function parseRpc(contentType, body) {
2311
+ let text = body;
2312
+ if (contentType.includes("text/event-stream")) {
2313
+ const data = body.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim());
2314
+ text = data.at(-1) ?? "";
2315
+ }
2316
+ const value = JSON.parse(text);
2317
+ if (!value || typeof value !== "object" || Array.isArray(value))
2318
+ throw new TransportError("rpc_invalid");
2319
+ return value;
2320
+ }
2321
+ var McpClient = class {
2322
+ constructor(apiBase, broker, userAgent) {
2323
+ this.apiBase = apiBase;
2324
+ this.broker = broker;
2325
+ this.userAgent = userAgent;
2326
+ }
2327
+ apiBase;
2328
+ broker;
2329
+ userAgent;
2330
+ id = 0;
2331
+ async post(token, payload) {
2332
+ return httpPost(
2333
+ new URL(`${this.apiBase}/mcp`),
2334
+ {
2335
+ "content-type": "application/json",
2336
+ accept: "application/json, text/event-stream",
2337
+ "mcp-protocol-version": MCP_PROTOCOL_VERSION,
2338
+ authorization: `Bearer ${token}`,
2339
+ "user-agent": this.userAgent
2340
+ },
2341
+ payload
2342
+ );
2343
+ }
2344
+ async call(name, args) {
2345
+ const payload = JSON.stringify({
2346
+ jsonrpc: "2.0",
2347
+ id: ++this.id,
2348
+ method: "tools/call",
2349
+ params: { name, arguments: args }
2350
+ });
2351
+ let response = await this.post(await this.broker.accessToken(), payload);
2352
+ if (response.status === 401) {
2353
+ this.broker.invalidate();
2354
+ response = await this.post(await this.broker.accessToken(), payload);
2355
+ if (response.status === 401) throw new AuthRequiredError();
2356
+ }
2357
+ if (response.status === 403) throw new AuthRequiredError();
2358
+ if (response.status === 429)
2359
+ throw new McpRateLimited(retryAfter(response.headers["retry-after"]));
2360
+ if (response.status !== 200)
2361
+ throw new TransportError(`mcp_http_${response.status}`);
2362
+ let message;
2363
+ try {
2364
+ message = parseRpc(
2365
+ String(response.headers["content-type"] ?? ""),
2366
+ response.body
2367
+ );
2368
+ } catch {
2369
+ throw new TransportError("rpc_invalid");
2370
+ }
2371
+ const error = message.error;
2372
+ if (error) {
2373
+ if (error.code === -32602) {
2374
+ const issues = error.data?.issues;
2375
+ const rules = Array.isArray(issues) ? issues.map((issue) => issue.rule).filter((rule) => typeof rule === "string") : [];
2376
+ throw new McpInputRejected(rules);
2377
+ }
2378
+ throw new TransportError(
2379
+ `rpc_error_${typeof error.code === "number" ? error.code : "unknown"}`
2380
+ );
2381
+ }
2382
+ const result = message.result;
2383
+ const text = result?.content?.find((item) => item.type === "text")?.text;
2384
+ let value;
2385
+ try {
2386
+ value = typeof text === "string" ? JSON.parse(text) : void 0;
2387
+ } catch {
2388
+ value = void 0;
2389
+ }
2390
+ if (result?.isError === true) {
2391
+ const error2 = value?.error;
2392
+ if (error2?.code === "invalid_request" && Array.isArray(error2.issues))
2393
+ throw new McpInputRejected(
2394
+ error2.issues.map((issue) => issue.rule).filter((rule) => typeof rule === "string")
2395
+ );
2396
+ const code = error2?.code;
2397
+ const safe = typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? code : "unknown";
2398
+ if (safe === "rate_limited") throw new McpRateLimited(6e4);
2399
+ throw new McpToolError(safe);
2400
+ }
2401
+ if (value === void 0) throw new TransportError("rpc_result_invalid");
2402
+ return value;
2403
+ }
2404
+ };
2405
+
2406
+ // src/capture/status.ts
2407
+ function readText(path) {
2408
+ try {
2409
+ return readFileSync3(path, "utf8");
2410
+ } catch {
2411
+ return void 0;
2412
+ }
2413
+ }
2414
+ function absoluteEnv2(name) {
2415
+ const value = process.env[name];
2416
+ return value && isAbsolute4(value) ? value : void 0;
2417
+ }
2418
+ function codexHookTrust() {
2419
+ const home = absoluteEnv2("CODEX_HOME") ?? join9(homedir3(), ".codex");
2420
+ const text = readText(join9(home, "config.toml"));
2421
+ if (text === void 0) return "not checked (no Codex config.toml)";
2422
+ const trusted = /* @__PURE__ */ new Set();
2423
+ let section;
2424
+ for (const line of text.split("\n")) {
2425
+ const header = line.match(
2426
+ /^\s*\[hooks\.state\."(memroot@[^"]*:hooks\/hooks\.json:[^"]*)"\]\s*$/
2427
+ );
2428
+ if (header) {
2429
+ section = header[1];
2430
+ continue;
2431
+ }
2432
+ if (/^\s*\[/.test(line)) section = void 0;
2433
+ else if (section && /^\s*trusted_hash\s*=\s*"sha256:[a-f0-9]+"/.test(line))
2434
+ trusted.add(section);
2435
+ }
2436
+ const end = [...trusted].some((key) => /:session_end:/.test(key));
2437
+ const start = [...trusted].some((key) => /:session_start:/.test(key));
2438
+ if (!end && !start)
2439
+ return "SessionEnd hook not trusted yet; open /hooks in Codex and trust the Memroot hooks";
2440
+ return `SessionStart ${start ? "trusted" : "not trusted"}, SessionEnd ${end ? "trusted" : "not trusted (open /hooks in Codex)"}`;
2441
+ }
2442
+ function claudeHooksDisabled() {
2443
+ const config = absoluteEnv2("CLAUDE_CONFIG_DIR") ?? join9(homedir3(), ".claude");
2444
+ const text = readText(join9(config, "settings.json"));
2445
+ if (!text) return false;
2446
+ try {
2447
+ return JSON.parse(text).disableAllHooks === true;
2448
+ } catch {
2449
+ return false;
2450
+ }
2451
+ }
2452
+ async function captureReport(options) {
2453
+ const lines = [];
2454
+ const consent = readConsent();
2455
+ if (consent.kind === "absent") {
2456
+ lines.push(
2457
+ "Session capture: off (not enabled). Run memroot capture on to opt in."
2458
+ );
2459
+ return lines;
2460
+ }
2461
+ if (consent.kind === "invalid") {
2462
+ lines.push(
2463
+ "Session capture: consent file is invalid; capture is disabled. Run memroot capture on to repair it."
2464
+ );
2465
+ return lines;
2466
+ }
2467
+ const value = consent.consent;
2468
+ lines.push(
2469
+ `Session capture: ${value.enabled ? "on" : "off"} (agents: ${value.agents.join(", ")}; cross-agent extraction ${value.crossAgentExtraction ? "on" : "off"}; headless sessions ${value.includeHeadlessSessions ? "included" : "excluded"})`
2470
+ );
2471
+ lines.push(
2472
+ ` Caps: ${value.maxItemsPerSession} per session, ${value.maxItemsPerDay} per 24 h; API ${value.apiBase}`
2473
+ );
2474
+ if (pathExists(stateDir())) {
2475
+ const states = {};
2476
+ for (const jobId of listJobIds()) {
2477
+ const job = readJob(jobId);
2478
+ if (job) states[job.state] = (states[job.state] ?? 0) + 1;
2479
+ }
2480
+ const pending = Object.entries(states).map(([state, count]) => `${state} ${count}`).join(", ");
2481
+ lines.push(` Queue: ${pending || "empty"}`);
2482
+ const ledger = readLedger();
2483
+ lines.push(` Uploaded in the last 24 h: ${uploadsInLastDay(ledger)}`);
2484
+ if (ledger.lastRun) {
2485
+ const outcomes = Object.entries(ledger.lastRun.outcomes).map(([key, count]) => `${key} ${count}`).join(", ");
2486
+ lines.push(
2487
+ ` Last worker run: ${ledger.lastRun.finishedAt} (${outcomes || "no jobs"})`
2488
+ );
2489
+ } else lines.push(" Last worker run: never");
2490
+ } else lines.push(" Queue: empty; the worker has not run yet");
2491
+ let credential = "missing (run memroot capture on)";
2492
+ try {
2493
+ if (await readSecret(mcpAccount(value.apiBase))) {
2494
+ credential = "present";
2495
+ if (options.probe) {
2496
+ const broker = new TokenBroker(value.apiBase);
2497
+ try {
2498
+ await broker.accessToken();
2499
+ const status2 = await new McpClient(
2500
+ value.apiBase,
2501
+ broker,
2502
+ `memroot-cli/${"0.1.0-alpha.3"}`
2503
+ ).call("connection_status", {});
2504
+ credential = status2?.connected === true ? "present; connection verified" : "present; connection not verified";
2505
+ } catch (error) {
2506
+ credential = error instanceof AuthRequiredError ? "rejected or expired (run memroot capture on)" : `present; check failed (${error instanceof Error ? error.message : "error"})`;
2507
+ }
2508
+ }
2509
+ }
2510
+ } catch (error) {
2511
+ credential = `unavailable (${error instanceof Error ? error.message.split(":")[0] : "error"})`;
2512
+ }
2513
+ lines.push(` Capture connection: ${credential}`);
2514
+ if (value.agents.includes("codex"))
2515
+ lines.push(` Codex hook trust: ${codexHookTrust()}`);
2516
+ if (value.agents.includes("claude") && claudeHooksDisabled())
2517
+ lines.push(
2518
+ " Claude Code: disableAllHooks is set in settings.json; capture cannot run."
2519
+ );
2520
+ const runners = ["claude", "codex", "grok"].map((agent) => `${agent} ${findExecutable(agent) ? "found" : "not found"}`).join(", ");
2521
+ lines.push(` Headless extractor CLIs on PATH: ${runners}`);
2522
+ return lines;
2523
+ }
2524
+
2525
+ // src/codex-mcp.ts
2526
+ import { spawnSync as spawnSync3 } from "node:child_process";
2527
+
2528
+ // src/native.ts
2529
+ import { spawnSync as spawnSync2 } from "node:child_process";
2530
+ import { accessSync as accessSync2, constants as constants3, realpathSync, statSync as statSync2 } from "node:fs";
2531
+ import { delimiter as delimiter2, isAbsolute as isAbsolute5, join as join10 } from "node:path";
2532
+ import { stripVTControlCharacters as stripVTControlCharacters2 } from "node:util";
2533
+ var agents = ["claude", "codex", "grok"];
2534
+ var labels = {
2535
+ claude: "Claude Code",
2536
+ codex: "Codex",
2537
+ grok: "Grok"
2538
+ };
2539
+ function executable(agent) {
2540
+ for (const directory of (process.env.PATH ?? "").split(delimiter2)) {
2541
+ if (!isAbsolute5(directory)) continue;
2542
+ for (const extension of [""]) {
2543
+ const path = join10(directory, agent + extension);
2544
+ try {
2545
+ accessSync2(path, constants3.X_OK);
2546
+ if (statSync2(path).isFile()) return path;
2547
+ } catch {
2548
+ }
2549
+ }
2550
+ }
2551
+ return void 0;
2552
+ }
2553
+ function native(path, agent, args) {
2554
+ const result = spawnSync2(path, args, {
2555
+ encoding: "utf8",
2556
+ timeout: 6e4,
2557
+ maxBuffer: 4 * 1024 * 1024,
2558
+ shell: false
2559
+ });
2560
+ if (result.error || result.status !== 0) {
2561
+ throw new Error(
2562
+ `NATIVE_COMMAND_FAILED: ${agent} ${args.slice(0, 3).join(" ")} failed (${result.status ?? result.error?.code ?? "signal"}). Check '${agent} plugin --help', update the agent CLI, and retry. Its plugin manager may have partially registered Memroot; no success receipt was written.`
2563
+ );
2564
+ }
2565
+ return result.stdout;
2566
+ }
2567
+ function installCommands(agent, marketplace) {
2568
+ if (agent === "claude")
2569
+ return [
2570
+ ["plugin", "marketplace", "add", marketplace, "--scope", "user"],
2571
+ ["plugin", "install", "memroot@memroot", "--scope", "user"]
2572
+ ];
2573
+ if (agent === "codex")
2574
+ return [
2575
+ ["plugin", "marketplace", "add", marketplace, "--json"],
2576
+ ["plugin", "add", "memroot@memroot", "--json"]
2577
+ ];
2578
+ return [
2579
+ ["plugin", "install", join10(marketplace, "plugins", "memroot"), "--trust"]
2580
+ ];
2581
+ }
2582
+ function preflight(path, agent) {
2583
+ native(path, agent, ["plugin", "--help"]);
2584
+ if (agent !== "grok")
2585
+ native(path, agent, ["plugin", "marketplace", "add", "--help"]);
2586
+ native(path, agent, [
2587
+ "plugin",
2588
+ agent === "codex" ? "add" : "install",
2589
+ "--help"
2590
+ ]);
2591
+ }
2592
+ function rows(output, field) {
2593
+ let value;
2594
+ try {
2595
+ value = JSON.parse(output);
2596
+ } catch {
2597
+ throw new Error(
2598
+ "NATIVE_STATUS_UNSUPPORTED: Agent plugin status is not valid JSON. Update the agent CLI and retry."
2599
+ );
2600
+ }
2601
+ if (field)
2602
+ value = value && typeof value === "object" ? value[field] : void 0;
2603
+ if (!Array.isArray(value) || value.some((row) => !row || typeof row !== "object" || Array.isArray(row)))
2604
+ throw new Error(
2605
+ "NATIVE_STATUS_UNSUPPORTED: Unrecognized agent plugin status; refusing to change it."
2606
+ );
2607
+ return value;
2608
+ }
2609
+ function samePath(left, right) {
2610
+ if (typeof left !== "string") return false;
2611
+ try {
2612
+ return realpathSync(left) === realpathSync(right);
2613
+ } catch {
2614
+ return left === right;
2615
+ }
2616
+ }
2617
+ function inspectNative(path, agent, marketplace, previousMarketplace) {
2618
+ if (agent === "grok") {
2619
+ const output = stripVTControlCharacters2(
2620
+ native(path, agent, ["plugin", "list"])
2621
+ );
2622
+ const lines = output.split("\n").filter((line) => line.trim());
2623
+ const empty2 = output.trim() === "No plugins installed. Run `grok plugin install --help` to get started.";
2624
+ if (!empty2 && (!lines.length || lines.some(
2625
+ (line) => !/^\s*\S+:\s+[^[]+\[.+\](?:\s+\(disabled\))?\s*$/.test(line)
2626
+ ))) {
2627
+ throw new Error(
2628
+ "NATIVE_STATUS_UNSUPPORTED: Unrecognized Grok plugin list format. Update Grok and inspect its native plugin manager before retrying."
2629
+ );
2630
+ }
2631
+ const matching = output.split("\n").filter((line) => /:\s*memroot(?:\s|$)/.test(line));
2632
+ for (const line of matching) {
2633
+ const source = line.match(/\[local: (.*)\]/)?.[1];
2634
+ if (!samePath(source, join10(marketplace, "plugins", "memroot")) && !(previousMarketplace && samePath(source, join10(previousMarketplace, "plugins", "memroot"))))
2635
+ throw new Error(
2636
+ "PLUGIN_COLLISION: Grok already has a different Memroot plugin. Resolve it with the native plugin manager first."
2637
+ );
2638
+ if (/disabled/i.test(line))
2639
+ throw new Error(
2640
+ "PLUGIN_DISABLED: Memroot is disabled in Grok. Enable it in the native plugin manager when ready."
2641
+ );
2642
+ }
2643
+ return {
2644
+ registered: false,
2645
+ currentMarketplace: false,
2646
+ installed: matching.length > 0
2647
+ };
2648
+ }
2649
+ const markets = rows(
2650
+ native(path, agent, ["plugin", "marketplace", "list", "--json"]),
2651
+ agent === "codex" ? "marketplaces" : void 0
2652
+ );
2653
+ const plugins = rows(
2654
+ native(path, agent, ["plugin", "list", "--json"]),
2655
+ agent === "codex" ? "installed" : void 0
2656
+ );
2657
+ const matchingMarkets = markets.filter((row) => row.name === "memroot");
2658
+ if (matchingMarkets.length > 1)
2659
+ throw new Error(
2660
+ "MARKETPLACE_COLLISION: Multiple Memroot marketplaces were reported; refusing to change them."
2661
+ );
2662
+ for (const market of matchingMarkets) {
2663
+ const source = agent === "codex" ? market.root : market.path;
2664
+ if (!samePath(source, marketplace) && !(previousMarketplace && samePath(source, previousMarketplace)))
2665
+ throw new Error(
2666
+ "MARKETPLACE_COLLISION: memroot is already registered from a different source. Resolve it with the native plugin manager first."
2667
+ );
2668
+ }
2669
+ const matchingPlugins = plugins.filter(
2670
+ (row) => row.id === "memroot@memroot" || row.pluginId === "memroot@memroot" || row.name === "memroot"
2671
+ );
2672
+ if (matchingPlugins.length && !matchingMarkets.length)
2673
+ throw new Error(
2674
+ "PLUGIN_COLLISION: An existing Memroot plugin has no matching managed marketplace. Resolve it with the native plugin manager first."
2675
+ );
2676
+ for (const plugin of matchingPlugins) {
2677
+ if (plugin.enabled === false)
2678
+ throw new Error(
2679
+ `PLUGIN_DISABLED: Memroot is disabled in ${labels[agent]}. Enable it in the native plugin manager when ready; setup preserves your choice.`
2680
+ );
2681
+ if (plugin.enabled !== true || agent === "claude" && plugin.scope !== "user")
2682
+ throw new Error(
2683
+ "PLUGIN_COLLISION: Existing Memroot plugin status or scope cannot be safely managed. Check the native plugin manager."
2684
+ );
2685
+ }
2686
+ const currentMarketplace = matchingMarkets.some(
2687
+ (market) => samePath(agent === "codex" ? market.root : market.path, marketplace)
2688
+ );
2689
+ if (agent === "codex" && matchingMarkets.length > 0 && !currentMarketplace && plugins.some(
2690
+ (plugin) => plugin.marketplaceName === "memroot" && plugin.pluginId !== "memroot@memroot"
2691
+ ))
2692
+ throw new Error(
2693
+ "MARKETPLACE_COLLISION: The previous Memroot marketplace contains another installed plugin; refusing to remove it."
2694
+ );
2695
+ return {
2696
+ registered: matchingMarkets.length > 0,
2697
+ currentMarketplace,
2698
+ installed: matchingPlugins.length > 0
2699
+ };
2700
+ }
2701
+
2702
+ // src/codex-mcp.ts
2703
+ var codexMcpUrl = "https://api.memroot.dev/mcp";
2704
+ function command(path, args, interactive = false) {
2705
+ const result = spawnSync3(path, ["mcp", ...args], {
2706
+ shell: false,
2707
+ encoding: "utf8",
2708
+ timeout: interactive ? 3e5 : 3e4,
2709
+ maxBuffer: 1024 * 1024,
2710
+ // Let Codex present browser authentication directly; never capture credentials.
2711
+ stdio: interactive ? "inherit" : "pipe"
2712
+ });
2713
+ if (result.error || result.status !== 0)
2714
+ throw new Error(
2715
+ "MCP_COMMAND_FAILED: Codex MCP command did not complete. Your configuration is preserved; retry memroot setup --agent codex --yes."
2716
+ );
2717
+ return result.stdout ?? "";
2718
+ }
2719
+ function inspectCodexMcp(path) {
2720
+ let rows2;
2721
+ try {
2722
+ rows2 = JSON.parse(command(path, ["list", "--json"]));
2723
+ } catch {
2724
+ throw new Error(
2725
+ "MCP_STATUS_UNAVAILABLE: Cannot inspect Codex MCP state. Retry later; no new authorization was requested."
2726
+ );
2727
+ }
2728
+ if (!Array.isArray(rows2) || rows2.some((row2) => !row2 || typeof row2 !== "object"))
2729
+ throw new Error(
2730
+ "MCP_STATUS_UNAVAILABLE: Unsupported Codex MCP status format."
2731
+ );
2732
+ const matching = rows2.filter((row2) => row2.name === "memroot");
2733
+ if (!matching.length)
2734
+ return { configured: false, authorized: false, verified: false };
2735
+ const row = matching[0];
2736
+ if (matching.length !== 1 || row.enabled !== true || row.transport?.type !== "streamable_http" || row.transport?.url !== codexMcpUrl || row.transport?.bearer_token_env_var || row.transport?.http_headers || row.transport?.env_http_headers || row.transport?.http_headers_helper)
2737
+ throw new Error(
2738
+ "MCP_CONFLICT: Existing memroot MCP configuration differs from production OAuth or is disabled. Inspect it in Codex; setup preserves your settings."
2739
+ );
2740
+ if (row.auth_status !== "oauth" && row.auth_status !== "not_logged_in")
2741
+ throw new Error(
2742
+ "MCP_AUTH_UNKNOWN: Codex could not establish OAuth status. Resolve it in Codex before retrying; no replacement grant was requested."
2743
+ );
2744
+ return {
2745
+ configured: true,
2746
+ authorized: row.auth_status === "oauth",
2747
+ verified: false
2748
+ };
2749
+ }
2750
+ function codexMcpPreflight(path) {
2751
+ for (const [args, flags] of [
2752
+ [["list", "--help"], ["--json"]],
2753
+ [
2754
+ ["add", "--help"],
2755
+ ["--url", "--oauth-resource"]
2756
+ ],
2757
+ [["login", "--help"], []]
2758
+ ]) {
2759
+ const help2 = command(path, [...args]);
2760
+ if (flags.some((flag) => !help2.includes(flag)))
2761
+ throw new Error(
2762
+ "MCP_UNSUPPORTED: Update Codex to a version supporting native remote OAuth MCP setup."
2763
+ );
2764
+ }
2765
+ }
2766
+ async function ensureCodexMcp(options) {
2767
+ if (options.dryRun)
2768
+ return { configured: false, authorized: false, verified: false };
2769
+ const path = executable("codex");
2770
+ if (!path)
2771
+ throw new Error("AGENT_NOT_FOUND: Install Codex before linking Memroot.");
2772
+ codexMcpPreflight(path);
2773
+ let state = inspectCodexMcp(path);
2774
+ if (!state.configured) {
2775
+ command(
2776
+ path,
2777
+ ["add", "memroot", "--url", codexMcpUrl, "--oauth-resource", codexMcpUrl],
2778
+ true
2779
+ );
2780
+ state = inspectCodexMcp(path);
2781
+ if (!state.authorized)
2782
+ throw new Error(
2783
+ "MCP_AUTH_PENDING: Codex MCP is configured; authorization has not completed. Retry setup to resume."
2784
+ );
2785
+ } else if (!state.authorized) {
2786
+ command(path, ["login", "memroot"], true);
2787
+ state = inspectCodexMcp(path);
2788
+ }
2789
+ if (!state.authorized)
2790
+ throw new Error(
2791
+ "MCP_AUTH_PENDING: Codex has not confirmed OAuth authorization."
2792
+ );
2793
+ return state;
2794
+ }
2795
+ function describeCodexMcp(state) {
2796
+ return `Codex MCP: ${state.configured ? "configured" : "not configured"}; OAuth: ${state.authorized ? "authorized" : "not authorized"}; memory verification: not verified. Ask Codex to call connection_status, then save and retrieve a test memory.`;
2797
+ }
2798
+
2799
+ // src/grok-mcp.ts
2800
+ import { spawnSync as spawnSync4 } from "node:child_process";
2801
+ import { homedir as homedir4 } from "node:os";
2802
+ var grokMcpUrl = "https://api.memroot.dev/mcp";
2803
+ var MANAGED_KEYS = /* @__PURE__ */ new Set([
2804
+ "name",
2805
+ "scope",
2806
+ "enabled",
2807
+ "url",
2808
+ "type",
2809
+ "startup_timeout_sec",
2810
+ "tool_timeout_sec",
2811
+ "tool_timeouts",
2812
+ "expose_image_base64"
2813
+ ]);
2814
+ function command2(path, args) {
2815
+ const result = spawnSync4(path, ["mcp", ...args], {
2816
+ shell: false,
2817
+ encoding: "utf8",
2818
+ timeout: 3e4,
2819
+ maxBuffer: 1024 * 1024,
2820
+ stdio: "pipe",
2821
+ // `grok mcp list` merges project-scoped servers from the working
2822
+ // directory. Setup manages the user scope, so it never runs in a repo.
2823
+ cwd: homedir4()
2824
+ });
2825
+ if (result.error || result.status !== 0)
2826
+ throw new Error(
2827
+ "MCP_COMMAND_FAILED: Grok MCP command did not complete. Your configuration is preserved; retry memroot setup --agent grok --yes."
2828
+ );
2829
+ return result.stdout ?? "";
2830
+ }
2831
+ function inspectGrokMcp(path) {
2832
+ let rows2;
2833
+ try {
2834
+ rows2 = JSON.parse(command2(path, ["list", "--json"]));
2835
+ } catch {
2836
+ throw new Error(
2837
+ "MCP_STATUS_UNAVAILABLE: Cannot inspect Grok MCP state. Retry later; your configuration was not changed."
2838
+ );
2839
+ }
2840
+ if (!Array.isArray(rows2) || rows2.some((row2) => !row2 || typeof row2 !== "object" || Array.isArray(row2)))
2841
+ throw new Error(
2842
+ "MCP_STATUS_UNAVAILABLE: Unsupported Grok MCP status format."
2843
+ );
2844
+ const matching = rows2.filter(
2845
+ (row2) => row2.name === "memroot"
2846
+ );
2847
+ const row = matching[0];
2848
+ if (!row) return { configured: false, verified: false };
2849
+ if (matching.length !== 1 || row.scope !== "user" || row.enabled !== true || row.url !== grokMcpUrl || row.type !== void 0 && row.type !== "http" || Object.keys(row).some((key) => !MANAGED_KEYS.has(key)))
2850
+ throw new Error(
2851
+ "MCP_CONFLICT: Existing memroot MCP configuration in Grok differs from production OAuth, is disabled or blocked, or is not user-scoped. Inspect it with grok mcp list; setup preserves your settings."
2852
+ );
2853
+ return { configured: true, verified: false };
2854
+ }
2855
+ function grokMcpPreflight(path) {
2856
+ for (const [args, flags] of [
2857
+ [["list", "--help"], ["--json"]],
2858
+ [
2859
+ ["add", "--help"],
2860
+ ["--transport", "--scope"]
2861
+ ]
2862
+ ]) {
2863
+ const help2 = command2(path, [...args]);
2864
+ if (flags.some((flag) => !help2.includes(flag)))
1870
2865
  throw new Error(
1871
- `AUTH_KEYCHAIN_FAILED: security find-generic-password failed (${result2.status ?? "signal"}).`
2866
+ "MCP_UNSUPPORTED: Update Grok to a version supporting remote HTTP MCP setup."
1872
2867
  );
1873
- }
1874
- const secret2 = result2.stdout.replace(/\n$/, "");
1875
- return secret2.length ? secret2 : void 0;
1876
2868
  }
1877
- const result = run(
1878
- "secret-tool",
1879
- ["lookup", "service", SERVICE, "account", account],
1880
- void 0,
1881
- true
1882
- );
1883
- if (result.status !== 0) return void 0;
1884
- const secret = result.stdout.replace(/\n$/, "");
1885
- return secret.length ? secret : void 0;
1886
2869
  }
1887
- async function deleteSecret(account) {
1888
- const kind = credentialStoreKind();
1889
- if (kind === "test-file") {
1890
- const entries = await readTestStore();
1891
- if (account in entries) {
1892
- delete entries[account];
1893
- await writeTestStore(entries);
2870
+ function ensureGrokMcp(options) {
2871
+ if (options.dryRun) return { configured: false, verified: false };
2872
+ const path = executable("grok");
2873
+ if (!path)
2874
+ throw new Error("AGENT_NOT_FOUND: Install Grok before linking Memroot.");
2875
+ grokMcpPreflight(path);
2876
+ let state = inspectGrokMcp(path);
2877
+ if (!state.configured) {
2878
+ command2(path, [
2879
+ "add",
2880
+ "--scope",
2881
+ "user",
2882
+ "--transport",
2883
+ "http",
2884
+ "memroot",
2885
+ grokMcpUrl
2886
+ ]);
2887
+ try {
2888
+ state = inspectGrokMcp(path);
2889
+ } catch {
2890
+ throw new Error(
2891
+ "MCP_COMMAND_FAILED: Setup added the memroot MCP server to Grok but could not verify the entry. Inspect it with grok mcp list; remove it with grok mcp remove memroot --scope user if it looks wrong, then retry memroot setup --agent grok --yes."
2892
+ );
1894
2893
  }
1895
- return;
1896
- }
1897
- if (kind === "macos-keychain") {
1898
- const result = run(
1899
- "security",
1900
- ["delete-generic-password", "-s", SERVICE, "-a", account],
1901
- void 0,
1902
- true
1903
- );
1904
- if (result.status !== 0 && !/could not be found|item not found/i.test(result.stderr))
2894
+ if (!state.configured)
1905
2895
  throw new Error(
1906
- `AUTH_KEYCHAIN_FAILED: security delete-generic-password failed (${result.status ?? "signal"}).`
2896
+ "MCP_COMMAND_FAILED: Grok did not record the memroot MCP server. Retry memroot setup --agent grok --yes."
1907
2897
  );
1908
- return;
1909
2898
  }
1910
- run(
1911
- "secret-tool",
1912
- ["clear", "service", SERVICE, "account", account],
1913
- void 0,
1914
- true
1915
- );
2899
+ return state;
2900
+ }
2901
+ function describeGrokMcp(state) {
2902
+ if (!state.configured)
2903
+ return "Grok MCP: not configured. Run memroot setup --agent grok --yes to register the Memroot MCP server.";
2904
+ return "Grok MCP: configured; OAuth: not checked, Grok keeps the credentials in its own store; memory verification: not verified. Ask Grok to call connection_status, then save and retrieve a test memory.";
1916
2905
  }
1917
2906
 
1918
2907
  // src/auth.ts
@@ -1921,7 +2910,7 @@ var DEV_API_BASE = "http://localhost:8787";
1921
2910
  var STATIC_CLIENT_ID = "memroot-cli";
1922
2911
  var CALLBACK_PATH = "/callback";
1923
2912
  var LOGIN_TIMEOUT_MS = 18e4;
1924
- var REFRESH_SKEW_MS = 3e4;
2913
+ var REFRESH_SKEW_MS2 = 3e4;
1925
2914
  var REQUESTED_SCOPE = "memory.read memory.write";
1926
2915
  var OAuthError = class extends Error {
1927
2916
  constructor(oauth, message) {
@@ -1969,7 +2958,7 @@ function parseTokenSet(body) {
1969
2958
  }
1970
2959
  var REQUEST_TIMEOUT_MS = 3e4;
1971
2960
  function request(target, method, contentType, payload) {
1972
- const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
2961
+ const transport = target.protocol === "https:" ? httpsRequest2 : httpRequest2;
1973
2962
  const headers = {
1974
2963
  accept: "application/json"
1975
2964
  };
@@ -2086,14 +3075,15 @@ async function registerClient(apiBase, redirectUri) {
2086
3075
  );
2087
3076
  return record.client_id;
2088
3077
  }
2089
- async function exchangeCode(apiBase, clientId, redirectUri, code, verifier) {
3078
+ async function exchangeCode(apiBase, clientId, redirectUri, code, verifier, resource) {
2090
3079
  return parseTokenSet(
2091
3080
  await postForm(apiBase, "/oauth/token", {
2092
3081
  grant_type: "authorization_code",
2093
3082
  code,
2094
3083
  redirect_uri: redirectUri,
2095
3084
  client_id: clientId,
2096
- code_verifier: verifier
3085
+ code_verifier: verifier,
3086
+ ...resource ? { resource } : {}
2097
3087
  })
2098
3088
  );
2099
3089
  }
@@ -2267,7 +3257,7 @@ function openBrowser(url) {
2267
3257
  }
2268
3258
  var cachedAccessToken;
2269
3259
  async function getAccessToken(options = {}) {
2270
- if (cachedAccessToken && cachedAccessToken.expiresAt - Date.now() > REFRESH_SKEW_MS)
3260
+ if (cachedAccessToken && cachedAccessToken.expiresAt - Date.now() > REFRESH_SKEW_MS2)
2271
3261
  return cachedAccessToken.token;
2272
3262
  const config = await readConfig();
2273
3263
  const apiBase = resolveApiBase(options, config);
@@ -2313,22 +3303,15 @@ async function tryExistingSession(apiBase, config) {
2313
3303
  throw error;
2314
3304
  }
2315
3305
  }
2316
- async function login(options) {
2317
- if (process.platform !== "darwin" && process.platform !== "linux")
2318
- throw new Error(
2319
- "UNSUPPORTED_PLATFORM: memroot login supports macOS and Linux."
2320
- );
2321
- assertCredentialStoreAvailable();
2322
- const config = await readConfig();
2323
- const apiBase = resolveApiBase(options, config);
2324
- if (!options.force && await tryExistingSession(apiBase, config)) return;
2325
- const state = randomBytes(24).toString("base64url");
2326
- const verifier = randomBytes(32).toString("base64url");
3306
+ async function authorizePkce(requestOptions) {
3307
+ const { apiBase, resource } = requestOptions;
3308
+ const state = randomBytes3(24).toString("base64url");
3309
+ const verifier = randomBytes3(32).toString("base64url");
2327
3310
  const challenge = createHash2("sha256").update(verifier).digest("base64url");
2328
3311
  const waiter = await startCallbackServer(state, apiBase);
2329
3312
  try {
2330
3313
  const redirectUri = `http://127.0.0.1:${waiter.port}${CALLBACK_PATH}`;
2331
- let clientId = config.clientId && config.clientIdApiBase === apiBase ? config.clientId : STATIC_CLIENT_ID;
3314
+ let clientId = requestOptions.clientId;
2332
3315
  let retriedWithRegistration = false;
2333
3316
  for (; ; ) {
2334
3317
  const authorize = new URL(`${apiBase}/authorize`);
@@ -2339,8 +3322,10 @@ async function login(options) {
2339
3322
  authorize.searchParams.set("code_challenge", challenge);
2340
3323
  authorize.searchParams.set("code_challenge_method", "S256");
2341
3324
  authorize.searchParams.set("scope", REQUESTED_SCOPE);
3325
+ if (resource) authorize.searchParams.set("resource", resource);
2342
3326
  const url = authorize.toString();
2343
- if (options.noBrowser) console.log(`Open this URL to sign in:
3327
+ if (requestOptions.noBrowser)
3328
+ console.log(`Open this URL to sign in:
2344
3329
  ${url}`);
2345
3330
  else {
2346
3331
  openBrowser(url);
@@ -2357,31 +3342,22 @@ ${url}`
2357
3342
  clientId,
2358
3343
  redirectUri,
2359
3344
  result.code,
2360
- verifier
3345
+ verifier,
3346
+ resource
2361
3347
  );
2362
3348
  if (!tokens.refreshToken)
2363
3349
  throw new Error(
2364
3350
  "AUTH_RESPONSE_INVALID: The token endpoint returned no refresh token."
2365
3351
  );
2366
- await storeSecret(apiBase, tokens.refreshToken);
2367
- const claims = decodeClaims(tokens.accessToken);
2368
- const next = {
2369
- schemaVersion: 1,
2370
- apiBase,
2371
- clientId,
2372
- clientIdApiBase: apiBase
2373
- };
2374
- if (typeof claims?.sub === "string") next.userId = claims.sub;
2375
- await writeConfig(next);
2376
- console.log(
2377
- `Signed in to ${apiBase}${next.userId ? ` as ${next.userId}` : ""}.`
2378
- );
2379
- console.log(
2380
- "Refresh token stored in the OS credential store; no tokens were written to files."
2381
- );
2382
- return;
3352
+ return { tokens, clientId };
2383
3353
  }
2384
- if ((result.error === "invalid_client" || result.error === "unauthorized_client") && !retriedWithRegistration) {
3354
+ const rejectedClient = result.error === "invalid_client" || result.error === "unauthorized_client";
3355
+ if (rejectedClient && !requestOptions.allowDcr)
3356
+ throw new OAuthError(
3357
+ result.error,
3358
+ "CAPTURE_CLIENT_REJECTED: The server did not accept the Memroot CLI capture client. Update memroot and retry."
3359
+ );
3360
+ if (rejectedClient && !retriedWithRegistration) {
2385
3361
  retriedWithRegistration = true;
2386
3362
  const registered = await registerClient(apiBase, redirectUri);
2387
3363
  if (registered && registered !== clientId) {
@@ -2405,6 +3381,59 @@ ${url}`
2405
3381
  waiter.close();
2406
3382
  }
2407
3383
  }
3384
+ async function login(options) {
3385
+ if (process.platform !== "darwin" && process.platform !== "linux")
3386
+ throw new Error(
3387
+ "UNSUPPORTED_PLATFORM: memroot login supports macOS and Linux."
3388
+ );
3389
+ assertCredentialStoreAvailable();
3390
+ const config = await readConfig();
3391
+ const apiBase = resolveApiBase(options, config);
3392
+ if (!options.force && await tryExistingSession(apiBase, config)) return;
3393
+ const { tokens, clientId } = await authorizePkce({
3394
+ apiBase,
3395
+ clientId: config.clientId && config.clientIdApiBase === apiBase ? config.clientId : STATIC_CLIENT_ID,
3396
+ noBrowser: Boolean(options.noBrowser),
3397
+ allowDcr: true
3398
+ });
3399
+ await storeSecret(apiBase, tokens.refreshToken);
3400
+ const claims = decodeClaims(tokens.accessToken);
3401
+ const next = {
3402
+ schemaVersion: 1,
3403
+ apiBase,
3404
+ clientId,
3405
+ clientIdApiBase: apiBase
3406
+ };
3407
+ if (typeof claims?.sub === "string") next.userId = claims.sub;
3408
+ if (config.mcpRegistered) next.mcpRegistered = config.mcpRegistered;
3409
+ await writeConfig(next);
3410
+ console.log(
3411
+ `Signed in to ${apiBase}${next.userId ? ` as ${next.userId}` : ""}.`
3412
+ );
3413
+ console.log(
3414
+ "Refresh token stored in the OS credential store; no tokens were written to files."
3415
+ );
3416
+ }
3417
+ function mcpCredentialAccount(apiBase) {
3418
+ return `${apiBase}#mcp`;
3419
+ }
3420
+ async function loginMcp(apiBase, options = {}) {
3421
+ assertCredentialStoreAvailable();
3422
+ const { tokens } = await authorizePkce({
3423
+ apiBase,
3424
+ clientId: STATIC_CLIENT_ID,
3425
+ noBrowser: Boolean(options.noBrowser),
3426
+ resource: `${apiBase}/mcp`,
3427
+ allowDcr: false
3428
+ });
3429
+ await storeSecret(
3430
+ mcpCredentialAccount(apiBase),
3431
+ tokens.refreshToken
3432
+ );
3433
+ console.log(
3434
+ "Capture connection authorized; its refresh token is stored in the OS credential store."
3435
+ );
3436
+ }
2408
3437
  async function logout(options) {
2409
3438
  const config = await readConfig();
2410
3439
  const apiBase = resolveApiBase(options, config);
@@ -2419,16 +3448,32 @@ async function logout(options) {
2419
3448
  );
2420
3449
  }
2421
3450
  }
2422
- try {
2423
- await deleteSecret(apiBase);
2424
- } catch (error) {
2425
- const message = error instanceof Error ? error.message : String(error);
2426
- if (!message.startsWith("AUTH_KEYCHAIN_UNAVAILABLE")) throw error;
2427
- console.error(message);
3451
+ const mcpStored = await readSecret(mcpCredentialAccount(apiBase)).catch(
3452
+ () => void 0
3453
+ );
3454
+ if (mcpStored) {
3455
+ try {
3456
+ await revokeRefreshToken(apiBase, mcpStored, STATIC_CLIENT_ID);
3457
+ } catch (error) {
3458
+ console.error(
3459
+ `AUTH_REVOKE_FAILED: ${error instanceof Error ? error.message : "revocation failed"} Removing the local capture credential anyway.`
3460
+ );
3461
+ }
3462
+ }
3463
+ for (const account of [apiBase, mcpCredentialAccount(apiBase)]) {
3464
+ try {
3465
+ await deleteSecret(account);
3466
+ } catch (error) {
3467
+ const message = error instanceof Error ? error.message : String(error);
3468
+ if (!message.startsWith("AUTH_KEYCHAIN_UNAVAILABLE")) throw error;
3469
+ console.error(message);
3470
+ break;
3471
+ }
2428
3472
  }
2429
3473
  cachedAccessToken = void 0;
2430
3474
  const next = { schemaVersion: 1 };
2431
3475
  if (config.apiBase) next.apiBase = config.apiBase;
3476
+ if (config.mcpRegistered) next.mcpRegistered = config.mcpRegistered;
2432
3477
  await writeConfig(next);
2433
3478
  console.log("Signed out. Stored credentials and client state were removed.");
2434
3479
  }
@@ -2476,6 +3521,7 @@ async function status(options) {
2476
3521
  );
2477
3522
  else console.log(`Cloud auth: ${message}`);
2478
3523
  }
3524
+ for (const line of await captureReport({ probe: false })) console.log(line);
2479
3525
  const data = dataDirectory();
2480
3526
  console.log("Agents:");
2481
3527
  for (const agent of agents) {
@@ -2499,8 +3545,254 @@ async function status(options) {
2499
3545
  );
2500
3546
  }
2501
3547
  }
3548
+ if (agent === "grok" && path) {
3549
+ try {
3550
+ console.log(` ${describeGrokMcp(inspectGrokMcp(path))}`);
3551
+ } catch {
3552
+ console.log(
3553
+ " Grok MCP: status unavailable; memory verification: not verified."
3554
+ );
3555
+ }
3556
+ }
3557
+ }
3558
+ }
3559
+
3560
+ // src/capture/queue/purge.ts
3561
+ import { join as join11 } from "node:path";
3562
+ function removeJobFiles(jobId) {
3563
+ deleteJob(jobId);
3564
+ const work = stateSubdir("work");
3565
+ for (const name of listFiles(work))
3566
+ if (name.startsWith(`${jobId}.`)) removeFile(join11(work, name));
3567
+ }
3568
+ function purgeQueue() {
3569
+ const jobs = listJobIds();
3570
+ for (const jobId of jobs) removeJobFiles(jobId);
3571
+ const work = stateSubdir("work");
3572
+ for (const name of listFiles(work)) removeFile(join11(work, name));
3573
+ return jobs.length;
3574
+ }
3575
+
3576
+ // src/capture/command.ts
3577
+ var CAPTURE_CONSENT_TEXT_V1 = `Memroot session capture (opt-in)
3578
+
3579
+ What runs locally, after a coding session ends:
3580
+ - The plugin's SessionEnd hook only records a small job (session id, transcript path, working directory).
3581
+ - A bundled background worker reads only that session's transcript (user and assistant text, tool names and
3582
+ file paths; never tool output), removes secrets, email addresses and home paths locally, and runs your own
3583
+ agent CLI (Claude Code, Codex or Grok) headless, with no tools, to propose memories. This uses your agent
3584
+ subscription or API quota and sends the redacted excerpt (at most 48,000 characters) to that agent's model
3585
+ provider. Other agents' CLIs are never used unless you pass --allow-cross-agent-extraction.
3586
+
3587
+ What leaves your machine for Memroot (https://api.memroot.dev):
3588
+ - Typed memories (title, summary, assertions, entities such as repo-relative file paths and symbols), labeled
3589
+ session-extracted, not user-attested, with confidence <= 0.7.
3590
+ - Up to 3 short evidence quotes per memory (<= 200 characters each) copied from the redacted transcript.
3591
+ - Your repository identity (normalized git remote such as github.com/org/repo), a salted hash of the session
3592
+ id, salted hashes of the quoted transcript segments, and the extractor's name, prompt version and model.
3593
+ - One retrieval query per session containing only the repository name, to avoid duplicating saved memories.
3594
+ - Raw transcripts are never uploaded. At most 3 memories per session and 10 per rolling 24 hours.
3595
+
3596
+ How to stop:
3597
+ - memroot capture off (stops capture and deletes queued work, including work already in progress),
3598
+ or revoke "Memroot CLI" in the console, or memroot logout.`;
3599
+ async function verifyConnection(apiBase) {
3600
+ const broker = new TokenBroker(apiBase);
3601
+ const status2 = await new McpClient(
3602
+ apiBase,
3603
+ broker,
3604
+ `memroot-cli/${"0.1.0-alpha.3"}`
3605
+ ).call("connection_status", {});
3606
+ if (status2?.connected !== true)
3607
+ throw new Error(
3608
+ "CAPTURE_CONNECTION_UNVERIFIED: connection_status did not confirm the connection."
3609
+ );
3610
+ }
3611
+ async function ensureCaptureConnection(apiBase, noBrowser) {
3612
+ const stored = await readSecret(mcpAccount(apiBase));
3613
+ if (stored) {
3614
+ try {
3615
+ await verifyConnection(apiBase);
3616
+ return;
3617
+ } catch (error) {
3618
+ if (!(error instanceof AuthRequiredError)) {
3619
+ if (error instanceof TransportError)
3620
+ throw new Error(
3621
+ `CAPTURE_CONNECTION_FAILED: ${error.code}. Check the network and retry.`
3622
+ );
3623
+ throw error;
3624
+ }
3625
+ }
3626
+ }
3627
+ await loginMcp(apiBase, { noBrowser });
3628
+ await verifyConnection(apiBase);
3629
+ }
3630
+ async function captureOn(options) {
3631
+ const config = await readConfig();
3632
+ const apiBase = resolveApiBase(options.auth, config);
3633
+ const agents2 = options.agents.length ? options.agents : [...CAPTURE_AGENTS];
3634
+ console.log(CAPTURE_CONSENT_TEXT_V1);
3635
+ console.log(`
3636
+ Agents: ${agents2.join(", ")}
3637
+ `);
3638
+ if (!options.yes) {
3639
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
3640
+ throw new Error(
3641
+ "CONFIRMATION_REQUIRED: Noninteractive capture on requires --yes after reading the disclosure above."
3642
+ );
3643
+ const confirmed = await confirm({
3644
+ message: "Enable automatic session capture for these agents?",
3645
+ initialValue: false
3646
+ });
3647
+ if (isCancel(confirmed) || !confirmed) {
3648
+ console.log("Session capture was not enabled. No changes made.");
3649
+ return;
3650
+ }
3651
+ }
3652
+ await ensureCaptureConnection(apiBase, options.noBrowser);
3653
+ const previous = readConsent();
3654
+ const base = previous.kind === "ok" ? previous.consent : void 0;
3655
+ const consent = {
3656
+ schemaVersion: 1,
3657
+ enabled: true,
3658
+ enabledAt: (/* @__PURE__ */ new Date()).toISOString(),
3659
+ consentTextVersion: CONSENT_TEXT_VERSION,
3660
+ apiBase,
3661
+ agents: agents2,
3662
+ crossAgentExtraction: options.allowCrossAgent,
3663
+ includeHeadlessSessions: base?.includeHeadlessSessions ?? false,
3664
+ maxItemsPerSession: base?.maxItemsPerSession ?? DEFAULT_MAX_ITEMS_PER_SESSION,
3665
+ maxItemsPerDay: base?.maxItemsPerDay ?? DEFAULT_MAX_ITEMS_PER_DAY,
3666
+ models: base?.models ?? { claude: null, codex: null, grok: null }
3667
+ };
3668
+ writeConsent(consent);
3669
+ console.log(`Session capture enabled for ${agents2.join(", ")}.`);
3670
+ if (agents2.includes("codex"))
3671
+ console.log(
3672
+ "Codex: open /hooks in Codex and trust the Memroot SessionStart and SessionEnd hooks; until then Codex skips them."
3673
+ );
3674
+ if (agents2.includes("claude"))
3675
+ console.log(
3676
+ "Claude Code: restart running sessions to load the SessionEnd hook."
3677
+ );
3678
+ }
3679
+ function captureOff() {
3680
+ const previous = readConsent();
3681
+ if (previous.kind === "ok")
3682
+ writeConsent({ ...previous.consent, enabled: false });
3683
+ const purged = pathExists(stateDir()) ? purgeQueue() : 0;
3684
+ console.log(
3685
+ `Session capture disabled. Removed ${purged} queued job${purged === 1 ? "" : "s"} and pending candidates.`
3686
+ );
3687
+ console.log(
3688
+ 'The server-side "Memroot CLI" connection stays authorized until memroot logout or revocation in the console.'
3689
+ );
3690
+ }
3691
+ async function captureStatus() {
3692
+ for (const line of await captureReport({ probe: true })) console.log(line);
3693
+ }
3694
+
3695
+ // src/claude-mcp.ts
3696
+ import { spawnSync as spawnSync5 } from "node:child_process";
3697
+ var claudeMcpUrl = "https://api.memroot.dev/mcp";
3698
+ function command3(path, args, allowFailure = false) {
3699
+ const result = spawnSync5(path, ["mcp", ...args], {
3700
+ shell: false,
3701
+ encoding: "utf8",
3702
+ timeout: 3e4,
3703
+ maxBuffer: 1024 * 1024,
3704
+ stdio: "pipe"
3705
+ });
3706
+ if (result.error || result.status !== 0 && !allowFailure)
3707
+ throw new Error(
3708
+ "MCP_COMMAND_FAILED: Claude Code MCP command did not complete. Your configuration is preserved; retry memroot setup --agent claude --yes."
3709
+ );
3710
+ return result.status === 0 ? result.stdout ?? "" : "";
3711
+ }
3712
+ function inspectClaudeMcp(path) {
3713
+ const output = command3(path, ["get", "memroot"], true);
3714
+ if (!output) return { configured: false, authorized: false, verified: false };
3715
+ const field = (name) => output.match(new RegExp(`^\\s*${name}:\\s*(.+)$`, "mi"))?.[1]?.trim();
3716
+ if (field("Type") !== "http" || field("URL") !== claudeMcpUrl)
3717
+ throw new Error(
3718
+ "MCP_CONFLICT: Existing memroot MCP configuration differs from production. Inspect it with claude mcp get memroot; setup preserves your settings."
3719
+ );
3720
+ const status2 = field("Status") ?? "";
3721
+ if (/needs authentication/i.test(status2))
3722
+ return { configured: true, authorized: false, verified: false };
3723
+ if (/connected/i.test(status2))
3724
+ return { configured: true, authorized: true, verified: false };
3725
+ throw new Error(
3726
+ "MCP_AUTH_UNKNOWN: Claude Code could not establish memroot MCP status. Resolve it with /mcp in Claude Code; no replacement grant was requested."
3727
+ );
3728
+ }
3729
+ function claudeMcpPreflight(path) {
3730
+ const help2 = command3(path, ["add", "--help"]);
3731
+ if (!help2.includes("--transport") || !help2.includes("--scope"))
3732
+ throw new Error(
3733
+ "MCP_UNSUPPORTED: Update Claude Code to a version supporting remote HTTP MCP setup."
3734
+ );
3735
+ }
3736
+ function ensureClaudeMcp(options) {
3737
+ if (options.dryRun)
3738
+ return { configured: false, authorized: false, verified: false };
3739
+ const path = executable("claude");
3740
+ if (!path)
3741
+ throw new Error(
3742
+ "AGENT_NOT_FOUND: Install Claude Code before linking Memroot."
3743
+ );
3744
+ claudeMcpPreflight(path);
3745
+ let state = inspectClaudeMcp(path);
3746
+ if (!state.configured) {
3747
+ command3(path, [
3748
+ "add",
3749
+ "--scope",
3750
+ "user",
3751
+ "--transport",
3752
+ "http",
3753
+ "memroot",
3754
+ claudeMcpUrl
3755
+ ]);
3756
+ state = inspectClaudeMcp(path);
2502
3757
  }
3758
+ return state;
3759
+ }
3760
+ function describeClaudeMcp(state) {
3761
+ const auth = state.authorized ? "OAuth: authorized" : "OAuth: not authorized. Run /mcp in Claude Code and authenticate memroot";
3762
+ return `Claude Code MCP: ${state.configured ? "configured" : "not configured"}; ${auth}; memory verification: not verified. Ask Claude Code to save and retrieve a test memory.`;
3763
+ }
3764
+
3765
+ // src/messages.ts
3766
+ function setupComplete(selected) {
3767
+ const parts = [];
3768
+ if (selected.includes("claude"))
3769
+ parts.push("Claude Code authorizes its MCP server through /mcp");
3770
+ if (selected.includes("codex")) parts.push("Codex uses native MCP OAuth");
3771
+ if (selected.includes("grok"))
3772
+ parts.push("Grok authorizes its MCP server through /mcps");
3773
+ return `Plugins ready. ${parts.join("; ")}.`;
3774
+ }
3775
+ function cloudNote(selected) {
3776
+ const parts = [];
3777
+ if (selected.includes("claude"))
3778
+ parts.push(
3779
+ "Claude Code setup registers the production MCP server; authorize it with /mcp in Claude Code"
3780
+ );
3781
+ if (selected.includes("codex"))
3782
+ parts.push("Codex setup links production MCP through its browser sign-in");
3783
+ if (selected.includes("grok"))
3784
+ parts.push(
3785
+ "Grok setup registers the production MCP server; authorize it with /mcps in Grok"
3786
+ );
3787
+ return `Cloud memory: ${parts.join(". ")}. Session capture: opt-in (memroot capture on).`;
3788
+ }
3789
+ function installationSummary(selected, data) {
3790
+ return `${selected.map((agent) => labels[agent]).join(", ")}
3791
+ User scope \xB7 ${data}
3792
+ ${selected.includes("grok") ? "Grok: trust this local Memroot plugin using native --trust.\n" : ""}${cloudNote(selected)}`;
2503
3793
  }
3794
+ var codexHookTrustStep = "Next step for Codex: open /hooks in Codex and trust the Memroot SessionStart and SessionEnd hooks. Until you do, Codex skips them, so sessions get no Memroot reminder.";
3795
+ var grokMcpAuthStep = "Next step for Grok: open Grok, run /mcps, select memroot and press i to sign in in the browser. Until you do, Grok has no Memroot memory tools. Skip this if memroot already shows as connected there.";
2504
3796
 
2505
3797
  // src/index.ts
2506
3798
  var installationDocs = {
@@ -2508,15 +3800,16 @@ var installationDocs = {
2508
3800
  codex: "https://developers.openai.com/codex/cli",
2509
3801
  grok: "https://docs.x.ai/build/overview"
2510
3802
  };
2511
- var setupComplete = "Plugins ready. Codex uses native MCP OAuth; other agent adapters use memroot login.";
2512
- var cloudStatus = "Cloud memory: Codex setup links production MCP through its browser sign-in. Other adapters use memroot login. Automatic session upload is not available.";
2513
- var help = `Memroot ${version} \u2014 native agent plugin installer and cloud sign-in
3803
+ var cloudStatus = cloudNote(["claude", "codex", "grok"]);
3804
+ var help = `Memroot ${version} \u2014 native agent plugin installer, cloud sign-in and opt-in session capture
2514
3805
 
2515
3806
  Usage:
2516
3807
  npx memroot setup
2517
3808
  memroot setup --agent codex --yes
2518
3809
  memroot setup --agent claude --agent grok --yes --dry-run
2519
3810
  memroot login
3811
+ memroot capture on
3812
+ memroot capture status
2520
3813
  memroot doctor
2521
3814
  memroot status
2522
3815
 
@@ -2524,16 +3817,21 @@ Commands:
2524
3817
  setup Install native plugins for your user account
2525
3818
  doctor Check installer receipts and plugin file integrity
2526
3819
  login Connect your Memroot account via browser sign-in
2527
- logout Remove stored cloud credentials
3820
+ logout Remove stored cloud and capture credentials
2528
3821
  whoami Show the signed-in account identity
2529
- status Show cloud auth and agent install state
3822
+ status Show cloud auth, capture and agent install state
2530
3823
  token Print a fresh access token for agent adapters
3824
+ capture on | off | status \u2014 opt-in session-end memory capture
2531
3825
 
2532
3826
  Options:
2533
- --agent claude, codex, or grok; repeat to select more than one (setup, doctor)
2534
- --yes Use the explicitly selected agents without prompts (setup)
3827
+ --agent claude, codex, or grok; repeat to select more than one (setup, doctor, capture on)
3828
+ --yes Use the explicitly selected agents without prompts (setup, capture on)
2535
3829
  --dry-run Show planned changes without running agent CLIs or writing files (setup)
2536
- --no-browser Print the sign-in URL instead of opening a browser (login)
3830
+ --capture Enable session capture after setup (runs the capture on flow)
3831
+ --no-capture Disable session capture during setup
3832
+ --allow-cross-agent-extraction
3833
+ Let capture fall back to another installed agent CLI (capture on)
3834
+ --no-browser Print the sign-in URL instead of opening a browser (login, capture on)
2537
3835
  --force Re-authenticate even if credentials are still valid (login)
2538
3836
  --env prod or dev; dev targets a local API stub on localhost:8787
2539
3837
  --api-base Override the API base URL (MEMROOT_API_BASE also works)
@@ -2541,6 +3839,8 @@ Options:
2541
3839
  --version Show version
2542
3840
 
2543
3841
  ${cloudStatus}
3842
+ Session capture runs your own agent CLI locally without tools after a session ends,
3843
+ redacts locally and uploads only typed, session-extracted memories.
2544
3844
  Grok setup trusts the selected local plugin via its native --trust option.
2545
3845
  Hook execution trust remains controlled by each agent.
2546
3846
  `;
@@ -2551,7 +3851,8 @@ var commands = [
2551
3851
  "logout",
2552
3852
  "whoami",
2553
3853
  "status",
2554
- "token"
3854
+ "token",
3855
+ "capture"
2555
3856
  ];
2556
3857
  var authCommands = [
2557
3858
  "login",
@@ -2561,22 +3862,35 @@ var authCommands = [
2561
3862
  "token"
2562
3863
  ];
2563
3864
  function parse(args) {
2564
- const command2 = args.shift();
2565
- if (!command2 || !commands.includes(command2))
3865
+ const command4 = args.shift();
3866
+ if (!command4 || !commands.includes(command4))
2566
3867
  throw new Error(
2567
- "INVALID_ARGUMENT: Choose setup, doctor, login, logout, whoami, status, or token. Run memroot --help."
3868
+ "INVALID_ARGUMENT: Choose setup, doctor, login, logout, whoami, status, token, or capture. Run memroot --help."
2568
3869
  );
2569
3870
  const options = {
2570
- command: command2,
3871
+ command: command4,
3872
+ allowCrossAgent: false,
2571
3873
  selected: [],
2572
3874
  yes: false,
2573
3875
  dryRun: false,
2574
3876
  noBrowser: false,
2575
3877
  force: false
2576
3878
  };
3879
+ if (command4 === "capture") {
3880
+ const action = args.shift();
3881
+ if (action !== "on" && action !== "off" && action !== "status")
3882
+ throw new Error(
3883
+ "INVALID_ARGUMENT: Use memroot capture on, off, or status."
3884
+ );
3885
+ options.captureAction = action;
3886
+ }
2577
3887
  while (args.length) {
2578
3888
  const flag = args.shift();
2579
3889
  if (flag === "--yes") options.yes = true;
3890
+ else if (flag === "--capture") options.capture = true;
3891
+ else if (flag === "--no-capture") options.capture = false;
3892
+ else if (flag === "--allow-cross-agent-extraction")
3893
+ options.allowCrossAgent = true;
2580
3894
  else if (flag === "--dry-run") options.dryRun = true;
2581
3895
  else if (flag === "--no-browser") options.noBrowser = true;
2582
3896
  else if (flag === "--force") options.force = true;
@@ -2605,20 +3919,37 @@ function parse(args) {
2605
3919
  `INVALID_ARGUMENT: Unknown option ${flag}. Run memroot --help.`
2606
3920
  );
2607
3921
  }
2608
- if (authCommands.includes(command2)) {
3922
+ if (options.capture !== void 0 && command4 !== "setup")
3923
+ throw new Error(
3924
+ "INVALID_ARGUMENT: --capture and --no-capture apply to setup."
3925
+ );
3926
+ if (options.allowCrossAgent && !(command4 === "capture" && options.captureAction === "on"))
3927
+ throw new Error(
3928
+ "INVALID_ARGUMENT: --allow-cross-agent-extraction applies to capture on."
3929
+ );
3930
+ if (command4 === "capture") {
3931
+ if (options.captureAction !== "on" && (options.selected.length || options.yes || options.noBrowser))
3932
+ throw new Error(
3933
+ `INVALID_ARGUMENT: capture ${options.captureAction} takes no --agent, --yes or --no-browser.`
3934
+ );
3935
+ if (options.dryRun || options.force)
3936
+ throw new Error(
3937
+ "INVALID_ARGUMENT: capture does not support --dry-run or --force."
3938
+ );
3939
+ } else if (authCommands.includes(command4)) {
2609
3940
  if (options.selected.length || options.yes || options.dryRun)
2610
3941
  throw new Error(
2611
- `INVALID_ARGUMENT: ${command2} does not support --agent, --yes, or --dry-run.`
3942
+ `INVALID_ARGUMENT: ${command4} does not support --agent, --yes, or --dry-run.`
2612
3943
  );
2613
- if (command2 !== "login" && (options.noBrowser || options.force))
3944
+ if (command4 !== "login" && (options.noBrowser || options.force))
2614
3945
  throw new Error(
2615
- `INVALID_ARGUMENT: ${command2} does not support --no-browser or --force.`
3946
+ `INVALID_ARGUMENT: ${command4} does not support --no-browser or --force.`
2616
3947
  );
2617
- } else if (options.noBrowser || options.force || options.env || options.apiBase)
3948
+ } else if (options.force || (options.noBrowser || options.env || options.apiBase) && !(command4 === "setup" && options.capture === true))
2618
3949
  throw new Error(
2619
- `INVALID_ARGUMENT: ${command2} does not support cloud sign-in options.`
3950
+ `INVALID_ARGUMENT: ${command4} does not support cloud sign-in options.`
2620
3951
  );
2621
- if (command2 === "doctor" && (options.yes || options.dryRun))
3952
+ if (command4 === "doctor" && (options.yes || options.dryRun))
2622
3953
  throw new Error("INVALID_ARGUMENT: doctor supports only --agent.");
2623
3954
  return options;
2624
3955
  }
@@ -2648,6 +3979,10 @@ async function doctor(selected) {
2648
3979
  );
2649
3980
  if (agent === "codex" && path)
2650
3981
  console.log(describeCodexMcp(inspectCodexMcp(path)));
3982
+ if (agent === "claude" && path)
3983
+ console.log(describeClaudeMcp(inspectClaudeMcp(path)));
3984
+ if (agent === "grok" && path)
3985
+ console.log(describeGrokMcp(inspectGrokMcp(path)));
2651
3986
  if (!path) failures++;
2652
3987
  } catch (error) {
2653
3988
  failures++;
@@ -2657,6 +3992,7 @@ async function doctor(selected) {
2657
3992
  }
2658
3993
  }
2659
3994
  console.log(cloudStatus);
3995
+ for (const line of await captureReport({ probe: false })) console.log(line);
2660
3996
  if (failures) process.exitCode = 1;
2661
3997
  }
2662
3998
  async function setup(options) {
@@ -2671,7 +4007,7 @@ async function setup(options) {
2671
4007
  if (interactive) {
2672
4008
  intro(`Memroot ${version}`);
2673
4009
  note(
2674
- "Install Memroot plugins for your coding agents.\nCodex setup opens browser sign-in for production memory.",
4010
+ "Install Memroot plugins for your coding agents.\nProduction memory is linked through each agent's native sign-in.",
2675
4011
  "Set up your agents"
2676
4012
  );
2677
4013
  if (!options.selected.length) {
@@ -2706,9 +4042,7 @@ async function setup(options) {
2706
4042
  "CONFIRMATION_REQUIRED: Noninteractive setup requires --yes with explicit --agent selections."
2707
4043
  );
2708
4044
  const data = dataDirectory();
2709
- const summary = `${options.selected.map((agent) => labels[agent]).join(", ")}
2710
- User scope \xB7 ${data}
2711
- ${options.selected.includes("grok") ? "Grok: trust this local Memroot plugin using native --trust.\n" : ""}${cloudStatus}`;
4045
+ const summary = installationSummary(options.selected, data);
2712
4046
  if (options.dryRun) {
2713
4047
  console.log(`Dry run \u2014 no changes
2714
4048
  ${summary}`);
@@ -2716,10 +4050,26 @@ ${summary}`);
2716
4050
  console.log(
2717
4051
  `${agent}: ${JSON.stringify(installCommands(agent, `${data}/releases/${version}/${agent}`))}`
2718
4052
  );
4053
+ if (options.selected.includes("claude"))
4054
+ console.log(
4055
+ `claude: mcp add --scope user --transport http memroot ${claudeMcpUrl}; authorize with /mcp.`
4056
+ );
2719
4057
  if (options.selected.includes("codex"))
2720
4058
  console.log(
2721
4059
  `codex: mcp add memroot --url ${codexMcpUrl} --oauth-resource ${codexMcpUrl}; reuse existing OAuth authorization.`
2722
4060
  );
4061
+ if (options.selected.includes("grok"))
4062
+ console.log(
4063
+ `grok: mcp add --scope user --transport http memroot ${grokMcpUrl}; authorize with /mcps in Grok.`
4064
+ );
4065
+ if (options.selected.includes("codex")) console.log(codexHookTrustStep);
4066
+ if (options.selected.includes("grok")) console.log(grokMcpAuthStep);
4067
+ if (options.capture === true)
4068
+ console.log(
4069
+ `capture: enable session capture for ${options.selected.join(", ")} (browser authorization of the Memroot CLI capture connection).`
4070
+ );
4071
+ else if (options.capture === false)
4072
+ console.log("capture: disable session capture.");
2723
4073
  return;
2724
4074
  }
2725
4075
  if (interactive) {
@@ -2753,7 +4103,27 @@ ${summary}`);
2753
4103
  );
2754
4104
  if (agent === "codex")
2755
4105
  console.log(
2756
- describeCodexMcp(await ensureCodexMcp({ dryRun: false }))
4106
+ describeCodexMcp(
4107
+ await recordMcp("codex", () => ensureCodexMcp({ dryRun: false }))
4108
+ )
4109
+ );
4110
+ if (agent === "claude")
4111
+ console.log(
4112
+ describeClaudeMcp(
4113
+ await recordMcp(
4114
+ "claude",
4115
+ async () => ensureClaudeMcp({ dryRun: false })
4116
+ )
4117
+ )
4118
+ );
4119
+ if (agent === "grok")
4120
+ console.log(
4121
+ describeGrokMcp(
4122
+ await recordMcp(
4123
+ "grok",
4124
+ async () => ensureGrokMcp({ dryRun: false })
4125
+ )
4126
+ )
2757
4127
  );
2758
4128
  continue;
2759
4129
  }
@@ -2785,13 +4155,93 @@ ${summary}`);
2785
4155
  `${labels[agent]}: plugin installed. Restart the agent to load it.`
2786
4156
  );
2787
4157
  if (agent === "codex")
2788
- console.log(describeCodexMcp(await ensureCodexMcp({ dryRun: false })));
4158
+ console.log(
4159
+ describeCodexMcp(
4160
+ await recordMcp("codex", () => ensureCodexMcp({ dryRun: false }))
4161
+ )
4162
+ );
4163
+ if (agent === "claude")
4164
+ console.log(
4165
+ describeClaudeMcp(
4166
+ await recordMcp(
4167
+ "claude",
4168
+ async () => ensureClaudeMcp({ dryRun: false })
4169
+ )
4170
+ )
4171
+ );
4172
+ if (agent === "grok")
4173
+ console.log(
4174
+ describeGrokMcp(
4175
+ await recordMcp(
4176
+ "grok",
4177
+ async () => ensureGrokMcp({ dryRun: false })
4178
+ )
4179
+ )
4180
+ );
2789
4181
  }
2790
4182
  } finally {
2791
4183
  await release();
2792
4184
  }
2793
- if (interactive) outro(setupComplete);
2794
- else console.log(setupComplete);
4185
+ await setupCapture(options, interactive);
4186
+ if (options.selected.includes("codex")) console.log(codexHookTrustStep);
4187
+ if (options.selected.includes("grok")) console.log(grokMcpAuthStep);
4188
+ if (interactive) outro(setupComplete(options.selected));
4189
+ else console.log(setupComplete(options.selected));
4190
+ }
4191
+ async function setupCapture(options, interactive) {
4192
+ const auth = authOptions(options);
4193
+ if (options.capture === false) {
4194
+ captureOff();
4195
+ return;
4196
+ }
4197
+ if (options.capture === true) {
4198
+ await captureOn({
4199
+ agents: options.selected,
4200
+ allowCrossAgent: false,
4201
+ yes: true,
4202
+ noBrowser: options.noBrowser,
4203
+ auth
4204
+ });
4205
+ return;
4206
+ }
4207
+ if (!interactive) return;
4208
+ note(CAPTURE_CONSENT_TEXT_V1, "Session capture");
4209
+ const enable = await confirm({
4210
+ message: "Enable automatic session capture? You can change this later with memroot capture on|off.",
4211
+ initialValue: false
4212
+ });
4213
+ if (isCancel(enable) || !enable) return;
4214
+ await captureOn({
4215
+ agents: options.selected,
4216
+ allowCrossAgent: false,
4217
+ yes: true,
4218
+ noBrowser: false,
4219
+ auth
4220
+ });
4221
+ }
4222
+ async function recordMcp(agent, ensure) {
4223
+ let state;
4224
+ try {
4225
+ state = await ensure();
4226
+ return state;
4227
+ } finally {
4228
+ const config = await readConfig();
4229
+ const registered = { ...config.mcpRegistered };
4230
+ if (state?.configured) registered[agent] = true;
4231
+ else delete registered[agent];
4232
+ if (registered.claude || registered.codex || registered.grok)
4233
+ config.mcpRegistered = registered;
4234
+ else delete config.mcpRegistered;
4235
+ await writeConfig(config);
4236
+ }
4237
+ }
4238
+ function authOptions(options) {
4239
+ const auth = {};
4240
+ if (options.env) auth.env = options.env;
4241
+ if (options.apiBase) auth.apiBase = options.apiBase;
4242
+ if (options.noBrowser) auth.noBrowser = true;
4243
+ if (options.force) auth.force = true;
4244
+ return auth;
2795
4245
  }
2796
4246
  async function main() {
2797
4247
  const args = process.argv.slice(2);
@@ -2804,11 +4254,20 @@ async function main() {
2804
4254
  return;
2805
4255
  }
2806
4256
  const options = parse(args);
2807
- const auth = {};
2808
- if (options.env) auth.env = options.env;
2809
- if (options.apiBase) auth.apiBase = options.apiBase;
2810
- if (options.noBrowser) auth.noBrowser = true;
2811
- if (options.force) auth.force = true;
4257
+ const auth = authOptions(options);
4258
+ if (options.command === "capture") {
4259
+ if (options.captureAction === "on")
4260
+ await captureOn({
4261
+ agents: options.selected,
4262
+ allowCrossAgent: options.allowCrossAgent,
4263
+ yes: options.yes,
4264
+ noBrowser: options.noBrowser,
4265
+ auth
4266
+ });
4267
+ else if (options.captureAction === "off") captureOff();
4268
+ else await captureStatus();
4269
+ return;
4270
+ }
2812
4271
  if (options.command === "setup") await setup(options);
2813
4272
  else if (options.command === "doctor") await doctor(options.selected);
2814
4273
  else if (options.command === "login") await login(auth);