granttap-mcp 0.8.48 → 0.8.50
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/apps/bridge/src/bin/runtime/monitor.ts +24 -27
- package/apps/bridge/src/engine/runtime/engine-projects.ts +10 -3
- package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +10 -2
- package/apps/bridge/src/monitor/handlers/inbound.ts +1 -1
- package/apps/bridge/src/monitor/support/runtime.ts +10 -7
- package/apps/bridge/src/session-catalog/README.md +5 -2
- package/apps/bridge/src/session-catalog/index.ts +1 -0
- package/apps/bridge/src/session-catalog/startup-recovery.ts +12 -0
- package/apps/mcp/src/desktop/task-activity-runner.ts +1 -1
- 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
|
@@ -3,7 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { RelayClient } from "../../../../../packages/core/relay-client";
|
|
4
4
|
import { configDir, loadConfig, machineConfigPath } from "../../config";
|
|
5
5
|
import { EngineSupervisor } from "../../engine/runtime/engine-supervisor";
|
|
6
|
-
import {
|
|
6
|
+
import { meshSnapshotsAsync } from "../../mesh/runtime";
|
|
7
|
+
import { publishedCatalog, recoverCatalogBindings } from "../../session-catalog";
|
|
7
8
|
import { recoverLocalGraphBindings } from "../../mesh/runtime/graph/binding-sync";
|
|
8
9
|
import { startSessionMonitor } from "../../monitor";
|
|
9
10
|
import { controllerPeerGate } from "../../pairing/controllers";
|
|
@@ -46,29 +47,25 @@ const stop = (): void => {
|
|
|
46
47
|
process.on("SIGINT", stop);
|
|
47
48
|
process.on("SIGTERM", stop);
|
|
48
49
|
|
|
49
|
-
void
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
process.stderr.write(
|
|
72
|
-
`[granttap-mcp] initial relay connection failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
73
|
-
);
|
|
74
|
-
});
|
|
50
|
+
void recoverCatalogBindings({
|
|
51
|
+
engineAvailable: async () => {
|
|
52
|
+
const health = await engine.ensureAvailable();
|
|
53
|
+
if (health.state !== "disabled" && health.state !== "healthy") {
|
|
54
|
+
process.stderr.write(`[granttap-mcp] Project engine ${health.state}; legacy behavior remains active\n`);
|
|
55
|
+
}
|
|
56
|
+
return health.state === "healthy";
|
|
57
|
+
},
|
|
58
|
+
publishCatalog: async () => {
|
|
59
|
+
try { await client.connect(); await monitor.publish(); }
|
|
60
|
+
catch (error) {
|
|
61
|
+
// RelayClient retries connection independently; keep this helper alive.
|
|
62
|
+
process.stderr.write(`[granttap-mcp] initial catalog connection failed (${error instanceof Error ? error.name : "unknown"})\n`);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
snapshots: () => {
|
|
67
|
+
const known = publishedCatalog(client);
|
|
68
|
+
return meshSnapshotsAsync(known ? [...known.sessions, ...(known.history ?? [])] : []);
|
|
69
|
+
},
|
|
70
|
+
recover: recoverLocalGraphBindings,
|
|
71
|
+
}).then(() => monitor.publish()).catch(() => undefined);
|
|
@@ -38,7 +38,10 @@ export async function syncProjectBinding(
|
|
|
38
38
|
): Promise<boolean> {
|
|
39
39
|
const env = options.env ?? process.env;
|
|
40
40
|
if (!engineFeatureEnabled(env)) return false;
|
|
41
|
-
|
|
41
|
+
// Repository admission is restore work; keep it off the policy connection.
|
|
42
|
+
const client = options.client ?? new EngineClient({
|
|
43
|
+
socketPath: join(configDir(), "engine.sock"), connectTimeoutMs: 10_000,
|
|
44
|
+
});
|
|
42
45
|
try {
|
|
43
46
|
const result = await client.request({
|
|
44
47
|
operation: "project.upsert_binding",
|
|
@@ -63,10 +66,12 @@ export async function syncProjectBinding(
|
|
|
63
66
|
last_seen_at: local.lastSeenAt,
|
|
64
67
|
},
|
|
65
68
|
},
|
|
66
|
-
}, { timeoutMs:
|
|
69
|
+
}, { timeoutMs: 30_000 });
|
|
67
70
|
return result.operation === "project.binding_upserted";
|
|
68
71
|
} catch {
|
|
69
72
|
return false;
|
|
73
|
+
} finally {
|
|
74
|
+
if (!options.client) client.close?.();
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -203,7 +208,9 @@ async function analyzeRepositoryGraph(
|
|
|
203
208
|
): Promise<ProjectRepositoryGraph | undefined> {
|
|
204
209
|
// The daemon serves a socket serially. Analysis must not occupy the channel
|
|
205
210
|
// used for policy, bindings and Backbone reads while a repository is scanned.
|
|
206
|
-
const channel = client ?? new EngineClient({
|
|
211
|
+
const channel = client ?? new EngineClient({
|
|
212
|
+
socketPath: join(configDir(), "engine.sock"), connectTimeoutMs: 10_000,
|
|
213
|
+
});
|
|
207
214
|
try {
|
|
208
215
|
const result = await channel.request({
|
|
209
216
|
operation: "graph.analyze_repository", input: {
|
|
@@ -29,17 +29,25 @@ export async function refreshLocalGraphBindings(
|
|
|
29
29
|
nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false });
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
await dependencies.sync(snapshot.project, {
|
|
32
|
+
const admitted = await dependencies.sync(snapshot.project, {
|
|
33
33
|
summary: binding,
|
|
34
34
|
localRoot: facts.root,
|
|
35
35
|
canonicalRemote: facts.baseRemote,
|
|
36
36
|
lastSeenAt: Date.now(),
|
|
37
37
|
});
|
|
38
|
-
|
|
38
|
+
if (!admitted) failures.push(admissionFailure(snapshot, binding));
|
|
39
|
+
} catch { failures.push(admissionFailure(snapshot, binding)); }
|
|
39
40
|
}));
|
|
40
41
|
return failures;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function admissionFailure(snapshot: MeshSnapshot, binding: NonNullable<MeshSnapshot["bindings"]>[number]): ProjectRepositoryGraph {
|
|
45
|
+
return { projectId: snapshot.projectId, repositoryId: binding.repositoryId,
|
|
46
|
+
revision: "unverified", weavatrixVersion: "unknown", analysisStatus: "UNAVAILABLE",
|
|
47
|
+
analysisErrorCode: "ENGINE_ADMISSION_UNAVAILABLE", nodes: [], relations: [],
|
|
48
|
+
totalNodes: 0, totalRelations: 0, truncated: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
/** Rehydrate previously admitted Git checkouts after Engine restart. */
|
|
44
52
|
export async function recoverLocalGraphBindings(
|
|
45
53
|
snapshots: MeshSnapshot[],
|
|
@@ -270,7 +270,7 @@ function handleInboundSubscription(
|
|
|
270
270
|
// The transcript goes first, before a potentially slow catalog rescan.
|
|
271
271
|
void publishSessionEvents(client, payload.sessionId).catch(() => false);
|
|
272
272
|
if (handleSubscription(subscriptions, payload)) {
|
|
273
|
-
void publish(
|
|
273
|
+
void publish().catch(() => {});
|
|
274
274
|
}
|
|
275
275
|
return true;
|
|
276
276
|
}
|
|
@@ -26,7 +26,8 @@ import { boundedCatalogHistory, INTERVAL_MS, publishSessionEvents } from "../han
|
|
|
26
26
|
import { sweepAttachments } from "../handlers/receipts";
|
|
27
27
|
import { handleMonitorMessage } from "../handlers/inbound";
|
|
28
28
|
|
|
29
|
-
const HISTORY_INTERVAL_MS =
|
|
29
|
+
const HISTORY_INTERVAL_MS = 60_000;
|
|
30
|
+
const resumedHeads = new WeakSet<RelayClient>();
|
|
30
31
|
import { publishMonitorMeshes, stopMonitorMeshes } from "../mesh-publication";
|
|
31
32
|
|
|
32
33
|
export type SessionMonitor = {
|
|
@@ -118,18 +119,20 @@ async function publishMonitorTick(input: {
|
|
|
118
119
|
if (!client.isConnected) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
|
|
119
120
|
await publishHeartbeat(client).catch(() => {});
|
|
120
121
|
if (!leadership.acquire()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
(session, status) => publishSessionEvents(client, session.sessionId, status));
|
|
124
|
-
}
|
|
125
|
-
const includeHistory = forceHistory || Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS;
|
|
122
|
+
const initial = lastHistoryPublishedAt === 0;
|
|
123
|
+
const includeHistory = forceHistory || (!initial && Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS);
|
|
126
124
|
const status = monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
|
|
127
125
|
rememberPublishedCatalog(client, status);
|
|
128
126
|
await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
|
|
127
|
+
if (initial && !resumedHeads.has(client)) {
|
|
128
|
+
resumedHeads.add(client);
|
|
129
|
+
void resumePublishedChats(client,
|
|
130
|
+
(session, saved) => publishSessionEvents(client, session.sessionId, saved)).catch(() => {});
|
|
131
|
+
}
|
|
129
132
|
// Provider review is independent of task visibility; a hung hook RPC must
|
|
130
133
|
// never hold the current catalog behind its discovery timeout.
|
|
131
134
|
void refreshCodexHooks().catch(() => {});
|
|
132
|
-
if (includeHistory) lastHistoryPublishedAt = Date.now();
|
|
135
|
+
if (includeHistory || initial) lastHistoryPublishedAt = Date.now();
|
|
133
136
|
loadLoop.updateStatus(status);
|
|
134
137
|
void refreshMcpLoad(status.sessions).catch(() => {});
|
|
135
138
|
for (const sessionId of subscriptions) {
|
|
@@ -9,8 +9,11 @@ unreadable merely because hundreds of newer logs appeared.
|
|
|
9
9
|
The private `session-catalog-cache.json` contains metadata and validated native
|
|
10
10
|
source locations. It excludes messages, capability descriptions, and credentials.
|
|
11
11
|
The room and provider roots bind its scope. On restart the primary monitor sends
|
|
12
|
-
the saved Now chats'
|
|
13
|
-
|
|
12
|
+
a fresh live catalog first, then resumes the saved Now chats' native heads under
|
|
13
|
+
their independent task keys in the background. Closed-history discovery runs
|
|
14
|
+
separately from that first live publication. It does not republish an old catalog
|
|
14
15
|
as fresh. The phone continues its durable historical backfill independently.
|
|
15
16
|
|
|
16
17
|
Tests live in `tests/`. This runtime module uses the repository MIT License.
|
|
18
|
+
|
|
19
|
+
Engine binding recovery waits for the first connected catalog publication before projecting repository graphs. A cold Engine cannot hold the initial relay handshake behind restored Mesh analysis.
|
|
@@ -5,6 +5,7 @@ import type { RelayClient } from "../../../../packages/core/relay-client";
|
|
|
5
5
|
import { SessionsStatus, type SessionInfo } from "../../../../packages/protocol/schema";
|
|
6
6
|
import { configDir, writePrivateFile } from "../config";
|
|
7
7
|
import { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "../sessions";
|
|
8
|
+
export { recoverCatalogBindings } from "./startup-recovery";
|
|
8
9
|
|
|
9
10
|
const catalogs = new WeakMap<RelayClient, SessionsStatus | undefined>();
|
|
10
11
|
const cachePath = () => join(configDir(), "session-catalog-cache.json");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type StartupRecovery<T> = {
|
|
2
|
+
engineAvailable: () => Promise<boolean>;
|
|
3
|
+
publishCatalog: () => Promise<unknown>;
|
|
4
|
+
snapshots: () => Promise<T>;
|
|
5
|
+
recover: (snapshots: T) => Promise<unknown>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/** Coordinate the initial catalog and restoration of verified Engine bindings. */
|
|
9
|
+
export async function recoverCatalogBindings<T>(input: StartupRecovery<T>): Promise<void> {
|
|
10
|
+
const [available] = await Promise.all([input.engineAvailable(), input.publishCatalog()]);
|
|
11
|
+
if (available) await input.recover(await input.snapshots());
|
|
12
|
+
}
|
|
@@ -35,7 +35,7 @@ export class DesktopTaskActivityRunner {
|
|
|
35
35
|
|| typeof taskId !== "string" || !taskId || taskId.length > 128) {
|
|
36
36
|
return Promise.resolve(undefined);
|
|
37
37
|
}
|
|
38
|
-
return this.enqueue(query, storePath);
|
|
38
|
+
return this.enqueue({ ...query, operation: "desktop.task_activity" }, storePath);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
usage(): Promise<unknown> {
|
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.50
|
|
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.50\";\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.50";
|
|
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;
|