granttap-mcp 0.8.120 → 0.8.121

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.
@@ -38,3 +38,8 @@ Recovery checks the private request's owning room, restores a discovered source
38
38
  inside the native Codex sessions root, and sends that exact question or its terminal
39
39
  state through the priority device channel. It does not require an admitted catalog,
40
40
  start a provider turn, send another wake notification, or schedule a full scan.
41
+
42
+ Question publication reads the private record collection once per batch, then
43
+ validates each matching question freshly before sending. Targeted notification
44
+ replay reads the requested identity directly. This keeps multiple chats from
45
+ repeatedly parsing the same collection.
@@ -13,7 +13,12 @@ const RETENTION_MS = 24 * 60 * 60_000;
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
15
  options: { replay?: boolean; peerPublicKey?: string; requestId?: string } = {}): Promise<void> {
16
- for (const session of sessions.filter(s => s.agent === 'codex')) {
16
+ const nativeSessions = sessions.filter(s => s.agent === 'codex');
17
+ if (!nativeSessions.length) return;
18
+ const initial = options.requestId ? [readQuestion(options.requestId)].filter((record): record is QuestionRecord => !!record)
19
+ : storedQuestions();
20
+ const batch = new Map(initial.map(record => [record.requestId, record]));
21
+ for (const session of nativeSessions) {
17
22
  const path = nativeTranscriptSource(session);
18
23
  if (!path) continue;
19
24
  let observed;
@@ -22,13 +27,20 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
22
27
  for (const question of observed.questions) {
23
28
  if (Date.now() - question.createdAt > RETENTION_MS) continue;
24
29
  const existing = readQuestion(question.requestId);
25
- if (!existing) saveQuestion({ ...question, room: client.room, sourcePath: path });
26
- else if (existing.room === client.room && existing.sourcePath !== path) saveQuestion({ ...existing, sourcePath: path });
30
+ if (!existing) {
31
+ const record = { ...question, room: client.room, sourcePath: path };
32
+ saveQuestion(record); batch.set(record.requestId, record);
33
+ } else {
34
+ if (existing.room === client.room && existing.sourcePath !== path) saveQuestion({ ...existing, sourcePath: path });
35
+ batch.set(existing.requestId, existing);
36
+ }
27
37
  }
28
- const records = storedQuestions().filter(q => q.room === client.room && q.sessionId === session.sessionId
38
+ const records = [...batch.values()].filter(q => q.room === client.room && q.sessionId === session.sessionId
29
39
  && (!options.requestId || q.requestId === options.requestId))
30
40
  .sort((a, b) => a.createdAt - b.createdAt || a.index - b.index);
31
- for (const record of records) {
41
+ for (const candidate of records) {
42
+ const record = readQuestion(candidate.requestId);
43
+ if (!record || record.room !== client.room || record.sessionId !== session.sessionId) continue;
32
44
  const expired = Date.now() - record.createdAt > RETENTION_MS;
33
45
  if (observed.answered.has(record.requestId) && !record.answered) {
34
46
  record.answered = true;
@@ -18,6 +18,7 @@ import { RepositoryGraphJobs } from "./repository-graph-jobs";
18
18
  import { fitRepositoryGraph } from "./repository-graph/fit";
19
19
  import { RepositoryGraphCache } from "./repository-graph/cache";
20
20
  import { repositorySourceMap } from "./repository-graph/source-map";
21
+ import { repositorySourceFingerprint } from "./repository-graph/source-state";
21
22
 
22
23
  export type LocalProjectBinding = {
23
24
  summary: ProjectBindingSummary;
@@ -193,15 +194,23 @@ export async function projectRepositoryGraphs(
193
194
  if (!engineFeatureEnabled(options.env ?? process.env)) return [];
194
195
  const client = options.client;
195
196
  const repositories = [...new Map(bindings.map((item) => [item.repositoryId, item])).values()].slice(0, 64);
197
+ const observations = await Promise.all(repositories.map(async binding => {
198
+ const fingerprint = binding.localPathHint
199
+ ? await repositorySourceFingerprint(binding.localPathHint, binding.revision, { fresh: !options.background })
200
+ : undefined;
201
+ return fingerprint ? { fingerprint, reusable: (graph: ProjectRepositoryGraph) =>
202
+ ['COMPLETE', 'INCOMPLETE'].includes(graph.analysisStatus ?? '') } : undefined;
203
+ }));
196
204
  const graphs = options.background
197
- ? repositories.map((binding) => repositoryGraphJobs.read(
205
+ ? repositories.map((binding, index) => repositoryGraphJobs.read(
198
206
  JSON.stringify([projectId, binding.repositoryId, binding.revision]),
199
207
  () => analyzeRepositoryGraph(client, projectId, binding.repositoryId, 120_000, binding.revision),
200
- options.priority ?? 0,
208
+ options.priority ?? 0, false, observations[index],
201
209
  ))
202
- : await Promise.all(repositories.map((binding) => repositoryGraphJobs.refresh(
210
+ : await Promise.all(repositories.map((binding, index) => repositoryGraphJobs.refresh(
203
211
  JSON.stringify([projectId, binding.repositoryId, binding.revision]),
204
212
  () => analyzeRepositoryGraph(client, projectId, binding.repositoryId, 120_000, binding.revision),
213
+ Number.MAX_SAFE_INTEGER, observations[index],
205
214
  )));
206
215
  return boundRepositoryGraphs(graphs.filter((item): item is ProjectRepositoryGraph => item != null));
207
216
  }
@@ -18,3 +18,9 @@ under the repository's `tests/engine` harness.
18
18
 
19
19
  License: this module is distributed under the GrantTap Commercial Source License
20
20
  in the repository-root `LICENSE` file.
21
+
22
+ Successful background reports retain an internal observation of Git-visible
23
+ source state. Unchanged state reuses the report across time and MCP restart;
24
+ changed state retains the existing analysis cooldown. Explicit refresh forces
25
+ analysis, and temporary failures keep their retry behavior. Source observation
26
+ is implemented by `source-state/`; its tests cover dirty edits and path changes.
@@ -9,6 +9,7 @@ export class RepositoryGraphCache {
9
9
  read(key: string): {
10
10
  value: ProjectRepositoryGraph;
11
11
  savedAt: number;
12
+ sourceFingerprint?: string;
12
13
  } | undefined {
13
14
  try {
14
15
  const path = this.path(key);
@@ -19,18 +20,22 @@ export class RepositoryGraphCache {
19
20
  if (record.key !== key || !Number.isFinite(record.savedAt) || !graph.success
20
21
  || !['COMPLETE', 'INCOMPLETE'].includes(graph.data.analysisStatus ?? ''))
21
22
  return undefined;
22
- return { value: graph.data, savedAt: record.savedAt };
23
+ const sourceFingerprint = typeof record.sourceFingerprint === 'string'
24
+ && /^[a-f0-9]{64}$/.test(record.sourceFingerprint) ? record.sourceFingerprint : undefined;
25
+ return { value: graph.data, savedAt: record.savedAt,
26
+ ...(sourceFingerprint ? { sourceFingerprint } : {}) };
23
27
  }
24
28
  catch {
25
29
  return undefined;
26
30
  }
27
31
  }
28
- write(key: string, value: ProjectRepositoryGraph): void {
32
+ write(key: string, value: ProjectRepositoryGraph, sourceFingerprint?: string): void {
29
33
  if (!['COMPLETE', 'INCOMPLETE'].includes(value.analysisStatus ?? ''))
30
34
  return;
31
35
  const bounded = fitRepositoryGraph(value, 192 * 1024);
32
36
  if (!bounded) return;
33
- const json = JSON.stringify({ key, value: bounded, savedAt: this.now() });
37
+ const json = JSON.stringify({ key, value: bounded, savedAt: this.now(),
38
+ ...(sourceFingerprint && /^[a-f0-9]{64}$/.test(sourceFingerprint) ? { sourceFingerprint } : {}) });
34
39
  if (Buffer.byteLength(json) > 256 * 1024)
35
40
  return;
36
41
  try {
@@ -0,0 +1,24 @@
1
+ # Repository source observation
2
+
3
+ Public entry: `index.ts`. This private machine-side observation identifies the
4
+ bound checkout, Git revision, staged and unstaged path status, and metadata of
5
+ changed or untracked files. It includes repeated edits with the same Git status,
6
+ renames, deletion and Unicode paths. Ignored build output does not invalidate it.
7
+ It reads metadata without parsing source or executing a build.
8
+
9
+ Background observations share in-flight work and a five-second observation
10
+ window. Explicit refresh bypasses it. Output, path count, filesystem concurrency
11
+ and retained observations are bounded. Unreadable or invalid state remains
12
+ unknown; it never becomes proof of unchanged source.
13
+
14
+ Repository graph jobs retain a successful report while this observation is
15
+ unchanged, including across MCP restart. A changed observation retains the
16
+ existing analysis cooldown; explicit builds still force analysis. Failed or
17
+ legacy reports retain normal retry behavior. No provider policy, grants,
18
+ repository files or user projects are changed.
19
+
20
+ Behavior tests live in `tests/` and `../tests/unchanged-source.test.ts`.
21
+
22
+ ## License
23
+
24
+ GrantTap Commercial Source License. See [LICENSE](../../../../../../../LICENSE).
@@ -0,0 +1,73 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, realpath } from 'node:fs/promises';
3
+ import { isAbsolute, resolve, sep } from 'node:path';
4
+ import { readRepositoryGit } from '../../../../mesh/catalog';
5
+
6
+ const cached = new Map<string, { at: number; value?: string }>();
7
+ const pending = new Map<string, Promise<string | undefined>>();
8
+
9
+ function changedPaths(status: string): string[] | undefined {
10
+ const fields = status.split('\0');
11
+ const paths: string[] = [];
12
+ for (let index = 0; index < fields.length; index++) {
13
+ const field = fields[index]!;
14
+ if (!field) continue;
15
+ if (field.length < 4 || field[2] !== ' ') return undefined;
16
+ paths.push(field.slice(3));
17
+ if (/[RC]/.test(field.slice(0, 2))) {
18
+ const previous = fields[++index];
19
+ if (!previous) return undefined;
20
+ paths.push(previous);
21
+ }
22
+ if (paths.length > 4096) return undefined;
23
+ }
24
+ return [...new Set(paths)].sort();
25
+ }
26
+
27
+ async function capture(root: string, revision?: string): Promise<string | undefined> {
28
+ try {
29
+ const directory = await realpath(root);
30
+ const status = await readRepositoryGit(directory,
31
+ ['status', '--porcelain=v1', '--untracked-files=all', '-z'], { timeoutMs: 5_000, maxBuffer: 4 * 1024 * 1024 });
32
+ if (status === undefined) return undefined;
33
+ const paths = changedPaths(status);
34
+ if (!paths) return undefined;
35
+ const hash = createHash('sha256').update(JSON.stringify([directory, revision])).update(status);
36
+ for (let start = 0; start < paths.length; start += 16) {
37
+ const rows = await Promise.all(paths.slice(start, start + 16).map(async path => {
38
+ const absolute = resolve(directory, path);
39
+ if (isAbsolute(path) || !absolute.startsWith(directory + sep)) throw new Error('Invalid repository path');
40
+ try {
41
+ const stat = await lstat(absolute, { bigint: true });
42
+ return [path, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs, stat.mode].map(String);
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [path, 'missing'];
45
+ throw error;
46
+ }
47
+ }));
48
+ hash.update(JSON.stringify(rows));
49
+ }
50
+ return hash.digest('hex');
51
+ } catch { return undefined; }
52
+ }
53
+
54
+ /** Observe committed and uncommitted source without parsing it or executing a build. */
55
+ export function repositorySourceFingerprint(root: string, revision?: string,
56
+ options: { fresh?: boolean } = {}): Promise<string | undefined> {
57
+ const key = JSON.stringify([root, revision]), previous = cached.get(key), now = Date.now();
58
+ if (!options.fresh && previous && now >= previous.at && now - previous.at < 5_000) {
59
+ return Promise.resolve(previous.value);
60
+ }
61
+ const active = pending.get(key);
62
+ if (!options.fresh && active) return active;
63
+ const next = capture(root, revision).then(value => {
64
+ if (pending.get(key) === next) {
65
+ cached.delete(key);
66
+ if (cached.size >= 128) cached.delete(cached.keys().next().value!);
67
+ cached.set(key, { value, at: Date.now() });
68
+ }
69
+ return value;
70
+ }).finally(() => { if (pending.get(key) === next) pending.delete(key); });
71
+ pending.set(key, next);
72
+ return next;
73
+ }
@@ -1,3 +1,4 @@
1
+ type SourceObservation<T> = { fingerprint: string; reusable?: (value: T) => boolean };
1
2
  /** Serializes expensive repository analysis without blocking Mesh snapshots. */
2
3
  export class RepositoryGraphJobs<T> {
3
4
  private readonly entries = new Map<string, {
@@ -5,6 +6,7 @@ export class RepositoryGraphJobs<T> {
5
6
  retryAt: number;
6
7
  pending: boolean;
7
8
  listeners: Array<(value: T | undefined) => void>;
9
+ sourceFingerprint?: string;
8
10
  }>();
9
11
  private readonly queue: Array<{
10
12
  key: string; run: () => Promise<T | undefined>; priority: number;
@@ -16,34 +18,50 @@ export class RepositoryGraphJobs<T> {
16
18
  private readonly successMs = 120_000,
17
19
  private readonly failureMs = 300_000,
18
20
  private readonly capacity = 32,
19
- private readonly storage?: {read(key:string):{value:T;savedAt:number}|undefined;write(key:string,value:T):void},
21
+ private readonly storage?: {read(key:string):{value:T;savedAt:number;sourceFingerprint?:string}|undefined;
22
+ write(key:string,value:T,sourceFingerprint?:string):void},
20
23
  ) {}
21
24
 
22
- read(key: string, run: () => Promise<T | undefined>, priority = 0, force = false): T | undefined {
25
+ read(key: string, run: () => Promise<T | undefined>, priority = 0, force = false,
26
+ source?: SourceObservation<T>): T | undefined {
23
27
  if (!this.entries.has(key)) {
24
28
  const saved = this.storage?.read(key);
25
29
  if (saved) this.entries.set(key, {value:saved.value,retryAt:saved.savedAt+this.successMs,
26
- pending:false,listeners:[]});
30
+ pending:false,listeners:[],sourceFingerprint:saved.sourceFingerprint});
27
31
  }
28
32
  const previous = this.entries.get(key);
29
33
  if (previous?.pending) return previous.value;
30
34
  if (previous && !force && this.now() < previous.retryAt) return previous.value;
35
+ if (!force && source && previous?.value !== undefined
36
+ && previous.sourceFingerprint === source.fingerprint && source.reusable?.(previous.value) !== false) {
37
+ return previous.value;
38
+ }
31
39
  if (!this.admit(priority)) return undefined;
32
40
  if (!previous && this.entries.size >= this.capacity * 2) {
33
41
  const oldest = [...this.entries].find(([, entry]) => !entry.pending)?.[0];
34
42
  if (!oldest) return undefined;
35
43
  this.entries.delete(oldest);
36
44
  }
37
- this.entries.set(key, { value: previous?.value, retryAt: 0, pending: true, listeners: [] });
45
+ this.entries.set(key, { value: previous?.value, retryAt: 0, pending: true, listeners: [],
46
+ sourceFingerprint: source?.fingerprint });
47
+ const admitted = this.entries.get(key);
38
48
  const before = this.queue.findIndex((job) => job.priority < priority);
39
- this.queue.splice(before < 0 ? this.queue.length : before, 0, { key, run, priority });
49
+ const observedRun = async () => {
50
+ const value = await run();
51
+ if (value !== undefined && source?.reusable?.(value) === false && this.entries.get(key) === admitted) {
52
+ admitted!.sourceFingerprint = undefined;
53
+ }
54
+ return value;
55
+ };
56
+ this.queue.splice(before < 0 ? this.queue.length : before, 0, { key, run: observedRun, priority });
40
57
  void this.drain();
41
58
  return previous?.value;
42
59
  }
43
60
 
44
61
  /** A requested build joins the same revision's automatic job and raises its queue priority. */
45
- refresh(key: string, run: () => Promise<T | undefined>, priority = Number.MAX_SAFE_INTEGER): Promise<T | undefined> {
46
- if (!this.entries.get(key)?.pending) this.read(key, run, priority, true);
62
+ refresh(key: string, run: () => Promise<T | undefined>, priority = Number.MAX_SAFE_INTEGER,
63
+ source?: SourceObservation<T>): Promise<T | undefined> {
64
+ if (!this.entries.get(key)?.pending) this.read(key, run, priority, true, source);
47
65
  const entry = this.entries.get(key);
48
66
  if (!entry?.pending) return Promise.resolve(undefined);
49
67
  const index = this.queue.findIndex(job => job.key === key);
@@ -103,12 +121,13 @@ export class RepositoryGraphJobs<T> {
103
121
  let value: T | undefined;
104
122
  try { value = await job.run(); } catch { /* Retry after backoff. */ }
105
123
  if (this.entries.get(job.key) !== entry) continue;
106
- if (value !== undefined) this.storage?.write(job.key, value);
124
+ if (value !== undefined) this.storage?.write(job.key, value, entry?.sourceFingerprint);
107
125
  this.entries.set(job.key, {
108
126
  value,
109
127
  retryAt: this.now() + (value === undefined ? this.failureMs : this.successMs),
110
128
  pending: false,
111
129
  listeners: [],
130
+ sourceFingerprint: entry?.sourceFingerprint,
112
131
  });
113
132
  for (const listener of entry?.listeners ?? []) listener(value);
114
133
  }
@@ -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.120
13
+ npm install -g granttap-mcp@0.8.121
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.120\";\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.121\";\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.120";
10
+ const PACKAGE = "granttap-mcp@0.8.121";
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.120",
3
+ "version": "0.8.121",
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": {