granttap-mcp 0.2.0 → 0.6.4

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
@@ -1,38 +1,95 @@
1
1
  # GrantTap MCP
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/granttap-mcp)](https://www.npmjs.com/package/granttap-mcp)
4
- [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
4
+ [![CI](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
5
+ [![Node.js 20+](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](package.json)
6
+ [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ **Keep Codex and Claude Code moving from your iPhone or Apple Watch.**
9
+
10
+ Approve commands, follow active tasks, reply, attach files, start new work, and
11
+ run local schedules without exposing agent traffic to the relay. GrantTap MCP is
12
+ the open-source machine-side bridge: the agents stay on your Mac, and Cloudflare
13
+ only routes authenticated ciphertext it cannot decrypt.
14
+
15
+ [Website](https://granttap.com) ·
16
+ [npm](https://www.npmjs.com/package/granttap-mcp) ·
17
+ [Security model](SECURITY.md) ·
18
+ [Self-hostable relay](https://github.com/sergii-ziborov/granttap-relay)
19
+
20
+ <p align="center">
21
+ <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-command-center.png" alt="GrantTap command center on iPhone showing a Codex approval, an agent question, task search, agent switcher, chat history, and MCP usage" width="330">
22
+ &nbsp;&nbsp;&nbsp;
23
+ <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-approval.png" alt="GrantTap Codex approval on Apple Watch" width="230">
24
+ </p>
25
+
26
+ <p align="center"><sub>Current iPhone and Apple Watch UI captured from the app's clearly labelled demo mode — no concept renders.</sub></p>
27
+
28
+ The companion iPhone and Apple Watch app is currently in testing. Release
29
+ status is published at [granttap.com](https://granttap.com).
30
+
31
+ ## What you get
32
+
33
+ - **Real approvals away from the Mac.** Claude Code or Codex pauses at its
34
+ permission hook; Allow or Deny returns to that same agent flow.
35
+ - **One task view for both agents.** See recent Codex and Claude Code tasks,
36
+ human-readable activity, delivery state, usage, and context data the agent
37
+ actually exposes.
38
+ - **Continue work from the phone.** Reply to an existing task or create a new
39
+ one for the selected agent and advertised workspace. Send up to five photos,
40
+ camera images, or documents in one message.
41
+ - **A useful Watch app, not an approval-only notification.** Browse tasks, open
42
+ recent activity, approve a command, or reply by voice or text.
43
+ - **Per-task controls that are enforced locally.** Inspect MCP servers and
44
+ repository skills, disable MCP servers for later GrantTap-delivered turns,
45
+ choose a relevant MCP or skill, and select Codex sandbox access.
46
+ - **A local scheduler, not a pretend provider API.** Create recurring Codex or
47
+ Claude Code runs manually or through a conversational planner, then inspect
48
+ the run history from the phone.
49
+ - **Receipts instead of optimistic UI.** Messages have stable IDs, encrypted
50
+ accepted/rejected receipts, bounded retries, and queued/sending/delivered/
51
+ failed state.
52
+
53
+ ## See it in action
54
+
55
+ ### iPhone
56
+
57
+ | Command center | Codex task controls | Observed MCP usage | Claude Code and scheduler |
58
+ | --- | --- | --- | --- |
59
+ | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-command-center.png" alt="GrantTap iPhone command center" width="210"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-task-detail.png" alt="Codex task context, MCP permissions, and sandbox access in GrantTap" width="210"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-mcp-usage.png" alt="Observed MCP and skill usage on iPhone" width="210"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/iphone-claude-tasks.png" alt="Claude Code task list and conversational scheduler on iPhone" width="210"> |
60
+
61
+ The Codex task screen exposes the context window, supported compaction, MCP
62
+ allow/deny state, and sandbox access. Claude Code gets its own visual treatment
63
+ and planner while keeping its different permission and compaction limits clear.
64
+ MCP usage counts only observed calls; context figures are labelled estimates,
65
+ not separate MCP billing.
66
+
67
+ ### Apple Watch
68
+
69
+ | Task inbox | Recent activity and reply | Command approval |
70
+ | --- | --- | --- |
71
+ | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-inbox.png" alt="GrantTap task and approval inbox on Apple Watch" width="230"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-task.png" alt="GrantTap task activity with voice and text reply on Apple Watch" width="230"> | <img src="https://raw.githubusercontent.com/sergii-ziborov/granttap-mcp/main/docs/images/apple-watch-approval.png" alt="Codex command approval on Apple Watch" width="230"> |
72
+
73
+ ## Connect in under a minute
74
+
75
+ Add GrantTap to each agent you use:
5
76
 
6
- The public, auditable machine-side bridge for [GrantTap](https://granttap.com):
7
- ask a person, send a status update, or request an approval on their iPhone or
8
- Apple Watch without sending agent traffic through a model proxy.
9
-
10
- GrantTap MCP complements the agents' native permission hooks:
11
-
12
- - MCP is the voluntary channel: the agent calls `ask`, `ask_yes_no`, or `notify`.
13
- - Hooks are the mandatory approval channel: Claude Code or Codex pauses before
14
- a tool call and waits for Allow or Deny.
15
- - Both channels use the same end-to-end encrypted GrantTap pairing.
16
-
17
- | Visible iPhone activity | Approval on Apple Watch |
18
- | --- | --- |
19
- | ![GrantTap activity on iPhone](docs/images/phone-activity.png) | ![GrantTap approval on Apple Watch](docs/images/watch-approval.png) |
20
-
21
- The iPhone and Apple Watch app is preparing for App Store release. Current
22
- status and target date are published at [granttap.com](https://granttap.com).
23
- Support, privacy, terms, and license information are linked below and from the
24
- app's About screen.
77
+ ```bash
78
+ codex mcp add granttap -- npx -y granttap-mcp@latest
79
+ claude mcp add granttap -- npx -y granttap-mcp@latest
80
+ ```
25
81
 
26
- ## Install
82
+ Start a fresh agent task and say **“Connect GrantTap.”** The `connect` tool:
27
83
 
28
- The shortest MCP-only setup is:
84
+ 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.
29
88
 
30
- ```bash
31
- codex mcp add granttap -- npx -y granttap-mcp
32
- claude mcp add granttap -- npx -y granttap-mcp
33
- ```
89
+ Scan the QR with GrantTap on iPhone. No terminal QR, copied pairing JSON, or
90
+ open background terminal is required.
34
91
 
35
- Pair this computer with the GrantTap app, then register the approval hooks:
92
+ If an MCP client cannot render image content, use the CLI fallback:
36
93
 
37
94
  ```bash
38
95
  npm install -g granttap-mcp
@@ -40,51 +97,92 @@ granttap-mcp connect
40
97
  granttap-mcp setup
41
98
  ```
42
99
 
43
- `connect` prints a QR and an optional one-time eight-character code. It uses
44
- the production zero-knowledge relay by default; pass your own `wss://` URL as
45
- the first argument to self-host. The pairing lives locally in
46
- `~/.granttap/machine.json`; this package never uploads the device secret key.
47
- Existing beta installations using `~/.nodvox/` are migrated automatically.
48
-
49
- `connect` also registers both hooks. `setup` is an idempotent standalone
50
- command for upgrades: it preserves unrelated settings and writes a backup
51
- before changing a configuration file.
100
+ CLI `connect` prints a one-time QR and short manual code. `setup` is idempotent:
101
+ it preserves unrelated agent settings and backs up a configuration file before
102
+ changing it. The pairing is stored locally in `~/.granttap/machine.json`.
103
+ Existing beta state under `~/.nodvox/` is migrated automatically.
104
+
105
+ ## Codex and Claude Code: honest capability matrix
106
+
107
+ | Capability | Codex | Claude Code |
108
+ | --- | --- | --- |
109
+ | Approval hook | `PermissionRequest` when Codex hooks are enabled | `PreToolUse` |
110
+ | Resume an existing task | Yes, through the local Codex CLI | Yes, through the local Claude CLI |
111
+ | 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 |
113
+ | Change filesystem access from iPhone | Read-only, workspace, or full for the next GrantTap turn | Not exposed; the existing Claude policy remains authoritative |
114
+ | Disable MCP per task | Enforced for later GrantTap-delivered turns | Enforced for later GrantTap-delivered turns |
115
+ | Usage and context | Reported when present in local task logs | Reported when present in local task logs |
116
+ | Trigger real context compaction | Yes, for an idle task through Codex app-server | No supported remote API; GrantTap reports this honestly |
117
+ | Conversational schedule planner | Ephemeral read-only run | Ephemeral plan-mode run |
118
+
119
+ GrantTap controls local Codex and Claude Code tasks. It does **not** claim to
120
+ create ordinary ChatGPT chats, private ChatGPT Scheduled Tasks, Codex
121
+ Automations, or Claude Routines through unpublished provider APIs.
122
+
123
+ Globally disabled MCP servers stay disabled. A phone choice only narrows later
124
+ turns sent through GrantTap; it cannot broaden the agent's global configuration.
125
+ Repository skills are discovered only in the selected task workspace under
126
+ `.agents/skills` or `.claude/skills`.
52
127
 
53
128
  ## MCP tools
54
129
 
55
130
  | Tool | Result |
56
131
  | --- | --- |
132
+ | `connect` | Creates a pairing and returns a secure one-time QR in chat |
57
133
  | `ask` | Sends an open question and waits for a spoken or typed reply |
58
134
  | `ask_yes_no` | Sends a yes/no question and waits for a tap |
59
- | `notify` | Sends a non-blocking status message |
60
- | `setup` | Registers the Claude Code and Codex approval hooks |
61
-
62
- ## CLI commands
63
-
64
- | Command | Purpose |
65
- | --- | --- |
66
- | *(no command)* | Starts the GrantTap MCP stdio server |
67
- | `connect [relayUrl]` | Creates an E2EE pairing and prints a QR/short code |
68
- | `setup` | Registers the Claude Code and Codex approval hooks |
135
+ | `notify` | Sends a non-blocking status update |
136
+ | `setup` | Registers both approval hooks and the terminal-free helper |
69
137
 
70
138
  The default answer timeout is three minutes. Override it with
71
139
  `GRANTTAP_ASK_TIMEOUT_MS`.
72
140
 
73
- ## Security model
141
+ ## Why the relay cannot read a session
142
+
143
+ - Endpoint keys are generated locally. The relay never receives a device's
144
+ secret encryption key.
145
+ - Pairing hand-off uses an opaque mailbox ID plus an independent 256-bit
146
+ transfer key. Only the mailbox ID reaches Cloudflare; the key stays in the QR
147
+ or manual token.
148
+ - Every attached Codex or Claude Code task receives its own random 256-bit task
149
+ key. Disclosure of one task key cannot decrypt another task.
150
+ - Questions, commands, replies, attachments, approvals, scheduler drafts, and
151
+ task activity remain authenticated ciphertext across the network, relay,
152
+ Durable Objects, and APNs path.
153
+ - APNs carries only a content-neutral wake. It contains no title, prompt,
154
+ command, path, task kind, or response.
155
+
156
+ The relay can still observe operational metadata: opaque room/mailbox IDs, IP
157
+ addresses, timing, ciphertext sizes, and APNs device token/environment. A
158
+ compromised authorized endpoint can read the tasks explicitly granted to that
159
+ endpoint; cryptography cannot hide plaintext from a device authorized to show
160
+ it. The exact boundary and threat-model limits are documented in
161
+ [SECURITY.md](SECURITY.md).
162
+
163
+ ## Task sync and scheduling
164
+
165
+ The background helper publishes a bounded window of recent local Codex and
166
+ Claude Code task metadata. Older chat metadata is available separately for up
167
+ to 90 days and 160 chats; full activity for a task is sent only after the phone
168
+ subscribes to it. Hidden reasoning is never converted into visible activity.
169
+
170
+ New phone-created tasks default to an isolated per-agent GrantTap workspace.
171
+ The phone can instead select a same-agent folder already advertised by a recent
172
+ local task. The helper rejects arbitrary unadvertised paths.
173
+
174
+ Schedules use standard five-field cron in the Mac's timezone. The app supports
175
+ hourly, daily, weekday, selected-weekday, and monthly series, plus enable,
176
+ disable, edit, delete, and run-now actions. Every run records local start/end
177
+ time, agent, status, result, and created task ID.
74
178
 
75
- Message payloads are authenticated and encrypted locally with NaCl
76
- public-key boxes. The relay receives only routing metadata and opaque
77
- ciphertext. It has no device secret key and cannot decrypt questions,
78
- commands, replies, or approvals.
79
-
80
- This repository intentionally includes the protocol, crypto client, relay
81
- client, MCP server, and agent hook adapters so that the complete public
82
- machine-side trust boundary can be reviewed.
179
+ ## CLI commands
83
180
 
84
- The relay still observes metadata such as room identifiers, IP addresses,
85
- timing, and message sizes. Review or self-host the public
86
- [GrantTap relay](https://github.com/sergii-ziborov/granttap-relay) if that
87
- metadata matters to your deployment.
181
+ | Command | Purpose |
182
+ | --- | --- |
183
+ | *(no command)* | Starts the GrantTap MCP stdio server |
184
+ | `connect [relayUrl]` | Creates an E2EE pairing; optionally targets a self-hosted `wss://` relay |
185
+ | `setup` | Registers the Codex/Claude hooks and background helper |
88
186
 
89
187
  ## Development
90
188
 
@@ -99,16 +197,15 @@ npm run typecheck
99
197
  ```
100
198
 
101
199
  Start the stdio server with `npm start`. Run `npm run setup` only on a machine
102
- where you want the hooks installed; existing config files are backed up once
103
- as `*.bak-granttap`.
200
+ where you want GrantTap hooks installed.
104
201
 
105
- ## Related
202
+ ## Links
106
203
 
107
- - Product: [granttap.com](https://granttap.com)
108
- - npm: [granttap-mcp](https://www.npmjs.com/package/granttap-mcp)
109
- - Relay: [sergii-ziborov/granttap-relay](https://github.com/sergii-ziborov/granttap-relay)
110
- - Privacy: [granttap.com/privacy](https://granttap.com/privacy)
111
- - Support: [granttap.com/support](https://granttap.com/support)
112
- - Security policy: [SECURITY.md](SECURITY.md)
204
+ - [GrantTap product site](https://granttap.com)
205
+ - [granttap-mcp on npm](https://www.npmjs.com/package/granttap-mcp)
206
+ - [Public relay source](https://github.com/sergii-ziborov/granttap-relay)
207
+ - [Privacy](https://granttap.com/privacy)
208
+ - [Support](https://granttap.com/support)
209
+ - [Security policy](SECURITY.md)
113
210
 
114
211
  GrantTap is not affiliated with Anthropic or OpenAI.
package/SECURITY.md CHANGED
@@ -6,7 +6,7 @@ Use GitHub's private
6
6
  [security advisory form](https://github.com/sergii-ziborov/granttap-mcp/security/advisories/new).
7
7
  For urgent coordination, contact `sergii.ziborov@gmail.com`.
8
8
 
9
- Do not open a public issue containing pairing codes, device keys, room
9
+ Do not open a public issue containing pairing tokens, device keys, room
10
10
  identifiers, real prompts, command payloads, local configuration, or a working
11
11
  exploit. Include the affected package version or commit, expected impact, and
12
12
  a minimal reproduction using synthetic data.
@@ -17,5 +17,61 @@ Pairing state belongs in `~/.granttap/` and must never be committed, attached
17
17
  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
+ 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.
23
+ The relay receives only a hash of `pushAuth` and never receives an endpoint's
24
+ secret encryption key.
25
+
26
+ ## Cryptographic boundary
27
+
28
+ - Every device pairing is generated locally and has unique Curve25519 secret
29
+ keys. A device from another pairing cannot authenticate or decrypt it.
30
+ - Pairing hand-off v2 uses a relay-visible random 128-bit mailbox id and a
31
+ separate random 256-bit transfer key. Only the mailbox id is used in the HTTP
32
+ path. The key stays in the QR/manual token, so a relay operator or a Durable
33
+ Object database dump cannot open the parked pairing blob.
34
+ - Every attached Codex or Claude Code task receives a separate random 256-bit
35
+ task key. Task messages, attachments, visible activity, access/MCP changes,
36
+ compaction results, and task-bound approvals use this additional authenticated
37
+ encryption layer. A key copied from one task cannot decrypt another task.
38
+ - Task keys are delivered only inside the already authenticated device-to-device
39
+ NaCl channel and are stored in device-only Keychain on iOS and a mode `0600`
40
+ file on the agent Mac.
41
+ - Plaintext exists only at an authorized endpoint. Every route between the Mac
42
+ and iPhone remains authenticated ciphertext while it crosses the app
43
+ transport, network, Cloudflare, Durable Objects, and APNs.
44
+ - APNs is only a content-neutral wake. It contains no task kind, request id,
45
+ delivery id, title, prompt, command, path, or response.
46
+ - Scheduler-planner turns and their structured drafts cross Cloudflare only as
47
+ authenticated device-to-device ciphertext. The selected CLI runs locally in
48
+ ephemeral read-only/plan mode; it cannot silently turn planning into a
49
+ persistent task or workspace mutation.
50
+
51
+ Cloudflare can observe service metadata required to operate the relay: an
52
+ opaque room/mailbox id, routing role, timestamps/expiry, IP address, ciphertext
53
+ size, APNs device token/environment, and a content-neutral wake flag. It cannot
54
+ derive plaintext from those fields or from its stored database.
55
+
56
+ The honest limit: a device can decrypt every task key that was explicitly
57
+ granted to that device. Possession of a device authorized for task A alone
58
+ cannot decrypt another pairing or task B unless B's independent key was also
59
+ granted to it. Cryptography cannot both authorize a device for a task and
60
+ prevent that same device from reading it. Compromise of the agent Mac is
61
+ also outside the relay threat model because the Mac necessarily has the local
62
+ agent transcripts and keys. The isolation guarantee is against Cloudflare,
63
+ database/network compromise, other pairings/devices, and disclosure of a
64
+ different task's key—not against an already authorized endpoint.
65
+
66
+ The delivery ledger and scheduler history are stored only on the paired Mac.
67
+ They contain random message ids and local task metadata and are bounded and
68
+ expired; do not attach real copies to bug reports.
69
+
70
+ Chat history and capability metadata follow the same cryptographic boundary.
71
+ The bridge reads a bounded set of local Codex/Claude logs, emits only chat
72
+ metadata and visible activity, and places it inside the existing device or
73
+ independent per-task NaCl box. Structured MCP/skill fields describe observed
74
+ tool calls; hidden reasoning is never turned into activity or usage records.
75
+
20
76
  Relay vulnerabilities should be reported through the
21
77
  [GrantTap relay advisory form](https://github.com/sergii-ziborov/granttap-relay/security/advisories/new).
@@ -8,6 +8,7 @@
8
8
  import type { PeerConfig } from "../../../packages/core/relay-client";
9
9
  import { RelayClient } from "../../../packages/core/relay-client";
10
10
  import type { ApprovalDecision, ApprovalRequest, Payload } from "../../../packages/protocol/schema";
11
+ import { primeSessionKeys, sendSessionPayload } from "./session-keys";
11
12
 
12
13
  export type RequestApprovalOpts = {
13
14
  timeoutMs?: number;
@@ -23,6 +24,7 @@ export async function requestApproval(
23
24
  const timeoutMs = opts.timeoutMs ?? 60_000;
24
25
  const client = opts.client ?? new RelayClient(cfg);
25
26
  const ownsClient = !opts.client;
27
+ primeSessionKeys(client);
26
28
 
27
29
  try {
28
30
  if (ownsClient) {
@@ -37,7 +39,16 @@ export async function requestApproval(
37
39
  };
38
40
  }
39
41
  }
40
- await client.send(req, "phone", { ttlMs: timeoutMs });
42
+ // The request id is random and carries no task content. Reusing it as the
43
+ // opaque delivery id lets a generic APNs action answer the request even if
44
+ // iOS has not yet pulled and decrypted the full card.
45
+ const sendOptions = {
46
+ ttlMs: timeoutMs,
47
+ wake: true,
48
+ deliveryId: req.requestId,
49
+ };
50
+ if (req.sessionId) await sendSessionPayload(client, req, req.sessionId, "phone", sendOptions);
51
+ else await client.send(req, "phone", sendOptions);
41
52
  const decision = await client
42
53
  .waitFor(
43
54
  (p: Payload): p is ApprovalDecision =>
@@ -1,15 +1,7 @@
1
1
  import QRCode from "qrcode";
2
- import { generatePairingCode, sealWithCode } from "../../../../packages/core/crypto";
3
- import { installClaudeHook, installCodexHook, type InstallResult } from "../install";
4
- import {
5
- createPairing,
6
- machineConfigPath,
7
- pairingUri,
8
- phonePairingPath,
9
- saveConfig,
10
- } from "../config";
11
-
12
- const DEFAULT_RELAY = "wss://granttap-relay.sergii-ziborov.workers.dev";
2
+ import type { InstallResult } from "../install";
3
+ import { machineConfigPath, phonePairingPath } from "../config";
4
+ import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from "../pairing";
13
5
 
14
6
  function hookLine(name: string, result: InstallResult): string {
15
7
  switch (result.status) {
@@ -23,69 +15,48 @@ function hookLine(name: string, result: InstallResult): string {
23
15
  }
24
16
 
25
17
  async function connect(relayUrl: string): Promise<void> {
26
- const { machineCfg, phoneCfg } = createPairing(relayUrl);
27
- saveConfig(machineConfigPath(), machineCfg);
28
- saveConfig(phonePairingPath(), phoneCfg);
29
-
30
- const uri = pairingUri(phoneCfg);
31
- const qr = await QRCode.toString(uri, {
18
+ const pairing = await createOneTimePairing(relayUrl);
19
+ const qr = await QRCode.toString(pairing.qrPayload, {
32
20
  type: "terminal",
33
21
  small: true,
34
22
  errorCorrectionLevel: "L",
35
23
  });
36
- process.stdout.write(["", " Scan this code in GrantTap on iPhone:", "", qr].join("\n"));
37
-
38
- const code = generatePairingCode(8);
39
- const sealed = sealWithCode(phoneCfg, code);
40
- const httpBase = relayUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
41
- let codeReady = false;
42
- try {
43
- const response = await fetch(`${httpBase}/pair/${code}`, {
44
- method: "PUT",
45
- headers: { "content-type": "application/json" },
46
- body: JSON.stringify(sealed),
47
- });
48
- codeReady = response.ok;
49
- } catch {
50
- codeReady = false;
51
- }
52
-
53
24
  process.stdout.write(
54
- codeReady
55
- ? [
56
- "",
57
- " Or enter this one-time code in the app:",
58
- "",
59
- ` ${code.slice(0, 4)} ${code.slice(4)}`,
60
- "",
61
- ` Relay: ${httpBase}`,
62
- " The code expires after 15 minutes and can be used once.",
63
- "",
64
- ].join("\n")
65
- : [
66
- "",
67
- ` ! The relay at ${httpBase} did not accept a short code.`,
68
- " The QR still works. Check the relay and retry for code pairing.",
69
- "",
70
- ].join("\n"),
25
+ [
26
+ "",
27
+ " Scan this one-time code in GrantTap on iPhone:",
28
+ "",
29
+ qr,
30
+ "",
31
+ " Or paste this one-time secure token in the app:",
32
+ "",
33
+ ` ${pairing.manualToken}`,
34
+ "",
35
+ ` Relay: ${pairing.httpBase}`,
36
+ ` The encrypted mailbox expires after ${PAIRING_CODE_TTL_MINUTES} minutes and can be used once.`,
37
+ " Its 256-bit key is never sent to the relay.",
38
+ "",
39
+ ].join("\n"),
71
40
  );
72
41
 
73
- const skipHooks = process.env.GRANTTAP_SKIP_HOOKS === "1";
74
- const claude = skipHooks ? null : installClaudeHook();
75
- const codex = skipHooks ? null : installCodexHook();
42
+ const skipHooks = pairing.claude == null || pairing.codex == null;
76
43
  process.stdout.write(
77
44
  [
78
45
  "",
79
46
  " GrantTap is paired on this machine.",
80
47
  "",
81
- ` room: ${machineCfg.room}`,
48
+ ` room: ${pairing.machineCfg.room}`,
82
49
  ` relay: ${relayUrl}`,
83
50
  ` machine cfg: ${machineConfigPath()}`,
84
51
  ` phone cfg: ${phonePairingPath()} (manual fallback; keep private)`,
85
52
  "",
86
53
  ...(skipHooks
87
54
  ? [" Hooks: skipped (GRANTTAP_SKIP_HOOKS=1)"]
88
- : [hookLine("Claude Code", claude!), hookLine("Codex", codex!)]),
55
+ : [
56
+ hookLine("Claude Code", pairing.claude!),
57
+ hookLine("Codex", pairing.codex!),
58
+ hookLine("Background task sync", pairing.monitor!),
59
+ ]),
89
60
  "",
90
61
  " Keep the MCP server configured in your agent. Approval requests now",
91
62
  " use the paired phone and watch; if the relay is unavailable, hooks fall",
@@ -0,0 +1,37 @@
1
+ /** Persistent, terminal-free task sync for the GrantTap phone app on macOS. */
2
+ import { RelayClient } from "../../../../packages/core/relay-client";
3
+ import { loadConfig, machineConfigPath } from "../config";
4
+ import { startSessionMonitor } from "../monitor";
5
+
6
+ let client: RelayClient;
7
+ let monitor: ReturnType<typeof startSessionMonitor>;
8
+
9
+ try {
10
+ client = new RelayClient(loadConfig(machineConfigPath()), { autoReconnect: true });
11
+ monitor = startSessionMonitor(client);
12
+ } catch (error) {
13
+ process.stderr.write(
14
+ `[granttap-mcp] monitor is not paired: ${error instanceof Error ? error.message : String(error)}\n`,
15
+ );
16
+ process.exit(1);
17
+ }
18
+
19
+ const stop = (): void => {
20
+ monitor.close();
21
+ client.close();
22
+ process.exit(0);
23
+ };
24
+
25
+ process.on("SIGINT", stop);
26
+ process.on("SIGTERM", stop);
27
+
28
+ void client
29
+ .connect()
30
+ .then(() => monitor.publish())
31
+ .catch((error: unknown) => {
32
+ // RelayClient keeps reconnecting in the background. Log the initial error
33
+ // for diagnostics without terminating the launchd-managed helper.
34
+ process.stderr.write(
35
+ `[granttap-mcp] initial relay connection failed: ${error instanceof Error ? error.message : String(error)}\n`,
36
+ );
37
+ });
@@ -1,11 +1,13 @@
1
- import { installClaudeHook, installCodexHook } from "../install";
1
+ import { installClaudeHook, installCodexHook, installMonitorHelper } from "../install";
2
2
 
3
3
  const claude = installClaudeHook();
4
4
  const codex = installCodexHook();
5
+ const monitor = installMonitorHelper();
5
6
 
6
7
  process.stdout.write(
7
8
  [
8
9
  `Claude Code: ${claude.status} (${claude.detail})`,
9
10
  `Codex: ${codex.status} (${codex.detail})`,
11
+ `Background task sync: ${monitor.status} (${monitor.detail})`,
10
12
  ].join("\n") + "\n",
11
13
  );