memroot 0.1.0-alpha.0 → 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/dist/index.js +1146 -24
- package/dist/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/dist/plugins/claude/README.md +1 -1
- package/dist/plugins/claude/scripts/session-start.mjs +1 -1
- package/dist/plugins/claude/scripts/status.mjs +2 -2
- package/dist/plugins/claude/skills/status/SKILL.md +1 -1
- package/dist/plugins/codex/.codex-plugin/plugin.json +4 -4
- package/dist/plugins/codex/README.md +4 -2
- package/dist/plugins/codex/scripts/session-start.mjs +1 -1
- package/dist/plugins/codex/scripts/status.mjs +5 -5
- package/dist/plugins/codex/skills/status/SKILL.md +6 -2
- package/dist/plugins/grok/.claude-plugin/plugin.json +1 -1
- package/dist/plugins/grok/README.md +1 -1
- package/dist/plugins/grok/scripts/status.mjs +2 -2
- package/dist/plugins/grok/skills/status/SKILL.md +1 -1
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -1127,6 +1127,15 @@ var SELECT_INSTRUCTIONS = [
|
|
|
1127
1127
|
];
|
|
1128
1128
|
var i = `${styleText("gray", S_BAR)} `;
|
|
1129
1129
|
|
|
1130
|
+
// src/auth.ts
|
|
1131
|
+
import { spawn } from "node:child_process";
|
|
1132
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
1133
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
1134
|
+
import { request as httpsRequest } from "node:https";
|
|
1135
|
+
|
|
1136
|
+
// src/codex-mcp.ts
|
|
1137
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1138
|
+
|
|
1130
1139
|
// src/native.ts
|
|
1131
1140
|
import { spawnSync } from "node:child_process";
|
|
1132
1141
|
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
@@ -1242,7 +1251,11 @@ function inspectNative(path, agent, marketplace, previousMarketplace) {
|
|
|
1242
1251
|
"PLUGIN_DISABLED: Memroot is disabled in Grok. Enable it in the native plugin manager when ready."
|
|
1243
1252
|
);
|
|
1244
1253
|
}
|
|
1245
|
-
return {
|
|
1254
|
+
return {
|
|
1255
|
+
registered: false,
|
|
1256
|
+
currentMarketplace: false,
|
|
1257
|
+
installed: matching.length > 0
|
|
1258
|
+
};
|
|
1246
1259
|
}
|
|
1247
1260
|
const markets = rows(
|
|
1248
1261
|
native(path, agent, ["plugin", "marketplace", "list", "--json"]),
|
|
@@ -1253,6 +1266,10 @@ function inspectNative(path, agent, marketplace, previousMarketplace) {
|
|
|
1253
1266
|
agent === "codex" ? "installed" : void 0
|
|
1254
1267
|
);
|
|
1255
1268
|
const matchingMarkets = markets.filter((row) => row.name === "memroot");
|
|
1269
|
+
if (matchingMarkets.length > 1)
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
"MARKETPLACE_COLLISION: Multiple Memroot marketplaces were reported; refusing to change them."
|
|
1272
|
+
);
|
|
1256
1273
|
for (const market of matchingMarkets) {
|
|
1257
1274
|
const source = agent === "codex" ? market.root : market.path;
|
|
1258
1275
|
if (!samePath(source, marketplace) && !(previousMarketplace && samePath(source, previousMarketplace)))
|
|
@@ -1277,12 +1294,132 @@ function inspectNative(path, agent, marketplace, previousMarketplace) {
|
|
|
1277
1294
|
"PLUGIN_COLLISION: Existing Memroot plugin status or scope cannot be safely managed. Check the native plugin manager."
|
|
1278
1295
|
);
|
|
1279
1296
|
}
|
|
1297
|
+
const currentMarketplace = matchingMarkets.some(
|
|
1298
|
+
(market) => samePath(agent === "codex" ? market.root : market.path, marketplace)
|
|
1299
|
+
);
|
|
1300
|
+
if (agent === "codex" && matchingMarkets.length > 0 && !currentMarketplace && plugins.some(
|
|
1301
|
+
(plugin) => plugin.marketplaceName === "memroot" && plugin.pluginId !== "memroot@memroot"
|
|
1302
|
+
))
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
"MARKETPLACE_COLLISION: The previous Memroot marketplace contains another installed plugin; refusing to remove it."
|
|
1305
|
+
);
|
|
1280
1306
|
return {
|
|
1281
1307
|
registered: matchingMarkets.length > 0,
|
|
1308
|
+
currentMarketplace,
|
|
1282
1309
|
installed: matchingPlugins.length > 0
|
|
1283
1310
|
};
|
|
1284
1311
|
}
|
|
1285
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
|
+
|
|
1286
1423
|
// src/storage.ts
|
|
1287
1424
|
import { createHash, randomUUID } from "node:crypto";
|
|
1288
1425
|
import {
|
|
@@ -1298,7 +1435,7 @@ import {
|
|
|
1298
1435
|
import { homedir } from "node:os";
|
|
1299
1436
|
import { dirname, isAbsolute as isAbsolute2, join as join2 } from "node:path";
|
|
1300
1437
|
import { fileURLToPath } from "node:url";
|
|
1301
|
-
var version = "0.1.0-alpha.
|
|
1438
|
+
var version = "0.1.0-alpha.1";
|
|
1302
1439
|
function dataDirectory() {
|
|
1303
1440
|
const base = process.platform === "win32" ? process.env.LOCALAPPDATA ?? join2(homedir(), "AppData", "Local") : process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
|
|
1304
1441
|
if (!isAbsolute2(base))
|
|
@@ -1483,49 +1620,981 @@ async function acquireLock(data) {
|
|
|
1483
1620
|
};
|
|
1484
1621
|
}
|
|
1485
1622
|
|
|
1623
|
+
// src/config.ts
|
|
1624
|
+
function configDirectory() {
|
|
1625
|
+
return join3(homedir2(), ".memroot");
|
|
1626
|
+
}
|
|
1627
|
+
function configPath() {
|
|
1628
|
+
return join3(configDirectory(), "config.json");
|
|
1629
|
+
}
|
|
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
|
+
function optionalString(value) {
|
|
1644
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1645
|
+
}
|
|
1646
|
+
function containsCredential(value) {
|
|
1647
|
+
if (!value || typeof value !== "object") return false;
|
|
1648
|
+
return Object.entries(value).some(
|
|
1649
|
+
([key, entry]) => /token|secret|password|credential|api[_-]?key/i.test(key) || containsCredential(entry)
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
async function migrateLegacy(record, raw) {
|
|
1653
|
+
if (!(typeof record.user_id === "string" || typeof record.email === "string") || containsCredential(record) || record.apiBase !== void 0 && (typeof record.apiBase !== "string" || !isAllowedApiBase(record.apiBase)))
|
|
1654
|
+
throw new Error(
|
|
1655
|
+
"INVALID_CONFIG: Legacy config cannot be safely migrated. No credentials were imported."
|
|
1656
|
+
);
|
|
1657
|
+
await safeDirectory(configDirectory());
|
|
1658
|
+
const backup = `${configPath()}.legacy-backup`;
|
|
1659
|
+
try {
|
|
1660
|
+
await writeFile2(backup, raw, { flag: "wx", mode: 384 });
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
if (error.code !== "EEXIST") throw error;
|
|
1663
|
+
const info = await lstat2(backup);
|
|
1664
|
+
if (!info.isFile() || info.isSymbolicLink() || await readFile2(backup, "utf8") !== raw)
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
"INVALID_CONFIG: Legacy backup differs or is unsafe. Original config was preserved."
|
|
1667
|
+
);
|
|
1668
|
+
await chmod(backup, 384);
|
|
1669
|
+
}
|
|
1670
|
+
const config = { schemaVersion: 1 };
|
|
1671
|
+
await writeConfig(config);
|
|
1672
|
+
return config;
|
|
1673
|
+
}
|
|
1674
|
+
async function readConfig() {
|
|
1675
|
+
const path = configPath();
|
|
1676
|
+
let raw;
|
|
1677
|
+
try {
|
|
1678
|
+
const info = await lstat2(path);
|
|
1679
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
1680
|
+
throw new Error("INVALID_CONFIG: Expected a regular config file.");
|
|
1681
|
+
raw = await readFile2(path, "utf8");
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
if (error.code === "ENOENT")
|
|
1684
|
+
return { schemaVersion: 1 };
|
|
1685
|
+
throw error;
|
|
1686
|
+
}
|
|
1687
|
+
let value;
|
|
1688
|
+
try {
|
|
1689
|
+
value = JSON.parse(raw);
|
|
1690
|
+
} catch {
|
|
1691
|
+
throw new Error(
|
|
1692
|
+
"INVALID_CONFIG: ~/.memroot/config.json is not valid JSON. Fix or remove it."
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1696
|
+
throw new Error("INVALID_CONFIG: Cannot safely read CLI config.");
|
|
1697
|
+
const record = value;
|
|
1698
|
+
if (record.schemaVersion === void 0) return migrateLegacy(record, raw);
|
|
1699
|
+
const apiBase = optionalString(record.apiBase);
|
|
1700
|
+
if (record.schemaVersion !== 1 || apiBase && !isAllowedApiBase(apiBase))
|
|
1701
|
+
throw new Error("INVALID_CONFIG: Cannot safely read CLI config.");
|
|
1702
|
+
const config = { schemaVersion: 1 };
|
|
1703
|
+
if (apiBase) config.apiBase = apiBase;
|
|
1704
|
+
const clientId = optionalString(record.clientId);
|
|
1705
|
+
if (clientId) config.clientId = clientId;
|
|
1706
|
+
const clientIdApiBase = optionalString(record.clientIdApiBase);
|
|
1707
|
+
if (clientIdApiBase) config.clientIdApiBase = clientIdApiBase;
|
|
1708
|
+
const userId = optionalString(record.userId);
|
|
1709
|
+
if (userId) config.userId = userId;
|
|
1710
|
+
return config;
|
|
1711
|
+
}
|
|
1712
|
+
async function writeConfig(config) {
|
|
1713
|
+
const directory = configDirectory();
|
|
1714
|
+
await safeDirectory(directory);
|
|
1715
|
+
const path = configPath();
|
|
1716
|
+
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
1717
|
+
try {
|
|
1718
|
+
await writeFile2(temporary, `${JSON.stringify(config, null, 2)}
|
|
1719
|
+
`, {
|
|
1720
|
+
flag: "wx",
|
|
1721
|
+
mode: 384
|
|
1722
|
+
});
|
|
1723
|
+
await rename2(temporary, path);
|
|
1724
|
+
await chmod(path, 384);
|
|
1725
|
+
} finally {
|
|
1726
|
+
await rm2(temporary, { force: true });
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// 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
|
+
var SERVICE = "memroot";
|
|
1742
|
+
var TOOL_TIMEOUT_MS = 15e3;
|
|
1743
|
+
function credentialStoreKind() {
|
|
1744
|
+
const override = process.env.MEMROOT_TEST_CREDENTIAL_STORE;
|
|
1745
|
+
if (override === "file" && process.env.NODE_ENV === "test")
|
|
1746
|
+
return "test-file";
|
|
1747
|
+
if (override)
|
|
1748
|
+
throw new Error(
|
|
1749
|
+
"INVALID_CREDENTIAL_STORE: MEMROOT_TEST_CREDENTIAL_STORE=file is honored only when NODE_ENV=test; refusing to file-back credentials."
|
|
1750
|
+
);
|
|
1751
|
+
return process.platform === "darwin" ? "macos-keychain" : "libsecret";
|
|
1752
|
+
}
|
|
1753
|
+
function unavailable(tool) {
|
|
1754
|
+
return new Error(
|
|
1755
|
+
`AUTH_KEYCHAIN_UNAVAILABLE: ${tool} is not installed, so there is no OS credential store for the Memroot refresh token. Tokens are never written to files; install ${tool === "security" ? "the macOS security tool" : "libsecret's secret-tool"} or sign in on a supported system.`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
function run(tool, args, input, allowNotFound = false) {
|
|
1759
|
+
const result = spawnSync3(tool, args, {
|
|
1760
|
+
encoding: "utf8",
|
|
1761
|
+
input,
|
|
1762
|
+
timeout: TOOL_TIMEOUT_MS,
|
|
1763
|
+
shell: false,
|
|
1764
|
+
maxBuffer: 1024 * 1024
|
|
1765
|
+
});
|
|
1766
|
+
if (result.error) {
|
|
1767
|
+
if (result.error.code === "ENOENT")
|
|
1768
|
+
throw unavailable(tool);
|
|
1769
|
+
throw new Error(
|
|
1770
|
+
`AUTH_KEYCHAIN_FAILED: ${tool} could not run (${result.error.code ?? "error"}).`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
if (allowNotFound) return result;
|
|
1774
|
+
if (result.status !== 0)
|
|
1775
|
+
throw new Error(
|
|
1776
|
+
`AUTH_KEYCHAIN_FAILED: ${tool} ${args[0] ?? ""} failed (${result.status ?? "signal"}). No credential was read or written.`
|
|
1777
|
+
);
|
|
1778
|
+
return result;
|
|
1779
|
+
}
|
|
1780
|
+
function quote(value) {
|
|
1781
|
+
return JSON.stringify(value);
|
|
1782
|
+
}
|
|
1783
|
+
function testStorePath() {
|
|
1784
|
+
return join4(configDirectory(), "test-credentials.json");
|
|
1785
|
+
}
|
|
1786
|
+
async function readTestStore() {
|
|
1787
|
+
try {
|
|
1788
|
+
const info = await lstat3(testStorePath());
|
|
1789
|
+
if (!info.isFile() || info.isSymbolicLink())
|
|
1790
|
+
throw new Error("INVALID_CREDENTIAL_STORE: Expected a regular file.");
|
|
1791
|
+
const value = JSON.parse(await readFile3(testStorePath(), "utf8"));
|
|
1792
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1793
|
+
return value;
|
|
1794
|
+
} catch (error) {
|
|
1795
|
+
if (error.code === "ENOENT") return {};
|
|
1796
|
+
throw error;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
async function writeTestStore(entries) {
|
|
1800
|
+
const directory = configDirectory();
|
|
1801
|
+
await safeDirectory(directory);
|
|
1802
|
+
const path = testStorePath();
|
|
1803
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
1804
|
+
try {
|
|
1805
|
+
await writeFile3(temporary, `${JSON.stringify(entries, null, 2)}
|
|
1806
|
+
`, {
|
|
1807
|
+
flag: "wx",
|
|
1808
|
+
mode: 384
|
|
1809
|
+
});
|
|
1810
|
+
await rename3(temporary, path);
|
|
1811
|
+
await chmod2(path, 384);
|
|
1812
|
+
} finally {
|
|
1813
|
+
await rm3(temporary, { force: true });
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function assertCredentialStoreAvailable() {
|
|
1817
|
+
const kind = credentialStoreKind();
|
|
1818
|
+
if (kind === "test-file") return;
|
|
1819
|
+
if (kind === "macos-keychain")
|
|
1820
|
+
run(
|
|
1821
|
+
"security",
|
|
1822
|
+
["find-generic-password", "-s", SERVICE, "-a", "memroot:probe"],
|
|
1823
|
+
void 0,
|
|
1824
|
+
true
|
|
1825
|
+
);
|
|
1826
|
+
else
|
|
1827
|
+
run(
|
|
1828
|
+
"secret-tool",
|
|
1829
|
+
["lookup", "service", SERVICE, "account", "memroot:probe"],
|
|
1830
|
+
void 0,
|
|
1831
|
+
true
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
async function storeSecret(account, secret) {
|
|
1835
|
+
const kind = credentialStoreKind();
|
|
1836
|
+
if (kind === "test-file") {
|
|
1837
|
+
const entries = await readTestStore();
|
|
1838
|
+
entries[account] = secret;
|
|
1839
|
+
await writeTestStore(entries);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
if (kind === "macos-keychain") {
|
|
1843
|
+
run(
|
|
1844
|
+
"security",
|
|
1845
|
+
["-i"],
|
|
1846
|
+
`add-generic-password -U -s ${quote(SERVICE)} -a ${quote(account)} -w ${quote(secret)}
|
|
1847
|
+
`
|
|
1848
|
+
);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
run(
|
|
1852
|
+
"secret-tool",
|
|
1853
|
+
["store", "--label", "Memroot CLI", "service", SERVICE, "account", account],
|
|
1854
|
+
secret
|
|
1855
|
+
);
|
|
1856
|
+
}
|
|
1857
|
+
async function readSecret(account) {
|
|
1858
|
+
const kind = credentialStoreKind();
|
|
1859
|
+
if (kind === "test-file") return (await readTestStore())[account];
|
|
1860
|
+
if (kind === "macos-keychain") {
|
|
1861
|
+
const result2 = run(
|
|
1862
|
+
"security",
|
|
1863
|
+
["find-generic-password", "-s", SERVICE, "-a", account, "-w"],
|
|
1864
|
+
void 0,
|
|
1865
|
+
true
|
|
1866
|
+
);
|
|
1867
|
+
if (result2.status !== 0) {
|
|
1868
|
+
if (/could not be found|item not found/i.test(result2.stderr))
|
|
1869
|
+
return void 0;
|
|
1870
|
+
throw new Error(
|
|
1871
|
+
`AUTH_KEYCHAIN_FAILED: security find-generic-password failed (${result2.status ?? "signal"}).`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
const secret2 = result2.stdout.replace(/\n$/, "");
|
|
1875
|
+
return secret2.length ? secret2 : void 0;
|
|
1876
|
+
}
|
|
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
|
+
}
|
|
1887
|
+
async function deleteSecret(account) {
|
|
1888
|
+
const kind = credentialStoreKind();
|
|
1889
|
+
if (kind === "test-file") {
|
|
1890
|
+
const entries = await readTestStore();
|
|
1891
|
+
if (account in entries) {
|
|
1892
|
+
delete entries[account];
|
|
1893
|
+
await writeTestStore(entries);
|
|
1894
|
+
}
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
if (kind === "macos-keychain") {
|
|
1898
|
+
const result = run(
|
|
1899
|
+
"security",
|
|
1900
|
+
["delete-generic-password", "-s", SERVICE, "-a", account],
|
|
1901
|
+
void 0,
|
|
1902
|
+
true
|
|
1903
|
+
);
|
|
1904
|
+
if (result.status !== 0 && !/could not be found|item not found/i.test(result.stderr))
|
|
1905
|
+
throw new Error(
|
|
1906
|
+
`AUTH_KEYCHAIN_FAILED: security delete-generic-password failed (${result.status ?? "signal"}).`
|
|
1907
|
+
);
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
run(
|
|
1911
|
+
"secret-tool",
|
|
1912
|
+
["clear", "service", SERVICE, "account", account],
|
|
1913
|
+
void 0,
|
|
1914
|
+
true
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// src/auth.ts
|
|
1919
|
+
var PROD_API_BASE = "https://api.memroot.dev";
|
|
1920
|
+
var DEV_API_BASE = "http://localhost:8787";
|
|
1921
|
+
var STATIC_CLIENT_ID = "memroot-cli";
|
|
1922
|
+
var CALLBACK_PATH = "/callback";
|
|
1923
|
+
var LOGIN_TIMEOUT_MS = 18e4;
|
|
1924
|
+
var REFRESH_SKEW_MS = 3e4;
|
|
1925
|
+
var REQUESTED_SCOPE = "memory.read memory.write";
|
|
1926
|
+
var OAuthError = class extends Error {
|
|
1927
|
+
constructor(oauth, message) {
|
|
1928
|
+
super(message);
|
|
1929
|
+
this.oauth = oauth;
|
|
1930
|
+
}
|
|
1931
|
+
oauth;
|
|
1932
|
+
};
|
|
1933
|
+
function resolveApiBase(options, config) {
|
|
1934
|
+
const base = options.apiBase ?? process.env.MEMROOT_API_BASE ?? (options.env ? options.env === "dev" ? DEV_API_BASE : PROD_API_BASE : void 0) ?? config.apiBase ?? PROD_API_BASE;
|
|
1935
|
+
const normalized = base.replace(/\/+$/, "");
|
|
1936
|
+
if (!isAllowedApiBase(normalized))
|
|
1937
|
+
throw new Error(
|
|
1938
|
+
"INSECURE_API_BASE: The API base must use https, or http only on a loopback address for local development."
|
|
1939
|
+
);
|
|
1940
|
+
return normalized;
|
|
1941
|
+
}
|
|
1942
|
+
function decodeClaims(token) {
|
|
1943
|
+
const parts = token.split(".");
|
|
1944
|
+
if (parts.length !== 3) return void 0;
|
|
1945
|
+
try {
|
|
1946
|
+
const claims = JSON.parse(
|
|
1947
|
+
Buffer.from(parts[1] ?? "", "base64url").toString("utf8")
|
|
1948
|
+
);
|
|
1949
|
+
if (!claims || typeof claims !== "object" || Array.isArray(claims))
|
|
1950
|
+
return void 0;
|
|
1951
|
+
return claims;
|
|
1952
|
+
} catch {
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function claimsExpiry(claims) {
|
|
1957
|
+
return typeof claims?.exp === "number" ? claims.exp * 1e3 : 0;
|
|
1958
|
+
}
|
|
1959
|
+
function parseTokenSet(body) {
|
|
1960
|
+
if (typeof body.access_token !== "string" || !body.access_token)
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
"AUTH_RESPONSE_INVALID: The token endpoint returned no access token."
|
|
1963
|
+
);
|
|
1964
|
+
const tokens = { accessToken: body.access_token };
|
|
1965
|
+
if (typeof body.refresh_token === "string" && body.refresh_token)
|
|
1966
|
+
tokens.refreshToken = body.refresh_token;
|
|
1967
|
+
if (typeof body.expires_in === "number") tokens.expiresIn = body.expires_in;
|
|
1968
|
+
return tokens;
|
|
1969
|
+
}
|
|
1970
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
1971
|
+
function request(target, method, contentType, payload) {
|
|
1972
|
+
const transport = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
1973
|
+
const headers = {
|
|
1974
|
+
accept: "application/json"
|
|
1975
|
+
};
|
|
1976
|
+
if (contentType) headers["content-type"] = contentType;
|
|
1977
|
+
if (payload !== void 0)
|
|
1978
|
+
headers["content-length"] = Buffer.byteLength(payload);
|
|
1979
|
+
return new Promise((resolve, reject) => {
|
|
1980
|
+
const request2 = transport(
|
|
1981
|
+
target,
|
|
1982
|
+
{
|
|
1983
|
+
method,
|
|
1984
|
+
agent: false,
|
|
1985
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
1986
|
+
headers
|
|
1987
|
+
},
|
|
1988
|
+
(response) => {
|
|
1989
|
+
let body = "";
|
|
1990
|
+
response.setEncoding("utf8");
|
|
1991
|
+
response.on("data", (chunk) => {
|
|
1992
|
+
body += chunk;
|
|
1993
|
+
});
|
|
1994
|
+
response.on(
|
|
1995
|
+
"end",
|
|
1996
|
+
() => resolve({ status: response.statusCode ?? 0, body })
|
|
1997
|
+
);
|
|
1998
|
+
}
|
|
1999
|
+
);
|
|
2000
|
+
request2.on("timeout", () => request2.destroy(new Error("request timeout")));
|
|
2001
|
+
request2.on(
|
|
2002
|
+
"error",
|
|
2003
|
+
() => reject(
|
|
2004
|
+
new Error(
|
|
2005
|
+
`AUTH_UNREACHABLE: Cannot reach ${target.origin}. Check the network connection and API base URL.`
|
|
2006
|
+
)
|
|
2007
|
+
)
|
|
2008
|
+
);
|
|
2009
|
+
request2.end(payload);
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
async function getJson(url) {
|
|
2013
|
+
const response = await request(new URL(url), "GET");
|
|
2014
|
+
if (response.status >= 400)
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
`AUTH_METADATA_FAILED: ${url} returned ${response.status}.`
|
|
2017
|
+
);
|
|
2018
|
+
return parseJsonBody(response.body);
|
|
2019
|
+
}
|
|
2020
|
+
function parseJsonBody(text) {
|
|
2021
|
+
let body;
|
|
2022
|
+
try {
|
|
2023
|
+
body = text ? JSON.parse(text) : {};
|
|
2024
|
+
} catch {
|
|
2025
|
+
body = void 0;
|
|
2026
|
+
}
|
|
2027
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
2028
|
+
throw new Error(
|
|
2029
|
+
"AUTH_RESPONSE_INVALID: The server returned a malformed response."
|
|
2030
|
+
);
|
|
2031
|
+
return body;
|
|
2032
|
+
}
|
|
2033
|
+
function oauthFailure(record, status2) {
|
|
2034
|
+
const code = typeof record.error === "string" ? record.error : `http_${status2}`;
|
|
2035
|
+
const description = typeof record.error_description === "string" ? ` ${record.error_description}` : "";
|
|
2036
|
+
return new OAuthError(code, `AUTH_TOKEN_REJECTED: ${code}.${description}`);
|
|
2037
|
+
}
|
|
2038
|
+
async function postForm(apiBase, path, params) {
|
|
2039
|
+
const response = await request(
|
|
2040
|
+
new URL(`${apiBase}${path}`),
|
|
2041
|
+
"POST",
|
|
2042
|
+
"application/x-www-form-urlencoded",
|
|
2043
|
+
new URLSearchParams(params).toString()
|
|
2044
|
+
);
|
|
2045
|
+
let record = {};
|
|
2046
|
+
try {
|
|
2047
|
+
record = response.body ? parseJsonBody(response.body) : {};
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
if (response.status < 400) throw error;
|
|
2050
|
+
}
|
|
2051
|
+
if (response.status >= 400) throw oauthFailure(record, response.status);
|
|
2052
|
+
return parseJsonBody(response.body);
|
|
2053
|
+
}
|
|
2054
|
+
async function registerClient(apiBase, redirectUri) {
|
|
2055
|
+
const response = await request(
|
|
2056
|
+
new URL(`${apiBase}/oauth/register`),
|
|
2057
|
+
"POST",
|
|
2058
|
+
"application/json",
|
|
2059
|
+
JSON.stringify({
|
|
2060
|
+
client_name: "Memroot CLI",
|
|
2061
|
+
redirect_uris: [redirectUri],
|
|
2062
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
2063
|
+
response_types: ["code"],
|
|
2064
|
+
token_endpoint_auth_method: "none",
|
|
2065
|
+
scope: REQUESTED_SCOPE
|
|
2066
|
+
})
|
|
2067
|
+
);
|
|
2068
|
+
if (response.status === 404 || response.status === 405 || response.status === 501)
|
|
2069
|
+
return void 0;
|
|
2070
|
+
let record = {};
|
|
2071
|
+
try {
|
|
2072
|
+
record = parseJsonBody(response.body);
|
|
2073
|
+
} catch {
|
|
2074
|
+
record = {};
|
|
2075
|
+
}
|
|
2076
|
+
if (response.status >= 400) {
|
|
2077
|
+
const code = typeof record.error === "string" ? record.error : `http_${response.status}`;
|
|
2078
|
+
throw new OAuthError(
|
|
2079
|
+
code,
|
|
2080
|
+
`AUTH_REGISTRATION_FAILED: Client registration failed (${code}).`
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
if (typeof record.client_id !== "string" || !record.client_id)
|
|
2084
|
+
throw new Error(
|
|
2085
|
+
"AUTH_RESPONSE_INVALID: Client registration returned no client_id."
|
|
2086
|
+
);
|
|
2087
|
+
return record.client_id;
|
|
2088
|
+
}
|
|
2089
|
+
async function exchangeCode(apiBase, clientId, redirectUri, code, verifier) {
|
|
2090
|
+
return parseTokenSet(
|
|
2091
|
+
await postForm(apiBase, "/oauth/token", {
|
|
2092
|
+
grant_type: "authorization_code",
|
|
2093
|
+
code,
|
|
2094
|
+
redirect_uri: redirectUri,
|
|
2095
|
+
client_id: clientId,
|
|
2096
|
+
code_verifier: verifier
|
|
2097
|
+
})
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
async function revokeRefreshToken(apiBase, refreshToken, clientId) {
|
|
2101
|
+
const metadata = await getJson(
|
|
2102
|
+
`${apiBase}/.well-known/oauth-authorization-server`
|
|
2103
|
+
);
|
|
2104
|
+
const endpoint = metadata.revocation_endpoint;
|
|
2105
|
+
if (typeof endpoint !== "string" || !endpoint || !isAllowedApiBase(endpoint))
|
|
2106
|
+
throw new Error(
|
|
2107
|
+
"AUTH_REVOKE_UNSUPPORTED: The server does not advertise a usable revocation endpoint."
|
|
2108
|
+
);
|
|
2109
|
+
const response = await request(
|
|
2110
|
+
new URL(endpoint),
|
|
2111
|
+
"POST",
|
|
2112
|
+
"application/x-www-form-urlencoded",
|
|
2113
|
+
new URLSearchParams({
|
|
2114
|
+
token: refreshToken,
|
|
2115
|
+
token_type_hint: "refresh_token",
|
|
2116
|
+
client_id: clientId
|
|
2117
|
+
}).toString()
|
|
2118
|
+
);
|
|
2119
|
+
if (response.status >= 400)
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
`AUTH_REVOKE_FAILED: Revocation returned ${response.status}.`
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
async function refreshTokens(apiBase, refreshToken, clientId) {
|
|
2125
|
+
try {
|
|
2126
|
+
return parseTokenSet(
|
|
2127
|
+
await postForm(apiBase, "/oauth/token", {
|
|
2128
|
+
grant_type: "refresh_token",
|
|
2129
|
+
refresh_token: refreshToken,
|
|
2130
|
+
client_id: clientId
|
|
2131
|
+
})
|
|
2132
|
+
);
|
|
2133
|
+
} catch (error) {
|
|
2134
|
+
if (error instanceof OAuthError && error.oauth === "invalid_grant")
|
|
2135
|
+
throw new OAuthError(
|
|
2136
|
+
"invalid_grant",
|
|
2137
|
+
"AUTH_EXPIRED: Stored credentials were rejected or expired. Run memroot login."
|
|
2138
|
+
);
|
|
2139
|
+
if (error instanceof OAuthError && error.oauth === "invalid_client")
|
|
2140
|
+
throw new OAuthError(
|
|
2141
|
+
"invalid_client",
|
|
2142
|
+
"AUTH_INVALID_CLIENT: The stored client registration was rejected. Run memroot login."
|
|
2143
|
+
);
|
|
2144
|
+
throw error;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function page(title, detail) {
|
|
2148
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>Memroot \u2014 ${title}</title></head><body><h1>${title}</h1><p>${detail}</p></body></html>`;
|
|
2149
|
+
}
|
|
2150
|
+
function startCallbackServer(expectedState, apiBase) {
|
|
2151
|
+
let failure;
|
|
2152
|
+
let timer;
|
|
2153
|
+
const pending = [];
|
|
2154
|
+
const waiters = [];
|
|
2155
|
+
const deliver = (result) => {
|
|
2156
|
+
const waiter = waiters.shift();
|
|
2157
|
+
if (waiter) waiter.resolve(result);
|
|
2158
|
+
else pending.push(result);
|
|
2159
|
+
};
|
|
2160
|
+
const server = createServer((request2, response) => {
|
|
2161
|
+
const url = new URL(request2.url ?? "/", "http://127.0.0.1");
|
|
2162
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
2163
|
+
response.writeHead(404).end("Not found");
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const errorParam = url.searchParams.get("error");
|
|
2167
|
+
if (errorParam) {
|
|
2168
|
+
const description = url.searchParams.get("error_description");
|
|
2169
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(page("Sign-in failed", "Return to the terminal for details."));
|
|
2170
|
+
deliver({
|
|
2171
|
+
kind: "error",
|
|
2172
|
+
error: errorParam,
|
|
2173
|
+
...typeof description === "string" ? { description } : {}
|
|
2174
|
+
});
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const code = url.searchParams.get("code");
|
|
2178
|
+
const state = url.searchParams.get("state");
|
|
2179
|
+
if (!code || !state) {
|
|
2180
|
+
response.writeHead(400).end("Missing code or state.");
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (state !== expectedState) {
|
|
2184
|
+
response.writeHead(400).end("State mismatch; rejected.");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const issuer = url.searchParams.get("iss");
|
|
2188
|
+
const issuerOrigin = (() => {
|
|
2189
|
+
try {
|
|
2190
|
+
return issuer ? new URL(issuer).origin : void 0;
|
|
2191
|
+
} catch {
|
|
2192
|
+
return void 0;
|
|
2193
|
+
}
|
|
2194
|
+
})();
|
|
2195
|
+
if (!issuer || !issuerOrigin) {
|
|
2196
|
+
response.writeHead(400).end("Missing or malformed issuer; rejected.");
|
|
2197
|
+
deliver({ kind: "error", error: "iss_missing" });
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (issuerOrigin !== new URL(apiBase).origin) {
|
|
2201
|
+
response.writeHead(400).end("Issuer mismatch; rejected.");
|
|
2202
|
+
deliver({ kind: "error", error: "iss_mismatch" });
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(
|
|
2206
|
+
page(
|
|
2207
|
+
"Sign-in complete",
|
|
2208
|
+
"You can close this tab and return to the terminal."
|
|
2209
|
+
)
|
|
2210
|
+
);
|
|
2211
|
+
deliver({ kind: "code", code });
|
|
2212
|
+
});
|
|
2213
|
+
const fail = (error) => {
|
|
2214
|
+
if (failure) return;
|
|
2215
|
+
failure = error;
|
|
2216
|
+
if (timer) clearTimeout(timer);
|
|
2217
|
+
server.close();
|
|
2218
|
+
server.closeAllConnections();
|
|
2219
|
+
for (const waiter of waiters.splice(0)) waiter.reject(error);
|
|
2220
|
+
};
|
|
2221
|
+
timer = setTimeout(
|
|
2222
|
+
() => fail(
|
|
2223
|
+
new Error(
|
|
2224
|
+
"AUTH_TIMEOUT: Timed out waiting for the browser sign-in callback. Retry memroot login."
|
|
2225
|
+
)
|
|
2226
|
+
),
|
|
2227
|
+
LOGIN_TIMEOUT_MS
|
|
2228
|
+
);
|
|
2229
|
+
return new Promise((resolve, reject) => {
|
|
2230
|
+
server.once("error", (error) => {
|
|
2231
|
+
fail(error);
|
|
2232
|
+
reject(error);
|
|
2233
|
+
});
|
|
2234
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2235
|
+
const address = server.address();
|
|
2236
|
+
resolve({
|
|
2237
|
+
port: address.port,
|
|
2238
|
+
next: () => {
|
|
2239
|
+
if (failure) return Promise.reject(failure);
|
|
2240
|
+
const queued = pending.shift();
|
|
2241
|
+
if (queued) return Promise.resolve(queued);
|
|
2242
|
+
const result = new Promise(
|
|
2243
|
+
(resolve2, rejectPromise) => waiters.push({ resolve: resolve2, reject: rejectPromise })
|
|
2244
|
+
);
|
|
2245
|
+
result.catch(() => {
|
|
2246
|
+
});
|
|
2247
|
+
return result;
|
|
2248
|
+
},
|
|
2249
|
+
close: () => fail(new Error("AUTH_ABORTED: Sign-in was aborted."))
|
|
2250
|
+
});
|
|
2251
|
+
});
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
function openBrowser(url) {
|
|
2255
|
+
const tool = process.platform === "darwin" ? "open" : "xdg-open";
|
|
2256
|
+
try {
|
|
2257
|
+
const child = spawn(tool, [url], {
|
|
2258
|
+
detached: true,
|
|
2259
|
+
stdio: "ignore",
|
|
2260
|
+
shell: false
|
|
2261
|
+
});
|
|
2262
|
+
child.on("error", () => {
|
|
2263
|
+
});
|
|
2264
|
+
child.unref();
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
var cachedAccessToken;
|
|
2269
|
+
async function getAccessToken(options = {}) {
|
|
2270
|
+
if (cachedAccessToken && cachedAccessToken.expiresAt - Date.now() > REFRESH_SKEW_MS)
|
|
2271
|
+
return cachedAccessToken.token;
|
|
2272
|
+
const config = await readConfig();
|
|
2273
|
+
const apiBase = resolveApiBase(options, config);
|
|
2274
|
+
const stored = await readSecret(apiBase);
|
|
2275
|
+
if (!stored)
|
|
2276
|
+
throw new Error(
|
|
2277
|
+
"AUTH_REQUIRED: Not signed in. Run memroot login to connect your account."
|
|
2278
|
+
);
|
|
2279
|
+
const clientId = config.clientId && config.clientIdApiBase === apiBase ? config.clientId : STATIC_CLIENT_ID;
|
|
2280
|
+
const tokens = await refreshTokens(apiBase, stored, clientId);
|
|
2281
|
+
if (tokens.refreshToken && tokens.refreshToken !== stored)
|
|
2282
|
+
await storeSecret(apiBase, tokens.refreshToken);
|
|
2283
|
+
const claims = decodeClaims(tokens.accessToken);
|
|
2284
|
+
cachedAccessToken = {
|
|
2285
|
+
token: tokens.accessToken,
|
|
2286
|
+
expiresAt: claimsExpiry(claims) || Date.now() + (tokens.expiresIn ?? 0) * 1e3
|
|
2287
|
+
};
|
|
2288
|
+
if (typeof claims?.sub === "string" && claims.sub !== config.userId) {
|
|
2289
|
+
config.userId = claims.sub;
|
|
2290
|
+
await writeConfig(config);
|
|
2291
|
+
}
|
|
2292
|
+
return tokens.accessToken;
|
|
2293
|
+
}
|
|
2294
|
+
async function tryExistingSession(apiBase, config) {
|
|
2295
|
+
const stored = await readSecret(apiBase);
|
|
2296
|
+
if (!stored) return false;
|
|
2297
|
+
const clientId = config.clientId && config.clientIdApiBase === apiBase ? config.clientId : STATIC_CLIENT_ID;
|
|
2298
|
+
try {
|
|
2299
|
+
const tokens = await refreshTokens(apiBase, stored, clientId);
|
|
2300
|
+
if (tokens.refreshToken && tokens.refreshToken !== stored)
|
|
2301
|
+
await storeSecret(apiBase, tokens.refreshToken);
|
|
2302
|
+
const sub = decodeClaims(tokens.accessToken)?.sub;
|
|
2303
|
+
console.log(
|
|
2304
|
+
`Already signed in to ${apiBase}${typeof sub === "string" ? ` as ${sub}` : ""}. Use --force to re-authenticate.`
|
|
2305
|
+
);
|
|
2306
|
+
return true;
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
if (error instanceof OAuthError && (error.oauth === "invalid_grant" || error.oauth === "invalid_client")) {
|
|
2309
|
+
await deleteSecret(apiBase).catch(() => {
|
|
2310
|
+
});
|
|
2311
|
+
return false;
|
|
2312
|
+
}
|
|
2313
|
+
throw error;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
async function login(options) {
|
|
2317
|
+
if (process.platform !== "darwin" && process.platform !== "linux")
|
|
2318
|
+
throw new Error(
|
|
2319
|
+
"UNSUPPORTED_PLATFORM: memroot login supports macOS and Linux."
|
|
2320
|
+
);
|
|
2321
|
+
assertCredentialStoreAvailable();
|
|
2322
|
+
const config = await readConfig();
|
|
2323
|
+
const apiBase = resolveApiBase(options, config);
|
|
2324
|
+
if (!options.force && await tryExistingSession(apiBase, config)) return;
|
|
2325
|
+
const state = randomBytes(24).toString("base64url");
|
|
2326
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
2327
|
+
const challenge = createHash2("sha256").update(verifier).digest("base64url");
|
|
2328
|
+
const waiter = await startCallbackServer(state, apiBase);
|
|
2329
|
+
try {
|
|
2330
|
+
const redirectUri = `http://127.0.0.1:${waiter.port}${CALLBACK_PATH}`;
|
|
2331
|
+
let clientId = config.clientId && config.clientIdApiBase === apiBase ? config.clientId : STATIC_CLIENT_ID;
|
|
2332
|
+
let retriedWithRegistration = false;
|
|
2333
|
+
for (; ; ) {
|
|
2334
|
+
const authorize = new URL(`${apiBase}/authorize`);
|
|
2335
|
+
authorize.searchParams.set("response_type", "code");
|
|
2336
|
+
authorize.searchParams.set("client_id", clientId);
|
|
2337
|
+
authorize.searchParams.set("redirect_uri", redirectUri);
|
|
2338
|
+
authorize.searchParams.set("state", state);
|
|
2339
|
+
authorize.searchParams.set("code_challenge", challenge);
|
|
2340
|
+
authorize.searchParams.set("code_challenge_method", "S256");
|
|
2341
|
+
authorize.searchParams.set("scope", REQUESTED_SCOPE);
|
|
2342
|
+
const url = authorize.toString();
|
|
2343
|
+
if (options.noBrowser) console.log(`Open this URL to sign in:
|
|
2344
|
+
${url}`);
|
|
2345
|
+
else {
|
|
2346
|
+
openBrowser(url);
|
|
2347
|
+
console.log(
|
|
2348
|
+
`Opening ${apiBase} for sign-in. If the browser did not open, visit:
|
|
2349
|
+
${url}`
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
console.log("Waiting for the browser sign-in callback\u2026");
|
|
2353
|
+
const result = await waiter.next();
|
|
2354
|
+
if (result.kind === "code") {
|
|
2355
|
+
const tokens = await exchangeCode(
|
|
2356
|
+
apiBase,
|
|
2357
|
+
clientId,
|
|
2358
|
+
redirectUri,
|
|
2359
|
+
result.code,
|
|
2360
|
+
verifier
|
|
2361
|
+
);
|
|
2362
|
+
if (!tokens.refreshToken)
|
|
2363
|
+
throw new Error(
|
|
2364
|
+
"AUTH_RESPONSE_INVALID: The token endpoint returned no refresh token."
|
|
2365
|
+
);
|
|
2366
|
+
await storeSecret(apiBase, tokens.refreshToken);
|
|
2367
|
+
const claims = decodeClaims(tokens.accessToken);
|
|
2368
|
+
const next = {
|
|
2369
|
+
schemaVersion: 1,
|
|
2370
|
+
apiBase,
|
|
2371
|
+
clientId,
|
|
2372
|
+
clientIdApiBase: apiBase
|
|
2373
|
+
};
|
|
2374
|
+
if (typeof claims?.sub === "string") next.userId = claims.sub;
|
|
2375
|
+
await writeConfig(next);
|
|
2376
|
+
console.log(
|
|
2377
|
+
`Signed in to ${apiBase}${next.userId ? ` as ${next.userId}` : ""}.`
|
|
2378
|
+
);
|
|
2379
|
+
console.log(
|
|
2380
|
+
"Refresh token stored in the OS credential store; no tokens were written to files."
|
|
2381
|
+
);
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
if ((result.error === "invalid_client" || result.error === "unauthorized_client") && !retriedWithRegistration) {
|
|
2385
|
+
retriedWithRegistration = true;
|
|
2386
|
+
const registered = await registerClient(apiBase, redirectUri);
|
|
2387
|
+
if (registered && registered !== clientId) {
|
|
2388
|
+
clientId = registered;
|
|
2389
|
+
console.log(
|
|
2390
|
+
"Client was not recognized; registered a fresh client and retrying sign-in."
|
|
2391
|
+
);
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (result.error === "iss_missing" || result.error === "iss_mismatch")
|
|
2396
|
+
throw new Error(
|
|
2397
|
+
"AUTH_ISS_MISMATCH: The callback issuer is missing or does not match the API base. Retry memroot login."
|
|
2398
|
+
);
|
|
2399
|
+
throw new OAuthError(
|
|
2400
|
+
result.error,
|
|
2401
|
+
`AUTH_AUTHORIZATION_FAILED: ${result.error}${result.description ? ` \u2014 ${result.description}` : ""}`
|
|
2402
|
+
);
|
|
2403
|
+
}
|
|
2404
|
+
} finally {
|
|
2405
|
+
waiter.close();
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
async function logout(options) {
|
|
2409
|
+
const config = await readConfig();
|
|
2410
|
+
const apiBase = resolveApiBase(options, config);
|
|
2411
|
+
const stored = await readSecret(apiBase).catch(() => void 0);
|
|
2412
|
+
if (stored) {
|
|
2413
|
+
const clientId = config.clientId && config.clientIdApiBase === apiBase ? config.clientId : STATIC_CLIENT_ID;
|
|
2414
|
+
try {
|
|
2415
|
+
await revokeRefreshToken(apiBase, stored, clientId);
|
|
2416
|
+
} catch (error) {
|
|
2417
|
+
console.error(
|
|
2418
|
+
`AUTH_REVOKE_FAILED: ${error instanceof Error ? error.message : "revocation failed"} Removing local credentials anyway.`
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
try {
|
|
2423
|
+
await deleteSecret(apiBase);
|
|
2424
|
+
} catch (error) {
|
|
2425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2426
|
+
if (!message.startsWith("AUTH_KEYCHAIN_UNAVAILABLE")) throw error;
|
|
2427
|
+
console.error(message);
|
|
2428
|
+
}
|
|
2429
|
+
cachedAccessToken = void 0;
|
|
2430
|
+
const next = { schemaVersion: 1 };
|
|
2431
|
+
if (config.apiBase) next.apiBase = config.apiBase;
|
|
2432
|
+
await writeConfig(next);
|
|
2433
|
+
console.log("Signed out. Stored credentials and client state were removed.");
|
|
2434
|
+
}
|
|
2435
|
+
async function whoami(options) {
|
|
2436
|
+
const config = await readConfig();
|
|
2437
|
+
const apiBase = resolveApiBase(options, config);
|
|
2438
|
+
const token = await getAccessToken({ ...options, apiBase });
|
|
2439
|
+
const claims = decodeClaims(token);
|
|
2440
|
+
if (!claims)
|
|
2441
|
+
throw new Error(
|
|
2442
|
+
"AUTH_RESPONSE_INVALID: Could not decode the access token claims."
|
|
2443
|
+
);
|
|
2444
|
+
console.log(`Signed in to ${apiBase}`);
|
|
2445
|
+
console.log(
|
|
2446
|
+
` sub: ${typeof claims.sub === "string" ? claims.sub : "unknown"}`
|
|
2447
|
+
);
|
|
2448
|
+
console.log(
|
|
2449
|
+
` iss: ${typeof claims.iss === "string" ? claims.iss : "unknown"}`
|
|
2450
|
+
);
|
|
2451
|
+
console.log(
|
|
2452
|
+
` exp: ${claimsExpiry(claims) ? new Date(claimsExpiry(claims)).toISOString() : "unknown"}`
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
async function printAccessToken(options) {
|
|
2456
|
+
console.log(await getAccessToken(options));
|
|
2457
|
+
}
|
|
2458
|
+
async function status(options) {
|
|
2459
|
+
const config = await readConfig();
|
|
2460
|
+
const apiBase = resolveApiBase(options, config);
|
|
2461
|
+
console.log(`API base: ${apiBase}`);
|
|
2462
|
+
console.log(`Credential store: ${credentialStoreKind()}`);
|
|
2463
|
+
try {
|
|
2464
|
+
const token = await getAccessToken({ ...options, apiBase });
|
|
2465
|
+
const claims = decodeClaims(token);
|
|
2466
|
+
const subject = typeof claims?.sub === "string" ? claims.sub : config.userId ?? "unknown";
|
|
2467
|
+
const expiry = claimsExpiry(claims) ? `; access token valid until ${new Date(claimsExpiry(claims)).toISOString()}` : "";
|
|
2468
|
+
console.log(`Cloud auth: signed in as ${subject}${expiry}`);
|
|
2469
|
+
} catch (error) {
|
|
2470
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2471
|
+
if (message.startsWith("AUTH_REQUIRED"))
|
|
2472
|
+
console.log("Cloud auth: not signed in. Run memroot login.");
|
|
2473
|
+
else if (message.startsWith("AUTH_EXPIRED"))
|
|
2474
|
+
console.log(
|
|
2475
|
+
"Cloud auth: stored credentials were rejected. Run memroot login."
|
|
2476
|
+
);
|
|
2477
|
+
else console.log(`Cloud auth: ${message}`);
|
|
2478
|
+
}
|
|
2479
|
+
const data = dataDirectory();
|
|
2480
|
+
console.log("Agents:");
|
|
2481
|
+
for (const agent of agents) {
|
|
2482
|
+
const path = executable(agent);
|
|
2483
|
+
let detail = "no plugin receipt";
|
|
2484
|
+
try {
|
|
2485
|
+
const receipt = await readReceipt(data, agent);
|
|
2486
|
+
if (receipt) detail = `plugin installed (${receipt.version})`;
|
|
2487
|
+
} catch {
|
|
2488
|
+
detail = "plugin receipt unreadable";
|
|
2489
|
+
}
|
|
2490
|
+
console.log(
|
|
2491
|
+
` ${labels[agent]}: ${path ? "CLI detected" : "CLI not found"}; ${detail}`
|
|
2492
|
+
);
|
|
2493
|
+
if (agent === "codex" && path) {
|
|
2494
|
+
try {
|
|
2495
|
+
console.log(` ${describeCodexMcp(inspectCodexMcp(path))}`);
|
|
2496
|
+
} catch {
|
|
2497
|
+
console.log(
|
|
2498
|
+
" Codex MCP: status unavailable; memory verification: not verified."
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
1486
2505
|
// src/index.ts
|
|
1487
2506
|
var installationDocs = {
|
|
1488
2507
|
claude: "https://code.claude.com/docs/en/quickstart",
|
|
1489
2508
|
codex: "https://developers.openai.com/codex/cli",
|
|
1490
2509
|
grok: "https://docs.x.ai/build/overview"
|
|
1491
2510
|
};
|
|
1492
|
-
var setupComplete = "Plugins ready.
|
|
1493
|
-
var cloudStatus = "Cloud memory:
|
|
1494
|
-
var help = `Memroot ${version} \u2014 native agent plugin installer
|
|
2511
|
+
var setupComplete = "Plugins ready. Codex uses native MCP OAuth; other agent adapters use memroot login.";
|
|
2512
|
+
var cloudStatus = "Cloud memory: Codex setup links production MCP through its browser sign-in. Other adapters use memroot login. Automatic session upload is not available.";
|
|
2513
|
+
var help = `Memroot ${version} \u2014 native agent plugin installer and cloud sign-in
|
|
1495
2514
|
|
|
1496
2515
|
Usage:
|
|
1497
2516
|
npx memroot setup
|
|
1498
2517
|
memroot setup --agent codex --yes
|
|
1499
2518
|
memroot setup --agent claude --agent grok --yes --dry-run
|
|
2519
|
+
memroot login
|
|
1500
2520
|
memroot doctor
|
|
2521
|
+
memroot status
|
|
1501
2522
|
|
|
1502
2523
|
Commands:
|
|
1503
2524
|
setup Install native plugins for your user account
|
|
1504
2525
|
doctor Check installer receipts and plugin file integrity
|
|
2526
|
+
login Connect your Memroot account via browser sign-in
|
|
2527
|
+
logout Remove stored cloud credentials
|
|
2528
|
+
whoami Show the signed-in account identity
|
|
2529
|
+
status Show cloud auth and agent install state
|
|
2530
|
+
token Print a fresh access token for agent adapters
|
|
1505
2531
|
|
|
1506
2532
|
Options:
|
|
1507
|
-
--agent
|
|
1508
|
-
--yes
|
|
1509
|
-
--dry-run
|
|
1510
|
-
--
|
|
1511
|
-
--
|
|
2533
|
+
--agent claude, codex, or grok; repeat to select more than one (setup, doctor)
|
|
2534
|
+
--yes Use the explicitly selected agents without prompts (setup)
|
|
2535
|
+
--dry-run Show planned changes without running agent CLIs or writing files (setup)
|
|
2536
|
+
--no-browser Print the sign-in URL instead of opening a browser (login)
|
|
2537
|
+
--force Re-authenticate even if credentials are still valid (login)
|
|
2538
|
+
--env prod or dev; dev targets a local API stub on localhost:8787
|
|
2539
|
+
--api-base Override the API base URL (MEMROOT_API_BASE also works)
|
|
2540
|
+
--help Show help
|
|
2541
|
+
--version Show version
|
|
1512
2542
|
|
|
1513
2543
|
${cloudStatus}
|
|
1514
2544
|
Grok setup trusts the selected local plugin via its native --trust option.
|
|
1515
2545
|
Hook execution trust remains controlled by each agent.
|
|
1516
2546
|
`;
|
|
2547
|
+
var commands = [
|
|
2548
|
+
"setup",
|
|
2549
|
+
"doctor",
|
|
2550
|
+
"login",
|
|
2551
|
+
"logout",
|
|
2552
|
+
"whoami",
|
|
2553
|
+
"status",
|
|
2554
|
+
"token"
|
|
2555
|
+
];
|
|
2556
|
+
var authCommands = [
|
|
2557
|
+
"login",
|
|
2558
|
+
"logout",
|
|
2559
|
+
"whoami",
|
|
2560
|
+
"status",
|
|
2561
|
+
"token"
|
|
2562
|
+
];
|
|
1517
2563
|
function parse(args) {
|
|
1518
|
-
const
|
|
1519
|
-
if (
|
|
2564
|
+
const command2 = args.shift();
|
|
2565
|
+
if (!command2 || !commands.includes(command2))
|
|
1520
2566
|
throw new Error(
|
|
1521
|
-
"INVALID_ARGUMENT: Choose setup or
|
|
2567
|
+
"INVALID_ARGUMENT: Choose setup, doctor, login, logout, whoami, status, or token. Run memroot --help."
|
|
1522
2568
|
);
|
|
1523
|
-
const options = {
|
|
2569
|
+
const options = {
|
|
2570
|
+
command: command2,
|
|
2571
|
+
selected: [],
|
|
2572
|
+
yes: false,
|
|
2573
|
+
dryRun: false,
|
|
2574
|
+
noBrowser: false,
|
|
2575
|
+
force: false
|
|
2576
|
+
};
|
|
1524
2577
|
while (args.length) {
|
|
1525
2578
|
const flag = args.shift();
|
|
1526
2579
|
if (flag === "--yes") options.yes = true;
|
|
1527
2580
|
else if (flag === "--dry-run") options.dryRun = true;
|
|
1528
|
-
else if (flag === "--
|
|
2581
|
+
else if (flag === "--no-browser") options.noBrowser = true;
|
|
2582
|
+
else if (flag === "--force") options.force = true;
|
|
2583
|
+
else if (flag === "--env") {
|
|
2584
|
+
const value = args.shift();
|
|
2585
|
+
if (value !== "prod" && value !== "dev")
|
|
2586
|
+
throw new Error("INVALID_ARGUMENT: --env must be prod or dev.");
|
|
2587
|
+
options.env = value;
|
|
2588
|
+
} else if (flag === "--api-base") {
|
|
2589
|
+
const value = args.shift();
|
|
2590
|
+
try {
|
|
2591
|
+
if (!value) throw new Error("missing");
|
|
2592
|
+
new URL(value);
|
|
2593
|
+
} catch {
|
|
2594
|
+
throw new Error("INVALID_ARGUMENT: --api-base needs a valid URL.");
|
|
2595
|
+
}
|
|
2596
|
+
options.apiBase = value;
|
|
2597
|
+
} else if (flag === "--agent") {
|
|
1529
2598
|
const value = args.shift();
|
|
1530
2599
|
if (!agents.includes(value))
|
|
1531
2600
|
throw new Error("INVALID_AGENT: Use claude, codex, or grok.");
|
|
@@ -1536,7 +2605,20 @@ function parse(args) {
|
|
|
1536
2605
|
`INVALID_ARGUMENT: Unknown option ${flag}. Run memroot --help.`
|
|
1537
2606
|
);
|
|
1538
2607
|
}
|
|
1539
|
-
if (
|
|
2608
|
+
if (authCommands.includes(command2)) {
|
|
2609
|
+
if (options.selected.length || options.yes || options.dryRun)
|
|
2610
|
+
throw new Error(
|
|
2611
|
+
`INVALID_ARGUMENT: ${command2} does not support --agent, --yes, or --dry-run.`
|
|
2612
|
+
);
|
|
2613
|
+
if (command2 !== "login" && (options.noBrowser || options.force))
|
|
2614
|
+
throw new Error(
|
|
2615
|
+
`INVALID_ARGUMENT: ${command2} does not support --no-browser or --force.`
|
|
2616
|
+
);
|
|
2617
|
+
} else if (options.noBrowser || options.force || options.env || options.apiBase)
|
|
2618
|
+
throw new Error(
|
|
2619
|
+
`INVALID_ARGUMENT: ${command2} does not support cloud sign-in options.`
|
|
2620
|
+
);
|
|
2621
|
+
if (command2 === "doctor" && (options.yes || options.dryRun))
|
|
1540
2622
|
throw new Error("INVALID_ARGUMENT: doctor supports only --agent.");
|
|
1541
2623
|
return options;
|
|
1542
2624
|
}
|
|
@@ -1564,6 +2646,8 @@ async function doctor(selected) {
|
|
|
1564
2646
|
console.log(
|
|
1565
2647
|
`${labels[agent]}: plugin files verified (${receipt.version}); CLI ${path ? "detected" : "not found"}. ${path ? "Native plugin present" : "Native presence not verified"}; hook execution trust remains controlled by the agent.`
|
|
1566
2648
|
);
|
|
2649
|
+
if (agent === "codex" && path)
|
|
2650
|
+
console.log(describeCodexMcp(inspectCodexMcp(path)));
|
|
1567
2651
|
if (!path) failures++;
|
|
1568
2652
|
} catch (error) {
|
|
1569
2653
|
failures++;
|
|
@@ -1587,7 +2671,7 @@ async function setup(options) {
|
|
|
1587
2671
|
if (interactive) {
|
|
1588
2672
|
intro(`Memroot ${version}`);
|
|
1589
2673
|
note(
|
|
1590
|
-
"Install Memroot plugins for your coding agents.\
|
|
2674
|
+
"Install Memroot plugins for your coding agents.\nCodex setup opens browser sign-in for production memory.",
|
|
1591
2675
|
"Set up your agents"
|
|
1592
2676
|
);
|
|
1593
2677
|
if (!options.selected.length) {
|
|
@@ -1632,6 +2716,10 @@ ${summary}`);
|
|
|
1632
2716
|
console.log(
|
|
1633
2717
|
`${agent}: ${JSON.stringify(installCommands(agent, `${data}/releases/${version}/${agent}`))}`
|
|
1634
2718
|
);
|
|
2719
|
+
if (options.selected.includes("codex"))
|
|
2720
|
+
console.log(
|
|
2721
|
+
`codex: mcp add memroot --url ${codexMcpUrl} --oauth-resource ${codexMcpUrl}; reuse existing OAuth authorization.`
|
|
2722
|
+
);
|
|
1635
2723
|
return;
|
|
1636
2724
|
}
|
|
1637
2725
|
if (interactive) {
|
|
@@ -1663,17 +2751,41 @@ ${summary}`);
|
|
|
1663
2751
|
console.log(
|
|
1664
2752
|
`${labels[agent]}: already installed; plugin files verified. Check native enablement in the agent.`
|
|
1665
2753
|
);
|
|
2754
|
+
if (agent === "codex")
|
|
2755
|
+
console.log(
|
|
2756
|
+
describeCodexMcp(await ensureCodexMcp({ dryRun: false }))
|
|
2757
|
+
);
|
|
1666
2758
|
continue;
|
|
1667
2759
|
}
|
|
1668
|
-
const
|
|
1669
|
-
if (
|
|
1670
|
-
|
|
1671
|
-
|
|
2760
|
+
const commands2 = installCommands(agent, candidate.marketplace);
|
|
2761
|
+
if (agent === "codex" && receipt && state.registered && !state.currentMarketplace) {
|
|
2762
|
+
const removeHelp = native(found.get(agent), agent, [
|
|
2763
|
+
"plugin",
|
|
2764
|
+
"marketplace",
|
|
2765
|
+
"remove",
|
|
2766
|
+
"--help"
|
|
2767
|
+
]);
|
|
2768
|
+
if (!removeHelp.includes("--json"))
|
|
2769
|
+
throw new Error(
|
|
2770
|
+
"NATIVE_COMMAND_UNSUPPORTED: Update Codex to support marketplace remove --json before upgrading Memroot. The previous installation is preserved."
|
|
2771
|
+
);
|
|
2772
|
+
native(found.get(agent), agent, [
|
|
2773
|
+
"plugin",
|
|
2774
|
+
"marketplace",
|
|
2775
|
+
"remove",
|
|
2776
|
+
"memroot",
|
|
2777
|
+
"--json"
|
|
2778
|
+
]);
|
|
2779
|
+
}
|
|
2780
|
+
if (state.currentMarketplace) commands2.shift();
|
|
2781
|
+
for (const args of commands2)
|
|
1672
2782
|
native(found.get(agent), agent, args);
|
|
1673
2783
|
await writeReceipt(data, candidate);
|
|
1674
2784
|
console.log(
|
|
1675
2785
|
`${labels[agent]}: plugin installed. Restart the agent to load it.`
|
|
1676
2786
|
);
|
|
2787
|
+
if (agent === "codex")
|
|
2788
|
+
console.log(describeCodexMcp(await ensureCodexMcp({ dryRun: false })));
|
|
1677
2789
|
}
|
|
1678
2790
|
} finally {
|
|
1679
2791
|
await release();
|
|
@@ -1692,12 +2804,22 @@ async function main() {
|
|
|
1692
2804
|
return;
|
|
1693
2805
|
}
|
|
1694
2806
|
const options = parse(args);
|
|
1695
|
-
|
|
1696
|
-
|
|
2807
|
+
const auth = {};
|
|
2808
|
+
if (options.env) auth.env = options.env;
|
|
2809
|
+
if (options.apiBase) auth.apiBase = options.apiBase;
|
|
2810
|
+
if (options.noBrowser) auth.noBrowser = true;
|
|
2811
|
+
if (options.force) auth.force = true;
|
|
2812
|
+
if (options.command === "setup") await setup(options);
|
|
2813
|
+
else if (options.command === "doctor") await doctor(options.selected);
|
|
2814
|
+
else if (options.command === "login") await login(auth);
|
|
2815
|
+
else if (options.command === "logout") await logout(auth);
|
|
2816
|
+
else if (options.command === "whoami") await whoami(auth);
|
|
2817
|
+
else if (options.command === "status") await status(auth);
|
|
2818
|
+
else await printAccessToken(auth);
|
|
1697
2819
|
}
|
|
1698
2820
|
main().catch((error) => {
|
|
1699
2821
|
console.error(
|
|
1700
|
-
error instanceof Error ? error.message : "
|
|
2822
|
+
error instanceof Error ? error.message : "MEMROOT_FAILED: Unexpected CLI failure."
|
|
1701
2823
|
);
|
|
1702
2824
|
process.exitCode = 1;
|
|
1703
2825
|
});
|