@appsoftwareltd/etherpk-mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -11
- package/dist/main.js +85 -10
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,18 +16,26 @@ Open the graph in EtherPK, pick **Settings → Agents**, and copy the two comman
|
|
|
16
16
|
server and graph filled in. They are:
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
# Once per computer. Prompts for an account-wide Personal Access Token (
|
|
20
|
-
#
|
|
21
|
-
# EtherPK
|
|
22
|
-
|
|
19
|
+
# Once per computer. Prompts for an account-wide Personal Access Token (make one at
|
|
20
|
+
# https://server.etherpk.com/account/tokens, also reachable from "Access tokens" in
|
|
21
|
+
# EtherPK's account menu), then shows a short code and the address of your EtherPK:
|
|
22
|
+
# open EtherPK there in a browser where you're signed in with your graphs unlocked -
|
|
23
|
+
# any page will do - and confirm the code. The same step as adding a phone.
|
|
24
|
+
npx @appsoftwareltd/etherpk-mcp login --server https://server.etherpk.com
|
|
25
|
+
|
|
26
|
+
# Self-hosting your own Sync Server? Give its address instead:
|
|
27
|
+
# npx @appsoftwareltd/etherpk-mcp login --server https://sync.your-domain.example
|
|
28
|
+
|
|
29
|
+
# No EtherPK to hand on this computer (a server you reach over SSH, say)? Press r while
|
|
30
|
+
# login is waiting, or use your Recovery Code from the start:
|
|
31
|
+
# npx @appsoftwareltd/etherpk-mcp login --server https://server.etherpk.com --recovery-code
|
|
23
32
|
|
|
24
33
|
# Tell the agent about the graph (Claude Code shown; the Agents tab has the others).
|
|
25
34
|
claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <graph id>
|
|
26
35
|
```
|
|
27
36
|
|
|
28
37
|
`npx @appsoftwareltd/etherpk-mcp graphs` lists the graphs the token can reach, by name and id.
|
|
29
|
-
|
|
30
|
-
instead (or `ETHERPK_RECOVERY_CODE` for a scripted setup; `ETHERPK_PAT` likewise).
|
|
38
|
+
For a scripted setup, `ETHERPK_PAT` and `ETHERPK_RECOVERY_CODE` stand in for the prompts.
|
|
31
39
|
|
|
32
40
|
## What the agent gets
|
|
33
41
|
|
|
@@ -36,11 +44,18 @@ unique old→new replacement that merges with anyone typing elsewhere on the pag
|
|
|
36
44
|
`append_document` (a page, or a day's journal entry, created if needed) and `create_page`.
|
|
37
45
|
One running instance serves one graph.
|
|
38
46
|
|
|
39
|
-
##
|
|
47
|
+
## What it keeps on your computer
|
|
48
|
+
|
|
49
|
+
- **Keys**: `~/.config/etherpk/mcp.json`, readable only by your user - the same trust as a browser
|
|
50
|
+
you've signed in on.
|
|
51
|
+
- **A cache of each graph you serve**: `~/.cache/etherpk/mcp/`, so a restart catches up on what
|
|
52
|
+
changed instead of downloading everything again. It holds your notes readably, like a signed-in
|
|
53
|
+
browser's own storage does. It is never the source of truth: on every start the Sync Server is
|
|
54
|
+
asked what moved and only that is fetched, edits made elsewhere arrive as they happen, and a
|
|
55
|
+
newer version of this program discards a cache it no longer understands and rebuilds it.
|
|
40
56
|
|
|
41
57
|
Revoke the token at the portal to cut the agent off, and `npx @appsoftwareltd/etherpk-mcp logout`
|
|
42
|
-
to forget the
|
|
43
|
-
`~/.config/etherpk/mcp.json` and the cached graphs under `~/.cache/etherpk/mcp/`, both readable
|
|
44
|
-
only by your user - the same trust as a browser you have signed in on.
|
|
58
|
+
to forget the keys and delete the cache on that computer.
|
|
45
59
|
|
|
46
|
-
Full guide:
|
|
60
|
+
Full guide, including how the cache stays current and what an agent can and can't do:
|
|
61
|
+
<https://docs.etherpk.com/using-ai-agents-with-your-notes>.
|
package/dist/main.js
CHANGED
|
@@ -618,7 +618,8 @@ async function connectAccount(config) {
|
|
|
618
618
|
id: me.principal.id,
|
|
619
619
|
email: me.principal.email,
|
|
620
620
|
name: me.principal.name
|
|
621
|
-
}
|
|
621
|
+
},
|
|
622
|
+
clientUrl: me.clientUrl ?? null
|
|
622
623
|
};
|
|
623
624
|
}
|
|
624
625
|
/**
|
|
@@ -858,6 +859,7 @@ z.strictObject({
|
|
|
858
859
|
image: httpsUrl.nullable()
|
|
859
860
|
}),
|
|
860
861
|
authentication: syncAuthenticationSchema,
|
|
862
|
+
clientUrl: httpsUrl.optional(),
|
|
861
863
|
entitlement: z.strictObject({
|
|
862
864
|
plan: z.string().min(1).max(64),
|
|
863
865
|
status: serviceEntitlementSchema.shape.status,
|
|
@@ -1976,37 +1978,50 @@ function serializeClientMessage(message) {
|
|
|
1976
1978
|
* still reads on a white surface, and light enough that dark label text works on all of them
|
|
1977
1979
|
* in both themes. The selection tint is the same colour at 40% alpha (`66`) — the old 20%
|
|
1978
1980
|
* tint of a primary was already faint; a pastel at 20% disappears on white.
|
|
1981
|
+
*
|
|
1982
|
+
* The same eight are offered as the ready-made toolbar colours in Graph Settings (ADR 0071):
|
|
1983
|
+
* one palette, so a graph's bar and a member's caret speak the same language. The label is
|
|
1984
|
+
* for a swatch there; presence itself never shows it (`label`, not `name`: an entry is
|
|
1985
|
+
* spread into a {@link PresenceIdentity}, whose `name` is the pseudonym).
|
|
1979
1986
|
*/
|
|
1980
1987
|
var PRESENCE_PALETTE = [
|
|
1981
1988
|
{
|
|
1989
|
+
label: "Sky",
|
|
1982
1990
|
color: "#7dd3fc",
|
|
1983
1991
|
colorLight: "#7dd3fc66"
|
|
1984
1992
|
},
|
|
1985
1993
|
{
|
|
1994
|
+
label: "Amber",
|
|
1986
1995
|
color: "#fcd34d",
|
|
1987
1996
|
colorLight: "#fcd34d66"
|
|
1988
1997
|
},
|
|
1989
1998
|
{
|
|
1999
|
+
label: "Mint",
|
|
1990
2000
|
color: "#6ee7b7",
|
|
1991
2001
|
colorLight: "#6ee7b766"
|
|
1992
2002
|
},
|
|
1993
2003
|
{
|
|
2004
|
+
label: "Salmon",
|
|
1994
2005
|
color: "#fca5a5",
|
|
1995
2006
|
colorLight: "#fca5a566"
|
|
1996
2007
|
},
|
|
1997
2008
|
{
|
|
2009
|
+
label: "Lavender",
|
|
1998
2010
|
color: "#c4b5fd",
|
|
1999
2011
|
colorLight: "#c4b5fd66"
|
|
2000
2012
|
},
|
|
2001
2013
|
{
|
|
2014
|
+
label: "Pink",
|
|
2002
2015
|
color: "#f9a8d4",
|
|
2003
2016
|
colorLight: "#f9a8d466"
|
|
2004
2017
|
},
|
|
2005
2018
|
{
|
|
2019
|
+
label: "Peach",
|
|
2006
2020
|
color: "#fdba74",
|
|
2007
2021
|
colorLight: "#fdba7466"
|
|
2008
2022
|
},
|
|
2009
2023
|
{
|
|
2024
|
+
label: "Lime",
|
|
2010
2025
|
color: "#bef264",
|
|
2011
2026
|
colorLight: "#bef26466"
|
|
2012
2027
|
}
|
|
@@ -7921,25 +7936,39 @@ async function pollDeviceApproval(api, request) {
|
|
|
7921
7936
|
*
|
|
7922
7937
|
* Pure over an injected clock and output so the flow is testable without a terminal.
|
|
7923
7938
|
*/
|
|
7939
|
+
/** The user left the approval wait on purpose (the signal fired); nothing went wrong. */
|
|
7940
|
+
var ApprovalAbandoned = class extends Error {
|
|
7941
|
+
constructor() {
|
|
7942
|
+
super("Approval abandoned.");
|
|
7943
|
+
this.name = "ApprovalAbandoned";
|
|
7944
|
+
}
|
|
7945
|
+
};
|
|
7924
7946
|
/** Approval rows expire server-side after ten minutes; poll a little longer and then give up. */
|
|
7925
7947
|
var APPROVAL_TIMEOUT_MS = 11 * 6e4;
|
|
7926
7948
|
var APPROVAL_POLL_MS = 2e3;
|
|
7927
7949
|
async function unlockByDeviceApproval(api, io) {
|
|
7928
7950
|
const request = await beginDeviceApproval(api);
|
|
7951
|
+
const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : "open EtherPK in a browser";
|
|
7929
7952
|
io.say("");
|
|
7930
|
-
io.say(`
|
|
7953
|
+
io.say(`To approve this device, ${where} (any page - it need not be a note) signed in to this account`);
|
|
7954
|
+
io.say("with its graphs unlocked. A prompt will show a code; confirm it matches this one:");
|
|
7931
7955
|
io.say("");
|
|
7932
7956
|
io.say(` ${request.sas}`);
|
|
7933
7957
|
io.say("");
|
|
7934
7958
|
io.say("Waiting (up to ten minutes)…");
|
|
7959
|
+
const abandon = async () => {
|
|
7960
|
+
await api.cancelDeviceApproval(request.id).catch(() => {});
|
|
7961
|
+
throw new ApprovalAbandoned();
|
|
7962
|
+
};
|
|
7935
7963
|
const deadline = Date.now() + APPROVAL_TIMEOUT_MS;
|
|
7936
7964
|
while (Date.now() < deadline) {
|
|
7937
|
-
if (io.signal?.aborted)
|
|
7965
|
+
if (io.signal?.aborted) await abandon();
|
|
7938
7966
|
const outcome = await pollDeviceApproval(api, request);
|
|
7939
7967
|
if (outcome.state === "unlocked") return outcome.deviceKey;
|
|
7940
7968
|
if (outcome.state === "rejected") throw new Error("The approval was rejected in EtherPK.");
|
|
7941
7969
|
if (outcome.state === "expired") throw new Error("The approval expired before it was confirmed. Run login again.");
|
|
7942
7970
|
await io.sleep(APPROVAL_POLL_MS);
|
|
7971
|
+
if (io.signal?.aborted) await abandon();
|
|
7943
7972
|
}
|
|
7944
7973
|
throw new Error("The approval was not confirmed in time. Run login again.");
|
|
7945
7974
|
}
|
|
@@ -8320,9 +8349,10 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
|
|
|
8320
8349
|
etherpk-mcp login --server <url> [--pat <token>] [--recovery-code]
|
|
8321
8350
|
Sign this machine in as a device of your account. Prompts for a Personal Access
|
|
8322
8351
|
Token (an account-wide one, from the Sync Server portal at <url>/account/tokens)
|
|
8323
|
-
unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval
|
|
8324
|
-
|
|
8325
|
-
|
|
8352
|
+
unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval:
|
|
8353
|
+
open EtherPK in a browser signed in to the account with its graphs unlocked and
|
|
8354
|
+
confirm the code shown. Press r while waiting, or pass --recovery-code, to type
|
|
8355
|
+
your Recovery Code instead (or ETHERPK_RECOVERY_CODE, for a scripted setup).
|
|
8326
8356
|
etherpk-mcp graphs
|
|
8327
8357
|
List the synced graphs this account can reach, by name and id.
|
|
8328
8358
|
etherpk-mcp serve --graph <id or name>
|
|
@@ -8393,10 +8423,8 @@ async function login(args) {
|
|
|
8393
8423
|
pat
|
|
8394
8424
|
});
|
|
8395
8425
|
console.log(`Signed in to ${server} as ${account.principal.email ?? account.principal.name ?? account.principal.id}.`);
|
|
8396
|
-
const
|
|
8397
|
-
|
|
8398
|
-
sleep: (ms) => new Promise((r) => setTimeout(r, ms))
|
|
8399
|
-
});
|
|
8426
|
+
const byRecoveryCode = async () => unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? await ask("Recovery Code: ", { secret: true }));
|
|
8427
|
+
const vaultKey = args["recovery-code"] ? await byRecoveryCode() : await approveOrFallBack(account, byRecoveryCode);
|
|
8400
8428
|
await writeConfig(path, {
|
|
8401
8429
|
server,
|
|
8402
8430
|
pat,
|
|
@@ -8409,6 +8437,53 @@ async function login(args) {
|
|
|
8409
8437
|
vaultKey: toBase64Url(vaultKey)
|
|
8410
8438
|
});
|
|
8411
8439
|
}
|
|
8440
|
+
/**
|
|
8441
|
+
* Device Approval, with the Recovery Code one keypress away: a user who has no unlocked EtherPK
|
|
8442
|
+
* to hand should not have to Ctrl-C and re-read the help to find `--recovery-code`. On a
|
|
8443
|
+
* terminal, `r` during the wait abandons the approval (cancelled server-side) and asks for the
|
|
8444
|
+
* code instead; without a terminal the wait runs to its outcome.
|
|
8445
|
+
*/
|
|
8446
|
+
async function approveOrFallBack(account, byRecoveryCode) {
|
|
8447
|
+
const abort = new AbortController();
|
|
8448
|
+
const io = {
|
|
8449
|
+
say: (line) => console.log(line),
|
|
8450
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
8451
|
+
signal: abort.signal,
|
|
8452
|
+
clientUrl: account.clientUrl
|
|
8453
|
+
};
|
|
8454
|
+
const stdin = process.stdin;
|
|
8455
|
+
const interactive = stdin.isTTY === true;
|
|
8456
|
+
const onKey = (chunk) => {
|
|
8457
|
+
const key = chunk.toString("utf8");
|
|
8458
|
+
if (key === "r" || key === "R") abort.abort();
|
|
8459
|
+
if (key === "") {
|
|
8460
|
+
console.log("");
|
|
8461
|
+
process.exit(130);
|
|
8462
|
+
}
|
|
8463
|
+
};
|
|
8464
|
+
if (interactive) {
|
|
8465
|
+
console.log("(Press r to type your Recovery Code instead.)");
|
|
8466
|
+
stdin.setRawMode(true);
|
|
8467
|
+
stdin.resume();
|
|
8468
|
+
stdin.on("data", onKey);
|
|
8469
|
+
}
|
|
8470
|
+
let abandoned = false;
|
|
8471
|
+
try {
|
|
8472
|
+
return await unlockByDeviceApproval(account.api, io);
|
|
8473
|
+
} catch (error) {
|
|
8474
|
+
if (!(error instanceof ApprovalAbandoned)) throw error;
|
|
8475
|
+
abandoned = true;
|
|
8476
|
+
} finally {
|
|
8477
|
+
if (interactive) {
|
|
8478
|
+
stdin.off("data", onKey);
|
|
8479
|
+
stdin.setRawMode(false);
|
|
8480
|
+
stdin.pause();
|
|
8481
|
+
}
|
|
8482
|
+
}
|
|
8483
|
+
if (!abandoned) throw new Error("unreachable");
|
|
8484
|
+
console.log("Approval cancelled; unlocking with your Recovery Code instead.");
|
|
8485
|
+
return byRecoveryCode();
|
|
8486
|
+
}
|
|
8412
8487
|
async function listGraphs(config) {
|
|
8413
8488
|
if (!config.vaultKey) fail("Keys are not unlocked on this machine. Run: etherpk-mcp login");
|
|
8414
8489
|
const account = await connectAccount(config);
|