granttap-mcp 0.8.52 → 0.8.54

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.
@@ -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.52
13
+ npm install -g granttap-mcp@0.8.54
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.52\";\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.54\";\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.52";
10
+ const PACKAGE = "granttap-mcp@0.8.54";
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.52",
3
+ "version": "0.8.54",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,3 +11,11 @@ Tests live in `tests/`; they include an actual WebSocket server with the relay's
11
11
  rate limit. A socket write is transport evidence, never an approval receipt.
12
12
 
13
13
  MIT License; see the repository LICENSE.
14
+ # Active controllers
15
+
16
+ Authenticated incoming phone traffic marks that controller active for 90 seconds.
17
+ Large replaceable encrypted task/project data targets recent controllers while
18
+ catalogs, liveness, approval status and reliable messages retain every authorized
19
+ recipient. Current catalogs and liveness pass queued bulk frames. A transport with no recent controller falls back to
20
+ the complete authorized set for legacy clients and reconnection. Routing never
21
+ deletes a pairing or replaces the controller authorization gate.
@@ -0,0 +1,15 @@
1
+ /** Activity routes replaceable traffic; authorization remains a separate check. */
2
+ export class ActiveControllerRecipients {
3
+ private readonly seen = new Map<string, number>();
4
+
5
+ observe(peer: string, now = Date.now()): void {
6
+ this.seen.set(peer, now);
7
+ }
8
+
9
+ allows(peer: string, replaceable: boolean, now = Date.now()): boolean {
10
+ if (!replaceable) return true;
11
+ const active = [...this.seen].filter(([, at]) => now >= at && now - at <= 90_000);
12
+ // A newly connected transport has not heard its controllers yet.
13
+ return active.length === 0 || active.some(([key]) => key === peer);
14
+ }
15
+ }
@@ -3,6 +3,7 @@ import type { PeerConfig, SendOptions } from "../relay-client-types";
3
3
  import type { Payload, Role } from "../../protocol/schema";
4
4
  import { peerPublicKeys } from "../crypto";
5
5
  import { sealEnvelope } from "../relay-envelope";
6
+ export { ActiveControllerRecipients } from "./active-controllers";
6
7
 
7
8
  type Job = { frame: string; bytes: number; resolve: () => void; reject: (error: Error) => void };
8
9
  const CLOSED = () => new Error("relay connection closed before delivery");
@@ -68,7 +69,8 @@ export class OutboundFrameQueue {
68
69
  }
69
70
 
70
71
  const control = (type: Payload["type"]) => type === "hello" || type.startsWith("approval.")
71
- || type.startsWith("config.") || type.startsWith("pairing.") || type === "session.key.grant";
72
+ || type.startsWith("config.") || type.startsWith("pairing.") || type === "session.key.grant"
73
+ || type === "sessions.status" || type === "machine.heartbeat" || type === "approvals.status";
72
74
 
73
75
  export async function sendEncryptedPayload(queue: OutboundFrameQueue, config: PeerConfig,
74
76
  allowed: (key: string) => boolean,
@@ -10,7 +10,7 @@
10
10
  * the Cloudflare Durable Object deployment.
11
11
  */
12
12
  import WebSocket from "ws";
13
- import { OutboundFrameQueue, sendEncryptedPayload } from "./outbound-scheduler";
13
+ import { OutboundFrameQueue, sendEncryptedPayload, ActiveControllerRecipients } from "./outbound-scheduler";
14
14
  import { createHash } from "node:crypto";
15
15
  import { Envelope, Payload, type Role } from "../protocol/schema";
16
16
  import { openFromPeersWithIdentity, openWithTransferKey, peerPublicKeys, seal, sealWithTransferKey } from "./crypto";
@@ -34,6 +34,7 @@ export class RelayClient {
34
34
  private livenessTimer?: NodeJS.Timeout;
35
35
  private awaitingPong = false;
36
36
  private readonly sessionKeys = new Map<string, string>();
37
+ private readonly activeControllers = new ActiveControllerRecipients();
37
38
  /**
38
39
  * Delivery ids are relay-controlled metadata, so replay protection is bound
39
40
  * to the authenticated ciphertext itself. Keep a bounded process-local set:
@@ -199,6 +200,9 @@ export class RelayClient {
199
200
  this.sessionKeys.set(payload.sessionId, payload.key);
200
201
  }
201
202
  if (this.processingCiphertexts.has(fingerprint)) return;
203
+ if (this.cfg.role === "machine" && (payload.type !== "hello" || payload.role === "phone")) {
204
+ this.activeControllers.observe(peerPublicKey);
205
+ }
202
206
  this.processingCiphertexts.add(fingerprint);
203
207
  try {
204
208
  const results = await Promise.all([...this.listeners].map(async (listener) => {
@@ -234,7 +238,11 @@ export class RelayClient {
234
238
  const ws = this.ws;
235
239
  if (!ws || ws.readyState !== WebSocket.OPEN) throw new Error("relay not connected");
236
240
  if (!this.frames) throw new Error("relay not connected");
237
- await sendEncryptedPayload(this.frames, this.cfg, key => this.peerIsAllowed(key), { payload, to, options });
241
+ const replaceable = this.cfg.role === "machine" && to === "phone"
242
+ && options.reliable === false && payload.type === "session.sealed";
243
+ await sendEncryptedPayload(this.frames, this.cfg,
244
+ key => this.peerIsAllowed(key) && this.activeControllers.allows(key, replaceable),
245
+ { payload, to, options });
238
246
  }
239
247
 
240
248
  /** Add a newly issued controller without waiting for the monitor to restart. */