granttap-mcp 0.8.89 → 0.8.90

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.
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { execFileSync } from "node:child_process";
3
+ import { realpathSync } from "node:fs";
3
4
  import {
4
5
  TaskCapsule,
5
6
  type MeshHandoffPrepare,
@@ -42,6 +43,11 @@ function changedFiles(cwd: string): string[] {
42
43
  return [...new Set(files)].slice(0, 64);
43
44
  }
44
45
 
46
+ function sameCheckout(path: string | undefined, cwd: string): boolean {
47
+ try { return path != null && realpathSync(path) === realpathSync(cwd); }
48
+ catch { return false; }
49
+ }
50
+
45
51
  /** The identity of the uncommitted changes, so a later reading can tell they are the same. */
46
52
  export function dirtyDiffHash(cwd: string): string | undefined {
47
53
  const diff = git(cwd, ["diff", "--binary", "HEAD"]);
@@ -74,6 +80,9 @@ export function buildTaskCapsule(
74
80
  if (!head) return undefined;
75
81
  const tree = workingTreeState(cwd);
76
82
  const files = changedFiles(cwd);
83
+ const sourceBinding = snapshot.bindings?.find(binding => binding.endpointId === computerId
84
+ && binding.available && sameCheckout(binding.localPathHint, cwd)
85
+ && (!request.sourceBindingId || binding.bindingId === request.sourceBindingId));
77
86
  const capsule = {
78
87
  taskId: request.taskId,
79
88
  goal: task.goal,
@@ -85,7 +94,7 @@ export function buildTaskCapsule(
85
94
  targetComputer: request.targetComputer,
86
95
  targetModel: request.targetModel,
87
96
  userComment: request.userComment,
88
- repository: project.canonicalRepositoryId,
97
+ repository: sourceBinding?.repositoryId ?? project.canonicalRepositoryId,
89
98
  baseSha: head,
90
99
  branch: session.branch ?? git(cwd, ["branch", "--show-current"]),
91
100
  latestCommit: head,
@@ -69,6 +69,11 @@ export function workspaceForRepository(
69
69
  canonicalRepositoryId: string,
70
70
  computerId?: string,
71
71
  ): string | undefined {
72
+ // A Project can contain several repositories. Its first execution may be
73
+ // rooted in their parent folder, so an exact owned binding takes precedence.
74
+ const binding = state.bindings.find(item => item.repositoryId === canonicalRepositoryId
75
+ && item.available && (computerId == null || item.endpointId === computerId));
76
+ if (binding?.localPathHint) return binding.localPathHint;
72
77
  const projectIds = new Set([
73
78
  ...state.bindings
74
79
  .filter((binding) => binding.repositoryId === canonicalRepositoryId)
@@ -139,6 +139,11 @@ export function linkSessionsToProjects(
139
139
  // Share one consistent reading within this catalog batch, then discard it.
140
140
  // A new Git checkout or origin must be discovered on the next refresh.
141
141
  const repositories = new Map<string, RepositoryFacts>();
142
+ const workingTrees = new Map<string, boolean | undefined>();
143
+ const inspectWorkingTree = (cwd: string): boolean | undefined => {
144
+ if (!workingTrees.has(cwd)) workingTrees.set(cwd, hasUncommittedWork(cwd));
145
+ return workingTrees.get(cwd);
146
+ };
142
147
  const inspectOnce = (cwd: string): RepositoryFacts => {
143
148
  let facts = repositories.get(cwd);
144
149
  if (!facts) {
@@ -147,7 +152,7 @@ export function linkSessionsToProjects(
147
152
  }
148
153
  return facts;
149
154
  };
150
- return visible.map((session) => linkSession({ store, session, computerId, inspect: inspectOnce }));
155
+ return visible.map((session) => linkSession({ store, session, computerId, inspect: inspectOnce, inspectWorkingTree }));
151
156
  }
152
157
 
153
158
  function linkSession(input: {
@@ -155,6 +160,7 @@ function linkSession(input: {
155
160
  session: SessionInfo;
156
161
  computerId: string;
157
162
  inspect: (cwd: string) => RepositoryFacts;
163
+ inspectWorkingTree: (cwd: string) => boolean | undefined;
158
164
  }): SessionInfo {
159
165
  const { store, session, computerId, inspect } = input;
160
166
  const agent = provider(session.agent);
@@ -228,7 +234,7 @@ function linkSession(input: {
228
234
  state: session.state, continuedAt: session.transfer?.continuedAt,
229
235
  branch: repository.branch ?? session.branch,
230
236
  worktree: repository.worktree,
231
- uncommitted: hasUncommittedWork(repository.worktree ?? cwd),
237
+ uncommitted: input.inspectWorkingTree(repository.worktree ?? cwd),
232
238
  startedAt: session.startedAt,
233
239
  });
234
240
  return { ...session, projectId, taskId, computerId, worktree: repository.worktree };
@@ -21,13 +21,10 @@ import {
21
21
  } from "../../reply";
22
22
  import { isProviderEnabled } from "../../config/runtime";
23
23
  import { sendMeshPayload } from "../../host/session-keys";
24
- import { linkSessionsToProjects, workingTreeState } from "../catalog";
24
+ import { linkSessionsToProjects } from "../catalog";
25
25
  import { catalogFromEndpoint } from "../catalog/models";
26
26
  import { saveProjectCapabilityRequest } from "../catalog/project/requests";
27
27
  import { computerId } from "../identity/computer";
28
- import { createCheckpoint } from "../admin/checkpoint";
29
- import { buildTaskCapsule } from "../admin/capsule";
30
- import { handoffReadiness } from "../tasks/readiness";
31
28
  import { createHandoffFlow } from "./handoff";
32
29
  import type { MeshRuntimeDependencies } from "./dependencies";
33
30
  import { localMeshStore } from "../local-remote/local";
@@ -37,9 +34,8 @@ import { createHandoffWorktree, repositoryHasCommit } from "./worktree";
37
34
  import { fetchRevision, pushBranch } from "../local-remote/remote";
38
35
  import { projectSessionCapabilityInventory } from "./capabilities";
39
36
  import { createMeshSessionCatalog } from "./catalog/session-catalog";
40
- import { capturePreparedHandoff, preparedHandoffMetadata } from './prepared-transfer';
37
+ import { prepareHandoff } from "./prepared-transfer/prepare";
41
38
  import { sourceCheckoutUnchanged } from './prepared-transfer/source-checkout';
42
- import { prepareCloudHandoff } from "./cloud-handoff";
43
39
 
44
40
  export type { MeshRuntimeDependencies };
45
41
 
@@ -84,105 +80,6 @@ const defaultDependencies: MeshRuntimeDependencies = {
84
80
  }),
85
81
  };
86
82
 
87
- async function prepareHandoff(
88
- deps: MeshRuntimeDependencies,
89
- flow: ReturnType<typeof createHandoffFlow>,
90
- client: RelayClient,
91
- request: MeshHandoffPrepare,
92
- catalog: (sessions: SessionInfo[]) => SessionInfo[],
93
- ): Promise<boolean> {
94
- const { blockHandoff, acceptHandoff } = flow;
95
- const sessions = catalog(deps.sessions());
96
- const session = sessions.find((item) => item.sessionId === request.sessionId);
97
- if (!session || session.projectId !== request.projectId || session.taskId !== request.taskId) {
98
- return false;
99
- }
100
- if (request.prepareOnly && session.state === "working") {
101
- await blockHandoff(client, request, "Pause or finish the current turn before transferring this Task.");
102
- return false;
103
- }
104
- const cwd = session.worktree ?? session.cwd;
105
- const made = request.checkpoint && cwd && workingTreeState(cwd) === "dirty"
106
- ? createCheckpoint(cwd, request.taskId, session.title ?? request.taskId, request.createdAt)
107
- : undefined;
108
- const shared = made && cwd
109
- ? sessions.some((other) =>
110
- other.sessionId !== session.sessionId
111
- && other.taskId != null && other.taskId !== request.taskId
112
- && (other.worktree ?? other.cwd) === cwd)
113
- : false;
114
- const checkpoint = made && shared ? { ...made, status: "requires_review" as const } : made;
115
- let capsule = buildTaskCapsule(deps.store(), session, request, deps.computer(), checkpoint);
116
- const readiness = handoffReadiness({
117
- capsule,
118
- targetProviderEnabled: request.targetProvider === "grok_bot"
119
- || deps.providerEnabled(request.targetProvider),
120
- conflicts: (capsule?.resourceClaims ?? []).flatMap((resource) =>
121
- deps.store().conflicts(request.projectId, request.sessionId, resource, {
122
- repositoryId: capsule?.repository, endpointId: deps.computer(), worktree: cwd,
123
- }, true)),
124
- moduleOverlaps: (capsule?.resourceClaims ?? []).flatMap((resource) =>
125
- deps.store().moduleOverlaps(request.projectId, request.sessionId, resource, {
126
- repositoryId: capsule?.repository, endpointId: deps.computer(), worktree: cwd,
127
- })),
128
- });
129
- if (!readiness.ready) {
130
- await blockHandoff(client, request, readiness.blockedReason ?? "Handoff is not ready.");
131
- return false;
132
- }
133
- if (!capsule) return false;
134
- if (request.cloud) return prepareCloudHandoff({ deps, block: blockHandoff, client, request, session, capsule });
135
- let transferContext: string | undefined;
136
- let confirmContext: (() => Promise<boolean>) | undefined;
137
- if (request.prepareOnly) {
138
- try {
139
- const captured = await capturePreparedHandoff(deps, session, capsule);
140
- transferContext = captured.transferContext;
141
- capsule = captured.capsule;
142
- confirmContext = captured.confirm;
143
- } catch (error) {
144
- await blockHandoff(client, request, error instanceof Error ? error.message : "The complete transfer context is unavailable.");
145
- return false;
146
- }
147
- }
148
- const local = request.targetComputer === deps.computer();
149
- if (request.push && !local && cwd) {
150
- const pushed = deps.push(cwd, capsule.branch);
151
- if (!pushed.ok) {
152
- await blockHandoff(client, request, pushed.error);
153
- return false;
154
- }
155
- }
156
- const createdAt = deps.now();
157
- const event: MeshEvent = {
158
- type: "mesh.event",
159
- sessionId: request.taskId,
160
- eventId: deps.eventId(),
161
- projectId: request.projectId,
162
- taskId: request.taskId,
163
- sourceSessionId: request.sessionId,
164
- eventType: "HANDOFF_REQUEST",
165
- createdAt,
166
- expiresAt: createdAt + 60 * 60_000,
167
- payload: { capsule, transferContext },
168
- };
169
- if (request.prepareOnly && !local && deps.dispatchPreparedTransfer) {
170
- try {
171
- if (await deps.dispatchPreparedTransfer(event, session, confirmContext!)) {
172
- await deps.send(client, preparedHandoffMetadata(event), { ttlMs: 60 * 60_000, wake: true }).catch(() => {});
173
- return true;
174
- }
175
- await blockHandoff(client, request, 'Project computer transfer is not configured for this destination. Connect its Project in GrantTap.');
176
- } catch (error) {
177
- await blockHandoff(client, request, error instanceof Error ? error.message : 'The transfer could not be saved.');
178
- }
179
- return false;
180
- }
181
- deps.store().acceptEvent(event);
182
- await deps.send(client, request.prepareOnly ? preparedHandoffMetadata(event) : event, { ttlMs: 60 * 60_000, wake: true });
183
- if (local) await acceptHandoff(client, event);
184
- return true;
185
- }
186
83
 
187
84
  export function createMeshRuntime(deps: MeshRuntimeDependencies) {
188
85
  const flow = createHandoffFlow(deps);
@@ -0,0 +1,26 @@
1
+ # Preparing a Task transfer
2
+
3
+ `prepare.ts` prepares one scoped transfer through the Mesh runtime's public
4
+ entry point. `source.ts` checks the source facts and the explicit checkpoint
5
+ choice; `index.ts` captures and confirms complete visible context.
6
+
7
+ A missing source chat sends a scoped `TASK_BLOCKED` result so the controller can
8
+ explain the refusal. Preparing does not launch the destination provider. Work
9
+ starts when the person sends the prepared chat its first message.
10
+
11
+ Catalog batches share a working-tree probe for chats in the same checkout.
12
+ The next batch reads it again, including unknown results, so this never hides
13
+ new uncommitted work behind a persistent cache.
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.
18
+ An exact repository binding takes precedence over a Project's parent execution
19
+ folder. Preparing keeps the native source folder unchanged and launches no
20
+ provider. Rejected preparations also publish their retained Project state.
21
+
22
+ Tests: `tests/metadata.test.ts`, `tests/source-checkout.test.ts`,
23
+ `tests/mesh/mesh-handoff-safety.test.ts`, and catalog
24
+ `tests/working-tree-batch.test.ts`.
25
+
26
+ Licensed under the GrantTap Commercial Source License. See LICENSE and NOTICE.
@@ -9,7 +9,8 @@ export function preparedHandoffMetadata(event: MeshEvent): MeshEvent {
9
9
  }
10
10
 
11
11
  /** Retains the original archive facts so reconfirmation hashes the same conversation. */
12
- export async function capturePreparedHandoff(deps: MeshRuntimeDependencies, session: SessionInfo, facts: TaskCapsule) {
12
+ export async function capturePreparedHandoff(deps: MeshRuntimeDependencies, session: SessionInfo,
13
+ facts: TaskCapsule, native = session) {
13
14
  const capture = deps.captureTransferContext ?? captureTransferContext;
14
15
  const captured = await capture(session, facts);
15
16
  const task = deps.store().task(facts.taskId);
@@ -19,9 +20,10 @@ export async function capturePreparedHandoff(deps: MeshRuntimeDependencies, sess
19
20
  confirm: async () => {
20
21
  const current = deps.sessions().find(row => row.sessionId === session.sessionId);
21
22
  if (!current || current.state === 'working' || current.model !== session.model || current.effort !== session.effort
22
- || (current.worktree ?? current.cwd) !== (session.worktree ?? session.cwd)
23
- || deps.sourceCaptureCurrent && !deps.sourceCaptureCurrent(current, facts)) return false;
24
- try { return (await capture(current, facts)).hash === captured.hash; }
23
+ || (current.worktree ?? current.cwd) !== (native.worktree ?? native.cwd)) return false;
24
+ const resolved = { ...current, worktree: session.worktree, branch: session.branch };
25
+ if (deps.sourceCaptureCurrent && !deps.sourceCaptureCurrent(resolved, facts)) return false;
26
+ try { return (await capture(resolved, facts)).hash === captured.hash; }
25
27
  catch { return false; }
26
28
  } };
27
29
  }
@@ -0,0 +1,93 @@
1
+ import type { RelayClient } from "../../../../../../packages/core/relay-client";
2
+ import type { MeshEvent, MeshHandoffPrepare, SessionInfo } from "../../../../../../packages/protocol/schema";
3
+ import type { MeshRuntimeDependencies } from "../dependencies";
4
+ import { createHandoffFlow } from "../handoff";
5
+ import { prepareCloudHandoff } from "../cloud-handoff";
6
+ import { capturePreparedHandoff, preparedHandoffMetadata } from ".";
7
+ import { prepareSourceCapsule } from "./source";
8
+ import { resolveHandoffSource } from "./source-binding";
9
+
10
+ export async function prepareHandoff(
11
+ deps: MeshRuntimeDependencies,
12
+ flow: ReturnType<typeof createHandoffFlow>,
13
+ client: RelayClient,
14
+ request: MeshHandoffPrepare,
15
+ catalog: (sessions: SessionInfo[]) => SessionInfo[],
16
+ ): Promise<boolean> {
17
+ const { blockHandoff, acceptHandoff } = flow;
18
+ const sessions = catalog(deps.sessions());
19
+ const native = sessions.find((item) => item.sessionId === request.sessionId);
20
+ let session = native;
21
+ if (!session) {
22
+ const task = deps.store().task(request.taskId);
23
+ if (task?.projectId === request.projectId && task.ownerSessionId === request.sessionId) {
24
+ await blockHandoff(client, request, "The source chat is no longer available. Refresh this Task on its computer and retry.");
25
+ }
26
+ return false;
27
+ }
28
+ if (session.projectId !== request.projectId || session.taskId !== request.taskId) return false;
29
+ try { session = resolveHandoffSource(deps.store(), session, request, deps.computer()); }
30
+ catch (error) {
31
+ await blockHandoff(client, request, (error as Error).message);
32
+ return false;
33
+ }
34
+ if (request.prepareOnly && session.state === "working") {
35
+ await blockHandoff(client, request, "Pause or finish the current turn before transferring this Task.");
36
+ return false;
37
+ }
38
+ const cwd = session.worktree ?? session.cwd;
39
+ let capsule = await prepareSourceCapsule({ deps, blockHandoff, client, request, session, sessions });
40
+ if (!capsule) return false;
41
+ if (request.cloud) return prepareCloudHandoff({ deps, block: blockHandoff, client, request, session, capsule });
42
+ let transferContext: string | undefined;
43
+ let confirmContext: (() => Promise<boolean>) | undefined;
44
+ if (request.prepareOnly) {
45
+ try {
46
+ const captured = await capturePreparedHandoff(deps, session, capsule, native);
47
+ transferContext = captured.transferContext;
48
+ capsule = captured.capsule;
49
+ confirmContext = captured.confirm;
50
+ } catch (error) {
51
+ await blockHandoff(client, request, error instanceof Error ? error.message : "The complete transfer context is unavailable.");
52
+ return false;
53
+ }
54
+ }
55
+ const local = request.targetComputer === deps.computer();
56
+ if (request.push && !local && cwd) {
57
+ const pushed = deps.push(cwd, capsule.branch);
58
+ if (!pushed.ok) {
59
+ await blockHandoff(client, request, pushed.error);
60
+ return false;
61
+ }
62
+ }
63
+ const createdAt = deps.now();
64
+ const event: MeshEvent = {
65
+ type: "mesh.event",
66
+ sessionId: request.taskId,
67
+ eventId: deps.eventId(),
68
+ projectId: request.projectId,
69
+ taskId: request.taskId,
70
+ sourceSessionId: request.sessionId,
71
+ eventType: "HANDOFF_REQUEST",
72
+ createdAt,
73
+ expiresAt: createdAt + 60 * 60_000,
74
+ payload: { capsule, transferContext },
75
+ };
76
+ if (request.prepareOnly && !local && deps.dispatchPreparedTransfer) {
77
+ try {
78
+ if (await deps.dispatchPreparedTransfer(event, session, confirmContext!)) {
79
+ await deps.send(client, preparedHandoffMetadata(event), { ttlMs: 60 * 60_000, wake: true }).catch(() => {});
80
+ return true;
81
+ }
82
+ await blockHandoff(client, request, 'Project computer transfer is not configured for this destination. Connect its Project in GrantTap.');
83
+ } catch (error) {
84
+ await blockHandoff(client, request, error instanceof Error ? error.message : 'The transfer could not be saved.');
85
+ }
86
+ return false;
87
+ }
88
+ deps.store().acceptEvent(event);
89
+ await deps.send(client, request.prepareOnly ? preparedHandoffMetadata(event) : event, { ttlMs: 60 * 60_000, wake: true });
90
+ if (local) await acceptHandoff(client, event);
91
+ return true;
92
+ }
93
+
@@ -0,0 +1,24 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import type { MeshHandoffPrepare, SessionInfo } from '../../../../../../packages/protocol/schema';
3
+ import { inspectRepository } from '../../catalog';
4
+ import type { MeshStore } from '../../store';
5
+ import { matchesLocalCheckout } from '../graph/checkout-identity';
6
+
7
+ /** The controller chooses an already admitted binding, never an arbitrary path. */
8
+ export function resolveHandoffSource(store: MeshStore, session: SessionInfo,
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);
14
+ if (binding?.localPathHint) {
15
+ const facts = inspectRepository(binding.localPathHint);
16
+ try {
17
+ if (facts.worktree && facts.revision && realpathSync(facts.root) === realpathSync(binding.localPathHint)
18
+ && (facts.canonicalRepositoryId === binding.repositoryId || matchesLocalCheckout(binding.repositoryId, facts.root))) {
19
+ return { ...session, worktree: facts.root, branch: facts.branch };
20
+ }
21
+ } catch { /* A removed checkout is not an admitted replacement. */ }
22
+ }
23
+ throw new Error('The selected repository is not available on this computer. Refresh the Project and choose its checkout again.');
24
+ }
@@ -0,0 +1,45 @@
1
+ import type { RelayClient } from "../../../../../../packages/core/relay-client";
2
+ import type { MeshHandoffPrepare, SessionInfo } from "../../../../../../packages/protocol/schema";
3
+ import type { MeshRuntimeDependencies } from "../dependencies";
4
+ import type { createHandoffFlow } from "../handoff";
5
+ import { workingTreeState } from "../../catalog";
6
+ import { createCheckpoint } from "../../admin/checkpoint";
7
+ import { buildTaskCapsule } from "../../admin/capsule";
8
+ import { handoffReadiness } from "../../tasks/readiness";
9
+
10
+ /** Read source facts once, preserving the person's checkpoint choice. */
11
+ export async function prepareSourceCapsule(input: { deps: MeshRuntimeDependencies;
12
+ blockHandoff: ReturnType<typeof createHandoffFlow>["blockHandoff"]; client: RelayClient;
13
+ request: MeshHandoffPrepare; session: SessionInfo; sessions: SessionInfo[] }) {
14
+ const { deps, blockHandoff, client, request, session, sessions } = input;
15
+ const cwd = session.worktree ?? session.cwd;
16
+ const made = request.checkpoint && cwd && workingTreeState(cwd) === "dirty"
17
+ ? createCheckpoint(cwd, request.taskId, session.title ?? request.taskId, request.createdAt)
18
+ : undefined;
19
+ const shared = made && cwd
20
+ ? sessions.some((other) =>
21
+ other.sessionId !== session.sessionId
22
+ && other.taskId != null && other.taskId !== request.taskId
23
+ && (other.worktree ?? other.cwd) === cwd)
24
+ : false;
25
+ const checkpoint = made && shared ? { ...made, status: "requires_review" as const } : made;
26
+ const capsule = buildTaskCapsule(deps.store(), session, request, deps.computer(), checkpoint);
27
+ const readiness = handoffReadiness({
28
+ capsule,
29
+ targetProviderEnabled: request.targetProvider === "grok_bot"
30
+ || deps.providerEnabled(request.targetProvider),
31
+ conflicts: (capsule?.resourceClaims ?? []).flatMap((resource) =>
32
+ deps.store().conflicts(request.projectId, request.sessionId, resource, {
33
+ repositoryId: capsule?.repository, endpointId: deps.computer(), worktree: cwd,
34
+ }, true)),
35
+ moduleOverlaps: (capsule?.resourceClaims ?? []).flatMap((resource) =>
36
+ deps.store().moduleOverlaps(request.projectId, request.sessionId, resource, {
37
+ repositoryId: capsule?.repository, endpointId: deps.computer(), worktree: cwd,
38
+ })),
39
+ });
40
+ if (!readiness.ready) {
41
+ await blockHandoff(client, request, readiness.blockedReason ?? "Handoff is not ready.");
42
+ return undefined;
43
+ }
44
+ return capsule;
45
+ }
@@ -4,9 +4,11 @@ import { prepareMeshHandoff } from '../../mesh/runtime';
4
4
  import { handleTransferContinuation } from './continuation';
5
5
 
6
6
  export async function handleTaskTransferCommand(client: RelayClient,
7
- command: MeshHandoffPrepare | TaskTransferContinue, onAccepted: () => void): Promise<boolean> {
8
- if (command.type === 'task.transfer.continue') return handleTransferContinuation(client, command, onAccepted);
9
- const accepted = await prepareMeshHandoff(client, command);
10
- if (accepted) onAccepted();
11
- return accepted;
7
+ command: MeshHandoffPrepare | TaskTransferContinue, publish: () => void,
8
+ prepare = prepareMeshHandoff): Promise<boolean> {
9
+ if (command.type === 'task.transfer.continue') return handleTransferContinuation(client, command, publish);
10
+ // Rejections are durable state too. Publish them even if the direct result
11
+ // could not cross the socket, so reconnecting controllers can explain them.
12
+ try { return await prepare(client, command); }
13
+ finally { publish(); }
12
14
  }
@@ -38,6 +38,7 @@ export async function desktopTaskHandoff(
38
38
  sessionId: row.session_id, projectId: row.project_id, taskId: row.task_id,
39
39
  targetProvider: row.target_provider, targetComputer: row.target_computer,
40
40
  targetModel: row.target_model, userComment: row.user_comment,
41
+ ...(row.source_binding_id === undefined ? {} : { sourceBindingId: row.source_binding_id }),
41
42
  checkpoint: row.checkpoint, push: row.push, createdAt: Date.now(),
42
43
  ...(row.cloud === true ? { cloud: {
43
44
  ...(row.cloud_environment === undefined ? {} : { environment: row.cloud_environment }),
@@ -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.89
13
+ npm install -g granttap-mcp@0.8.90
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.89\";\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.90\";\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.89";
10
+ const PACKAGE = "granttap-mcp@0.8.90";
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.89",
3
+ "version": "0.8.90",
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": {
@@ -206,6 +206,7 @@ export const MeshClaimReleaseResult = z.object({
206
206
  export type MeshClaimReleaseResult = z.infer<typeof MeshClaimReleaseResult>;
207
207
 
208
208
  export const MeshHandoffPrepare = z.object({
209
+ sourceBindingId: Identifier.optional(),
209
210
  prepareOnly: z.boolean().optional(),
210
211
  type: z.literal("mesh.handoff.prepare"),
211
212
  sessionId: Identifier,