granttap-mcp 0.8.89 → 0.8.91
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/host/session-keys.ts +2 -1
- package/apps/bridge/src/mesh/admin/capsule.ts +10 -1
- package/apps/bridge/src/mesh/catalog/binding-state.ts +5 -0
- package/apps/bridge/src/mesh/catalog/index.ts +8 -2
- package/apps/bridge/src/mesh/knowledge/README.md +8 -0
- package/apps/bridge/src/mesh/knowledge/read.ts +65 -0
- package/apps/bridge/src/mesh/knowledge/runtime-read.ts +49 -0
- package/apps/bridge/src/mesh/runtime/index.ts +2 -105
- package/apps/bridge/src/mesh/runtime/prepared-transfer/README.md +26 -0
- package/apps/bridge/src/mesh/runtime/prepared-transfer/index.ts +6 -4
- package/apps/bridge/src/mesh/runtime/prepared-transfer/prepare.ts +93 -0
- package/apps/bridge/src/mesh/runtime/prepared-transfer/source-binding.ts +24 -0
- package/apps/bridge/src/mesh/runtime/prepared-transfer/source.ts +45 -0
- package/apps/bridge/src/monitor/handlers/inbound.ts +3 -0
- package/apps/bridge/src/policy/governed-projects.ts +1 -1
- package/apps/bridge/src/task-routing/admission/index.ts +2 -1
- package/apps/bridge/src/task-transfer/runtime/commands.ts +7 -5
- package/apps/mcp/src/desktop/delivery/task-handoff.ts +1 -0
- package/apps/mcp/src/desktop/engine-bridge.ts +1 -0
- package/apps/mcp/src/desktop/operations/index.ts +2 -0
- 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 +2 -2
- package/packages/protocol/messages/approval-questions/index.ts +1 -1
- package/packages/protocol/messages/mesh/events.ts +1 -0
- package/packages/protocol/messages/mesh/knowledge/refresh.ts +44 -0
- package/packages/protocol/messages/sessions/index.ts +2 -2
- package/packages/protocol/schema.ts +4 -0
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
MeshEvent,
|
|
9
9
|
MeshSnapshot,
|
|
10
10
|
MeshInvocationPage,
|
|
11
|
+
ProjectKnowledgePage,
|
|
11
12
|
Payload,
|
|
12
13
|
ProjectPolicyPayload,
|
|
13
14
|
Role,
|
|
@@ -53,7 +54,7 @@ export async function sendMeshPayload(
|
|
|
53
54
|
|
|
54
55
|
export async function sendProjectPayload(
|
|
55
56
|
client: RelayClient,
|
|
56
|
-
payload: ProjectPolicyPayload | MeshClaimReleaseResult | MeshInvocationPage,
|
|
57
|
+
payload: ProjectPolicyPayload | MeshClaimReleaseResult | MeshInvocationPage | ProjectKnowledgePage,
|
|
57
58
|
to: Role | "all" = "phone",
|
|
58
59
|
options: SendOptions = {},
|
|
59
60
|
): Promise<void> {
|
|
@@ -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:
|
|
237
|
+
uncommitted: input.inspectWorkingTree(repository.worktree ?? cwd),
|
|
232
238
|
startedAt: session.startedAt,
|
|
233
239
|
});
|
|
234
240
|
return { ...session, projectId, taskId, computerId, worktree: repository.worktree };
|
|
@@ -12,3 +12,11 @@ Engine validates its scope. The phone may replay the same ID after a lost reply.
|
|
|
12
12
|
This is not a migration of old chats into verified knowledge.
|
|
13
13
|
|
|
14
14
|
GrantTap Commercial Source License.
|
|
15
|
+
|
|
16
|
+
`read.ts` is the bounded Project evidence read: Memory and the invocation journal
|
|
17
|
+
run in parallel with independent deadlines. Calls may belong to older Tasks
|
|
18
|
+
outside the current phone snapshot. A failed source leaves the other source
|
|
19
|
+
available. Whole records are omitted to fit the encrypted frame budget; record
|
|
20
|
+
content is never shortened. `runtime-read.ts` exposes this read over local Mac
|
|
21
|
+
IPC and an authenticated, requester-scoped Project relay response. It does not
|
|
22
|
+
rescan provider chats or rebuild architecture.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { MeshSnapshot, ProjectKnowledgeRecord } from "../../../../../packages/protocol/schema";
|
|
2
|
+
import { ProjectKnowledgePage, ProjectKnowledgeQuery } from "../../../../../packages/protocol/messages/mesh/knowledge/refresh";
|
|
3
|
+
import type { InvocationHistoryPage } from "../../engine/protocol/engine-invocation-protocol";
|
|
4
|
+
import { activeProjectKnowledge, supersededProjectKnowledgeIds } from "./active";
|
|
5
|
+
|
|
6
|
+
export type KnowledgeReadDependencies = {
|
|
7
|
+
snapshot: (projectId: string) => MeshSnapshot | undefined;
|
|
8
|
+
memory: (projectId: string) => Promise<ProjectKnowledgeRecord[] | undefined>;
|
|
9
|
+
history: (projectId: string) => Promise<InvocationHistoryPage | undefined>;
|
|
10
|
+
endpointId: string;
|
|
11
|
+
now: () => number;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function readProjectKnowledge(
|
|
16
|
+
query: ProjectKnowledgeQuery, dependencies: KnowledgeReadDependencies,
|
|
17
|
+
): Promise<ProjectKnowledgePage | undefined> {
|
|
18
|
+
if (!ProjectKnowledgeQuery.safeParse(query).success) return undefined;
|
|
19
|
+
const snapshot = dependencies.snapshot(query.projectId);
|
|
20
|
+
if (!snapshot || snapshot.projectId !== query.projectId) return undefined;
|
|
21
|
+
const timeout = dependencies.timeoutMs ?? 2_000;
|
|
22
|
+
let [memory, history] = await Promise.all([
|
|
23
|
+
boundedSource(() => dependencies.memory(query.projectId), timeout),
|
|
24
|
+
boundedSource(() => dependencies.history(query.projectId), timeout),
|
|
25
|
+
]);
|
|
26
|
+
if (memory?.some(record => record.projectId !== query.projectId || record.visibility !== "project")) {
|
|
27
|
+
memory = undefined;
|
|
28
|
+
}
|
|
29
|
+
if (history?.events.some(row => row.event.project_id !== query.projectId)) history = undefined;
|
|
30
|
+
const records = memory ?? [];
|
|
31
|
+
const knowledge = activeProjectKnowledge(query.projectId, records)
|
|
32
|
+
.sort((a, b) => b.recordedAt - a.recordedAt).slice(0, 32);
|
|
33
|
+
const invocations = (history?.events ?? []).slice(-32);
|
|
34
|
+
const page: ProjectKnowledgePage = {
|
|
35
|
+
type: "mesh.knowledge.page", sessionId: query.projectId, projectId: query.projectId,
|
|
36
|
+
requestId: query.requestId, sourceEndpointId: dependencies.endpointId,
|
|
37
|
+
memoryAvailability: memory ? "ready" : "unavailable",
|
|
38
|
+
invocationAvailability: history ? "ready" : "unavailable",
|
|
39
|
+
knowledge, invocations,
|
|
40
|
+
supersededKnowledgeRecordIds: supersededProjectKnowledgeIds(query.projectId, records),
|
|
41
|
+
incomplete: records.length > knowledge.length || history?.has_older === true
|
|
42
|
+
|| history?.has_more === true,
|
|
43
|
+
generatedAt: dependencies.now(),
|
|
44
|
+
};
|
|
45
|
+
// Keep encrypted frames below the transport limit without shortening a fact.
|
|
46
|
+
while (Buffer.byteLength(JSON.stringify(page)) > 128 * 1_024
|
|
47
|
+
&& (knowledge.length > 0 || invocations.length > 0)) {
|
|
48
|
+
const oldestRecord = knowledge.at(-1)?.recordedAt ?? Infinity;
|
|
49
|
+
const oldestCall = invocations[0]?.event.occurred_at ?? Infinity;
|
|
50
|
+
if (oldestRecord <= oldestCall) knowledge.pop();
|
|
51
|
+
else invocations.shift();
|
|
52
|
+
page.incomplete = true;
|
|
53
|
+
}
|
|
54
|
+
return ProjectKnowledgePage.parse(page);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function boundedSource<T>(read: () => Promise<T>, timeoutMs: number): Promise<T | undefined> {
|
|
58
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
59
|
+
try {
|
|
60
|
+
return await Promise.race([Promise.resolve().then(read), new Promise<undefined>(resolve => {
|
|
61
|
+
timer = setTimeout(() => resolve(undefined), timeoutMs);
|
|
62
|
+
})]);
|
|
63
|
+
} catch { return undefined; }
|
|
64
|
+
finally { if (timer) clearTimeout(timer); }
|
|
65
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { RelayClient } from "../../../../../packages/core/relay-client";
|
|
3
|
+
import { ProjectKnowledgeQuery } from "../../../../../packages/protocol/messages/mesh/knowledge/refresh";
|
|
4
|
+
import { configDir } from "../../config/runtime/paths";
|
|
5
|
+
import { EngineClient } from "../../engine/runtime/engine-client";
|
|
6
|
+
import { engineFeatureEnabled } from "../../engine/runtime/engine-supervisor";
|
|
7
|
+
import { projectKnowledge } from "../../engine/runtime/engine-memory";
|
|
8
|
+
import { sendProjectPayload } from "../../host/session-keys";
|
|
9
|
+
import { computerId } from "../identity/computer";
|
|
10
|
+
import { localMeshStore } from "../local-remote/local";
|
|
11
|
+
import { readProjectKnowledge } from "./read";
|
|
12
|
+
|
|
13
|
+
const engine = new EngineClient({ socketPath: join(configDir(), "engine.sock") });
|
|
14
|
+
|
|
15
|
+
export async function currentProjectKnowledge(query: ProjectKnowledgeQuery) {
|
|
16
|
+
return readProjectKnowledge(query, {
|
|
17
|
+
snapshot: projectId => localMeshStore().snapshot(projectId),
|
|
18
|
+
endpointId: computerId(), now: Date.now,
|
|
19
|
+
memory: projectId => projectKnowledge(projectId),
|
|
20
|
+
history: async projectId => {
|
|
21
|
+
if (!engineFeatureEnabled()) return undefined;
|
|
22
|
+
const result = await engine.request({ operation: "invocation.history",
|
|
23
|
+
input: { project_id: projectId, tail: true, limit: 32 } }, { timeoutMs: 1_000 });
|
|
24
|
+
return result.operation === "invocation.history" ? result.page : undefined;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleProjectKnowledgeQuery(
|
|
30
|
+
client: RelayClient, query: ProjectKnowledgeQuery, peerPublicKey?: string,
|
|
31
|
+
): Promise<boolean> {
|
|
32
|
+
if (!peerPublicKey) return false;
|
|
33
|
+
const page = await currentProjectKnowledge(query);
|
|
34
|
+
if (!page) return false;
|
|
35
|
+
if (page.memoryAvailability === "ready") {
|
|
36
|
+
localMeshStore().cacheKnowledge(page.projectId, page.knowledge);
|
|
37
|
+
}
|
|
38
|
+
await sendProjectPayload(client, page, "phone", {
|
|
39
|
+
peerPublicKey, ttlMs: 15_000, reliable: false,
|
|
40
|
+
}).catch(() => {});
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function desktopProjectKnowledge(input: unknown) {
|
|
45
|
+
const value = input as { project_id?: unknown; request_id?: unknown } | undefined;
|
|
46
|
+
const query = ProjectKnowledgeQuery.safeParse({ type: "mesh.knowledge.query",
|
|
47
|
+
projectId: value?.project_id, sessionId: value?.project_id, requestId: value?.request_id });
|
|
48
|
+
return query.success ? currentProjectKnowledge(query.data) : undefined;
|
|
49
|
+
}
|
|
@@ -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
|
|
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 {
|
|
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,
|
|
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) !== (
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../../mesh/runtime";
|
|
19
19
|
import { releaseClaimByPerson, releaseResult } from "../../mesh/admin";
|
|
20
20
|
import { handleInvocationQuery } from "../../engine/invocation/query";
|
|
21
|
+
import { handleProjectKnowledgeQuery } from "../../mesh/knowledge/runtime-read";
|
|
21
22
|
import { applyPairingJoin } from "../../pairing";
|
|
22
23
|
import { recordPhoneSeen } from "../../pairing/presence";
|
|
23
24
|
import { handleProjectPolicySet } from "../../project-policy/runtime";
|
|
@@ -123,6 +124,8 @@ export async function handleMonitorMessage(
|
|
|
123
124
|
const accepted = requestProjectCapability(payload);
|
|
124
125
|
if (accepted) void publish().catch(() => {});
|
|
125
126
|
return accepted;
|
|
127
|
+
} else if (payload.type === "mesh.knowledge.query" && loadRuntimeConfig().meshEnabled) {
|
|
128
|
+
return handleProjectKnowledgeQuery(client, payload, input.peerPublicKey);
|
|
126
129
|
} else if (payload.type === "mesh.invocation.query" && loadRuntimeConfig().meshEnabled) {
|
|
127
130
|
return handleInvocationQuery(client, payload);
|
|
128
131
|
} else if ((payload.type === "mesh.event" || payload.type === "mesh.snapshot")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { TaskRoutingNeeds } from '../../../../../packages/protocol/task-resources';
|
|
2
3
|
import { loadConfig, loadRuntimeConfig, machineConfigPath } from '../../config';
|
|
3
4
|
import { applyNetworkRoute } from '../../device-network/settings';
|
|
4
5
|
import { scanSessions } from '../../sessions';
|
|
@@ -14,7 +15,7 @@ import { observedRoutingSession } from '../observed-turn';
|
|
|
14
15
|
|
|
15
16
|
export const OffloadInput = z.object({ action: z.enum(['request', 'cancel', 'status']).optional(),
|
|
16
17
|
operationId: z.string().trim().min(1).max(128).optional(),
|
|
17
|
-
requirements:
|
|
18
|
+
requirements: TaskRoutingNeeds.partial().extend({ reason: RoutingRequirementsInput.shape.reason }).optional() }).strict();
|
|
18
19
|
export type OffloadArguments = z.infer<typeof OffloadInput>;
|
|
19
20
|
type Proof = { sessionId: string; provider: RoutingRequestSource['provider'] };
|
|
20
21
|
type Ports = { store: RoutingRequests; enabled: () => boolean;
|
|
@@ -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,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if
|
|
11
|
-
|
|
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 }),
|
|
@@ -21,6 +21,7 @@ export const DESKTOP_OPERATIONS = new Set([
|
|
|
21
21
|
"desktop.machine_load", "desktop.capability_usage", "desktop.task_image", "desktop.task_workspace",
|
|
22
22
|
"desktop.installed_skills", "desktop.policy_status", "desktop.policy_set", "desktop.project_auto_accept",
|
|
23
23
|
"desktop.mesh_snapshots",
|
|
24
|
+
"desktop.project_knowledge",
|
|
24
25
|
"desktop.task_send", "desktop.task_queue", "desktop.transfer_continue", "desktop.task_control", "desktop.task_preferences",
|
|
25
26
|
"desktop.task_handoff",
|
|
26
27
|
"desktop.project_transport",
|
|
@@ -20,6 +20,7 @@ import { desktopTaskHandoff } from "../delivery/task-handoff";
|
|
|
20
20
|
import { desktopTaskCreate } from "../delivery/task-create";
|
|
21
21
|
import { desktopMeshCreate } from "../delivery/mesh-create";
|
|
22
22
|
import { desktopInvocationHistory } from "../invocation";
|
|
23
|
+
import { desktopProjectKnowledge } from "../../../../bridge/src/mesh/knowledge/runtime-read";
|
|
23
24
|
import { desktopUsage } from "../projection/usage";
|
|
24
25
|
import { desktopInstalledSkills } from "../projection/installed-skills";
|
|
25
26
|
import type { DesktopControllerEnrollment } from "../pairing";
|
|
@@ -74,6 +75,7 @@ export function desktopReadOperation(input: {
|
|
|
74
75
|
if (operation === "desktop.invocation_history") {
|
|
75
76
|
return desktopInvocationHistory(queryInput, engine, storePath);
|
|
76
77
|
}
|
|
78
|
+
if (operation === "desktop.project_knowledge") return desktopProjectKnowledge(queryInput);
|
|
77
79
|
if (operation === "desktop.project") {
|
|
78
80
|
return Promise.resolve(desktopMeshProject(queryInput, storePath));
|
|
79
81
|
}
|
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.91
|
|
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.91\";\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.91";
|
|
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.
|
|
3
|
+
"version": "0.8.91",
|
|
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": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"tsx": "^4.23.4",
|
|
43
43
|
"tweetnacl": "^1.0.3",
|
|
44
44
|
"ws": "^8.18.0",
|
|
45
|
-
"zod": "
|
|
45
|
+
"zod": "4.6.7"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/express": "^5.0.3",
|
|
@@ -15,7 +15,7 @@ export function questionAnswers(questions: z.infer<typeof ApprovalQuestions> | u
|
|
|
15
15
|
note: string | null | undefined): Record<string, string> | undefined {
|
|
16
16
|
if (!questions || !note || note.length > 20_000) return undefined;
|
|
17
17
|
try {
|
|
18
|
-
const answers = z.record(z.string().trim().min(1).max(4_000)).parse(JSON.parse(note));
|
|
18
|
+
const answers = z.record(z.string(), z.string().trim().min(1).max(4_000)).parse(JSON.parse(note));
|
|
19
19
|
if (Object.keys(answers).length !== questions.length
|
|
20
20
|
|| questions.some(item => !Object.hasOwn(answers, item.question))) return undefined;
|
|
21
21
|
return answers;
|
|
@@ -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,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Identifier } from "../endpoint";
|
|
3
|
+
import { MeshInvocationEvent } from "../invocations";
|
|
4
|
+
import { ProjectKnowledgeRecord } from "./index";
|
|
5
|
+
|
|
6
|
+
const Sequence = z.number().int().nonnegative().safe();
|
|
7
|
+
export const ProjectKnowledgeQuery = z.object({
|
|
8
|
+
type: z.literal("mesh.knowledge.query"),
|
|
9
|
+
sessionId: Identifier, projectId: Identifier, requestId: Identifier,
|
|
10
|
+
}).strict().refine(value => value.sessionId === value.projectId, "Project scope mismatch");
|
|
11
|
+
export type ProjectKnowledgeQuery = z.infer<typeof ProjectKnowledgeQuery>;
|
|
12
|
+
|
|
13
|
+
/** One bounded Project read; no architecture rebuild or per-Task fan-out. */
|
|
14
|
+
export const ProjectKnowledgePage = z.object({
|
|
15
|
+
type: z.literal("mesh.knowledge.page"),
|
|
16
|
+
sessionId: Identifier, projectId: Identifier, requestId: Identifier,
|
|
17
|
+
sourceEndpointId: Identifier,
|
|
18
|
+
memoryAvailability: z.enum(["ready", "unavailable"]),
|
|
19
|
+
invocationAvailability: z.enum(["ready", "unavailable"]),
|
|
20
|
+
knowledge: z.array(ProjectKnowledgeRecord).max(32),
|
|
21
|
+
supersededKnowledgeRecordIds: z.array(Identifier).max(128),
|
|
22
|
+
invocations: z.array(z.object({ sequence: Sequence, event: MeshInvocationEvent }).strict()).max(32),
|
|
23
|
+
incomplete: z.boolean(), generatedAt: Sequence,
|
|
24
|
+
}).strict().superRefine((value, ctx) => {
|
|
25
|
+
let previous = 0;
|
|
26
|
+
const seenCalls = new Set<string>();
|
|
27
|
+
const orderedCalls = value.invocations.every(row => {
|
|
28
|
+
const valid = row.sequence > previous && !seenCalls.has(row.event.event_id);
|
|
29
|
+
previous = row.sequence;
|
|
30
|
+
seenCalls.add(row.event.event_id);
|
|
31
|
+
return valid;
|
|
32
|
+
});
|
|
33
|
+
if (!orderedCalls
|
|
34
|
+
|| new Set(value.knowledge.map(row => row.recordId)).size !== value.knowledge.length
|
|
35
|
+
|| new Set(value.supersededKnowledgeRecordIds).size !== value.supersededKnowledgeRecordIds.length
|
|
36
|
+
|| value.sessionId !== value.projectId
|
|
37
|
+
|| value.knowledge.some(record => record.projectId !== value.projectId || record.visibility !== "project")
|
|
38
|
+
|| value.invocations.some(row => row.event.project_id !== value.projectId)
|
|
39
|
+
|| value.memoryAvailability === "unavailable" && value.knowledge.length > 0
|
|
40
|
+
|| value.invocationAvailability === "unavailable" && value.invocations.length > 0) {
|
|
41
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Knowledge source or Project scope mismatch" });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
export type ProjectKnowledgePage = z.infer<typeof ProjectKnowledgePage>;
|
|
@@ -149,8 +149,8 @@ export const SessionsStatus = z.object({
|
|
|
149
149
|
gatingEnabled: z.boolean().optional(),
|
|
150
150
|
excludedSessions: z.array(z.string()).optional(),
|
|
151
151
|
autoAcceptDefault: AutoAcceptLevel.optional(),
|
|
152
|
-
autoAcceptBySession: z.record(AutoAcceptLevel).optional(),
|
|
153
|
-
autoAcceptByProject: z.record(AutoAcceptLevel).optional(),
|
|
152
|
+
autoAcceptBySession: z.record(z.string(), AutoAcceptLevel).optional(),
|
|
153
|
+
autoAcceptByProject: z.record(z.string(), AutoAcceptLevel).optional(),
|
|
154
154
|
autoAcceptPaused: z.boolean().optional(),
|
|
155
155
|
providerSettings: ProviderRuntimeSettings.optional(),
|
|
156
156
|
meshEnabled: z.boolean().optional(),
|
|
@@ -45,6 +45,7 @@ import { PairingJoin } from "./messages/pairing-join";
|
|
|
45
45
|
import { ControllerPairOffer, ControllerPairRequest } from "./messages/device/controller-pairing";
|
|
46
46
|
import { AccountMachineLink } from "./messages/device/account-machine-link";
|
|
47
47
|
import { KnowledgeWrite, KnowledgeWriteResult } from "./messages/mesh/knowledge/write";
|
|
48
|
+
import { ProjectKnowledgeQuery, ProjectKnowledgePage } from "./messages/mesh/knowledge/refresh";
|
|
48
49
|
import { MeshInvocationPage, MeshInvocationQuery } from "./messages/mesh/invocations";
|
|
49
50
|
import {
|
|
50
51
|
ProjectPolicyAck,
|
|
@@ -82,6 +83,7 @@ export * from "./messages/pairing-join";
|
|
|
82
83
|
export * from "./messages/device/controller-pairing";
|
|
83
84
|
export * from "./messages/device/account-machine-link";
|
|
84
85
|
export * from "./messages/mesh/knowledge/write";
|
|
86
|
+
export * from "./messages/mesh/knowledge/refresh";
|
|
85
87
|
export * from "./messages/mesh";
|
|
86
88
|
export * from "./messages/mesh/invocations";
|
|
87
89
|
export * from "./messages/primitives";
|
|
@@ -165,6 +167,8 @@ export const Payload = z.union([
|
|
|
165
167
|
ProjectCapabilityRequestSet,
|
|
166
168
|
MeshInvocationQuery,
|
|
167
169
|
MeshInvocationPage,
|
|
170
|
+
ProjectKnowledgeQuery,
|
|
171
|
+
ProjectKnowledgePage,
|
|
168
172
|
MeshEndpointPolicy,
|
|
169
173
|
ProjectPolicySet,
|
|
170
174
|
ProjectPolicyStatus,
|