granttap-mcp 0.8.20 → 0.8.21
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 +9 -0
- package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +10 -0
- package/apps/bridge/src/engine/protocol/transport/result-parser.ts +50 -0
- package/apps/bridge/src/engine/runtime/engine-projects.ts +17 -23
- package/apps/bridge/src/engine/runtime/repository-graph/fit.ts +86 -0
- package/apps/bridge/src/mesh/runtime/catalog/session-catalog.ts +23 -0
- package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +22 -10
- package/apps/bridge/src/mesh/runtime/index.ts +6 -5
- package/apps/bridge/src/monitor/handlers/inbound.ts +16 -5
- package/apps/bridge/src/reply/README.md +6 -0
- package/apps/bridge/src/reply/process/index.ts +20 -3
- package/apps/mcp/src/connection-center/widget.html +1 -1
- package/cursor-plugin/.cursor-plugin/plugin.json +1 -1
- package/cursor-plugin/README.md +5 -5
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/docs/cursor-authorize.md +2 -2
- package/package.json +1 -1
- package/packages/protocol/messages/mesh/graph/code-map.ts +44 -0
- package/packages/protocol/messages/mesh/snapshot.ts +2 -0
|
@@ -3,6 +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 { meshSnapshots } from "../../mesh/runtime";
|
|
7
|
+
import { recoverLocalGraphBindings } from "../../mesh/runtime/graph/binding-sync";
|
|
6
8
|
import { startSessionMonitor } from "../../monitor";
|
|
7
9
|
import { controllerPeerGate } from "../../pairing/controllers";
|
|
8
10
|
|
|
@@ -38,6 +40,13 @@ process.on("SIGINT", stop);
|
|
|
38
40
|
process.on("SIGTERM", stop);
|
|
39
41
|
|
|
40
42
|
void engine.ensureAvailable().then((health) => {
|
|
43
|
+
if (health.state === "healthy") {
|
|
44
|
+
// A new Engine binary starts with its own registry. Re-admit existing,
|
|
45
|
+
// locally verified Git bindings so linked repositories have a graph again.
|
|
46
|
+
void recoverLocalGraphBindings(meshSnapshots())
|
|
47
|
+
.then(() => monitor.publish())
|
|
48
|
+
.catch(() => undefined);
|
|
49
|
+
}
|
|
41
50
|
if (health.state !== "disabled" && health.state !== "healthy") {
|
|
42
51
|
const reason = health.reason ? `: ${health.reason}` : "";
|
|
43
52
|
process.stderr.write(
|
|
@@ -45,6 +45,16 @@ export type EngineRepositoryGraph = {
|
|
|
45
45
|
status: "SUPPORTED" | "CANDIDATE" | "CONTRADICTED" | "INSUFFICIENT_EVIDENCE";
|
|
46
46
|
evidence: string[]; contradictions: string[]; unknowns: string[];
|
|
47
47
|
}>;
|
|
48
|
+
code_map?: {
|
|
49
|
+
files: Array<{ path: string; language: string | null; line_count: number | null;
|
|
50
|
+
symbols: Array<{ id: string; label: string; kind: string;
|
|
51
|
+
start_line: number; line_count: number }> }>;
|
|
52
|
+
externals?: Array<{ id: string; label: string; kind: string }>;
|
|
53
|
+
roads: Array<{ source: string; target: string; relation: string }>;
|
|
54
|
+
total_files: number;
|
|
55
|
+
total_externals?: number;
|
|
56
|
+
truncated: boolean;
|
|
57
|
+
} | null;
|
|
48
58
|
nodes: Array<{ id: string; kind: string; label: string }>;
|
|
49
59
|
relations: Array<{ source: string; target: string; relation: string; evidence_count?: number }>;
|
|
50
60
|
total_nodes: number;
|
|
@@ -148,6 +148,7 @@ function parseRepositoryGraph(value: unknown): void {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
if (graph.code_map != null) parseCodeMap(graph.code_map);
|
|
151
152
|
if (!Array.isArray(graph.nodes) || graph.nodes.length > 256
|
|
152
153
|
|| !Array.isArray(graph.relations) || graph.relations.length > 512
|
|
153
154
|
|| !Number.isSafeInteger(graph.total_nodes) || Number(graph.total_nodes) < 0
|
|
@@ -170,6 +171,55 @@ function parseRepositoryGraph(value: unknown): void {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
function parseCodeMap(value: unknown): void {
|
|
175
|
+
const map = requireObject(value, "repository code map");
|
|
176
|
+
if (!Array.isArray(map.files) || map.files.length > 650
|
|
177
|
+
|| (map.externals != null && (!Array.isArray(map.externals) || map.externals.length > 24))
|
|
178
|
+
|| !Array.isArray(map.roads) || map.roads.length > 1_200
|
|
179
|
+
|| !Number.isSafeInteger(map.total_files) || Number(map.total_files) < map.files.length
|
|
180
|
+
|| (map.total_externals != null && !Number.isSafeInteger(map.total_externals))
|
|
181
|
+
|| Number(map.total_externals ?? 0) < (Array.isArray(map.externals) ? map.externals.length : 0)
|
|
182
|
+
|| typeof map.truncated !== "boolean") invalidResult();
|
|
183
|
+
const paths = new Set<string>();
|
|
184
|
+
for (const value of map.files) {
|
|
185
|
+
const file = requireObject(value, "repository code file");
|
|
186
|
+
requireBoundedString(file.path, "code path", 512);
|
|
187
|
+
const path = file.path as string;
|
|
188
|
+
if (path.startsWith("/") || path.split("/").includes("..") || paths.has(path)) invalidResult();
|
|
189
|
+
paths.add(path);
|
|
190
|
+
requireOptionalString(file.language, "code language", 64);
|
|
191
|
+
if (file.line_count != null
|
|
192
|
+
&& (!Number.isSafeInteger(file.line_count) || Number(file.line_count) < 1)) invalidResult();
|
|
193
|
+
if (!Array.isArray(file.symbols) || file.symbols.length > 72) invalidResult();
|
|
194
|
+
for (const value of file.symbols) {
|
|
195
|
+
const symbol = requireObject(value, "code symbol");
|
|
196
|
+
requireBoundedString(symbol.id, "code symbol id", 512);
|
|
197
|
+
requireBoundedString(symbol.label, "code symbol label", 160);
|
|
198
|
+
requireBoundedString(symbol.kind, "code symbol kind", 64);
|
|
199
|
+
if (!Number.isSafeInteger(symbol.start_line) || Number(symbol.start_line) < 1
|
|
200
|
+
|| !Number.isSafeInteger(symbol.line_count) || Number(symbol.line_count) < 1) invalidResult();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const value of (map.externals ?? []) as unknown[]) {
|
|
204
|
+
const external = requireObject(value, "repository external resource");
|
|
205
|
+
requireBoundedString(external.id, "external id", 512);
|
|
206
|
+
requireBoundedString(external.label, "external label", 160);
|
|
207
|
+
requireBoundedString(external.kind, "external kind", 64);
|
|
208
|
+
const id = external.id as string;
|
|
209
|
+
if (!id.startsWith("ext:") || id.length < 5 || paths.has(id)) invalidResult();
|
|
210
|
+
paths.add(id);
|
|
211
|
+
}
|
|
212
|
+
for (const value of map.roads) {
|
|
213
|
+
const road = requireObject(value, "code road");
|
|
214
|
+
requireBoundedString(road.source, "road source", 512);
|
|
215
|
+
requireBoundedString(road.target, "road target", 512);
|
|
216
|
+
const source = road.source as string;
|
|
217
|
+
const target = road.target as string;
|
|
218
|
+
requireBoundedString(road.relation, "road relation", 64);
|
|
219
|
+
if (!paths.has(source) || !paths.has(target)) invalidResult();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
173
223
|
function parseBackbone(value: unknown): void {
|
|
174
224
|
const backbone = requireObject(value, "project backbone");
|
|
175
225
|
requireBoundedString(backbone.project_id, "project_id", 128);
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
EngineContextEvidence,
|
|
16
16
|
} from "../protocol/engine-protocol";
|
|
17
17
|
import { RepositoryGraphJobs } from "./repository-graph-jobs";
|
|
18
|
+
import { fitRepositoryGraph } from "./repository-graph/fit";
|
|
18
19
|
|
|
19
20
|
export type LocalProjectBinding = {
|
|
20
21
|
summary: ProjectBindingSummary;
|
|
@@ -26,8 +27,8 @@ export type LocalProjectBinding = {
|
|
|
26
27
|
|
|
27
28
|
let sharedClient: EngineClient | undefined;
|
|
28
29
|
const pendingBindingSyncs = new Map<string, Promise<boolean>>();
|
|
29
|
-
const MAX_REPOSITORY_GRAPH_WIRE_BYTES =
|
|
30
|
-
const MAX_SINGLE_GRAPH_WIRE_BYTES =
|
|
30
|
+
const MAX_REPOSITORY_GRAPH_WIRE_BYTES = 256 * 1_024;
|
|
31
|
+
const MAX_SINGLE_GRAPH_WIRE_BYTES = 192 * 1_024;
|
|
31
32
|
const repositoryGraphJobs = new RepositoryGraphJobs<ProjectRepositoryGraph>();
|
|
32
33
|
|
|
33
34
|
export async function syncProjectBinding(
|
|
@@ -211,6 +212,20 @@ async function analyzeRepositoryGraph(
|
|
|
211
212
|
analysisId: graph.analysis_id ?? undefined,
|
|
212
213
|
analysisStatus: graph.analysis_status,
|
|
213
214
|
architectureHypotheses: graph.architecture_hypotheses,
|
|
215
|
+
codeMap: graph.code_map ? {
|
|
216
|
+
files: graph.code_map.files.map((file) => ({
|
|
217
|
+
path: file.path, language: file.language, lineCount: file.line_count,
|
|
218
|
+
symbols: file.symbols.map((symbol) => ({
|
|
219
|
+
id: symbol.id, label: symbol.label, kind: symbol.kind,
|
|
220
|
+
startLine: symbol.start_line, lineCount: symbol.line_count,
|
|
221
|
+
})),
|
|
222
|
+
})),
|
|
223
|
+
externals: graph.code_map.externals ?? [],
|
|
224
|
+
roads: graph.code_map.roads,
|
|
225
|
+
totalFiles: graph.code_map.total_files,
|
|
226
|
+
totalExternals: graph.code_map.total_externals ?? 0,
|
|
227
|
+
truncated: graph.code_map.truncated,
|
|
228
|
+
} : undefined,
|
|
214
229
|
nodes: graph.nodes,
|
|
215
230
|
relations: graph.relations.map((edge) => ({
|
|
216
231
|
source: edge.source, target: edge.target, relation: edge.relation,
|
|
@@ -249,27 +264,6 @@ function boundRepositoryGraphs(graphs: ProjectRepositoryGraph[]): ProjectReposit
|
|
|
249
264
|
return result;
|
|
250
265
|
}
|
|
251
266
|
|
|
252
|
-
function fitRepositoryGraph(
|
|
253
|
-
graph: ProjectRepositoryGraph,
|
|
254
|
-
maxBytes: number,
|
|
255
|
-
): ProjectRepositoryGraph | undefined {
|
|
256
|
-
let nodes = graph.nodes;
|
|
257
|
-
let relations = graph.relations;
|
|
258
|
-
let bounded = graph;
|
|
259
|
-
while (Buffer.byteLength(JSON.stringify(bounded), "utf8") > maxBytes
|
|
260
|
-
&& (nodes.length > 8 || relations.length > 8)) {
|
|
261
|
-
if (relations.length >= nodes.length && relations.length > 8) {
|
|
262
|
-
relations = relations.slice(0, Math.max(8, Math.floor(relations.length / 2)));
|
|
263
|
-
} else if (nodes.length > 8) {
|
|
264
|
-
nodes = nodes.slice(0, Math.max(8, Math.floor(nodes.length / 2)));
|
|
265
|
-
const ids = new Set(nodes.map((node) => node.id));
|
|
266
|
-
relations = relations.filter((edge) => ids.has(edge.source) && ids.has(edge.target));
|
|
267
|
-
}
|
|
268
|
-
bounded = { ...graph, nodes, relations, truncated: true };
|
|
269
|
-
}
|
|
270
|
-
return Buffer.byteLength(JSON.stringify(bounded), "utf8") <= maxBytes ? bounded : undefined;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
267
|
function defaultClient(): EngineClient {
|
|
274
268
|
sharedClient ??= new EngineClient({ socketPath: join(configDir(), "engine.sock") });
|
|
275
269
|
return sharedClient;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ProjectRepositoryGraph } from "../../../../../../packages/protocol/schema";
|
|
2
|
+
|
|
3
|
+
type CodeMap = NonNullable<ProjectRepositoryGraph["codeMap"]>;
|
|
4
|
+
type Relation = ProjectRepositoryGraph["relations"][number];
|
|
5
|
+
|
|
6
|
+
/** Keep a navigable code city and the strongest architecture evidence in the snapshot envelope. */
|
|
7
|
+
export function fitRepositoryGraph(
|
|
8
|
+
graph: ProjectRepositoryGraph,
|
|
9
|
+
maxBytes: number,
|
|
10
|
+
): ProjectRepositoryGraph | undefined {
|
|
11
|
+
let bounded = graph;
|
|
12
|
+
const bytes = () => Buffer.byteLength(JSON.stringify(bounded), "utf8");
|
|
13
|
+
if (bytes() <= maxBytes) return bounded;
|
|
14
|
+
if (bounded.relations.length > 128) {
|
|
15
|
+
bounded = { ...bounded, relations: strongestRelations(bounded.relations, 128), truncated: true };
|
|
16
|
+
}
|
|
17
|
+
while (bytes() > maxBytes) {
|
|
18
|
+
const map = bounded.codeMap;
|
|
19
|
+
if (map?.files.some((file) => file.symbols.length > 0)) {
|
|
20
|
+
bounded = { ...bounded, codeMap: trimSymbols(map) };
|
|
21
|
+
} else if (bounded.relations.length > 8) {
|
|
22
|
+
bounded = { ...bounded, relations: strongestRelations(bounded.relations,
|
|
23
|
+
Math.ceil(bounded.relations.length / 2)), truncated: true };
|
|
24
|
+
} else if (bounded.nodes.length > 8) {
|
|
25
|
+
const nodes = bounded.nodes.slice(0, Math.ceil(bounded.nodes.length / 2));
|
|
26
|
+
const ids = new Set(nodes.map((node) => node.id));
|
|
27
|
+
bounded = { ...bounded, nodes, relations: bounded.relations.filter((edge) =>
|
|
28
|
+
ids.has(edge.source) && ids.has(edge.target)), truncated: true };
|
|
29
|
+
} else if (map && map.roads.length > 16) {
|
|
30
|
+
bounded = { ...bounded, codeMap: { ...map, truncated: true,
|
|
31
|
+
roads: map.roads.slice(0, Math.ceil(map.roads.length / 2)) } };
|
|
32
|
+
} else if (map && map.files.length > 8) {
|
|
33
|
+
bounded = { ...bounded, codeMap: trimFiles(map) };
|
|
34
|
+
} else if (map) {
|
|
35
|
+
bounded = { ...bounded, codeMap: undefined };
|
|
36
|
+
} else {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return bounded;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function strongestRelations(relations: Relation[], limit: number): Relation[] {
|
|
44
|
+
return relations.map((edge, index) => ({ edge, index }))
|
|
45
|
+
.sort((left, right) => (right.edge.evidenceCount ?? 0) - (left.edge.evidenceCount ?? 0)
|
|
46
|
+
|| left.index - right.index)
|
|
47
|
+
.slice(0, limit).sort((left, right) => left.index - right.index)
|
|
48
|
+
.map(({ edge }) => edge);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function roadDegree(map: CodeMap): Map<string, number> {
|
|
52
|
+
const degree = new Map<string, number>();
|
|
53
|
+
for (const road of map.roads) {
|
|
54
|
+
degree.set(road.source, (degree.get(road.source) ?? 0) + 1);
|
|
55
|
+
degree.set(road.target, (degree.get(road.target) ?? 0) + 1);
|
|
56
|
+
}
|
|
57
|
+
return degree;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function rankedFiles(map: CodeMap): CodeMap["files"] {
|
|
61
|
+
const degree = roadDegree(map);
|
|
62
|
+
return [...map.files].sort((left, right) =>
|
|
63
|
+
(degree.get(right.path) ?? 0) - (degree.get(left.path) ?? 0)
|
|
64
|
+
|| (right.lineCount ?? 0) - (left.lineCount ?? 0)
|
|
65
|
+
|| left.path.localeCompare(right.path));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function trimSymbols(map: CodeMap): CodeMap {
|
|
69
|
+
const ranked = rankedFiles(map).filter((file) => file.symbols.length > 0);
|
|
70
|
+
const keep = new Set(ranked.slice(0, Math.floor(ranked.length / 2)).map((file) => file.path));
|
|
71
|
+
return { ...map, truncated: true, files: map.files.map((file) => {
|
|
72
|
+
if (!keep.has(file.path)) return { ...file, symbols: [] };
|
|
73
|
+
if (ranked.length === 1) return { ...file, symbols: file.symbols.slice(0,
|
|
74
|
+
Math.floor(file.symbols.length / 2)) };
|
|
75
|
+
return file;
|
|
76
|
+
}) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function trimFiles(map: CodeMap): CodeMap {
|
|
80
|
+
const files = rankedFiles(map).slice(0, Math.ceil(map.files.length / 2))
|
|
81
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
82
|
+
const paths = new Set(files.map((file) => file.path));
|
|
83
|
+
for (const external of map.externals ?? []) paths.add(external.id);
|
|
84
|
+
return { ...map, files, truncated: true,
|
|
85
|
+
roads: map.roads.filter((road) => paths.has(road.source) && paths.has(road.target)) };
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SessionInfo } from "../../../../../../packages/protocol/schema";
|
|
2
|
+
import { scanSessionHistory, scanSessions } from "../../../sessions";
|
|
3
|
+
import { deduplicateNativeSessions } from "../capabilities/session-discovery";
|
|
4
|
+
|
|
5
|
+
type Readers = {
|
|
6
|
+
history: () => SessionInfo[];
|
|
7
|
+
live: () => SessionInfo[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Mesh keeps old Task identities, while current provider state stays fresh. */
|
|
11
|
+
export function createMeshSessionCatalog(readers: Readers = {
|
|
12
|
+
history: scanSessionHistory,
|
|
13
|
+
live: () => scanSessions().sessions,
|
|
14
|
+
}) {
|
|
15
|
+
let history: SessionInfo[] | undefined;
|
|
16
|
+
return {
|
|
17
|
+
sessions(): SessionInfo[] {
|
|
18
|
+
history ??= readers.history();
|
|
19
|
+
return deduplicateNativeSessions(history, readers.live());
|
|
20
|
+
},
|
|
21
|
+
invalidateHistory(): void { history = undefined; },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -17,15 +17,27 @@ export async function refreshLocalGraphBindings(
|
|
|
17
17
|
const candidates = (snapshot.bindings ?? []).filter((binding) =>
|
|
18
18
|
binding.endpointId === localEndpoint && binding.available && binding.localPathHint);
|
|
19
19
|
await Promise.all(candidates.map(async (binding) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|| facts.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
try {
|
|
21
|
+
const path = binding.localPathHint!;
|
|
22
|
+
const facts = dependencies.inspect(path);
|
|
23
|
+
if (!facts.worktree || resolve(facts.root) !== resolve(path)
|
|
24
|
+
|| facts.canonicalRepositoryId !== binding.repositoryId) return;
|
|
25
|
+
await dependencies.sync(snapshot.project, {
|
|
26
|
+
summary: binding,
|
|
27
|
+
localRoot: facts.root,
|
|
28
|
+
canonicalRemote: facts.baseRemote,
|
|
29
|
+
lastSeenAt: Date.now(),
|
|
30
|
+
});
|
|
31
|
+
} catch { /* A stale checkout cannot prevent other verified bindings from recovery. */ }
|
|
30
32
|
}));
|
|
31
33
|
}
|
|
34
|
+
|
|
35
|
+
/** Rehydrate previously admitted Git checkouts after Engine restart. */
|
|
36
|
+
export async function recoverLocalGraphBindings(
|
|
37
|
+
snapshots: MeshSnapshot[],
|
|
38
|
+
dependencies?: Parameters<typeof refreshLocalGraphBindings>[1],
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
for (const snapshot of snapshots) {
|
|
41
|
+
await refreshLocalGraphBindings(snapshot, dependencies);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
deliverToSession,
|
|
25
25
|
} from "../../reply";
|
|
26
26
|
import { isProviderEnabled } from "../../config/runtime";
|
|
27
|
-
import { scanSessionHistory, scanSessions } from "../../sessions";
|
|
28
27
|
import { sendMeshPayload } from "../../host/session-keys";
|
|
29
28
|
import { linkSessionsToProjects, workingTreeState } from "../catalog";
|
|
30
29
|
import { catalogFromSessions } from "../catalog/models";
|
|
@@ -42,21 +41,23 @@ import { localMeshStore } from "../local-remote/local";
|
|
|
42
41
|
import { createHandoffWorktree, repositoryHasCommit } from "./worktree";
|
|
43
42
|
import { fetchRevision, pushBranch } from "../local-remote/remote";
|
|
44
43
|
import { projectSessionCapabilityInventory, projectExecutionCapabilitySessions } from "./capabilities";
|
|
45
|
-
import { deduplicateNativeSessions } from "./capabilities/session-discovery";
|
|
46
44
|
import { refreshLocalGraphBindings } from "./graph/binding-sync";
|
|
45
|
+
import { createMeshSessionCatalog } from "./catalog/session-catalog";
|
|
47
46
|
|
|
48
47
|
export type { MeshRuntimeDependencies };
|
|
49
48
|
|
|
50
49
|
/** Who handed the payload in: the relay (the phone, another computer) or an agent's tool call. */
|
|
51
50
|
export type MeshPayloadOrigin = "relay" | "agent";
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const sessionCatalog = createMeshSessionCatalog();
|
|
53
|
+
|
|
54
|
+
export function invalidateMeshSessionHistory(): void {
|
|
55
|
+
sessionCatalog.invalidateHistory();
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const defaultDependencies: MeshRuntimeDependencies = {
|
|
58
59
|
store: localMeshStore,
|
|
59
|
-
sessions:
|
|
60
|
+
sessions: sessionCatalog.sessions,
|
|
60
61
|
capabilityInventory: projectSessionCapabilityInventory,
|
|
61
62
|
computer: () => computerId(),
|
|
62
63
|
now: Date.now,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
handleMeshPayload,
|
|
11
11
|
prepareMeshHandoff,
|
|
12
12
|
requestProjectCapability,
|
|
13
|
+
invalidateMeshSessionHistory,
|
|
13
14
|
} from "../../mesh/runtime";
|
|
14
15
|
import { releaseClaimByPerson, releaseResult } from "../../mesh/admin";
|
|
15
16
|
import { handleInvocationQuery } from "../../engine/invocation/query";
|
|
@@ -72,11 +73,7 @@ export async function handleMonitorMessage(
|
|
|
72
73
|
void publishSessionEvents(client, payload.sessionId, undefined, payload.threadId).catch(() => false);
|
|
73
74
|
return true;
|
|
74
75
|
} else if (payload.type === "sessions.refresh") {
|
|
75
|
-
|
|
76
|
-
// a phone that cleared local state can receive an apparently empty tick.
|
|
77
|
-
void publish(true).catch(() => {});
|
|
78
|
-
if (payload.graphProjectId && loadRuntimeConfig().meshEnabled) requestProjectGraphRefresh(client, payload.graphProjectId);
|
|
79
|
-
return true;
|
|
76
|
+
return handleInboundRefresh(client, payload, publish);
|
|
80
77
|
} else if (payload.type === "sessions.history.query") {
|
|
81
78
|
await publishHistoryPage(client, payload);
|
|
82
79
|
return true;
|
|
@@ -135,6 +132,20 @@ export async function handleMonitorMessage(
|
|
|
135
132
|
return false;
|
|
136
133
|
}
|
|
137
134
|
|
|
135
|
+
function handleInboundRefresh(
|
|
136
|
+
client: RelayClient, payload: Extract<Payload, { type: "sessions.refresh" }>,
|
|
137
|
+
publish: (forceHistory?: boolean) => Promise<void>,
|
|
138
|
+
): boolean {
|
|
139
|
+
// An explicit refresh may pay for the full history scan; routine Mesh
|
|
140
|
+
// snapshots use the cached history and fresh live sessions.
|
|
141
|
+
invalidateMeshSessionHistory();
|
|
142
|
+
void publish(true).catch(() => {});
|
|
143
|
+
if (payload.graphProjectId && loadRuntimeConfig().meshEnabled) {
|
|
144
|
+
requestProjectGraphRefresh(client, payload.graphProjectId);
|
|
145
|
+
}
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
function handleInboundToolUpdate(
|
|
139
150
|
client: RelayClient, payload: Extract<Payload, { type: "tool.update" }>,
|
|
140
151
|
publish: (forceHistory?: boolean) => Promise<void>,
|
|
@@ -6,5 +6,11 @@ This module prepares phone attachments, routes selected MCP/skill intent, and ru
|
|
|
6
6
|
handoff resumes the same Task on another agent or computer. Providers without an
|
|
7
7
|
implemented remote-start path fail closed instead of reporting parity.
|
|
8
8
|
|
|
9
|
+
On POSIX, a managed provider command owns a process group. Phone pause and
|
|
10
|
+
timeouts signal that group, so local descendants that remain in it are also
|
|
11
|
+
requested to stop. The stop count records accepted signals, not confirmed exit
|
|
12
|
+
of every descendant. Windows uses the direct-child stop path; remote jobs and
|
|
13
|
+
resources that outlive the agent process need their own cancellation receipts.
|
|
14
|
+
|
|
9
15
|
License: this module is distributed under the MIT License
|
|
10
16
|
in the repository-root `LICENSE` file.
|
|
@@ -36,14 +36,30 @@ function track(key: string, child: ChildProcess): () => void {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
function signalOwnedProcess(child: ChildProcess, signal: NodeJS.Signals): boolean {
|
|
40
|
+
if (process.platform !== "win32" && child.pid && child.pid > 0) {
|
|
41
|
+
try {
|
|
42
|
+
// POSIX detached children lead a new process group. Signal descendants
|
|
43
|
+
// as well as the provider CLI, without touching the monitor's group.
|
|
44
|
+
process.kill(-child.pid, signal);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
// The group may already have exited; fall back to the direct child.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return child.kill(signal);
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/** Request a stop for owned child processes; count signals accepted, not exits. */
|
|
40
54
|
export function abortProcesses(key: string): number {
|
|
41
55
|
const set = running.get(key);
|
|
42
56
|
if (!set) return 0;
|
|
43
57
|
let stopped = 0;
|
|
44
58
|
for (const child of set) {
|
|
45
|
-
(child
|
|
46
|
-
|
|
59
|
+
if (signalOwnedProcess(child, "SIGTERM")) {
|
|
60
|
+
(child as ChildProcess & { granttapStopped?: boolean }).granttapStopped = true;
|
|
61
|
+
stopped += 1;
|
|
62
|
+
}
|
|
47
63
|
}
|
|
48
64
|
return stopped;
|
|
49
65
|
}
|
|
@@ -70,6 +86,7 @@ export function runProcess(
|
|
|
70
86
|
// hand a background run the journal kept for the live session.
|
|
71
87
|
env: { ...process.env, GRANTTAP_DELIVERY: "1", ...projectEnvForCwd(cwd) },
|
|
72
88
|
stdio: [stdin == null ? "ignore" : "pipe", "pipe", "pipe"],
|
|
89
|
+
detached: process.platform !== "win32",
|
|
73
90
|
});
|
|
74
91
|
} catch (error) {
|
|
75
92
|
resolve({ ok: false, error: `${command} did not start: ${(error as Error).message}` });
|
|
@@ -89,7 +106,7 @@ export function runProcess(
|
|
|
89
106
|
};
|
|
90
107
|
const timer = setTimeout(() => {
|
|
91
108
|
if (done) return;
|
|
92
|
-
child
|
|
109
|
+
signalOwnedProcess(child, "SIGKILL");
|
|
93
110
|
finish({ ok: false, error: `${command} did not respond within ${Math.round(timeoutMs / 1000)}s.` });
|
|
94
111
|
}, timeoutMs);
|
|
95
112
|
child.stdout?.on("data", (chunk) => { stdout += chunk; });
|
|
@@ -38,7 +38,7 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
|
|
|
38
38
|
<h3>Provider readiness</h3><ul id="providers"></ul>
|
|
39
39
|
<p>No GrantTap account or password is required. Pair using GrantTap on iPhone → Settings → Connections.
|
|
40
40
|
Provider sign-in is separate.</p>
|
|
41
|
-
<p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.
|
|
41
|
+
<p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.21</code> and <code>granttap setup</code> locally.
|
|
42
42
|
A saved pairing alone does not prove your phone or the background helper is online.</p>
|
|
43
43
|
<p><a href="https://granttap.com" target="_blank" rel="noopener noreferrer">Website</a> ·
|
|
44
44
|
<a href="https://granttap.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> ·
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granttap",
|
|
3
3
|
"displayName": "GrantTap",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.14",
|
|
5
5
|
"description": "The agent connects this computer in chat. Authorize in plugin settings opens the device window. Scan the QR in GrantTap — that scan authorizes. Device list and Add another stay there and in the GrantTap app.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GrantTap",
|
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.21
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
|
@@ -49,10 +49,10 @@ import the public repository once and reload Cursor. The plugin adds `/connect`,
|
|
|
49
49
|
skill, and the exact-correlation rule; `granttap setup` configures the local
|
|
50
50
|
MCP endpoint separately.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
second
|
|
52
|
+
Pushing this repository updates the plugin source. Cursor reviews marketplace
|
|
53
|
+
updates separately, so confirm the displayed version after review. Keep `name`
|
|
54
|
+
as `granttap` and the GitHub URL as `sergii-ziborov/granttap-mcp`; do not create
|
|
55
|
+
a second listing. The only Cursor marketplace file is the repo-root
|
|
56
56
|
`.cursor-plugin/marketplace.json`, and it lists this plugin once.
|
|
57
57
|
|
|
58
58
|
If Customize or Marketplace shows both a lowercase **granttap** cube and an
|
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.21\";\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.21";
|
|
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/docs/cursor-authorize.md
CHANGED
|
@@ -14,8 +14,8 @@ GrantTap in Cursor Customize → MCPs.
|
|
|
14
14
|
Cursor Cloud Agents cannot reach `http://127.0.0.1`. An HTTP MCP entry
|
|
15
15
|
there — leftover user Customize → MCPs, or an old plugin `mcp.json` — is why
|
|
16
16
|
Cloud shows **Error · fetch failed**. The Marketplace plugin uses stdio
|
|
17
|
-
(`node -e` so cwd `$HOME` still starts GrantTap; on Windows
|
|
18
|
-
|
|
17
|
+
(`node -e` so cwd `$HOME` still starts GrantTap; on Windows the bootstrap
|
|
18
|
+
launches npm's JavaScript entry point through Node).
|
|
19
19
|
Authorize in the browser on `granttap.com/connect`; that page talks only to
|
|
20
20
|
the website, not to loopback. Pairing stays on the connection card, last
|
|
21
21
|
resort a QR there.
|
package/package.json
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const CodePath = z.string().trim().min(1).max(512).refine((value) =>
|
|
4
|
+
!value.startsWith("/") && !value.split("/").includes(".."), "repository-relative path required");
|
|
5
|
+
|
|
6
|
+
export const ProjectCodeMap = z.object({
|
|
7
|
+
files: z.array(z.object({
|
|
8
|
+
path: CodePath,
|
|
9
|
+
language: z.string().trim().min(1).max(64).nullable(),
|
|
10
|
+
lineCount: z.number().int().positive().nullable(),
|
|
11
|
+
symbols: z.array(z.object({
|
|
12
|
+
id: z.string().trim().min(1).max(512),
|
|
13
|
+
label: z.string().max(160),
|
|
14
|
+
kind: z.string().trim().min(1).max(64),
|
|
15
|
+
startLine: z.number().int().positive(),
|
|
16
|
+
lineCount: z.number().int().positive(),
|
|
17
|
+
}).strict()).max(72),
|
|
18
|
+
}).strict()).max(650),
|
|
19
|
+
externals: z.array(z.object({
|
|
20
|
+
id: z.string().startsWith("ext:").min(5).max(512),
|
|
21
|
+
label: z.string().max(160),
|
|
22
|
+
kind: z.string().trim().min(1).max(64),
|
|
23
|
+
}).strict()).max(24).optional(),
|
|
24
|
+
roads: z.array(z.object({
|
|
25
|
+
source: CodePath,
|
|
26
|
+
target: CodePath,
|
|
27
|
+
relation: z.string().trim().min(1).max(64),
|
|
28
|
+
}).strict()).max(1_200),
|
|
29
|
+
totalFiles: z.number().int().nonnegative(),
|
|
30
|
+
totalExternals: z.number().int().nonnegative().optional(),
|
|
31
|
+
truncated: z.boolean(),
|
|
32
|
+
}).strict().superRefine((value, ctx) => {
|
|
33
|
+
const paths = new Set(value.files.map((file) => file.path));
|
|
34
|
+
const externals = new Set((value.externals ?? []).map((node) => node.id));
|
|
35
|
+
const references = new Set([...paths, ...externals]);
|
|
36
|
+
if (paths.size !== value.files.length || value.totalFiles < value.files.length
|
|
37
|
+
|| externals.size !== (value.externals?.length ?? 0)
|
|
38
|
+
|| (value.totalExternals ?? 0) < (value.externals?.length ?? 0)
|
|
39
|
+
|| value.roads.some((road) => !references.has(road.source) || !references.has(road.target))) {
|
|
40
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "invalid code tower references" });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type ProjectCodeMap = z.infer<typeof ProjectCodeMap>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ProjectCodeMap } from "./graph/code-map";
|
|
2
3
|
import { Identifier, Label, MeshProvider } from "./endpoint";
|
|
3
4
|
import {
|
|
4
5
|
ProjectEnvironment,
|
|
@@ -161,6 +162,7 @@ export const ProjectRepositoryGraph = z.object({
|
|
|
161
162
|
contradictions: z.array(z.string().max(240)).max(16),
|
|
162
163
|
unknowns: z.array(z.string().max(160)).max(4),
|
|
163
164
|
}).strict()).max(8).optional(),
|
|
165
|
+
codeMap: ProjectCodeMap.optional(),
|
|
164
166
|
nodes: z.array(z.object({
|
|
165
167
|
id: z.string().trim().min(1).max(512),
|
|
166
168
|
kind: z.string().trim().min(1).max(64),
|