granttap-mcp 0.6.4 → 0.6.7

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.
Files changed (69) hide show
  1. package/README.md +103 -10
  2. package/SECURITY.md +13 -3
  3. package/apps/bridge/src/adapters.ts +50 -0
  4. package/apps/bridge/src/approval-state.ts +715 -0
  5. package/apps/bridge/src/approval.ts +219 -15
  6. package/apps/bridge/src/bin/claude-hook.ts +64 -15
  7. package/apps/bridge/src/bin/codex-hook.ts +44 -6
  8. package/apps/bridge/src/bin/codex-policy-hook.ts +38 -0
  9. package/apps/bridge/src/bin/connect.ts +8 -3
  10. package/apps/bridge/src/bin/cursor-after-shell.ts +97 -0
  11. package/apps/bridge/src/bin/cursor-hook.ts +96 -0
  12. package/apps/bridge/src/bin/cursor-mcp-hook.ts +103 -0
  13. package/apps/bridge/src/bin/pair-uri.ts +28 -0
  14. package/apps/bridge/src/bin/setup.ts +16 -2
  15. package/apps/bridge/src/bin/web.ts +33 -0
  16. package/apps/bridge/src/capabilities.ts +43 -8
  17. package/apps/bridge/src/cloud-approvals.ts +324 -0
  18. package/apps/bridge/src/config.ts +347 -14
  19. package/apps/bridge/src/cursor-mcp-policy.ts +163 -0
  20. package/apps/bridge/src/delivery.ts +61 -13
  21. package/apps/bridge/src/install.ts +491 -66
  22. package/apps/bridge/src/monitor.ts +229 -60
  23. package/apps/bridge/src/pair-uri-file.ts +46 -0
  24. package/apps/bridge/src/pairing.ts +9 -1
  25. package/apps/bridge/src/policy.ts +160 -0
  26. package/apps/bridge/src/scheduler.ts +3 -1
  27. package/apps/bridge/src/session-keys.ts +2 -1
  28. package/apps/bridge/src/sessions/activity-helpers.ts +149 -0
  29. package/apps/bridge/src/sessions/child-threads.ts +77 -0
  30. package/apps/bridge/src/sessions/claude.ts +549 -0
  31. package/apps/bridge/src/sessions/codex.ts +762 -0
  32. package/apps/bridge/src/sessions/common.ts +123 -0
  33. package/apps/bridge/src/sessions/copilot.ts +411 -0
  34. package/apps/bridge/src/sessions/cursor.ts +868 -0
  35. package/apps/bridge/src/sessions/telemetry.ts +562 -0
  36. package/apps/bridge/src/sessions.ts +184 -613
  37. package/apps/mcp/src/bin/authorize.ts +90 -0
  38. package/apps/mcp/src/bin/serve.ts +31 -0
  39. package/apps/mcp/src/bin/status.ts +39 -0
  40. package/apps/mcp/src/create-server.ts +344 -0
  41. package/apps/mcp/src/cursor-config.ts +148 -0
  42. package/apps/mcp/src/http-server.ts +234 -0
  43. package/apps/mcp/src/http-service.ts +396 -0
  44. package/apps/mcp/src/oauth-provider.ts +467 -0
  45. package/apps/mcp/src/pairing-status.ts +25 -0
  46. package/apps/mcp/src/provider-status.ts +239 -0
  47. package/apps/mcp/src/server.ts +6 -225
  48. package/bin/granttap-mcp.mjs +83 -16
  49. package/cursor-plugin/.cursor-plugin/marketplace.json +16 -0
  50. package/cursor-plugin/.cursor-plugin/plugin.json +29 -0
  51. package/cursor-plugin/README.md +73 -0
  52. package/cursor-plugin/assets/logo.svg +7 -0
  53. package/cursor-plugin/commands/connect.md +19 -0
  54. package/cursor-plugin/mcp.json +8 -0
  55. package/cursor-plugin/rules/dual-channel.mdc +11 -0
  56. package/cursor-plugin/skills/connect/SKILL.md +89 -0
  57. package/docs/cursor-authorize.md +67 -0
  58. package/docs/images/apple-watch-approval.png +0 -0
  59. package/docs/images/apple-watch-inbox.png +0 -0
  60. package/docs/images/apple-watch-task.png +0 -0
  61. package/docs/images/iphone-claude-tasks.png +0 -0
  62. package/docs/images/iphone-command-center.png +0 -0
  63. package/docs/images/iphone-mcp-usage.png +0 -0
  64. package/docs/images/iphone-task-detail.png +0 -0
  65. package/docs/images/phone-activity.png +0 -0
  66. package/docs/images/watch-approval.png +0 -0
  67. package/package.json +11 -3
  68. package/packages/core/relay-client.ts +74 -10
  69. package/packages/protocol/schema.ts +186 -12
package/README.md CHANGED
@@ -75,16 +75,23 @@ not separate MCP billing.
75
75
  Add GrantTap to each agent you use:
76
76
 
77
77
  ```bash
78
- codex mcp add granttap -- npx -y granttap-mcp@latest
79
- claude mcp add granttap -- npx -y granttap-mcp@latest
78
+ npm install
79
+ npm install -g .
80
+ codex mcp add granttap -- granttap
81
+ claude mcp add granttap -- granttap
80
82
  ```
81
83
 
84
+ Those commands use this checkout. Publish/release it before replacing the local
85
+ install with an npm registry version.
86
+
82
87
  Start a fresh agent task and say **“Connect GrantTap.”** The `connect` tool:
83
88
 
84
89
  1. creates a new end-to-end encrypted pairing;
85
- 2. returns a scannable one-time QR directly in the agent chat;
86
- 3. installs the Codex and Claude Code approval hooks; and
87
- 4. installs the per-user background helper for task sync and schedules.
90
+ 2. returns a scannable one-time QR directly in the agent chat.
91
+
92
+ Then run the separate MCP `setup` tool. It installs Cursor shell/MCP policy
93
+ hooks, the complete Claude Code matcher, both Codex hooks, and the per-user
94
+ background helper for task sync and schedules.
88
95
 
89
96
  Scan the QR with GrantTap on iPhone. No terminal QR, copied pairing JSON, or
90
97
  open background terminal is required.
@@ -93,8 +100,9 @@ If an MCP client cannot render image content, use the CLI fallback:
93
100
 
94
101
  ```bash
95
102
  npm install -g granttap-mcp
96
- granttap-mcp connect
97
- granttap-mcp setup
103
+ granttap connect
104
+ granttap setup
105
+ granttap status
98
106
  ```
99
107
 
100
108
  CLI `connect` prints a one-time QR and short manual code. `setup` is idempotent:
@@ -102,6 +110,72 @@ it preserves unrelated agent settings and backs up a configuration file before
102
110
  changing it. The pairing is stored locally in `~/.granttap/machine.json`.
103
111
  Existing beta state under `~/.nodvox/` is migrated automatically.
104
112
 
113
+ After setup, open `/hooks` in Codex, review and trust both exact GrantTap hooks,
114
+ then restart Codex. Installation alone is not reported as trusted or connected.
115
+
116
+ ## Cursor Settings → Authorize
117
+
118
+ Cursor only shows **Authorize / Sign in** for **HTTP/SSE** MCP servers that
119
+ speak OAuth (same pattern as Lovable / Figma). The default stdio entry
120
+ (`command` + `args`) cannot show that button — Cursor documents stdio auth as
121
+ **Manual**.
122
+
123
+ To enable Authorize for GrantTap, use the one-step local setup:
124
+
125
+ ```bash
126
+ npm install -g granttap-mcp
127
+ granttap authorize
128
+ ```
129
+
130
+ It preserves unrelated entries in `~/.cursor/mcp.json`, replaces only the
131
+ `granttap` entry with the loopback HTTP endpoint, keeps a one-time
132
+ `.bak-granttap`, and starts the OAuth MCP server. Then open
133
+ **Cursor Settings → MCP → GrantTap → Authorize**. The local consent page shows
134
+ the pairing QR plus a manual-token fallback when the Mac is not paired yet.
135
+ `granttap authorize` installs a loopback-only per-user LaunchAgent, verifies its
136
+ exact `/healthz` identity, writes Cursor config only after that check succeeds,
137
+ and exits. The service uses RunAtLoad + KeepAlive, so closing the terminal or
138
+ restarting the Mac does not leave Cursor pointing at a dead URL.
139
+
140
+ The foreground troubleshooting flow is:
141
+
142
+ 1. Run the local HTTP OAuth server (loopback only):
143
+
144
+ ```bash
145
+ granttap serve
146
+ # listens on http://127.0.0.1:17342/mcp
147
+ ```
148
+
149
+ 2. Point Cursor at the HTTP URL in `~/.cursor/mcp.json` (replace the stdio
150
+ `command` entry):
151
+
152
+ ```json
153
+ {
154
+ "mcpServers": {
155
+ "granttap": {
156
+ "url": "http://127.0.0.1:17342/mcp"
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ 3. Open **Cursor Settings → MCP → GrantTap** and click **Authorize**.
163
+ A local browser page confirms linking Cursor to this Mac’s pairing
164
+ (`~/.granttap`). If unpaired, it shows a one-time QR first.
165
+
166
+ OAuth tokens are stored in `~/.granttap/mcp-oauth.json` (mode `0600`). They do
167
+ **not** replace E2EE pairing keys. Claude Code / Codex can keep using the
168
+ stdio transport; Authorize is a Cursor Settings affordance.
169
+
170
+ Details: [docs/cursor-authorize.md](docs/cursor-authorize.md).
171
+
172
+ `granttap setup` is the policy-hook entry point: it installs Cursor
173
+ `beforeShellExecution`, `afterShellExecution`, and `beforeMCPExecution` hooks,
174
+ plus Claude/Codex hooks and background task sync. It does not opt a new user
175
+ into OAuth; when an exact Cursor HTTP entry already exists, it also repairs the
176
+ persistent OAuth service. Per-chat blocks are checked before phone routing,
177
+ and ambiguous/unscoped Cursor calls fall back to Cursor's native permission UI.
178
+
105
179
  ## Codex and Claude Code: honest capability matrix
106
180
 
107
181
  | Capability | Codex | Claude Code |
@@ -109,7 +183,7 @@ Existing beta state under `~/.nodvox/` is migrated automatically.
109
183
  | Approval hook | `PermissionRequest` when Codex hooks are enabled | `PreToolUse` |
110
184
  | Resume an existing task | Yes, through the local Codex CLI | Yes, through the local Claude CLI |
111
185
  | Start a new persistent task | Yes | Yes |
112
- | Up to five attachments | Images through Codex image inputs; documents as local paths | Local image/document paths in the turn |
186
+ | Up to five attachments within one encrypted-frame budget | Images through Codex image inputs; documents as local paths | Local image/document paths in the turn |
113
187
  | Change filesystem access from iPhone | Read-only, workspace, or full for the next GrantTap turn | Not exposed; the existing Claude policy remains authoritative |
114
188
  | Disable MCP per task | Enforced for later GrantTap-delivered turns | Enforced for later GrantTap-delivered turns |
115
189
  | Usage and context | Reported when present in local task logs | Reported when present in local task logs |
@@ -133,7 +207,7 @@ Repository skills are discovered only in the selected task workspace under
133
207
  | `ask` | Sends an open question and waits for a spoken or typed reply |
134
208
  | `ask_yes_no` | Sends a yes/no question and waits for a tap |
135
209
  | `notify` | Sends a non-blocking status update |
136
- | `setup` | Registers both approval hooks and the terminal-free helper |
210
+ | `setup` | Registers Cursor/Claude/Codex policy hooks and the terminal-free helper |
137
211
 
138
212
  The default answer timeout is three minutes. Override it with
139
213
  `GRANTTAP_ASK_TIMEOUT_MS`.
@@ -152,6 +226,14 @@ The default answer timeout is three minutes. Override it with
152
226
  Durable Objects, and APNs path.
153
227
  - APNs carries only a content-neutral wake. It contains no title, prompt,
154
228
  command, path, task kind, or response.
229
+ - Cloudflare accepts at most a 32 MiB WebSocket frame. Because task messages are
230
+ sealed and base64-encoded twice, attachments share a 16,000,000-character
231
+ base64 budget (about 12 MB raw total); five small files fit, five 6 MB files do not.
232
+
233
+ The in-chat QR is marked user-only for MCP hosts, but that annotation is not a
234
+ cryptographic separation from the model provider. Use CLI `connect` when the
235
+ model provider itself is part of your threat model; Cloudflare still receives
236
+ only the mailbox id and ciphertext in either flow.
155
237
 
156
238
  The relay can still observe operational metadata: opaque room/mailbox IDs, IP
157
239
  addresses, timing, ciphertext sizes, and APNs device token/environment. A
@@ -181,8 +263,19 @@ time, agent, status, result, and created task ID.
181
263
  | Command | Purpose |
182
264
  | --- | --- |
183
265
  | *(no command)* | Starts the GrantTap MCP stdio server |
266
+ | `serve` | HTTP MCP + loopback OAuth for Cursor Settings → Authorize |
267
+ | `authorize` | Installs the persistent loopback OAuth service, verifies health, and configures Cursor |
184
268
  | `connect [relayUrl]` | Creates an E2EE pairing; optionally targets a self-hosted `wss://` relay |
185
- | `setup` | Registers the Codex/Claude hooks and background helper |
269
+ | `setup` | Registers Cursor/Claude/Codex hooks, background sync, and repairs configured OAuth |
270
+ | `status [--json]` | Reads local readiness; JSON uses `granttap.provider-status.v1` and contains no keys |
271
+
272
+ The installed public command is `granttap`; `granttap-mcp` remains an alias for
273
+ existing scripts. `status` only reads configuration and runtime state—it does
274
+ not install hooks, pair a device, start OAuth, or reload the background helper.
275
+ If Cursor has an HTTP GrantTap entry, status requires both an owned persistent
276
+ service and a live identity-checked health response; a dead URL is never shown
277
+ as connected. Without an HTTP entry, OAuth remains optional and Cursor policy
278
+ readiness is based on the full hook set, pairing, and background sync.
186
279
 
187
280
  ## Development
188
281
 
package/SECURITY.md CHANGED
@@ -18,8 +18,8 @@ to an issue, or pasted into logs. The package avoids printing device secret
18
18
  keys. Existing configuration files are backed up before hook installation.
19
19
 
20
20
  Current pairings contain three independent secret classes: NaCl endpoint keys,
21
- random per-task keys, and a random `pushAuth` room credential that only
22
- authorizes APNs device-token registration at the relay. None may be logged.
21
+ random per-task keys, and a random `pushAuth` room credential that authorizes
22
+ the room WebSocket and APNs device-token registration at the relay. None may be logged.
23
23
  The relay receives only a hash of `pushAuth` and never receives an endpoint's
24
24
  secret encryption key.
25
25
 
@@ -31,6 +31,11 @@ secret encryption key.
31
31
  separate random 256-bit transfer key. Only the mailbox id is used in the HTTP
32
32
  path. The key stays in the QR/manual token, so a relay operator or a Durable
33
33
  Object database dump cannot open the parked pairing blob.
34
+ - The terminal `connect` flow keeps that transfer key outside model context.
35
+ The MCP `connect` tool deliberately returns a QR in chat for convenience and
36
+ marks it user-only, but MCP audience annotations are not a cryptographic
37
+ boundary: depending on the host, the chat/model provider may receive tool
38
+ images. Use terminal pairing when the model provider is in your threat model.
34
39
  - Every attached Codex or Claude Code task receives a separate random 256-bit
35
40
  task key. Task messages, attachments, visible activity, access/MCP changes,
36
41
  compaction results, and task-bound approvals use this additional authenticated
@@ -43,6 +48,8 @@ secret encryption key.
43
48
  transport, network, Cloudflare, Durable Objects, and APNs.
44
49
  - APNs is only a content-neutral wake. It contains no task kind, request id,
45
50
  delivery id, title, prompt, command, path, or response.
51
+ It is a best-effort silent background notification; after waking, iOS pulls
52
+ and decrypts the queue and creates the single actionable local notification.
46
53
  - Scheduler-planner turns and their structured drafts cross Cloudflare only as
47
54
  authenticated device-to-device ciphertext. The selected CLI runs locally in
48
55
  ephemeral read-only/plan mode; it cannot silently turn planning into a
@@ -65,7 +72,10 @@ different task's key—not against an already authorized endpoint.
65
72
 
66
73
  The delivery ledger and scheduler history are stored only on the paired Mac.
67
74
  They contain random message ids and local task metadata and are bounded and
68
- expired; do not attach real copies to bug reports.
75
+ expired; do not attach real copies to bug reports. Delivery ACK is emitted only
76
+ after a consumer accepts the decrypted payload. A processing lease recovers
77
+ after a crash; agent execution is at-least-once across that rare crash window,
78
+ because third-party Codex/Claude CLIs do not expose a transactional idempotency key.
69
79
 
70
80
  Chat history and capability metadata follow the same cryptographic boundary.
71
81
  The bridge reads a bounded set of local Codex/Claude logs, emits only chat
@@ -113,6 +113,56 @@ export function decisionToCodexOutput(d: ApprovalDecision): unknown {
113
113
  };
114
114
  }
115
115
 
116
+ // --------------------------------------------------------------------- Cursor
117
+
118
+ export type CursorHookInput = {
119
+ conversation_id?: string;
120
+ generation_id?: string;
121
+ command?: string;
122
+ cwd?: string;
123
+ hook_event_name?: string;
124
+ workspace_roots?: string[];
125
+ session_id?: string;
126
+ tool_name?: string;
127
+ tool_input?: Record<string, unknown>;
128
+ };
129
+
130
+ export function cursorToRequest(input: CursorHookInput): ApprovalRequest {
131
+ const command = typeof input.command === "string" && input.command.trim()
132
+ ? input.command
133
+ : extractCommand("Shell", input.tool_input ?? {});
134
+ const sessionId = input.conversation_id ?? input.session_id;
135
+ const cwd = (typeof input.cwd === "string" && input.cwd.trim() ? input.cwd : undefined)
136
+ ?? (Array.isArray(input.workspace_roots) ? input.workspace_roots[0] : undefined);
137
+ return {
138
+ type: "approval.request",
139
+ requestId: randomId(6),
140
+ agent: "cursor",
141
+ kind: "permission",
142
+ tool: input.tool_name ?? "Shell",
143
+ title: shortTitle("Shell", command),
144
+ command,
145
+ cwd,
146
+ sessionId,
147
+ risk: guessRisk("Shell", command),
148
+ createdAt: Date.now(),
149
+ };
150
+ }
151
+
152
+ export function decisionToCursorOutput(d: ApprovalDecision): unknown {
153
+ const permission = d.decision === "allow" ? "allow" : "deny";
154
+ const message = d.note
155
+ ?? (d.decision === "allow" ? "Approved from GrantTap" : "Denied from GrantTap");
156
+ return {
157
+ permission,
158
+ continue: d.decision === "allow",
159
+ user_message: message,
160
+ agent_message: message,
161
+ userMessage: message,
162
+ agentMessage: message,
163
+ };
164
+ }
165
+
116
166
  // ------------------------------------------------------------------ helpers
117
167
 
118
168
  function shortTitle(tool: string, command: string): string {