granttap-mcp 0.8.116 → 0.8.118

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,3 +18,7 @@ do not borrow an implementation's description or icon. These are display data,
18
18
  not instructions or evidence that a capability was used.
19
19
 
20
20
  License: this module is distributed under the GrantTap Commercial Source License in the repository-root `LICENSE` file.
21
+
22
+ Catalog publication warms native Codex MCP configuration asynchronously through
23
+ `refreshCodexMcpDescriptors`, preserving its existing retry window and last known
24
+ rows while allowing request-delivery callbacks to run during a slow native query.
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { execFileSync } from "node:child_process";
2
+ import { execFile, execFileSync } from "node:child_process";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
@@ -65,6 +65,28 @@ function codexMcpDescriptors(): McpDescriptor[] {
65
65
  .sort((a, b) => a.name.localeCompare(b.name));
66
66
  }
67
67
 
68
+ let codexRefresh: Promise<void> | undefined;
69
+
70
+ /** Warm native configuration asynchronously before a catalog consumes the synchronous projection. */
71
+ export function refreshCodexMcpDescriptors(): Promise<void> {
72
+ if (codexCache && Date.now() - codexCache.at < CACHE_MS) return Promise.resolve();
73
+ if (codexRefresh) return codexRefresh;
74
+ codexRefresh = new Promise<void>(resolve => {
75
+ const finish = (error: Error | null, output = '') => {
76
+ let rows = codexCache?.rows ?? [];
77
+ if (!error) {
78
+ try { const parsed = JSON.parse(output); if (Array.isArray(parsed)) rows = parsed; } catch { /* Keep last known rows. */ }
79
+ }
80
+ codexCache = { at: Date.now(), rows };
81
+ resolve();
82
+ };
83
+ try { execFile(resolveCodexBinary(), ['mcp', 'list', '--json'],
84
+ { encoding: 'utf8', timeout: 15_000, maxBuffer: 4 * 1024 * 1024 }, finish); }
85
+ catch { finish(new Error('Native configuration is unavailable.')); }
86
+ }).finally(() => { codexRefresh = undefined; });
87
+ return codexRefresh;
88
+ }
89
+
68
90
  function codexMcpRows(): CodexMcpRow[] {
69
91
  if (codexCache && Date.now() - codexCache.at < CACHE_MS) return codexCache.rows;
70
92
  try {
@@ -41,3 +41,5 @@ function mcpServersFromDescriptors(
41
41
  return metadata ? { ...base, ...metadata } : base;
42
42
  });
43
43
  }
44
+
45
+ export { refreshCodexMcpDescriptors } from "./descriptors";
@@ -32,3 +32,9 @@ answer, image and file payload together; ordinary encrypted attachment transport
32
32
  Task policy, native-owner admission and stable question idempotency still apply.
33
33
  Legacy text-only replies keep their existing contract. Files remain available
34
34
  for the admitted native turn; another phone cannot start a second answer.
35
+
36
+ Notification refresh can name one request through `sessions.refresh.notificationRequestId`.
37
+ Recovery checks the private request's owning room, restores a discovered source only
38
+ inside the native Codex sessions root, and sends that exact question or its terminal
39
+ state through the priority device channel. It does not require an admitted catalog,
40
+ start a provider turn, send another wake notification, or schedule a full scan.
@@ -1,6 +1,6 @@
1
1
  import type { RelayClient } from '../../../../packages/core/relay-client';
2
2
  import type { SessionInfo, UserMessage } from '../../../../packages/protocol/schema';
3
- import { nativeTranscriptSource } from '../sessions';
3
+ import { nativeTranscriptSource, restoreNativeTranscriptSource } from '../sessions';
4
4
  import { handleInboundUserMessage } from '../monitor/message-delivery';
5
5
  import { sendDeliveryReceipt } from '../monitor/handlers/receipts';
6
6
  import { nativeAnswerEnvelope } from './observations';
@@ -12,7 +12,7 @@ const RETENTION_MS = 24 * 60 * 60_000;
12
12
 
13
13
  /** Phone questions travel as device-encrypted control events, independently of task-key grants. */
14
14
  export async function publishCodexQuestions(client: RelayClient, sessions: SessionInfo[],
15
- options: { replay?: boolean; peerPublicKey?: string } = {}): Promise<void> {
15
+ options: { replay?: boolean; peerPublicKey?: string; requestId?: string } = {}): Promise<void> {
16
16
  for (const session of sessions.filter(s => s.agent === 'codex')) {
17
17
  const path = nativeTranscriptSource(session);
18
18
  if (!path) continue;
@@ -22,9 +22,11 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
22
22
  for (const question of observed.questions) {
23
23
  if (Date.now() - question.createdAt > RETENTION_MS) continue;
24
24
  const existing = readQuestion(question.requestId);
25
- if (!existing) saveQuestion({ ...question, room: client.room });
25
+ if (!existing) saveQuestion({ ...question, room: client.room, sourcePath: path });
26
+ else if (existing.room === client.room && existing.sourcePath !== path) saveQuestion({ ...existing, sourcePath: path });
26
27
  }
27
- const records = storedQuestions().filter(q => q.room === client.room && q.sessionId === session.sessionId)
28
+ const records = storedQuestions().filter(q => q.room === client.room && q.sessionId === session.sessionId
29
+ && (!options.requestId || q.requestId === options.requestId))
28
30
  .sort((a, b) => a.createdAt - b.createdAt || a.index - b.index);
29
31
  for (const record of records) {
30
32
  const expired = Date.now() - record.createdAt > RETENTION_MS;
@@ -44,7 +46,7 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
44
46
  sessionId: session.sessionId, agent: 'codex', projectId: session.projectId,
45
47
  taskId: session.taskId, computerId: session.computerId, text, createdAt: record.createdAt },
46
48
  'phone', { ttlMs: Math.max(1, record.createdAt + RETENTION_MS - Date.now()),
47
- wake: !record.announced, peerPublicKey: options.peerPublicKey,
49
+ wake: !record.announced, urgent: true, peerPublicKey: options.peerPublicKey,
48
50
  deliveryId: 'native-question-' + record.requestId });
49
51
  saveQuestion({ ...record, announced: true });
50
52
  }
@@ -52,6 +54,20 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
52
54
  }
53
55
  }
54
56
 
57
+ /** Restore one notification from private owned records; catalog discovery is not a prerequisite. */
58
+ export async function replayCodexQuestion(client: RelayClient, requestId: string, peerPublicKey?: string): Promise<boolean> {
59
+ if (!peerPublicKey) return false;
60
+ const record = readQuestion(requestId);
61
+ if (!record || record.room !== client.room) return false;
62
+ const identity = { sessionId: record.sessionId, agent: 'codex' as const };
63
+ if (!nativeTranscriptSource(identity)
64
+ && (!record.sourcePath || !restoreNativeTranscriptSource(identity, record.sourcePath))) return false;
65
+ const session: SessionInfo = { ...identity, state: 'idle', startedAt: record.createdAt,
66
+ lastActivityAt: record.createdAt, tokensSession: 0, tokensLastTurn: 0 };
67
+ await publishCodexQuestions(client, [session], { replay: true, peerPublicKey, requestId });
68
+ return true;
69
+ }
70
+
55
71
  /** Reuse ordinary scoped delivery, policy and receipts; only the human's explicit answer is wrapped. */
56
72
  export async function handleCodexQuestionReply(client: RelayClient, message: UserMessage,
57
73
  publish: () => Promise<void>, peerPublicKey?: string,
@@ -7,6 +7,7 @@ import { questionId } from './observations';
7
7
  const Record = z.object({ requestId: z.string(), sessionId: z.string(), callId: z.string(),
8
8
  index: z.number().int().min(0).max(3), question: z.string().max(2_000),
9
9
  options: z.array(z.string().max(512)).max(8), createdAt: z.number(), room: z.string(),
10
+ sourcePath: z.string().min(1).max(4096).optional(),
10
11
  announced: z.boolean().optional(), answered: z.boolean().optional(), resolvedSent: z.boolean().optional() });
11
12
  export type QuestionRecord = z.infer<typeof Record>;
12
13
  const directory = () => join(configDir(), 'codex-question-records');
@@ -20,7 +20,10 @@ 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 } from "./repository-inspection";
23
+ export { inspectRepositoryAsync, inspectWorkingTreeAsync } from "./repository-inspection";
24
+ export { prepareMonitorInspection } from "./monitor-inspection";
25
+ import type { CatalogInspection } from "./monitor-inspection";
26
+ export type { CatalogInspection } from "./monitor-inspection";
24
27
 
25
28
  export type RepositoryFacts = {
26
29
  root: string;
@@ -120,7 +123,7 @@ export function linkSessionsToProjects(
120
123
  store: MeshStore,
121
124
  sessions: SessionInfo[],
122
125
  computerId: string,
123
- inspect: (cwd: string) => RepositoryFacts = inspectRepository,
126
+ inspect: ((cwd: string) => RepositoryFacts) | CatalogInspection = inspectRepository,
124
127
  formerNames: string[] = formerComputerNames(),
125
128
  ): SessionInfo[] {
126
129
  localTaskTransfers().reconcileCatalogOwnership(store, computerId);
@@ -143,13 +146,14 @@ export function linkSessionsToProjects(
143
146
  const repositories = new Map<string, RepositoryFacts>();
144
147
  const workingTrees = new Map<string, boolean | undefined>();
145
148
  const inspectWorkingTree = (cwd: string): boolean | undefined => {
146
- if (!workingTrees.has(cwd)) workingTrees.set(cwd, hasUncommittedWork(cwd));
149
+ if (!workingTrees.has(cwd)) workingTrees.set(cwd, typeof inspect === "function"
150
+ ? hasUncommittedWork(cwd) : inspect.workingTree(cwd));
147
151
  return workingTrees.get(cwd);
148
152
  };
149
153
  const inspectOnce = (cwd: string): RepositoryFacts => {
150
154
  let facts = repositories.get(cwd);
151
155
  if (!facts) {
152
- facts = inspect(cwd);
156
+ facts = typeof inspect === "function" ? inspect(cwd) : inspect.repository(cwd);
153
157
  repositories.set(cwd, facts);
154
158
  }
155
159
  return facts;
@@ -0,0 +1,14 @@
1
+ # Monitor repository inspection
2
+
3
+ Public entry: `prepareMonitorInspection` through the catalog entry point. The
4
+ monitor prepares Git observations asynchronously with the existing two-command
5
+ pool. Each catalog reads uncommitted work afresh. Only stable repository metadata
6
+ is reused for at most 30 seconds; missing repositories retry after five seconds.
7
+ An explicit refresh bypasses reuse. Retired directories are removed, older
8
+ in-flight results cannot replace a forced refresh, and cache size is bounded.
9
+
10
+ These are catalog observations. Explicit Git and Task transfer operations retain
11
+ their fresh native inspection and never use this observation cache as authority.
12
+ Tests cover fresh unsaved work, forced refresh, expiry, retirement and ordering.
13
+
14
+ License: GrantTap Commercial Source License in the repository LICENSE.
@@ -0,0 +1,48 @@
1
+ import { inspectRepositoryAsync, inspectWorkingTreeAsync } from '../repository-inspection';
2
+ import { inspectRepository, hasUncommittedWork, type RepositoryFacts } from '..';
3
+
4
+ type Readers = { repository: (cwd: string) => Promise<RepositoryFacts>;
5
+ workingTree: (cwd: string) => Promise<boolean | undefined>; now: () => number };
6
+ export type CatalogInspection = { repository: (cwd: string) => RepositoryFacts;
7
+ workingTree: (cwd: string) => boolean | undefined };
8
+ type Observation = { facts: RepositoryFacts; observedAt: number; expiresAt: number };
9
+
10
+ /** Observation only. Explicit Git and transfer operations continue to inspect native state freshly. */
11
+ export function createMonitorInspection(readers: Readers = {
12
+ repository: inspectRepositoryAsync, workingTree: inspectWorkingTreeAsync, now: Date.now,
13
+ }) {
14
+ const metadata = new Map<string, Observation>();
15
+ const pending = new Map<string, Promise<RepositoryFacts>>();
16
+ let admitted = new Set<string>();
17
+ const repository = (cwd: string, force: boolean): Promise<RepositoryFacts> => {
18
+ const cached = metadata.get(cwd);
19
+ if (!force && cached && readers.now() >= cached.observedAt && readers.now() < cached.expiresAt) return Promise.resolve(cached.facts);
20
+ const active = pending.get(cwd);
21
+ if (!force && active) return active;
22
+ const next = readers.repository(cwd).then(facts => {
23
+ const observed = Object.freeze({ ...facts }), now = readers.now();
24
+ if (pending.get(cwd) === next && admitted.has(cwd) && (metadata.has(cwd) || metadata.size < 512)) {
25
+ metadata.set(cwd, { facts: observed, observedAt: now, expiresAt: now + (facts.worktree ? 30_000 : 5_000) });
26
+ }
27
+ return observed;
28
+ }).finally(() => { if (pending.get(cwd) === next) pending.delete(cwd); });
29
+ pending.set(cwd, next);
30
+ return next;
31
+ };
32
+ return async (directories: string[], force = false): Promise<CatalogInspection> => {
33
+ admitted = new Set(directories);
34
+ for (const cwd of metadata.keys()) if (!admitted.has(cwd)) metadata.delete(cwd);
35
+ const facts = new Map(await Promise.all([...admitted].map(async cwd => [cwd, await repository(cwd, force)] as const)));
36
+ const roots = new Set([...facts.values()].flatMap(value => value.worktree ? [value.worktree] : []));
37
+ const workingTrees = new Map(await Promise.all([...roots].map(async root => [root, await readers.workingTree(root)] as const)));
38
+ const values = new Map([...facts].map(([cwd, value]) => [cwd, { repository: value,
39
+ workingTree: value.worktree ? workingTrees.get(value.worktree) : undefined }] as const));
40
+ for (const value of [...values.values()]) {
41
+ if (value.repository.worktree && !values.has(value.repository.worktree)) values.set(value.repository.worktree, value);
42
+ }
43
+ return { repository: cwd => values.get(cwd)?.repository ?? inspectRepository(cwd),
44
+ workingTree: cwd => values.has(cwd) ? values.get(cwd)!.workingTree : hasUncommittedWork(cwd) };
45
+ };
46
+ }
47
+
48
+ export const prepareMonitorInspection = createMonitorInspection();
@@ -7,18 +7,18 @@ 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[]): Promise<string | undefined> {
10
+ function git(cwd: string, args: string[], status = false): Promise<string | undefined> {
11
11
  return new Promise(resolve => {
12
12
  const run = () => {
13
13
  running++;
14
14
  const done = (error: Error | null, stdout = "") => {
15
15
  running--;
16
16
  waiting.shift()?.();
17
- resolve(error ? undefined : stdout.trim() || undefined);
17
+ resolve(error ? undefined : status ? stdout : stdout.trim() || undefined);
18
18
  };
19
19
  try {
20
20
  execFile("git", ["-C", cwd, ...args], {
21
- encoding: "utf8", timeout: 2_000, maxBuffer: 1024 * 1024, windowsHide: true,
21
+ encoding: "utf8", timeout: status ? 5_000 : 2_000, maxBuffer: (status ? 4 : 1) * 1024 * 1024, windowsHide: true,
22
22
  }, done);
23
23
  } catch { done(new Error("Repository command could not start")); }
24
24
  };
@@ -47,3 +47,9 @@ export function inspectRepositoryAsync(cwd: string): Promise<RepositoryFacts> {
47
47
  pending.set(cwd, next);
48
48
  return next;
49
49
  }
50
+
51
+ /** A fresh working-tree observation shares the bounded, nonblocking Git command pool. */
52
+ export async function inspectWorkingTreeAsync(cwd: string): Promise<boolean | undefined> {
53
+ const output = await git(cwd, ['status', '--porcelain=v1', '--untracked-files=normal', '-z'], true);
54
+ return output === undefined ? undefined : output.length > 0;
55
+ }
@@ -21,7 +21,7 @@ import {
21
21
  } from "../../reply";
22
22
  import { isProviderEnabled } from "../../config/runtime";
23
23
  import { sendMeshPayload } from "../../host/session-keys";
24
- import { linkSessionsToProjects } from "../catalog";
24
+ import { linkSessionsToProjects, type CatalogInspection } from "../catalog";
25
25
  import { catalogFromEndpoint } from "../catalog/models";
26
26
  import { saveProjectCapabilityRequest } from "../catalog/project/requests";
27
27
  import { computerId } from "../identity/computer";
@@ -84,8 +84,8 @@ const defaultDependencies: MeshRuntimeDependencies = {
84
84
  export function createMeshRuntime(deps: MeshRuntimeDependencies) {
85
85
  const flow = createHandoffFlow(deps);
86
86
  const { capsulePrompt, acceptHandoff, answerAgentQuestion } = flow;
87
- const catalog = (sessions: SessionInfo[]) =>
88
- linkSessionsToProjects(deps.store(), sessions, deps.computer());
87
+ const catalog = (sessions: SessionInfo[], inspection?: CatalogInspection) =>
88
+ linkSessionsToProjects(deps.store(), sessions, deps.computer(), inspection);
89
89
 
90
90
 
91
91
  return {
@@ -165,8 +165,8 @@ export function createLocalMeshRuntime(overrides: Partial<MeshRuntimeDependencie
165
165
 
166
166
  const defaultRuntime = createLocalMeshRuntime();
167
167
 
168
- export function meshCatalog(sessions: SessionInfo[]): SessionInfo[] {
169
- return defaultRuntime.catalog(sessions);
168
+ export function meshCatalog(sessions: SessionInfo[], inspection?: CatalogInspection): SessionInfo[] {
169
+ return defaultRuntime.catalog(sessions, inspection);
170
170
  }
171
171
 
172
172
  export function meshSnapshots(): MeshSnapshot[] {
@@ -47,7 +47,7 @@ import { handleControllerPairRequest, handleInboundKnowledgeWrite } from "../con
47
47
  import { acceptPhoneAccountLink } from "../../account-recovery/phone-link";
48
48
  import { approvalsStatus } from '../../approvals/state';
49
49
  import { handleDeliveryReceiptQuery } from '../message-delivery/status-query';
50
- import { handleCodexQuestionReply, publishCodexQuestions } from '../../codex-questions';
50
+ import { handleCodexQuestionReply, publishCodexQuestions, replayCodexQuestion } from '../../codex-questions';
51
51
  import { publishedCatalog } from '../../session-catalog';
52
52
 
53
53
  export async function handleMonitorMessage(
@@ -205,6 +205,10 @@ async function handleInboundRefresh(
205
205
  // Notification actions must not wait behind provider scans or graph analysis.
206
206
  await client.send(approvalsStatus(), 'phone', { peerPublicKey, reliable: false, ttlMs: 90_000 })
207
207
  .catch(() => {});
208
+ if (payload.notificationRequestId) {
209
+ await replayCodexQuestion(client, payload.notificationRequestId, peerPublicKey).catch(() => false);
210
+ return true;
211
+ }
208
212
  const known = publishedCatalog(client);
209
213
  if (known) await publishCodexQuestions(client, [...known.sessions, ...(known.history ?? [])],
210
214
  { replay: true, peerPublicKey }).catch(() => {});
@@ -7,7 +7,8 @@ import { RelayClient } from "../../../../../packages/core/relay-client";
7
7
  import type { Payload, SessionInfo, SessionsStatus } from "../../../../../packages/protocol/schema";
8
8
  import { loadRuntimeConfig, autoAcceptLevelFor } from "../../config";
9
9
  import { monitorLeadership } from "./leadership";
10
- import { mcpServersForSession, workspaceSkills } from "../../capabilities";
10
+ import { mcpServersForSession, workspaceSkills, refreshCodexMcpDescriptors } from "../../capabilities";
11
+ import { prepareMonitorInspection, type CatalogInspection } from "../../mesh/catalog";
11
12
  import { inspectAgentIntegrations } from "../../install";
12
13
  import { withCloudRunners } from "../../cloud-runners";
13
14
  import { currentConfigRevision } from "../../config/commands";
@@ -48,12 +49,13 @@ export type SessionMonitor = {
48
49
  function decorateSessions(
49
50
  sessions: SessionsStatus["sessions"],
50
51
  subscriptions: Set<string>,
52
+ inspection?: CatalogInspection,
51
53
  ): SessionsStatus["sessions"] {
52
54
  const runtime = loadRuntimeConfig();
53
55
  const visible = sessions.filter((session) =>
54
56
  runtime.providerSettings[session.agent as keyof typeof runtime.providerSettings] !== false
55
57
  );
56
- const coordinated = runtime.meshEnabled ? meshCatalog(visible) : visible;
58
+ const coordinated = runtime.meshEnabled ? meshCatalog(visible, inspection) : visible;
57
59
  return coordinated.map((session) => {
58
60
  const base: SessionInfo = {
59
61
  ...session,
@@ -88,13 +90,17 @@ async function monitorSnapshot(input: {
88
90
  if (loadRuntimeConfig().meshEnabled) localMeshStore().flush();
89
91
  const { sessions, tokensRecent } = await scanSessionsCooperatively();
90
92
  const runtime = loadRuntimeConfig();
93
+ const historical = includeHistory ? boundedCatalogHistory(await history(forceHistory), subscriptions) : undefined;
94
+ const all = [...sessions, ...(historical ?? [])].filter(session =>
95
+ runtime.providerSettings[session.agent as keyof typeof runtime.providerSettings] !== false);
96
+ if (all.some(session => session.agent === 'codex')) await refreshCodexMcpDescriptors();
97
+ const directories = all.flatMap(session => [session.worktree, session.cwd].filter((path): path is string => !!path));
98
+ const inspection = runtime.meshEnabled ? await prepareMonitorInspection(directories, forceHistory) : undefined;
91
99
  return {
92
100
  type: "sessions.status",
93
101
  machine: computerDisplayName(),
94
- sessions: decorateSessions(sessions, subscriptions),
95
- history: includeHistory
96
- ? decorateSessions(boundedCatalogHistory(await history(forceHistory), subscriptions), subscriptions)
97
- : undefined,
102
+ sessions: decorateSessions(sessions, subscriptions, inspection),
103
+ history: historical ? decorateSessions(historical, subscriptions, inspection) : undefined,
98
104
  tokensRecent,
99
105
  tokenWindowHours: TOKEN_WINDOW_HOURS,
100
106
  tokensAllTime: tokensRecent,
@@ -17,7 +17,7 @@ export function nativeTranscriptSource(session: Pick<SessionInfo, 'agent' | 'ses
17
17
  }
18
18
 
19
19
  /** Restore an owned native source without walking every provider's logs. */
20
- export function restoreNativeTranscriptSource(session: SessionInfo, path: string): boolean {
20
+ export function restoreNativeTranscriptSource(session: Pick<SessionInfo, 'agent' | 'sessionId'>, path: string): boolean {
21
21
  if (!["claude", "codex"].includes(session.agent)
22
22
  || !/^[a-zA-Z0-9-]{1,128}$/.test(session.sessionId)) return false;
23
23
  const name = basename(path);
@@ -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.116
13
+ npm install -g granttap-mcp@0.8.118
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.116\";\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.118\";\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.116";
10
+ const PACKAGE = "granttap-mcp@0.8.118";
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.116",
3
+ "version": "0.8.118",
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": {
@@ -161,5 +161,6 @@ export const SessionsRefresh = z.object({
161
161
  createdAt: z.number(),
162
162
  graphProjectId: z.string().min(1).max(128).optional(),
163
163
  graphAnalysisRequestId: z.string().min(1).max(128).optional(),
164
+ notificationRequestId: z.string().min(1).max(256).optional(),
164
165
  });
165
166
  export type SessionsRefresh = z.infer<typeof SessionsRefresh>;