granttap-mcp 0.8.50 → 0.8.52

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.
@@ -18,7 +18,8 @@ import { confirmPendingController } from "../../pairing/controllers";
18
18
  import { recordControllerName } from "../../pairing/presence";
19
19
  import { startPublishLoop } from "./publish-loop";
20
20
  import { singleFlightPublisher } from "./single-flight";
21
- import { publishedCatalog, rememberPublishedCatalog, resumePublishedChats } from "../../session-catalog";
21
+ import { publishedCatalog, rememberPublishedCatalog, resumePublishedChats,
22
+ publishCatalogHeads, stopCatalogHeads } from "../../session-catalog";
22
23
  import { createMachineLoadPublisher } from "../../machine-load";
23
24
  import { startMachineLoadLoop } from "../../machine-load/host/loop";
24
25
  import { scanCapabilityUsage, scanSessionHistory, scanSessions, scopeCapabilityUsageToRoom, TOKEN_WINDOW_HOURS } from "../../sessions";
@@ -135,9 +136,7 @@ async function publishMonitorTick(input: {
135
136
  if (includeHistory || initial) lastHistoryPublishedAt = Date.now();
136
137
  loadLoop.updateStatus(status);
137
138
  void refreshMcpLoad(status.sessions).catch(() => {});
138
- for (const sessionId of subscriptions) {
139
- await publishSessionEvents(client, sessionId, status).catch(() => false);
140
- }
139
+ publishCatalogHeads(client, status, subscriptions, publishSessionEvents);
141
140
  await client.send(approvalsStatus(), "phone", { ttlMs: INTERVAL_MS * 3, reliable: false });
142
141
  if (loadRuntimeConfig().meshEnabled) publishMonitorMeshes(client, status);
143
142
  if (Date.now() - lastCapabilityPublishedAt >= 30_000) {
@@ -226,6 +225,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
226
225
  publish: () => publish(false),
227
226
  close: () => {
228
227
  stopCatalog();
228
+ stopCatalogHeads(client);
229
229
  stopMonitorMeshes(client);
230
230
  stopHeartbeat();
231
231
  loadLoop.stop();
@@ -17,3 +17,7 @@ as fresh. The phone continues its durable historical backfill independently.
17
17
  Tests live in `tests/`. This runtime module uses the repository MIT License.
18
18
 
19
19
  Engine binding recovery waits for the first connected catalog publication before projecting repository graphs. A cold Engine cannot hold the initial relay handshake behind restored Mesh analysis.
20
+
21
+ Codex discovery shares repeated checkout verification only within one synchronous scan. The next scan verifies repository identity again; no long-lived checkout cache can cross a repository change. This keeps startup catalog delivery ahead of repeated Git subprocesses.
22
+
23
+ Subscribed transcript heads use a separate, single-flight transfer loop. A stalled head never occupies the current catalog publication slot; stopping the monitor cancels further queued reads. The iPhone keeps its historical backfill independent of both loops.
@@ -6,6 +6,7 @@ import { SessionsStatus, type SessionInfo } from "../../../../packages/protocol/
6
6
  import { configDir, writePrivateFile } from "../config";
7
7
  import { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "../sessions";
8
8
  export { recoverCatalogBindings } from "./startup-recovery";
9
+ export { publishCatalogHeads, stopCatalogHeads } from "./publish-heads";
9
10
 
10
11
  const catalogs = new WeakMap<RelayClient, SessionsStatus | undefined>();
11
12
  const cachePath = () => join(configDir(), "session-catalog-cache.json");
@@ -0,0 +1,27 @@
1
+ import type { RelayClient } from "../../../../packages/core/relay-client";
2
+ import type { SessionsStatus } from "../../../../packages/protocol/schema";
3
+
4
+ type HeadRun = { running: boolean; closed: boolean };
5
+ const runs = new WeakMap<RelayClient, HeadRun>();
6
+
7
+ /** Transcript transfers must never occupy the fresh catalog's publication slot. */
8
+ export function publishCatalogHeads(client: RelayClient, status: SessionsStatus,
9
+ subscriptions: ReadonlySet<string>,
10
+ publish: (client: RelayClient, sessionId: string, status: SessionsStatus) => Promise<unknown>): void {
11
+ const state = runs.get(client) ?? { running: false, closed: false };
12
+ runs.set(client, state);
13
+ if (state.running || state.closed || subscriptions.size === 0) return;
14
+ state.running = true;
15
+ void (async () => {
16
+ for (const sessionId of [...subscriptions]) {
17
+ await new Promise<void>(resolve => setImmediate(resolve));
18
+ if (state.closed || !client.isConnected) break;
19
+ await publish(client, sessionId, status).catch(() => {});
20
+ }
21
+ })().finally(() => { state.running = false; });
22
+ }
23
+
24
+ export function stopCatalogHeads(client: RelayClient): void {
25
+ const state = runs.get(client);
26
+ if (state) state.closed = true;
27
+ }
@@ -8,8 +8,13 @@ import {
8
8
  import { parseCodexFile } from "./parse";
9
9
  import { assembleCodexScan } from "./assemble";
10
10
  import { recentLogs, codexSessionsRoot, MAX_FILES, type Scan } from "../../support/common";
11
+ import { withCheckoutObservationCache } from "../../support/repository-checkout";
11
12
 
12
13
  export function scanCodex(maxFiles = MAX_FILES): Scan {
14
+ return withCheckoutObservationCache(() => scanCodexCatalog(maxFiles));
15
+ }
16
+
17
+ function scanCodexCatalog(maxFiles: number): Scan {
13
18
  const root = codexSessionsRoot();
14
19
  const listed = recentLogs(root, 5, maxFiles + 1);
15
20
  const files = listed.slice(0, maxFiles);
@@ -2,11 +2,25 @@ import { execFileSync } from "node:child_process";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { isAbsolute } from "node:path";
4
4
 
5
+ let scanRoots: Map<string, string | undefined> | undefined;
6
+
7
+ /** Share verified lookups only during this synchronous catalog pass. */
8
+ export function withCheckoutObservationCache<T>(scan: () => T): T {
9
+ if (scanRoots) return scan();
10
+ scanRoots = new Map();
11
+ try { return scan(); }
12
+ finally { scanRoots = undefined; }
13
+ }
14
+
5
15
  function gitRoot(path: string): string | undefined {
16
+ if (scanRoots?.has(path)) return scanRoots.get(path);
17
+ let root: string | undefined;
6
18
  try {
7
- return execFileSync("git", ["--no-optional-locks", "-C", path, "rev-parse", "--show-toplevel"],
19
+ root = execFileSync("git", ["--no-optional-locks", "-C", path, "rev-parse", "--show-toplevel"],
8
20
  {encoding:"utf8", stdio:["ignore","pipe","ignore"], timeout:2_000}).trim() || undefined;
9
- } catch { return undefined; }
21
+ } catch { root = undefined; }
22
+ scanRoots?.set(path, root);
23
+ return root;
10
24
  }
11
25
 
12
26
  /** Recognize only a literal initial shell directory change, without evaluating shell code. */
@@ -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.50
13
+ npm install -g granttap-mcp@0.8.52
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.50\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
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.52\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
8
8
  ]
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
7
7
  const { existsSync } = require("node:fs");
8
8
  const { delimiter, dirname, join } = require("node:path");
9
9
 
10
- const PACKAGE = "granttap-mcp@0.8.50";
10
+ const PACKAGE = "granttap-mcp@0.8.52";
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.50",
3
+ "version": "0.8.52",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {