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.
@@ -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 ?? session.title ?? task.title,
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` resolves an
16
- explicit `sourceBindingId` only within the owning endpoint's available Project
17
- bindings. It verifies the actual Git root and identity before capturing context.
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
- /** The controller chooses an already admitted binding, never an arbitrary path. */
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
- if (!request.sourceBindingId) return session;
11
- const binding = store.snapshot(request.projectId)?.bindings?.find(row =>
12
- row.bindingId === request.sourceBindingId && row.projectId === request.projectId
13
- && row.endpointId === computer && row.available);
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.cwd && !state.cwd) state.cwd = String(d.cwd);
54
- if (d.gitBranch && !state.branch) state.branch = String(d.gitBranch);
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 =
@@ -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.92
13
+ npm install -g granttap-mcp@0.8.94
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.92\";\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.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.92";
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.92",
3
+ "version": "0.8.94",
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": {