granttap-mcp 0.8.35 → 0.8.36
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 +22 -0
- package/apps/bridge/src/account-recovery/enroll.ts +20 -9
- package/apps/bridge/src/config/access/pairing.ts +2 -2
- package/apps/bridge/src/machine-load/index.ts +2 -2
- package/apps/bridge/src/monitor/support/heartbeat.ts +3 -3
- package/apps/bridge/src/monitor/support/runtime.ts +4 -2
- package/apps/bridge/src/pairing/presence.ts +21 -0
- package/apps/bridge/src/reply/process/index.ts +8 -1
- package/apps/mcp/src/connection-center/state.ts +2 -2
- package/apps/mcp/src/connection-center/widget.html +1 -1
- package/apps/mcp/src/desktop/load/index.ts +2 -2
- package/apps/mcp/src/desktop/pairing/index.ts +4 -3
- package/apps/mcp/src/oauth/session/connect-snapshot.ts +3 -3
- package/apps/mcp/src/status/desktop-status.ts +4 -2
- package/apps/mcp/src/status/pairing-status.ts +5 -3
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
- package/packages/core/computer-name.ts +31 -0
package/README.md
CHANGED
|
@@ -25,6 +25,28 @@ products or source. Their licenses do not change this repository's MIT grant.
|
|
|
25
25
|
[Security model](SECURITY.md) ·
|
|
26
26
|
[Relay source](https://github.com/sergii-ziborov/granttap-relay)
|
|
27
27
|
|
|
28
|
+
## Account Mesh and devices
|
|
29
|
+
|
|
30
|
+
The passkey Account Mesh is the person's relay space. It exists with zero
|
|
31
|
+
computers and can contain many computers and Project Meshes. Create or join it
|
|
32
|
+
from **Devices** in the iPhone, iPad, or Mac app, then use the same passkey in
|
|
33
|
+
the MCP connection card to attach this computer. A previously linked computer
|
|
34
|
+
reuses its machine identity; reconnecting does not create a duplicate. The
|
|
35
|
+
local helper reports the Mac's user-assigned Computer Name and updates its
|
|
36
|
+
account display name when that name changes. Device names are display labels,
|
|
37
|
+
not routing keys: each Task keeps the computer route selected by its Mesh.
|
|
38
|
+
|
|
39
|
+
Install the local helper separately from the Mac App Store app:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g granttap-mcp
|
|
43
|
+
granttap setup
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The Mac app can join the Account Mesh before the helper is present and links it
|
|
47
|
+
when the installed helper becomes available. A one-time QR still provides direct
|
|
48
|
+
device pairing without a passkey account.
|
|
49
|
+
|
|
28
50
|
## Mac, iPhone, iPad, and Apple Watch
|
|
29
51
|
|
|
30
52
|
<p align="center">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computerDisplayName } from "../../../../packages/core/computer-name";
|
|
2
2
|
import { loadAccountLink, saveAccountLink } from "./link";
|
|
3
3
|
|
|
4
4
|
const TOKEN = /^[A-Za-z0-9_-]{43}$/;
|
|
@@ -6,7 +6,8 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
|
|
|
6
6
|
|
|
7
7
|
/** Native Mac app authorizes this MCP with its account session over protected loopback. */
|
|
8
8
|
export async function enrollMacAccount(accountToken: string, origin: string,
|
|
9
|
-
request: typeof fetch = fetch
|
|
9
|
+
request: typeof fetch = fetch,
|
|
10
|
+
computerName: () => string = computerDisplayName): Promise<{ accountId: string; machineId: string }> {
|
|
10
11
|
if (!TOKEN.test(accountToken) || new URL(origin).protocol !== "https:") {
|
|
11
12
|
throw new Error("Invalid account authorization.");
|
|
12
13
|
}
|
|
@@ -22,19 +23,29 @@ export async function enrollMacAccount(accountToken: string, origin: string,
|
|
|
22
23
|
if (existing.accountId !== me.accountId) {
|
|
23
24
|
throw new Error("This Mac is linked to a different account. Revoke it first.");
|
|
24
25
|
}
|
|
25
|
-
const linked = await request(`${origin}/api/account/
|
|
26
|
-
headers
|
|
26
|
+
const linked = await request(`${origin}/api/account/machine/identity`, {
|
|
27
|
+
headers: { authorization: `Bearer ${existing.machineToken}` },
|
|
28
|
+
signal: AbortSignal.timeout(8_000), redirect: "error",
|
|
27
29
|
});
|
|
28
|
-
if (!linked.ok) throw new Error("Account
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
if (linked.status !== 401 && !linked.ok) throw new Error("Account machine is unavailable.");
|
|
31
|
+
if (linked.ok) {
|
|
32
|
+
const identity = await linked.json() as { accountId?: string; machineId?: string };
|
|
33
|
+
if (identity.accountId !== me.accountId || identity.machineId !== existing.machineId) {
|
|
34
|
+
throw new Error("Account machine identity changed unexpectedly.");
|
|
35
|
+
}
|
|
36
|
+
const renamed = await request(`${origin}/api/account/machine/self`, {
|
|
37
|
+
method: "PATCH", headers: { authorization: `Bearer ${existing.machineToken}`,
|
|
38
|
+
"content-type": "application/json" },
|
|
39
|
+
body: JSON.stringify({ name: computerName() }),
|
|
40
|
+
signal: AbortSignal.timeout(8_000), redirect: "error",
|
|
41
|
+
});
|
|
42
|
+
if (!renamed.ok) throw new Error("Account computer name could not be updated.");
|
|
32
43
|
return { accountId: existing.accountId, machineId: existing.machineId };
|
|
33
44
|
}
|
|
34
45
|
}
|
|
35
46
|
const registration = await request(`${origin}/api/account/machines`, {
|
|
36
47
|
method: "POST", headers: { ...headers, "content-type": "application/json" },
|
|
37
|
-
body: JSON.stringify({ name:
|
|
48
|
+
body: JSON.stringify({ name: computerName() }),
|
|
38
49
|
signal: AbortSignal.timeout(8_000), redirect: "error",
|
|
39
50
|
});
|
|
40
51
|
if (registration.status !== 201 && registration.status !== 200) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
3
3
|
import { generateKeyPair, randomId } from "../../../../../packages/core/crypto";
|
|
4
4
|
import type { PeerConfig } from "../../../../../packages/core/relay-client";
|
|
5
5
|
import { configDir } from "../runtime/paths";
|
|
@@ -129,7 +129,7 @@ export function createPairing(relayUrl: string): {
|
|
|
129
129
|
relayUrl,
|
|
130
130
|
room,
|
|
131
131
|
role: "machine",
|
|
132
|
-
deviceName:
|
|
132
|
+
deviceName: computerDisplayName(),
|
|
133
133
|
senderId: randomId(4),
|
|
134
134
|
myPublicKey: machine.publicKey,
|
|
135
135
|
mySecretKey: machine.secretKey,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computerDisplayName } from "../../../../packages/core/computer-name";
|
|
2
2
|
import type {
|
|
3
3
|
MachineLoad,
|
|
4
4
|
SessionsStatus,
|
|
@@ -78,7 +78,7 @@ export function buildMachineLoad(input: {
|
|
|
78
78
|
]);
|
|
79
79
|
return {
|
|
80
80
|
type: "machine.load",
|
|
81
|
-
machine: input.machine ??
|
|
81
|
+
machine: input.machine ?? computerDisplayName(),
|
|
82
82
|
monitorCpuPercent: input.self.cpuPercent,
|
|
83
83
|
monitorMemoryBytes: input.self.memoryBytes,
|
|
84
84
|
agents: [...agents]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
2
2
|
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -20,7 +20,7 @@ export const HEARTBEAT_INTERVAL_MS = Number(
|
|
|
20
20
|
*/
|
|
21
21
|
export async function publishHeartbeat(client: RelayClient): Promise<void> {
|
|
22
22
|
await client.send(
|
|
23
|
-
{ type: "machine.heartbeat", machine:
|
|
23
|
+
{ type: "machine.heartbeat", machine: computerDisplayName(), createdAt: Date.now() },
|
|
24
24
|
"phone",
|
|
25
25
|
{ ttlMs: HEARTBEAT_INTERVAL_MS * 3, reliable: false },
|
|
26
26
|
);
|
|
@@ -38,7 +38,7 @@ export const HELD_HEARTBEAT_TTL_MS = 60_000;
|
|
|
38
38
|
|
|
39
39
|
export async function publishHeldHeartbeat(client: RelayClient): Promise<void> {
|
|
40
40
|
await client.send(
|
|
41
|
-
{ type: "machine.heartbeat", machine:
|
|
41
|
+
{ type: "machine.heartbeat", machine: computerDisplayName(), createdAt: Date.now() },
|
|
42
42
|
"phone",
|
|
43
43
|
{ ttlMs: HELD_HEARTBEAT_TTL_MS, reliable: true },
|
|
44
44
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { refreshCodexHooks } from "../../codex-hook-trust";
|
|
2
|
-
import {
|
|
2
|
+
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
3
3
|
import { RelayClient } from "../../../../../packages/core/relay-client";
|
|
4
4
|
import type { Payload, SessionInfo, SessionsStatus } from "../../../../../packages/protocol/schema";
|
|
5
5
|
import { loadRuntimeConfig } from "../../config";
|
|
@@ -18,6 +18,7 @@ import { deriveObservedClaims } from "../../mesh/observed/claims";
|
|
|
18
18
|
import { ingestRuntimeInvocations } from "../../engine/invocation/ingest";
|
|
19
19
|
import { HEARTBEAT_INTERVAL_MS, publishHeartbeat } from "./heartbeat";
|
|
20
20
|
import { confirmPendingController } from "../../pairing/controllers";
|
|
21
|
+
import { recordControllerName } from "../../pairing/presence";
|
|
21
22
|
import { startPublishLoop } from "./publish-loop";
|
|
22
23
|
import { singleFlightPublisher } from "./single-flight";
|
|
23
24
|
import { createMachineLoadPublisher } from "../../machine-load";
|
|
@@ -81,7 +82,7 @@ function monitorSnapshot(input: {
|
|
|
81
82
|
const runtime = loadRuntimeConfig();
|
|
82
83
|
return {
|
|
83
84
|
type: "sessions.status",
|
|
84
|
-
machine:
|
|
85
|
+
machine: computerDisplayName(),
|
|
85
86
|
sessions: decorateSessions(sessions, subscriptions),
|
|
86
87
|
history: includeHistory
|
|
87
88
|
? decorateSessions(boundedCatalogHistory(history(forceHistory), subscriptions), subscriptions)
|
|
@@ -218,6 +219,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
|
218
219
|
const off = client.onMessage(async (payload: Payload, peerPublicKey) => {
|
|
219
220
|
if (payload.type === "hello" && payload.role === "phone") {
|
|
220
221
|
confirmPendingController(client.room, peerPublicKey);
|
|
222
|
+
recordControllerName(peerPublicKey, payload.deviceName);
|
|
221
223
|
}
|
|
222
224
|
return handleMonitorMessage(client, payload, { leadership, subscriptions, publish, peerPublicKey });
|
|
223
225
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { configDir } from "../config/runtime/paths";
|
|
4
|
+
import { writePrivateFile } from "../config/access/write-private";
|
|
4
5
|
|
|
5
6
|
type PresenceState = {
|
|
6
7
|
phoneLastSeenAt?: number;
|
|
7
8
|
controllers?: Record<string, number>;
|
|
9
|
+
names?: Record<string, string>;
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
function presencePath(): string {
|
|
@@ -31,6 +33,25 @@ export function readControllerLastSeenAt(peerPublicKey: string): number | null {
|
|
|
31
33
|
return typeof at === "number" && Number.isFinite(at) ? at : null;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
export function readControllerName(peerPublicKey: string): string | null {
|
|
37
|
+
const name = readPresence().names?.[peerPublicKey];
|
|
38
|
+
return typeof name === "string" && name.length > 0 && name.length <= 80
|
|
39
|
+
&& !/^(phone|machine|iphone|ipad)$/i.test(name) ? name : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A controller chooses its own visible label; never infer it from a pairing role. */
|
|
43
|
+
export function recordControllerName(peerPublicKey: string, value: string): boolean {
|
|
44
|
+
const name = value.trim();
|
|
45
|
+
if (!/^[A-Za-z0-9+/]{43}=$/.test(peerPublicKey)
|
|
46
|
+
|| !name || name.length > 80 || /[\x00-\x1f\x7f]/.test(name)
|
|
47
|
+
|| /^(phone|machine|iphone|ipad)$/i.test(name)) return false;
|
|
48
|
+
const previous = readPresence();
|
|
49
|
+
const names = { ...previous.names, [peerPublicKey]: name };
|
|
50
|
+
const bounded = Object.fromEntries(Object.entries(names).slice(-32));
|
|
51
|
+
writePrivateFile(presencePath(), JSON.stringify({ ...previous, names: bounded }));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
export function hasControllerPresence(): boolean {
|
|
35
56
|
return Object.keys(readPresence().controllers ?? {}).length > 0;
|
|
36
57
|
}
|
|
@@ -93,7 +93,6 @@ export function runProcess(
|
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
95
|
const untrack = key ? track(key, child) : () => {};
|
|
96
|
-
if (stdin != null) child.stdin?.end(stdin);
|
|
97
96
|
let stdout = "";
|
|
98
97
|
let stderr = "";
|
|
99
98
|
let done = false;
|
|
@@ -134,5 +133,13 @@ export function runProcess(
|
|
|
134
133
|
}` });
|
|
135
134
|
}
|
|
136
135
|
});
|
|
136
|
+
if (stdin != null) {
|
|
137
|
+
child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
|
|
138
|
+
// A CLI can finish with a valid result before it reads the whole prompt.
|
|
139
|
+
// Its closed pipe must not become an uncaught exception in the bridge.
|
|
140
|
+
if (error.code !== "EPIPE") finish({ ok: false, error: `${command}: ${error.message}` });
|
|
141
|
+
});
|
|
142
|
+
child.stdin?.end(stdin);
|
|
143
|
+
}
|
|
137
144
|
});
|
|
138
145
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { computerDisplayName } from "../../../../packages/core/computer-name";
|
|
3
3
|
import { loadConfig } from "../../../bridge/src/config";
|
|
4
4
|
import { isMachineConfigured, listPairedPhones, readOnlyMachineConfigPath } from "../status/pairing-status";
|
|
5
5
|
import { inspectProviderStatusSnapshot } from "../status/provider-status";
|
|
@@ -68,7 +68,7 @@ export class ConnectionState {
|
|
|
68
68
|
return {
|
|
69
69
|
structuredContent: {
|
|
70
70
|
status,
|
|
71
|
-
computer:
|
|
71
|
+
computer: computerDisplayName(),
|
|
72
72
|
version: packageVersion(),
|
|
73
73
|
relay: config ? new URL(config.relayUrl).host : "",
|
|
74
74
|
relayStatus: runtime.relayStatus,
|
|
@@ -37,7 +37,7 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
|
|
|
37
37
|
<dt>Last encrypted message from iPhone</dt><dd id="phone">Not observed</dd></dl>
|
|
38
38
|
<h3>Provider readiness</h3><ul id="providers"></ul>
|
|
39
39
|
<p>QR pairs a phone directly. A passkey links this Mac to your GrantTap account so your signed-in phone can discover its computers. Provider sign-in is separate.</p>
|
|
40
|
-
<p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.
|
|
40
|
+
<p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.36</code> and <code>granttap setup</code> locally.
|
|
41
41
|
A saved pairing alone does not prove your phone or the background helper is online.</p>
|
|
42
42
|
<p><a href="https://granttap.com" target="_blank" rel="noopener noreferrer">Website</a> ·
|
|
43
43
|
<a href="https://granttap.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> ·
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
2
2
|
import {
|
|
3
3
|
sampleAgentProcesses, type AgentProcessLoad,
|
|
4
4
|
} from "../../../../bridge/src/machine-load/host/process-sampler";
|
|
@@ -12,7 +12,7 @@ export async function desktopMachineLoad(
|
|
|
12
12
|
return {
|
|
13
13
|
operation: "desktop.machine_load" as const,
|
|
14
14
|
source: "process_sample" as const,
|
|
15
|
-
computer:
|
|
15
|
+
computer: computerDisplayName(),
|
|
16
16
|
observed_at: now(),
|
|
17
17
|
agents: Object.entries(processes)
|
|
18
18
|
.filter(([agent]) => agent.length > 0)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
2
2
|
import { loadConfig } from "../../../../bridge/src/config";
|
|
3
3
|
import { reloadPairingHelper } from "../../../../bridge/src/install";
|
|
4
4
|
import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from "../../../../bridge/src/pairing";
|
|
@@ -37,7 +37,7 @@ export class DesktopControllerEnrollment {
|
|
|
37
37
|
|
|
38
38
|
snapshot(now = Date.now()): DesktopControllerCode {
|
|
39
39
|
const result: DesktopControllerCode = {
|
|
40
|
-
operation: "desktop.controller_enrollment", status: "idle", computer:
|
|
40
|
+
operation: "desktop.controller_enrollment", status: "idle", computer: computerDisplayName(),
|
|
41
41
|
};
|
|
42
42
|
if (!this.pending) return result;
|
|
43
43
|
if (!isMachineConfigured()) { this.pending = null; return result; }
|
|
@@ -77,7 +77,8 @@ export class DesktopControllerEnrollment {
|
|
|
77
77
|
this.onPairingChanged();
|
|
78
78
|
return this.snapshot();
|
|
79
79
|
} catch {
|
|
80
|
-
return { operation: "desktop.controller_enrollment", status: "unavailable",
|
|
80
|
+
return { operation: "desktop.controller_enrollment", status: "unavailable",
|
|
81
|
+
computer: computerDisplayName() };
|
|
81
82
|
}
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -2,7 +2,7 @@ import { codexHooksReady } from "../../../../bridge/src/codex-hook-trust";
|
|
|
2
2
|
/**
|
|
3
3
|
* Public consent snapshot for granttap.com. No pairing keys, tokens, or codes.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
6
6
|
import { inspectAgentIntegrations } from "../../../../bridge/src/install";
|
|
7
7
|
import { loadConfig } from "../../../../bridge/src/config";
|
|
8
8
|
import { computerId } from "../../../../bridge/src/mesh/identity/computer";
|
|
@@ -49,7 +49,7 @@ export function buildConnectSnapshot(clientName?: string): ConnectSnapshot {
|
|
|
49
49
|
const runtime = connectionRuntimeStatus(room || undefined);
|
|
50
50
|
return {
|
|
51
51
|
clientName: publicClientName(clientName),
|
|
52
|
-
computerName:
|
|
52
|
+
computerName: computerDisplayName(),
|
|
53
53
|
paired,
|
|
54
54
|
passkeyCapable: true,
|
|
55
55
|
machineId: loadAccountLink()?.machineId,
|
|
@@ -70,7 +70,7 @@ export function buildConnectSnapshot(clientName?: string): ConnectSnapshot {
|
|
|
70
70
|
|
|
71
71
|
/** Computers and open tasks only. No transcripts, keys, or room ids. */
|
|
72
72
|
export function publicMeshSummary(): ConnectMesh {
|
|
73
|
-
const thisComputer =
|
|
73
|
+
const thisComputer = computerDisplayName();
|
|
74
74
|
try {
|
|
75
75
|
const store = localMeshStore();
|
|
76
76
|
const names = new Set<string>([thisComputer, computerId().slice(0, 80)]);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computerDisplayName } from "../../../../packages/core/computer-name";
|
|
2
2
|
import { loadConfig } from "../../../bridge/src/config";
|
|
3
3
|
import { connectionRuntimeStatus } from "../mcp-tools/connect/relay";
|
|
4
4
|
import { packageVersion } from "./package-version";
|
|
5
5
|
import { computerId } from "../../../bridge/src/mesh/identity/computer";
|
|
6
|
+
import { loadAccountLink } from "../../../bridge/src/account-recovery/link";
|
|
6
7
|
import {
|
|
7
8
|
isMachineConfigured, listPairedPhones, phoneReachability, readOnlyMachineConfigPath,
|
|
8
9
|
} from "./pairing-status";
|
|
@@ -18,8 +19,9 @@ export function desktopStatusSnapshot(desktopEngineSocket?: string) {
|
|
|
18
19
|
schema: "granttap.desktop-status.v1" as const,
|
|
19
20
|
ok: true,
|
|
20
21
|
service: "granttap-mcp" as const,
|
|
21
|
-
computer:
|
|
22
|
+
computer: computerDisplayName(),
|
|
22
23
|
endpointId: computerId(),
|
|
24
|
+
accountLinkSaved: loadAccountLink() !== null,
|
|
23
25
|
version: packageVersion(),
|
|
24
26
|
paired: configured,
|
|
25
27
|
phoneReachability: phoneReachability(phones),
|
|
@@ -3,7 +3,8 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { loadConfig } from "../../../bridge/src/config";
|
|
6
|
-
import { hasControllerPresence, readControllerLastSeenAt,
|
|
6
|
+
import { hasControllerPresence, readControllerLastSeenAt, readControllerName,
|
|
7
|
+
readPhoneLastSeenAt } from "../../../bridge/src/pairing/presence";
|
|
7
8
|
import { controllerPeerGate } from "../../../bridge/src/pairing/controllers";
|
|
8
9
|
|
|
9
10
|
export type PairedPhone = {
|
|
@@ -59,14 +60,15 @@ export function listPairedPhones(phoneLastSeenAt: number | null = null, now = Da
|
|
|
59
60
|
value != null && (best == null || value > best) ? value : best
|
|
60
61
|
), null);
|
|
61
62
|
const seen = lastSeenAt != null && now - lastSeenAt < 60_000;
|
|
62
|
-
const primary: PairedPhone = { name
|
|
63
|
+
const primary: PairedPhone = { name: readControllerName(machine.peerPublicKey) ?? name,
|
|
64
|
+
status: seen ? "seen" : "paired", lastSeenAt };
|
|
63
65
|
const allowed = controllerPeerGate(machine);
|
|
64
66
|
const additional = (machine.extraPeerPublicKeys ?? [])
|
|
65
67
|
.filter((key) => key && key !== machine.peerPublicKey && allowed(key))
|
|
66
68
|
.map((key, index): PairedPhone => {
|
|
67
69
|
const at = readControllerLastSeenAt(key);
|
|
68
70
|
const suffix = createHash("sha256").update(key).digest("hex").slice(0, 6);
|
|
69
|
-
return { name: `Controller ${index + 2} · ${suffix}`,
|
|
71
|
+
return { name: readControllerName(key) ?? `Controller ${index + 2} · ${suffix}`,
|
|
70
72
|
status: at != null && now - at < 60_000 ? "seen" : "paired", lastSeenAt: at };
|
|
71
73
|
});
|
|
72
74
|
return [primary, ...additional];
|
package/cursor-plugin/README.md
CHANGED
|
@@ -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.
|
|
13
|
+
npm install -g granttap-mcp@0.8.36
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -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.
|
|
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.36\";\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.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.36";
|
|
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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
|
|
4
|
+
type NameSources = {
|
|
5
|
+
platform?: NodeJS.Platform;
|
|
6
|
+
host?: string;
|
|
7
|
+
override?: string;
|
|
8
|
+
macName?: () => string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function usableName(value: string | undefined): string | null {
|
|
12
|
+
const name = value?.trim();
|
|
13
|
+
return name && name.length <= 80 && !/[\x00-\x1f\x7f]/.test(name) ? name : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** User-visible computer name; network hostname remains only a fallback. */
|
|
17
|
+
export function computerDisplayName(sources: NameSources = {}): string {
|
|
18
|
+
const override = usableName(sources.override ?? process.env.GRANTTAP_COMPUTER_NAME);
|
|
19
|
+
if (override) return override;
|
|
20
|
+
if ((sources.platform ?? process.platform) === "darwin") {
|
|
21
|
+
try {
|
|
22
|
+
const macName = sources.macName ?? (() => execFileSync(
|
|
23
|
+
"/usr/sbin/scutil", ["--get", "ComputerName"],
|
|
24
|
+
{ encoding: "utf8", timeout: 2_000, maxBuffer: 4_096 },
|
|
25
|
+
));
|
|
26
|
+
const name = usableName(macName());
|
|
27
|
+
if (name) return name;
|
|
28
|
+
} catch { /* Computer Name may be unset; preserve the network fallback. */ }
|
|
29
|
+
}
|
|
30
|
+
return usableName(sources.host ?? hostname()) ?? "Computer";
|
|
31
|
+
}
|