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/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 { registered: false, installed: matching.length > 0 };
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.0";
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. Cloud memory is not connected.";
1493
- var cloudStatus = "Cloud memory: not connected. This installer alpha does not provide cloud authentication, memory retrieval, or session upload.";
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 claude, codex, or grok; repeat to select more than one
1508
- --yes Use the explicitly selected agents without interactive prompts
1509
- --dry-run Show planned changes without running agent CLIs or writing files
1510
- --help Show help
1511
- --version Show version
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 command = args.shift();
1519
- if (command !== "setup" && command !== "doctor")
2564
+ const command2 = args.shift();
2565
+ if (!command2 || !commands.includes(command2))
1520
2566
  throw new Error(
1521
- "INVALID_ARGUMENT: Choose setup or doctor. Run memroot --help."
2567
+ "INVALID_ARGUMENT: Choose setup, doctor, login, logout, whoami, status, or token. Run memroot --help."
1522
2568
  );
1523
- const options = { command, selected: [], yes: false, dryRun: false };
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 === "--agent") {
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 (command === "doctor" && (options.yes || options.dryRun))
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.\nCloud memory connection is not available in this alpha.",
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 commands = installCommands(agent, candidate.marketplace);
1669
- if (state.registered && (!receipt || receipt.marketplace === candidate.marketplace))
1670
- commands.shift();
1671
- for (const args of commands)
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
- if (options.command === "doctor") await doctor(options.selected);
1696
- else await setup(options);
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 : "SETUP_FAILED: Unexpected installer failure."
2822
+ error instanceof Error ? error.message : "MEMROOT_FAILED: Unexpected CLI failure."
1701
2823
  );
1702
2824
  process.exitCode = 1;
1703
2825
  });