granttap-mcp 0.8.49 → 0.8.51

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.
@@ -4,7 +4,7 @@ import { RelayClient } from "../../../../../packages/core/relay-client";
4
4
  import { configDir, loadConfig, machineConfigPath } from "../../config";
5
5
  import { EngineSupervisor } from "../../engine/runtime/engine-supervisor";
6
6
  import { meshSnapshotsAsync } from "../../mesh/runtime";
7
- import { publishedCatalog } from "../../session-catalog";
7
+ import { publishedCatalog, recoverCatalogBindings } from "../../session-catalog";
8
8
  import { recoverLocalGraphBindings } from "../../mesh/runtime/graph/binding-sync";
9
9
  import { startSessionMonitor } from "../../monitor";
10
10
  import { controllerPeerGate } from "../../pairing/controllers";
@@ -47,31 +47,25 @@ const stop = (): void => {
47
47
  process.on("SIGINT", stop);
48
48
  process.on("SIGTERM", stop);
49
49
 
50
- void engine.ensureAvailable().then((health) => {
51
- if (health.state === "healthy") {
52
- // A new Engine binary starts with its own registry. Re-admit existing,
53
- // locally verified Git bindings so linked repositories have a graph again.
50
+ void recoverCatalogBindings({
51
+ engineAvailable: async () => {
52
+ const health = await engine.ensureAvailable();
53
+ if (health.state !== "disabled" && health.state !== "healthy") {
54
+ process.stderr.write(`[granttap-mcp] Project engine ${health.state}; legacy behavior remains active\n`);
55
+ }
56
+ return health.state === "healthy";
57
+ },
58
+ publishCatalog: async () => {
59
+ try { await client.connect(); await monitor.publish(); }
60
+ catch (error) {
61
+ // RelayClient retries connection independently; keep this helper alive.
62
+ process.stderr.write(`[granttap-mcp] initial catalog connection failed (${error instanceof Error ? error.name : "unknown"})\n`);
63
+ throw error;
64
+ }
65
+ },
66
+ snapshots: () => {
54
67
  const known = publishedCatalog(client);
55
- void meshSnapshotsAsync(known ? [...known.sessions, ...(known.history ?? [])] : [])
56
- .then(recoverLocalGraphBindings)
57
- .then(() => monitor.publish())
58
- .catch(() => undefined);
59
- }
60
- if (health.state !== "disabled" && health.state !== "healthy") {
61
- const reason = health.reason ? `: ${health.reason}` : "";
62
- process.stderr.write(
63
- `[granttap-mcp] Project engine ${health.state}${reason}; legacy behavior remains active\n`,
64
- );
65
- }
66
- });
67
-
68
- void client
69
- .connect()
70
- .then(() => monitor.publish())
71
- .catch((error: unknown) => {
72
- // RelayClient keeps reconnecting in the background. Log the initial error
73
- // for diagnostics without terminating the launchd-managed helper.
74
- process.stderr.write(
75
- `[granttap-mcp] initial relay connection failed: ${error instanceof Error ? error.message : String(error)}\n`,
76
- );
77
- });
68
+ return meshSnapshotsAsync(known ? [...known.sessions, ...(known.history ?? [])] : []);
69
+ },
70
+ recover: recoverLocalGraphBindings,
71
+ }).then(() => monitor.publish()).catch(() => undefined);
@@ -38,7 +38,10 @@ export async function syncProjectBinding(
38
38
  ): Promise<boolean> {
39
39
  const env = options.env ?? process.env;
40
40
  if (!engineFeatureEnabled(env)) return false;
41
- const client = options.client ?? defaultClient();
41
+ // Repository admission is restore work; keep it off the policy connection.
42
+ const client = options.client ?? new EngineClient({
43
+ socketPath: join(configDir(), "engine.sock"), connectTimeoutMs: 10_000,
44
+ });
42
45
  try {
43
46
  const result = await client.request({
44
47
  operation: "project.upsert_binding",
@@ -63,10 +66,12 @@ export async function syncProjectBinding(
63
66
  last_seen_at: local.lastSeenAt,
64
67
  },
65
68
  },
66
- }, { timeoutMs: 5_000 });
69
+ }, { timeoutMs: 30_000 });
67
70
  return result.operation === "project.binding_upserted";
68
71
  } catch {
69
72
  return false;
73
+ } finally {
74
+ if (!options.client) client.close?.();
70
75
  }
71
76
  }
72
77
 
@@ -203,7 +208,9 @@ async function analyzeRepositoryGraph(
203
208
  ): Promise<ProjectRepositoryGraph | undefined> {
204
209
  // The daemon serves a socket serially. Analysis must not occupy the channel
205
210
  // used for policy, bindings and Backbone reads while a repository is scanned.
206
- const channel = client ?? new EngineClient({ socketPath: join(configDir(), "engine.sock") });
211
+ const channel = client ?? new EngineClient({
212
+ socketPath: join(configDir(), "engine.sock"), connectTimeoutMs: 10_000,
213
+ });
207
214
  try {
208
215
  const result = await channel.request({
209
216
  operation: "graph.analyze_repository", input: {
@@ -29,17 +29,25 @@ export async function refreshLocalGraphBindings(
29
29
  nodes: [], relations: [], totalNodes: 0, totalRelations: 0, truncated: false });
30
30
  return;
31
31
  }
32
- await dependencies.sync(snapshot.project, {
32
+ const admitted = await dependencies.sync(snapshot.project, {
33
33
  summary: binding,
34
34
  localRoot: facts.root,
35
35
  canonicalRemote: facts.baseRemote,
36
36
  lastSeenAt: Date.now(),
37
37
  });
38
- } catch { /* A stale checkout cannot prevent other verified bindings from recovery. */ }
38
+ if (!admitted) failures.push(admissionFailure(snapshot, binding));
39
+ } catch { failures.push(admissionFailure(snapshot, binding)); }
39
40
  }));
40
41
  return failures;
41
42
  }
42
43
 
44
+ function admissionFailure(snapshot: MeshSnapshot, binding: NonNullable<MeshSnapshot["bindings"]>[number]): ProjectRepositoryGraph {
45
+ return { projectId: snapshot.projectId, repositoryId: binding.repositoryId,
46
+ revision: "unverified", weavatrixVersion: "unknown", analysisStatus: "UNAVAILABLE",
47
+ analysisErrorCode: "ENGINE_ADMISSION_UNAVAILABLE", nodes: [], relations: [],
48
+ totalNodes: 0, totalRelations: 0, truncated: false };
49
+ }
50
+
43
51
  /** Rehydrate previously admitted Git checkouts after Engine restart. */
44
52
  export async function recoverLocalGraphBindings(
45
53
  snapshots: MeshSnapshot[],
@@ -15,3 +15,7 @@ separately from that first live publication. It does not republish an old catalo
15
15
  as fresh. The phone continues its durable historical backfill independently.
16
16
 
17
17
  Tests live in `tests/`. This runtime module uses the repository MIT License.
18
+
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.
@@ -5,6 +5,7 @@ import type { RelayClient } from "../../../../packages/core/relay-client";
5
5
  import { SessionsStatus, type SessionInfo } from "../../../../packages/protocol/schema";
6
6
  import { configDir, writePrivateFile } from "../config";
7
7
  import { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "../sessions";
8
+ export { recoverCatalogBindings } from "./startup-recovery";
8
9
 
9
10
  const catalogs = new WeakMap<RelayClient, SessionsStatus | undefined>();
10
11
  const cachePath = () => join(configDir(), "session-catalog-cache.json");
@@ -0,0 +1,12 @@
1
+ type StartupRecovery<T> = {
2
+ engineAvailable: () => Promise<boolean>;
3
+ publishCatalog: () => Promise<unknown>;
4
+ snapshots: () => Promise<T>;
5
+ recover: (snapshots: T) => Promise<unknown>;
6
+ };
7
+
8
+ /** Coordinate the initial catalog and restoration of verified Engine bindings. */
9
+ export async function recoverCatalogBindings<T>(input: StartupRecovery<T>): Promise<void> {
10
+ const [available] = await Promise.all([input.engineAvailable(), input.publishCatalog()]);
11
+ if (available) await input.recover(await input.snapshots());
12
+ }
@@ -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. */
@@ -35,7 +35,7 @@ export class DesktopTaskActivityRunner {
35
35
  || typeof taskId !== "string" || !taskId || taskId.length > 128) {
36
36
  return Promise.resolve(undefined);
37
37
  }
38
- return this.enqueue(query, storePath);
38
+ return this.enqueue({ ...query, operation: "desktop.task_activity" }, storePath);
39
39
  }
40
40
 
41
41
  usage(): Promise<unknown> {
@@ -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.49
13
+ npm install -g granttap-mcp@0.8.51
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.49\";\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.51\";\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.49";
10
+ const PACKAGE = "granttap-mcp@0.8.51";
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.49",
3
+ "version": "0.8.51",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {