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.
- package/README.md +26 -2
- package/dist/index.js +1929 -470
- package/dist/plugins/claude/.claude-plugin/plugin.json +2 -2
- package/dist/plugins/claude/README.md +8 -4
- package/dist/plugins/claude/hooks/hooks.json +10 -0
- package/dist/plugins/claude/scripts/capture-worker.mjs +19294 -0
- package/dist/plugins/claude/scripts/session-end.mjs +469 -0
- package/dist/plugins/claude/scripts/session-start.mjs +372 -53
- package/dist/plugins/claude/scripts/status.mjs +152 -12
- package/dist/plugins/claude/skills/memroot-memory/SKILL.md +48 -0
- package/dist/plugins/claude/skills/memroot-memory/references/save.md +76 -0
- package/dist/plugins/claude/skills/memroot-status/SKILL.md +19 -0
- package/dist/plugins/codex/.codex-plugin/plugin.json +4 -4
- package/dist/plugins/codex/README.md +13 -9
- package/dist/plugins/codex/hooks/hooks.json +11 -0
- package/dist/plugins/codex/scripts/capture-worker.mjs +19294 -0
- package/dist/plugins/codex/scripts/session-end.mjs +469 -0
- package/dist/plugins/codex/scripts/session-start.mjs +372 -53
- package/dist/plugins/codex/scripts/status.mjs +149 -9
- package/dist/plugins/codex/skills/memroot-memory/SKILL.md +48 -0
- package/dist/plugins/codex/skills/memroot-memory/references/save.md +76 -0
- package/dist/plugins/codex/skills/memroot-status/SKILL.md +20 -0
- package/dist/plugins/grok/.claude-plugin/plugin.json +2 -2
- package/dist/plugins/grok/README.md +10 -4
- package/dist/plugins/grok/hooks/hooks.json +24 -1
- package/dist/plugins/grok/scripts/capture-worker.mjs +19294 -0
- package/dist/plugins/grok/scripts/session-end.mjs +469 -0
- package/dist/plugins/grok/scripts/session-reminder.mjs +275 -0
- package/dist/plugins/grok/scripts/status.mjs +152 -12
- package/dist/plugins/grok/skills/memroot-memory/SKILL.md +48 -0
- package/dist/plugins/grok/skills/memroot-memory/references/save.md +76 -0
- package/dist/plugins/grok/skills/memroot-status/SKILL.md +20 -0
- package/package.json +6 -3
- package/dist/plugins/claude/skills/status/SKILL.md +0 -18
- package/dist/plugins/codex/skills/status/SKILL.md +0 -22
- 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
|
|
1134
|
-
import { request as
|
|
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/
|
|
1137
|
-
import {
|
|
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/
|
|
1141
|
+
// src/credentials.ts
|
|
1140
1142
|
import { spawnSync } from "node:child_process";
|
|
1141
|
-
import {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
|
|
1249
|
+
const info = lstatSync(target);
|
|
1250
|
+
if ((info.mode & 511) !== 448) chmodSync(target, 448);
|
|
1163
1251
|
}
|
|
1164
|
-
function
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
|
1179
|
-
|
|
1180
|
-
return
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
|
1204
|
-
|
|
1288
|
+
function removeFile(path) {
|
|
1289
|
+
rmSync(path, { force: true });
|
|
1290
|
+
}
|
|
1291
|
+
function listFiles(path) {
|
|
1205
1292
|
try {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
|
1221
|
-
if (typeof left !== "string") return false;
|
|
1300
|
+
function pathExists(path) {
|
|
1222
1301
|
try {
|
|
1223
|
-
|
|
1302
|
+
lstatSync(path);
|
|
1303
|
+
return true;
|
|
1224
1304
|
} catch {
|
|
1225
|
-
return
|
|
1305
|
+
return false;
|
|
1226
1306
|
}
|
|
1227
1307
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
"
|
|
1329
|
+
"INVALID_DATA_DIRECTORY: XDG_DATA_HOME or LOCALAPPDATA must be absolute."
|
|
1272
1330
|
);
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
);
|
|
1283
|
-
if (
|
|
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
|
-
"
|
|
1355
|
+
"INTEGRITY_ERROR: Managed plugin directory is not a real directory."
|
|
1286
1356
|
);
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
if (
|
|
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
|
-
"
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
|
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:" ?
|
|
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() >
|
|
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
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
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
|
|
2512
|
-
var
|
|
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
|
-
--
|
|
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
|
|
2565
|
-
if (!
|
|
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
|
|
3868
|
+
"INVALID_ARGUMENT: Choose setup, doctor, login, logout, whoami, status, token, or capture. Run memroot --help."
|
|
2568
3869
|
);
|
|
2569
3870
|
const options = {
|
|
2570
|
-
command:
|
|
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 (
|
|
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: ${
|
|
3942
|
+
`INVALID_ARGUMENT: ${command4} does not support --agent, --yes, or --dry-run.`
|
|
2612
3943
|
);
|
|
2613
|
-
if (
|
|
3944
|
+
if (command4 !== "login" && (options.noBrowser || options.force))
|
|
2614
3945
|
throw new Error(
|
|
2615
|
-
`INVALID_ARGUMENT: ${
|
|
3946
|
+
`INVALID_ARGUMENT: ${command4} does not support --no-browser or --force.`
|
|
2616
3947
|
);
|
|
2617
|
-
} else if (options.
|
|
3948
|
+
} else if (options.force || (options.noBrowser || options.env || options.apiBase) && !(command4 === "setup" && options.capture === true))
|
|
2618
3949
|
throw new Error(
|
|
2619
|
-
`INVALID_ARGUMENT: ${
|
|
3950
|
+
`INVALID_ARGUMENT: ${command4} does not support cloud sign-in options.`
|
|
2620
3951
|
);
|
|
2621
|
-
if (
|
|
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.\
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2794
|
-
|
|
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.
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
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);
|