granttap-mcp 0.8.119 → 0.8.121
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/codex-questions/README.md +5 -0
- package/apps/bridge/src/codex-questions/index.ts +17 -5
- package/apps/bridge/src/engine/runtime/engine-projects.ts +12 -3
- package/apps/bridge/src/engine/runtime/repository-graph/README.md +6 -0
- package/apps/bridge/src/engine/runtime/repository-graph/cache.ts +8 -3
- package/apps/bridge/src/engine/runtime/repository-graph/source-state/README.md +24 -0
- package/apps/bridge/src/engine/runtime/repository-graph/source-state/index.ts +73 -0
- package/apps/bridge/src/engine/runtime/repository-graph-jobs.ts +27 -8
- package/apps/bridge/src/mesh/catalog/index.ts +1 -1
- package/apps/bridge/src/mesh/catalog/repository-inspection.ts +12 -5
- package/apps/bridge/src/mesh/repository-details/README.md +9 -0
- package/apps/bridge/src/mesh/repository-details/git.ts +6 -0
- package/apps/bridge/src/mesh/repository-details/history.ts +47 -0
- package/apps/bridge/src/mesh/repository-details/index.ts +20 -35
- package/apps/bridge/src/mesh/runtime/graph/README.md +4 -0
- package/apps/bridge/src/mesh/runtime/graph/checkout-identity.ts +21 -3
- package/apps/bridge/src/mesh/runtime/snapshot/enriched.ts +9 -5
- 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
|
@@ -38,3 +38,8 @@ Recovery checks the private request's owning room, restores a discovered source
|
|
|
38
38
|
inside the native Codex sessions root, and sends that exact question or its terminal
|
|
39
39
|
state through the priority device channel. It does not require an admitted catalog,
|
|
40
40
|
start a provider turn, send another wake notification, or schedule a full scan.
|
|
41
|
+
|
|
42
|
+
Question publication reads the private record collection once per batch, then
|
|
43
|
+
validates each matching question freshly before sending. Targeted notification
|
|
44
|
+
replay reads the requested identity directly. This keeps multiple chats from
|
|
45
|
+
repeatedly parsing the same collection.
|
|
@@ -13,7 +13,12 @@ const RETENTION_MS = 24 * 60 * 60_000;
|
|
|
13
13
|
/** Phone questions travel as device-encrypted control events, independently of task-key grants. */
|
|
14
14
|
export async function publishCodexQuestions(client: RelayClient, sessions: SessionInfo[],
|
|
15
15
|
options: { replay?: boolean; peerPublicKey?: string; requestId?: string } = {}): Promise<void> {
|
|
16
|
-
|
|
16
|
+
const nativeSessions = sessions.filter(s => s.agent === 'codex');
|
|
17
|
+
if (!nativeSessions.length) return;
|
|
18
|
+
const initial = options.requestId ? [readQuestion(options.requestId)].filter((record): record is QuestionRecord => !!record)
|
|
19
|
+
: storedQuestions();
|
|
20
|
+
const batch = new Map(initial.map(record => [record.requestId, record]));
|
|
21
|
+
for (const session of nativeSessions) {
|
|
17
22
|
const path = nativeTranscriptSource(session);
|
|
18
23
|
if (!path) continue;
|
|
19
24
|
let observed;
|
|
@@ -22,13 +27,20 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
|
|
|
22
27
|
for (const question of observed.questions) {
|
|
23
28
|
if (Date.now() - question.createdAt > RETENTION_MS) continue;
|
|
24
29
|
const existing = readQuestion(question.requestId);
|
|
25
|
-
if (!existing)
|
|
26
|
-
|
|
30
|
+
if (!existing) {
|
|
31
|
+
const record = { ...question, room: client.room, sourcePath: path };
|
|
32
|
+
saveQuestion(record); batch.set(record.requestId, record);
|
|
33
|
+
} else {
|
|
34
|
+
if (existing.room === client.room && existing.sourcePath !== path) saveQuestion({ ...existing, sourcePath: path });
|
|
35
|
+
batch.set(existing.requestId, existing);
|
|
36
|
+
}
|
|
27
37
|
}
|
|
28
|
-
const records =
|
|
38
|
+
const records = [...batch.values()].filter(q => q.room === client.room && q.sessionId === session.sessionId
|
|
29
39
|
&& (!options.requestId || q.requestId === options.requestId))
|
|
30
40
|
.sort((a, b) => a.createdAt - b.createdAt || a.index - b.index);
|
|
31
|
-
for (const
|
|
41
|
+
for (const candidate of records) {
|
|
42
|
+
const record = readQuestion(candidate.requestId);
|
|
43
|
+
if (!record || record.room !== client.room || record.sessionId !== session.sessionId) continue;
|
|
32
44
|
const expired = Date.now() - record.createdAt > RETENTION_MS;
|
|
33
45
|
if (observed.answered.has(record.requestId) && !record.answered) {
|
|
34
46
|
record.answered = true;
|
|
@@ -18,6 +18,7 @@ import { RepositoryGraphJobs } from "./repository-graph-jobs";
|
|
|
18
18
|
import { fitRepositoryGraph } from "./repository-graph/fit";
|
|
19
19
|
import { RepositoryGraphCache } from "./repository-graph/cache";
|
|
20
20
|
import { repositorySourceMap } from "./repository-graph/source-map";
|
|
21
|
+
import { repositorySourceFingerprint } from "./repository-graph/source-state";
|
|
21
22
|
|
|
22
23
|
export type LocalProjectBinding = {
|
|
23
24
|
summary: ProjectBindingSummary;
|
|
@@ -193,15 +194,23 @@ export async function projectRepositoryGraphs(
|
|
|
193
194
|
if (!engineFeatureEnabled(options.env ?? process.env)) return [];
|
|
194
195
|
const client = options.client;
|
|
195
196
|
const repositories = [...new Map(bindings.map((item) => [item.repositoryId, item])).values()].slice(0, 64);
|
|
197
|
+
const observations = await Promise.all(repositories.map(async binding => {
|
|
198
|
+
const fingerprint = binding.localPathHint
|
|
199
|
+
? await repositorySourceFingerprint(binding.localPathHint, binding.revision, { fresh: !options.background })
|
|
200
|
+
: undefined;
|
|
201
|
+
return fingerprint ? { fingerprint, reusable: (graph: ProjectRepositoryGraph) =>
|
|
202
|
+
['COMPLETE', 'INCOMPLETE'].includes(graph.analysisStatus ?? '') } : undefined;
|
|
203
|
+
}));
|
|
196
204
|
const graphs = options.background
|
|
197
|
-
? repositories.map((binding) => repositoryGraphJobs.read(
|
|
205
|
+
? repositories.map((binding, index) => repositoryGraphJobs.read(
|
|
198
206
|
JSON.stringify([projectId, binding.repositoryId, binding.revision]),
|
|
199
207
|
() => analyzeRepositoryGraph(client, projectId, binding.repositoryId, 120_000, binding.revision),
|
|
200
|
-
options.priority ?? 0,
|
|
208
|
+
options.priority ?? 0, false, observations[index],
|
|
201
209
|
))
|
|
202
|
-
: await Promise.all(repositories.map((binding) => repositoryGraphJobs.refresh(
|
|
210
|
+
: await Promise.all(repositories.map((binding, index) => repositoryGraphJobs.refresh(
|
|
203
211
|
JSON.stringify([projectId, binding.repositoryId, binding.revision]),
|
|
204
212
|
() => analyzeRepositoryGraph(client, projectId, binding.repositoryId, 120_000, binding.revision),
|
|
213
|
+
Number.MAX_SAFE_INTEGER, observations[index],
|
|
205
214
|
)));
|
|
206
215
|
return boundRepositoryGraphs(graphs.filter((item): item is ProjectRepositoryGraph => item != null));
|
|
207
216
|
}
|
|
@@ -18,3 +18,9 @@ under the repository's `tests/engine` harness.
|
|
|
18
18
|
|
|
19
19
|
License: this module is distributed under the GrantTap Commercial Source License
|
|
20
20
|
in the repository-root `LICENSE` file.
|
|
21
|
+
|
|
22
|
+
Successful background reports retain an internal observation of Git-visible
|
|
23
|
+
source state. Unchanged state reuses the report across time and MCP restart;
|
|
24
|
+
changed state retains the existing analysis cooldown. Explicit refresh forces
|
|
25
|
+
analysis, and temporary failures keep their retry behavior. Source observation
|
|
26
|
+
is implemented by `source-state/`; its tests cover dirty edits and path changes.
|
|
@@ -9,6 +9,7 @@ export class RepositoryGraphCache {
|
|
|
9
9
|
read(key: string): {
|
|
10
10
|
value: ProjectRepositoryGraph;
|
|
11
11
|
savedAt: number;
|
|
12
|
+
sourceFingerprint?: string;
|
|
12
13
|
} | undefined {
|
|
13
14
|
try {
|
|
14
15
|
const path = this.path(key);
|
|
@@ -19,18 +20,22 @@ export class RepositoryGraphCache {
|
|
|
19
20
|
if (record.key !== key || !Number.isFinite(record.savedAt) || !graph.success
|
|
20
21
|
|| !['COMPLETE', 'INCOMPLETE'].includes(graph.data.analysisStatus ?? ''))
|
|
21
22
|
return undefined;
|
|
22
|
-
|
|
23
|
+
const sourceFingerprint = typeof record.sourceFingerprint === 'string'
|
|
24
|
+
&& /^[a-f0-9]{64}$/.test(record.sourceFingerprint) ? record.sourceFingerprint : undefined;
|
|
25
|
+
return { value: graph.data, savedAt: record.savedAt,
|
|
26
|
+
...(sourceFingerprint ? { sourceFingerprint } : {}) };
|
|
23
27
|
}
|
|
24
28
|
catch {
|
|
25
29
|
return undefined;
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
|
-
write(key: string, value: ProjectRepositoryGraph): void {
|
|
32
|
+
write(key: string, value: ProjectRepositoryGraph, sourceFingerprint?: string): void {
|
|
29
33
|
if (!['COMPLETE', 'INCOMPLETE'].includes(value.analysisStatus ?? ''))
|
|
30
34
|
return;
|
|
31
35
|
const bounded = fitRepositoryGraph(value, 192 * 1024);
|
|
32
36
|
if (!bounded) return;
|
|
33
|
-
const json = JSON.stringify({ key, value: bounded, savedAt: this.now()
|
|
37
|
+
const json = JSON.stringify({ key, value: bounded, savedAt: this.now(),
|
|
38
|
+
...(sourceFingerprint && /^[a-f0-9]{64}$/.test(sourceFingerprint) ? { sourceFingerprint } : {}) });
|
|
34
39
|
if (Buffer.byteLength(json) > 256 * 1024)
|
|
35
40
|
return;
|
|
36
41
|
try {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Repository source observation
|
|
2
|
+
|
|
3
|
+
Public entry: `index.ts`. This private machine-side observation identifies the
|
|
4
|
+
bound checkout, Git revision, staged and unstaged path status, and metadata of
|
|
5
|
+
changed or untracked files. It includes repeated edits with the same Git status,
|
|
6
|
+
renames, deletion and Unicode paths. Ignored build output does not invalidate it.
|
|
7
|
+
It reads metadata without parsing source or executing a build.
|
|
8
|
+
|
|
9
|
+
Background observations share in-flight work and a five-second observation
|
|
10
|
+
window. Explicit refresh bypasses it. Output, path count, filesystem concurrency
|
|
11
|
+
and retained observations are bounded. Unreadable or invalid state remains
|
|
12
|
+
unknown; it never becomes proof of unchanged source.
|
|
13
|
+
|
|
14
|
+
Repository graph jobs retain a successful report while this observation is
|
|
15
|
+
unchanged, including across MCP restart. A changed observation retains the
|
|
16
|
+
existing analysis cooldown; explicit builds still force analysis. Failed or
|
|
17
|
+
legacy reports retain normal retry behavior. No provider policy, grants,
|
|
18
|
+
repository files or user projects are changed.
|
|
19
|
+
|
|
20
|
+
Behavior tests live in `tests/` and `../tests/unchanged-source.test.ts`.
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
24
|
+
GrantTap Commercial Source License. See [LICENSE](../../../../../../../LICENSE).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { isAbsolute, resolve, sep } from 'node:path';
|
|
4
|
+
import { readRepositoryGit } from '../../../../mesh/catalog';
|
|
5
|
+
|
|
6
|
+
const cached = new Map<string, { at: number; value?: string }>();
|
|
7
|
+
const pending = new Map<string, Promise<string | undefined>>();
|
|
8
|
+
|
|
9
|
+
function changedPaths(status: string): string[] | undefined {
|
|
10
|
+
const fields = status.split('\0');
|
|
11
|
+
const paths: string[] = [];
|
|
12
|
+
for (let index = 0; index < fields.length; index++) {
|
|
13
|
+
const field = fields[index]!;
|
|
14
|
+
if (!field) continue;
|
|
15
|
+
if (field.length < 4 || field[2] !== ' ') return undefined;
|
|
16
|
+
paths.push(field.slice(3));
|
|
17
|
+
if (/[RC]/.test(field.slice(0, 2))) {
|
|
18
|
+
const previous = fields[++index];
|
|
19
|
+
if (!previous) return undefined;
|
|
20
|
+
paths.push(previous);
|
|
21
|
+
}
|
|
22
|
+
if (paths.length > 4096) return undefined;
|
|
23
|
+
}
|
|
24
|
+
return [...new Set(paths)].sort();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function capture(root: string, revision?: string): Promise<string | undefined> {
|
|
28
|
+
try {
|
|
29
|
+
const directory = await realpath(root);
|
|
30
|
+
const status = await readRepositoryGit(directory,
|
|
31
|
+
['status', '--porcelain=v1', '--untracked-files=all', '-z'], { timeoutMs: 5_000, maxBuffer: 4 * 1024 * 1024 });
|
|
32
|
+
if (status === undefined) return undefined;
|
|
33
|
+
const paths = changedPaths(status);
|
|
34
|
+
if (!paths) return undefined;
|
|
35
|
+
const hash = createHash('sha256').update(JSON.stringify([directory, revision])).update(status);
|
|
36
|
+
for (let start = 0; start < paths.length; start += 16) {
|
|
37
|
+
const rows = await Promise.all(paths.slice(start, start + 16).map(async path => {
|
|
38
|
+
const absolute = resolve(directory, path);
|
|
39
|
+
if (isAbsolute(path) || !absolute.startsWith(directory + sep)) throw new Error('Invalid repository path');
|
|
40
|
+
try {
|
|
41
|
+
const stat = await lstat(absolute, { bigint: true });
|
|
42
|
+
return [path, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs, stat.mode].map(String);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [path, 'missing'];
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
hash.update(JSON.stringify(rows));
|
|
49
|
+
}
|
|
50
|
+
return hash.digest('hex');
|
|
51
|
+
} catch { return undefined; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Observe committed and uncommitted source without parsing it or executing a build. */
|
|
55
|
+
export function repositorySourceFingerprint(root: string, revision?: string,
|
|
56
|
+
options: { fresh?: boolean } = {}): Promise<string | undefined> {
|
|
57
|
+
const key = JSON.stringify([root, revision]), previous = cached.get(key), now = Date.now();
|
|
58
|
+
if (!options.fresh && previous && now >= previous.at && now - previous.at < 5_000) {
|
|
59
|
+
return Promise.resolve(previous.value);
|
|
60
|
+
}
|
|
61
|
+
const active = pending.get(key);
|
|
62
|
+
if (!options.fresh && active) return active;
|
|
63
|
+
const next = capture(root, revision).then(value => {
|
|
64
|
+
if (pending.get(key) === next) {
|
|
65
|
+
cached.delete(key);
|
|
66
|
+
if (cached.size >= 128) cached.delete(cached.keys().next().value!);
|
|
67
|
+
cached.set(key, { value, at: Date.now() });
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}).finally(() => { if (pending.get(key) === next) pending.delete(key); });
|
|
71
|
+
pending.set(key, next);
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
type SourceObservation<T> = { fingerprint: string; reusable?: (value: T) => boolean };
|
|
1
2
|
/** Serializes expensive repository analysis without blocking Mesh snapshots. */
|
|
2
3
|
export class RepositoryGraphJobs<T> {
|
|
3
4
|
private readonly entries = new Map<string, {
|
|
@@ -5,6 +6,7 @@ export class RepositoryGraphJobs<T> {
|
|
|
5
6
|
retryAt: number;
|
|
6
7
|
pending: boolean;
|
|
7
8
|
listeners: Array<(value: T | undefined) => void>;
|
|
9
|
+
sourceFingerprint?: string;
|
|
8
10
|
}>();
|
|
9
11
|
private readonly queue: Array<{
|
|
10
12
|
key: string; run: () => Promise<T | undefined>; priority: number;
|
|
@@ -16,34 +18,50 @@ export class RepositoryGraphJobs<T> {
|
|
|
16
18
|
private readonly successMs = 120_000,
|
|
17
19
|
private readonly failureMs = 300_000,
|
|
18
20
|
private readonly capacity = 32,
|
|
19
|
-
private readonly storage?: {read(key:string):{value:T;savedAt:number}|undefined;
|
|
21
|
+
private readonly storage?: {read(key:string):{value:T;savedAt:number;sourceFingerprint?:string}|undefined;
|
|
22
|
+
write(key:string,value:T,sourceFingerprint?:string):void},
|
|
20
23
|
) {}
|
|
21
24
|
|
|
22
|
-
read(key: string, run: () => Promise<T | undefined>, priority = 0, force = false
|
|
25
|
+
read(key: string, run: () => Promise<T | undefined>, priority = 0, force = false,
|
|
26
|
+
source?: SourceObservation<T>): T | undefined {
|
|
23
27
|
if (!this.entries.has(key)) {
|
|
24
28
|
const saved = this.storage?.read(key);
|
|
25
29
|
if (saved) this.entries.set(key, {value:saved.value,retryAt:saved.savedAt+this.successMs,
|
|
26
|
-
pending:false,listeners:[]});
|
|
30
|
+
pending:false,listeners:[],sourceFingerprint:saved.sourceFingerprint});
|
|
27
31
|
}
|
|
28
32
|
const previous = this.entries.get(key);
|
|
29
33
|
if (previous?.pending) return previous.value;
|
|
30
34
|
if (previous && !force && this.now() < previous.retryAt) return previous.value;
|
|
35
|
+
if (!force && source && previous?.value !== undefined
|
|
36
|
+
&& previous.sourceFingerprint === source.fingerprint && source.reusable?.(previous.value) !== false) {
|
|
37
|
+
return previous.value;
|
|
38
|
+
}
|
|
31
39
|
if (!this.admit(priority)) return undefined;
|
|
32
40
|
if (!previous && this.entries.size >= this.capacity * 2) {
|
|
33
41
|
const oldest = [...this.entries].find(([, entry]) => !entry.pending)?.[0];
|
|
34
42
|
if (!oldest) return undefined;
|
|
35
43
|
this.entries.delete(oldest);
|
|
36
44
|
}
|
|
37
|
-
this.entries.set(key, { value: previous?.value, retryAt: 0, pending: true, listeners: []
|
|
45
|
+
this.entries.set(key, { value: previous?.value, retryAt: 0, pending: true, listeners: [],
|
|
46
|
+
sourceFingerprint: source?.fingerprint });
|
|
47
|
+
const admitted = this.entries.get(key);
|
|
38
48
|
const before = this.queue.findIndex((job) => job.priority < priority);
|
|
39
|
-
|
|
49
|
+
const observedRun = async () => {
|
|
50
|
+
const value = await run();
|
|
51
|
+
if (value !== undefined && source?.reusable?.(value) === false && this.entries.get(key) === admitted) {
|
|
52
|
+
admitted!.sourceFingerprint = undefined;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
this.queue.splice(before < 0 ? this.queue.length : before, 0, { key, run: observedRun, priority });
|
|
40
57
|
void this.drain();
|
|
41
58
|
return previous?.value;
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
/** A requested build joins the same revision's automatic job and raises its queue priority. */
|
|
45
|
-
refresh(key: string, run: () => Promise<T | undefined>, priority = Number.MAX_SAFE_INTEGER
|
|
46
|
-
|
|
62
|
+
refresh(key: string, run: () => Promise<T | undefined>, priority = Number.MAX_SAFE_INTEGER,
|
|
63
|
+
source?: SourceObservation<T>): Promise<T | undefined> {
|
|
64
|
+
if (!this.entries.get(key)?.pending) this.read(key, run, priority, true, source);
|
|
47
65
|
const entry = this.entries.get(key);
|
|
48
66
|
if (!entry?.pending) return Promise.resolve(undefined);
|
|
49
67
|
const index = this.queue.findIndex(job => job.key === key);
|
|
@@ -103,12 +121,13 @@ export class RepositoryGraphJobs<T> {
|
|
|
103
121
|
let value: T | undefined;
|
|
104
122
|
try { value = await job.run(); } catch { /* Retry after backoff. */ }
|
|
105
123
|
if (this.entries.get(job.key) !== entry) continue;
|
|
106
|
-
if (value !== undefined) this.storage?.write(job.key, value);
|
|
124
|
+
if (value !== undefined) this.storage?.write(job.key, value, entry?.sourceFingerprint);
|
|
107
125
|
this.entries.set(job.key, {
|
|
108
126
|
value,
|
|
109
127
|
retryAt: this.now() + (value === undefined ? this.failureMs : this.successMs),
|
|
110
128
|
pending: false,
|
|
111
129
|
listeners: [],
|
|
130
|
+
sourceFingerprint: entry?.sourceFingerprint,
|
|
112
131
|
});
|
|
113
132
|
for (const listener of entry?.listeners ?? []) listener(value);
|
|
114
133
|
}
|
|
@@ -20,7 +20,7 @@ import type { MeshStore } from "../store";
|
|
|
20
20
|
import { queueProjectBindingSync } from "../../engine/runtime/engine-projects";
|
|
21
21
|
import { isCursorTaskCloneId } from "../../sessions/cursor/catalog";
|
|
22
22
|
import { localTaskTransfers } from '../../task-transfer';
|
|
23
|
-
export { inspectRepositoryAsync, inspectWorkingTreeAsync } from "./repository-inspection";
|
|
23
|
+
export { inspectRepositoryAsync, inspectWorkingTreeAsync, readRepositoryGit } from "./repository-inspection";
|
|
24
24
|
export { prepareMonitorInspection } from "./monitor-inspection";
|
|
25
25
|
import type { CatalogInspection } from "./monitor-inspection";
|
|
26
26
|
export type { CatalogInspection } from "./monitor-inspection";
|
|
@@ -7,18 +7,20 @@ const waiting: Array<() => void> = [];
|
|
|
7
7
|
const pending = new Map<string, Promise<RepositoryFacts>>();
|
|
8
8
|
|
|
9
9
|
/** Native read-only Git work stays bounded without blocking relay callbacks. */
|
|
10
|
-
function
|
|
10
|
+
export function readRepositoryGit(cwd: string, args: string[],
|
|
11
|
+
options: { timeoutMs?: number; maxBuffer?: number } = {}): Promise<string | undefined> {
|
|
11
12
|
return new Promise(resolve => {
|
|
12
13
|
const run = () => {
|
|
13
14
|
running++;
|
|
14
15
|
const done = (error: Error | null, stdout = "") => {
|
|
15
16
|
running--;
|
|
16
17
|
waiting.shift()?.();
|
|
17
|
-
resolve(error ? undefined :
|
|
18
|
+
resolve(error ? undefined : stdout);
|
|
18
19
|
};
|
|
19
20
|
try {
|
|
20
|
-
execFile("git", ["-C", cwd, ...args], {
|
|
21
|
-
encoding: "utf8", timeout:
|
|
21
|
+
execFile("git", ["--no-optional-locks", "-C", cwd, ...args], {
|
|
22
|
+
encoding: "utf8", timeout: options.timeoutMs ?? 2_000,
|
|
23
|
+
maxBuffer: options.maxBuffer ?? 1024 * 1024, windowsHide: true,
|
|
22
24
|
}, done);
|
|
23
25
|
} catch { done(new Error("Repository command could not start")); }
|
|
24
26
|
};
|
|
@@ -26,6 +28,10 @@ function git(cwd: string, args: string[], status = false): Promise<string | unde
|
|
|
26
28
|
});
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
async function git(cwd: string, args: string[]): Promise<string | undefined> {
|
|
32
|
+
return (await readRepositoryGit(cwd, args))?.trim() || undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
async function inspect(cwd: string): Promise<RepositoryFacts> {
|
|
30
36
|
const worktree = await git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
31
37
|
const root = worktree ?? cwd;
|
|
@@ -50,6 +56,7 @@ export function inspectRepositoryAsync(cwd: string): Promise<RepositoryFacts> {
|
|
|
50
56
|
|
|
51
57
|
/** A fresh working-tree observation shares the bounded, nonblocking Git command pool. */
|
|
52
58
|
export async function inspectWorkingTreeAsync(cwd: string): Promise<boolean | undefined> {
|
|
53
|
-
const output = await
|
|
59
|
+
const output = await readRepositoryGit(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '-z'],
|
|
60
|
+
{ timeoutMs: 5_000, maxBuffer: 4 * 1024 * 1024 });
|
|
54
61
|
return output === undefined ? undefined : output.length > 0;
|
|
55
62
|
}
|
|
@@ -20,6 +20,15 @@ session's initial workspace. Titles and chat prose never select repositories.
|
|
|
20
20
|
The Apple client uses this evidence for placement, preserving durable Task
|
|
21
21
|
identity and access scope.
|
|
22
22
|
|
|
23
|
+
Background Mesh publication shares one asynchronous checkout observation across
|
|
24
|
+
admission, Graph selection and repository details. It reads working-tree status
|
|
25
|
+
on every publication. Bounded history for an unchanged commit can be reused for
|
|
26
|
+
up to 30 seconds in the background; explicit observations bypass that cache.
|
|
27
|
+
History commands name the observed commit so a concurrent commit cannot silently
|
|
28
|
+
substitute another revision. At most 128 history observations remain in memory.
|
|
29
|
+
History, status and checkout metadata share a maximum of two native Git
|
|
30
|
+
subprocesses, keeping background publication off the relay's event loop.
|
|
31
|
+
|
|
23
32
|
Behavior tests live in `tests/`. See the [runtime README](../../../../../README.md).
|
|
24
33
|
|
|
25
34
|
Licensed under the [GrantTap Commercial Source License](../../../../../LICENSE).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ProjectRepositoryDetails } from "../../../../../packages/protocol/schema";
|
|
2
|
+
import { git } from "./git";
|
|
3
|
+
|
|
4
|
+
type History = Pick<ProjectRepositoryDetails, "commits" | "contributors" | "commitCount" | "contributorCount">;
|
|
5
|
+
const cached = new Map<string, { value: History; at: number }>();
|
|
6
|
+
const pending = new Map<string, Promise<History>>();
|
|
7
|
+
const clean = (text: string, limit: number) => text.replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, limit);
|
|
8
|
+
|
|
9
|
+
async function read(path: string, revision?: string): Promise<History> {
|
|
10
|
+
const reference = revision ?? "HEAD";
|
|
11
|
+
const [history, shortlog, count] = await Promise.all([
|
|
12
|
+
git(path, ["log", "-8", "--format=%H%x1f%an%x1f%ct%x1f%s%x1e", reference]),
|
|
13
|
+
git(path, ["shortlog", "-sn", reference]), git(path, ["rev-list", "--count", reference]),
|
|
14
|
+
]);
|
|
15
|
+
const commits = (history ?? "").split("\u001e").flatMap(row => {
|
|
16
|
+
const [sha, author, time, subject] = row.trim().split("\u001f");
|
|
17
|
+
return sha && /^[a-f0-9]{40,64}$/.test(sha) && Number.isFinite(Number(time)) && Number(time) >= 0
|
|
18
|
+
? [{ sha, author: clean(author ?? "", 160), subject: clean(subject ?? "", 240), committedAt: Number(time) * 1_000 }] : [];
|
|
19
|
+
});
|
|
20
|
+
const contributors = (shortlog ?? "").split("\n").flatMap(row => {
|
|
21
|
+
const match = row.match(/^\s*(\d+)\s+(.+)$/);
|
|
22
|
+
return match ? [{ name: clean(match[2]!, 160), commits: Number(match[1]) }] : [];
|
|
23
|
+
});
|
|
24
|
+
return { commits, contributors: contributors.slice(0, 12),
|
|
25
|
+
commitCount: count == null ? (revision ? undefined : 0) : Number(count),
|
|
26
|
+
contributorCount: shortlog == null ? (revision ? undefined : 0) : contributors.length };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Only background history is reused; checkout metadata and working-tree state remain fresh. */
|
|
30
|
+
export async function repositoryHistory(path: string, revision: string | undefined, background: boolean): Promise<History> {
|
|
31
|
+
const key = JSON.stringify([path, revision]), previous = cached.get(key), now = Date.now();
|
|
32
|
+
if (background && previous && now >= previous.at && now - previous.at < 30_000) {
|
|
33
|
+
return structuredClone(previous.value);
|
|
34
|
+
}
|
|
35
|
+
const active = pending.get(key);
|
|
36
|
+
if (background && active) return structuredClone(await active);
|
|
37
|
+
const next = read(path, revision).then(value => {
|
|
38
|
+
if (pending.get(key) === next) {
|
|
39
|
+
cached.delete(key);
|
|
40
|
+
if (cached.size >= 128) cached.delete(cached.keys().next().value!);
|
|
41
|
+
cached.set(key, { value: structuredClone(value), at: Date.now() });
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}).finally(() => { if (pending.get(key) === next) pending.delete(key); });
|
|
45
|
+
pending.set(key, next);
|
|
46
|
+
return structuredClone(await next);
|
|
47
|
+
}
|
|
@@ -1,53 +1,38 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
1
|
import { ProjectRepositoryDetails, type MeshSnapshot, type ProjectBindingSummary } from "../../../../../packages/protocol/schema";
|
|
4
|
-
import {
|
|
2
|
+
import { inspectRepositoryAsync, type RepositoryFacts } from "../catalog";
|
|
3
|
+
import { git } from "./git";
|
|
4
|
+
import { repositoryHistory } from "./history";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} catch { return undefined; }
|
|
6
|
+
type ObservationOptions = { background?: boolean; inspect?: (path: string) => RepositoryFacts };
|
|
7
|
+
|
|
8
|
+
async function observed(path: string, options: ObservationOptions): Promise<RepositoryFacts> {
|
|
9
|
+
try { if (options.inspect) return options.inspect(path); }
|
|
10
|
+
catch { /* Removed paths still need an explicit unavailable report. */ }
|
|
11
|
+
return inspectRepositoryAsync(path);
|
|
13
12
|
}
|
|
14
|
-
const clean = (text: string, limit: number) => text.replace(/[\u0000-\u001f\u007f]/g," ").trim().slice(0,limit);
|
|
15
13
|
|
|
16
|
-
async function read(binding: ProjectBindingSummary): Promise<ProjectRepositoryDetails> {
|
|
14
|
+
async function read(binding: ProjectBindingSummary, options: ObservationOptions): Promise<ProjectRepositoryDetails> {
|
|
17
15
|
const path=binding.localPathHint!;
|
|
18
|
-
const
|
|
16
|
+
const facts = await observed(path, options);
|
|
17
|
+
const root = facts.worktree;
|
|
19
18
|
const base={projectId:binding.projectId,repositoryId:binding.repositoryId,endpointId:binding.endpointId,
|
|
20
19
|
commits:[],contributors:[],observedAt:Date.now()};
|
|
21
20
|
if (!root) {
|
|
22
21
|
const exists=await import("node:fs/promises").then(fs=>fs.stat(path).then(s=>s.isDirectory(),()=>false));
|
|
23
22
|
return {...base,status:exists ? "not_git" : "unavailable"};
|
|
24
23
|
}
|
|
25
|
-
const [
|
|
26
|
-
git(root,["remote","get-url","origin"]),git(root,["rev-parse","--verify","HEAD"]),
|
|
27
|
-
git(root,["symbolic-ref","--quiet","--short","HEAD"]),
|
|
24
|
+
const [status, history] = await Promise.all([
|
|
28
25
|
git(root,["status","--porcelain=v1","--untracked-files=normal"]),
|
|
29
|
-
|
|
30
|
-
git(root,["shortlog","-sn","HEAD"]),git(root,["rev-list","--count","HEAD"]),
|
|
26
|
+
repositoryHistory(root, facts.revision, options.background === true),
|
|
31
27
|
]);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
? [{sha,author:clean(author ?? "",160),subject:clean(subject ?? "",240),committedAt:Number(time)*1_000}] : [];
|
|
36
|
-
});
|
|
37
|
-
const contributors=(shortlog ?? "").split("\n").flatMap(row=>{
|
|
38
|
-
const match=row.match(/^\s*(\d+)\s+(.+)$/);
|
|
39
|
-
return match ? [{name:clean(match[2]!,160),commits:Number(match[1])}] : [];
|
|
40
|
-
});
|
|
41
|
-
return ProjectRepositoryDetails.parse({...base,status:"ready",
|
|
42
|
-
canonicalRepositoryId:canonicalRepositoryIdentity(remote,root),revision:revision || undefined,
|
|
43
|
-
branch:branch || undefined,dirty:status == null ? undefined : status.length>0,
|
|
44
|
-
commitCount:count == null ? (revision ? undefined : 0) : Number(count),commits,
|
|
45
|
-
contributorCount:shortlog == null ? (revision ? undefined : 0) : contributors.length,
|
|
46
|
-
contributors:contributors.slice(0,12)});
|
|
28
|
+
return ProjectRepositoryDetails.parse({...base, ...history, status:"ready",
|
|
29
|
+
canonicalRepositoryId: facts.canonicalRepositoryId, revision: facts.revision,
|
|
30
|
+
branch: facts.branch, dirty:status == null ? undefined : status.length>0 });
|
|
47
31
|
}
|
|
48
32
|
|
|
49
33
|
/** Bounded Git observations published under the existing Mesh encryption scope. */
|
|
50
|
-
export async function repositoryDetails(snapshot: MeshSnapshot, endpointId: string
|
|
34
|
+
export async function repositoryDetails(snapshot: MeshSnapshot, endpointId: string,
|
|
35
|
+
options: ObservationOptions = {}): Promise<ProjectRepositoryDetails[]> {
|
|
51
36
|
const bindings=(snapshot.bindings ?? []).filter(binding=>binding.projectId===snapshot.projectId
|
|
52
37
|
&& binding.endpointId===endpointId && binding.available && binding.localPathHint);
|
|
53
38
|
const reports:ProjectRepositoryDetails[]=[];
|
|
@@ -55,7 +40,7 @@ export async function repositoryDetails(snapshot: MeshSnapshot, endpointId: stri
|
|
|
55
40
|
for (const binding of bindings.slice(0,64)) {
|
|
56
41
|
if (seen.has(binding.repositoryId)) continue;
|
|
57
42
|
seen.add(binding.repositoryId);
|
|
58
|
-
reports.push(await read(binding));
|
|
43
|
+
reports.push(await read(binding, options));
|
|
59
44
|
}
|
|
60
45
|
return reports;
|
|
61
46
|
}
|
|
@@ -30,6 +30,10 @@ Behavior tests: `tests/`, root `tests/graph/graph-binding-sync.test.ts`, and
|
|
|
30
30
|
`engine-selection-live.test.ts` uses `GRANTTAP_ENGINE_TEST_BINARY` to verify the
|
|
31
31
|
actual native scanner respects `.git/info/exclude` and retains untracked source.
|
|
32
32
|
|
|
33
|
+
Background publication prepares checkout facts asynchronously once and reuses
|
|
34
|
+
that observation for admission and Graph selection. The selector never needs a
|
|
35
|
+
second synchronous Git read in the monitor's relay event loop.
|
|
36
|
+
|
|
33
37
|
## License
|
|
34
38
|
|
|
35
39
|
GrantTap Commercial Source License. See [LICENSE](../../../../../../LICENSE).
|
|
@@ -1,7 +1,24 @@
|
|
|
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
|
+
import { inspectRepository, inspectRepositoryAsync, type RepositoryFacts } from "../../catalog";
|
|
5
|
+
|
|
6
|
+
/** One nonblocking native observation shared by admission, graph selection and Git details. */
|
|
7
|
+
export async function prepareGraphInspection(bindings: ProjectBindingSummary[], endpointId: string) {
|
|
8
|
+
const paths = new Set(bindings.filter(binding => binding.endpointId === endpointId
|
|
9
|
+
&& binding.available && binding.localPathHint).flatMap(binding => {
|
|
10
|
+
try { return [realpathSync(binding.localPathHint!)]; } catch { return []; }
|
|
11
|
+
}));
|
|
12
|
+
const observations = new Map(await Promise.all([...paths].map(async path => {
|
|
13
|
+
try { return [path, await inspectRepositoryAsync(path)] as const; }
|
|
14
|
+
catch { return [path, undefined] as const; }
|
|
15
|
+
})));
|
|
16
|
+
return (path: string): RepositoryFacts => {
|
|
17
|
+
const facts = observations.get(realpathSync(path));
|
|
18
|
+
if (!facts) throw new Error("The checkout was not observed for this publication");
|
|
19
|
+
return { ...facts };
|
|
20
|
+
};
|
|
21
|
+
}
|
|
5
22
|
|
|
6
23
|
/** Local identities name an exact directory, even after that directory gains an origin. */
|
|
7
24
|
export function matchesLocalCheckout(repositoryId: string, root: string): boolean {
|
|
@@ -13,7 +30,8 @@ export function matchesLocalCheckout(repositoryId: string, root: string): boolea
|
|
|
13
30
|
}
|
|
14
31
|
|
|
15
32
|
/** Once admitted, one checkout contributes one graph to this Mesh. */
|
|
16
|
-
export function graphAnalysisBindings(bindings: ProjectBindingSummary[], endpointId?: string
|
|
33
|
+
export function graphAnalysisBindings(bindings: ProjectBindingSummary[], endpointId?: string,
|
|
34
|
+
inspect: (path: string) => RepositoryFacts = inspectRepository): ProjectBindingSummary[] {
|
|
17
35
|
const selected = new Map<string, ProjectBindingSummary>();
|
|
18
36
|
const observations = new Map<string, RepositoryFacts | undefined>();
|
|
19
37
|
const nonRepositories = new Set<string>();
|
|
@@ -27,7 +45,7 @@ export function graphAnalysisBindings(bindings: ProjectBindingSummary[], endpoin
|
|
|
27
45
|
if (path && !observations.has(path)) {
|
|
28
46
|
let facts: RepositoryFacts | undefined;
|
|
29
47
|
try {
|
|
30
|
-
const observed =
|
|
48
|
+
const observed = inspect(path);
|
|
31
49
|
if (observed.worktree && realpathSync(observed.root) === path) facts = observed;
|
|
32
50
|
else nonRepositories.add(path);
|
|
33
51
|
} catch { /* Unavailable roots retain their own identity. */ }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { projectRelationshipPeers, mergeRelationshipPeers } from "../../../engine/runtime/project-relationships";
|
|
2
2
|
import { refreshLocalGraphBindings } from "../graph/binding-sync";
|
|
3
|
-
import { graphAnalysisBindings } from "../graph/checkout-identity";
|
|
3
|
+
import { graphAnalysisBindings, prepareGraphInspection } from "../graph/checkout-identity";
|
|
4
4
|
import type { MeshSnapshot } from "../../../../../../packages/protocol/schema";
|
|
5
5
|
import {
|
|
6
|
-
projectBackbone, projectRepositoryGraphs, waitForProjectBindingSync,
|
|
6
|
+
projectBackbone, projectRepositoryGraphs, waitForProjectBindingSync, queueProjectBindingSync,
|
|
7
7
|
} from "../../../engine/runtime/engine-projects";
|
|
8
8
|
import { projectCortexIntegration } from "../../../cortex/integration";
|
|
9
9
|
import { projectKnowledge, queueProjectKnowledgeSync } from "../../../engine/runtime/engine-memory";
|
|
@@ -16,10 +16,14 @@ import { computerId } from "../../identity/computer";
|
|
|
16
16
|
export async function enrichMeshSnapshot(
|
|
17
17
|
snapshot: MeshSnapshot, options: { refreshGraph?: boolean } = {},
|
|
18
18
|
): Promise<MeshSnapshot> {
|
|
19
|
-
const
|
|
19
|
+
const endpoint = computerId();
|
|
20
|
+
const inspect = await prepareGraphInspection(snapshot.bindings ?? [], endpoint);
|
|
21
|
+
const identityFailures = await refreshLocalGraphBindings(snapshot, {
|
|
22
|
+
endpoint: () => endpoint, inspect, sync: queueProjectBindingSync,
|
|
23
|
+
});
|
|
20
24
|
const excludedRepositories = new Set(identityFailures.map((report) => report.repositoryId));
|
|
21
25
|
const bindings = graphAnalysisBindings((snapshot.bindings ?? [])
|
|
22
|
-
.filter((binding) => !excludedRepositories.has(binding.repositoryId)),
|
|
26
|
+
.filter((binding) => !excludedRepositories.has(binding.repositoryId)), endpoint, inspect);
|
|
23
27
|
await waitForProjectBindingSync(snapshot.projectId);
|
|
24
28
|
queueProjectKnowledgeSync({
|
|
25
29
|
projectId: snapshot.projectId,
|
|
@@ -32,7 +36,7 @@ export async function enrichMeshSnapshot(
|
|
|
32
36
|
priority: snapshot.tasks.reduce((latest, task) => Math.max(latest, task.updatedAt), 0),
|
|
33
37
|
}),
|
|
34
38
|
projectKnowledge(snapshot.projectId),
|
|
35
|
-
repositoryDetails(snapshot,
|
|
39
|
+
repositoryDetails(snapshot, endpoint, { background: options.refreshGraph !== true, inspect }),
|
|
36
40
|
]);
|
|
37
41
|
const relationships = await projectRelationshipPeers(snapshot.projectId, bindings, {
|
|
38
42
|
background: options.refreshGraph !== true,
|
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.121
|
|
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.121\";\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.121";
|
|
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;
|