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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/auth.d.ts +48 -0
  4. package/dist/auth.js +200 -0
  5. package/dist/context/agents.d.ts +8 -0
  6. package/dist/context/agents.js +76 -0
  7. package/dist/context/build.d.ts +12 -0
  8. package/dist/context/build.js +83 -0
  9. package/dist/context/env.d.ts +1 -0
  10. package/dist/context/env.js +29 -0
  11. package/dist/context/git.d.ts +20 -0
  12. package/dist/context/git.js +59 -0
  13. package/dist/context/index.d.ts +2 -0
  14. package/dist/context/index.js +2 -0
  15. package/dist/context/layout.d.ts +17 -0
  16. package/dist/context/layout.js +58 -0
  17. package/dist/context/paths.d.ts +3 -0
  18. package/dist/context/paths.js +11 -0
  19. package/dist/context/plugins.d.ts +8 -0
  20. package/dist/context/plugins.js +50 -0
  21. package/dist/context/rules.d.ts +19 -0
  22. package/dist/context/rules.js +198 -0
  23. package/dist/context/skills.d.ts +11 -0
  24. package/dist/context/skills.js +104 -0
  25. package/dist/index.d.ts +15 -0
  26. package/dist/index.js +18 -0
  27. package/dist/language-model.d.ts +45 -0
  28. package/dist/language-model.js +834 -0
  29. package/dist/models.d.ts +49 -0
  30. package/dist/models.js +136 -0
  31. package/dist/plugin-v2.d.ts +2 -0
  32. package/dist/plugin-v2.js +48 -0
  33. package/dist/plugin.d.ts +2 -0
  34. package/dist/plugin.js +201 -0
  35. package/dist/protocol/blob-store.d.ts +15 -0
  36. package/dist/protocol/blob-store.js +52 -0
  37. package/dist/protocol/checkpoint.d.ts +17 -0
  38. package/dist/protocol/checkpoint.js +29 -0
  39. package/dist/protocol/checksum.d.ts +2 -0
  40. package/dist/protocol/checksum.js +23 -0
  41. package/dist/protocol/client-version.d.ts +5 -0
  42. package/dist/protocol/client-version.js +150 -0
  43. package/dist/protocol/device-id.d.ts +8 -0
  44. package/dist/protocol/device-id.js +121 -0
  45. package/dist/protocol/framing.d.ts +10 -0
  46. package/dist/protocol/framing.js +90 -0
  47. package/dist/protocol/kv.d.ts +24 -0
  48. package/dist/protocol/kv.js +81 -0
  49. package/dist/protocol/messages.d.ts +11 -0
  50. package/dist/protocol/messages.js +676 -0
  51. package/dist/protocol/request.d.ts +36 -0
  52. package/dist/protocol/request.js +90 -0
  53. package/dist/protocol/stream.d.ts +38 -0
  54. package/dist/protocol/stream.js +64 -0
  55. package/dist/protocol/struct.d.ts +19 -0
  56. package/dist/protocol/struct.js +186 -0
  57. package/dist/protocol/thinking.d.ts +15 -0
  58. package/dist/protocol/thinking.js +17 -0
  59. package/dist/protocol/tools.d.ts +94 -0
  60. package/dist/protocol/tools.js +631 -0
  61. package/dist/session.d.ts +81 -0
  62. package/dist/session.js +96 -0
  63. package/dist/shared.d.ts +15 -0
  64. package/dist/shared.js +13 -0
  65. package/dist/transport/connect.d.ts +23 -0
  66. package/dist/transport/connect.js +275 -0
  67. package/package.json +65 -0
@@ -0,0 +1,49 @@
1
+ export type ModelParameterValue = {
2
+ id: string;
3
+ value: string;
4
+ };
5
+ export type ModelVariant = {
6
+ key: string;
7
+ parameterValues: ModelParameterValue[];
8
+ displayName: string;
9
+ isDefaultNonMax: boolean;
10
+ isDefaultMax: boolean;
11
+ };
12
+ export type ModelInfo = {
13
+ id: string;
14
+ displayName?: string;
15
+ family?: string;
16
+ supportsThinking?: boolean;
17
+ supportsAgent?: boolean;
18
+ maxContext?: number;
19
+ supportsMaxMode?: boolean;
20
+ variants: ModelVariant[];
21
+ };
22
+ export type ModelCache = {
23
+ models: ModelInfo[];
24
+ fetchedAt: number;
25
+ };
26
+ export declare function isCacheFresh(cache: ModelCache, ttlMs?: number): boolean;
27
+ export declare function cacheFilePath(configDir: string): string;
28
+ export declare function readCache(configDir: string): Promise<ModelCache | null>;
29
+ export declare function writeCache(configDir: string, cache: ModelCache): Promise<void>;
30
+ export declare function mapAvailableModelsResponse(raw: Record<string, unknown>): ModelInfo[];
31
+ /**
32
+ * Resolve the parameter values to send in `requested_model.parameters` for a
33
+ * given model + requested reasoning effort. Each variant already carries the
34
+ * full `{id,value}` set (per-model vocabulary), so we pick the matching variant
35
+ * and return its parameters verbatim — the client never constructs these by
36
+ * hand.
37
+ */
38
+ export declare function resolveVariantParameters(model: ModelInfo | undefined, opts?: {
39
+ reasoningEffort?: string;
40
+ maxMode?: boolean;
41
+ }): ModelParameterValue[];
42
+ export declare function fetchModels(token: string, options?: {
43
+ baseURL?: string;
44
+ headers?: Record<string, string>;
45
+ }): Promise<ModelInfo[]>;
46
+ export declare function discoverModels(token: string, configDir: string, options?: {
47
+ baseURL?: string;
48
+ headers?: Record<string, string>;
49
+ }): Promise<ModelInfo[]>;
package/dist/models.js ADDED
@@ -0,0 +1,136 @@
1
+ import { MODEL_CACHE_FILE, MODEL_CACHE_TTL_MS } from "./shared.js";
2
+ import { unaryAvailableModels } from "./transport/connect.js";
3
+ import { buildRequestedModelParams } from "./protocol/thinking.js";
4
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import path from "node:path";
7
+ // ── Cache helpers ──
8
+ export function isCacheFresh(cache, ttlMs = MODEL_CACHE_TTL_MS) {
9
+ return Date.now() - cache.fetchedAt < ttlMs;
10
+ }
11
+ export function cacheFilePath(configDir) {
12
+ return path.join(configDir, MODEL_CACHE_FILE);
13
+ }
14
+ export async function readCache(configDir) {
15
+ const filePath = cacheFilePath(configDir);
16
+ try {
17
+ if (!existsSync(filePath))
18
+ return null;
19
+ const data = await readFile(filePath, "utf-8");
20
+ return JSON.parse(data);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ export async function writeCache(configDir, cache) {
27
+ const filePath = cacheFilePath(configDir);
28
+ await mkdir(path.dirname(filePath), { recursive: true });
29
+ await writeFile(filePath, JSON.stringify(cache, null, 2), "utf-8");
30
+ }
31
+ // ── Map live API response to ModelInfo[] ──
32
+ export function mapAvailableModelsResponse(raw) {
33
+ const entries = raw.models ?? [];
34
+ const models = [];
35
+ for (const entry of entries) {
36
+ const e = entry;
37
+ const name = e.name;
38
+ if (!name)
39
+ continue;
40
+ const variants = [];
41
+ const rawVariants = (e.variants ?? []);
42
+ for (const v of rawVariants) {
43
+ const rawParams = (v.parameterValues ?? v.parameter_values ?? []);
44
+ const parameterValues = rawParams.map((p) => ({
45
+ id: p.id,
46
+ value: String(p.value ?? ""),
47
+ }));
48
+ variants.push({
49
+ key: name,
50
+ parameterValues,
51
+ displayName: (v.displayName ?? v.display_name ?? name),
52
+ isDefaultNonMax: !!(v.isDefaultNonMaxConfig ?? v.is_default_non_max_config ?? false),
53
+ isDefaultMax: !!(v.isDefaultMaxConfig ?? v.is_default_max_config ?? false),
54
+ });
55
+ }
56
+ models.push({
57
+ id: name,
58
+ displayName: (e.clientDisplayName ?? e.client_display_name ?? name),
59
+ supportsThinking: !!(e.supportsThinking ?? e.supports_thinking ?? false),
60
+ supportsAgent: !!(e.supportsAgent ?? e.supports_agent ?? false),
61
+ maxContext: (e.contextTokenLimit ?? e.context_token_limit ?? undefined),
62
+ supportsMaxMode: !!(e.supportsMaxMode ?? e.supports_max_mode ?? false),
63
+ variants,
64
+ });
65
+ }
66
+ return models;
67
+ }
68
+ // ── Variant resolution ──
69
+ /**
70
+ * Resolve the parameter values to send in `requested_model.parameters` for a
71
+ * given model + requested reasoning effort. Each variant already carries the
72
+ * full `{id,value}` set (per-model vocabulary), so we pick the matching variant
73
+ * and return its parameters verbatim — the client never constructs these by
74
+ * hand.
75
+ */
76
+ export function resolveVariantParameters(model, opts = {}) {
77
+ if (!model || model.variants.length === 0) {
78
+ // No variant metadata: fall back to a bare effort param if requested.
79
+ return opts.reasoningEffort ? [{ id: "effort", value: opts.reasoningEffort }] : [];
80
+ }
81
+ const effortOf = (v) => v.parameterValues.find((p) => p.id === "effort" || p.id === "reasoning")?.value;
82
+ const isFast = (v) => v.parameterValues.find((p) => p.id === "fast")?.value === "true";
83
+ const wantMax = opts.maxMode ?? false;
84
+ const candidates = model.variants.filter((v) => !isFast(v));
85
+ const pool = candidates.length > 0 ? candidates : model.variants;
86
+ if (opts.reasoningEffort) {
87
+ const match = pool.find((v) => effortOf(v) === opts.reasoningEffort);
88
+ if (match) {
89
+ return buildRequestedModelParams(match.parameterValues, {
90
+ reasoningEffort: opts.reasoningEffort,
91
+ maxMode: wantMax,
92
+ });
93
+ }
94
+ }
95
+ const byDefault = pool.find((v) => (wantMax ? v.isDefaultMax : v.isDefaultNonMax));
96
+ const base = (byDefault ?? pool[0]).parameterValues;
97
+ return buildRequestedModelParams(base, {
98
+ reasoningEffort: opts.reasoningEffort,
99
+ maxMode: wantMax,
100
+ });
101
+ }
102
+ // ── Fetch + cache orchestration ──
103
+ export async function fetchModels(token, options = {}) {
104
+ const raw = await unaryAvailableModels(token, options);
105
+ return mapAvailableModelsResponse(raw);
106
+ }
107
+ export async function discoverModels(token, configDir, options = {}) {
108
+ const cached = await readCache(configDir);
109
+ // Cache is fresh → return it; refresh in background
110
+ if (cached && isCacheFresh(cached)) {
111
+ // Background refresh (fire and forget)
112
+ fetchModels(token, options)
113
+ .then((models) => writeCache(configDir, { models, fetchedAt: Date.now() }))
114
+ .catch(() => {
115
+ /* background refresh failure is non-fatal */
116
+ });
117
+ return cached.models;
118
+ }
119
+ // Cache exists but expired → try fetch, serve stale on failure
120
+ if (cached) {
121
+ try {
122
+ const models = await fetchModels(token, options);
123
+ const newCache = { models, fetchedAt: Date.now() };
124
+ await writeCache(configDir, newCache);
125
+ return models;
126
+ }
127
+ catch {
128
+ return cached.models;
129
+ }
130
+ }
131
+ // No cache → must fetch
132
+ const models = await fetchModels(token, options);
133
+ const newCache = { models, fetchedAt: Date.now() };
134
+ await writeCache(configDir, newCache);
135
+ return models;
136
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("@opencode-ai/plugin/v2/promise").Plugin;
2
+ export default _default;
@@ -0,0 +1,48 @@
1
+ import { define } from "@opencode-ai/plugin/v2/promise";
2
+ import { CURSOR_PROVIDER_ID } from "./shared.js";
3
+ import { createCursorLanguageModel } from "./language-model.js";
4
+ /**
5
+ * OpenCode Effect / Promise v2 plugin.
6
+ *
7
+ * Wires the Cursor provider through `ctx.aisdk.sdk` / `ctx.aisdk.language`
8
+ * (the V2 extension points). Auth still lives in the classic Hooks plugin
9
+ * (`plugin.ts`) until OpenCode integrations fully replace provider OAuth.
10
+ */
11
+ function isCursorPackage(pkg, providerID) {
12
+ if (providerID === CURSOR_PROVIDER_ID)
13
+ return true;
14
+ return (pkg.includes("cursor-opencode-provider") ||
15
+ /cursor-opencode-provider[/\\]dist[/\\]index\.js/.test(pkg));
16
+ }
17
+ function createSdk(options) {
18
+ const providerId = options.name || CURSOR_PROVIDER_ID;
19
+ return {
20
+ languageModel(modelId) {
21
+ return createCursorLanguageModel(modelId, providerId, options);
22
+ },
23
+ };
24
+ }
25
+ export default define({
26
+ id: "cursor.provider",
27
+ setup: async (ctx) => {
28
+ await ctx.aisdk.sdk((event) => {
29
+ if (event.sdk)
30
+ return;
31
+ if (!isCursorPackage(event.package, event.model.providerID))
32
+ return;
33
+ event.sdk = createSdk({
34
+ name: event.model.providerID || CURSOR_PROVIDER_ID,
35
+ ...event.options,
36
+ });
37
+ });
38
+ await ctx.aisdk.language((event) => {
39
+ if (event.model.providerID !== CURSOR_PROVIDER_ID)
40
+ return;
41
+ if (event.language)
42
+ return;
43
+ if (typeof event.sdk?.languageModel !== "function")
44
+ return;
45
+ event.language = event.sdk.languageModel(event.model.api.id);
46
+ });
47
+ },
48
+ });
@@ -0,0 +1,2 @@
1
+ import type { Hooks, PluginInput } from "@opencode-ai/plugin";
2
+ export declare function CursorPlugin(input: PluginInput): Promise<Hooks>;
package/dist/plugin.js ADDED
@@ -0,0 +1,201 @@
1
+ import { CURSOR_PROVIDER_ID, CURSOR_WEBSITE_HOST, CURSOR_API_HOST } from "./shared.js";
2
+ import { pollForTokens, exchangeApiKey, refreshAccessToken, isExpiringSoon, generatePkceParams, generatePkceChallenge, buildLoginUrl, decodeJwtPayload } from "./auth.js";
3
+ import { readCache, discoverModels } from "./models.js";
4
+ const MODULE_URL = new URL("./index.js", import.meta.url).href;
5
+ function modelInfoVariants(mi) {
6
+ if (mi.variants.length === 0)
7
+ return undefined;
8
+ const entries = {};
9
+ for (const v of mi.variants) {
10
+ const key = v.displayName || v.key || "default";
11
+ const params = {};
12
+ for (const p of v.parameterValues) {
13
+ params[p.id] = p.value;
14
+ }
15
+ entries[key] = params;
16
+ }
17
+ return entries;
18
+ }
19
+ function modelInfoToConfig(mi) {
20
+ const config = {
21
+ name: mi.displayName ?? mi.id,
22
+ reasoning: mi.supportsThinking ?? false,
23
+ tool_call: mi.supportsAgent ?? true,
24
+ temperature: false,
25
+ limit: {
26
+ context: mi.maxContext ?? 200000,
27
+ output: 4096,
28
+ },
29
+ };
30
+ const variants = modelInfoVariants(mi);
31
+ if (variants)
32
+ config.variants = variants;
33
+ return config;
34
+ }
35
+ export async function CursorPlugin(input) {
36
+ const configDir = process.env.CURSOR_CONFIG_DIR || input.directory;
37
+ async function loadModels() {
38
+ const cached = await readCache(configDir);
39
+ if (!cached || cached.models.length === 0)
40
+ return {};
41
+ const models = {};
42
+ for (const m of cached.models) {
43
+ models[m.id] = modelInfoToConfig(m);
44
+ }
45
+ return models;
46
+ }
47
+ return {
48
+ async config(cfg) {
49
+ cfg.provider ??= {};
50
+ const models = await loadModels();
51
+ const existing = cfg.provider[CURSOR_PROVIDER_ID];
52
+ if (existing) {
53
+ // Provider already declared (e.g. README stub with models: {}) —
54
+ // still inject the cached model list when the user hasn't filled it in.
55
+ const existingModels = existing.models;
56
+ if (!existingModels || Object.keys(existingModels).length === 0) {
57
+ ;
58
+ existing.models = models;
59
+ }
60
+ return;
61
+ }
62
+ cfg.provider[CURSOR_PROVIDER_ID] = {
63
+ name: "Cursor Integration",
64
+ npm: MODULE_URL,
65
+ models,
66
+ };
67
+ },
68
+ auth: {
69
+ provider: CURSOR_PROVIDER_ID,
70
+ methods: [
71
+ {
72
+ type: "oauth",
73
+ label: "Cursor account (browser login)",
74
+ async authorize() {
75
+ const params = generatePkceParams();
76
+ const challenge = await generatePkceChallenge(params.verifier);
77
+ const websiteUrl = process.env.CURSOR_WEBSITE_URL ?? `https://${CURSOR_WEBSITE_HOST}`;
78
+ const apiBaseUrl = process.env.CURSOR_API_BASE_URL ?? `https://${CURSOR_API_HOST}`;
79
+ const url = buildLoginUrl(challenge, params.uuid, websiteUrl);
80
+ return {
81
+ url,
82
+ instructions: "Open this URL in a browser to sign in to Cursor",
83
+ method: "auto",
84
+ async callback() {
85
+ const result = await pollForTokens(params.uuid, params.verifier, apiBaseUrl);
86
+ return {
87
+ type: "success",
88
+ provider: CURSOR_PROVIDER_ID,
89
+ access: result.accessToken,
90
+ refresh: result.refreshToken,
91
+ expires: decodeExpFromJwt(result.accessToken),
92
+ };
93
+ },
94
+ };
95
+ },
96
+ },
97
+ {
98
+ type: "api",
99
+ label: "API key (cursor.com/settings)",
100
+ prompts: [
101
+ {
102
+ type: "text",
103
+ key: "apiKey",
104
+ message: "Cursor API key",
105
+ placeholder: "sk-...",
106
+ validate(value) {
107
+ if (!value.startsWith("sk-"))
108
+ return "API key should start with sk-";
109
+ return undefined;
110
+ },
111
+ },
112
+ ],
113
+ async authorize(inputs) {
114
+ const apiKey = inputs?.apiKey;
115
+ if (!apiKey)
116
+ return { type: "failed" };
117
+ try {
118
+ const result = await exchangeApiKey(apiKey);
119
+ return {
120
+ type: "success",
121
+ key: result.accessToken,
122
+ provider: CURSOR_PROVIDER_ID,
123
+ metadata: { refreshToken: result.refreshToken },
124
+ };
125
+ }
126
+ catch {
127
+ return { type: "failed" };
128
+ }
129
+ },
130
+ },
131
+ ],
132
+ async loader(getAuth) {
133
+ const auth = await getAuth();
134
+ if (!auth)
135
+ return {};
136
+ let accessToken;
137
+ if (auth.type === "api") {
138
+ accessToken = auth.key;
139
+ const refreshToken = auth.metadata?.refreshToken;
140
+ // API-key exchange returns a short-lived JWT stored as `key`. Refresh
141
+ // it the same way as OAuth when it is expiring / already expired.
142
+ if (refreshToken && isExpiringSoon(auth.key)) {
143
+ try {
144
+ const newTokens = await refreshAccessToken(refreshToken);
145
+ await input.client.auth.set({
146
+ path: { id: CURSOR_PROVIDER_ID },
147
+ body: {
148
+ type: "api",
149
+ key: newTokens.accessToken,
150
+ metadata: { refreshToken: newTokens.refreshToken },
151
+ },
152
+ });
153
+ accessToken = newTokens.accessToken;
154
+ }
155
+ catch {
156
+ // refresh failed — keep the existing key; the next call may still work
157
+ }
158
+ }
159
+ }
160
+ else if (auth.type === "oauth") {
161
+ if (!isExpiringSoon(auth.access)) {
162
+ accessToken = auth.access;
163
+ }
164
+ else if (auth.refresh) {
165
+ try {
166
+ const newTokens = await refreshAccessToken(auth.refresh);
167
+ await input.client.auth.set({
168
+ path: { id: CURSOR_PROVIDER_ID },
169
+ body: {
170
+ type: "oauth",
171
+ access: newTokens.accessToken,
172
+ refresh: newTokens.refreshToken,
173
+ expires: decodeExpFromJwt(newTokens.accessToken),
174
+ },
175
+ });
176
+ accessToken = newTokens.accessToken;
177
+ }
178
+ catch {
179
+ // refresh failed
180
+ }
181
+ }
182
+ }
183
+ if (accessToken) {
184
+ // Use discoverModels so we respect TTL / serve-stale semantics and
185
+ // write through the same cache path language-model reads.
186
+ discoverModels(accessToken, configDir).catch(() => { });
187
+ }
188
+ return {
189
+ ...(accessToken ? { accessToken } : {}),
190
+ workspaceRoot: input.directory,
191
+ };
192
+ },
193
+ },
194
+ };
195
+ }
196
+ function decodeExpFromJwt(jwt) {
197
+ const payload = decodeJwtPayload(jwt);
198
+ if (payload && typeof payload.exp === "number")
199
+ return payload.exp * 1000;
200
+ return Date.now() + 3600_000;
201
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cursor CLI keeps conversation blobs in a durable client store (SQLite) across
3
+ * Runs. ConversationStateStructure only holds blob IDs; the server re-fetches
4
+ * them via get_blob on the next turn. Our per-Run session.blobs Map was wiped
5
+ * on stream close, so follow-up gets echoed 32-byte hashes → server JSON.parse
6
+ * fails ("Unexpected token ... is not valid JSON").
7
+ *
8
+ * This store is keyed by conversation_id and survives across Run streams.
9
+ */
10
+ export declare function setConversationBlob(conversationId: string, blobId: Uint8Array, blobData: Uint8Array): string;
11
+ export declare function getConversationBlob(conversationId: string, blobId: Uint8Array): Uint8Array | undefined;
12
+ export declare function conversationBlobCount(conversationId: string): number;
13
+ /** SHA-256 content hashes are 32 non-text bytes — never echo those as content. */
14
+ export declare function isBlobIdHash(blobId: Uint8Array): boolean;
15
+ export declare function resetConversationBlobsForTests(): void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Cursor CLI keeps conversation blobs in a durable client store (SQLite) across
3
+ * Runs. ConversationStateStructure only holds blob IDs; the server re-fetches
4
+ * them via get_blob on the next turn. Our per-Run session.blobs Map was wiped
5
+ * on stream close, so follow-up gets echoed 32-byte hashes → server JSON.parse
6
+ * fails ("Unexpected token ... is not valid JSON").
7
+ *
8
+ * This store is keyed by conversation_id and survives across Run streams.
9
+ */
10
+ function hex(b) {
11
+ let s = "";
12
+ for (let i = 0; i < b.length; i++)
13
+ s += b[i].toString(16).padStart(2, "0");
14
+ return s;
15
+ }
16
+ const byConversation = new Map();
17
+ function bucket(conversationId) {
18
+ let m = byConversation.get(conversationId);
19
+ if (!m) {
20
+ m = new Map();
21
+ byConversation.set(conversationId, m);
22
+ }
23
+ return m;
24
+ }
25
+ export function setConversationBlob(conversationId, blobId, blobData) {
26
+ const key = hex(blobId);
27
+ // Copy so decode buffers can't mutate the store later.
28
+ bucket(conversationId).set(key, Uint8Array.from(blobData));
29
+ return key;
30
+ }
31
+ export function getConversationBlob(conversationId, blobId) {
32
+ return bucket(conversationId).get(hex(blobId));
33
+ }
34
+ export function conversationBlobCount(conversationId) {
35
+ return byConversation.get(conversationId)?.size ?? 0;
36
+ }
37
+ /** SHA-256 content hashes are 32 non-text bytes — never echo those as content. */
38
+ export function isBlobIdHash(blobId) {
39
+ if (blobId.length !== 32)
40
+ return false;
41
+ // Content-as-id payloads can also be 32 bytes (short JSON). Real hashes are
42
+ // binary; if every byte is printable ASCII/UTF-8 text, treat as content.
43
+ for (let i = 0; i < blobId.length; i++) {
44
+ const b = blobId[i];
45
+ if (b < 0x09 || (b > 0x0d && b < 0x20) || b > 0x7e)
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ export function resetConversationBlobsForTests() {
51
+ byConversation.clear();
52
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Cursor CLI parity: the server emits `conversation_checkpoint_update` after
3
+ * (and during) a turn. The CLI replaces its local ConversationStateStructure
4
+ * and re-sends that structure as `AgentRunRequest.conversation_state` on the
5
+ * next Run. We mirror that with an in-process store keyed by conversation_id.
6
+ *
7
+ * Checkpoints are kept as opaque protobuf bytes — CLI's structure uses blob-id
8
+ * fields (repeated bytes), not the seed JSON strings we invent on turn 1.
9
+ */
10
+ /** Replace the stored checkpoint for a conversation (CLI handleCheckpoint). */
11
+ export declare function setCheckpoint(conversationId: string, bytes: Uint8Array): void;
12
+ /** Last checkpoint for this conversation, if any. */
13
+ export declare function getCheckpoint(conversationId: string): Uint8Array | undefined;
14
+ /** Drop a conversation's checkpoint (tests / explicit reset). */
15
+ export declare function clearCheckpoint(conversationId: string): void;
16
+ /** Test helper — wipe all stored checkpoints. */
17
+ export declare function resetCheckpointsForTests(): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cursor CLI parity: the server emits `conversation_checkpoint_update` after
3
+ * (and during) a turn. The CLI replaces its local ConversationStateStructure
4
+ * and re-sends that structure as `AgentRunRequest.conversation_state` on the
5
+ * next Run. We mirror that with an in-process store keyed by conversation_id.
6
+ *
7
+ * Checkpoints are kept as opaque protobuf bytes — CLI's structure uses blob-id
8
+ * fields (repeated bytes), not the seed JSON strings we invent on turn 1.
9
+ */
10
+ const byConversationId = new Map();
11
+ /** Replace the stored checkpoint for a conversation (CLI handleCheckpoint). */
12
+ export function setCheckpoint(conversationId, bytes) {
13
+ if (!conversationId || bytes.length === 0)
14
+ return;
15
+ // Copy so later mutations of the decode buffer can't corrupt the store.
16
+ byConversationId.set(conversationId, Uint8Array.from(bytes));
17
+ }
18
+ /** Last checkpoint for this conversation, if any. */
19
+ export function getCheckpoint(conversationId) {
20
+ return byConversationId.get(conversationId);
21
+ }
22
+ /** Drop a conversation's checkpoint (tests / explicit reset). */
23
+ export function clearCheckpoint(conversationId) {
24
+ byConversationId.delete(conversationId);
25
+ }
26
+ /** Test helper — wipe all stored checkpoints. */
27
+ export function resetCheckpointsForTests() {
28
+ byConversationId.clear();
29
+ }
@@ -0,0 +1,2 @@
1
+ export declare function obfuscate(bytes: Uint8Array): Uint8Array;
2
+ export declare function createCursorChecksumHeader(machineId: string, macMachineId?: string): string;
@@ -0,0 +1,23 @@
1
+ export function obfuscate(bytes) {
2
+ const out = new Uint8Array(bytes);
3
+ let a = 165;
4
+ for (let i = 0; i < out.length; i++) {
5
+ out[i] = ((out[i] ^ a) + i) & 0xff;
6
+ a = out[i];
7
+ }
8
+ return out;
9
+ }
10
+ export function createCursorChecksumHeader(machineId, macMachineId) {
11
+ const n = Math.floor(Date.now() / 1e6);
12
+ const ts = new Uint8Array([
13
+ (n >> 40) & 0xff,
14
+ (n >> 32) & 0xff,
15
+ (n >> 24) & 0xff,
16
+ (n >> 16) & 0xff,
17
+ (n >> 8) & 0xff,
18
+ n & 0xff,
19
+ ]);
20
+ const obfuscated = obfuscate(ts);
21
+ const prefix = btoa(String.fromCharCode(...obfuscated)).replace(/=+$/, "");
22
+ return macMachineId ? `${prefix}${machineId}/${macMachineId}` : `${prefix}${machineId}`;
23
+ }
@@ -0,0 +1,5 @@
1
+ export declare function resetClientVersionCache(): void;
2
+ export declare function resolveClientVersion(): Promise<string>;
3
+ export declare function cursorAgentVersionsDir(): string | undefined;
4
+ export declare function discoverLocalVersion(dir?: string | undefined): string | undefined;
5
+ export declare function extractVersionFromInstaller(script: string): string | undefined;