cursor-opencode-provider 0.1.1
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/LICENSE +21 -0
- package/README.md +200 -0
- package/dist/auth.d.ts +48 -0
- package/dist/auth.js +200 -0
- package/dist/context/agents.d.ts +8 -0
- package/dist/context/agents.js +76 -0
- package/dist/context/build.d.ts +12 -0
- package/dist/context/build.js +83 -0
- package/dist/context/env.d.ts +1 -0
- package/dist/context/env.js +29 -0
- package/dist/context/git.d.ts +20 -0
- package/dist/context/git.js +59 -0
- package/dist/context/index.d.ts +2 -0
- package/dist/context/index.js +2 -0
- package/dist/context/layout.d.ts +17 -0
- package/dist/context/layout.js +58 -0
- package/dist/context/paths.d.ts +3 -0
- package/dist/context/paths.js +11 -0
- package/dist/context/plugins.d.ts +8 -0
- package/dist/context/plugins.js +50 -0
- package/dist/context/rules.d.ts +19 -0
- package/dist/context/rules.js +198 -0
- package/dist/context/skills.d.ts +11 -0
- package/dist/context/skills.js +104 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +18 -0
- package/dist/language-model.d.ts +45 -0
- package/dist/language-model.js +834 -0
- package/dist/models.d.ts +49 -0
- package/dist/models.js +136 -0
- package/dist/plugin-v2.d.ts +2 -0
- package/dist/plugin-v2.js +48 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +201 -0
- package/dist/protocol/blob-store.d.ts +15 -0
- package/dist/protocol/blob-store.js +52 -0
- package/dist/protocol/checkpoint.d.ts +17 -0
- package/dist/protocol/checkpoint.js +29 -0
- package/dist/protocol/checksum.d.ts +2 -0
- package/dist/protocol/checksum.js +23 -0
- package/dist/protocol/client-version.d.ts +5 -0
- package/dist/protocol/client-version.js +150 -0
- package/dist/protocol/device-id.d.ts +8 -0
- package/dist/protocol/device-id.js +121 -0
- package/dist/protocol/framing.d.ts +10 -0
- package/dist/protocol/framing.js +90 -0
- package/dist/protocol/kv.d.ts +24 -0
- package/dist/protocol/kv.js +81 -0
- package/dist/protocol/messages.d.ts +11 -0
- package/dist/protocol/messages.js +676 -0
- package/dist/protocol/request.d.ts +36 -0
- package/dist/protocol/request.js +90 -0
- package/dist/protocol/stream.d.ts +38 -0
- package/dist/protocol/stream.js +64 -0
- package/dist/protocol/struct.d.ts +19 -0
- package/dist/protocol/struct.js +186 -0
- package/dist/protocol/thinking.d.ts +15 -0
- package/dist/protocol/thinking.js +17 -0
- package/dist/protocol/tools.d.ts +94 -0
- package/dist/protocol/tools.js +631 -0
- package/dist/session.d.ts +81 -0
- package/dist/session.js +96 -0
- package/dist/shared.d.ts +15 -0
- package/dist/shared.js +13 -0
- package/dist/transport/connect.d.ts +23 -0
- package/dist/transport/connect.js +275 -0
- package/package.json +65 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { FALLBACK_CLIENT_VERSION, MODEL_CACHE_TTL_MS, VERSION_CACHE_FILE, } from "../shared.js";
|
|
4
|
+
const INSTALL_URL = "https://cursor.com/install";
|
|
5
|
+
const REMOTE_TIMEOUT_MS = 5_000;
|
|
6
|
+
const BUILD_RE = /^\d{4}\.\d{2}\.\d{2}-[0-9A-Za-z][0-9A-Za-z.-]*$/;
|
|
7
|
+
const CLIENT_VERSION_RE = /^cli-[0-9A-Za-z][0-9A-Za-z._-]*$/;
|
|
8
|
+
let cachedResolution;
|
|
9
|
+
export function resetClientVersionCache() {
|
|
10
|
+
cachedResolution = undefined;
|
|
11
|
+
}
|
|
12
|
+
export function resolveClientVersion() {
|
|
13
|
+
cachedResolution ??= resolve();
|
|
14
|
+
return cachedResolution;
|
|
15
|
+
}
|
|
16
|
+
async function resolve() {
|
|
17
|
+
const env = process.env.CURSOR_CLIENT_VERSION?.trim();
|
|
18
|
+
if (isClientVersion(env))
|
|
19
|
+
return env;
|
|
20
|
+
const local = discoverLocalVersion();
|
|
21
|
+
if (local)
|
|
22
|
+
return local;
|
|
23
|
+
return (await resolveRemoteVersion()) ?? FALLBACK_CLIENT_VERSION;
|
|
24
|
+
}
|
|
25
|
+
function isClientVersion(value) {
|
|
26
|
+
return typeof value === "string" && CLIENT_VERSION_RE.test(value);
|
|
27
|
+
}
|
|
28
|
+
export function cursorAgentVersionsDir() {
|
|
29
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
30
|
+
if (!home)
|
|
31
|
+
return undefined;
|
|
32
|
+
switch (process.platform) {
|
|
33
|
+
case "linux":
|
|
34
|
+
case "darwin":
|
|
35
|
+
return path.join(home, ".local", "share", "cursor-agent", "versions");
|
|
36
|
+
default:
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function discoverLocalVersion(dir = cursorAgentVersionsDir()) {
|
|
41
|
+
if (!dir)
|
|
42
|
+
return undefined;
|
|
43
|
+
let entries;
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
let newest;
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory() || !BUILD_RE.test(entry.name))
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const mtimeMs = fs.statSync(path.join(dir, entry.name)).mtimeMs;
|
|
56
|
+
if (!newest ||
|
|
57
|
+
mtimeMs > newest.mtimeMs ||
|
|
58
|
+
(mtimeMs === newest.mtimeMs && entry.name > newest.name)) {
|
|
59
|
+
newest = { name: entry.name, mtimeMs };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Directory disappeared during discovery.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return newest ? `cli-${newest.name}` : undefined;
|
|
67
|
+
}
|
|
68
|
+
export function extractVersionFromInstaller(script) {
|
|
69
|
+
const match = script.match(/downloads\.cursor\.com\/lab\/([^/"'\s]+)\//);
|
|
70
|
+
return match && BUILD_RE.test(match[1]) ? match[1] : undefined;
|
|
71
|
+
}
|
|
72
|
+
function resolveCacheDir() {
|
|
73
|
+
if (process.env.CURSOR_CONFIG_DIR)
|
|
74
|
+
return process.env.CURSOR_CONFIG_DIR;
|
|
75
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
76
|
+
if (!home)
|
|
77
|
+
return undefined;
|
|
78
|
+
return process.env.XDG_CONFIG_HOME
|
|
79
|
+
? path.join(process.env.XDG_CONFIG_HOME, "opencode")
|
|
80
|
+
: path.join(home, ".config", "opencode");
|
|
81
|
+
}
|
|
82
|
+
function versionCachePath() {
|
|
83
|
+
const dir = resolveCacheDir();
|
|
84
|
+
return dir ? path.join(dir, VERSION_CACHE_FILE) : undefined;
|
|
85
|
+
}
|
|
86
|
+
function readVersionCache() {
|
|
87
|
+
const file = versionCachePath();
|
|
88
|
+
if (!file)
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
92
|
+
if (!isClientVersion(value.version) ||
|
|
93
|
+
typeof value.fetchedAt !== "number" ||
|
|
94
|
+
!Number.isFinite(value.fetchedAt)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return { version: value.version, fetchedAt: value.fetchedAt };
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writeVersionCache(cache) {
|
|
104
|
+
const file = versionCachePath();
|
|
105
|
+
if (!file)
|
|
106
|
+
return;
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
109
|
+
fs.writeFileSync(file, JSON.stringify(cache, null, 2));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Cache writes are optional.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isCacheFresh(cache, now = Date.now()) {
|
|
116
|
+
const age = now - cache.fetchedAt;
|
|
117
|
+
return age >= 0 && age < MODEL_CACHE_TTL_MS;
|
|
118
|
+
}
|
|
119
|
+
async function fetchInstallerVersion() {
|
|
120
|
+
const response = await fetch(INSTALL_URL, {
|
|
121
|
+
signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS),
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
return undefined;
|
|
125
|
+
const build = extractVersionFromInstaller(await response.text());
|
|
126
|
+
return build ? `cli-${build}` : undefined;
|
|
127
|
+
}
|
|
128
|
+
async function refreshVersionCache() {
|
|
129
|
+
const version = await fetchInstallerVersion();
|
|
130
|
+
if (version)
|
|
131
|
+
writeVersionCache({ version, fetchedAt: Date.now() });
|
|
132
|
+
}
|
|
133
|
+
async function resolveRemoteVersion() {
|
|
134
|
+
const cached = readVersionCache();
|
|
135
|
+
if (cached && isCacheFresh(cached)) {
|
|
136
|
+
void refreshVersionCache().catch(() => { });
|
|
137
|
+
return cached.version;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const version = await fetchInstallerVersion();
|
|
141
|
+
if (version) {
|
|
142
|
+
writeVersionCache({ version, fetchedAt: Date.now() });
|
|
143
|
+
return version;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Use stale cache or the fallback below.
|
|
148
|
+
}
|
|
149
|
+
return cached?.version;
|
|
150
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
// Device fingerprints MUST be stable across restarts: Cursor keys "device
|
|
6
|
+
// identity" off the ids embedded in x-cursor-checksum, and treats every new
|
|
7
|
+
// machineId as a separate device ("too many connections from different
|
|
8
|
+
// devices"). The real Cursor CLI derives these from stable OS identifiers
|
|
9
|
+
// (IOPlatformUUID / MAC address) rather than persisting them, so recomputing
|
|
10
|
+
// each launch yields the SAME value forever on a given machine. We replicate
|
|
11
|
+
// that derivation verbatim so we present exactly one device, matching the CLI.
|
|
12
|
+
//
|
|
13
|
+
// Source: cursor-agent-extracted — machineId = sha256(IOPlatformUUID) with a
|
|
14
|
+
// MAC/hostname fallback; macMachineId = sha256(first non-zero MAC).
|
|
15
|
+
let _cached;
|
|
16
|
+
function sha256hex(s) {
|
|
17
|
+
return crypto.createHash("sha256").update(s, "utf8").digest("hex");
|
|
18
|
+
}
|
|
19
|
+
// macOS: `ioreg -rd1 -c IOPlatformExpertDevice` → IOPlatformUUID, parsed and
|
|
20
|
+
// normalised exactly as the CLI does.
|
|
21
|
+
function readMacOSUUID() {
|
|
22
|
+
try {
|
|
23
|
+
const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice", {
|
|
24
|
+
timeout: 5000,
|
|
25
|
+
}).toString();
|
|
26
|
+
const after = out.split("IOPlatformUUID")[1];
|
|
27
|
+
if (!after)
|
|
28
|
+
return undefined;
|
|
29
|
+
return after
|
|
30
|
+
.split("\n")[0]
|
|
31
|
+
.replace(/=|\s+|"/gi, "")
|
|
32
|
+
.toLowerCase();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Linux: machine-id files (stable, per-install).
|
|
39
|
+
function readLinuxMachineId() {
|
|
40
|
+
for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
|
|
41
|
+
try {
|
|
42
|
+
const v = fs.readFileSync(p, "utf8");
|
|
43
|
+
if (v)
|
|
44
|
+
return v.trim();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* ignore */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// Windows: MachineGuid from the registry.
|
|
53
|
+
function readWindowsMachineGuid() {
|
|
54
|
+
try {
|
|
55
|
+
const out = execSync('reg query "HKLM\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid', { timeout: 5000 }).toString();
|
|
56
|
+
const m = out.split("MachineGuid")[1];
|
|
57
|
+
if (!m)
|
|
58
|
+
return undefined;
|
|
59
|
+
const hex = m.replace(/=|\s+|"/gi, "");
|
|
60
|
+
return hex || undefined;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function platformUuid() {
|
|
67
|
+
switch (process.platform) {
|
|
68
|
+
case "darwin":
|
|
69
|
+
return readMacOSUUID();
|
|
70
|
+
case "linux":
|
|
71
|
+
return readLinuxMachineId();
|
|
72
|
+
case "win32":
|
|
73
|
+
return readWindowsMachineGuid();
|
|
74
|
+
default:
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// First non-zero MAC address across all interfaces (matches CLI's $e()).
|
|
79
|
+
function firstMacAddress() {
|
|
80
|
+
try {
|
|
81
|
+
const ifaces = os.networkInterfaces();
|
|
82
|
+
for (const list of Object.values(ifaces)) {
|
|
83
|
+
if (!list)
|
|
84
|
+
continue;
|
|
85
|
+
for (const ni of list) {
|
|
86
|
+
if (ni && ni.mac && ni.mac !== "00:00:00:00:00:00")
|
|
87
|
+
return ni.mac;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
/* ignore */
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Stable device fingerprints, derived exactly like the Cursor CLI.
|
|
98
|
+
* Cached for the process lifetime after first computation.
|
|
99
|
+
*/
|
|
100
|
+
export function getDeviceIds() {
|
|
101
|
+
if (_cached)
|
|
102
|
+
return _cached;
|
|
103
|
+
const mac = firstMacAddress();
|
|
104
|
+
const uuid = platformUuid();
|
|
105
|
+
// machineId: sha256(platform UUID); fall back to sha256(MAC) then hostname,
|
|
106
|
+
// mirroring the CLI's Xe() fallback chain.
|
|
107
|
+
let machineId;
|
|
108
|
+
if (uuid) {
|
|
109
|
+
machineId = sha256hex(uuid);
|
|
110
|
+
}
|
|
111
|
+
else if (mac) {
|
|
112
|
+
machineId = sha256hex(mac);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
machineId = sha256hex(os.hostname());
|
|
116
|
+
}
|
|
117
|
+
// macMachineId: sha256(MAC), or undefined if no interface MAC is available.
|
|
118
|
+
const macMachineId = mac ? sha256hex(mac) : undefined;
|
|
119
|
+
_cached = { machineId, macMachineId };
|
|
120
|
+
return _cached;
|
|
121
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const FLAG_GZIP = 1;
|
|
2
|
+
export declare const FLAG_END_STREAM = 2;
|
|
3
|
+
export type Frame = {
|
|
4
|
+
flags: number;
|
|
5
|
+
payload: Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
export declare function encodeFrame(flags: number, payload: Uint8Array): Uint8Array;
|
|
8
|
+
export declare function decodeFramePayload(frame: Frame): Uint8Array;
|
|
9
|
+
export declare function streamFrames(buffer: Uint8Array): Generator<Frame, void, void>;
|
|
10
|
+
export declare function asyncStreamFrames(stream: ReadableStream<Uint8Array>): AsyncGenerator<Frame, void, void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { gunzipSync, gzipSync } from "node:zlib";
|
|
2
|
+
export const FLAG_GZIP = 0x01;
|
|
3
|
+
export const FLAG_END_STREAM = 0x02;
|
|
4
|
+
export function encodeFrame(flags, payload) {
|
|
5
|
+
let data = payload;
|
|
6
|
+
if (flags & FLAG_GZIP) {
|
|
7
|
+
data = gzipSync(data);
|
|
8
|
+
}
|
|
9
|
+
const len = data.length;
|
|
10
|
+
const header = new Uint8Array(5);
|
|
11
|
+
header[0] = flags;
|
|
12
|
+
header[1] = (len >> 24) & 0xff;
|
|
13
|
+
header[2] = (len >> 16) & 0xff;
|
|
14
|
+
header[3] = (len >> 8) & 0xff;
|
|
15
|
+
header[4] = len & 0xff;
|
|
16
|
+
const out = new Uint8Array(5 + len);
|
|
17
|
+
out.set(header, 0);
|
|
18
|
+
out.set(data, 5);
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
export function decodeFramePayload(frame) {
|
|
22
|
+
return frame.flags & FLAG_GZIP ? gunzipSync(frame.payload) : frame.payload;
|
|
23
|
+
}
|
|
24
|
+
export function* streamFrames(buffer) {
|
|
25
|
+
let offset = 0;
|
|
26
|
+
while (offset + 5 <= buffer.length) {
|
|
27
|
+
const flags = buffer[offset];
|
|
28
|
+
const len = ((buffer[offset + 1] << 24) |
|
|
29
|
+
(buffer[offset + 2] << 16) |
|
|
30
|
+
(buffer[offset + 3] << 8) |
|
|
31
|
+
buffer[offset + 4]) >>> 0;
|
|
32
|
+
const start = offset + 5;
|
|
33
|
+
if (start + len > buffer.length)
|
|
34
|
+
break;
|
|
35
|
+
yield { flags, payload: buffer.subarray(start, start + len) };
|
|
36
|
+
offset = start + len;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function* asyncStreamFrames(stream) {
|
|
40
|
+
const reader = stream.getReader();
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let pendingLength = 0;
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done && chunks.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
if (value) {
|
|
49
|
+
chunks.push(value);
|
|
50
|
+
pendingLength += value.length;
|
|
51
|
+
}
|
|
52
|
+
// Try to parse frames from accumulated data
|
|
53
|
+
const merged = mergeChunks(chunks);
|
|
54
|
+
const frames = Array.from(streamFrames(merged));
|
|
55
|
+
const consumed = frames.reduce((sum, f) => sum + 5 + f.payload.length, 0);
|
|
56
|
+
if (frames.length > 0) {
|
|
57
|
+
for (const frame of frames) {
|
|
58
|
+
yield frame;
|
|
59
|
+
}
|
|
60
|
+
// Keep any trailing bytes for next iteration
|
|
61
|
+
const remaining = merged.subarray(consumed);
|
|
62
|
+
chunks.length = 0;
|
|
63
|
+
if (remaining.length > 0) {
|
|
64
|
+
chunks.push(remaining);
|
|
65
|
+
pendingLength = remaining.length;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
pendingLength = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (done)
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
reader.releaseLock();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function mergeChunks(chunks) {
|
|
80
|
+
if (chunks.length === 1)
|
|
81
|
+
return chunks[0];
|
|
82
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
83
|
+
const merged = new Uint8Array(total);
|
|
84
|
+
let offset = 0;
|
|
85
|
+
for (const c of chunks) {
|
|
86
|
+
merged.set(c, offset);
|
|
87
|
+
offset += c.length;
|
|
88
|
+
}
|
|
89
|
+
return merged;
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CursorSession } from "../session.js";
|
|
2
|
+
/**
|
|
3
|
+
* Handle a server `KvServerMessage` (AgentServerMessage #4). Cursor moves large
|
|
4
|
+
* payloads out-of-band via this blob channel:
|
|
5
|
+
* - `set_blob_args{blob_id, blob_data}` → server stores a blob on the client.
|
|
6
|
+
* We persist it (per-session + durable per conversation_id) and ACK.
|
|
7
|
+
* - `get_blob_args{blob_id}` → server asks the client for a blob it stored.
|
|
8
|
+
* We return `get_blob_result{blob_data}` (empty if unknown hash).
|
|
9
|
+
* The reply MUST be sent as `AgentClientMessage.kv_client_message` (field #3) on
|
|
10
|
+
* the same Run stream, echoing `id`. If we don't reply, the server hangs the
|
|
11
|
+
* turn (endless heartbeats, never any interaction_update) — the "no response"
|
|
12
|
+
* root cause.
|
|
13
|
+
*
|
|
14
|
+
* Returns the AgentClientMessage bytes to write back, or null if the message
|
|
15
|
+
* carried no get/set blob request.
|
|
16
|
+
*/
|
|
17
|
+
export declare function handleKvServerMessage(ksm: Record<string, unknown>, session: CursorSession): {
|
|
18
|
+
reply: Uint8Array;
|
|
19
|
+
kind: "set" | "get";
|
|
20
|
+
id: number;
|
|
21
|
+
blobIdHex: string;
|
|
22
|
+
found: boolean;
|
|
23
|
+
echoed?: boolean;
|
|
24
|
+
} | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { encodeMessage } from "./messages.js";
|
|
2
|
+
import { getConversationBlob, isBlobIdHash, setConversationBlob, } from "./blob-store.js";
|
|
3
|
+
function hex(b) {
|
|
4
|
+
let s = "";
|
|
5
|
+
for (let i = 0; i < b.length; i++)
|
|
6
|
+
s += b[i].toString(16).padStart(2, "0");
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Handle a server `KvServerMessage` (AgentServerMessage #4). Cursor moves large
|
|
11
|
+
* payloads out-of-band via this blob channel:
|
|
12
|
+
* - `set_blob_args{blob_id, blob_data}` → server stores a blob on the client.
|
|
13
|
+
* We persist it (per-session + durable per conversation_id) and ACK.
|
|
14
|
+
* - `get_blob_args{blob_id}` → server asks the client for a blob it stored.
|
|
15
|
+
* We return `get_blob_result{blob_data}` (empty if unknown hash).
|
|
16
|
+
* The reply MUST be sent as `AgentClientMessage.kv_client_message` (field #3) on
|
|
17
|
+
* the same Run stream, echoing `id`. If we don't reply, the server hangs the
|
|
18
|
+
* turn (endless heartbeats, never any interaction_update) — the "no response"
|
|
19
|
+
* root cause.
|
|
20
|
+
*
|
|
21
|
+
* Returns the AgentClientMessage bytes to write back, or null if the message
|
|
22
|
+
* carried no get/set blob request.
|
|
23
|
+
*/
|
|
24
|
+
export function handleKvServerMessage(ksm, session) {
|
|
25
|
+
const id = ksm.id ?? 0;
|
|
26
|
+
const setArgs = ksm.set_blob_args;
|
|
27
|
+
const getArgs = ksm.get_blob_args;
|
|
28
|
+
if (setArgs && setArgs.blob_id) {
|
|
29
|
+
const data = setArgs.blob_data ?? new Uint8Array(0);
|
|
30
|
+
const key = hex(setArgs.blob_id);
|
|
31
|
+
session.blobs.set(key, data);
|
|
32
|
+
// Survive across Run streams — required for checkpoint echo on turn 2+.
|
|
33
|
+
if (session.conversationId) {
|
|
34
|
+
setConversationBlob(session.conversationId, setArgs.blob_id, data);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
kind: "set",
|
|
38
|
+
id,
|
|
39
|
+
blobIdHex: key,
|
|
40
|
+
found: true,
|
|
41
|
+
reply: encodeMessage("AgentClientMessage", {
|
|
42
|
+
kv_client_message: { id, set_blob_result: {} },
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (getArgs && getArgs.blob_id) {
|
|
47
|
+
const key = hex(getArgs.blob_id);
|
|
48
|
+
const stored = session.blobs.get(key) ??
|
|
49
|
+
(session.conversationId
|
|
50
|
+
? getConversationBlob(session.conversationId, getArgs.blob_id)
|
|
51
|
+
: undefined);
|
|
52
|
+
let blobData;
|
|
53
|
+
let echoed = false;
|
|
54
|
+
let found = false;
|
|
55
|
+
if (stored) {
|
|
56
|
+
blobData = stored;
|
|
57
|
+
found = true;
|
|
58
|
+
}
|
|
59
|
+
else if (!isBlobIdHash(getArgs.blob_id)) {
|
|
60
|
+
// Content-as-id: server sometimes sends blob_id as the literal payload
|
|
61
|
+
// (e.g. inline JSON). Echo it. Never do this for 32-byte hashes — that
|
|
62
|
+
// made the server JSON.parse binary and return "Unexpected token".
|
|
63
|
+
blobData = getArgs.blob_id;
|
|
64
|
+
echoed = true;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
blobData = new Uint8Array(0);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: "get",
|
|
71
|
+
id,
|
|
72
|
+
blobIdHex: key,
|
|
73
|
+
found,
|
|
74
|
+
echoed,
|
|
75
|
+
reply: encodeMessage("AgentClientMessage", {
|
|
76
|
+
kv_client_message: { id, get_blob_result: { blob_data: blobData } },
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import protobuf from "protobufjs";
|
|
2
|
+
export declare function createMessageTypes(): protobuf.Root;
|
|
3
|
+
export declare function getMessageTypes(): protobuf.Root;
|
|
4
|
+
export declare function encodeMessage(typeName: string, message: Record<string, unknown>): Uint8Array;
|
|
5
|
+
export declare function decodeMessage<T = Record<string, unknown>>(typeName: string, data: Uint8Array): T;
|
|
6
|
+
/**
|
|
7
|
+
* Decode a protobuf sub-message from a frame payload that wraps it in
|
|
8
|
+
* a top-level field key + length varint. Skips the outer wrapper and
|
|
9
|
+
* decodes the inner body as `typeName`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function decodeWrappedMessage<T = Record<string, unknown>>(typeName: string, data: Uint8Array): T;
|