granttap-mcp 0.8.101 → 0.8.104
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 +14 -0
- package/apps/bridge/src/bin/runtime/monitor.ts +8 -0
- package/apps/bridge/src/monitor/handlers/user-message.ts +9 -10
- package/apps/bridge/src/monitor/message-delivery/README.md +5 -0
- package/apps/bridge/src/monitor/message-delivery/index.ts +10 -3
- package/apps/bridge/src/monitor/support/runtime.ts +34 -22
- package/apps/bridge/src/runtime-updates/README.md +32 -0
- package/apps/bridge/src/runtime-updates/http-readiness.ts +17 -0
- package/apps/bridge/src/runtime-updates/index.ts +74 -0
- package/apps/bridge/src/runtime-updates/preferences/README.md +18 -0
- package/apps/bridge/src/runtime-updates/preferences/index.ts +12 -0
- package/apps/bridge/src/runtime-updates/preferences/storage.d.mts +4 -0
- package/apps/bridge/src/runtime-updates/preferences/storage.mjs +35 -0
- package/apps/bridge/src/runtime-updates/readiness.ts +23 -0
- package/apps/bridge/src/runtime-updates/registry.ts +58 -0
- package/apps/bridge/src/runtime-updates/selection.d.mts +10 -0
- package/apps/bridge/src/runtime-updates/selection.mjs +46 -0
- package/apps/bridge/src/runtime-updates/supervision.mjs +29 -0
- package/apps/bridge/src/sessions/README.md +5 -0
- package/apps/bridge/src/sessions/catalog.ts +7 -3
- package/apps/mcp/src/connection-center/README.md +7 -0
- package/apps/mcp/src/connection-center/bridge.js +2 -2
- package/apps/mcp/src/connection-center/state.ts +3 -0
- package/apps/mcp/src/connection-center/update-controls.ts +23 -0
- package/apps/mcp/src/connection-center/view.js +12 -0
- package/apps/mcp/src/connection-center/widget.html +1 -0
- package/apps/mcp/src/http/http-server.ts +6 -0
- package/apps/mcp/src/mcp-tools/README.md +6 -1
- package/apps/mcp/src/mcp-tools/connect/index.ts +4 -2
- package/apps/mcp/src/mcp-tools/connect/widget.ts +1 -1
- package/bin/granttap-mcp.mjs +18 -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/README.md
CHANGED
|
@@ -855,3 +855,17 @@ See [repository observations](apps/bridge/src/mesh/repository-details/README.md)
|
|
|
855
855
|
The relay can retain recipient-encrypted catalogs, Mesh snapshots, capability usage and recent chat heads for reconnect without the computer. This bounded seven-day read cache is separate from the durable encrypted command queue. See [relay state publication](packages/core/retained-state/README.md). The relay never receives decryption keys or readable chat content.
|
|
856
856
|
|
|
857
857
|
Tools & Skills matches recorded usage to the exact computer, provider and execution. Codex's native history projection supplies actual MCP invocation IDs, results and durations, including calls made through Code Mode. Claude Code uses observed tool-use, result and Skill records, aggregating MCP and skill calls before its recent-detail cache is trimmed. Execution summaries remain available when older call details fall outside the recent-event budget. Counts describe recorded evidence; installation and permission do not prove usage. Missing resource measurements remain unknown. See [native capability history](apps/bridge/src/sessions/native-capability-history/README.md).
|
|
858
|
+
|
|
859
|
+
## Automatic runtime updates
|
|
860
|
+
|
|
861
|
+
Installed GrantTap background services check for stable MCP updates automatically.
|
|
862
|
+
Verified updates are staged separately and applied after active requests and pending
|
|
863
|
+
decisions finish. Startup failures restore the previous version. Pairing and task
|
|
864
|
+
policies are retained; source checkouts and running provider processes stay in place.
|
|
865
|
+
|
|
866
|
+
In the plugin's GrantTap connection card, use **Automatic updates: On / Off**.
|
|
867
|
+
The choice is saved for this computer and takes effect without restarting the
|
|
868
|
+
service. Off prevents further update checks and activation; an already running
|
|
869
|
+
download may finish staging, and the current runtime keeps running. Turn it on to
|
|
870
|
+
resume automatic checks. `GRANTTAP_DISABLE_AUTO_UPDATE=1` on the service overrides
|
|
871
|
+
the card and keeps the switch disabled.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { startRuntimeUpdates } from "../../runtime-updates";
|
|
2
|
+
import { runtimeUpdateReadiness } from "../../runtime-updates/readiness";
|
|
1
3
|
/** Persistent, terminal-free task sync for the GrantTap phone app on macOS. */
|
|
2
4
|
import { join } from "node:path";
|
|
3
5
|
import { RelayClient } from "../../../../../packages/core/relay-client";
|
|
@@ -25,6 +27,8 @@ let stopEndpointPublisher = () => {};
|
|
|
25
27
|
let stopTaskDispatch = () => {};
|
|
26
28
|
let stopTaskNetwork = () => {};
|
|
27
29
|
let stopEngineRecovery = () => {};
|
|
30
|
+
let stopRuntimeUpdates = () => {};
|
|
31
|
+
let stopUpdateReadiness = () => {};
|
|
28
32
|
|
|
29
33
|
try {
|
|
30
34
|
const cfg = applyNetworkRoute(loadConfig(machineConfigPath()));
|
|
@@ -40,6 +44,8 @@ try {
|
|
|
40
44
|
stopTaskNetwork = startTaskNetworkMonitor({ config: cfg, client,
|
|
41
45
|
isLeader: monitor.isLeader, peerAllowed: controllerPeerGate(cfg), publish: monitor.publish });
|
|
42
46
|
stopEndpointPublisher = startEndpointPublisher(cfg);
|
|
47
|
+
stopRuntimeUpdates = startRuntimeUpdates(monitor.canUpdate);
|
|
48
|
+
stopUpdateReadiness = runtimeUpdateReadiness(monitor.canUpdate);
|
|
43
49
|
} catch (error) {
|
|
44
50
|
process.stderr.write(
|
|
45
51
|
`[granttap-mcp] monitor is not paired: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
@@ -50,6 +56,8 @@ try {
|
|
|
50
56
|
const stop = (): void => {
|
|
51
57
|
if (stopping) return;
|
|
52
58
|
stopping = true;
|
|
59
|
+
stopRuntimeUpdates();
|
|
60
|
+
stopUpdateReadiness();
|
|
53
61
|
stopEngineRecovery();
|
|
54
62
|
stopTaskDispatch();
|
|
55
63
|
stopTaskNetwork();
|
|
@@ -121,6 +121,7 @@ export async function handleUserMessage(
|
|
|
121
121
|
onProcessing?: () => Promise<void>,
|
|
122
122
|
peerPublicKey?: string,
|
|
123
123
|
native?: { messageId: string; nativeOnly: true },
|
|
124
|
+
onRefusal?: (error: string) => Promise<void>,
|
|
124
125
|
): Promise<"accepted" | "rejected"> {
|
|
125
126
|
const say = (text: string, sessionId?: string, wake = false, kind: "status" | "response" = "response") => {
|
|
126
127
|
const payload = agentEventForUserMessage(message, text, sessionId, kind);
|
|
@@ -129,6 +130,11 @@ export async function handleUserMessage(
|
|
|
129
130
|
? sendSessionPayload(client, payload, sessionId, "phone", options)
|
|
130
131
|
: client.send(payload, "phone", options)).catch(() => {});
|
|
131
132
|
};
|
|
133
|
+
const refuse = async (error: string, wake = false): Promise<"rejected"> => {
|
|
134
|
+
await onRefusal?.(error);
|
|
135
|
+
await say(error, message.sessionId, wake);
|
|
136
|
+
return "rejected";
|
|
137
|
+
};
|
|
132
138
|
if (!message.sessionId) {
|
|
133
139
|
const origin = message.taskOriginId && peerPublicKey ? {
|
|
134
140
|
registry: localTaskOrigins(),
|
|
@@ -150,23 +156,16 @@ export async function handleUserMessage(
|
|
|
150
156
|
// so it runs only for a chat newer than that catalog.
|
|
151
157
|
const target = publishedSession(client, message.sessionId)
|
|
152
158
|
?? scanSessions().sessions.find((session) => session.sessionId === message.sessionId);
|
|
153
|
-
if (!target)
|
|
154
|
-
await say("This task is no longer available on the computer.", message.sessionId);
|
|
155
|
-
return "rejected";
|
|
156
|
-
}
|
|
159
|
+
if (!target) return refuse("This task is no longer available on the computer.");
|
|
157
160
|
|
|
158
161
|
const settings = loadRuntimeConfig().providerSettings;
|
|
159
162
|
if (target.agent in settings
|
|
160
163
|
&& settings[target.agent as keyof typeof settings] === false) {
|
|
161
|
-
|
|
162
|
-
return "rejected";
|
|
164
|
+
return refuse("This agent is disabled in GrantTap Settings.", true);
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
const routingError = deliveryRoutingError(target, message);
|
|
166
|
-
if (routingError)
|
|
167
|
-
await say(routingError, target.sessionId);
|
|
168
|
-
return "rejected";
|
|
169
|
-
}
|
|
168
|
+
if (routingError) return refuse(routingError);
|
|
170
169
|
|
|
171
170
|
// No "sent, waiting" line from a middleman: the delivery receipt already
|
|
172
171
|
// marks the person's bubble, and the next words in the chat are the answer.
|
|
@@ -22,6 +22,11 @@ message again. Only the original authenticated controller, room, and session
|
|
|
22
22
|
may read it. Older unscoped records are not exposed through this query.
|
|
23
23
|
Receipts to a known controller use its device encryption directly, so a missed
|
|
24
24
|
task-key grant cannot hide a provider rejection or processing acknowledgement.
|
|
25
|
+
For existing chats, refusals before provider dispatch (missing task, disabled provider, or rejected
|
|
26
|
+
task capability) also persist a scoped rejected receipt before the relay box is
|
|
27
|
+
consumed. The original controller can recover that specific refusal after a
|
|
28
|
+
restart or lost response. Missing attachment recovery retains its existing
|
|
29
|
+
transport behavior; an admission refusal is never reported as agent activity.
|
|
25
30
|
|
|
26
31
|
A correlated `agentActivityAt` receipt is emitted only after provider stream output
|
|
27
32
|
confirms activity. It is retained under the original controller, room and session
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Payload } from "../../../../../packages/protocol/schema";
|
|
2
2
|
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
3
3
|
import { abandonDelivery, beginDelivery, completeDelivery,
|
|
4
|
-
acknowledgeProcessingDelivery, processingDeliveryAcknowledged, completedDeliveryFailure, deliveryReceiptStatus
|
|
4
|
+
acknowledgeProcessingDelivery, processingDeliveryAcknowledged, completedDeliveryFailure, deliveryReceiptStatus,
|
|
5
|
+
rejectCompletedDelivery } from "../../delivery";
|
|
5
6
|
import { sendDeliveryReceipt } from "../handlers/receipts";
|
|
6
7
|
import { handleUserMessage } from "../handlers/user-message";
|
|
7
8
|
|
|
@@ -34,9 +35,15 @@ export async function handleInboundUserMessage(
|
|
|
34
35
|
if (!payload.messageId) return;
|
|
35
36
|
acknowledgeProcessingDelivery(payload.messageId);
|
|
36
37
|
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId, peerPublicKey);
|
|
37
|
-
}, peerPublicKey, native
|
|
38
|
+
}, peerPublicKey, native, async error => {
|
|
39
|
+
if (!payload.messageId) return;
|
|
40
|
+
rejectCompletedDelivery(payload.messageId, error);
|
|
41
|
+
await sendDeliveryReceipt(client, payload.messageId, 'rejected', error, payload.sessionId, peerPublicKey);
|
|
42
|
+
});
|
|
38
43
|
if (payload.messageId && deliveryStarted) {
|
|
39
|
-
if (outcome === "rejected")
|
|
44
|
+
if (outcome === "rejected") {
|
|
45
|
+
if (!completedDeliveryFailure(payload.messageId)) abandonDelivery(payload.messageId);
|
|
46
|
+
}
|
|
40
47
|
else {
|
|
41
48
|
completeDelivery(payload.messageId);
|
|
42
49
|
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId, peerPublicKey);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runtimeWorkTracker } from "../../runtime-updates/readiness";
|
|
1
2
|
import { refreshCodexHooks } from "../../codex-hook-trust";
|
|
2
3
|
import { computerDisplayName } from "../../../../../packages/core/computer-name";
|
|
3
4
|
import { RelayClient } from "../../../../../packages/core/relay-client";
|
|
@@ -38,6 +39,7 @@ import { publishMonitorCapabilities, stopMonitorCapabilities } from "../capabili
|
|
|
38
39
|
export type SessionMonitor = {
|
|
39
40
|
publish: () => Promise<void>;
|
|
40
41
|
isLeader: () => boolean;
|
|
42
|
+
canUpdate: () => boolean;
|
|
41
43
|
close: () => void;
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -163,29 +165,13 @@ async function publishMonitorTick(input: {
|
|
|
163
165
|
export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
164
166
|
const subscriptions = new Set<string>();
|
|
165
167
|
const leadership = monitorLeadership();
|
|
166
|
-
|
|
167
|
-
publishedCatalog(client);
|
|
168
|
+
restoreMonitorState(client);
|
|
168
169
|
let historyCache: { generatedAt: number; sessions: SessionsStatus["sessions"] } | undefined;
|
|
169
170
|
let historyRead: Promise<SessionsStatus["sessions"]> | undefined;
|
|
170
171
|
let lastHistoryPublishedAt = 0;
|
|
171
172
|
let closed = false;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
});
|
|
175
|
-
const loadLoop = startMachineLoadLoop({
|
|
176
|
-
connected: () => client.isConnected,
|
|
177
|
-
// Only the leader reports load. Every MCP server used to run this loop
|
|
178
|
-
// too, each with the code of the day it started, and the phone flickered
|
|
179
|
-
// between their answers.
|
|
180
|
-
publish: (status, intervalMs) => (leadership.acquire()
|
|
181
|
-
? publishMachineLoad(client, status, intervalMs)
|
|
182
|
-
: Promise.resolve(undefined)),
|
|
183
|
-
onError: (error) => {
|
|
184
|
-
process.stderr.write(
|
|
185
|
-
`[monitor] load report failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
186
|
-
);
|
|
187
|
-
},
|
|
188
|
-
});
|
|
173
|
+
const work = runtimeWorkTracker(() => approvalsStatus().pending.length > 0);
|
|
174
|
+
const loadLoop = monitorLoadLoop(client, leadership);
|
|
189
175
|
|
|
190
176
|
const history = (force = false): Promise<SessionsStatus["sessions"]> => {
|
|
191
177
|
if (historyRead) return historyRead;
|
|
@@ -209,7 +195,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
|
209
195
|
lastHistoryPublishedAt = next.lastHistoryPublishedAt;
|
|
210
196
|
});
|
|
211
197
|
|
|
212
|
-
const off = client.onMessage(
|
|
198
|
+
const off = client.onMessage((payload: Payload, peerPublicKey) => work.run(async () => {
|
|
213
199
|
if (payload.type === "hello" && payload.role === "phone") {
|
|
214
200
|
confirmPendingController(client.room, peerPublicKey);
|
|
215
201
|
recordControllerName(peerPublicKey, payload.deviceName);
|
|
@@ -219,8 +205,8 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
|
219
205
|
await replayNativeQuestions(client, peerPublicKey);
|
|
220
206
|
}
|
|
221
207
|
}
|
|
222
|
-
return handleMonitorMessage(client, payload, { leadership, subscriptions, publish, peerPublicKey });
|
|
223
|
-
});
|
|
208
|
+
return await handleMonitorMessage(client, payload, { leadership, subscriptions, publish, peerPublicKey });
|
|
209
|
+
}));
|
|
224
210
|
|
|
225
211
|
const offConnected = client.onConnected?.(() => publish().catch(() => {}));
|
|
226
212
|
|
|
@@ -244,6 +230,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
|
|
|
244
230
|
return {
|
|
245
231
|
publish: () => publish(false),
|
|
246
232
|
isLeader: () => leadership.acquire(),
|
|
233
|
+
canUpdate: work.idle,
|
|
247
234
|
close: () => {
|
|
248
235
|
closed = true;
|
|
249
236
|
offConnected?.();
|
|
@@ -279,3 +266,28 @@ async function replayNativeQuestions(client: RelayClient, peerPublicKey: string)
|
|
|
279
266
|
const known = publishedCatalog(client);
|
|
280
267
|
if (known) await publishCodexQuestions(client, known.sessions, { replay: true, peerPublicKey }).catch(() => {});
|
|
281
268
|
}
|
|
269
|
+
|
|
270
|
+
function restoreMonitorState(client: RelayClient): void {
|
|
271
|
+
primeSessionKeys(client);
|
|
272
|
+
publishedCatalog(client);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function monitorLoadLoop(client: RelayClient, leadership: ReturnType<typeof monitorLeadership>) {
|
|
276
|
+
const publishMachineLoad = createMachineLoadPublisher({
|
|
277
|
+
log: (line) => process.stderr.write(`[monitor] ${line}\n`),
|
|
278
|
+
});
|
|
279
|
+
return startMachineLoadLoop({
|
|
280
|
+
connected: () => client.isConnected,
|
|
281
|
+
// Only the leader reports load. Every MCP server used to run this loop
|
|
282
|
+
// too, each with the code of the day it started, and the phone flickered
|
|
283
|
+
// between their answers.
|
|
284
|
+
publish: (status, intervalMs) => (leadership.acquire()
|
|
285
|
+
? publishMachineLoad(client, status, intervalMs)
|
|
286
|
+
: Promise.resolve(undefined)),
|
|
287
|
+
onError: (error) => {
|
|
288
|
+
process.stderr.write(
|
|
289
|
+
`[monitor] load report failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
290
|
+
);
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Automatic GrantTap runtime updates
|
|
2
|
+
|
|
3
|
+
The installed background monitor checks the official `granttap-mcp` npm stable
|
|
4
|
+
release every four hours. New versions are installed in a separate private
|
|
5
|
+
version directory under `~/.granttap/runtime-updates`, with lifecycle scripts
|
|
6
|
+
disabled, Node engine checks enabled, and registry integrity verified. A failed
|
|
7
|
+
installation leaves the running version in place.
|
|
8
|
+
|
|
9
|
+
The stable launcher selects the verified version for future invocations. Owned
|
|
10
|
+
background services switch only after their current requests finish and no
|
|
11
|
+
approval is pending. Existing provider processes and active stdio MCP calls keep
|
|
12
|
+
running. A new service that cannot start restores the previous version; that
|
|
13
|
+
failed version is quarantined until a newer release appears.
|
|
14
|
+
|
|
15
|
+
Pairing, account credentials, task history, capability policy, and provider
|
|
16
|
+
installations are not modified by runtime updates. Offline checks retry later.
|
|
17
|
+
Source checkouts and test processes do not update themselves. The plugin's
|
|
18
|
+
connection card offers a persistent **Automatic updates: On / Off** switch for
|
|
19
|
+
this computer. The running monitor reads it before registry checks and activation;
|
|
20
|
+
the launcher checks it again before handing off an owned service. Off keeps the
|
|
21
|
+
current selected runtime and rollback protection. An in-progress download may
|
|
22
|
+
finish staging but cannot activate while off. Re-enabling is observed on the next
|
|
23
|
+
minute tick without restarting services. Administrators may
|
|
24
|
+
set `GRANTTAP_DISABLE_AUTO_UPDATE=1` on their service to retain their installed
|
|
25
|
+
version. Private staging consumes space; old versions are retained for rollback.
|
|
26
|
+
|
|
27
|
+
Public entry: `startRuntimeUpdates`, `runtimeUpdateReadiness`, and the launcher's
|
|
28
|
+
`selection.mjs` / `supervision.mjs` boundary. Tests exercise failed downloads and
|
|
29
|
+
verification, deferred activation, concurrent installation, selection, and startup
|
|
30
|
+
rollback using isolated directories and owned child processes.
|
|
31
|
+
|
|
32
|
+
License: [GrantTap Commercial Source License](../../../../LICENSE).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
|
|
3
|
+
/** Long-lived HTTP/SSE calls defer replacement until their normal completion. */
|
|
4
|
+
export function trackHttpRuntimeWork(app:Express):()=>boolean {
|
|
5
|
+
let activeRequests=0;
|
|
6
|
+
app.use((_req,res,next)=>{
|
|
7
|
+
activeRequests++;
|
|
8
|
+
let finished=false;
|
|
9
|
+
const complete=()=>{
|
|
10
|
+
if(!finished) {finished=true;activeRequests--;}
|
|
11
|
+
};
|
|
12
|
+
res.once('finish',complete);
|
|
13
|
+
res.once('close',complete);
|
|
14
|
+
next();
|
|
15
|
+
});
|
|
16
|
+
return ()=>activeRequests===0;
|
|
17
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { configDir } from '../config/runtime/paths';
|
|
5
|
+
import { latestRelease, trustedRelease, installRelease, verifyRelease, type Release } from './registry';
|
|
6
|
+
import { newerVersion, readState, runtimeVersion, saveState, validRuntime, versionRoot } from './selection.mjs';
|
|
7
|
+
import { automaticUpdatesAllowed } from './preferences/storage.mjs';
|
|
8
|
+
export { runtimeUpdateSettings, setRuntimeAutoUpdate } from './preferences';
|
|
9
|
+
|
|
10
|
+
type Options={root:string;stateDirectory:string;now?:()=>number;latest?:()=>Promise<unknown>;
|
|
11
|
+
idle:()=>boolean;install?:(release:Release,directory:string)=>Promise<void>;
|
|
12
|
+
verify?:(root:string)=>Promise<void>;activate?:(root:string)=>void};
|
|
13
|
+
|
|
14
|
+
/** Stage verified releases independently; activation waits until no request or decision is pending. */
|
|
15
|
+
export async function checkForRuntimeUpdate(options:Options):Promise<string|undefined> {
|
|
16
|
+
if(!automaticUpdatesAllowed(options.stateDirectory))return;
|
|
17
|
+
mkdirSync(options.stateDirectory,{recursive:true,mode:0o700});
|
|
18
|
+
const lock=join(options.stateDirectory,'install.lock');let fd:number;
|
|
19
|
+
try {fd=openSync(lock,'wx',0o600);} catch {return undefined;}
|
|
20
|
+
try {
|
|
21
|
+
const release=await (options.latest??latestRelease)();
|
|
22
|
+
if(!automaticUpdatesAllowed(options.stateDirectory))return;
|
|
23
|
+
const state=readState(options.stateDirectory);
|
|
24
|
+
if(!trustedRelease(release) || !newerVersion(release.version,runtimeVersion(options.root))
|
|
25
|
+
|| state.rejectedVersion===release.version || (state.version && !newerVersion(release.version,state.version)))return;
|
|
26
|
+
const candidate=versionRoot(options.stateDirectory,release.version);
|
|
27
|
+
if(!validRuntime(candidate,options.stateDirectory,release.version)) {
|
|
28
|
+
await (options.install??installRelease)(release,options.stateDirectory);
|
|
29
|
+
}
|
|
30
|
+
if(!automaticUpdatesAllowed(options.stateDirectory))return;
|
|
31
|
+
await (options.verify??verifyRelease)(candidate);
|
|
32
|
+
if(!validRuntime(candidate,options.stateDirectory,release.version))throw Error('Runtime verification failed');
|
|
33
|
+
if(!options.idle() || !automaticUpdatesAllowed(options.stateDirectory))return;
|
|
34
|
+
saveState(options.stateDirectory,{root:candidate,version:release.version,
|
|
35
|
+
previous:state.root?{root:state.root,version:state.version}:undefined});
|
|
36
|
+
options.activate?.(candidate);return candidate;
|
|
37
|
+
} finally {closeSync(fd);unlinkSync(lock);}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type LoopOptions={root:string;directory:string;idle:()=>boolean;check?:typeof checkForRuntimeUpdate;
|
|
41
|
+
now?:()=>number;schedule?:typeof setTimeout;report?:()=>void;activate?:()=>void};
|
|
42
|
+
|
|
43
|
+
/** Check failure never stops sync; timers are owned and cancellation prevents another check. */
|
|
44
|
+
export function runtimeUpdateLoop(options:LoopOptions):()=>void {
|
|
45
|
+
let stopped=false,timer:NodeJS.Timeout|undefined,lastCheck=0;
|
|
46
|
+
const now=options.now??Date.now,schedule=options.schedule??setTimeout;
|
|
47
|
+
const tick=async()=>{
|
|
48
|
+
if(stopped)return;
|
|
49
|
+
try {
|
|
50
|
+
if(!automaticUpdatesAllowed(options.directory)){lastCheck=0;return;}
|
|
51
|
+
const lock=join(options.directory,'install.lock');
|
|
52
|
+
try {if(now()-statSync(lock).mtimeMs>10*60_000)unlinkSync(lock);} catch { /* no stale lock */ }
|
|
53
|
+
if(now()-lastCheck>=4*60*60_000) {
|
|
54
|
+
await (options.check??checkForRuntimeUpdate)({root:options.root,stateDirectory:options.directory,idle:options.idle,
|
|
55
|
+
activate:()=>options.activate?.()});
|
|
56
|
+
lastCheck=options.idle()?now():0;
|
|
57
|
+
}
|
|
58
|
+
} catch {options.report?.();}
|
|
59
|
+
finally {if(!stopped){timer=schedule(()=>void tick(),60_000);timer.unref();}}
|
|
60
|
+
};
|
|
61
|
+
timer=schedule(()=>void tick(),60_000);timer.unref();
|
|
62
|
+
return ()=>{stopped=true;if(timer)clearTimeout(timer);};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Only the owned installed background monitor downloads updates; source checkouts stay fixed. */
|
|
66
|
+
export function startRuntimeUpdates(idle:()=>boolean):()=>void {
|
|
67
|
+
if(process.env.GRANTTAP_DISABLE_AUTO_UPDATE==='1' || process.env.GRANTTAP_LOCAL==='1'
|
|
68
|
+
|| process.env.NODE_TEST_CONTEXT || !process.send)return ()=>{};
|
|
69
|
+
const root=fileURLToPath(new URL('../../../../',import.meta.url));
|
|
70
|
+
if(!root.includes('node_modules'))return ()=>{};
|
|
71
|
+
return runtimeUpdateLoop({root,directory:join(configDir(),'runtime-updates'),idle,
|
|
72
|
+
activate:()=>process.send?.({type:'granttap.runtime-update-ready'},()=>{}),
|
|
73
|
+
report:()=>process.stderr.write('[granttap-mcp] Automatic runtime update deferred; current version retained.\n')});
|
|
74
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Runtime update preference
|
|
2
|
+
|
|
3
|
+
The public runtime-updates entry exports `runtimeUpdateSettings` and
|
|
4
|
+
`setRuntimeAutoUpdate`. The connection card stores the computer-wide choice in
|
|
5
|
+
`runtime-updates/preferences.json`, separately from version selection and pairing.
|
|
6
|
+
Missing settings retain the existing enabled default; unreadable settings pause
|
|
7
|
+
updates. A service-level `GRANTTAP_DISABLE_AUTO_UPDATE=1` takes precedence.
|
|
8
|
+
|
|
9
|
+
The monitor checks the saved preference before registry access and before
|
|
10
|
+
activation. Its loop observes changes without restarting. The launcher checks it
|
|
11
|
+
again before asking the owned service to drain or stopping that service. Turning
|
|
12
|
+
updates off retains the selected working version and startup rollback protection.
|
|
13
|
+
|
|
14
|
+
Integration tests in `../tests/preferences.test.ts` cover live off/on and an
|
|
15
|
+
in-flight update. Connection-center tests cover persistence through actual MCP
|
|
16
|
+
calls, pairing preservation, service overrides, and the DOM switch.
|
|
17
|
+
|
|
18
|
+
License: [GrantTap Commercial Source License](../../../../../LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { configDir } from '../../config';
|
|
3
|
+
import { automaticUpdateSettings, saveAutomaticUpdateSetting } from './storage.mjs';
|
|
4
|
+
|
|
5
|
+
export function runtimeUpdateSettings() {
|
|
6
|
+
return automaticUpdateSettings(join(configDir(), 'runtime-updates'));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A running monitor reads this preference before its next check and before activation. */
|
|
10
|
+
export function setRuntimeAutoUpdate(enabled: boolean): void {
|
|
11
|
+
saveAutomaticUpdateSetting(join(configDir(), 'runtime-updates'), enabled);
|
|
12
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type AutomaticUpdateSettings = { enabled: boolean; lockedByEnvironment: boolean };
|
|
2
|
+
export function automaticUpdateSettings(directory: string): AutomaticUpdateSettings;
|
|
3
|
+
export function automaticUpdatesAllowed(directory: string): boolean;
|
|
4
|
+
export function saveAutomaticUpdateSetting(directory: string, enabled: boolean): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/** Missing preferences preserve the installed default; unreadable settings pause updates. */
|
|
6
|
+
export function automaticUpdateSettings(directory) {
|
|
7
|
+
const lockedByEnvironment = process.env.GRANTTAP_DISABLE_AUTO_UPDATE === '1';
|
|
8
|
+
let enabled;
|
|
9
|
+
try {
|
|
10
|
+
const value = JSON.parse(readFileSync(join(directory, 'preferences.json'), 'utf8'));
|
|
11
|
+
enabled = value?.automaticUpdatesEnabled === true;
|
|
12
|
+
} catch (error) { enabled = error?.code === 'ENOENT'; }
|
|
13
|
+
return { enabled: enabled && !lockedByEnvironment, lockedByEnvironment };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function automaticUpdatesAllowed(directory) {
|
|
17
|
+
return automaticUpdateSettings(directory).enabled;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Separate from selected versions, pairing, credentials, and capability policy. */
|
|
21
|
+
export function saveAutomaticUpdateSetting(directory, enabled) {
|
|
22
|
+
if (typeof enabled !== 'boolean') throw new TypeError('Automatic updates require a boolean setting');
|
|
23
|
+
if (enabled && process.env.GRANTTAP_DISABLE_AUTO_UPDATE === '1') {
|
|
24
|
+
throw new Error('Automatic updates are disabled by service configuration');
|
|
25
|
+
}
|
|
26
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
27
|
+
const path = join(directory, 'preferences.json');
|
|
28
|
+
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
29
|
+
try {
|
|
30
|
+
writeFileSync(temporary, JSON.stringify({ automaticUpdatesEnabled: enabled }) + '\n', { mode: 0o600 });
|
|
31
|
+
renameSync(temporary, path);
|
|
32
|
+
} finally {
|
|
33
|
+
try { unlinkSync(temporary); } catch (error) { if (error?.code !== 'ENOENT') throw error; }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Only the launcher's private IPC channel can ask the owned service to drain for an update. */
|
|
2
|
+
export function runtimeUpdateReadiness(idle:()=>boolean):()=>void {
|
|
3
|
+
const listener=(message:unknown)=>{
|
|
4
|
+
if((message as {type?:string})?.type==='granttap.runtime-update-check' && idle()) {
|
|
5
|
+
process.send?.({type:'granttap.runtime-update-ready'},()=>{});
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
process.on('message',listener);
|
|
9
|
+
process.send?.({type:'granttap.runtime-healthy'},()=>{});
|
|
10
|
+
return ()=>{process.off('message',listener);};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Count async work until completion; an update cannot interrupt a partially delivered command. */
|
|
14
|
+
export function runtimeWorkTracker(blocked:()=>boolean) {
|
|
15
|
+
let count=0;
|
|
16
|
+
return {
|
|
17
|
+
idle:()=>count===0 && !blocked(),
|
|
18
|
+
run:async<T>(work:()=>Promise<T>):Promise<T>=>{
|
|
19
|
+
count++;
|
|
20
|
+
try {return await work();} finally {count--;}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { stableVersion, validRuntime, versionRoot } from './selection.mjs';
|
|
5
|
+
|
|
6
|
+
export type Release = {name:string;version:string;dist:{integrity:string;tarball:string}};
|
|
7
|
+
const registry='https://registry.npmjs.org';
|
|
8
|
+
export function trustedRelease(value:unknown):value is Release {
|
|
9
|
+
const r=value as Release|undefined;
|
|
10
|
+
return r?.name==='granttap-mcp' && stableVersion(r.version)
|
|
11
|
+
&& typeof r.dist?.integrity==='string' && /^sha512-[A-Za-z0-9+/]{86}==$/.test(r.dist.integrity)
|
|
12
|
+
&& r.dist.tarball===`${registry}/granttap-mcp/-/granttap-mcp-${r.version}.tgz`;
|
|
13
|
+
}
|
|
14
|
+
/** Registry metadata is bounded, timed, and never changes the installed runtime by itself. */
|
|
15
|
+
export async function latestRelease():Promise<unknown> {
|
|
16
|
+
const response=await fetch(registry+'/granttap-mcp/latest',{signal:AbortSignal.timeout(10_000),redirect:'error'});
|
|
17
|
+
if(!response.ok || !response.body) throw Error('Runtime registry unavailable');
|
|
18
|
+
const reader=response.body.getReader();let size=0;const chunks:Uint8Array[]=[];
|
|
19
|
+
try { for(;;) {const {done,value}=await reader.read();if(done)break;
|
|
20
|
+
size+=value.byteLength;if(size>262_144)throw Error('Runtime metadata exceeds limit');chunks.push(value);
|
|
21
|
+
} } finally {await reader.cancel();}
|
|
22
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
23
|
+
}
|
|
24
|
+
export function run(command:string,args:string[],timeoutMs=180_000,env:NodeJS.ProcessEnv=process.env):Promise<void> {
|
|
25
|
+
return new Promise((resolve,reject)=>{
|
|
26
|
+
const child=spawn(command,args,{env,stdio:'ignore',windowsHide:true});let done=false;
|
|
27
|
+
const finish=(error?:Error)=>{if(done)return;done=true;clearTimeout(timer);error?reject(error):resolve();};
|
|
28
|
+
const timer=setTimeout(()=>{child.kill();finish(Error('Runtime update command timed out'));},timeoutMs);
|
|
29
|
+
child.once('error',()=>finish(Error('Runtime update command could not start')));
|
|
30
|
+
child.once('exit',code=>finish(code===0?undefined:Error('Runtime update command failed')));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function npmCli():string {
|
|
34
|
+
const bin=dirname(process.execPath);
|
|
35
|
+
for(const path of [join(bin,'npm'),join(bin,'node_modules','npm','bin','npm-cli.js'),
|
|
36
|
+
join(bin,'..','lib','node_modules','npm','bin','npm-cli.js')]) {
|
|
37
|
+
if(existsSync(path) && !path.endsWith('.cmd'))return realpathSync(path);
|
|
38
|
+
}
|
|
39
|
+
throw Error('The installed Node runtime has no npm updater');
|
|
40
|
+
}
|
|
41
|
+
/** npm verifies registry integrity and Node compatibility in a version-specific, isolated prefix. */
|
|
42
|
+
export async function installRelease(release:Release,directory:string,runner=run):Promise<void> {
|
|
43
|
+
await runner(process.execPath,[npmCli(),'install','--prefix',join(directory,'versions',release.version),
|
|
44
|
+
'--registry',registry,'--ignore-scripts','--engine-strict','--omit=dev','--no-audit','--no-fund',
|
|
45
|
+
'--save-exact',`granttap-mcp@${release.version}`]);
|
|
46
|
+
const lock=JSON.parse(readFileSync(join(directory,'versions',release.version,'package-lock.json'),'utf8'));
|
|
47
|
+
const installed=lock.packages?.['node_modules/granttap-mcp'];
|
|
48
|
+
if(installed?.version!==release.version || installed?.integrity!==release.dist.integrity
|
|
49
|
+
|| installed?.resolved!==release.dist.tarball || !validRuntime(versionRoot(directory,release.version),directory,release.version)) {
|
|
50
|
+
throw Error('Installed runtime identity or registry integrity did not match');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function verifyRelease(root:string):Promise<void> {
|
|
54
|
+
await run(process.execPath,[join(root,'bin','granttap-mcp.mjs'),'--help'],15_000,
|
|
55
|
+
{...process.env,GRANTTAP_DISABLE_AUTO_UPDATE:'1'});
|
|
56
|
+
// Validate the launcher without starting services or reading the live pairing.
|
|
57
|
+
await run(process.execPath,['--check',join(root,'bin','granttap-mcp.mjs')],15_000);
|
|
58
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type RuntimeState = {root?:string;version?:string;rejectedVersion?:string;previous?:RuntimeState};
|
|
2
|
+
export function stableVersion(value:unknown):value is string;
|
|
3
|
+
export function newerVersion(candidate:string,current:string):boolean;
|
|
4
|
+
export function readState(directory:string):RuntimeState;
|
|
5
|
+
export function saveState(directory:string,state:RuntimeState):void;
|
|
6
|
+
export function runtimeVersion(root:string):string;
|
|
7
|
+
export function versionRoot(directory:string,version:string):string;
|
|
8
|
+
export function validRuntime(root:string|undefined,directory:string,version:string|undefined):boolean;
|
|
9
|
+
export function selectedRuntime(bundled:string,directory:string):string;
|
|
10
|
+
export function rollbackRuntime(bundled:string,directory:string,failedRoot?:string):boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const stableVersion = value => typeof value === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value);
|
|
5
|
+
export function newerVersion(candidate, current) {
|
|
6
|
+
if (!stableVersion(candidate) || !stableVersion(current)) return false;
|
|
7
|
+
const a=candidate.split('.').map(Number), b=current.split('.').map(Number);
|
|
8
|
+
for(let i=0;i<3;i++) { if(a[i]!==b[i]) return a[i]>b[i]; }
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
export function readState(directory) {
|
|
12
|
+
try { return JSON.parse(readFileSync(join(directory,'active.json'),'utf8')); } catch { return {}; }
|
|
13
|
+
}
|
|
14
|
+
export function saveState(directory, state) {
|
|
15
|
+
mkdirSync(directory,{recursive:true,mode:0o700});
|
|
16
|
+
const path=join(directory,'active.json'), temporary=path+'.'+process.pid+'.tmp';
|
|
17
|
+
writeFileSync(temporary,JSON.stringify(state),{mode:0o600});renameSync(temporary,path);
|
|
18
|
+
}
|
|
19
|
+
export function runtimeVersion(root) {
|
|
20
|
+
const p=JSON.parse(readFileSync(join(root,'package.json'),'utf8'));
|
|
21
|
+
if(p.name!=='granttap-mcp' || !stableVersion(p.version)) throw Error('Invalid GrantTap runtime package');
|
|
22
|
+
return p.version;
|
|
23
|
+
}
|
|
24
|
+
export function versionRoot(directory, version) {
|
|
25
|
+
return join(directory,'versions',version,'node_modules','granttap-mcp');
|
|
26
|
+
}
|
|
27
|
+
export function validRuntime(root, directory, version) {
|
|
28
|
+
try { return stableVersion(version) && resolve(root)===resolve(versionRoot(directory,version))
|
|
29
|
+
&& runtimeVersion(root)===version && existsSync(join(root,'bin','granttap-mcp.mjs')); } catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
/** The stable launcher accepts only a verified, newer package inside its private version cache. */
|
|
32
|
+
export function selectedRuntime(bundled, directory) {
|
|
33
|
+
const state=readState(directory);
|
|
34
|
+
return validRuntime(state.root,directory,state.version) && newerVersion(state.version,runtimeVersion(bundled))
|
|
35
|
+
? state.root : bundled;
|
|
36
|
+
}
|
|
37
|
+
/** A new background service that exits during startup restores the last working selection. */
|
|
38
|
+
export function rollbackRuntime(bundled, directory, failedRoot) {
|
|
39
|
+
const state=readState(directory);
|
|
40
|
+
if(!state.version || (failedRoot && state.root!==failedRoot)) return false;
|
|
41
|
+
const rejectedVersion=state.version, previous=state.previous;
|
|
42
|
+
if(previous && validRuntime(previous.root,directory,previous.version)) {
|
|
43
|
+
saveState(directory,{...previous,rejectedVersion});
|
|
44
|
+
} else saveState(directory,{rejectedVersion});
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { selectedRuntime, rollbackRuntime } from './selection.mjs';
|
|
2
|
+
import { automaticUpdatesAllowed } from './preferences/storage.mjs';
|
|
3
|
+
|
|
4
|
+
/** Monitor only the child started by this launcher; external providers are never stopped. */
|
|
5
|
+
export function superviseRuntime(child, options) {
|
|
6
|
+
const {bundled,root,directory}=options;
|
|
7
|
+
const started=(options.now??Date.now)();let healthy=root===bundled,stopped=false;
|
|
8
|
+
const read=()=>selectedRuntime(bundled,directory);
|
|
9
|
+
const changed=()=>read()!==root;
|
|
10
|
+
const stop=()=>{if(stopped)return;stopped=true;clearInterval(timer);child.off('message',onMessage);};
|
|
11
|
+
const onMessage=message=>{
|
|
12
|
+
if(message?.type==='granttap.runtime-healthy')healthy=true;
|
|
13
|
+
if(message?.type==='granttap.runtime-update-ready' && automaticUpdatesAllowed(directory) && changed()) {stop();child.kill('SIGTERM');}
|
|
14
|
+
};
|
|
15
|
+
const tick=()=>{
|
|
16
|
+
if(stopped)return;
|
|
17
|
+
if(!healthy && (options.now??Date.now)()-started>120_000) {
|
|
18
|
+
rollbackRuntime(bundled,directory,root);stop();child.kill('SIGTERM');return;
|
|
19
|
+
}
|
|
20
|
+
if(automaticUpdatesAllowed(directory) && changed() && child.connected)child.send({type:'granttap.runtime-update-check'},()=>{});
|
|
21
|
+
};
|
|
22
|
+
const timer=(options.schedule??setInterval)(tick,10_000);timer.unref?.();child.on('message',onMessage);
|
|
23
|
+
return {stop,onExit:(code,signal)=>{
|
|
24
|
+
if(!stopped && !healthy && code!==0 && !['SIGTERM','SIGINT'].includes(signal)) {
|
|
25
|
+
rollbackRuntime(bundled,directory,root);
|
|
26
|
+
}
|
|
27
|
+
stop();
|
|
28
|
+
}};
|
|
29
|
+
}
|
|
@@ -35,6 +35,11 @@ answers below the current request. Native completion marks the final visible
|
|
|
35
35
|
answer. Existing entry IDs replace cached timestamps without duplicating history.
|
|
36
36
|
Session continuation uses the installed Grok CLI and its existing authentication.
|
|
37
37
|
|
|
38
|
+
Prepared transfer chats retain their stable Task identity and are ranked again
|
|
39
|
+
after native-session aliasing, before the bounded live and history windows.
|
|
40
|
+
A transferred chat therefore keeps its native activity position instead of
|
|
41
|
+
being appended behind older chats and disappearing from the phone's catalog.
|
|
42
|
+
|
|
38
43
|
## License
|
|
39
44
|
|
|
40
45
|
Licensed under the GrantTap Commercial Source License. See LICENSE and NOTICE.
|
|
@@ -56,9 +56,9 @@ async function cooperativeProviderScans(maxFiles = MAX_FILES): Promise<ProviderS
|
|
|
56
56
|
return scans;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function sortedUniqueSessions(
|
|
59
|
+
function sortedUniqueSessions(sessions: SessionInfo[]): SessionInfo[] {
|
|
60
60
|
const merged = new Map<string, SessionInfo>();
|
|
61
|
-
for (const session of
|
|
61
|
+
for (const session of sessions) {
|
|
62
62
|
const key = sessionKey(session);
|
|
63
63
|
const previous = merged.get(key);
|
|
64
64
|
if (!previous || session.lastActivityAt >= previous.lastActivityAt) {
|
|
@@ -97,8 +97,12 @@ function pickWithReserve(
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function catalogFromScans(scans: ProviderScan[]) {
|
|
100
|
+
const native = sortedUniqueSessions(scans.flatMap((scan) => scan.sessions));
|
|
101
|
+
// Prepared transfers replace native ids; rank their native activity before
|
|
102
|
+
// the bounded live/history windows, rather than appending them after older chats.
|
|
103
|
+
const sessions = sortedUniqueSessions(localTaskTransfers().catalog(native));
|
|
100
104
|
return {
|
|
101
|
-
all:
|
|
105
|
+
all: sessions.map(session => ({ ...session,
|
|
102
106
|
transfer: localTransferContinuations().notice(session.sessionId) })),
|
|
103
107
|
tokensRecent: scans.reduce((sum, scan) => sum + scan.tokensRecent, 0),
|
|
104
108
|
sourceLimited: scans.some((scan) => scan.sourceLimited === true),
|
|
@@ -32,6 +32,13 @@ the one-time URL from structured tool output when a host omits MCP metadata.
|
|
|
32
32
|
The separate coding-app OAuth page can also use passkey to authorize that MCP
|
|
33
33
|
client. The Codex-owned plugin management page is not this card.
|
|
34
34
|
|
|
35
|
+
The Automatic updates switch calls `set_runtime_auto_update` with an explicit
|
|
36
|
+
boolean and displays the confirmed persisted result from the server. The setting
|
|
37
|
+
is computer-wide for the GrantTap runtime; marketplace plugin updates stay under
|
|
38
|
+
the coding app's control. A service environment override locks it off. Failed
|
|
39
|
+
saves do not optimistically change the switch. Pairing and capability policy are
|
|
40
|
+
stored separately and preserved.
|
|
41
|
+
|
|
35
42
|
Tests in `tests/` exercise isolated keys, a loopback encrypted phone connection,
|
|
36
43
|
expiry, non-mutating status, and actual DOM button/host-message behavior.
|
|
37
44
|
|
|
@@ -70,11 +70,11 @@ async function call(name, args = {}) {
|
|
|
70
70
|
const result = legacyBridge
|
|
71
71
|
? await legacyCall(name, args)
|
|
72
72
|
: await request("tools/call", { name, arguments: args });
|
|
73
|
-
if (result?.isError) message("Could not complete the request. Refresh status
|
|
73
|
+
if (result?.isError) message("Could not complete the request. Refresh status before retrying.");
|
|
74
74
|
else { render(result); message("Status updated."); }
|
|
75
75
|
return result;
|
|
76
76
|
} catch {
|
|
77
|
-
message("No successful response. Refresh status before retrying
|
|
77
|
+
message("No successful response. Refresh status before retrying.");
|
|
78
78
|
} finally {
|
|
79
79
|
setBusy(false);
|
|
80
80
|
}
|
|
@@ -7,6 +7,7 @@ import { packageVersion } from "../status/package-version";
|
|
|
7
7
|
import { connectionRuntimeStatus } from "../mcp-tools/connect/relay";
|
|
8
8
|
import { beginEnrollment, cancelEnrollment, enrollmentIsOpen } from "./enrollment";
|
|
9
9
|
import { loadAccountLink } from "../../../bridge/src/account-recovery/link";
|
|
10
|
+
import { runtimeUpdateSettings } from "../../../bridge/src/runtime-updates";
|
|
10
11
|
|
|
11
12
|
export const connectionOutput = {
|
|
12
13
|
status: z.enum(["disconnected", "paired", "pairing", "expired", "connected"]),
|
|
@@ -25,6 +26,7 @@ export const connectionOutput = {
|
|
|
25
26
|
providers: z.array(z.object({ id: z.string(), status: z.string(), detail: z.string() })),
|
|
26
27
|
roomPrefix: z.string(),
|
|
27
28
|
accountLinkSaved: z.boolean(),
|
|
29
|
+
runtimeUpdates: z.object({ enabled: z.boolean(), lockedByEnvironment: z.boolean() }),
|
|
28
30
|
passkeyUrl: z.string().url().optional(),
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -80,6 +82,7 @@ export class ConnectionState {
|
|
|
80
82
|
providers: inspectProviderStatusSnapshot().providers.map(({ id, status, detail }) => ({ id, status, detail })),
|
|
81
83
|
roomPrefix: config?.room ? config.room.slice(0, 8) : "",
|
|
82
84
|
accountLinkSaved: loadAccountLink() !== null,
|
|
85
|
+
runtimeUpdates: runtimeUpdateSettings(),
|
|
83
86
|
},
|
|
84
87
|
_meta: { granttap: code ? { pairingUri: code.pairingUri, qrDataUrl: code.qrDataUrl } : {} },
|
|
85
88
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { setRuntimeAutoUpdate } from '../../../bridge/src/runtime-updates';
|
|
5
|
+
import { connectionOutput } from './state';
|
|
6
|
+
|
|
7
|
+
export function registerRuntimeUpdateControl(
|
|
8
|
+
server: McpServer, result: () => CallToolResult, meta: Record<string, unknown>,
|
|
9
|
+
): void {
|
|
10
|
+
server.registerTool('set_runtime_auto_update', {
|
|
11
|
+
title: 'GrantTap automatic updates',
|
|
12
|
+
description: 'Enable or disable automatic GrantTap runtime updates on this computer when the user requests it. Saves the preference without restarting services or changing pairing, tasks, or provider policy.',
|
|
13
|
+
inputSchema: { enabled: z.boolean().describe('Whether this computer should automatically check for and apply stable GrantTap runtime updates') },
|
|
14
|
+
outputSchema: connectionOutput,
|
|
15
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
16
|
+
_meta: meta,
|
|
17
|
+
}, async ({ enabled }) => {
|
|
18
|
+
try { setRuntimeAutoUpdate(enabled); return result(); }
|
|
19
|
+
catch {
|
|
20
|
+
return { isError: true, content: [{ type: 'text', text: 'Could not save automatic update settings. Service configuration may disable updates. Refresh status to see the current setting.' }] };
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -7,6 +7,16 @@ function message(text) { $("message").textContent = text; }
|
|
|
7
7
|
function setBusy(value) {
|
|
8
8
|
busy = value;
|
|
9
9
|
document.querySelectorAll("button").forEach(button => { button.disabled = value; });
|
|
10
|
+
renderUpdateSetting();
|
|
11
|
+
}
|
|
12
|
+
function renderUpdateSetting() {
|
|
13
|
+
const setting = current.runtimeUpdates;
|
|
14
|
+
$("automatic-updates").textContent = "Automatic updates: " + (setting ? setting.enabled ? "On" : "Off" : "Unknown");
|
|
15
|
+
$("automatic-updates").setAttribute("aria-checked", String(setting?.enabled === true));
|
|
16
|
+
$("automatic-updates").disabled = busy || !setting || setting.lockedByEnvironment;
|
|
17
|
+
$("updates-detail").textContent = setting?.lockedByEnvironment
|
|
18
|
+
? "Disabled by service configuration."
|
|
19
|
+
: "Checks for stable GrantTap runtime updates every 4 hours. Off keeps this computer’s current version.";
|
|
10
20
|
}
|
|
11
21
|
function clearCode() {
|
|
12
22
|
pairingUri = "";
|
|
@@ -19,6 +29,7 @@ function render(value) {
|
|
|
19
29
|
const state = result?.structuredContent ?? result?.structured_content;
|
|
20
30
|
if (!state?.status || result.isError) return;
|
|
21
31
|
current = state;
|
|
32
|
+
renderUpdateSetting();
|
|
22
33
|
clearCode();
|
|
23
34
|
const meta = result._meta?.granttap ?? {};
|
|
24
35
|
const candidate = state.passkeyUrl || meta.passkeyUrl;
|
|
@@ -102,6 +113,7 @@ $("connect").addEventListener("click", () => {
|
|
|
102
113
|
call("connect");
|
|
103
114
|
});
|
|
104
115
|
$("refresh").addEventListener("click", () => call("connection_status"));
|
|
116
|
+
$("automatic-updates").addEventListener("click", () => call("set_runtime_auto_update", { enabled: !current.runtimeUpdates.enabled }));
|
|
105
117
|
$("passkey").addEventListener("click", () => call("connect_with_passkey"));
|
|
106
118
|
$("reconnect").addEventListener("click", () => {
|
|
107
119
|
pendingMode = "reconnect";
|
|
@@ -31,6 +31,7 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
|
|
|
31
31
|
<img id="qr" class="hidden" alt="GrantTap pairing QR" />
|
|
32
32
|
</section>
|
|
33
33
|
<p id="expiry" class="small" aria-live="polite"></p>
|
|
34
|
+
<section aria-label="Runtime updates"><div class="actions"><button id="automatic-updates" role="switch" aria-checked="false" aria-describedby="updates-detail" disabled>Automatic updates: Unknown</button></div><p id="updates-detail" class="small">Checking update settings…</p></section>
|
|
34
35
|
<details><summary>Connection details & help</summary>
|
|
35
36
|
<dl><dt>Computer</dt><dd id="computer">—</dd><dt>MCP version</dt><dd id="version">—</dd>
|
|
36
37
|
<dt>Relay observed by this MCP</dt><dd id="relay">Not checked</dd>
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { trackHttpRuntimeWork } from "../../../bridge/src/runtime-updates/http-readiness";
|
|
2
|
+
import { runtimeUpdateReadiness } from "../../../bridge/src/runtime-updates/readiness";
|
|
3
|
+
import { approvalsStatus } from "../../../bridge/src/approvals/state";
|
|
1
4
|
/**
|
|
2
5
|
* GrantTap MCP over Streamable HTTP + loopback OAuth with website consent.
|
|
3
6
|
*
|
|
@@ -63,6 +66,7 @@ export async function startHttpMcpServer(options: ServeOptions = {}): Promise<{
|
|
|
63
66
|
const issuerUrl = new URL(`http://${host === "::1" ? "[::1]" : host}:${port}`);
|
|
64
67
|
const provider = new GrantTapOAuthProvider(mcpUrl.href);
|
|
65
68
|
const app = createMcpExpressApp({ host });
|
|
69
|
+
const httpIdle = trackHttpRuntimeWork(app);
|
|
66
70
|
app.use(express.urlencoded({ extended: false }));
|
|
67
71
|
app.use(mcpAuthRouter({
|
|
68
72
|
provider,
|
|
@@ -120,11 +124,13 @@ export async function startHttpMcpServer(options: ServeOptions = {}): Promise<{
|
|
|
120
124
|
const stopAccountRecovery = accountOrigin
|
|
121
125
|
? startAccountRecoveryPoller(accountOrigin, refreshRecoveredPairing) : () => {};
|
|
122
126
|
|
|
127
|
+
const stopUpdateReadiness=runtimeUpdateReadiness(()=>httpIdle() && approvalsStatus().pending.length===0);
|
|
123
128
|
return {
|
|
124
129
|
host,
|
|
125
130
|
port,
|
|
126
131
|
mcpUrl: mcpUrl.href,
|
|
127
132
|
close: async () => {
|
|
133
|
+
stopUpdateReadiness();
|
|
128
134
|
stopAccountRecovery();
|
|
129
135
|
await sessions.close();
|
|
130
136
|
resetConnectWatchers();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# MCP tools
|
|
2
2
|
|
|
3
3
|
The Personal MCP surface is an exact allowlist: `connection_status`, `connect`,
|
|
4
|
-
`
|
|
4
|
+
`connect_with_passkey`, `reconnect`, `set_runtime_auto_update`, `notify`,
|
|
5
|
+
`ask_yes_no`, `ask`, and `offload_task`. Context compilation belongs
|
|
5
6
|
inside Project Mesh, not as extra tools. `relay.ts` owns one shared encrypted relay connection
|
|
6
7
|
and the durable question lifecycle. Machine setup, custom relay selection, and
|
|
7
8
|
pairing reset remains an explicit CLI operation. The narrower `reconnect` MCP
|
|
@@ -32,3 +33,7 @@ The connection center lives in `../connection-center`. Status requests are
|
|
|
32
33
|
read-only. Saved pairing, a connected relay socket, and recent encrypted phone
|
|
33
34
|
activity are separate observations. Only explicit connect/reconnect requests
|
|
34
35
|
create codes; transfer material is carried in UI metadata and a user-facing QR.
|
|
36
|
+
|
|
37
|
+
`set_runtime_auto_update` saves the user’s computer-wide runtime update choice
|
|
38
|
+
from the connection card. It preserves pairing and capability policy, and cannot
|
|
39
|
+
override a service-level disable. Status exposes the confirmed setting.
|
|
@@ -8,6 +8,7 @@ import { ConnectionState, connectionOutput } from "../../connection-center/state
|
|
|
8
8
|
import { CONNECTION_WIDGET_URI } from "./widget";
|
|
9
9
|
import { resetRelay, relay } from "./relay";
|
|
10
10
|
import { startPasskeyAccountLink } from "../../connection-center/passkey";
|
|
11
|
+
import { registerRuntimeUpdateControl } from "../../connection-center/update-controls";
|
|
11
12
|
|
|
12
13
|
const widgetMeta = {
|
|
13
14
|
ui: { resourceUri: CONNECTION_WIDGET_URI, visibility: ["model", "app"] },
|
|
@@ -26,6 +27,7 @@ const changes = {
|
|
|
26
27
|
|
|
27
28
|
export function registerConnectTool(server: McpServer): void {
|
|
28
29
|
const state = new ConnectionState();
|
|
30
|
+
registerRuntimeUpdateControl(server, () => connectionResult(state), widgetMeta);
|
|
29
31
|
let pendingCall: Promise<CallToolResult> | null = null;
|
|
30
32
|
const perform = (options: { issue?: boolean; replace?: boolean; addController?: boolean } = {}) => {
|
|
31
33
|
if (pendingCall) return pendingCall;
|
|
@@ -126,7 +128,7 @@ async function connect(
|
|
|
126
128
|
|
|
127
129
|
function connectionResult(state: ConnectionState): CallToolResult {
|
|
128
130
|
const snapshot = state.snapshot();
|
|
129
|
-
const { status, computer, relay, version, relayStatus } = snapshot.structuredContent;
|
|
131
|
+
const { status, computer, relay, version, relayStatus, runtimeUpdates } = snapshot.structuredContent;
|
|
130
132
|
const instructions = status === "pairing"
|
|
131
133
|
? "Use the GrantTap card: tap Add a device if needed, then scan the QR on the card."
|
|
132
134
|
: status === "disconnected" ? "Use the GrantTap card and tap Add a device. The QR stays on the card."
|
|
@@ -135,7 +137,7 @@ function connectionResult(state: ConnectionState): CallToolResult {
|
|
|
135
137
|
return {
|
|
136
138
|
...snapshot,
|
|
137
139
|
content: [{ type: "text", text:
|
|
138
|
-
`GrantTap: ${status}. Computer: ${computer}. MCP ${version}. Relay: ${relay || "not configured"} (${relayStatus}).\n${instructions}`,
|
|
140
|
+
`GrantTap: ${status}. Computer: ${computer}. MCP ${version}. Relay: ${relay || "not configured"} (${relayStatus}). Automatic runtime updates: ${runtimeUpdates.enabled ? "on" : "off"}.\n${instructions}`,
|
|
139
141
|
}],
|
|
140
142
|
};
|
|
141
143
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
|
|
3
|
-
export const CONNECTION_WIDGET_URI = "ui://granttap/connection/
|
|
3
|
+
export const CONNECTION_WIDGET_URI = "ui://granttap/connection/v5.html";
|
|
4
4
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
|
package/bin/granttap-mcp.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { selectedRuntime, runtimeVersion, rollbackRuntime } from "../apps/bridge/src/runtime-updates/selection.mjs";
|
|
6
|
+
import { superviseRuntime } from "../apps/bridge/src/runtime-updates/supervision.mjs";
|
|
4
7
|
import { createRequire } from "node:module";
|
|
5
8
|
import { delimiter, dirname, join } from "node:path";
|
|
6
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -12,7 +15,12 @@ if (!isSupportedNode(process.versions.node)) {
|
|
|
12
15
|
process.exit(1);
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
const
|
|
18
|
+
const bundledRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const updateDirectory = join(process.env.GRANTTAP_CONFIG_DIR ?? process.env.NODVOX_CONFIG_DIR
|
|
20
|
+
?? join(homedir(), ".granttap"), "runtime-updates");
|
|
21
|
+
const managed = bundledRoot.includes("node_modules") && process.env.GRANTTAP_LOCAL !== "1"
|
|
22
|
+
&& process.env.GRANTTAP_DISABLE_AUTO_UPDATE !== "1" && !process.env.NODE_TEST_CONTEXT;
|
|
23
|
+
const root = managed ? selectedRuntime(bundledRoot, updateDirectory) : bundledRoot;
|
|
16
24
|
const [command = "mcp", ...commandArgs] = process.argv.slice(2);
|
|
17
25
|
const argument = commandArgs[0];
|
|
18
26
|
|
|
@@ -85,7 +93,7 @@ if (command === "mcp") {
|
|
|
85
93
|
|
|
86
94
|
// Cursor (and LaunchAgents) often spawn with cwd=$HOME. Bare `--import tsx` then
|
|
87
95
|
// resolves from home and dies with ERR_MODULE_NOT_FOUND. Pin absolute loaders + cwd.
|
|
88
|
-
const requireFromBin = createRequire(
|
|
96
|
+
const requireFromBin = createRequire(join(root,"package.json"));
|
|
89
97
|
let preflight;
|
|
90
98
|
let loader;
|
|
91
99
|
try {
|
|
@@ -105,6 +113,7 @@ try {
|
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
if (!preflight || !loader) {
|
|
116
|
+
if (root !== bundledRoot) rollbackRuntime(bundledRoot, updateDirectory, root);
|
|
108
117
|
process.stderr.write(
|
|
109
118
|
"[granttap-mcp] unable to resolve the installed tsx runtime — reinstall granttap-mcp\n",
|
|
110
119
|
);
|
|
@@ -114,22 +123,27 @@ if (!preflight || !loader) {
|
|
|
114
123
|
const forwardedArgs = ["connect", "status", "reset", "mesh", "setup", "restrictions", "phone"].includes(command)
|
|
115
124
|
? (command === "mesh" || command === "restrictions" ? commandArgs.slice(1) : commandArgs)
|
|
116
125
|
: [];
|
|
126
|
+
const background = command === "internal" && ["monitor", "serve"].includes(argument);
|
|
117
127
|
const child = spawn(
|
|
118
128
|
process.execPath,
|
|
119
129
|
["--require", preflight, "--import", pathToFileURL(loader).href,
|
|
120
130
|
"--import", pathToFileURL(join(root, "apps", "bridge", "src", "engine", "startup", "activation-entry.ts")).href,
|
|
121
131
|
entry, ...forwardedArgs],
|
|
122
132
|
{
|
|
123
|
-
stdio: "inherit",
|
|
133
|
+
stdio: background ? ["inherit", "inherit", "inherit", "ipc"] : "inherit",
|
|
124
134
|
cwd: root,
|
|
125
135
|
env: {
|
|
126
136
|
...process.env,
|
|
137
|
+
GRANTTAP_PACKAGE_VERSION: runtimeVersion(root),
|
|
127
138
|
NODE_PATH: [join(root, "node_modules"), process.env.NODE_PATH].filter(Boolean).join(delimiter),
|
|
128
139
|
},
|
|
129
140
|
},
|
|
130
141
|
);
|
|
131
142
|
|
|
143
|
+
const supervision = background && managed ? superviseRuntime(child, {bundled:bundledRoot, root, directory:updateDirectory}) : undefined;
|
|
144
|
+
|
|
132
145
|
function stopChild() {
|
|
146
|
+
supervision?.stop();
|
|
133
147
|
try {
|
|
134
148
|
if (process.platform === "win32") child.kill();
|
|
135
149
|
else child.kill("SIGTERM");
|
|
@@ -148,6 +162,7 @@ child.on("error", (error) => {
|
|
|
148
162
|
});
|
|
149
163
|
|
|
150
164
|
child.on("exit", (code, signal) => {
|
|
165
|
+
supervision?.onExit(code, signal);
|
|
151
166
|
if (signal && process.platform !== "win32") {
|
|
152
167
|
try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }
|
|
153
168
|
} else process.exit(code ?? 1);
|
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.104
|
|
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.104\";\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 22.13.0+ 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.104";
|
|
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;
|