granttap-mcp 0.8.119 → 0.8.120

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.
@@ -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 git(cwd: string, args: string[], status = false): Promise<string | undefined> {
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 : status ? stdout : stdout.trim() || undefined);
18
+ resolve(error ? undefined : stdout);
18
19
  };
19
20
  try {
20
- execFile("git", ["-C", cwd, ...args], {
21
- encoding: "utf8", timeout: status ? 5_000 : 2_000, maxBuffer: (status ? 4 : 1) * 1024 * 1024, windowsHide: true,
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 git(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '-z'], true);
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,6 @@
1
+ import { readRepositoryGit } from "../catalog";
2
+
3
+ export async function git(path: string, args: string[]): Promise<string | undefined> {
4
+ const output = await readRepositoryGit(path, args, { maxBuffer: 256 * 1_024 });
5
+ return output?.trim();
6
+ }
@@ -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 { canonicalRepositoryIdentity } from "../identity";
2
+ import { inspectRepositoryAsync, type RepositoryFacts } from "../catalog";
3
+ import { git } from "./git";
4
+ import { repositoryHistory } from "./history";
5
5
 
6
- const run = promisify(execFile);
7
- async function git(path: string, args: string[]): Promise<string | undefined> {
8
- try {
9
- const result=await run("git",["--no-optional-locks","-C",path,...args],
10
- {encoding:"utf8",timeout:2_000,maxBuffer:256*1_024});
11
- return result.stdout.trim();
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 root=await git(path,["rev-parse","--show-toplevel"]);
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 [remote,revision,branch,status,history,shortlog,count]=await Promise.all([
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
- git(root,["log","-8","--format=%H%x1f%an%x1f%ct%x1f%s%x1e","HEAD"]),
30
- git(root,["shortlog","-sn","HEAD"]),git(root,["rev-list","--count","HEAD"]),
26
+ repositoryHistory(root, facts.revision, options.background === true),
31
27
  ]);
32
- const commits=(history ?? "").split("\u001e").flatMap(row=>{
33
- const [sha,author,time,subject]=row.trim().split("\u001f");
34
- return sha && /^[a-f0-9]{40,64}$/.test(sha) && Number.isFinite(Number(time)) && Number(time)>=0
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): Promise<ProjectRepositoryDetails[]> {
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): ProjectBindingSummary[] {
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 = inspectRepository(path);
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 identityFailures = await refreshLocalGraphBindings(snapshot);
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)), computerId());
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, computerId()),
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,
@@ -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.119
13
+ npm install -g granttap-mcp@0.8.120
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -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.119\";\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"
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.120\";\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.119";
10
+ const PACKAGE = "granttap-mcp@0.8.120";
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.119",
3
+ "version": "0.8.120",
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": {