granttap-mcp 0.8.32 → 0.8.33

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
@@ -509,12 +509,14 @@ is left to a trusted terminal, with the command spelled out in the answer. The
509
509
  result — version before and after, the updater's own output — comes back as
510
510
  `tool.update.result`.
511
511
 
512
- Cost is reported as attributed rather than measured, because that is what it
513
- is. A call is read back from the transcript once it has finished, so it can
514
- never be measured directly: an MCP server outlives its calls and is sampled
515
- directly, while a built-in tool leaves nothing behind and is costed from the
516
- samples that fall inside its own start and end. A call with no sample near it
517
- reports nothing rather than a number borrowed from another moment.
512
+ GrantTap derives usage from native provider transcripts and local OS samples;
513
+ it does not ask an agent to write a resource report. An MCP server can be
514
+ sampled directly while it runs. For a completed CLI call, a unique observed
515
+ process tree yields measured CPU time and peak RSS. If only agent-wide samples
516
+ cover a built-in tool call, GrantTap labels its divided share as attributed.
517
+ Ambiguous, unsampled, and historical calls remain unknown rather than borrowing
518
+ a number from another moment. Estimated context tokens are labeled separately
519
+ from provider-reported model tokens.
518
520
 
519
521
  ## Local enforcement
520
522
 
@@ -2,8 +2,12 @@
2
2
 
3
3
  `link.ts` persists the local Mac's opt-in account machine credential in its
4
4
  private GrantTap config directory. The Mac app can enroll through its existing
5
- authorized loopback connection, or the provider MCP consent flow can enroll
6
- after a verified passkey assertion. The credential lets `poller.ts` answer
5
+ authorized loopback connection; the provider MCP consent flow can enroll after
6
+ a verified passkey assertion. `phone-link.ts` lets a QR-paired phone grant a
7
+ machine-scoped credential through the existing encrypted device room after
8
+ passkey sign-in. The Mac verifies it against the account service before saving
9
+ and never receives the phone's account session token. The credential lets
10
+ `poller.ts` answer
7
11
  short-lived, account-authorized phone requests. `offer.ts` creates a new phone
8
12
  controller key in the current room and seals its pairing half to the phone's
9
13
  ephemeral public key. The website stores only the machine token hash and sealed
@@ -0,0 +1,28 @@
1
+ import type { AccountMachineLink } from "../../../../packages/protocol/schema";
2
+ import { loadAccountLink, saveAccountLink } from "./link";
3
+
4
+ /** A trusted QR room can grant only a machine credential, never an account session. */
5
+ export async function acceptPhoneAccountLink(
6
+ link: AccountMachineLink, origin: string, request: typeof fetch = fetch,
7
+ ): Promise<boolean> {
8
+ if (new URL(origin).protocol !== "https:") return false;
9
+ const current = loadAccountLink();
10
+ if (current && current.accountId !== link.accountId) return false;
11
+ const response = await request(`${origin}/api/account/machine/identity`, {
12
+ headers: { authorization: `Bearer ${link.machineToken}`, accept: "application/json" },
13
+ signal: AbortSignal.timeout(8_000), redirect: "error",
14
+ });
15
+ if (!response.ok) return false;
16
+ const identity = await response.json() as { accountId?: string; machineId?: string };
17
+ if (identity.accountId !== link.accountId || identity.machineId !== link.machineId) return false;
18
+ if (current && current.machineId !== link.machineId) {
19
+ const revoked = await request(`${origin}/api/account/machine/self`, {
20
+ method: "DELETE",
21
+ headers: { authorization: `Bearer ${current.machineToken}`, accept: "application/json" },
22
+ signal: AbortSignal.timeout(8_000), redirect: "error",
23
+ });
24
+ if (!revoked.ok && revoked.status !== 401) return false;
25
+ }
26
+ return saveAccountLink({ accountId: link.accountId,
27
+ machineId: link.machineId, machineToken: link.machineToken });
28
+ }
@@ -36,6 +36,7 @@ import { publishSessionEvents } from "./catalog";
36
36
  import { publishHistoryPage } from "../history/pages";
37
37
  import { requestProjectGraphRefresh } from "../graph-refresh";
38
38
  import { handleControllerPairRequest, handleInboundKnowledgeWrite } from "../controller-knowledge";
39
+ import { acceptPhoneAccountLink } from "../../account-recovery/phone-link";
39
40
 
40
41
  export async function handleMonitorMessage(
41
42
  client: RelayClient,
@@ -55,8 +56,8 @@ export async function handleMonitorMessage(
55
56
  // Codex may start one MCP server per open task. Exactly one instance owns
56
57
  // phone routing, so a single phone message can never create duplicate tasks.
57
58
  if (!leadership.acquire()) return false;
58
- if (payload.type === "controller.pair.request") {
59
- return handleControllerPairRequest(client, payload, input.peerPublicKey);
59
+ if (payload.type === "controller.pair.request" || payload.type === "account.machine.link") {
60
+ return handleDeviceEnrollment(client, payload, input.peerPublicKey);
60
61
  }
61
62
  if (payload.type === "provider.hook.trust") {
62
63
  return handleHookTrust(client, payload, input.peerPublicKey, publish);
@@ -138,6 +139,19 @@ export async function handleMonitorMessage(
138
139
  return false;
139
140
  }
140
141
 
142
+ function handleDeviceEnrollment(
143
+ client: RelayClient,
144
+ payload: Extract<Payload, { type: "account.machine.link" | "controller.pair.request" }>,
145
+ peerPublicKey?: string,
146
+ ): Promise<boolean> {
147
+ if (payload.type === "controller.pair.request") {
148
+ return handleControllerPairRequest(client, payload, peerPublicKey);
149
+ }
150
+ if (!peerPublicKey) return Promise.resolve(false);
151
+ const origin = process.env.GRANTTAP_WEBSITE_ORIGIN || "https://granttap.com";
152
+ return acceptPhoneAccountLink(payload, origin).catch(() => false);
153
+ }
154
+
141
155
  async function handleInboundClaimRelease(
142
156
  client: RelayClient, payload: Extract<Payload, { type: "mesh.claim.release" }>,
143
157
  publish: (forceHistory?: boolean) => Promise<void>,
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
10
10
  Install the supported CLI on this computer, then run:
11
11
 
12
12
  ```bash
13
- npm install -g granttap-mcp@0.8.32
13
+ npm install -g granttap-mcp@0.8.33
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -4,7 +4,7 @@
4
4
  "command": "node",
5
5
  "args": [
6
6
  "-e",
7
- "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.32\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
7
+ "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.33\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
8
8
  ]
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
7
7
  const { existsSync } = require("node:fs");
8
8
  const { delimiter, dirname, join } = require("node:path");
9
9
 
10
- const PACKAGE = "granttap-mcp@0.8.32";
10
+ const PACKAGE = "granttap-mcp@0.8.33";
11
11
  const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
12
12
  const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
13
13
  ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.32",
3
+ "version": "0.8.33",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+
3
+ /** A QR-paired phone grants this computer a machine-scoped account credential. */
4
+ export const AccountMachineLink = z.object({
5
+ type: z.literal("account.machine.link"),
6
+ accountId: z.string().uuid(),
7
+ machineId: z.string().uuid(),
8
+ machineToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
9
+ createdAt: z.number().nonnegative(),
10
+ }).strict();
11
+ export type AccountMachineLink = z.infer<typeof AccountMachineLink>;
@@ -33,6 +33,7 @@ import {
33
33
  import { MachineLoad } from "./messages/machine";
34
34
  import { PairingJoin } from "./messages/pairing-join";
35
35
  import { ControllerPairOffer, ControllerPairRequest } from "./messages/device/controller-pairing";
36
+ import { AccountMachineLink } from "./messages/device/account-machine-link";
36
37
  import { KnowledgeWrite, KnowledgeWriteResult } from "./messages/mesh/knowledge/write";
37
38
  import { MeshInvocationPage, MeshInvocationQuery } from "./messages/mesh/invocations";
38
39
  import {
@@ -68,6 +69,7 @@ export * from "./messages/interaction";
68
69
  export * from "./messages/machine";
69
70
  export * from "./messages/pairing-join";
70
71
  export * from "./messages/device/controller-pairing";
72
+ export * from "./messages/device/account-machine-link";
71
73
  export * from "./messages/mesh/knowledge/write";
72
74
  export * from "./messages/mesh";
73
75
  export * from "./messages/mesh/invocations";
@@ -157,6 +159,7 @@ export const Payload = z.union([
157
159
  PairingJoin,
158
160
  ControllerPairRequest,
159
161
  ControllerPairOffer,
162
+ AccountMachineLink,
160
163
  KnowledgeWrite,
161
164
  KnowledgeWriteResult,
162
165
  ]).superRefine((payload, ctx) => {