granttap-mcp 0.8.92 → 0.8.94
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/mesh/admin/capsule.ts +1 -1
- package/apps/bridge/src/mesh/catalog/index.ts +14 -4
- package/apps/bridge/src/mesh/runtime/prepared-transfer/README.md +10 -3
- package/apps/bridge/src/mesh/runtime/prepared-transfer/source-binding.ts +64 -6
- package/apps/bridge/src/sessions/README.md +6 -0
- package/apps/bridge/src/sessions/scan/claude/parse.ts +5 -3
- 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
|
@@ -86,7 +86,7 @@ export function buildTaskCapsule(
|
|
|
86
86
|
const capsule = {
|
|
87
87
|
taskId: request.taskId,
|
|
88
88
|
goal: task.goal,
|
|
89
|
-
currentStatus: session.summary
|
|
89
|
+
currentStatus: (session.summary?.trim() || session.title?.trim() || task.title).slice(0, 1_000),
|
|
90
90
|
sourceProvider: session.agent,
|
|
91
91
|
sourceComputer: computerId,
|
|
92
92
|
targetProvider: request.targetProvider,
|
|
@@ -166,8 +166,17 @@ function linkSession(input: {
|
|
|
166
166
|
const agent = provider(session.agent);
|
|
167
167
|
const cwd = session.cwd?.trim();
|
|
168
168
|
if (!agent || !cwd) return session;
|
|
169
|
-
const repository = inspect(session.worktree ?? cwd);
|
|
170
169
|
const knownTaskId = store.taskForExecution(computerId, agent, session.sessionId);
|
|
170
|
+
const knownProjectId = knownTaskId ? store.task(knownTaskId)?.projectId : undefined;
|
|
171
|
+
const previous = knownProjectId ? store.snapshot(knownProjectId)?.executions.find(row =>
|
|
172
|
+
row.taskId === knownTaskId && row.provider === agent && row.sessionId === session.sessionId
|
|
173
|
+
&& row.computerId === computerId && row.workspace === cwd) : undefined;
|
|
174
|
+
let repository = inspect(session.worktree ?? cwd);
|
|
175
|
+
if (!repository.worktree && previous?.worktree) {
|
|
176
|
+
const retained = inspect(previous.worktree);
|
|
177
|
+
if (retained.worktree && retained.revision
|
|
178
|
+
&& retained.canonicalRepositoryId === previous.repositoryId) repository = retained;
|
|
179
|
+
}
|
|
171
180
|
const projectId = (knownTaskId ? store.task(knownTaskId)?.projectId : undefined)
|
|
172
181
|
?? store.projectIdForRepository(repository.canonicalRepositoryId, computerId)
|
|
173
182
|
?? projectIdentity(repository.canonicalRepositoryId);
|
|
@@ -229,13 +238,14 @@ function linkSession(input: {
|
|
|
229
238
|
provider: agent,
|
|
230
239
|
computerId,
|
|
231
240
|
workspace: cwd,
|
|
232
|
-
repositoryId: repository.canonicalRepositoryId
|
|
241
|
+
repositoryId: repository.worktree ? repository.canonicalRepositoryId
|
|
242
|
+
: previous?.repositoryId ?? repository.canonicalRepositoryId,
|
|
233
243
|
activeAt: session.lastActivityAt,
|
|
234
244
|
state: session.state, continuedAt: session.transfer?.continuedAt,
|
|
235
245
|
branch: repository.branch ?? session.branch,
|
|
236
|
-
worktree: repository.worktree,
|
|
246
|
+
worktree: repository.worktree ?? previous?.worktree,
|
|
237
247
|
uncommitted: input.inspectWorkingTree(repository.worktree ?? cwd),
|
|
238
248
|
startedAt: session.startedAt,
|
|
239
249
|
});
|
|
240
|
-
return { ...session, projectId, taskId, computerId, worktree: repository.worktree };
|
|
250
|
+
return { ...session, projectId, taskId, computerId, worktree: repository.worktree ?? previous?.worktree };
|
|
241
251
|
}
|
|
@@ -12,13 +12,20 @@ Catalog batches share a working-tree probe for chats in the same checkout.
|
|
|
12
12
|
The next batch reads it again, including unknown results, so this never hides
|
|
13
13
|
new uncommitted work behind a persistent cache.
|
|
14
14
|
|
|
15
|
-
For a chat started above its repositories, `source-binding.ts`
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
For a chat started above its repositories, `source-binding.ts` first uses the
|
|
16
|
+
Task's own execution, current resource claim, Project repository, or sole owned
|
|
17
|
+
checkout. Multiple unresolved choices require an explicit `sourceBindingId`.
|
|
18
|
+
Both paths stay within the owning endpoint's available Project bindings and
|
|
19
|
+
verify the actual Git root and identity before capturing context. A catalog
|
|
20
|
+
refresh preserves an established checkout when the native chat still reports
|
|
21
|
+
the same parent folder; missing known checkouts never select another repository.
|
|
18
22
|
An exact repository binding takes precedence over a Project's parent execution
|
|
19
23
|
folder. Preparing keeps the native source folder unchanged and launches no
|
|
20
24
|
provider. Rejected preparations also publish their retained Project state.
|
|
21
25
|
|
|
26
|
+
Capsule status is bounded independently from the complete conversation capture;
|
|
27
|
+
a long native summary cannot invalidate an otherwise transferable Git Task.
|
|
28
|
+
|
|
22
29
|
Tests: `tests/metadata.test.ts`, `tests/source-checkout.test.ts`,
|
|
23
30
|
`tests/mesh/mesh-handoff-safety.test.ts`, and catalog
|
|
24
31
|
`tests/working-tree-batch.test.ts`.
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { realpathSync } from 'node:fs';
|
|
2
|
-
import type { MeshHandoffPrepare, SessionInfo } from '../../../../../../packages/protocol/schema';
|
|
2
|
+
import type { MeshHandoffPrepare, MeshSnapshot, ProjectBindingSummary, SessionInfo } from '../../../../../../packages/protocol/schema';
|
|
3
3
|
import { inspectRepository } from '../../catalog';
|
|
4
4
|
import type { MeshStore } from '../../store';
|
|
5
5
|
import { matchesLocalCheckout } from '../graph/checkout-identity';
|
|
6
6
|
|
|
7
|
-
/**
|
|
7
|
+
/** Resolve the Task's admitted checkout; an explicit choice can replace that default. */
|
|
8
8
|
export function resolveHandoffSource(store: MeshStore, session: SessionInfo,
|
|
9
9
|
request: MeshHandoffPrepare, computer: string): SessionInfo {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
const snapshot = store.snapshot(request.projectId);
|
|
11
|
+
if (!request.sourceBindingId) {
|
|
12
|
+
const cwd = session.worktree ?? session.cwd;
|
|
13
|
+
if (cwd) {
|
|
14
|
+
const native = inspectRepository(cwd);
|
|
15
|
+
if (native.worktree && native.revision) {
|
|
16
|
+
return { ...session, worktree: native.root, branch: session.branch ?? native.branch };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const binding = request.sourceBindingId
|
|
21
|
+
? snapshot?.bindings?.find(row => row.bindingId === request.sourceBindingId
|
|
22
|
+
&& row.projectId === request.projectId && row.endpointId === computer && row.available)
|
|
23
|
+
: inferBinding(snapshot, session, request, computer);
|
|
24
|
+
if (!binding && !request.sourceBindingId) return session;
|
|
14
25
|
if (binding?.localPathHint) {
|
|
15
26
|
const facts = inspectRepository(binding.localPathHint);
|
|
16
27
|
try {
|
|
@@ -22,3 +33,50 @@ export function resolveHandoffSource(store: MeshStore, session: SessionInfo,
|
|
|
22
33
|
}
|
|
23
34
|
throw new Error('The selected repository is not available on this computer. Refresh the Project and choose its checkout again.');
|
|
24
35
|
}
|
|
36
|
+
|
|
37
|
+
function samePath(left: string | undefined, right: string | undefined): boolean {
|
|
38
|
+
if (!left || !right) return false;
|
|
39
|
+
try { return realpathSync(left) === realpathSync(right); } catch { return false; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inferBinding(snapshot: MeshSnapshot | undefined, session: SessionInfo,
|
|
43
|
+
request: MeshHandoffPrepare, computer: string): ProjectBindingSummary | undefined {
|
|
44
|
+
const paths = new Set<string>();
|
|
45
|
+
const bindings = (snapshot?.bindings ?? []).filter(row => {
|
|
46
|
+
if (row.projectId !== request.projectId || row.endpointId !== computer
|
|
47
|
+
|| !row.available || !row.revision || !row.localPathHint) return false;
|
|
48
|
+
try {
|
|
49
|
+
const path = realpathSync(row.localPathHint);
|
|
50
|
+
if (paths.has(path)) return false;
|
|
51
|
+
paths.add(path);
|
|
52
|
+
return true;
|
|
53
|
+
} catch { return false; }
|
|
54
|
+
});
|
|
55
|
+
if (!bindings.length) return undefined;
|
|
56
|
+
const execution = snapshot?.executions.find(row => row.taskId === request.taskId
|
|
57
|
+
&& row.provider === session.agent && row.sessionId === session.sessionId
|
|
58
|
+
&& row.computerId === computer && row.endedAt == null);
|
|
59
|
+
if (execution?.worktree) {
|
|
60
|
+
const exact = bindings.find(row => samePath(row.localPathHint, execution.worktree));
|
|
61
|
+
if (exact) return exact;
|
|
62
|
+
throw new Error('This Task\'s source checkout is unavailable. Refresh its computer before transferring.');
|
|
63
|
+
}
|
|
64
|
+
const byExecution = execution?.repositoryId
|
|
65
|
+
? bindings.filter(row => row.repositoryId === execution.repositoryId) : [];
|
|
66
|
+
if (byExecution.length === 1) return byExecution[0];
|
|
67
|
+
const parent = snapshot?.bindings?.some(row => row.repositoryId === execution?.repositoryId
|
|
68
|
+
&& row.endpointId === computer && !row.revision && samePath(row.localPathHint, session.cwd));
|
|
69
|
+
if (execution?.repositoryId && !parent) {
|
|
70
|
+
throw new Error('This Task\'s source repository is unavailable. Refresh its computer before transferring.');
|
|
71
|
+
}
|
|
72
|
+
const claims = snapshot?.claims.filter(row => row.taskId === request.taskId
|
|
73
|
+
&& row.ownerSessionId === session.sessionId && row.expiresAt > Date.now()
|
|
74
|
+
&& (!row.endpointId || row.endpointId === computer)) ?? [];
|
|
75
|
+
const byClaim = bindings.filter(row => claims.some(claim =>
|
|
76
|
+
claim.repositoryId === row.repositoryId && (!claim.worktree || samePath(claim.worktree, row.localPathHint))));
|
|
77
|
+
if (byClaim.length === 1) return byClaim[0];
|
|
78
|
+
const byProject = bindings.filter(row => row.repositoryId === snapshot?.project.canonicalRepositoryId);
|
|
79
|
+
if (byProject.length === 1) return byProject[0];
|
|
80
|
+
if (bindings.length === 1) return bindings[0];
|
|
81
|
+
throw new Error('Multiple Project checkouts are available. Choose the repository this Task should continue in.');
|
|
82
|
+
}
|
|
@@ -9,6 +9,12 @@ so discovery cannot monopolize relay callbacks. Synchronous CLI readers use the
|
|
|
9
9
|
same parsers. Published native bindings stay readable until a complete scan
|
|
10
10
|
replaces them; checkout verification caches belong to one scan across its yields.
|
|
11
11
|
|
|
12
|
+
Claude root records update the current working folder and branch as the native
|
|
13
|
+
conversation moves. Its launch directory is not a permanent repository binding.
|
|
14
|
+
Sidechain folders and tool observations cannot replace the root conversation's
|
|
15
|
+
checkout. Catalog linking verifies the current Git root while preserving the
|
|
16
|
+
existing Task and Project identities.
|
|
17
|
+
|
|
12
18
|
Claude child reads use only the requested owned log and its bounded recent
|
|
13
19
|
window. Native `agentId` remains authoritative when it differs from the filename;
|
|
14
20
|
the scanner records that mapping. Parent and sibling logs are not reparsed for
|
|
@@ -47,11 +47,13 @@ function noteTurnBoundary(d: any, state: ClaudeParseState): void {
|
|
|
47
47
|
|
|
48
48
|
function applyClaudeLine(d: any, state: ClaudeParseState): void {
|
|
49
49
|
noteTurnBoundary(d, state);
|
|
50
|
-
state.repositoryPaths.push(...claudeRepositoryPaths(d));
|
|
50
|
+
if (d.isSidechain !== true) state.repositoryPaths.push(...claudeRepositoryPaths(d));
|
|
51
51
|
state.repositoryPaths = state.repositoryPaths.slice(-64);
|
|
52
52
|
if (d.sessionId && !state.sessionId) state.sessionId = String(d.sessionId);
|
|
53
|
-
if (d.
|
|
54
|
-
|
|
53
|
+
if (d.isSidechain !== true) {
|
|
54
|
+
if (typeof d.cwd === "string" && d.cwd.trim()) state.cwd = d.cwd;
|
|
55
|
+
if (typeof d.gitBranch === "string" && d.gitBranch.trim()) state.branch = d.gitBranch;
|
|
56
|
+
}
|
|
55
57
|
if (d.type === "ai-title" && typeof d.content === "string" && !state.title) state.title = d.content;
|
|
56
58
|
if (!state.title && (d.type === "user" || d.message?.role === "user")) {
|
|
57
59
|
const raw =
|
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.94
|
|
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.94\";\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.94";
|
|
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;
|