granttap-mcp 0.8.87 → 0.8.88

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
@@ -155,7 +155,8 @@ device pairing without a passkey account.
155
155
 
156
156
  - Claude Code and Codex: primary local control and continuation paths.
157
157
  - Cursor (Beta): Task visibility and supported local controls, with narrower coverage.
158
- - Grok Build: observable work where the installed runtime exposes it.
158
+ - Grok Build: local history and headless follow-ups through an installed,
159
+ authenticated CLI; provider hook and scoped Mesh coverage remain narrower.
159
160
 
160
161
  GrantTap reports the depth each provider actually exposes. Visibility does not
161
162
  imply deterministic remote blocking or full mobile continuation.
@@ -477,6 +478,13 @@ grok plugin marketplace add sergii-ziborov/granttap-mcp
477
478
  grok plugin install granttap --trust
478
479
  ```
479
480
 
481
+ In Grok, open `/mcps` and authenticate the GrantTap server, then run
482
+ `/granttap-connect`. GrantTap's phone pairing and Grok's permission to use that
483
+ MCP server are separate. An existing pairing does not authorize a new MCP client.
484
+ If a Grok session does not start responding within one minute, GrantTap asks you
485
+ to inspect the original task before retrying. A turn that has started can keep
486
+ running; its later failure does not mean that the message was never delivered.
487
+
480
488
  For Cursor, use the reviewed **GrantTap** Marketplace listing when it is visible
481
489
  to your account. Reload Cursor. On this computer, run `granttap setup`. Pair
482
490
  and change settings on [granttap.com/connect](https://granttap.com/connect)
@@ -98,20 +98,20 @@ async function startNewAgentTask(input: {
98
98
  }
99
99
  const settle = async (result: ReplyResult, late: boolean) => {
100
100
  input.origin?.registry.complete(input.origin.scope,
101
- { ok: result.ok, sessionId: result.ok ? result.sessionId : undefined });
102
- if (!result.ok && message.messageId) {
101
+ { ok: result.ok || result.delivery === 'native', sessionId: result.ok ? result.sessionId : undefined });
102
+ if (!result.ok && message.messageId && result.delivery !== 'native') {
103
103
  if (late) rejectCompletedDelivery(message.messageId, result.error);
104
104
  await sendDeliveryReceipt(client, message.messageId, 'rejected', result.error,
105
105
  undefined, input.peerPublicKey);
106
106
  }
107
107
  return result.ok ? say(result.text, result.sessionId, true)
108
- : say(late ? `The ${displayName} task stopped: ${result.error}`
108
+ : say(late || result.delivery === 'native' ? `The ${displayName} task stopped: ${result.error}`
109
109
  : `Could not create a ${displayName} task: ${result.error}`, undefined, true);
110
110
  };
111
111
  const early = await acceptWhenStarted(run, (result) => settle(result, true));
112
112
  if (early === "started") return "accepted";
113
113
  await settle(early, false);
114
- return early.ok ? "accepted" : "rejected";
114
+ return early.ok || early.delivery === 'native' ? "accepted" : "rejected";
115
115
  }
116
116
 
117
117
  export async function handleUserMessage(
@@ -196,15 +196,18 @@ export async function handleUserMessage(
196
196
  }
197
197
  if (result.ok) await say(result.text, result.sessionId ?? target.sessionId, true);
198
198
  else {
199
- if (late && message.messageId) rejectCompletedDelivery(message.messageId, result.error);
200
- if (message.messageId) await sendDeliveryReceipt(client, message.messageId, 'rejected',
201
- result.error, target.sessionId, peerPublicKey);
202
- await say(late ? `The run stopped: ${result.error}` : `Could not deliver the message: ${result.error}`,
199
+ if (result.delivery !== 'native') {
200
+ if (late && message.messageId) rejectCompletedDelivery(message.messageId, result.error);
201
+ if (message.messageId) await sendDeliveryReceipt(client, message.messageId, 'rejected',
202
+ result.error, target.sessionId, peerPublicKey);
203
+ }
204
+ await say(late || result.delivery === 'native' ? `The run stopped: ${result.error}`
205
+ : `Could not deliver the message: ${result.error}`,
203
206
  target.sessionId, true);
204
207
  }
205
208
  };
206
209
  const early = await acceptWhenStarted(run, (result) => settle(result, true));
207
210
  if (early === "started") return "accepted";
208
211
  await settle(early, false);
209
- return early.ok ? "accepted" : "rejected";
212
+ return early.ok || early.delivery === 'native' ? "accepted" : "rejected";
210
213
  }
@@ -0,0 +1,20 @@
1
+ # Grok response admission
2
+
3
+ [`index.ts`](index.ts) reads Grok Build's `streaming-json` response contract.
4
+ Actual text, thinking, or a started tool call confirms that the prompt reached
5
+ execution. Startup metadata alone does not establish admission. Native error
6
+ messages remain failures and metadata is never returned as answer text.
7
+
8
+ Opening a session has a 60-second response-activity deadline, separate from the
9
+ long execution budget. A stalled opening stops only the process group GrantTap
10
+ started and asks the person to inspect the native task before retrying. Once
11
+ response activity is witnessed, a later timeout or run failure cannot rewrite
12
+ the accepted message as a rejected delivery. Execution failure still reaches
13
+ the chat. This does not grant tools, authenticate MCP, or alter Grok policy.
14
+
15
+ Behavior tests live in `../tests/grok-response.test.ts` and
16
+ `../tests/admission-deadline.test.ts`; durable receipt tests live in
17
+ `../../monitor/message-delivery/tests/run-outcome.test.ts`.
18
+
19
+ License: this module is distributed under the GrantTap Commercial Source License
20
+ in the repository-root `LICENSE` file.
@@ -0,0 +1,48 @@
1
+ import type { ReplyResult } from '../payload/types';
2
+ import type { ProviderAdmission } from '../process';
3
+
4
+ function events(stdout: string): Record<string, unknown>[] {
5
+ return stdout.split('\n').flatMap(line => {
6
+ try {
7
+ const value: unknown = JSON.parse(line);
8
+ return value && typeof value === 'object' && !Array.isArray(value)
9
+ ? [value as Record<string, unknown>] : [];
10
+ } catch { return []; }
11
+ });
12
+ }
13
+
14
+ /** Response activity proves admission; startup lists and diagnostic text do not. */
15
+ export function grokAccepted(stdout: string): boolean {
16
+ return events(stdout).some(event => {
17
+ if (['text', 'thought'].includes(String(event.type))) {
18
+ return typeof event.data === 'string' && !!event.data.trim();
19
+ }
20
+ if (event.type === 'result') return typeof event.result === 'string' && !!event.result.trim();
21
+ return event.type === 'tool_call' && event.status === 'in_progress'
22
+ && typeof event.toolCallId === 'string' && !!event.toolCallId;
23
+ });
24
+ }
25
+
26
+ /** Opening a native session has a separate limit from executing its long turn. */
27
+ export const grokAdmission: ProviderAdmission = {
28
+ confirmed: grokAccepted,
29
+ withinMs: 60_000,
30
+ timeoutMessage: 'Grok did not confirm receipt within 60s. Open the task before retrying.',
31
+ };
32
+
33
+ export function parseGrok(stdout: string, fallback: string): ReplyResult {
34
+ let text = '';
35
+ let sessionId = fallback;
36
+ for (const event of events(stdout)) {
37
+ if (event.type === 'text' && typeof event.data === 'string') text += event.data;
38
+ if (event.type === 'result' && typeof event.result === 'string') text = event.result;
39
+ if (typeof event.sessionId === 'string') sessionId = event.sessionId;
40
+ if (typeof event.session_id === 'string') sessionId = event.session_id;
41
+ const error = typeof event.message === 'string' ? event.message : event.error;
42
+ if (event.type === 'error' && typeof error === 'string') {
43
+ return { ok: false, error: error.slice(0, 500) };
44
+ }
45
+ }
46
+ return text.trim() ? { ok: true, text: text.trim().slice(0, 4_000), sessionId }
47
+ : { ok: false, error: 'Grok Build returned an empty response.' };
48
+ }
@@ -72,6 +72,12 @@ export function runningProcessCount(key: string): number {
72
72
  return running.get(key)?.size ?? 0;
73
73
  }
74
74
 
75
+ export type ProviderAdmission = {
76
+ confirmed: (stdout: string) => boolean;
77
+ withinMs: number;
78
+ timeoutMessage: string;
79
+ };
80
+
75
81
  export function runProcess(
76
82
  command: string,
77
83
  args: string[],
@@ -80,6 +86,7 @@ export function runProcess(
80
86
  parse: (stdout: string) => ReplyResult,
81
87
  stdin?: string,
82
88
  key?: string,
89
+ admission?: ProviderAdmission,
83
90
  ): Promise<ReplyResult> {
84
91
  return new Promise((resolve) => {
85
92
  let child: ChildProcess;
@@ -104,14 +111,20 @@ export function runProcess(
104
111
  if (done) return;
105
112
  done = true;
106
113
  clearTimeout(timer);
114
+ clearTimeout(startupTimer);
107
115
  untrack();
108
- resolve(result);
116
+ resolve(!result.ok && admission?.confirmed(stdout) ? { ...result, delivery: 'native' } : result);
109
117
  };
110
118
  const timer = setTimeout(() => {
111
119
  if (done) return;
112
120
  signalOwnedProcess(child, "SIGKILL");
113
121
  finish({ ok: false, error: `${command} did not respond within ${Math.round(timeoutMs / 1000)}s.` });
114
122
  }, timeoutMs);
123
+ const startupTimer = admission ? setTimeout(() => {
124
+ if (done || admission.confirmed(stdout)) return;
125
+ signalOwnedProcess(child, "SIGKILL");
126
+ finish({ ok: false, error: admission.timeoutMessage });
127
+ }, admission.withinMs) : undefined;
115
128
  child.stdout?.on("data", (chunk) => { stdout += chunk; });
116
129
  child.stderr?.on("data", (chunk) => { stderr += chunk; });
117
130
  child.on("error", (error) => {
@@ -1,3 +1,4 @@
1
+ import { grokAdmission, parseGrok } from "../grok-response";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import type { SessionInfo } from "../../../../../packages/protocol/schema";
3
4
  import { runProcess } from ".";
@@ -11,33 +12,6 @@ function grokBin(): string {
11
12
  return resolveGrokBinary();
12
13
  }
13
14
 
14
- function parseGrok(stdout: string, fallback: string): ReplyResult {
15
- let text = "";
16
- let sessionId = fallback;
17
- for (const line of stdout.split("\n")) {
18
- if (!line.trim()) continue;
19
- try {
20
- const value = JSON.parse(line) as {
21
- type?: string; data?: unknown; sessionId?: unknown; session_id?: unknown;
22
- result?: unknown; error?: unknown;
23
- };
24
- if (value.type === "text" && typeof value.data === "string") text += value.data;
25
- if (value.type === "result" && typeof value.result === "string") text = value.result;
26
- if (typeof value.sessionId === "string") sessionId = value.sessionId;
27
- if (typeof value.session_id === "string") sessionId = value.session_id;
28
- if (value.type === "error" && typeof value.error === "string") {
29
- return { ok: false, error: value.error.slice(0, 500) };
30
- }
31
- } catch {
32
- // Ignore diagnostic lines outside Grok's streaming JSON contract.
33
- }
34
- }
35
- const visible = text.trim() || stdout.trim();
36
- return visible
37
- ? { ok: true, text: visible.slice(0, 4_000), sessionId }
38
- : { ok: false, error: "Grok Build returned an empty response." };
39
- }
40
-
41
15
  export function runCursorNew(
42
16
  text: string, cwd: string, timeoutMs: number, model?: string, operationKey?: string, effort?: string,
43
17
  ): Promise<ReplyResult> {
@@ -62,7 +36,7 @@ export function runGrokNew(
62
36
  "-p", text, "--output-format", "streaming-json",
63
37
  ];
64
38
  return runProcess(grokBin(), args, cwd, timeoutMs, (stdout) => parseGrok(stdout, sessionId),
65
- undefined, operationKey);
39
+ undefined, operationKey, grokAdmission);
66
40
  }
67
41
 
68
42
  export function runGrokResume(
@@ -79,6 +53,6 @@ export function runGrokResume(
79
53
  return runProcess(
80
54
  grokBin(), args, session.cwd, timeoutMs,
81
55
  (stdout) => parseGrok(stdout, session.sessionId),
82
- undefined, session.sessionId,
56
+ undefined, session.sessionId, grokAdmission,
83
57
  );
84
58
  }
@@ -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.87
13
+ npm install -g granttap-mcp@0.8.88
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.87\";\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.88\";\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.87";
10
+ const PACKAGE = "granttap-mcp@0.8.88";
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.87",
3
+ "version": "0.8.88",
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": {