@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 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 (from your Sync
20
- # Server's portal, Account → Tokens), then shows a short code to confirm in any unlocked
21
- # EtherPK tab - the same step as adding a phone.
22
- npx @appsoftwareltd/etherpk-mcp login --server https://sync.example.com
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
- On a computer with no EtherPK tab to hand, `login --recovery-code` takes your Recovery Code
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
- ## Stopping it
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 token, keys and cached graphs on that computer. The keys live in
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: <https://docs.etherpk.com/using-ai-agents-with-your-notes>.
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(`Approve this device from EtherPK: open any unlocked tab of this account and confirm the code`);
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) throw new Error("Cancelled.");
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
- (confirm a code in any unlocked EtherPK tab) or, with --recovery-code, by typing
8325
- your Recovery Code (or ETHERPK_RECOVERY_CODE, for a scripted setup).
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 vaultKey = args["recovery-code"] ? await unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? await ask("Recovery Code: ", { secret: true })) : await unlockByDeviceApproval(account.api, {
8397
- say: (line) => console.log(line),
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);