granttap-mcp 0.8.82 → 0.8.84
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 +19 -1
- package/apps/bridge/src/config/README.md +5 -1
- package/apps/bridge/src/config/index.ts +1 -0
- package/apps/bridge/src/config/runtime/node-requirement.d.mts +2 -0
- package/apps/bridge/src/config/runtime/node-requirement.mjs +15 -0
- package/apps/bridge/src/engine/distribution/artifacts.ts +6 -6
- package/apps/bridge/src/engine/protocol/transport/code-checks/README.md +11 -0
- package/apps/bridge/src/engine/protocol/transport/code-checks/index.ts +14 -0
- package/apps/bridge/src/engine/protocol/transport/protocol-types.ts +3 -1
- package/apps/bridge/src/engine/protocol/transport/result-parser.ts +2 -0
- package/apps/bridge/src/engine/runtime/engine-projects.ts +2 -14
- package/apps/bridge/src/engine/runtime/repository-graph/fit.ts +12 -7
- package/apps/bridge/src/engine/runtime/repository-graph/source-map.ts +21 -0
- package/apps/bridge/src/engine/runtime/repository-graph/source-sampling.ts +39 -0
- package/apps/bridge/src/install/monitor-helper.ts +2 -2
- package/apps/bridge/src/mesh/runtime/graph/README.md +4 -2
- package/apps/bridge/src/mesh/runtime/graph/analysis.ts +13 -3
- package/apps/bridge/src/mesh/runtime/graph/binding-sync.ts +1 -1
- package/apps/bridge/src/mesh/runtime/graph/checkout-identity.ts +26 -3
- package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +1 -1
- package/apps/mcp/src/http-service/installer.ts +3 -4
- package/bin/granttap-mcp.mjs +7 -0
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +2 -2
- package/package.json +2 -2
- package/packages/protocol/messages/mesh/graph/code-map.ts +6 -0
package/README.md
CHANGED
|
@@ -239,7 +239,17 @@ manual rebuilds join pending work and return their own delivery receipts.
|
|
|
239
239
|
Usable reports persist privately on the computer across MCP restarts, while
|
|
240
240
|
the phone keeps its last received map visible during a rebuild. Source Git
|
|
241
241
|
revision and analysis content fingerprint are separate freshness facts.
|
|
242
|
-
Partial and unavailable reports retain their scope and status.
|
|
242
|
+
Partial and unavailable reports retain their scope and status. Health separates
|
|
243
|
+
report freshness from completeness and offers local filtering, sorting and
|
|
244
|
+
diagnostics options. The code city has an independent legend, symbol-kind and
|
|
245
|
+
size colours, label and relation controls, and optional measured code checks.
|
|
246
|
+
|
|
247
|
+
Engine source checks carry measured function extent, a token-based cyclomatic
|
|
248
|
+
estimate, loop nesting and resolved call fan-in. The clients compare those
|
|
249
|
+
measurements with the selected thresholds. Green means within those checks; red
|
|
250
|
+
means review a threshold; unmeasured code remains unknown. Correctness, test
|
|
251
|
+
coverage and runtime performance are not inferred. Analysis uses this computer's
|
|
252
|
+
verified Git checkout roots, observed revision and local Git exclusions.
|
|
243
253
|
|
|
244
254
|
<p align="center">
|
|
245
255
|
<img src="docs/images/iphone-chat-work-summary.png" width="200" alt="Files and Agents above the composer with deterministic sample work">
|
|
@@ -781,6 +791,14 @@ in the Apple apps. Use the support contact there for product issues. Report
|
|
|
781
791
|
security vulnerabilities through [SECURITY.md](SECURITY.md); do not put pairing
|
|
782
792
|
QR codes, credentials or private chat transcripts into public issues.
|
|
783
793
|
|
|
794
|
+
## Node.js requirement
|
|
795
|
+
|
|
796
|
+
Install the latest patched Node.js LTS release. GrantTap requires Node.js
|
|
797
|
+
22.13.0 or newer; Node.js 18 and 20 are not supported. The CLI checks this
|
|
798
|
+
before loading dependencies or changing local configuration. The npm `engines`
|
|
799
|
+
field also reports this requirement, but npm may only warn unless engine checks
|
|
800
|
+
are enforced by the installer.
|
|
801
|
+
|
|
784
802
|
## Development
|
|
785
803
|
|
|
786
804
|
```bash
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# Bridge Configuration
|
|
2
2
|
|
|
3
3
|
This module owns local runtime policy, exact-session capability controls, paths,
|
|
4
|
-
and device pairing serialization.
|
|
4
|
+
and device pairing serialization. `index.ts` is its public entry point.
|
|
5
5
|
|
|
6
6
|
Behavior is covered by `tests/config.test.ts`, `tests/capabilities.test.ts`, and
|
|
7
7
|
`tests/core.test.ts`.
|
|
8
8
|
|
|
9
9
|
License: this module is distributed under the GrantTap Commercial Source License
|
|
10
10
|
in the repository-root `LICENSE` file.
|
|
11
|
+
|
|
12
|
+
Runtime startup and persistent OAuth share `runtime/node-requirement.mjs`, which
|
|
13
|
+
reads the package Node minimum before loading TypeScript dependencies. Runtime
|
|
14
|
+
compatibility regressions live in `runtime/tests/node-requirement.test.ts`.
|
|
@@ -4,6 +4,7 @@ export * from "./access/pairing";
|
|
|
4
4
|
export { writePrivateFile } from "./access/write-private";
|
|
5
5
|
export * from "./runtime/paths";
|
|
6
6
|
export * from "./runtime";
|
|
7
|
+
export { isSupportedNode, minimumNodeVersion } from "./runtime/node-requirement.mjs";
|
|
7
8
|
export {
|
|
8
9
|
classifyAction,
|
|
9
10
|
isSafeReadonlyShell,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const manifest = JSON.parse(readFileSync(new URL("../../../../../package.json", import.meta.url), "utf8"));
|
|
4
|
+
export const minimumNodeVersion = manifest.engines.node.replace(/^>=/, "");
|
|
5
|
+
|
|
6
|
+
/** Shared by the native launcher and persistent-service installer before loading tsx. */
|
|
7
|
+
export function isSupportedNode(version) {
|
|
8
|
+
if (!/^v?\d+\.\d+\.\d+$/.test(version)) return false;
|
|
9
|
+
const actual = version.replace(/^v/, "").split(".").map(Number);
|
|
10
|
+
const minimum = minimumNodeVersion.split(".").map(Number);
|
|
11
|
+
for (let index = 0; index < 3; index += 1) {
|
|
12
|
+
if (actual[index] !== minimum[index]) return actual[index] > minimum[index];
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
@@ -8,12 +8,12 @@ export type EngineArtifact = {
|
|
|
8
8
|
|
|
9
9
|
/** Release hashes are part of the reviewed MCP, never supplied by a remote manifest. */
|
|
10
10
|
export const ENGINE_ARTIFACTS: readonly EngineArtifact[] = [
|
|
11
|
-
{ version: "0.1.
|
|
12
|
-
sha256: "
|
|
13
|
-
{ version: "0.1.
|
|
14
|
-
sha256: "
|
|
15
|
-
{ version: "0.1.
|
|
16
|
-
sha256: "
|
|
11
|
+
{ version: "0.1.2", platform: "darwin", arch: "arm64",
|
|
12
|
+
sha256: "fa23668f2f2cbb716a0ba66c5a6e027a25ed4e7e2dedf0c870edb32199045b97", bytes: 12101408 },
|
|
13
|
+
{ version: "0.1.2", platform: "darwin", arch: "x64",
|
|
14
|
+
sha256: "b3235a15936a77ec323d5ecbc54a0bc19994fe6e5a36044da964b68524b4c1dd", bytes: 13944984 },
|
|
15
|
+
{ version: "0.1.2", platform: "linux", arch: "x64",
|
|
16
|
+
sha256: "1a6b227289b69459a64cd13078044960cf83bb858e6be1b70b1b38c950b00cc0", bytes: 14753312 },
|
|
17
17
|
];
|
|
18
18
|
|
|
19
19
|
export function selectEngineArtifact(platform: string = process.platform, arch: string = process.arch): EngineArtifact | undefined {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Measured source checks
|
|
2
|
+
|
|
3
|
+
Public entry: `index.ts`. The Engine response parser admits optional, measured
|
|
4
|
+
function size, token-based complexity, nesting and resolved call fan-in. Absent
|
|
5
|
+
measurements stay absent. Malformed, negative and out-of-range values fail the
|
|
6
|
+
response instead of becoming healthy zeroes.
|
|
7
|
+
|
|
8
|
+
Integration tests: `runtime/repository-graph/tests/code-checks.test.ts`, relative
|
|
9
|
+
to the Engine module. The additive fields remain compatible with older Engines.
|
|
10
|
+
|
|
11
|
+
License: GrantTap Commercial Source License; see the repository LICENSE.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EngineProtocolError } from "../protocol-base";
|
|
2
|
+
|
|
3
|
+
/** Missing checks stay unknown; present checks must be bounded measurements. */
|
|
4
|
+
export function parseCodeChecks(value: unknown): void {
|
|
5
|
+
if (value == null) return;
|
|
6
|
+
const checks = value as Record<string, unknown>;
|
|
7
|
+
if (typeof checks !== "object" || Array.isArray(checks) || checks.measured !== true
|
|
8
|
+
|| !["source_lines", "cyclomatic", "max_loop_depth", "callers"].every(key =>
|
|
9
|
+
Number.isSafeInteger(checks[key]) && Number(checks[key]) >=
|
|
10
|
+
(["source_lines", "cyclomatic"].includes(key) ? 1 : 0)
|
|
11
|
+
&& Number(checks[key]) <= 0xffff_ffff)) {
|
|
12
|
+
throw new EngineProtocolError("invalid measured code checks");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -49,7 +49,9 @@ export type EngineRepositoryGraph = {
|
|
|
49
49
|
code_map?: {
|
|
50
50
|
files: Array<{ path: string; language: string | null; line_count: number | null;
|
|
51
51
|
symbols: Array<{ id: string; label: string; kind: string;
|
|
52
|
-
start_line: number; line_count: number
|
|
52
|
+
start_line: number; line_count: number;
|
|
53
|
+
metrics?: { measured: boolean; source_lines: number; cyclomatic: number;
|
|
54
|
+
max_loop_depth: number; callers: number } }> }>;
|
|
53
55
|
externals?: Array<{ id: string; label: string; kind: string }>;
|
|
54
56
|
roads: Array<{ source: string; target: string; relation: string }>;
|
|
55
57
|
total_files: number;
|
|
@@ -4,6 +4,7 @@ import { parsePolicyResult } from "../engine-policy-protocol";
|
|
|
4
4
|
import { parseImpactResult } from "../impact/engine-impact-protocol";
|
|
5
5
|
import { parseEngineIdentityResult } from "./identity-result-parser";
|
|
6
6
|
import { parseProjectCatalog } from "./project-catalog-parser";
|
|
7
|
+
import { parseCodeChecks } from "./code-checks";
|
|
7
8
|
import {
|
|
8
9
|
ENGINE_PROTOCOL_VERSION,
|
|
9
10
|
EngineProtocolError,
|
|
@@ -186,6 +187,7 @@ function parseCodeMap(value: unknown): void {
|
|
|
186
187
|
requireBoundedString(symbol.id, "code symbol id", 512);
|
|
187
188
|
requireBoundedString(symbol.label, "code symbol label", 160);
|
|
188
189
|
requireBoundedString(symbol.kind, "code symbol kind", 64);
|
|
190
|
+
parseCodeChecks(symbol.metrics);
|
|
189
191
|
if (!Number.isSafeInteger(symbol.start_line) || Number(symbol.start_line) < 1
|
|
190
192
|
|| !Number.isSafeInteger(symbol.line_count) || Number(symbol.line_count) < 1) invalidResult();
|
|
191
193
|
}
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
import { RepositoryGraphJobs } from "./repository-graph-jobs";
|
|
18
18
|
import { fitRepositoryGraph } from "./repository-graph/fit";
|
|
19
19
|
import { RepositoryGraphCache } from "./repository-graph/cache";
|
|
20
|
+
import { repositorySourceMap } from "./repository-graph/source-map";
|
|
20
21
|
|
|
21
22
|
export type LocalProjectBinding = {
|
|
22
23
|
summary: ProjectBindingSummary;
|
|
@@ -229,20 +230,7 @@ async function analyzeRepositoryGraph(
|
|
|
229
230
|
analysisId: graph.analysis_id ?? undefined,
|
|
230
231
|
analysisStatus: graph.analysis_status,
|
|
231
232
|
architectureHypotheses: graph.architecture_hypotheses,
|
|
232
|
-
codeMap: graph
|
|
233
|
-
files: graph.code_map.files.map((file) => ({
|
|
234
|
-
path: file.path, language: file.language, lineCount: file.line_count,
|
|
235
|
-
symbols: file.symbols.map((symbol) => ({
|
|
236
|
-
id: symbol.id, label: symbol.label, kind: symbol.kind,
|
|
237
|
-
startLine: symbol.start_line, lineCount: symbol.line_count,
|
|
238
|
-
})),
|
|
239
|
-
})),
|
|
240
|
-
externals: graph.code_map.externals ?? [],
|
|
241
|
-
roads: graph.code_map.roads,
|
|
242
|
-
totalFiles: graph.code_map.total_files,
|
|
243
|
-
totalExternals: graph.code_map.total_externals ?? 0,
|
|
244
|
-
truncated: graph.code_map.truncated,
|
|
245
|
-
} : undefined,
|
|
233
|
+
codeMap: repositorySourceMap(graph),
|
|
246
234
|
nodes: graph.nodes,
|
|
247
235
|
relations: graph.relations.map((edge) => ({
|
|
248
236
|
source: edge.source, target: edge.target, relation: edge.relation,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ProjectRepositoryGraph } from "../../../../../../packages/protocol/schema";
|
|
2
|
+
import { representativeSourcePaths, canReduceSymbols, sampleSymbols } from "./source-sampling";
|
|
2
3
|
|
|
3
4
|
type CodeMap = NonNullable<ProjectRepositoryGraph["codeMap"]>;
|
|
4
5
|
type Relation = ProjectRepositoryGraph["relations"][number];
|
|
@@ -21,7 +22,7 @@ export function fitRepositoryGraph(
|
|
|
21
22
|
if (map && map.roads.length > 16
|
|
22
23
|
&& Buffer.byteLength(JSON.stringify(map.roads), "utf8") > maxBytes / 3) {
|
|
23
24
|
bounded = { ...bounded, codeMap: trimRoads(map) };
|
|
24
|
-
} else if (map
|
|
25
|
+
} else if (map && canReduceSymbols(map)) {
|
|
25
26
|
bounded = { ...bounded, codeMap: trimSymbols(map) };
|
|
26
27
|
} else if (bounded.relations.length > 8) {
|
|
27
28
|
bounded = { ...bounded, relations: strongestRelations(bounded.relations,
|
|
@@ -75,27 +76,31 @@ function roadDegree(map: CodeMap): Map<string, number> {
|
|
|
75
76
|
return degree;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function rankedFiles(map: CodeMap): CodeMap["files"] {
|
|
79
|
+
function rankedFiles(map: CodeMap, preserveSource = false): CodeMap["files"] {
|
|
79
80
|
const degree = roadDegree(map);
|
|
81
|
+
const representatives = preserveSource ? representativeSourcePaths(map) : new Set<string>();
|
|
80
82
|
return [...map.files].sort((left, right) =>
|
|
81
|
-
(
|
|
83
|
+
Number(representatives.has(right.path)) - Number(representatives.has(left.path))
|
|
84
|
+
|| (preserveSource ? Number(right.symbols.length > 0) - Number(left.symbols.length > 0) : 0)
|
|
85
|
+
|| (degree.get(right.path) ?? 0) - (degree.get(left.path) ?? 0)
|
|
82
86
|
|| (right.lineCount ?? 0) - (left.lineCount ?? 0)
|
|
83
87
|
|| left.path.localeCompare(right.path));
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
function trimSymbols(map: CodeMap): CodeMap {
|
|
87
91
|
const ranked = rankedFiles(map).filter((file) => file.symbols.length > 0);
|
|
88
|
-
|
|
92
|
+
if (ranked.length <= 16) return { ...map, truncated: true,
|
|
93
|
+
files: map.files.map(file => ({ ...file, symbols: sampleSymbols(file.symbols) })) };
|
|
94
|
+
const candidates = [...representativeSourcePaths(map), ...ranked.map(file => file.path)];
|
|
95
|
+
const keep = new Set([...new Set(candidates)].slice(0, Math.max(16, Math.floor(ranked.length / 2))));
|
|
89
96
|
return { ...map, truncated: true, files: map.files.map((file) => {
|
|
90
97
|
if (!keep.has(file.path)) return { ...file, symbols: [] };
|
|
91
|
-
if (ranked.length === 1) return { ...file, symbols: file.symbols.slice(0,
|
|
92
|
-
Math.floor(file.symbols.length / 2)) };
|
|
93
98
|
return file;
|
|
94
99
|
}) };
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
function trimFiles(map: CodeMap): CodeMap {
|
|
98
|
-
const files = rankedFiles(map).slice(0, Math.ceil(map.files.length / 2))
|
|
103
|
+
const files = rankedFiles(map, true).slice(0, Math.ceil(map.files.length / 2))
|
|
99
104
|
.sort((left, right) => left.path.localeCompare(right.path));
|
|
100
105
|
const paths = new Set(files.map((file) => file.path));
|
|
101
106
|
for (const external of map.externals ?? []) paths.add(external.id);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EngineRepositoryGraph } from "../../protocol/engine-protocol";
|
|
2
|
+
import type { ProjectCodeMap } from "../../../../../../packages/protocol/messages/mesh/graph/code-map";
|
|
3
|
+
|
|
4
|
+
/** Preserve optional source checks without turning absent measurements into zero. */
|
|
5
|
+
export function repositorySourceMap(graph: EngineRepositoryGraph): ProjectCodeMap | undefined {
|
|
6
|
+
const map = graph.code_map;
|
|
7
|
+
if (!map) return undefined;
|
|
8
|
+
return {
|
|
9
|
+
files: map.files.map(file => ({ path: file.path, language: file.language, lineCount: file.line_count,
|
|
10
|
+
symbols: file.symbols.map(symbol => ({
|
|
11
|
+
id: symbol.id, label: symbol.label, kind: symbol.kind, startLine: symbol.start_line, lineCount: symbol.line_count,
|
|
12
|
+
...(symbol.metrics?.measured === true ? { metrics: {
|
|
13
|
+
measured: true as const, sourceLines: symbol.metrics.source_lines, cyclomatic: symbol.metrics.cyclomatic,
|
|
14
|
+
maxLoopDepth: symbol.metrics.max_loop_depth, callers: symbol.metrics.callers,
|
|
15
|
+
} } : {}),
|
|
16
|
+
})),
|
|
17
|
+
})),
|
|
18
|
+
externals: map.externals ?? [], roads: map.roads, totalFiles: map.total_files,
|
|
19
|
+
totalExternals: map.total_externals ?? 0, truncated: map.truncated,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ProjectRepositoryGraph } from "../../../../../../packages/protocol/schema";
|
|
2
|
+
|
|
3
|
+
type CodeMap = NonNullable<ProjectRepositoryGraph["codeMap"]>;
|
|
4
|
+
type Symbol = CodeMap["files"][number]["symbols"][number];
|
|
5
|
+
|
|
6
|
+
/** Reserve actual measured checks and a small sample of distinct source kinds. */
|
|
7
|
+
export function representativeSourcePaths(map: CodeMap): Set<string> {
|
|
8
|
+
const paths = new Set<string>();
|
|
9
|
+
const measured = map.files.find(file => file.symbols.some(symbol => symbol.metrics?.measured));
|
|
10
|
+
if (measured) paths.add(measured.path);
|
|
11
|
+
const kinds = new Set<string>();
|
|
12
|
+
for (const file of map.files) for (const symbol of file.symbols) {
|
|
13
|
+
if (kinds.has(symbol.kind) || paths.size >= 8) continue;
|
|
14
|
+
kinds.add(symbol.kind); paths.add(file.path);
|
|
15
|
+
}
|
|
16
|
+
return paths;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function canReduceSymbols(map: CodeMap): boolean {
|
|
20
|
+
return map.files.filter(file => file.symbols.length > 0).length > 16
|
|
21
|
+
|| map.files.some(file => file.symbols.length > 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sampleSymbols(symbols: Symbol[]): Symbol[] {
|
|
25
|
+
if (symbols.length <= 1) return symbols;
|
|
26
|
+
const keep = new Set<Symbol>(), kinds = new Set<string>();
|
|
27
|
+
const limit = Math.max(1, Math.floor(symbols.length / 2));
|
|
28
|
+
const measured = symbols.find(symbol => symbol.metrics?.measured);
|
|
29
|
+
if (measured) { keep.add(measured); kinds.add(measured.kind); }
|
|
30
|
+
for (const symbol of symbols) {
|
|
31
|
+
if (keep.size >= limit) break;
|
|
32
|
+
if (!kinds.has(symbol.kind)) { keep.add(symbol); kinds.add(symbol.kind); }
|
|
33
|
+
}
|
|
34
|
+
for (const symbol of symbols) {
|
|
35
|
+
if (keep.size >= limit) break;
|
|
36
|
+
keep.add(symbol);
|
|
37
|
+
}
|
|
38
|
+
return symbols.filter(symbol => keep.has(symbol));
|
|
39
|
+
}
|
|
@@ -209,13 +209,13 @@ export function installMonitorHelper(): InstallResult {
|
|
|
209
209
|
const node = resolveMonitorNodeBin();
|
|
210
210
|
return node && !isCursorHelperNode(node)
|
|
211
211
|
? installLinuxService("monitor", node, join(repoRoot, "bin", "granttap-mcp.mjs"), { forceReload: true })
|
|
212
|
-
: { status: "manual", detail: "A stable Node.js
|
|
212
|
+
: { status: "manual", detail: "A stable Node.js 22.13.0+ installation is required for Linux background sync." };
|
|
213
213
|
}
|
|
214
214
|
if (process.platform === "win32") {
|
|
215
215
|
const node = resolveMonitorNodeBin();
|
|
216
216
|
return node && !isCursorHelperNode(node)
|
|
217
217
|
? installWindowsTask("monitor", node, join(repoRoot, "bin", "granttap-mcp.mjs"), true)
|
|
218
|
-
: { status: "manual", detail: "A stable Node.js
|
|
218
|
+
: { status: "manual", detail: "A stable Node.js 22.13.0+ installation is required for Windows background sync." };
|
|
219
219
|
}
|
|
220
220
|
if (process.platform !== "darwin") {
|
|
221
221
|
return { status: "manual", detail: "background task sync currently requires macOS" };
|
|
@@ -11,8 +11,10 @@ remote claim. Changed remote identities and local paths to different checkouts
|
|
|
11
11
|
remain rejected. Neither Task identity nor Project membership is rewritten.
|
|
12
12
|
|
|
13
13
|
After admission, `graphAnalysisBindings` selects one graph per endpoint and real
|
|
14
|
-
checkout path, preferring
|
|
15
|
-
|
|
14
|
+
checkout path, preferring the currently verified canonical binding over retired aliases.
|
|
15
|
+
Observed Git revisions replace old catalog observations in admission and report
|
|
16
|
+
provenance. A subfolder with a remembered parent commit is not a checkout. Each
|
|
17
|
+
computer analyzes only its own endpoint bindings. Failed bindings remain visible with their original error. Both the Mac
|
|
16
18
|
projection and phone refresh use this rule. The graph contains actual Engine
|
|
17
19
|
analysis; unavailable analysis is never synthesized as a successful graph.
|
|
18
20
|
|
|
@@ -6,6 +6,8 @@ import { publishedCatalog } from "../../../session-catalog";
|
|
|
6
6
|
import { meshSnapshot, meshSnapshots } from "..";
|
|
7
7
|
import { refreshLocalGraphBindings } from "./binding-sync";
|
|
8
8
|
import { graphAnalysisBindings } from "./checkout-identity";
|
|
9
|
+
import { computerId } from "../../identity/computer";
|
|
10
|
+
import { repositoryDetails } from "../../repository-details";
|
|
9
11
|
|
|
10
12
|
/** A person's explicit Graph refresh bypasses background job backoff. */
|
|
11
13
|
type GraphAnalysisDependencies = {
|
|
@@ -16,6 +18,8 @@ type GraphAnalysisDependencies = {
|
|
|
16
18
|
backbone: typeof projectBackbone;
|
|
17
19
|
graphs: typeof projectRepositoryGraphs;
|
|
18
20
|
send: (client: RelayClient, snapshot: MeshSnapshot) => Promise<void>;
|
|
21
|
+
endpoint?: () => string;
|
|
22
|
+
details?: (snapshot: MeshSnapshot) => ReturnType<typeof repositoryDetails>;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
const graphAnalysisDependencies: GraphAnalysisDependencies = {
|
|
@@ -24,6 +28,8 @@ const graphAnalysisDependencies: GraphAnalysisDependencies = {
|
|
|
24
28
|
sync: refreshLocalGraphBindings,
|
|
25
29
|
backbone: projectBackbone,
|
|
26
30
|
graphs: projectRepositoryGraphs,
|
|
31
|
+
endpoint: computerId,
|
|
32
|
+
details: snapshot => repositoryDetails(snapshot, computerId()),
|
|
27
33
|
send: (client, snapshot) => sendMeshPayload(client, snapshot, "phone", {
|
|
28
34
|
ttlMs: 60_000, reliable: true,
|
|
29
35
|
}),
|
|
@@ -47,10 +53,13 @@ export async function analyzeProjectGraphNow(
|
|
|
47
53
|
if (!current) return false;
|
|
48
54
|
const failures = dependencies.sync ? await dependencies.sync(current) : [];
|
|
49
55
|
const rejected = new Set(failures.map(report => report.repositoryId));
|
|
50
|
-
const
|
|
56
|
+
const bindings = graphAnalysisBindings((current.bindings ?? [])
|
|
57
|
+
.filter(binding => !rejected.has(binding.repositoryId)), dependencies.endpoint?.());
|
|
58
|
+
if (dependencies.endpoint && !bindings.length && !failures.length && current.bindings?.length) return false;
|
|
59
|
+
const [backbone, repositoryGraphs, details] = await Promise.all([
|
|
51
60
|
dependencies.backbone(projectId),
|
|
52
|
-
dependencies.graphs(projectId,
|
|
53
|
-
|
|
61
|
+
dependencies.graphs(projectId, bindings, { background: false }),
|
|
62
|
+
dependencies.details?.(current),
|
|
54
63
|
]);
|
|
55
64
|
const observed = [...repositoryGraphs, ...failures];
|
|
56
65
|
const reports = observed.length > 0 ? observed : [{
|
|
@@ -63,6 +72,7 @@ export async function analyzeProjectGraphNow(
|
|
|
63
72
|
for (const id of receipts) {
|
|
64
73
|
await dependencies.send(client, {
|
|
65
74
|
...current, backbone, repositoryGraphs: reports, generatedAt: Date.now(),
|
|
75
|
+
...(details ? { repositoryDetails: details } : {}),
|
|
66
76
|
...(id ? { graphAnalysisRequestId: id } : {}),
|
|
67
77
|
});
|
|
68
78
|
}
|
|
@@ -44,7 +44,7 @@ export async function refreshLocalGraphBindings(
|
|
|
44
44
|
}
|
|
45
45
|
try {
|
|
46
46
|
const admitted = await dependencies.sync(snapshot.project, {
|
|
47
|
-
summary: binding,
|
|
47
|
+
summary: { ...binding, revision: facts.revision ?? binding.revision },
|
|
48
48
|
localRoot: facts.root,
|
|
49
49
|
// A local binding is scoped by its verified path. Do not relabel it as
|
|
50
50
|
// a remote repository or move it into that repository's Project scope.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import type { ProjectBindingSummary } from "../../../../../../packages/protocol/schema";
|
|
4
|
+
import { inspectRepository, type RepositoryFacts } from "../../catalog";
|
|
4
5
|
|
|
5
6
|
/** Local identities name an exact directory, even after that directory gains an origin. */
|
|
6
7
|
export function matchesLocalCheckout(repositoryId: string, root: string): boolean {
|
|
@@ -12,17 +13,39 @@ export function matchesLocalCheckout(repositoryId: string, root: string): boolea
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/** Once admitted, one checkout contributes one graph to this Mesh. */
|
|
15
|
-
export function graphAnalysisBindings(bindings: ProjectBindingSummary[]): ProjectBindingSummary[] {
|
|
16
|
+
export function graphAnalysisBindings(bindings: ProjectBindingSummary[], endpointId?: string): ProjectBindingSummary[] {
|
|
16
17
|
const selected = new Map<string, ProjectBindingSummary>();
|
|
18
|
+
const observations = new Map<string, RepositoryFacts | undefined>();
|
|
19
|
+
const nonRepositories = new Set<string>();
|
|
20
|
+
const ranks = new Map<string, number>();
|
|
17
21
|
for (const binding of bindings) {
|
|
22
|
+
if (endpointId && binding.endpointId !== endpointId) continue;
|
|
18
23
|
let path: string | undefined;
|
|
19
24
|
if (binding.available && binding.localPathHint) {
|
|
20
25
|
try { path = realpathSync(binding.localPathHint); } catch { /* Keep an unavailable checkout distinct. */ }
|
|
21
26
|
}
|
|
27
|
+
if (path && !observations.has(path)) {
|
|
28
|
+
let facts: RepositoryFacts | undefined;
|
|
29
|
+
try {
|
|
30
|
+
const observed = inspectRepository(path);
|
|
31
|
+
if (observed.worktree && realpathSync(observed.root) === path) facts = observed;
|
|
32
|
+
else nonRepositories.add(path);
|
|
33
|
+
} catch { /* Unavailable roots retain their own identity. */ }
|
|
34
|
+
observations.set(path, facts);
|
|
35
|
+
}
|
|
36
|
+
// A remembered commit from the parent does not make a subfolder a checkout.
|
|
37
|
+
if (path && nonRepositories.has(path)) continue;
|
|
38
|
+
const facts = path ? observations.get(path) : undefined;
|
|
39
|
+
const canonical = facts?.canonicalRepositoryId === binding.repositoryId;
|
|
40
|
+
const scoped = facts && matchesLocalCheckout(binding.repositoryId, facts.root);
|
|
41
|
+
const rank = canonical ? 3 : scoped ? 2 : binding.repositoryId.startsWith("local:") ? 0 : 1;
|
|
42
|
+
const candidate = (canonical || scoped) && facts?.revision
|
|
43
|
+
? { ...binding, revision: facts.revision } : binding;
|
|
22
44
|
const key = JSON.stringify([binding.endpointId, path ?? binding.repositoryId]);
|
|
23
45
|
const previous = selected.get(key);
|
|
24
|
-
if (!previous || (
|
|
25
|
-
selected.set(key,
|
|
46
|
+
if (!previous || rank > (ranks.get(key) ?? 0)) {
|
|
47
|
+
selected.set(key, candidate);
|
|
48
|
+
ranks.set(key, rank);
|
|
26
49
|
}
|
|
27
50
|
}
|
|
28
51
|
return [...selected.values()];
|
|
@@ -18,7 +18,7 @@ export async function enrichMeshSnapshot(
|
|
|
18
18
|
const identityFailures = await refreshLocalGraphBindings(snapshot);
|
|
19
19
|
const excludedRepositories = new Set(identityFailures.map((report) => report.repositoryId));
|
|
20
20
|
const bindings = graphAnalysisBindings((snapshot.bindings ?? [])
|
|
21
|
-
.filter((binding) => !excludedRepositories.has(binding.repositoryId)));
|
|
21
|
+
.filter((binding) => !excludedRepositories.has(binding.repositoryId)), computerId());
|
|
22
22
|
await waitForProjectBindingSync(snapshot.projectId);
|
|
23
23
|
queueProjectKnowledgeSync({
|
|
24
24
|
projectId: snapshot.projectId,
|
|
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileS
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { isCursorHelperNode, resolveMonitorNodeBin, type InstallResult } from "../../../bridge/src/install";
|
|
7
|
-
import { configDir, normalizeRelayUrl, loadRuntimeConfig, verifiableEngine } from "../../../bridge/src/config";
|
|
7
|
+
import { configDir, normalizeRelayUrl, loadRuntimeConfig, verifiableEngine, isSupportedNode, minimumNodeVersion } from "../../../bridge/src/config";
|
|
8
8
|
import { installWindowsTask } from "../../../bridge/src/install/windows-service";
|
|
9
9
|
import { installLinuxService } from "../../../bridge/src/install/linux-service";
|
|
10
10
|
import { configuredCursorHttpMcpUrl } from "../cursor-config";
|
|
@@ -65,9 +65,8 @@ function validateInstallation(nodeBin: string, executable: string): InstallResul
|
|
|
65
65
|
return { status: "manual", detail: "Authorization requires a stable granttap-mcp install; npm's temporary _npx cache is not durable." };
|
|
66
66
|
}
|
|
67
67
|
const version = spawnSync(nodeBin, ["--version"], { encoding: "utf8" });
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
? { status: "manual", detail: `Persistent OAuth requires Node 20+ (${nodeBin}).` }
|
|
68
|
+
return version.status !== 0 || !isSupportedNode(version.stdout.trim())
|
|
69
|
+
? { status: "manual", detail: `Persistent OAuth requires Node ${minimumNodeVersion}+ (${nodeBin}).` }
|
|
71
70
|
: undefined;
|
|
72
71
|
}
|
|
73
72
|
|
package/bin/granttap-mcp.mjs
CHANGED
|
@@ -5,6 +5,13 @@ import { createRequire } from "node:module";
|
|
|
5
5
|
import { delimiter, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
|
+
import { isSupportedNode, minimumNodeVersion } from "../apps/bridge/src/config/runtime/node-requirement.mjs";
|
|
9
|
+
|
|
10
|
+
if (!isSupportedNode(process.versions.node)) {
|
|
11
|
+
process.stderr.write(`[granttap-mcp] requires Node.js ${minimumNodeVersion} or newer; found ${process.versions.node}. Install the latest supported Node.js LTS release.\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
16
|
const [command = "mcp", ...commandArgs] = process.argv.slice(2);
|
|
10
17
|
const argument = commandArgs[0];
|
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.84
|
|
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.84\";\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.84";
|
|
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;
|
|
@@ -31,7 +31,7 @@ function launch() {
|
|
|
31
31
|
const npx = first(roots.map((root) =>
|
|
32
32
|
join(root, "node_modules", "npm", "bin", "npx-cli.js")));
|
|
33
33
|
if (npx) return { command: process.execPath, args: [npx, "-y", PACKAGE] };
|
|
34
|
-
throw new Error("Node.js/npm
|
|
34
|
+
throw new Error("Node.js/npm 22.13.0+ is required on this Windows device. Install npm and retry GrantTap.");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
let target;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granttap-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.84",
|
|
4
4
|
"description": "Commercial computer runtime connecting local coding agents to GrantTap on Mac, iPhone, iPad and Apple Watch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"typescript": "^5.7.2"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
58
|
+
"node": ">=22.13.0"
|
|
59
59
|
},
|
|
60
60
|
"repository": {
|
|
61
61
|
"type": "git",
|
|
@@ -14,6 +14,12 @@ export const ProjectCodeMap = z.object({
|
|
|
14
14
|
kind: z.string().trim().min(1).max(64),
|
|
15
15
|
startLine: z.number().int().positive(),
|
|
16
16
|
lineCount: z.number().int().positive(),
|
|
17
|
+
metrics: z.object({
|
|
18
|
+
measured: z.literal(true), sourceLines: z.number().int().positive().max(0xffff_ffff),
|
|
19
|
+
cyclomatic: z.number().int().positive().max(0xffff_ffff),
|
|
20
|
+
maxLoopDepth: z.number().int().nonnegative().max(0xffff_ffff),
|
|
21
|
+
callers: z.number().int().nonnegative().max(0xffff_ffff),
|
|
22
|
+
}).strict().optional(),
|
|
17
23
|
}).strict()).max(72),
|
|
18
24
|
}).strict()).max(650),
|
|
19
25
|
externals: z.array(z.object({
|