granttap-mcp 0.8.46 → 0.8.47

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/README.md CHANGED
@@ -17,6 +17,12 @@ adapters, and wire contracts. It powers Project Mesh, Governance, and observed
17
17
  usage while agents and provider credentials stay on your computer. Controller
18
18
  traffic is end-to-end encrypted.
19
19
 
20
+ Known chats are read from their published native identities, without another
21
+ walk through every provider log. The local helper retains a private metadata
22
+ index so a restart can send current heads of the saved Now chats before a fresh
23
+ discovery scan. The Apple clients keep downloaded conversations, pictures, and
24
+ Mesh data locally for offline use; clearing that cache preserves the account login.
25
+
20
26
  The MCP runtime, plugins, and CLI are open source under the MIT License.
21
27
  The Apple apps, Engine, relay, and website are separately licensed commercial
22
28
  products or source. Their licenses do not change this repository's MIT grant.
@@ -1,6 +1,7 @@
1
1
  /** Public entry point for local bridge configuration and pairing. */
2
2
  export * from "./access/capability-policy";
3
3
  export * from "./access/pairing";
4
+ export { writePrivateFile } from "./access/write-private";
4
5
  export * from "./runtime/paths";
5
6
  export * from "./runtime";
6
7
  export {
@@ -3,6 +3,7 @@ import type { RelayClient } from "../../../../../packages/core/relay-client";
3
3
  import { sendSessionPayload } from "../../host/session-keys";
4
4
  import { cachedSessionActivity } from "../support/session-activity";
5
5
  import { readTranscriptHistory } from "../../transcript-history";
6
+ import { publishedSession } from "../../session-catalog";
6
7
  import { activityWithImages } from '../../session-images';
7
8
  import {
8
9
  scanSessionHistory,
@@ -38,11 +39,13 @@ export function boundedCatalogHistory(
38
39
  return [...pinned, ...rest].sort((a, b) => b.lastActivityAt - a.lastActivityAt);
39
40
  }
40
41
 
41
- function resolveSession(sessionId: string, status?: SessionsStatus): SessionInfo | undefined {
42
+ function resolveSession(client: RelayClient, sessionId: string, status?: SessionsStatus): SessionInfo | undefined {
42
43
  if (status) {
43
44
  return status.sessions.find((session) => session.sessionId === sessionId)
44
45
  ?? status.history?.find((session) => session.sessionId === sessionId);
45
46
  }
47
+ const known = publishedSession(client, sessionId);
48
+ if (known) return known;
46
49
  return scanSessions().sessions.find((session) => session.sessionId === sessionId)
47
50
  ?? scanSessionHistory().find((session) => session.sessionId === sessionId);
48
51
  }
@@ -55,7 +58,7 @@ export async function publishSessionEvents(
55
58
  threadId?: string,
56
59
  history?: { history?: boolean; historyCursor?: string },
57
60
  ): Promise<boolean> {
58
- const session = resolveSession(sessionId, status);
61
+ const session = resolveSession(client, sessionId, status);
59
62
  if (!session) return false;
60
63
  const activity = threadId ? scanThreadActivity(session, threadId)
61
64
  : (history?.history ? readTranscriptHistory(session, history.historyCursor) : undefined)
@@ -21,6 +21,7 @@ import { confirmPendingController } from "../../pairing/controllers";
21
21
  import { recordControllerName } from "../../pairing/presence";
22
22
  import { startPublishLoop } from "./publish-loop";
23
23
  import { singleFlightPublisher } from "./single-flight";
24
+ import { publishedCatalog, rememberPublishedCatalog, resumePublishedChats } from "../../session-catalog";
24
25
  import { createMachineLoadPublisher } from "../../machine-load";
25
26
  import { startMachineLoadLoop } from "../../machine-load/host/loop";
26
27
  import { publishProjectPolicyStatuses } from "../../project-policy/runtime";
@@ -121,8 +122,13 @@ async function publishMonitorTick(input: {
121
122
  if (!client.isConnected) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
122
123
  await publishHeartbeat(client).catch(() => {});
123
124
  if (!leadership.acquire()) return { lastHistoryPublishedAt, lastCapabilityPublishedAt };
125
+ if (lastHistoryPublishedAt === 0) {
126
+ await resumePublishedChats(client,
127
+ (session, status) => publishSessionEvents(client, session.sessionId, status));
128
+ }
124
129
  const includeHistory = forceHistory || Date.now() - lastHistoryPublishedAt >= HISTORY_INTERVAL_MS;
125
130
  const status = monitorSnapshot({ includeHistory, forceHistory, subscriptions, history });
131
+ rememberPublishedCatalog(client, status);
126
132
  await client.send(status, "phone", { ttlMs: INTERVAL_MS * 24, reliable: false });
127
133
  // Provider review is independent of task visibility; a hung hook RPC must
128
134
  // never hold the current catalog behind its discovery timeout.
@@ -182,6 +188,7 @@ export function startSessionMonitor(client: RelayClient): SessionMonitor {
182
188
  const subscriptions = new Set<string>();
183
189
  const leadership = monitorLeadership();
184
190
  primeSessionKeys(client);
191
+ publishedCatalog(client);
185
192
  let historyCache: { generatedAt: number; sessions: SessionsStatus["sessions"] } | undefined;
186
193
  let lastHistoryPublishedAt = 0;
187
194
  let lastCapabilityPublishedAt = 0;
@@ -0,0 +1,15 @@
1
+ # Session catalog
2
+
3
+ `index.ts` keeps the native identities of the latest published catalog. Requests
4
+ for a chat already shown on the phone use that identity instead of walking every
5
+ provider directory again. A new discovery window cannot make a known chat
6
+ unreadable merely because hundreds of newer logs appeared.
7
+
8
+ The private `session-catalog-cache.json` contains metadata and validated native
9
+ source locations. It excludes messages, capability descriptions, and credentials.
10
+ The room and provider roots bind its scope. On restart the primary monitor sends
11
+ the saved Now chats' current native heads under their independent task keys before
12
+ performing an expensive fresh discovery scan. It does not republish an old catalog
13
+ as fresh. The phone continues its durable historical backfill independently.
14
+
15
+ Tests live in `tests/`. This runtime module uses the repository MIT License.
@@ -0,0 +1,68 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import type { RelayClient } from "../../../../packages/core/relay-client";
5
+ import { SessionsStatus, type SessionInfo } from "../../../../packages/protocol/schema";
6
+ import { configDir, writePrivateFile } from "../config";
7
+ import { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "../sessions";
8
+
9
+ const catalogs = new WeakMap<RelayClient, SessionsStatus | undefined>();
10
+ const cachePath = () => join(configDir(), "session-catalog-cache.json");
11
+ const scope = (client: RelayClient) => createHash("sha256")
12
+ .update([client.room, ...Object.values(nativeTranscriptRoots())].join("\0")).digest("hex");
13
+
14
+ function metadata(session: SessionInfo): SessionInfo {
15
+ const { sessionId, agent, state, startedAt, lastActivityAt, tokensSession, tokensLastTurn,
16
+ cwd, model, projectId, taskId, computerId } = session;
17
+ return { sessionId, agent, state, startedAt, lastActivityAt, tokensSession, tokensLastTurn,
18
+ cwd, model, projectId, taskId, computerId };
19
+ }
20
+
21
+ /** Keep native identity, not capability descriptors or message contents. */
22
+ export function rememberPublishedCatalog(client: RelayClient, status: SessionsStatus): void {
23
+ const compact: SessionsStatus = { type: "sessions.status", machine: status.machine,
24
+ sessions: status.sessions.map(metadata), history: status.history?.map(metadata),
25
+ generatedAt: status.generatedAt };
26
+ catalogs.set(client, compact);
27
+ const paths = Object.fromEntries([...compact.sessions, ...(compact.history ?? [])]
28
+ .flatMap(session => { const path = nativeTranscriptSource(session);
29
+ return path ? [[session.sessionId, path]] : []; }));
30
+ try { writePrivateFile(cachePath(), JSON.stringify({ scope: scope(client), status: compact, paths })); }
31
+ catch { /* The current in-memory catalog remains readable on a full disk. */ }
32
+ }
33
+
34
+ export function publishedCatalog(client: RelayClient): SessionsStatus | undefined {
35
+ if (catalogs.has(client)) return catalogs.get(client);
36
+ let catalog: SessionsStatus | undefined;
37
+ try {
38
+ if (statSync(cachePath()).size > 1024 * 1024) throw new Error("catalog cache exceeds limit");
39
+ const record = JSON.parse(readFileSync(cachePath(), "utf8"));
40
+ const parsed = SessionsStatus.safeParse(record.status);
41
+ if (record.scope === scope(client) && parsed.success) {
42
+ catalog = parsed.data;
43
+ for (const session of [...catalog.sessions, ...(catalog.history ?? [])]) {
44
+ const path = record.paths?.[session.sessionId];
45
+ if (typeof path === "string") restoreNativeTranscriptSource(session, path);
46
+ }
47
+ }
48
+ } catch { /* Missing, malformed, or foreign catalogs fall back to discovery. */ }
49
+ catalogs.set(client, catalog);
50
+ return catalog;
51
+ }
52
+
53
+ export function publishedSession(client: RelayClient, sessionId: string): SessionInfo | undefined {
54
+ const status = publishedCatalog(client);
55
+ return status?.sessions.find(session => session.sessionId === sessionId)
56
+ ?? status?.history?.find(session => session.sessionId === sessionId);
57
+ }
58
+
59
+ /** Send saved Now heads under their task keys before an expensive fresh scan. */
60
+ export async function resumePublishedChats(client: RelayClient,
61
+ publish: (session: SessionInfo, status: SessionsStatus) => Promise<unknown>): Promise<void> {
62
+ const status = publishedCatalog(client);
63
+ if (!status) return;
64
+ for (const session of status.sessions) {
65
+ if (session.agent !== "codex" && session.agent !== "claude") continue;
66
+ try { await publish(session, status); } catch { /* Retry on the regular tick. */ }
67
+ }
68
+ }
@@ -50,6 +50,7 @@ import { loadRuntimeConfig } from "../config";
50
50
  import { timedProviderScan } from "../machine-load/mcp/scan-cost";
51
51
 
52
52
  export { MAX_ACTIVITY_ENTRIES, MAX_ACTIVITY_TEXT, TOKEN_WINDOW_HOURS };
53
+ export { nativeTranscriptRoots, nativeTranscriptSource, restoreNativeTranscriptSource } from "./native-source";
53
54
 
54
55
  type ProviderScan = ReturnType<typeof scanClaude>;
55
56
 
@@ -0,0 +1,35 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import { basename, isAbsolute, relative } from "node:path";
3
+ import type { SessionInfo } from "../../../../packages/protocol/schema";
4
+ import { claudeLogPathBySession } from "./scan/claude/shared";
5
+ import { codexLogPathBySession } from "./scan/codex/shared";
6
+ import { claudeProjectsRoot, codexSessionsRoot } from "./support/common";
7
+
8
+ export function nativeTranscriptRoots(): { claude: string; codex: string } {
9
+ return { claude: claudeProjectsRoot(), codex: codexSessionsRoot() };
10
+ }
11
+
12
+ /** Native paths come only from discovery; a phone never supplies one. */
13
+ export function nativeTranscriptSource(session: SessionInfo): string | undefined {
14
+ if (session.agent === "claude") return claudeLogPathBySession.get(session.sessionId);
15
+ if (session.agent === "codex") return codexLogPathBySession.get(session.sessionId);
16
+ return undefined;
17
+ }
18
+
19
+ /** Restore an owned native source without walking every provider's logs. */
20
+ export function restoreNativeTranscriptSource(session: SessionInfo, path: string): boolean {
21
+ if (!["claude", "codex"].includes(session.agent)
22
+ || !/^[a-zA-Z0-9-]{1,128}$/.test(session.sessionId)) return false;
23
+ const name = basename(path);
24
+ if (session.agent === "claude" ? name !== `${session.sessionId}.jsonl`
25
+ : !name.startsWith("rollout-") || !name.endsWith(`-${session.sessionId}.jsonl`)) return false;
26
+ try {
27
+ if (!lstatSync(path).isFile()) return false;
28
+ const root = session.agent === "claude" ? claudeProjectsRoot() : codexSessionsRoot();
29
+ const inside = relative(realpathSync(root), realpathSync(path));
30
+ if (!inside || inside.startsWith("..") || isAbsolute(inside)) return false;
31
+ const index = session.agent === "claude" ? claudeLogPathBySession : codexLogPathBySession;
32
+ index.set(session.sessionId, path);
33
+ return true;
34
+ } catch { return false; }
35
+ }
@@ -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.46
13
+ npm install -g granttap-mcp@0.8.47
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.46\";\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.47\";\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.46";
10
+ const PACKAGE = "granttap-mcp@0.8.47";
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.46",
3
+ "version": "0.8.47",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {