granttap-mcp 0.8.116 → 0.8.117
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/codex-questions/README.md +6 -0
- package/apps/bridge/src/codex-questions/index.ts +21 -5
- package/apps/bridge/src/codex-questions/records.ts +1 -0
- package/apps/bridge/src/monitor/handlers/inbound.ts +5 -1
- package/apps/bridge/src/sessions/native-source.ts +1 -1
- 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
- package/packages/protocol/messages/interaction/index.ts +1 -0
|
@@ -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');
|
|
@@ -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(() => {});
|
|
@@ -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);
|
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.117
|
|
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.117\";\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.
|
|
10
|
+
const PACKAGE = "granttap-mcp@0.8.117";
|
|
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
|
@@ -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>;
|