granttap-mcp 0.8.23 → 0.8.24
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 +5 -0
- package/apps/bridge/src/sessions/cursor/scan.ts +6 -0
- package/apps/bridge/src/sessions/cursor/transcripts.ts +6 -0
- package/apps/bridge/src/sessions/scan/claude/parse.ts +15 -1
- package/apps/bridge/src/sessions/scan/codex/parse.ts +12 -1
- package/apps/bridge/src/sessions/scan/grok.ts +10 -2
- package/cursor-plugin/.cursor-plugin/plugin.json +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/sessions/index.ts +2 -0
package/README.md
CHANGED
|
@@ -43,6 +43,11 @@ hosted service is governed by its own terms.
|
|
|
43
43
|
GrantTap reports the depth each provider actually exposes. Visibility does not
|
|
44
44
|
imply deterministic remote blocking or full mobile continuation.
|
|
45
45
|
|
|
46
|
+
Session catalogs report `lastMessageAt` only when a provider transcript has a
|
|
47
|
+
timestamped user or assistant message. `lastActivityAt` remains the separate
|
|
48
|
+
runtime activity timestamp and can include metadata or file updates. Clients
|
|
49
|
+
label that fallback as activity rather than presenting it as a message time.
|
|
50
|
+
|
|
46
51
|
## Project Mesh and task handoff
|
|
47
52
|
|
|
48
53
|
Project Mesh adds stable Project and Task identity above provider-native
|
|
@@ -137,6 +137,8 @@ function addComposerSession(context: ScanContext, composer: ComposerRow): void {
|
|
|
137
137
|
sessionId: composer.id, files, cwd: composer.cwd, composers: context.composers, descendants,
|
|
138
138
|
});
|
|
139
139
|
const childLast = child.children.reduce((latest, item) => Math.max(latest, item.lastActivityAt), 0);
|
|
140
|
+
const lastMessageAt = Math.max(root.lastMessageAt ?? 0,
|
|
141
|
+
...child.summaries.map((item) => item.lastMessageAt ?? 0));
|
|
140
142
|
const lastActivityAt = Math.max(
|
|
141
143
|
composerActivityAt(composer), root.lastActivityAt, childLast,
|
|
142
144
|
);
|
|
@@ -153,6 +155,7 @@ function addComposerSession(context: ScanContext, composer: ComposerRow): void {
|
|
|
153
155
|
root.startedAt || lastActivityAt || context.now,
|
|
154
156
|
),
|
|
155
157
|
lastActivityAt: lastActivityAt || composer.createdAt,
|
|
158
|
+
...(lastMessageAt ? { lastMessageAt } : {}),
|
|
156
159
|
tokensSession,
|
|
157
160
|
tokensLastTurn: root.files.length ? root.tokensLastTurn : 0,
|
|
158
161
|
contextTokensUsed: composer.contextTokensUsed ?? root.contextTokensUsed ?? tokensSession,
|
|
@@ -175,6 +178,8 @@ function addOrphanSession(context: ScanContext, file: CursorLogFile): void {
|
|
|
175
178
|
const root = transcriptSummary(sessionId, files.filter((item) => !item.isSubagent), sessionId, cwd);
|
|
176
179
|
const child = childSummaries({ sessionId, files, cwd, composers: context.composers });
|
|
177
180
|
const childLast = child.children.reduce((latest, item) => Math.max(latest, item.lastActivityAt), 0);
|
|
181
|
+
const lastMessageAt = Math.max(root.lastMessageAt ?? 0,
|
|
182
|
+
...child.summaries.map((item) => item.lastMessageAt ?? 0));
|
|
178
183
|
const lastActivityAt = Math.max(file.mtimeMs, root.lastActivityAt, childLast);
|
|
179
184
|
const title = childTitle(context.sidebar.get(sessionId) || root.titleFromUser);
|
|
180
185
|
const session = aggregateChildThreads({
|
|
@@ -185,6 +190,7 @@ function addOrphanSession(context: ScanContext, file: CursorLogFile): void {
|
|
|
185
190
|
state: stateFor(lastActivityAt),
|
|
186
191
|
startedAt: Math.min(file.birthtimeMs || lastActivityAt, root.startedAt || lastActivityAt),
|
|
187
192
|
lastActivityAt,
|
|
193
|
+
...(lastMessageAt ? { lastMessageAt } : {}),
|
|
188
194
|
tokensSession: root.tokensSession,
|
|
189
195
|
tokensLastTurn: root.tokensLastTurn,
|
|
190
196
|
contextTokensUsed: root.contextTokensUsed,
|
|
@@ -25,6 +25,7 @@ export type CursorLogFile = {
|
|
|
25
25
|
|
|
26
26
|
export type CursorTranscriptSummary = {
|
|
27
27
|
lastActivityAt: number;
|
|
28
|
+
lastMessageAt?: number;
|
|
28
29
|
startedAt: number;
|
|
29
30
|
files: string[];
|
|
30
31
|
model?: string;
|
|
@@ -196,6 +197,11 @@ function summarizeFile(
|
|
|
196
197
|
const item = safeParse(line);
|
|
197
198
|
if (!item) continue;
|
|
198
199
|
observeBlocks({ item, file, lineIndex, rootSessionId, cwd, acc });
|
|
200
|
+
if (["user", "assistant"].includes(item.role)
|
|
201
|
+
&& textBlocks(item.message?.content).some((text) => !!text.trim())) {
|
|
202
|
+
const at = ts(item.timestamp ?? item.message?.timestamp);
|
|
203
|
+
if (at) acc.lastMessageAt = Math.max(acc.lastMessageAt ?? 0, at);
|
|
204
|
+
}
|
|
199
205
|
if (!file.isSubagent) {
|
|
200
206
|
if (!acc.titleFromUser && item.role === "user") {
|
|
201
207
|
acc.titleFromUser = titleFrom(textBlocks(item.message?.content).join("\n"));
|
|
@@ -24,6 +24,7 @@ type ClaudeParseState = {
|
|
|
24
24
|
summary?: string;
|
|
25
25
|
startedAt: number;
|
|
26
26
|
lastActivityAt: number;
|
|
27
|
+
lastMessageAt: number;
|
|
27
28
|
tokensSession: number;
|
|
28
29
|
tokensLastTurn: number;
|
|
29
30
|
contextTokensUsed?: number;
|
|
@@ -53,6 +54,15 @@ function applyClaudeLine(d: any, state: ClaudeParseState): void {
|
|
|
53
54
|
if (t) {
|
|
54
55
|
if (!state.startedAt || t < state.startedAt) state.startedAt = t;
|
|
55
56
|
if (t > state.lastActivityAt) state.lastActivityAt = t;
|
|
57
|
+
const role = d.message?.role ?? d.type;
|
|
58
|
+
const content = d.message?.content ?? d.content;
|
|
59
|
+
const visible = typeof content === "string" ? content.trim().length > 0
|
|
60
|
+
: Array.isArray(content) && content.some((block: any) =>
|
|
61
|
+
["text", "image", "image_url", "input_image"].includes(block?.type)
|
|
62
|
+
&& (block.type !== "text" || typeof block.text === "string" && !!block.text.trim()));
|
|
63
|
+
if (["user", "assistant"].includes(role) && visible) {
|
|
64
|
+
state.lastMessageAt = Math.max(state.lastMessageAt, t);
|
|
65
|
+
}
|
|
56
66
|
}
|
|
57
67
|
const usage = d?.message?.usage;
|
|
58
68
|
if (usage) {
|
|
@@ -117,7 +127,10 @@ export function parseClaudeFile(file: string): {
|
|
|
117
127
|
return undefined;
|
|
118
128
|
}
|
|
119
129
|
const lines = text.split("\n");
|
|
120
|
-
const state: ClaudeParseState = {
|
|
130
|
+
const state: ClaudeParseState = {
|
|
131
|
+
sessionId: "", startedAt: 0, lastActivityAt: 0, lastMessageAt: 0,
|
|
132
|
+
tokensSession: 0, tokensLastTurn: 0,
|
|
133
|
+
};
|
|
121
134
|
for (const line of lines) {
|
|
122
135
|
if (!line) continue;
|
|
123
136
|
const d = safeParse(line);
|
|
@@ -135,6 +148,7 @@ export function parseClaudeFile(file: string): {
|
|
|
135
148
|
state: stateFor(state.lastActivityAt),
|
|
136
149
|
startedAt: state.startedAt || state.lastActivityAt,
|
|
137
150
|
lastActivityAt: state.lastActivityAt,
|
|
151
|
+
...(state.lastMessageAt ? { lastMessageAt: state.lastMessageAt } : {}),
|
|
138
152
|
tokensSession: state.tokensSession,
|
|
139
153
|
tokensLastTurn: state.tokensLastTurn,
|
|
140
154
|
contextTokensUsed: state.contextTokensUsed,
|
|
@@ -27,6 +27,7 @@ type CodexParseState = {
|
|
|
27
27
|
child?: CodexChildSource;
|
|
28
28
|
startedAt: number;
|
|
29
29
|
lastActivityAt: number;
|
|
30
|
+
lastMessageAt: number;
|
|
30
31
|
tokensSession: number;
|
|
31
32
|
tokensLastTurn: number;
|
|
32
33
|
contextTokensUsed?: number;
|
|
@@ -40,6 +41,15 @@ function applyCodexLine(d: any, state: CodexParseState): void {
|
|
|
40
41
|
if (t) {
|
|
41
42
|
if (!state.startedAt || t < state.startedAt) state.startedAt = t;
|
|
42
43
|
if (t > state.lastActivityAt) state.lastActivityAt = t;
|
|
44
|
+
const eventMessage = d.type === "event_msg"
|
|
45
|
+
&& ["user_message", "agent_message"].includes(d.payload?.type);
|
|
46
|
+
const responseMessage = d.type === "response_item"
|
|
47
|
+
&& d.payload?.type === "message"
|
|
48
|
+
&& ["user", "assistant"].includes(d.payload?.role)
|
|
49
|
+
&& Array.isArray(d.payload?.content) && d.payload.content.length > 0;
|
|
50
|
+
if (eventMessage || responseMessage) {
|
|
51
|
+
state.lastMessageAt = Math.max(state.lastMessageAt, t);
|
|
52
|
+
}
|
|
43
53
|
}
|
|
44
54
|
|
|
45
55
|
if (d.type === "session_meta") {
|
|
@@ -130,7 +140,7 @@ export function parseCodexFile(file: string): CodexCandidate | undefined {
|
|
|
130
140
|
return undefined;
|
|
131
141
|
}
|
|
132
142
|
const state: CodexParseState = {
|
|
133
|
-
sessionId: "", startedAt: 0, lastActivityAt: 0,
|
|
143
|
+
sessionId: "", startedAt: 0, lastActivityAt: 0, lastMessageAt: 0,
|
|
134
144
|
tokensSession: 0, tokensLastTurn: 0, workdirs: [],
|
|
135
145
|
};
|
|
136
146
|
for (const line of lines) {
|
|
@@ -155,6 +165,7 @@ export function parseCodexFile(file: string): CodexCandidate | undefined {
|
|
|
155
165
|
state: stateFor(state.lastActivityAt),
|
|
156
166
|
startedAt: state.startedAt || state.lastActivityAt,
|
|
157
167
|
lastActivityAt: state.lastActivityAt,
|
|
168
|
+
...(state.lastMessageAt ? { lastMessageAt: state.lastMessageAt } : {}),
|
|
158
169
|
tokensSession: state.tokensSession,
|
|
159
170
|
tokensLastTurn: state.tokensLastTurn,
|
|
160
171
|
contextTokensUsed: state.contextTokensUsed,
|
|
@@ -32,6 +32,7 @@ type ParsedLog = {
|
|
|
32
32
|
entries: ActivityEntry[];
|
|
33
33
|
usage: CapabilityObservation[];
|
|
34
34
|
firstUser?: string;
|
|
35
|
+
lastMessageAt?: number;
|
|
35
36
|
tokens: number;
|
|
36
37
|
lastTurn: number;
|
|
37
38
|
};
|
|
@@ -78,6 +79,7 @@ function parseLog(path: string, sessionId: string, fallbackTime: number): Parsed
|
|
|
78
79
|
const usage: CapabilityObservation[] = [];
|
|
79
80
|
const seen = new Set<string>();
|
|
80
81
|
let firstUser: string | undefined;
|
|
82
|
+
let lastMessageAt = 0;
|
|
81
83
|
let tokens = 0;
|
|
82
84
|
let lastTurn = 0;
|
|
83
85
|
lines.forEach((line, index) => {
|
|
@@ -86,11 +88,13 @@ function parseLog(path: string, sessionId: string, fallbackTime: number): Parsed
|
|
|
86
88
|
const message = row.message && typeof row.message === "object" ? row.message : row;
|
|
87
89
|
const role = String(message.role ?? row.role ?? "");
|
|
88
90
|
if (role === "reasoning" || role === "system") return;
|
|
89
|
-
const
|
|
91
|
+
const messageAt = ts(row.timestamp ?? message.timestamp);
|
|
92
|
+
const createdAt = messageAt || fallbackTime + index;
|
|
90
93
|
const texts = textBlocks(message.content);
|
|
91
94
|
if (role === "user") {
|
|
92
95
|
const text = texts.join("\n").trim();
|
|
93
96
|
if (text) {
|
|
97
|
+
if (messageAt) lastMessageAt = Math.max(lastMessageAt, messageAt);
|
|
94
98
|
firstUser ??= text;
|
|
95
99
|
pushEntry({ out: entries, seen: seen, sessionId: sessionId, kind: "user", text: text, createdAt: createdAt, ordinal: index });
|
|
96
100
|
tokens += estimateTokens(text);
|
|
@@ -103,6 +107,9 @@ function parseLog(path: string, sessionId: string, fallbackTime: number): Parsed
|
|
|
103
107
|
pushEntry({ out: entries, seen: seen, sessionId: sessionId, kind: "message", text: text, createdAt: createdAt, ordinal: index * 100 + block });
|
|
104
108
|
turnTokens += estimateTokens(text);
|
|
105
109
|
});
|
|
110
|
+
if (texts.some((text) => !!text.trim()) && messageAt) {
|
|
111
|
+
lastMessageAt = Math.max(lastMessageAt, messageAt);
|
|
112
|
+
}
|
|
106
113
|
const blocks = Array.isArray(message.content) ? message.content : [];
|
|
107
114
|
blocks.forEach((block: Record<string, unknown>, blockIndex: number) => {
|
|
108
115
|
if (block?.type !== "tool_use") return;
|
|
@@ -122,7 +129,7 @@ function parseLog(path: string, sessionId: string, fallbackTime: number): Parsed
|
|
|
122
129
|
tokens += turnTokens;
|
|
123
130
|
if (turnTokens > 0) lastTurn = turnTokens;
|
|
124
131
|
});
|
|
125
|
-
return { entries, usage, firstUser, tokens, lastTurn };
|
|
132
|
+
return { entries, usage, firstUser, lastMessageAt: lastMessageAt || undefined, tokens, lastTurn };
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
function parseSummary(path: string): Summary | null {
|
|
@@ -156,6 +163,7 @@ export function scanGrok(maxFiles = MAX_FILES): Scan {
|
|
|
156
163
|
sessionId, agent: "grok", title: title?.trim().slice(0, 120), cwd,
|
|
157
164
|
model: typeof summary.current_model_id === "string" ? summary.current_model_id : undefined,
|
|
158
165
|
state: stateFor(updatedAt), startedAt, lastActivityAt: updatedAt,
|
|
166
|
+
...(parsed.lastMessageAt ? { lastMessageAt: parsed.lastMessageAt } : {}),
|
|
159
167
|
tokensSession: parsed.tokens, tokensLastTurn: parsed.lastTurn,
|
|
160
168
|
contextTokensUsed: parsed.tokens,
|
|
161
169
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granttap",
|
|
3
3
|
"displayName": "GrantTap",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"description": "GrantTap pairs this computer with iPhone. The connection card shows a one-time QR when Cursor supports interactive MCP Apps; an interactive terminal command is available if it does not. The website approves the coding app.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GrantTap",
|
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.24
|
|
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.24\";\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.24";
|
|
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
|
@@ -74,6 +74,8 @@ export const SessionInfo = z.object({
|
|
|
74
74
|
state: SessionState,
|
|
75
75
|
startedAt: z.number(),
|
|
76
76
|
lastActivityAt: z.number(),
|
|
77
|
+
/** Timestamp of a visible user or assistant message; absent when no message timestamp is known. */
|
|
78
|
+
lastMessageAt: z.number().optional(),
|
|
77
79
|
tokensSession: z.number(),
|
|
78
80
|
tokensLastTurn: z.number(),
|
|
79
81
|
contextTokensUsed: z.number().optional(),
|