granttap-mcp 0.8.113 → 0.8.115
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/engine/README.md +6 -2
- package/apps/bridge/src/engine/distribution/artifacts.ts +2 -2
- package/apps/bridge/src/engine/invocation/delivery/README.md +11 -0
- package/apps/bridge/src/engine/invocation/delivery/index.ts +53 -0
- package/apps/bridge/src/engine/invocation/ingest.ts +50 -57
- package/cursor-plugin/README.md +1 -1
- package/cursor-plugin/mcp.json +1 -1
- package/cursor-plugin/stdio-bootstrap.cjs +1 -1
- package/package.json +1 -1
|
@@ -48,8 +48,12 @@ explicitly unavailable.
|
|
|
48
48
|
|
|
49
49
|
`invocation-ingest.ts` reads bounded complete transcript lines and exact-call
|
|
50
50
|
hook denial metadata without forwarding tool arguments, output, or reason text.
|
|
51
|
-
Cursor decisions without a native call ID stay unattributed.
|
|
52
|
-
|
|
51
|
+
Cursor decisions without a native call ID stay unattributed. Unconfirmed events retain their exact IDs and timestamps across IPC retries;
|
|
52
|
+
confirmed prefixes are not resent. Transient failures back off up to one minute,
|
|
53
|
+
while journal-capacity refusals wait five minutes. Source cursors advance only
|
|
54
|
+
after the retained batch is confirmed; pending sources are bounded and deferred
|
|
55
|
+
source files remain unread. This neither clears history nor increases the Engine
|
|
56
|
+
journal limit. `invocation-query.ts` returns bounded
|
|
53
57
|
Engine history to the phone under the Project key, while
|
|
54
58
|
`invocation-scope.ts` exposes one Task slice through the caller's existing
|
|
55
59
|
opaque Mesh capability. The Engine, not this bridge or Usage telemetry, owns
|
|
@@ -8,8 +8,8 @@ export type EngineArtifact = {
|
|
|
8
8
|
|
|
9
9
|
/** Release hashes are part of the reviewed MCP, never supplied by a remote manifest. */
|
|
10
10
|
export const ENGINE_ARTIFACTS: readonly EngineArtifact[] = [
|
|
11
|
-
{ version: "0.1.
|
|
12
|
-
sha256: "
|
|
11
|
+
{ version: "0.1.6", platform: "darwin", arch: "arm64",
|
|
12
|
+
sha256: "71819584e4a5edb064bcc23173edfc13ed3dcd61aa4487ba3febc9eb10708bb8", bytes: 12201872 },
|
|
13
13
|
{ version: "0.1.2", platform: "darwin", arch: "x64",
|
|
14
14
|
sha256: "b3235a15936a77ec323d5ecbc54a0bc19994fe6e5a36044da964b68524b4c1dd", bytes: 13944984 },
|
|
15
15
|
{ version: "0.1.2", platform: "linux", arch: "x64",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Invocation delivery recovery
|
|
2
|
+
|
|
3
|
+
Public entry: `index.ts`. Retains the unconfirmed suffix of a bounded transcript
|
|
4
|
+
batch across IPC failures, preserving exact event identity and source cursors.
|
|
5
|
+
Transient errors use exponential backoff; a full journal waits five minutes.
|
|
6
|
+
No durable history is deleted or silently replaced to recover capacity.
|
|
7
|
+
|
|
8
|
+
Behavior is exercised through the ingestor by
|
|
9
|
+
`tests/invocation/recovery/ingest.test.ts`.
|
|
10
|
+
|
|
11
|
+
License: GrantTap Commercial Source License in the repository-root `LICENSE`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { EngineRemoteError, type EngineOperation, type EngineResult } from '../../protocol/engine-protocol';
|
|
2
|
+
import type { InvocationEvent } from '../../protocol/engine-invocation-protocol';
|
|
3
|
+
import type { InvocationLogCursor } from '../log-reader';
|
|
4
|
+
import type { InvocationParseState } from '../parse';
|
|
5
|
+
|
|
6
|
+
export type InvocationBatch = {
|
|
7
|
+
events: InvocationEvent[];
|
|
8
|
+
nextEvent: number;
|
|
9
|
+
next: InvocationLogCursor;
|
|
10
|
+
parse?: InvocationParseState;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const MAX_PENDING_SOURCES = 32;
|
|
14
|
+
const CAPACITY_RETRY_MS = 5 * 60_000;
|
|
15
|
+
type Client = { request(operation: EngineOperation): Promise<EngineResult> };
|
|
16
|
+
|
|
17
|
+
/** Retain uncertain evidence, not a replay of every successfully committed event. */
|
|
18
|
+
export class InvocationDelivery {
|
|
19
|
+
private pending = new Map<string, InvocationBatch>();
|
|
20
|
+
private retryAt = 0;
|
|
21
|
+
private failures = 0;
|
|
22
|
+
|
|
23
|
+
constructor(private client: Client, private now: () => number = Date.now) {}
|
|
24
|
+
|
|
25
|
+
ready(): boolean { return this.now() >= this.retryAt; }
|
|
26
|
+
|
|
27
|
+
async advance(key: string, prepare: () => InvocationBatch) {
|
|
28
|
+
if (!this.pending.has(key) && this.pending.size >= MAX_PENDING_SOURCES) return undefined;
|
|
29
|
+
let batch = this.pending.get(key);
|
|
30
|
+
if (!batch) {
|
|
31
|
+
try { batch = prepare(); }
|
|
32
|
+
catch { return undefined; } // An unavailable source is not a committed cursor.
|
|
33
|
+
}
|
|
34
|
+
let accepted = 0;
|
|
35
|
+
for (; batch.nextEvent < batch.events.length; batch.nextEvent++) {
|
|
36
|
+
try {
|
|
37
|
+
await this.client.request({ operation: 'invocation.observe', input: batch.events[batch.nextEvent]! });
|
|
38
|
+
accepted++;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
this.pending.set(key, batch);
|
|
41
|
+
this.failures++;
|
|
42
|
+
const capacity = error instanceof EngineRemoteError && (error.code === 'INVOCATION_CAPACITY'
|
|
43
|
+
|| (error.code === 'INVOCATION_STATE_WRITE_FAILED' && /size limit/i.test(error.message)));
|
|
44
|
+
this.retryAt = this.now() + (capacity ? CAPACITY_RETRY_MS
|
|
45
|
+
: Math.min(60_000, 1_000 * 2 ** Math.min(this.failures - 1, 6)));
|
|
46
|
+
return { batch, accepted, complete: false };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.pending.delete(key);
|
|
50
|
+
if (accepted) { this.failures = 0; this.retryAt = 0; }
|
|
51
|
+
return { batch, accepted, complete: true };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "./parse";
|
|
18
18
|
import type { InvocationEvent } from "../protocol/engine-invocation-protocol";
|
|
19
19
|
import { parseInvocationDecision } from "./decision-parse";
|
|
20
|
+
import { InvocationDelivery, type InvocationBatch } from './delivery';
|
|
20
21
|
|
|
21
22
|
type Provider = "claude" | "codex" | "cursor";
|
|
22
23
|
type SourceState = { cursor: InvocationLogCursor; parse: InvocationParseState };
|
|
@@ -29,6 +30,7 @@ export type InvocationIngestOptions = {
|
|
|
29
30
|
paths: (session: SessionInfo) => string[];
|
|
30
31
|
inspect: (cwd: string) => RepositoryFacts;
|
|
31
32
|
decisionPath?: (session: SessionInfo) => string;
|
|
33
|
+
now?: () => number;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
function transcriptPaths(session: SessionInfo): string[] {
|
|
@@ -41,9 +43,10 @@ function transcriptPaths(session: SessionInfo): string[] {
|
|
|
41
43
|
export function createInvocationIngestor(options: InvocationIngestOptions) {
|
|
42
44
|
const states = new Map<string, SourceState>();
|
|
43
45
|
const decisionStates = new Map<string, InvocationLogCursor>();
|
|
46
|
+
const delivery = new InvocationDelivery(options.client, options.now);
|
|
44
47
|
return {
|
|
45
48
|
async ingest(sessions: readonly SessionInfo[]): Promise<number> {
|
|
46
|
-
if (!options.enabled()) return 0;
|
|
49
|
+
if (!options.enabled() || !delivery.ready()) return 0;
|
|
47
50
|
const candidates = sessions.filter((session) => session.projectId && session.taskId
|
|
48
51
|
&& ["claude", "codex", "cursor"].includes(session.agent));
|
|
49
52
|
if (candidates.length === 0) return 0;
|
|
@@ -54,68 +57,28 @@ export function createInvocationIngestor(options: InvocationIngestOptions) {
|
|
|
54
57
|
const provider = session.agent as Provider;
|
|
55
58
|
const facts = session.cwd ? options.inspect(session.cwd) : undefined;
|
|
56
59
|
for (const path of options.paths(session)) {
|
|
57
|
-
const key =
|
|
60
|
+
const key = `transcript\0${provider}\0${session.sessionId}\0${path}`;
|
|
58
61
|
const known = states.get(key);
|
|
59
|
-
let batch;
|
|
60
|
-
try { batch = readInvocationBatch(path, known?.cursor); }
|
|
61
|
-
catch { continue; }
|
|
62
|
-
const parse: InvocationParseState = {
|
|
63
|
-
pending: new Map(known?.parse.pending ?? []),
|
|
64
|
-
};
|
|
65
|
-
const records = [
|
|
66
|
-
...batch.gaps.map((offset) => ({ offset, gap: true as const })),
|
|
67
|
-
...batch.lines.map((row) => ({ ...row, gap: false as const })),
|
|
68
|
-
].sort((a, b) => a.offset - b.offset || Number(b.gap) - Number(a.gap));
|
|
69
62
|
const thread = hash([path]);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
events.push(eventFor({
|
|
78
|
-
session, provider, computer: options.computer(), thread,
|
|
79
|
-
offset: record.offset, fact, repository: facts,
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
try {
|
|
84
|
-
for (const event of events) {
|
|
85
|
-
await options.client.request({ operation: "invocation.observe", input: event });
|
|
86
|
-
accepted += 1;
|
|
87
|
-
}
|
|
88
|
-
} catch {
|
|
89
|
-
// No cursor advance: the next pass replays this batch under stable
|
|
90
|
-
// event IDs. Engine deduplicates already committed evidence.
|
|
91
|
-
return accepted;
|
|
92
|
-
}
|
|
93
|
-
states.set(key, { cursor: batch.next, parse });
|
|
63
|
+
const result = await delivery.advance(key, () => transcriptBatch(path, known, provider,
|
|
64
|
+
(fact, offset) => eventFor({ session, provider, computer: options.computer(), thread,
|
|
65
|
+
offset, fact, repository: facts })));
|
|
66
|
+
if (!result) continue;
|
|
67
|
+
accepted += result.accepted;
|
|
68
|
+
if (!result.complete) return accepted;
|
|
69
|
+
states.set(key, { cursor: result.batch.next, parse: result.batch.parse! });
|
|
94
70
|
}
|
|
95
71
|
const decisionPath = options.decisionPath?.(session);
|
|
96
72
|
if (decisionPath) {
|
|
97
|
-
const key =
|
|
98
|
-
let batch;
|
|
99
|
-
try { batch = readInvocationBatch(decisionPath, decisionStates.get(key)); }
|
|
100
|
-
catch { continue; }
|
|
73
|
+
const key = `decision\0${provider}\0${session.sessionId}\0${decisionPath}`;
|
|
101
74
|
const thread = hash([decisionPath]);
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
session, provider, computer: options.computer(), thread,
|
|
110
|
-
offset: record.offset, fact, repository: facts,
|
|
111
|
-
})));
|
|
112
|
-
try {
|
|
113
|
-
for (const event of events) {
|
|
114
|
-
await options.client.request({ operation: "invocation.observe", input: event });
|
|
115
|
-
accepted += 1;
|
|
116
|
-
}
|
|
117
|
-
} catch { return accepted; }
|
|
118
|
-
decisionStates.set(key, batch.next);
|
|
75
|
+
const result = await delivery.advance(key, () => decisionBatch(decisionPath, decisionStates.get(key), provider,
|
|
76
|
+
(fact, offset) => eventFor({ session, provider, computer: options.computer(), thread,
|
|
77
|
+
offset, fact, repository: facts })));
|
|
78
|
+
if (!result) continue;
|
|
79
|
+
accepted += result.accepted;
|
|
80
|
+
if (!result.complete) return accepted;
|
|
81
|
+
decisionStates.set(key, result.batch.next);
|
|
119
82
|
}
|
|
120
83
|
}
|
|
121
84
|
return accepted;
|
|
@@ -123,6 +86,36 @@ export function createInvocationIngestor(options: InvocationIngestOptions) {
|
|
|
123
86
|
};
|
|
124
87
|
}
|
|
125
88
|
|
|
89
|
+
type EventBuilder = (fact: InvocationFact, offset: number) => InvocationEvent;
|
|
90
|
+
|
|
91
|
+
function transcriptBatch(path: string, known: SourceState | undefined, provider: Provider, event: EventBuilder): InvocationBatch {
|
|
92
|
+
const batch = readInvocationBatch(path, known?.cursor);
|
|
93
|
+
const parse: InvocationParseState = { pending: new Map(known?.parse.pending ?? []) };
|
|
94
|
+
const records = [
|
|
95
|
+
...batch.gaps.map(offset => ({ offset, gap: true as const })),
|
|
96
|
+
...batch.lines.map(row => ({ ...row, gap: false as const })),
|
|
97
|
+
].sort((a, b) => a.offset - b.offset || Number(b.gap) - Number(a.gap));
|
|
98
|
+
const events: InvocationEvent[] = [];
|
|
99
|
+
for (const record of records) {
|
|
100
|
+
if (record.gap) parse.pending.clear();
|
|
101
|
+
const observed = record.gap ? [sourceGapFact(record.offset)]
|
|
102
|
+
: parseInvocationLine(provider, record.line, parse, record.offset);
|
|
103
|
+
for (const fact of observed) events.push(event(fact, record.offset));
|
|
104
|
+
}
|
|
105
|
+
return { events, nextEvent: 0, next: batch.next, parse };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function decisionBatch(path: string, cursor: InvocationLogCursor | undefined, provider: Provider, event: EventBuilder): InvocationBatch {
|
|
109
|
+
const batch = readInvocationBatch(path, cursor);
|
|
110
|
+
const records = [
|
|
111
|
+
...batch.gaps.map(offset => ({ offset, gap: true as const })),
|
|
112
|
+
...batch.lines.map(row => ({ ...row, gap: false as const })),
|
|
113
|
+
].sort((a, b) => a.offset - b.offset);
|
|
114
|
+
const events = records.flatMap(record => (record.gap ? [sourceGapFact(record.offset)]
|
|
115
|
+
: parseInvocationDecision(record.line, provider, record.offset)).map(fact => event(fact, record.offset)));
|
|
116
|
+
return { events, nextEvent: 0, next: batch.next };
|
|
117
|
+
}
|
|
118
|
+
|
|
126
119
|
function eventFor(input: {
|
|
127
120
|
session: SessionInfo;
|
|
128
121
|
provider: Provider;
|
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.115
|
|
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.115\";\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.115";
|
|
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;
|