granttap-mcp 0.8.85 → 0.8.86

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.
@@ -17,6 +17,16 @@ negative probes nor old repository identities persist across refreshes.
17
17
  Behavior tests: `tests/agentic/codex-workspace-binding.test.ts` and
18
18
  `tests/project/project-mesh-core.test.ts`.
19
19
 
20
+ ## Background Graph admission
21
+
22
+ `inspectRepositoryAsync` provides the same repository facts without blocking
23
+ relay callbacks. Native Git subprocesses are limited to two across the process.
24
+ Concurrent inspections of the same path share their in-flight result; a later
25
+ inspection verifies the checkout again. Missing Git facts remain unknown.
26
+
27
+ Tests for asynchronous inspection live in `tests/`; Graph admission also has
28
+ `runtime/graph/tests/responsiveness/checkout-inspection.test.ts`.
29
+
20
30
  ## License
21
31
 
22
32
  GrantTap Commercial Source License. See [LICENSE](../../../../../LICENSE).
@@ -19,6 +19,7 @@ import { readIntegrationMap } from "../observed/integration-map";
19
19
  import type { MeshStore } from "../store";
20
20
  import { queueProjectBindingSync } from "../../engine/runtime/engine-projects";
21
21
  import { isCursorTaskCloneId } from "../../sessions/cursor/catalog";
22
+ export { inspectRepositoryAsync } from "./repository-inspection";
22
23
 
23
24
  export type RepositoryFacts = {
24
25
  root: string;
@@ -0,0 +1,49 @@
1
+ import { execFile } from "node:child_process";
2
+ import { canonicalRepositoryIdentity, sanitizedRepositoryRemote } from "../identity";
3
+ import type { RepositoryFacts } from ".";
4
+
5
+ let running = 0;
6
+ const waiting: Array<() => void> = [];
7
+ const pending = new Map<string, Promise<RepositoryFacts>>();
8
+
9
+ /** Native read-only Git work stays bounded without blocking relay callbacks. */
10
+ function git(cwd: string, args: string[]): Promise<string | undefined> {
11
+ return new Promise(resolve => {
12
+ const run = () => {
13
+ running++;
14
+ const done = (error: Error | null, stdout = "") => {
15
+ running--;
16
+ waiting.shift()?.();
17
+ resolve(error ? undefined : stdout.trim() || undefined);
18
+ };
19
+ try {
20
+ execFile("git", ["-C", cwd, ...args], {
21
+ encoding: "utf8", timeout: 2_000, maxBuffer: 1024 * 1024, windowsHide: true,
22
+ }, done);
23
+ } catch { done(new Error("Repository command could not start")); }
24
+ };
25
+ if (running < 2) run(); else waiting.push(run);
26
+ });
27
+ }
28
+
29
+ async function inspect(cwd: string): Promise<RepositoryFacts> {
30
+ const worktree = await git(cwd, ["rev-parse", "--show-toplevel"]);
31
+ const root = worktree ?? cwd;
32
+ const [rawRemote, revision, branch] = await Promise.all([
33
+ git(root, ["remote", "get-url", "origin"]),
34
+ git(root, ["rev-parse", "HEAD"]),
35
+ git(root, ["symbolic-ref", "--quiet", "--short", "HEAD"]),
36
+ ]);
37
+ return { root, worktree, revision, branch,
38
+ baseRemote: rawRemote ? sanitizedRepositoryRemote(rawRemote) : undefined,
39
+ canonicalRepositoryId: canonicalRepositoryIdentity(rawRemote, root) };
40
+ }
41
+
42
+ /** Share only in-flight observations; a later refresh verifies the checkout again. */
43
+ export function inspectRepositoryAsync(cwd: string): Promise<RepositoryFacts> {
44
+ const active = pending.get(cwd);
45
+ if (active) return active;
46
+ const next = inspect(cwd).finally(() => { pending.delete(cwd); });
47
+ pending.set(cwd, next);
48
+ return next;
49
+ }
@@ -1,7 +1,7 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import type { MeshSnapshot, ProjectRepositoryGraph } from "../../../../../../packages/protocol/schema";
3
3
  import { queueProjectBindingSync } from "../../../engine/runtime/engine-projects";
4
- import { inspectRepository } from "../../catalog";
4
+ import { inspectRepositoryAsync, type RepositoryFacts } from "../../catalog";
5
5
  import { computerId } from "../../identity/computer";
6
6
  import { matchesLocalCheckout } from "./checkout-identity";
7
7
 
@@ -18,18 +18,18 @@ export async function refreshLocalGraphBindings(
18
18
  snapshot: MeshSnapshot,
19
19
  dependencies: {
20
20
  endpoint: () => string;
21
- inspect: typeof inspectRepository;
21
+ inspect: (cwd: string) => RepositoryFacts | Promise<RepositoryFacts>;
22
22
  sync: typeof queueProjectBindingSync;
23
- } = { endpoint: computerId, inspect: inspectRepository, sync: queueProjectBindingSync },
23
+ } = { endpoint: computerId, inspect: inspectRepositoryAsync, sync: queueProjectBindingSync },
24
24
  ): Promise<ProjectRepositoryGraph[]> {
25
25
  const localEndpoint = dependencies.endpoint();
26
26
  const candidates = (snapshot.bindings ?? []).filter((binding) =>
27
27
  binding.endpointId === localEndpoint && binding.available && binding.localPathHint);
28
28
  const failures: ProjectRepositoryGraph[] = [];
29
- const checkouts = candidates.flatMap((binding) => {
30
- const facts = readCheckout(dependencies.inspect, binding.localPathHint!);
29
+ const checkouts = (await Promise.all(candidates.map(async binding => {
30
+ const facts = await readCheckout(dependencies.inspect, binding.localPathHint!);
31
31
  return facts ? [{ binding, facts, real: realpathSync(facts.root) }] : [];
32
- });
32
+ }))).flat();
33
33
  await Promise.all(checkouts.map(async ({ binding, facts, real }) => {
34
34
  const pathIdentity = matchesLocalCheckout(binding.repositoryId, facts.root);
35
35
  if (facts.canonicalRepositoryId !== binding.repositoryId && !pathIdentity) {
@@ -58,9 +58,9 @@ export async function refreshLocalGraphBindings(
58
58
  }
59
59
 
60
60
  /** A Git checkout rooted exactly at the bound path, or nothing a Graph can be built from. */
61
- function readCheckout(inspect: typeof inspectRepository, path: string) {
61
+ async function readCheckout(inspect: (cwd: string) => RepositoryFacts | Promise<RepositoryFacts>, path: string) {
62
62
  try {
63
- const facts = inspect(path);
63
+ const facts = await inspect(path);
64
64
  if (!facts.worktree || realpathSync(facts.root) !== realpathSync(path)) return undefined;
65
65
  return facts;
66
66
  } catch {
@@ -0,0 +1,13 @@
1
+ # Usage publication
2
+
3
+ Public entry: `index.ts`. Complete capability totals are computed from the live
4
+ catalog and full discovered history. This work has one independent in-flight
5
+ publication per relay client. A slow history read or encrypted fan-out does not
6
+ hold the current chat catalog. Successful reports retain their existing cadence;
7
+ failures retry on a later catalog publication. Closing the monitor prevents
8
+ further queued reads and sends. Counts and unknown usage semantics are unchanged.
9
+ Concurrent catalog and Usage requests share one in-flight history read. The
10
+ history cache's lifetime begins when that read completes.
11
+
12
+ Behavior tests live in `tests/`.
13
+ Licensed under the GrantTap Commercial Source License in the repository root.
@@ -0,0 +1,33 @@
1
+ import { setImmediate } from "node:timers/promises";
2
+ import type { RelayClient } from "../../../../../packages/core/relay-client";
3
+ import type { SessionInfo, SessionsStatus } from "../../../../../packages/protocol/schema";
4
+ import { scanCapabilityUsage, scopeCapabilityUsageToRoom } from "../../sessions";
5
+
6
+ type Publication = { running: boolean; closed: boolean; lastPublishedAt: number };
7
+ const publications = new WeakMap<RelayClient, Publication>();
8
+
9
+ /** A slow usage fan-out never occupies the current chat catalog's publication slot. */
10
+ export function publishMonitorCapabilities(client: RelayClient, status: SessionsStatus,
11
+ history: () => Promise<SessionInfo[]>): void {
12
+ const state = publications.get(client) ?? { running: false, closed: false, lastPublishedAt: 0 };
13
+ publications.set(client, state);
14
+ if (state.running || state.closed || Date.now() - state.lastPublishedAt < 30_000) return;
15
+ state.running = true;
16
+ const work = (async () => {
17
+ await setImmediate();
18
+ if (state.closed || !client.isConnected) return;
19
+ const older = await history();
20
+ if (state.closed || !client.isConnected) return;
21
+ const usage = scanCapabilityUsage([...status.sessions, ...older]);
22
+ await client.send(scopeCapabilityUsageToRoom(usage, client.room), "phone",
23
+ { ttlMs: 90_000, reliable: false });
24
+ state.lastPublishedAt = Date.now();
25
+ })();
26
+ void work.catch(() => {}).finally(() => { state.running = false; });
27
+ }
28
+
29
+ export function stopMonitorCapabilities(client: RelayClient): void {
30
+ const state = publications.get(client);
31
+ if (state) state.closed = true;
32
+ publications.delete(client);
33
+ }
@@ -23,7 +23,7 @@ import { publishedCatalog, rememberPublishedCatalog, resumePublishedChats,
23
23
  publishCatalogHeads, stopCatalogHeads } from "../../session-catalog";
24
24
  import { createMachineLoadPublisher } from "../../machine-load";
25
25
  import { startMachineLoadLoop } from "../../machine-load/host/loop";
26
- import { scanCapabilityUsage, scanSessionHistoryCooperatively, scanSessionsCooperatively, scopeCapabilityUsageToRoom, TOKEN_WINDOW_HOURS } from "../../sessions";
26
+ import { scanSessionHistoryCooperatively, scanSessionsCooperatively, TOKEN_WINDOW_HOURS } from "../../sessions";
27
27
  import { boundedCatalogHistory, INTERVAL_MS, publishSessionEvents } from "../handlers/catalog";
28
28
  import { sweepAttachments } from "../handlers/receipts";
29
29
  import { handleMonitorMessage } from "../handlers/inbound";
@@ -32,6 +32,7 @@ import { decorateTaskOriginCatalog } from '../../task-origins';
32
32
  const HISTORY_INTERVAL_MS = 60_000;
33
33
  const resumedHeads = new WeakSet<RelayClient>();
34
34
  import { publishMonitorMeshes, stopMonitorMeshes } from "../mesh-publication";
35
+ import { publishMonitorCapabilities, stopMonitorCapabilities } from "../capability-publication";
35
36
 
36
37
  export type SessionMonitor = {
37
38
  publish: () => Promise<void>;
@@ -114,22 +115,22 @@ async function publishMonitorTick(input: {
114
115
  leadership: ReturnType<typeof monitorLeadership>;
115
116
  subscriptions: Set<string>;
116
117
  history: (force?: boolean) => Promise<SessionsStatus["sessions"]>;
117
- clocks: { lastHistoryPublishedAt: number; lastCapabilityPublishedAt: number };
118
+ clocks: { lastHistoryPublishedAt: number };
118
119
  loadLoop: { updateStatus: (status: SessionsStatus) => void };
119
120
  forceHistory: boolean;
120
121
  active: () => boolean;
121
- }): Promise<{ lastHistoryPublishedAt: number; lastCapabilityPublishedAt: number }> {
122
+ }): Promise<{ lastHistoryPublishedAt: number }> {
122
123
  const { client, leadership, subscriptions, history, clocks, loadLoop, forceHistory } = input;
123
- let { lastHistoryPublishedAt, lastCapabilityPublishedAt } = clocks;
124
- if (!client.isConnected) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
124
+ let { lastHistoryPublishedAt } = clocks;
125
+ if (!client.isConnected) return { lastHistoryPublishedAt };
125
126
  await publishHeartbeat(client).catch(() => {});
126
- if (!input.active()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
127
- if (!leadership.acquire()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
127
+ if (!input.active()) return { lastHistoryPublishedAt };
128
+ if (!leadership.acquire()) return { lastHistoryPublishedAt };
128
129
  await client.send(approvalsStatus(), "phone", { ttlMs: INTERVAL_MS * 3, reliable: false });
129
130
  const initial = lastHistoryPublishedAt === 0;
130
131
  const includeHistory = forceHistory || (!initial && Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS);
131
132
  const snapshot = await monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
132
- if (!input.active()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
133
+ if (!input.active()) return { lastHistoryPublishedAt };
133
134
  const status = decorateTaskOriginCatalog(snapshot, client.room);
134
135
  rememberPublishedCatalog(client, status);
135
136
  await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
@@ -146,15 +147,8 @@ async function publishMonitorTick(input: {
146
147
  void refreshMcpLoad(status.sessions).catch(() => {});
147
148
  publishCatalogHeads(client, status, subscriptions, publishSessionEvents);
148
149
  if (loadRuntimeConfig().meshEnabled) publishMonitorMeshes(client, status);
149
- if (Date.now() - lastCapabilityPublishedAt >= 30_000) {
150
- const usage = scanCapabilityUsage([
151
- ...status.sessions,
152
- ...await history(false),
153
- ]);
154
- await client.send(scopeCapabilityUsageToRoom(usage, client.room), "phone", { ttlMs: 90_000, reliable: false });
155
- lastCapabilityPublishedAt = Date.now();
156
- }
157
- return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
150
+ publishMonitorCapabilities(client, status, () => history(false));
151
+ return { lastHistoryPublishedAt };
158
152
  }
159
153
 
160
154
  /**
@@ -168,8 +162,8 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
168
162
  primeSessionKeys(client);
169
163
  publishedCatalog(client);
170
164
  let historyCache: { generatedAt: number; sessions: SessionsStatus["sessions"] } | undefined;
165
+ let historyRead: Promise<SessionsStatus["sessions"]> | undefined;
171
166
  let lastHistoryPublishedAt = 0;
172
- let lastCapabilityPublishedAt = 0;
173
167
  let closed = false;
174
168
  const publishMachineLoad = createMachineLoadPublisher({
175
169
  log: (line) => process.stderr.write(`[monitor] ${line}\n`),
@@ -189,11 +183,16 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
189
183
  },
190
184
  });
191
185
 
192
- const history = async (force = false): Promise<SessionsStatus["sessions"]> => {
193
- if (force || !historyCache || Date.now() - historyCache.generatedAt > 60_000) {
194
- historyCache = { generatedAt: Date.now(), sessions: await scanSessionHistoryCooperatively() };
186
+ const history = (force = false): Promise<SessionsStatus["sessions"]> => {
187
+ if (historyRead) return historyRead;
188
+ if (!force && historyCache && Date.now() - historyCache.generatedAt <= 60_000) {
189
+ return Promise.resolve(historyCache.sessions);
195
190
  }
196
- return historyCache.sessions;
191
+ historyRead = scanSessionHistoryCooperatively().then(sessions => {
192
+ historyCache = { generatedAt: Date.now(), sessions };
193
+ return sessions;
194
+ }).finally(() => { historyRead = undefined; });
195
+ return historyRead;
197
196
  };
198
197
 
199
198
  const publish = singleFlightPublisher(async (forceHistory: boolean): Promise<void> => {
@@ -201,10 +200,9 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
201
200
  const next = await publishMonitorTick({
202
201
  client, leadership, subscriptions, history, loadLoop, forceHistory,
203
202
  active: () => !closed,
204
- clocks: { lastHistoryPublishedAt, lastCapabilityPublishedAt },
203
+ clocks: { lastHistoryPublishedAt },
205
204
  });
206
205
  lastHistoryPublishedAt = next.lastHistoryPublishedAt;
207
- lastCapabilityPublishedAt = next.lastCapabilityPublishedAt;
208
206
  });
209
207
 
210
208
  const off = client.onMessage(async (payload: Payload, peerPublicKey) => {
@@ -245,6 +243,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
245
243
  offConnected?.();
246
244
  stopCatalog();
247
245
  stopCatalogHeads(client);
246
+ stopMonitorCapabilities(client);
248
247
  stopMonitorMeshes(client);
249
248
  stopHeartbeat();
250
249
  loadLoop.stop();
@@ -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.85
13
+ npm install -g granttap-mcp@0.8.86
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.85\";\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.86\";\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.85";
10
+ const PACKAGE = "granttap-mcp@0.8.86";
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.85",
3
+ "version": "0.8.86",
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": {