@appsoftwareltd/etherpk-mcp 0.2.0 → 0.3.0

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 CHANGED
@@ -31,11 +31,30 @@ npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.etherpk.com
31
31
  # npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.etherpk.com --recovery-code
32
32
 
33
33
  # Tell the agent about the graph (Claude Code shown; the Agents tab has the others).
34
- claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <graph id>
34
+ claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --sync-server https://sync.etherpk.com --graph <graph id>
35
35
  ```
36
36
 
37
- `npx @appsoftwareltd/etherpk-mcp graphs` lists the graphs the token can reach, by name and id.
38
- For a scripted setup, `ETHERPK_PAT` and `ETHERPK_RECOVERY_CODE` stand in for the prompts.
37
+ `npx @appsoftwareltd/etherpk-mcp graphs` lists the graphs each signed-in account can reach, by
38
+ name and id. For a scripted setup, `ETHERPK_PAT` and `ETHERPK_RECOVERY_CODE` stand in for the
39
+ prompts.
40
+
41
+ ## More than one Sync Server
42
+
43
+ One computer can be signed in to several Sync Servers at once - your own self-hosted one beside
44
+ the managed service, say. Run `login` once per server; every login lives in the one config file.
45
+ `--sync-server <url>` then says which server a command means:
46
+
47
+ ```sh
48
+ npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.your-domain.example
49
+ npx @appsoftwareltd/etherpk-mcp graphs # every server, in turn
50
+ npx @appsoftwareltd/etherpk-mcp serve --sync-server https://sync.your-domain.example --graph <graph id>
51
+ npx @appsoftwareltd/etherpk-mcp logout --sync-server https://sync.your-domain.example
52
+ ```
53
+
54
+ While only one server is signed in, `--sync-server` can be left off `graphs`, `serve` and
55
+ `logout`. With two or more, `serve` and `logout` refuse to guess and ask for it. The commands
56
+ the Agents tab shows always include it, so they stay right whatever else the computer is signed
57
+ in to. `logout --all` forgets every server at once.
39
58
 
40
59
  Every command runs through `npx`, which fetches the package but never puts `etherpk-mcp` on your
41
60
  PATH. If you'd rather type the short form, `npm install -g @appsoftwareltd/etherpk-mcp` once and
@@ -50,8 +69,8 @@ One running instance serves one graph.
50
69
 
51
70
  ## What it keeps on your computer
52
71
 
53
- - **Keys**: `~/.config/etherpk/mcp.json`, readable only by your user - the same trust as a browser
54
- you've signed in on.
72
+ - **Keys**: `~/.config/etherpk/mcp.json`, one entry per Sync Server, readable only by your user -
73
+ the same trust as a browser you've signed in on.
55
74
  - **A cache of each graph you serve**: `~/.cache/etherpk/mcp/`, so a restart catches up on what
56
75
  changed instead of downloading everything again. It holds your notes readably, like a signed-in
57
76
  browser's own storage does. It is never the source of truth: on every start the Sync Server is
@@ -59,7 +78,7 @@ One running instance serves one graph.
59
78
  newer version of this program discards a cache it no longer understands and rebuilds it.
60
79
 
61
80
  Revoke the token at the portal to cut the agent off, and `npx @appsoftwareltd/etherpk-mcp logout`
62
- to forget the keys and delete the cache on that computer.
81
+ to forget that server's keys and delete its cache on that computer (`--all` for every server).
63
82
 
64
83
  ## Building and publishing
65
84
 
package/dist/main.js CHANGED
@@ -17,7 +17,7 @@ import { deserialize, serialize } from "node:v8";
17
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  var package_default = {
19
19
  name: "@appsoftwareltd/etherpk-mcp",
20
- version: "0.2.0",
20
+ version: "0.3.0",
21
21
  license: "Elastic-2.0",
22
22
  description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
23
23
  type: "module",
@@ -705,12 +705,16 @@ function resolveGraphById(graphs, vault, graphId) {
705
705
  //#endregion
706
706
  //#region src/config.ts
707
707
  /**
708
- * What `login` leaves behind and `serve` reads: the Sync Server origin, the account-wide
709
- * [[Personal Access Token]], and the vault key that opens the account's keys on this machine.
708
+ * What `login` leaves behind and `serve` reads: for each Sync Server this machine is signed in
709
+ * to, keyed by origin, the account-wide [[Personal Access Token]] and the vault key that opens
710
+ * the account's keys here. One file holds every login (ADR 0075), so a dev instance beside a
711
+ * production one, or a self-hosted server beside the managed service, need no second file and
712
+ * no environment variable to keep them apart; a command names its server with `--sync-server`
713
+ * and may leave it out while only one is signed in.
710
714
  *
711
715
  * The file is the same trust class as the browser's `localStorage` cache of the same key
712
716
  * (DESIGN.md → Key storage between sessions): whoever can read this user's files can read the
713
- * account. It is written `0600` in the user's config directory, never anywhere a project
717
+ * accounts. It is written `0600` in the user's config directory, never anywhere a project
714
718
  * checkout could pick it up, and `ETHERPK_MCP_CONFIG` overrides the path for tests and for a
715
719
  * box that keeps its secrets elsewhere. An OS keychain would be the next step (ADR 0072).
716
720
  */
@@ -719,7 +723,18 @@ function defaultConfigPath(env = process.env) {
719
723
  if (override) return override;
720
724
  return join(env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "etherpk", "mcp.json");
721
725
  }
722
- /** Tolerant of a hand-edited file: keeps the fields it knows, refuses the rest. */
726
+ function emptyConfig() {
727
+ return { servers: {} };
728
+ }
729
+ /** The origin form every key and `--sync-server` value is reduced to before comparison. */
730
+ function normaliseSyncServer(value) {
731
+ return value.trim().replace(/\/+$/, "");
732
+ }
733
+ /**
734
+ * Tolerant of a hand-edited file: keeps the entries it knows, refuses the rest. A file in the
735
+ * single-login shape written before 0.3.0 reads as null, so its commands say "not logged in"
736
+ * and one `login` rewrites it; the token in it was never lost, only its shape.
737
+ */
723
738
  function parseConfig(raw) {
724
739
  let parsed;
725
740
  try {
@@ -727,14 +742,20 @@ function parseConfig(raw) {
727
742
  } catch {
728
743
  return null;
729
744
  }
730
- if (typeof parsed !== "object" || parsed === null) return null;
731
- const rec = parsed;
732
- if (typeof rec.syncServer !== "string" || typeof rec.pat !== "string") return null;
733
- const out = {
734
- syncServer: rec.syncServer.replace(/\/$/, ""),
735
- pat: rec.pat
736
- };
737
- if (typeof rec.vaultKey === "string" && rec.vaultKey !== "") out.vaultKey = rec.vaultKey;
745
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
746
+ const servers = parsed.servers;
747
+ if (typeof servers !== "object" || servers === null || Array.isArray(servers)) return null;
748
+ const out = emptyConfig();
749
+ for (const [key, entry] of Object.entries(servers)) {
750
+ if (typeof entry !== "object" || entry === null) return null;
751
+ const login = entry;
752
+ if (typeof login.pat !== "string" || login.pat === "") return null;
753
+ const syncServer = normaliseSyncServer(key);
754
+ if (!/^https?:\/\//.test(syncServer)) return null;
755
+ const kept = { pat: login.pat };
756
+ if (typeof login.vaultKey === "string" && login.vaultKey !== "") kept.vaultKey = login.vaultKey;
757
+ out.servers[syncServer] = kept;
758
+ }
738
759
  return out;
739
760
  }
740
761
  function serialiseConfig(config) {
@@ -744,7 +765,7 @@ async function readConfig(path) {
744
765
  try {
745
766
  return parseConfig(await readFile(path, "utf8"));
746
767
  } catch (error) {
747
- if (error.code === "ENOENT") return null;
768
+ if (error.code === "ENOENT") return emptyConfig();
748
769
  throw error;
749
770
  }
750
771
  }
@@ -757,6 +778,54 @@ async function writeConfig(path, config) {
757
778
  await writeFile(path, serialiseConfig(config), { mode: 384 });
758
779
  await chmod(path, 384);
759
780
  }
781
+ /** The logins as a list, in file order, each carrying its server. */
782
+ function listLogins(config) {
783
+ return Object.entries(config.servers).map(([syncServer, login]) => ({
784
+ syncServer,
785
+ ...login
786
+ }));
787
+ }
788
+ /**
789
+ * Which login a command means. Named, it must exist; unnamed, it is the only one there is.
790
+ * With two or more logins and no name the command refuses rather than guess: the graphs of
791
+ * one server are not the graphs of another, and a guess would serve the wrong one silently.
792
+ */
793
+ function selectServer(config, wanted) {
794
+ const known = Object.keys(config.servers);
795
+ if (wanted !== void 0 && wanted.trim() !== "") {
796
+ const syncServer = normaliseSyncServer(wanted);
797
+ const login = config.servers[syncServer];
798
+ return login ? {
799
+ ok: true,
800
+ credentials: {
801
+ syncServer,
802
+ ...login
803
+ }
804
+ } : {
805
+ ok: false,
806
+ reason: "unknown",
807
+ syncServer,
808
+ known
809
+ };
810
+ }
811
+ if (known.length === 0) return {
812
+ ok: false,
813
+ reason: "none"
814
+ };
815
+ if (known.length > 1) return {
816
+ ok: false,
817
+ reason: "ambiguous",
818
+ known
819
+ };
820
+ const syncServer = known[0];
821
+ return {
822
+ ok: true,
823
+ credentials: {
824
+ syncServer,
825
+ ...config.servers[syncServer]
826
+ }
827
+ };
828
+ }
760
829
  //#endregion
761
830
  //#region ../client/src/lib/diagnostics/performance.ts
762
831
  function assertSafeName(name) {
@@ -7853,13 +7922,20 @@ function nodeIndexHost(dir) {
7853
7922
  }
7854
7923
  };
7855
7924
  }
7856
- /** Remove every persisted graph under the cache root (logout). */
7925
+ /** Remove every persisted graph under the cache root (logout of the last server). */
7857
7926
  async function removeCacheRoot(env) {
7858
7927
  await rm(cacheRoot(env), {
7859
7928
  recursive: true,
7860
7929
  force: true
7861
7930
  });
7862
7931
  }
7932
+ /** Remove one server's persisted graphs (logout of that server while others stay). */
7933
+ async function removeServerCache(env, serverBaseUrl) {
7934
+ await rm(dirname(graphCacheDir(env, serverBaseUrl, "x")), {
7935
+ recursive: true,
7936
+ force: true
7937
+ });
7938
+ }
7863
7939
  //#endregion
7864
7940
  //#region src/headless-graph.ts
7865
7941
  /** Open the graph, scan its registry and build the index; resolves once tools can answer. */
@@ -8391,9 +8467,12 @@ function createMcpServer(graph, info) {
8391
8467
  * `etherpk-mcp`: the [[Headless Client]]'s command line (ADR 0072).
8392
8468
  *
8393
8469
  * etherpk-mcp login --sync-server <url> [--pat <token>] [--recovery-code]
8394
- * etherpk-mcp graphs
8395
- * etherpk-mcp serve --graph <id or name>
8396
- * etherpk-mcp logout
8470
+ * etherpk-mcp graphs [--sync-server <url>]
8471
+ * etherpk-mcp serve --graph <id or name> [--sync-server <url>]
8472
+ * etherpk-mcp logout [--sync-server <url> | --all]
8473
+ *
8474
+ * One config file holds a login per Sync Server (ADR 0075). `--sync-server` names the one a
8475
+ * command means and may be left out while only one is signed in.
8397
8476
  *
8398
8477
  * `serve` speaks MCP over stdio, so everything for the human goes to stderr; stdout belongs
8399
8478
  * to the agent. `login` and `graphs` are interactive and print to stdout.
@@ -8415,16 +8494,19 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
8415
8494
  open EtherPK in a browser signed in to the account with its graphs unlocked and
8416
8495
  confirm the code shown. Press r while waiting, or pass --recovery-code, to type
8417
8496
  your Recovery Code instead (or ETHERPK_RECOVERY_CODE, for a scripted setup).
8418
- ${CMD} graphs
8419
- List the synced graphs this account can reach, by name and id.
8420
- ${CMD} serve --graph <id or name>
8497
+ ${CMD} graphs [--sync-server <url>]
8498
+ List the synced graphs each signed-in account can reach, by name and id.
8499
+ ${CMD} serve --graph <id or name> [--sync-server <url>]
8421
8500
  Serve one graph to an agent over stdio. For Claude Code:
8422
- claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <id>
8423
- ${CMD} logout
8424
- Forget the token, keys and cached graphs on this machine.
8501
+ claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --sync-server <url> --graph <id>
8502
+ ${CMD} logout [--sync-server <url> | --all]
8503
+ Forget that server's token, keys and cached graphs on this machine.
8425
8504
 
8426
- The config file is ${defaultConfigPath()} (override with ETHERPK_MCP_CONFIG); cached graphs live
8427
- under ~/.cache/etherpk/mcp (override with ETHERPK_MCP_CACHE_DIR).
8505
+ This machine can be signed in to several Sync Servers at once (a self-hosted one beside the
8506
+ managed service, say); --sync-server says which one a command means, and can be left out
8507
+ while only one is signed in. The config file is ${defaultConfigPath()} (override with
8508
+ ETHERPK_MCP_CONFIG); cached graphs live under ~/.cache/etherpk/mcp (override with
8509
+ ETHERPK_MCP_CACHE_DIR).
8428
8510
  Docs: https://docs.etherpk.com/using-ai-agents-with-your-notes
8429
8511
  `;
8430
8512
  function fail(message) {
@@ -8469,14 +8551,26 @@ async function ask(question, { secret = false } = {}) {
8469
8551
  stdin.on("data", onData);
8470
8552
  });
8471
8553
  }
8472
- async function requireConfig(path) {
8554
+ async function loadConfig(path) {
8473
8555
  const config = await readConfig(path);
8474
- if (!config) fail(`Not logged in on this machine. Run: ${CMD} login --sync-server <url>`);
8556
+ if (!config) fail(`${path} is not a config file this version understands. Run: ${CMD} login --sync-server <url> (it will be rewritten; nothing else is affected).`);
8475
8557
  return config;
8476
8558
  }
8559
+ /** The login a command means, or the reason there is none - in words the user can act on. */
8560
+ function requireServer(config, wanted) {
8561
+ const selection = selectServer(config, wanted);
8562
+ if (selection.ok) return selection.credentials;
8563
+ switch (selection.reason) {
8564
+ case "none": return fail(`Not logged in on this machine. Run: ${CMD} login --sync-server <url>`);
8565
+ case "unknown": return fail(`Not logged in to ${selection.syncServer}. Signed in to: ${selection.known.join(", ") || "(none)"}. Run: ${CMD} login --sync-server ${selection.syncServer}`);
8566
+ case "ambiguous": return fail(`Signed in to more than one Sync Server here: ${selection.known.join(", ")}. Say which with --sync-server <url>.`);
8567
+ }
8568
+ }
8477
8569
  async function login(args) {
8478
8570
  const path = defaultConfigPath();
8479
- const syncServer = (args["sync-server"] ?? (await readConfig(path))?.syncServer ?? await ask("Sync Server URL: ")).replace(/\/$/, "");
8571
+ const config = await readConfig(path) ?? emptyConfig();
8572
+ const known = Object.keys(config.servers);
8573
+ const syncServer = normaliseSyncServer(args["sync-server"] ?? (known.length === 1 ? known[0] : await ask("Sync Server URL: ")));
8480
8574
  if (!/^https?:\/\//.test(syncServer)) fail("The Sync Server must be an http(s) URL.");
8481
8575
  const pat = args.pat ?? process.env.ETHERPK_PAT ?? await ask(`Personal Access Token (account-wide, from ${syncServer}/account/tokens): `, { secret: true });
8482
8576
  if (!pat) fail("A Personal Access Token is required.");
@@ -8487,17 +8581,19 @@ async function login(args) {
8487
8581
  console.log(`Signed in to ${syncServer} as ${account.principal.email ?? account.principal.name ?? account.principal.id}.`);
8488
8582
  const byRecoveryCode = async () => unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? await ask("Recovery Code: ", { secret: true }));
8489
8583
  const vaultKey = args["recovery-code"] ? await byRecoveryCode() : await approveOrFallBack(account, byRecoveryCode);
8490
- await writeConfig(path, {
8491
- syncServer,
8584
+ config.servers[syncServer] = {
8492
8585
  pat,
8493
8586
  vaultKey: toBase64Url(vaultKey)
8494
- });
8587
+ };
8588
+ await writeConfig(path, config);
8495
8589
  console.log(`Keys unlocked and cached in ${path} (owner-only). Anyone who can read your files on this machine can read this account, as with a signed-in browser.`);
8590
+ const others = Object.keys(config.servers).filter((server) => server !== syncServer);
8591
+ if (others.length > 0) console.log(`Also signed in to ${others.join(", ")}; commands now need --sync-server <url> to say which.`);
8496
8592
  await listGraphs({
8497
8593
  syncServer,
8498
8594
  pat,
8499
8595
  vaultKey: toBase64Url(vaultKey)
8500
- });
8596
+ }, others.length > 0);
8501
8597
  }
8502
8598
  /**
8503
8599
  * Device Approval, with the Recovery Code one keypress away: a user who has no unlocked EtherPK
@@ -8546,16 +8642,28 @@ async function approveOrFallBack(account, byRecoveryCode) {
8546
8642
  console.log("Approval cancelled; unlocking with your Recovery Code instead.");
8547
8643
  return byRecoveryCode();
8548
8644
  }
8549
- async function listGraphs(config) {
8550
- if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`);
8551
- const account = await connectAccount(config);
8552
- const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey));
8645
+ /**
8646
+ * `graphs` with a server named lists that server; unnamed, it lists every signed-in server in
8647
+ * turn, because "what can the agent reach from here" is the question and it has one answer
8648
+ * per login.
8649
+ */
8650
+ async function graphsCommand(args) {
8651
+ const config = await loadConfig(defaultConfigPath());
8652
+ const logins = args["sync-server"] ? [requireServer(config, args["sync-server"])] : listLogins(config);
8653
+ if (logins.length === 0) fail(`Not logged in on this machine. Run: ${CMD} login --sync-server <url>`);
8654
+ for (const login of logins) await listGraphs(login, logins.length > 1);
8655
+ }
8656
+ async function listGraphs(login, several) {
8657
+ if (!login.vaultKey) fail(`Keys are not unlocked on this machine for ${login.syncServer}. Run: ${CMD} login --sync-server ${login.syncServer}`);
8658
+ const account = await connectAccount(login);
8659
+ const vault = await openAccountVault(account.api, fromBase64Url(login.vaultKey));
8553
8660
  const graphs = await account.api.listGraphs();
8661
+ const serverFlag = several ? ` --sync-server ${login.syncServer}` : "";
8554
8662
  if (graphs.length === 0) {
8555
- console.log("No synced graphs are reachable with this token.");
8663
+ console.log(`No synced graphs are reachable with the token for ${login.syncServer}.`);
8556
8664
  return;
8557
8665
  }
8558
- console.log("Synced graphs:");
8666
+ console.log(several ? `Synced graphs on ${login.syncServer}:` : "Synced graphs:");
8559
8667
  for (const record of graphs) {
8560
8668
  const keyring = vault.keyrings.find((entry) => entry.graphId === record.id);
8561
8669
  const name = keyring ? await readGraphName({
@@ -8569,16 +8677,16 @@ async function listGraphs(config) {
8569
8677
  console.log(` ${record.id} ${label} [${record.role}]`);
8570
8678
  }
8571
8679
  console.log("");
8572
- console.log(`Serve one to an agent with: ${CMD} serve --graph <id>`);
8573
- console.log(`For Claude Code: claude mcp add etherpk -- ${CMD} serve --graph <id>`);
8680
+ console.log(`Serve one to an agent with: ${CMD} serve${serverFlag} --graph <id>`);
8681
+ console.log(`For Claude Code: claude mcp add etherpk -- ${CMD} serve${serverFlag} --graph <id>`);
8574
8682
  }
8575
8683
  async function serve(args) {
8576
8684
  const wanted = args.graph?.trim();
8577
8685
  if (!wanted) fail("serve needs --graph <id or name>.");
8578
- const config = await requireConfig(defaultConfigPath());
8579
- if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`);
8580
- const account = await connectAccount(config);
8581
- const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey));
8686
+ const login = requireServer(await loadConfig(defaultConfigPath()), args["sync-server"]);
8687
+ if (!login.vaultKey) fail(`Keys are not unlocked on this machine for ${login.syncServer}. Run: ${CMD} login --sync-server ${login.syncServer}`);
8688
+ const account = await connectAccount(login);
8689
+ const vault = await openAccountVault(account.api, fromBase64Url(login.vaultKey));
8582
8690
  const graphs = await account.api.listGraphs();
8583
8691
  let graphId = graphs.find((graph) => graph.id === wanted)?.id;
8584
8692
  let graphName = null;
@@ -8598,7 +8706,7 @@ async function serve(args) {
8598
8706
  break;
8599
8707
  }
8600
8708
  }
8601
- if (!graphId) fail(`No synced graph is named or identified by "${wanted}". Run: ${CMD} graphs`);
8709
+ if (!graphId) fail(`No synced graph on ${login.syncServer} is named or identified by "${wanted}". Run: ${CMD} graphs`);
8602
8710
  const { record, keyring } = resolveGraphById(graphs, vault, graphId);
8603
8711
  console.error(`etherpk-mcp: opening graph ${graphId} on ${account.serverBaseUrl}…`);
8604
8712
  const graph = await openHeadlessGraph({
@@ -8629,13 +8737,27 @@ async function serve(args) {
8629
8737
  await server.connect(transport);
8630
8738
  console.error(`etherpk-mcp: serving "${graphName}" over stdio as "Agent on ${hostname()}".`);
8631
8739
  }
8632
- async function logout() {
8740
+ async function logout(args) {
8633
8741
  const path = defaultConfigPath();
8634
- await unlink(path).catch((error) => {
8635
- if (error.code !== "ENOENT") throw error;
8636
- });
8637
- await removeCacheRoot(process.env);
8638
- console.log(`Forgot the token and keys in ${path} and the cached graphs. Revoke the Personal Access Token in the Sync Server portal too if this machine is not yours to keep.`);
8742
+ const config = await readConfig(path);
8743
+ if (args.all || !config) {
8744
+ await unlink(path).catch((error) => {
8745
+ if (error.code !== "ENOENT") throw error;
8746
+ });
8747
+ await removeCacheRoot(process.env);
8748
+ console.log(`Forgot every token and key in ${path} and the cached graphs. Revoke the Personal Access Tokens in each Sync Server portal too if this machine is not yours to keep.`);
8749
+ return;
8750
+ }
8751
+ const login = requireServer(config, args["sync-server"]);
8752
+ delete config.servers[login.syncServer];
8753
+ await removeServerCache(process.env, login.syncServer);
8754
+ if (Object.keys(config.servers).length === 0) {
8755
+ await unlink(path).catch((error) => {
8756
+ if (error.code !== "ENOENT") throw error;
8757
+ });
8758
+ await removeCacheRoot(process.env);
8759
+ } else await writeConfig(path, config);
8760
+ console.log(`Forgot the token, keys and cached graphs for ${login.syncServer}. Revoke the Personal Access Token in its portal too if this machine is not yours to keep.`);
8639
8761
  }
8640
8762
  async function main() {
8641
8763
  const { values, positionals } = parseArgs({
@@ -8646,6 +8768,7 @@ async function main() {
8646
8768
  pat: { type: "string" },
8647
8769
  "recovery-code": { type: "boolean" },
8648
8770
  graph: { type: "string" },
8771
+ all: { type: "boolean" },
8649
8772
  help: {
8650
8773
  type: "boolean",
8651
8774
  short: "h"
@@ -8667,9 +8790,9 @@ async function main() {
8667
8790
  }
8668
8791
  switch (command) {
8669
8792
  case "login": return login(values);
8670
- case "graphs": return listGraphs(await requireConfig(defaultConfigPath()));
8793
+ case "graphs": return graphsCommand(values);
8671
8794
  case "serve": return serve(values);
8672
- case "logout": return logout();
8795
+ case "logout": return logout(values);
8673
8796
  default: fail(`Unknown command "${command}".\n\n${USAGE}`);
8674
8797
  }
8675
8798
  }