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.
@@ -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);
@@ -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.117
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.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.116";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.116",
3
+ "version": "0.8.117",
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>;