granttap-mcp 0.8.104 → 0.8.105

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.
@@ -29,3 +29,8 @@ consumed them. Tests exercise native delivery, framing, socket trust, durable
29
29
  deduplication, and refusal without launching the CLI.
30
30
 
31
31
  License: GrantTap Commercial Source License in the repository-root `LICENSE`.
32
+
33
+ Document staging for a native-only question keeps the file note inside its
34
+ original answer envelope. The host's native question and all paired controllers
35
+ therefore observe the same question item as answered; document paths remain
36
+ available to the admitted turn. Ordinary chat text is unchanged.
@@ -1,3 +1,4 @@
1
+ import { nativeQuestionAttachmentEnvelope } from './question-attachment-envelope';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import type { SessionInfo } from '../../../../packages/protocol/schema';
@@ -72,7 +73,7 @@ function turnRequest(input: { session: SessionInfo; text: string; images: string
72
73
  networkAccess: false, excludeTmpdirEnvVar: false, excludeSlashTmp: false }
73
74
  : access === 'full' ? { type: 'dangerFullAccess' } : undefined;
74
75
  return { threadId: session.sessionId, clientUserMessageId: options.messageId ?? randomUUID(),
75
- input: [{ type: 'text', text, text_elements: [] }, ...images.map(path => ({ type: 'localImage', path }))],
76
+ input: [{ type: 'text', text: options.nativeOnly ? nativeQuestionAttachmentEnvelope(text) : text, text_elements: [] }, ...images.map(path => ({ type: 'localImage', path }))],
76
77
  ...(options.model ? { model: options.model } : {}), ...(effort ? { effort } : {}),
77
78
  ...(sandboxPolicy ? { sandboxPolicy } : {}) };
78
79
  }
@@ -0,0 +1,23 @@
1
+ /** Preserve the native question envelope after document staging appends its local file note. */
2
+ export function nativeQuestionAttachmentEnvelope(text: string): string {
3
+ const opening = '<send_user_message_question_reply>';
4
+ const closing = '</send_user_message_question_reply>';
5
+ const boundary = text.lastIndexOf(closing);
6
+ if (!text.startsWith(opening) || boundary < opening.length) return text;
7
+ const note = text.slice(boundary + closing.length);
8
+ const header = '\n\nAttached files available locally:\n';
9
+ if (!note.startsWith(header)) return text;
10
+ const files = note.slice(header.length).split('\n');
11
+ if (!files.length || files.some(line => !/^- .+: .+$/.test(line))) return text;
12
+ try {
13
+ const rows = JSON.parse(text.slice(opening.length, boundary));
14
+ if (!Array.isArray(rows) || rows.length !== 1 || typeof rows[0]?.questionItemId !== 'string'
15
+ || typeof rows[0]?.answer !== 'string') return text;
16
+ const identity = JSON.parse(rows[0].questionItemId);
17
+ if (!Array.isArray(identity) || identity.length !== 3 || identity[0] !== 'request_user_input_async'
18
+ || typeof identity[1] !== 'string' || !/^[a-zA-Z0-9_-]{1,180}$/.test(identity[1])
19
+ || !Number.isInteger(identity[2]) || identity[2] < 0 || identity[2] > 3) return text;
20
+ rows[0].answer += note;
21
+ return opening + '\n' + JSON.stringify(rows) + '\n' + closing;
22
+ } catch { return text; }
23
+ }
@@ -25,3 +25,10 @@ Tests under `tests/` cover publication, reconnect replay, native acknowledgement
25
25
  scope rejection, expiry and delivery through an isolated native IPC fixture.
26
26
 
27
27
  License: GrantTap Commercial Source License in the repository-root `LICENSE`.
28
+
29
+ Attachment-capable controllers opt in using the advertised
30
+ `questionReplyFormat: attachments-v1`. The original question item owns the
31
+ answer, image and file payload together; ordinary encrypted attachment transport,
32
+ Task policy, native-owner admission and stable question idempotency still apply.
33
+ Legacy text-only replies keep their existing contract. Files remain available
34
+ for the admitted native turn; another phone cannot start a second answer.
@@ -40,6 +40,7 @@ export async function publishCodexQuestions(client: RelayClient, sessions: Sessi
40
40
  } else if (!record.announced || options.replay) {
41
41
  const text = [record.question, ...record.options.map(value => '• ' + value)].join('\n');
42
42
  await client.send({ type: 'agent.event', kind: 'question', requestId: record.requestId,
43
+ questionReplyFormat: 'attachments-v1',
43
44
  sessionId: session.sessionId, agent: 'codex', projectId: session.projectId,
44
45
  taskId: session.taskId, computerId: session.computerId, text, createdAt: record.createdAt },
45
46
  'phone', { ttlMs: Math.max(1, record.createdAt + RETENTION_MS - Date.now()),
@@ -73,8 +74,9 @@ export async function handleCodexQuestionReply(client: RelayClient, message: Use
73
74
  await resolveQuestion(client, record, peerPublicKey, !record.answered);
74
75
  return true;
75
76
  }
76
- if (!message.text.trim() || message.text.length > 4_000 || message.attachments?.length
77
- || message.attachmentRefs?.length) return false;
77
+ const hasAttachments = !!(message.attachments?.length || message.attachmentRefs?.length);
78
+ if (!message.text.trim() || message.text.length > 4_000
79
+ || (hasAttachments && message.questionReplyFormat !== 'attachments-v1')) return false;
78
80
  return deliver(client, { ...message, requestId: undefined,
79
81
  messageId: message.messageId ?? 'codex-answer-' + record.requestId,
80
82
  text: nativeAnswerEnvelope(record, message.text) }, publish, peerPublicKey,
@@ -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.104
13
+ npm install -g granttap-mcp@0.8.105
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.104\";\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.105\";\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.104";
10
+ const PACKAGE = "granttap-mcp@0.8.105";
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.104",
3
+ "version": "0.8.105",
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": {
@@ -49,6 +49,8 @@ export const UserMessage = z.object({
49
49
  agent: CodingAgent.optional(),
50
50
  cwd: z.string().max(4_096).optional(),
51
51
  requestId: z.string().optional(),
52
+ /** Explicit compatible opt-in for native question file/image replies. */
53
+ questionReplyFormat: z.literal("attachments-v1").optional(),
52
54
  sessionId: z.string().optional(),
53
55
  attachments: z.array(UserAttachment).max(MAX_USER_ATTACHMENTS).optional(),
54
56
  /** Attachments that came ahead of this message, by id. */
@@ -122,6 +124,8 @@ export const AgentEvent = z.object({
122
124
  type: z.literal("agent.event"),
123
125
  text: z.string(),
124
126
  requestId: z.string().optional(),
127
+ /** Explicit compatible opt-in for native question file/image replies. */
128
+ questionReplyFormat: z.literal("attachments-v1").optional(),
125
129
  kind: z.enum(["question", "status", "response"]).optional(),
126
130
  sessionId: z.string().optional(),
127
131
  agent: CodingAgent.optional(),