granttap-mcp 0.8.58 → 0.8.59
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/apps/bridge/src/delivery/index.ts +16 -1
- package/apps/bridge/src/monitor/README.md +7 -0
- package/apps/bridge/src/monitor/handlers/inbound.ts +1 -37
- package/apps/bridge/src/monitor/handlers/user-message.ts +9 -5
- package/apps/bridge/src/monitor/message-delivery/README.md +11 -0
- package/apps/bridge/src/monitor/message-delivery/index.ts +44 -0
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
|
@@ -5,11 +5,13 @@ import { configDir } from "../config";
|
|
|
5
5
|
|
|
6
6
|
const LIMIT = 500;
|
|
7
7
|
const MAX_AGE_MS = 7 * 24 * 60 * 60_000;
|
|
8
|
-
|
|
8
|
+
// Cover the ten-minute provider call and completion propagation before retry.
|
|
9
|
+
const PROCESSING_LEASE_MS = 11 * 60_000;
|
|
9
10
|
|
|
10
11
|
type DeliveryRecord = {
|
|
11
12
|
state: "processing" | "completed";
|
|
12
13
|
updatedAt: number;
|
|
14
|
+
processingAcknowledged?: boolean;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export type DeliveryStart = "started" | "processing" | "completed";
|
|
@@ -55,6 +57,19 @@ function save(records: Record<string, DeliveryRecord>, now: number): void {
|
|
|
55
57
|
chmodSync(deliveryLedgerPath(), 0o600);
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/** Only validated provider work may emit a non-terminal processing receipt. */
|
|
61
|
+
export function acknowledgeProcessingDelivery(messageId: string, now = Date.now()): void {
|
|
62
|
+
const records = load();
|
|
63
|
+
if (records[messageId]?.state !== "processing") return;
|
|
64
|
+
records[messageId] = { state: "processing", updatedAt: now, processingAcknowledged: true };
|
|
65
|
+
save(records, now);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function processingDeliveryAcknowledged(messageId: string): boolean {
|
|
69
|
+
const record = load()[messageId];
|
|
70
|
+
return record?.state === "processing" && record.processingAcknowledged === true;
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/** Acquire a durable processing lease before invoking an agent. */
|
|
59
74
|
export function beginDelivery(messageId: string, now = Date.now()): DeliveryStart {
|
|
60
75
|
const records = load();
|
|
@@ -11,3 +11,10 @@ Backbone requests responsive.
|
|
|
11
11
|
|
|
12
12
|
Integration tests remain in `tests/monitor`; background publication tests live
|
|
13
13
|
beside `mesh-publication`. This module uses the repository MIT License.
|
|
14
|
+
|
|
15
|
+
`message-delivery/index.ts` reserves the durable delivery lease, then
|
|
16
|
+
emits a non-terminal processing receipt only after workspace, attachment and
|
|
17
|
+
provider validation. Duplicate validated requests replay that receipt without
|
|
18
|
+
starting another provider run. The eleven-minute lease covers the ten-minute
|
|
19
|
+
provider timeout. New-task startup events are status updates; the correlated
|
|
20
|
+
result is terminal. Delivery integration tests live in `tests/delivery`.
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
handleSubscription,
|
|
33
33
|
} from "./session-commands";
|
|
34
34
|
import { handleTaskCreate } from "./task-create";
|
|
35
|
-
import {
|
|
35
|
+
import { handleInboundUserMessage } from "../message-delivery";
|
|
36
36
|
import { publishSessionEvents } from "./catalog";
|
|
37
37
|
import { publishSessionImage } from '../../session-images';
|
|
38
38
|
import { publishTaskWorkspace } from '../../task-files/relay';
|
|
@@ -201,42 +201,6 @@ function handleInboundPairingJoin(payload: Extract<Payload, { type: "pairing.joi
|
|
|
201
201
|
return true;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
async function handleInboundUserMessage(
|
|
205
|
-
client: RelayClient, payload: Extract<Payload, { type: "user.message" }>,
|
|
206
|
-
publish: (forceHistory?: boolean) => Promise<void>,
|
|
207
|
-
): Promise<boolean> {
|
|
208
|
-
// Correlated replies belong to the MCP `ask` waiter in one process.
|
|
209
|
-
if (payload.requestId) return false;
|
|
210
|
-
let deliveryStarted = false;
|
|
211
|
-
if (payload.messageId) {
|
|
212
|
-
const state = beginDelivery(payload.messageId);
|
|
213
|
-
if (state === "completed") {
|
|
214
|
-
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId);
|
|
215
|
-
return true;
|
|
216
|
-
}
|
|
217
|
-
if (state === "processing") return false;
|
|
218
|
-
deliveryStarted = true;
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const outcome = await handleUserMessage(client, payload);
|
|
222
|
-
if (payload.messageId && deliveryStarted) {
|
|
223
|
-
if (outcome === "rejected") {
|
|
224
|
-
// The phone sends the message again with what was missing; the
|
|
225
|
-
// ledger must let that second copy through.
|
|
226
|
-
abandonDelivery(payload.messageId);
|
|
227
|
-
} else {
|
|
228
|
-
completeDelivery(payload.messageId);
|
|
229
|
-
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
void publish().catch(() => {});
|
|
233
|
-
return true;
|
|
234
|
-
} catch {
|
|
235
|
-
if (payload.messageId && deliveryStarted) abandonDelivery(payload.messageId);
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
204
|
async function handleInboundTaskCreate(
|
|
241
205
|
client: RelayClient, payload: Extract<Payload, { type: "project.task.create" }>,
|
|
242
206
|
publish: (forceHistory?: boolean) => Promise<void>,
|
|
@@ -22,7 +22,8 @@ import { DELIVERY_TIMEOUT_MS } from "./receipts";
|
|
|
22
22
|
async function startNewAgentTask(input: {
|
|
23
23
|
client: RelayClient;
|
|
24
24
|
message: UserMessage;
|
|
25
|
-
say: (text: string, sessionId?: string, wake?: boolean) => Promise<unknown>;
|
|
25
|
+
say: (text: string, sessionId?: string, wake?: boolean, kind?: "status" | "response") => Promise<unknown>;
|
|
26
|
+
onProcessing?: () => Promise<void>;
|
|
26
27
|
}): Promise<"rejected" | void> {
|
|
27
28
|
const { client, message, say } = input;
|
|
28
29
|
const agent = message.agent ?? "codex";
|
|
@@ -57,7 +58,8 @@ async function startNewAgentTask(input: {
|
|
|
57
58
|
}
|
|
58
59
|
return "rejected";
|
|
59
60
|
}
|
|
60
|
-
await
|
|
61
|
+
await input.onProcessing?.();
|
|
62
|
+
await say(`Creating a new ${displayName} task…`, undefined, false, "status");
|
|
61
63
|
const create = {
|
|
62
64
|
claude: createClaudeSession,
|
|
63
65
|
codex: createCodexSession,
|
|
@@ -77,16 +79,17 @@ async function startNewAgentTask(input: {
|
|
|
77
79
|
export async function handleUserMessage(
|
|
78
80
|
client: RelayClient,
|
|
79
81
|
message: UserMessage,
|
|
82
|
+
onProcessing?: () => Promise<void>,
|
|
80
83
|
): Promise<"accepted" | "rejected" | void> {
|
|
81
|
-
const say = (text: string, sessionId?: string, wake = false) => {
|
|
82
|
-
const payload = agentEventForUserMessage(message, text, sessionId);
|
|
84
|
+
const say = (text: string, sessionId?: string, wake = false, kind: "status" | "response" = "response") => {
|
|
85
|
+
const payload = agentEventForUserMessage(message, text, sessionId, kind);
|
|
83
86
|
const options = { ttlMs: 15 * 60_000, wake: wake || undefined };
|
|
84
87
|
return (sessionId
|
|
85
88
|
? sendSessionPayload(client, payload, sessionId, "phone", options)
|
|
86
89
|
: client.send(payload, "phone", options)).catch(() => {});
|
|
87
90
|
};
|
|
88
91
|
if (!message.sessionId) {
|
|
89
|
-
return startNewAgentTask({ client, message, say });
|
|
92
|
+
return startNewAgentTask({ client, message, say, onProcessing });
|
|
90
93
|
}
|
|
91
94
|
const resolved = resolveMessageAttachments(message, (attachmentId) => takeAttachment(attachmentId, client.room));
|
|
92
95
|
if (!resolved.ok) {
|
|
@@ -135,6 +138,7 @@ export async function handleUserMessage(
|
|
|
135
138
|
|
|
136
139
|
// No "sent, waiting" line from a middleman: the delivery receipt already
|
|
137
140
|
// marks the person's bubble, and the next words in the chat are the answer.
|
|
141
|
+
await onProcessing?.();
|
|
138
142
|
const startedAt = Date.now();
|
|
139
143
|
const result = await deliverToSession(target, message.text, DELIVERY_TIMEOUT_MS, attachments, {
|
|
140
144
|
preferredMcp: message.preferredMcp,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Phone message delivery
|
|
2
|
+
|
|
3
|
+
Public entry: `index.ts`. The handler owns the durable delivery lease and receipt
|
|
4
|
+
sequence for non-correlated phone messages. Validation happens before the first
|
|
5
|
+
processing receipt, and duplicate validated messages never start another run.
|
|
6
|
+
Terminal responses remain correlated to the original message id.
|
|
7
|
+
|
|
8
|
+
Integration tests: `tests/delivery/processing-receipt.test.ts`; attachment rejection
|
|
9
|
+
coverage: `tests/attachment/attachment-uploads.test.ts`. See the monitor README.
|
|
10
|
+
|
|
11
|
+
This module uses the repository MIT License.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Payload } from "../../../../../packages/protocol/schema";
|
|
2
|
+
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
3
|
+
import { abandonDelivery, beginDelivery, completeDelivery,
|
|
4
|
+
acknowledgeProcessingDelivery, processingDeliveryAcknowledged } from "../../delivery";
|
|
5
|
+
import { sendDeliveryReceipt } from "../handlers/receipts";
|
|
6
|
+
import { handleUserMessage } from "../handlers/user-message";
|
|
7
|
+
|
|
8
|
+
/** Receipts acknowledge durable receipt, while the correlated response is terminal. */
|
|
9
|
+
export async function handleInboundUserMessage(
|
|
10
|
+
client: RelayClient, payload: Extract<Payload, { type: "user.message" }>,
|
|
11
|
+
publish: (forceHistory?: boolean) => Promise<void>,
|
|
12
|
+
): Promise<boolean> {
|
|
13
|
+
if (payload.requestId) return false;
|
|
14
|
+
let deliveryStarted = false;
|
|
15
|
+
if (payload.messageId) {
|
|
16
|
+
const state = beginDelivery(payload.messageId);
|
|
17
|
+
if (state !== "started") {
|
|
18
|
+
if (state === "completed" || processingDeliveryAcknowledged(payload.messageId)) {
|
|
19
|
+
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId);
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
deliveryStarted = true;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const outcome = await handleUserMessage(client, payload, async () => {
|
|
27
|
+
if (!payload.messageId) return;
|
|
28
|
+
acknowledgeProcessingDelivery(payload.messageId);
|
|
29
|
+
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId);
|
|
30
|
+
});
|
|
31
|
+
if (payload.messageId && deliveryStarted) {
|
|
32
|
+
if (outcome === "rejected") abandonDelivery(payload.messageId);
|
|
33
|
+
else {
|
|
34
|
+
completeDelivery(payload.messageId);
|
|
35
|
+
await sendDeliveryReceipt(client, payload.messageId, "accepted", undefined, payload.sessionId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
void publish().catch(() => {});
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
if (payload.messageId && deliveryStarted) abandonDelivery(payload.messageId);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/cursor-plugin/README.md
CHANGED
|
@@ -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.
|
|
13
|
+
npm install -g granttap-mcp@0.8.59
|
|
14
14
|
granttap setup
|
|
15
15
|
granttap status
|
|
16
16
|
```
|
package/cursor-plugin/mcp.json
CHANGED
|
@@ -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.
|
|
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.59\";\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.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.59";
|
|
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;
|