@appsoftwareltd/etherpk-mcp 0.1.2 → 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 +30 -11
- package/dist/main.js +188 -65
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,25 +17,44 @@ server and graph filled in. They are:
|
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
# Once per computer. Prompts for an account-wide Personal Access Token (make one at
|
|
20
|
-
# https://
|
|
20
|
+
# https://sync.etherpk.com/account/tokens, also reachable from "Access tokens" in
|
|
21
21
|
# EtherPK's account menu), then shows a short code and the address of your EtherPK:
|
|
22
22
|
# open EtherPK there in a browser where you're signed in with your graphs unlocked -
|
|
23
23
|
# any page will do - and confirm the code. The same step as adding a phone.
|
|
24
|
-
npx @appsoftwareltd/etherpk-mcp login --server https://
|
|
24
|
+
npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.etherpk.com
|
|
25
25
|
|
|
26
26
|
# Self-hosting your own Sync Server? Give its address instead:
|
|
27
|
-
# npx @appsoftwareltd/etherpk-mcp login --server https://sync.your-domain.example
|
|
27
|
+
# npx @appsoftwareltd/etherpk-mcp login --sync-server https://sync.your-domain.example
|
|
28
28
|
|
|
29
29
|
# No EtherPK to hand on this computer (a server you reach over SSH, say)? Press r while
|
|
30
30
|
# login is waiting, or use your Recovery Code from the start:
|
|
31
|
-
# npx @appsoftwareltd/etherpk-mcp login --server https://
|
|
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
|
|
38
|
-
For a scripted setup, `ETHERPK_PAT` and `ETHERPK_RECOVERY_CODE` stand in for the
|
|
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 -
|
|
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
|
|
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
|
|
|
@@ -83,7 +102,7 @@ pnpm publish --access public --no-git-checks # prepack runs the build; pnpm re
|
|
|
83
102
|
npx -y @appsoftwareltd/etherpk-mcp@<version> --version # verify, once the registry lists it (a few minutes)
|
|
84
103
|
```
|
|
85
104
|
|
|
86
|
-
The end-to-end specs in `tests-
|
|
105
|
+
The end-to-end specs in `tests-sync/agents/` build and drive this bundle against a real Sync
|
|
87
106
|
Server; the technical notes are in the repo under `docs/docs/technical/Headless Client.md`.
|
|
88
107
|
|
|
89
108
|
Full guide, including how the cache stays current and what an agent can and can't do:
|
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.
|
|
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",
|
|
@@ -653,13 +653,13 @@ function createSyncTokenSource(mint, opts) {
|
|
|
653
653
|
*/
|
|
654
654
|
function createHeadlessAccount(config) {
|
|
655
655
|
const api = createSyncApi({
|
|
656
|
-
baseUrl: config.
|
|
656
|
+
baseUrl: config.syncServer,
|
|
657
657
|
token: config.pat
|
|
658
658
|
});
|
|
659
659
|
return {
|
|
660
660
|
api,
|
|
661
|
-
serverBaseUrl: config.
|
|
662
|
-
relayUrl: relayUrlFrom(config.
|
|
661
|
+
serverBaseUrl: config.syncServer,
|
|
662
|
+
relayUrl: relayUrlFrom(config.syncServer),
|
|
663
663
|
tokenFor: (graphId) => createSyncTokenSource(() => api.mintSyncToken(graphId))
|
|
664
664
|
};
|
|
665
665
|
}
|
|
@@ -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:
|
|
709
|
-
* [[Personal Access Token]]
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
732
|
-
if (typeof
|
|
733
|
-
const out =
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
|
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. */
|
|
@@ -8390,10 +8466,13 @@ function createMcpServer(graph, info) {
|
|
|
8390
8466
|
/**
|
|
8391
8467
|
* `etherpk-mcp`: the [[Headless Client]]'s command line (ADR 0072).
|
|
8392
8468
|
*
|
|
8393
|
-
* etherpk-mcp login --server <url> [--pat <token>] [--recovery-code]
|
|
8394
|
-
* etherpk-mcp graphs
|
|
8395
|
-
* etherpk-mcp serve --graph <id or name>
|
|
8396
|
-
* etherpk-mcp logout
|
|
8469
|
+
* etherpk-mcp login --sync-server <url> [--pat <token>] [--recovery-code]
|
|
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.
|
|
@@ -8408,23 +8487,26 @@ var VERSION = package_default.version;
|
|
|
8408
8487
|
var CMD = /[\\/]_npx[\\/]/.test(process.argv[1] ?? "") ? "npx @appsoftwareltd/etherpk-mcp" : "etherpk-mcp";
|
|
8409
8488
|
var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synced graph)
|
|
8410
8489
|
|
|
8411
|
-
${CMD} login --server <url> [--pat <token>] [--recovery-code]
|
|
8490
|
+
${CMD} login --sync-server <url> [--pat <token>] [--recovery-code]
|
|
8412
8491
|
Sign this machine in as a device of your account. Prompts for a Personal Access
|
|
8413
8492
|
Token (an account-wide one, from the Sync Server portal at <url>/account/tokens)
|
|
8414
8493
|
unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval:
|
|
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
|
|
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
|
|
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
|
-
|
|
8427
|
-
|
|
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,35 +8551,49 @@ async function ask(question, { secret = false } = {}) {
|
|
|
8469
8551
|
stdin.on("data", onData);
|
|
8470
8552
|
});
|
|
8471
8553
|
}
|
|
8472
|
-
async function
|
|
8554
|
+
async function loadConfig(path) {
|
|
8473
8555
|
const config = await readConfig(path);
|
|
8474
|
-
if (!config) fail(
|
|
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
|
|
8480
|
-
|
|
8481
|
-
const
|
|
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: ")));
|
|
8574
|
+
if (!/^https?:\/\//.test(syncServer)) fail("The Sync Server must be an http(s) URL.");
|
|
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.");
|
|
8483
8577
|
const account = await connectAccount({
|
|
8484
|
-
|
|
8578
|
+
syncServer,
|
|
8485
8579
|
pat
|
|
8486
8580
|
});
|
|
8487
|
-
console.log(`Signed in to ${
|
|
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
|
-
|
|
8491
|
-
server,
|
|
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
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
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(
|
|
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
|
|
8579
|
-
if (!
|
|
8580
|
-
const account = await connectAccount(
|
|
8581
|
-
const vault = await openAccountVault(account.api, fromBase64Url(
|
|
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,23 +8737,38 @@ 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
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
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({
|
|
8642
8764
|
args: process.argv.slice(2),
|
|
8643
8765
|
allowPositionals: true,
|
|
8644
8766
|
options: {
|
|
8645
|
-
server: { type: "string" },
|
|
8767
|
+
"sync-server": { type: "string" },
|
|
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
|
|
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
|
}
|