@bastani/atomic 0.8.28-alpha.4 → 0.8.29-alpha.2
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/CHANGELOG.md +75 -0
- package/dist/builtin/cursor/CHANGELOG.md +27 -0
- package/dist/builtin/cursor/LICENSE +26 -0
- package/dist/builtin/cursor/README.md +22 -0
- package/dist/builtin/cursor/index.ts +9 -0
- package/dist/builtin/cursor/package.json +46 -0
- package/dist/builtin/cursor/src/auth.ts +352 -0
- package/dist/builtin/cursor/src/catalog-cache.ts +155 -0
- package/dist/builtin/cursor/src/config.ts +123 -0
- package/dist/builtin/cursor/src/conversation-state.ts +135 -0
- package/dist/builtin/cursor/src/cursor-models-raw.json +583 -0
- package/dist/builtin/cursor/src/model-mapper.ts +270 -0
- package/dist/builtin/cursor/src/models.ts +54 -0
- package/dist/builtin/cursor/src/native-loader.ts +71 -0
- package/dist/builtin/cursor/src/proto/README.md +34 -0
- package/dist/builtin/cursor/src/proto/agent_pb.ts +15294 -0
- package/dist/builtin/cursor/src/proto/protobuf-codec.ts +717 -0
- package/dist/builtin/cursor/src/provider.ts +301 -0
- package/dist/builtin/cursor/src/stream.ts +564 -0
- package/dist/builtin/cursor/src/transport.ts +791 -0
- package/dist/builtin/intercom/CHANGELOG.md +10 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/intercom/skills/intercom/SKILL.md +5 -5
- package/dist/builtin/mcp/CHANGELOG.md +10 -0
- package/dist/builtin/mcp/package.json +3 -3
- package/dist/builtin/subagents/CHANGELOG.md +18 -0
- package/dist/builtin/subagents/README.md +7 -3
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +9 -24
- package/dist/builtin/subagents/agents/debugger.md +3 -5
- package/dist/builtin/subagents/package.json +4 -4
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +2 -1
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +2 -1
- package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +19 -2
- package/dist/builtin/subagents/src/runs/shared/structured-output.ts +271 -10
- package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +12 -39
- package/dist/builtin/subagents/src/shared/types.ts +1 -0
- package/dist/builtin/subagents/src/shared/utils.ts +50 -10
- package/dist/builtin/subagents/src/slash/saved-chain-mapping.ts +77 -0
- package/dist/builtin/subagents/src/slash/slash-commands.ts +1 -55
- package/dist/builtin/web-access/CHANGELOG.md +11 -1
- package/dist/builtin/web-access/README.md +1 -1
- package/dist/builtin/web-access/github-extract.ts +1 -1
- package/dist/builtin/web-access/package.json +3 -3
- package/dist/builtin/workflows/CHANGELOG.md +44 -0
- package/dist/builtin/workflows/README.md +19 -1
- package/dist/builtin/workflows/package.json +2 -2
- package/dist/builtin/workflows/skills/research-codebase/SKILL.md +17 -3
- package/dist/builtin/workflows/src/extension/wiring.ts +17 -1
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +34 -0
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +13 -2
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +86 -14
- package/dist/builtin/workflows/src/shared/authoring-contract.d.ts +11 -3
- package/dist/builtin/workflows/src/shared/types.ts +8 -4
- package/dist/builtin/workflows/src/tui/overlay-adapter.ts +64 -2
- package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
- package/dist/builtin/workflows/src/tui/workflow-status.ts +2 -0
- package/dist/core/builtin-packages.d.ts.map +1 -1
- package/dist/core/builtin-packages.js +6 -0
- package/dist/core/builtin-packages.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +20 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/model-resolver.d.ts +1 -0
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +17 -8
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +11 -9
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +55 -10
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/project-trust.d.ts +1 -0
- package/dist/core/project-trust.d.ts.map +1 -1
- package/dist/core/project-trust.js +3 -3
- package/dist/core/project-trust.js.map +1 -1
- package/dist/core/resource-loader.d.ts +9 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +72 -9
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +3 -3
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +5 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/structured-output.d.ts +39 -0
- package/dist/core/tools/structured-output.d.ts.map +1 -0
- package/dist/core/tools/structured-output.js +141 -0
- package/dist/core/tools/structured-output.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +36 -14
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +3 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +16 -0
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +11 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +158 -11
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +39 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/custom-provider.md +1 -0
- package/docs/extensions.md +2 -2
- package/docs/models.md +2 -0
- package/docs/packages.md +3 -1
- package/docs/providers.md +15 -0
- package/docs/sdk.md +61 -0
- package/docs/security.md +1 -1
- package/docs/subagents.md +21 -0
- package/docs/usage.md +2 -0
- package/docs/workflows.md +10 -7
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/structured-output.ts +22 -53
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +12 -9
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { ThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
|
|
2
|
+
import { CURSOR_API, CURSOR_API_BASE_URL } from "./config.js";
|
|
3
|
+
import rawFallbackModels from "./cursor-models-raw.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
export type CursorCatalogSource = "live" | "estimated";
|
|
6
|
+
export type CursorEffort = "none" | "low" | "medium" | "high" | "xhigh" | "max" | "default";
|
|
7
|
+
|
|
8
|
+
export interface CursorUsableModel {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly name?: string;
|
|
11
|
+
readonly displayName?: string;
|
|
12
|
+
readonly contextWindow?: number;
|
|
13
|
+
readonly maxTokens?: number;
|
|
14
|
+
readonly supportsReasoning?: boolean;
|
|
15
|
+
readonly supportsThinking?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CursorModelCatalog {
|
|
19
|
+
readonly source: CursorCatalogSource;
|
|
20
|
+
readonly fetchedAt: number;
|
|
21
|
+
readonly note?: string;
|
|
22
|
+
readonly models: readonly CursorUsableModel[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CursorProviderModelDefinition {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly api?: string;
|
|
29
|
+
readonly baseUrl?: string;
|
|
30
|
+
readonly reasoning: boolean;
|
|
31
|
+
readonly thinkingLevelMap?: ThinkingLevelMap;
|
|
32
|
+
readonly input: ["text"];
|
|
33
|
+
readonly cost: { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number };
|
|
34
|
+
readonly contextWindow: number;
|
|
35
|
+
readonly maxTokens: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CursorVariant {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly baseId: string;
|
|
41
|
+
readonly displayName: string;
|
|
42
|
+
readonly effort?: CursorEffort;
|
|
43
|
+
readonly fast: boolean;
|
|
44
|
+
readonly thinking: boolean;
|
|
45
|
+
readonly contextWindow?: number;
|
|
46
|
+
readonly maxTokens?: number;
|
|
47
|
+
readonly supportsReasoning?: boolean;
|
|
48
|
+
readonly supportsThinking?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface CursorVariantGroup {
|
|
52
|
+
readonly baseId: string;
|
|
53
|
+
readonly primaryId: string;
|
|
54
|
+
readonly displayName: string;
|
|
55
|
+
readonly variants: readonly CursorVariant[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const CURSOR_FALLBACK_RAW_MODELS = rawFallbackModels satisfies readonly CursorUsableModel[];
|
|
59
|
+
const PARSEABLE_EFFORTS: readonly Exclude<CursorEffort, "default">[] = ["none", "low", "medium", "high", "xhigh", "max"];
|
|
60
|
+
const EFFORT_ORDER: readonly CursorEffort[] = ["none", "low", "default", "medium", "high", "xhigh", "max"];
|
|
61
|
+
const THINKING_LEVEL_EFFORT_PREFERENCES: Record<ThinkingLevel, readonly CursorEffort[]> = {
|
|
62
|
+
minimal: ["none", "low", "default"],
|
|
63
|
+
low: ["low", "none", "default"],
|
|
64
|
+
medium: ["medium", "default", "low"],
|
|
65
|
+
high: ["high", "medium", "default"],
|
|
66
|
+
xhigh: ["max", "xhigh", "high"],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const ESTIMATED_CONTEXT_WINDOW = 200_000;
|
|
70
|
+
const ESTIMATED_MAX_TOKENS = 64_000;
|
|
71
|
+
|
|
72
|
+
export function createEstimatedCursorCatalog(now = Date.now()): CursorModelCatalog {
|
|
73
|
+
return {
|
|
74
|
+
source: "estimated",
|
|
75
|
+
fetchedAt: now,
|
|
76
|
+
note: "static fallback; Cursor private API metadata and limits are estimated; token costs are reported as zero for subscription usage",
|
|
77
|
+
models: CURSOR_FALLBACK_RAW_MODELS.map((model) => ({
|
|
78
|
+
...model,
|
|
79
|
+
supportsReasoning: supportsReasoningModelId(model.id),
|
|
80
|
+
...(model.id.endsWith("-thinking") || model.id.includes("-thinking-") ? { supportsThinking: true } : {}),
|
|
81
|
+
})),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function mapCursorCatalogToProviderModels(catalog: CursorModelCatalog): CursorProviderModelDefinition[] {
|
|
86
|
+
return groupCursorModels(catalog.models).map((group) => {
|
|
87
|
+
const effortVariants = collectEffortVariants(group.variants, group.primaryId);
|
|
88
|
+
const supportsEffort = group.variants.some((variant) => Boolean(variant.effort)) || effortVariants.size >= 2;
|
|
89
|
+
const supportsReasoning = supportsReasoningModelId(group.primaryId);
|
|
90
|
+
const name = catalog.source === "estimated" ? `${group.displayName} (estimated)` : group.displayName;
|
|
91
|
+
return {
|
|
92
|
+
id: group.primaryId,
|
|
93
|
+
name,
|
|
94
|
+
api: CURSOR_API,
|
|
95
|
+
baseUrl: CURSOR_API_BASE_URL,
|
|
96
|
+
reasoning: supportsReasoning,
|
|
97
|
+
thinkingLevelMap: supportsEffort ? buildThinkingLevelMap(effortVariants, group.primaryId) : undefined,
|
|
98
|
+
input: ["text"],
|
|
99
|
+
cost: subscriptionCost(),
|
|
100
|
+
contextWindow: chooseLargestNumber(group.variants.map((variant) => variant.contextWindow)) ?? ESTIMATED_CONTEXT_WINDOW,
|
|
101
|
+
maxTokens: chooseLargestNumber(group.variants.map((variant) => variant.maxTokens)) ?? ESTIMATED_MAX_TOKENS,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveCursorModelVariant(
|
|
107
|
+
baseModelId: string,
|
|
108
|
+
thinkingLevelMap: ThinkingLevelMap | undefined,
|
|
109
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
110
|
+
): string {
|
|
111
|
+
if (!thinkingLevel || !thinkingLevelMap) return baseModelId;
|
|
112
|
+
const mapped = thinkingLevelMap[thinkingLevel];
|
|
113
|
+
if (!mapped || mapped === "default") return baseModelId;
|
|
114
|
+
if (isCursorEffort(mapped)) return replaceEffortBeforeCursorSuffix(baseModelId, mapped);
|
|
115
|
+
return mapped;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function insertEffortBeforeCursorSuffix(modelId: string, effort: CursorEffort): string {
|
|
119
|
+
if (effort === "default") return modelId;
|
|
120
|
+
let base = modelId;
|
|
121
|
+
let fast = false;
|
|
122
|
+
let thinking = false;
|
|
123
|
+
if (base.endsWith("-fast")) {
|
|
124
|
+
fast = true;
|
|
125
|
+
base = base.slice(0, -"-fast".length);
|
|
126
|
+
}
|
|
127
|
+
if (base.endsWith("-thinking")) {
|
|
128
|
+
thinking = true;
|
|
129
|
+
base = base.slice(0, -"-thinking".length);
|
|
130
|
+
}
|
|
131
|
+
return `${base}-${effort}${thinking ? "-thinking" : ""}${fast ? "-fast" : ""}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function replaceEffortBeforeCursorSuffix(modelId: string, effort: CursorEffort): string {
|
|
135
|
+
if (effort === "default") return modelId;
|
|
136
|
+
const variant = parseCursorVariant({ id: modelId });
|
|
137
|
+
return `${variant.baseId}-${effort}${variant.thinking ? "-thinking" : ""}${variant.fast ? "-fast" : ""}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function parseCursorVariant(model: CursorUsableModel): CursorVariant {
|
|
141
|
+
let base = model.id;
|
|
142
|
+
let fast = false;
|
|
143
|
+
let thinking = false;
|
|
144
|
+
if (base.endsWith("-fast")) {
|
|
145
|
+
fast = true;
|
|
146
|
+
base = base.slice(0, -"-fast".length);
|
|
147
|
+
}
|
|
148
|
+
if (base.endsWith("-thinking")) {
|
|
149
|
+
thinking = true;
|
|
150
|
+
base = base.slice(0, -"-thinking".length);
|
|
151
|
+
}
|
|
152
|
+
const effort = PARSEABLE_EFFORTS.find((candidate) => base.endsWith(`-${candidate}`));
|
|
153
|
+
if (effort) {
|
|
154
|
+
base = base.slice(0, -effort.length - 1);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
id: model.id,
|
|
158
|
+
baseId: base,
|
|
159
|
+
displayName: model.displayName ?? model.name ?? titleCaseModelId(base),
|
|
160
|
+
effort,
|
|
161
|
+
fast,
|
|
162
|
+
thinking,
|
|
163
|
+
contextWindow: model.contextWindow,
|
|
164
|
+
maxTokens: model.maxTokens,
|
|
165
|
+
supportsReasoning: model.supportsReasoning,
|
|
166
|
+
supportsThinking: model.supportsThinking,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function groupCursorModels(models: readonly CursorUsableModel[]): CursorVariantGroup[] {
|
|
171
|
+
const groups = new Map<string, CursorVariant[]>();
|
|
172
|
+
for (const model of models) {
|
|
173
|
+
const variant = parseCursorVariant(model);
|
|
174
|
+
const key = cursorVariantGroupKey(variant);
|
|
175
|
+
const existing = groups.get(key) ?? [];
|
|
176
|
+
existing.push(variant);
|
|
177
|
+
groups.set(key, existing);
|
|
178
|
+
}
|
|
179
|
+
return [...groups.values()]
|
|
180
|
+
.map((variants) => {
|
|
181
|
+
const baseId = variants[0]?.baseId ?? "cursor";
|
|
182
|
+
const primaryId = choosePrimaryId(variants, baseId);
|
|
183
|
+
return {
|
|
184
|
+
baseId,
|
|
185
|
+
primaryId,
|
|
186
|
+
displayName: chooseDisplayName(variants, baseId, primaryId),
|
|
187
|
+
variants,
|
|
188
|
+
};
|
|
189
|
+
})
|
|
190
|
+
.sort((left, right) => left.primaryId.localeCompare(right.primaryId));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cursorVariantGroupKey(variant: CursorVariant): string {
|
|
194
|
+
return `${variant.baseId}|fast=${variant.fast ? "1" : "0"}|thinking=${variant.thinking ? "1" : "0"}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectEffortVariants(variants: readonly CursorVariant[], primaryId: string): ReadonlyMap<CursorEffort, string> {
|
|
198
|
+
const byEffort = new Map<CursorEffort, string>();
|
|
199
|
+
for (const variant of variants) {
|
|
200
|
+
const effort = variant.effort ?? (variant.id === primaryId || variant.supportsReasoning || variant.supportsThinking || variant.thinking ? "default" : undefined);
|
|
201
|
+
if (effort && !byEffort.has(effort)) byEffort.set(effort, variant.id);
|
|
202
|
+
}
|
|
203
|
+
return byEffort;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildThinkingLevelMap(effortVariants: ReadonlyMap<CursorEffort, string>, primaryId: string): ThinkingLevelMap {
|
|
207
|
+
return {
|
|
208
|
+
minimal: chooseEffortVariant(effortVariants, THINKING_LEVEL_EFFORT_PREFERENCES.minimal, primaryId),
|
|
209
|
+
low: chooseEffortVariant(effortVariants, THINKING_LEVEL_EFFORT_PREFERENCES.low, primaryId),
|
|
210
|
+
medium: chooseEffortVariant(effortVariants, THINKING_LEVEL_EFFORT_PREFERENCES.medium, primaryId),
|
|
211
|
+
high: chooseEffortVariant(effortVariants, THINKING_LEVEL_EFFORT_PREFERENCES.high, primaryId),
|
|
212
|
+
xhigh: chooseEffortVariant(effortVariants, THINKING_LEVEL_EFFORT_PREFERENCES.xhigh, primaryId),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function chooseEffortVariant(effortVariants: ReadonlyMap<CursorEffort, string>, preferences: readonly CursorEffort[], _primaryId: string): string | null {
|
|
217
|
+
for (const effort of preferences) {
|
|
218
|
+
const variantId = effortVariants.get(effort);
|
|
219
|
+
if (variantId) return variantId;
|
|
220
|
+
}
|
|
221
|
+
for (const effort of EFFORT_ORDER) {
|
|
222
|
+
const variantId = effortVariants.get(effort);
|
|
223
|
+
if (variantId) return variantId;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isCursorEffort(value: string): value is CursorEffort {
|
|
229
|
+
return value === "default" || (PARSEABLE_EFFORTS as readonly string[]).includes(value);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function chooseLargestNumber(values: readonly (number | undefined)[]): number | undefined {
|
|
233
|
+
const finiteValues = values.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
234
|
+
return finiteValues.length > 0 ? Math.max(...finiteValues) : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function choosePrimaryId(variants: readonly CursorVariant[], baseId: string): string {
|
|
238
|
+
if (variants.some((variant) => variant.effort)) {
|
|
239
|
+
const representative = variants[0];
|
|
240
|
+
return `${baseId}${representative?.thinking ? "-thinking" : ""}${representative?.fast ? "-fast" : ""}`;
|
|
241
|
+
}
|
|
242
|
+
return variants.find((variant) => variant.id === baseId)?.id ?? variants[0]?.id ?? baseId;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function chooseDisplayName(variants: readonly CursorVariant[], baseId: string, primaryId: string): string {
|
|
246
|
+
return variants.find((variant) => variant.id === primaryId)?.displayName
|
|
247
|
+
?? variants.find((variant) => variant.effort === "medium")?.displayName
|
|
248
|
+
?? variants.find((variant) => !variant.effort)?.displayName
|
|
249
|
+
?? variants[0]?.displayName
|
|
250
|
+
?? titleCaseModelId(baseId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function supportsReasoningModelId(id: string): boolean {
|
|
254
|
+
const variant = parseCursorVariant({ id });
|
|
255
|
+
if (variant.effort || variant.thinking) return true;
|
|
256
|
+
if (variant.baseId === "default") return true;
|
|
257
|
+
return /^(claude|composer|gemini|gpt|grok|kimi)(-|$)/iu.test(variant.baseId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function titleCaseModelId(id: string): string {
|
|
261
|
+
return id
|
|
262
|
+
.split(/[-_/]+/u)
|
|
263
|
+
.filter((part) => part.length > 0)
|
|
264
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
265
|
+
.join(" ");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function subscriptionCost(): { readonly input: number; readonly output: number; readonly cacheRead: number; readonly cacheWrite: number } {
|
|
269
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
270
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createEstimatedCursorCatalog, type CursorModelCatalog } from "./model-mapper.js";
|
|
2
|
+
import { CursorTransportError, type CursorAgentTransport, type CursorTransportErrorCode } from "./transport.js";
|
|
3
|
+
|
|
4
|
+
export type CursorDiscoveryErrorCode = CursorTransportErrorCode | "NoUsableModels";
|
|
5
|
+
|
|
6
|
+
export class CursorModelDiscoveryError extends Error {
|
|
7
|
+
constructor(
|
|
8
|
+
readonly code: CursorDiscoveryErrorCode,
|
|
9
|
+
message: string,
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "CursorModelDiscoveryError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CursorModelDiscoveryServiceOptions {
|
|
17
|
+
readonly transport: CursorAgentTransport;
|
|
18
|
+
readonly now?: () => number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class CursorModelDiscoveryService {
|
|
22
|
+
readonly #transport: CursorAgentTransport;
|
|
23
|
+
readonly #now: () => number;
|
|
24
|
+
|
|
25
|
+
constructor(options: CursorModelDiscoveryServiceOptions) {
|
|
26
|
+
this.#transport = options.transport;
|
|
27
|
+
this.#now = options.now ?? Date.now;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async discover(accessToken: string, requestId: string, signal?: AbortSignal): Promise<CursorModelCatalog> {
|
|
31
|
+
try {
|
|
32
|
+
const models = await this.#transport.getUsableModels(accessToken, requestId, signal);
|
|
33
|
+
if (models.length === 0) {
|
|
34
|
+
throw new CursorModelDiscoveryError("NoUsableModels", "Cursor account has no usable models.");
|
|
35
|
+
}
|
|
36
|
+
return { source: "live", fetchedAt: this.#now(), models };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof CursorModelDiscoveryError) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (error instanceof CursorTransportError) {
|
|
42
|
+
throw new CursorModelDiscoveryError(error.code, error.message);
|
|
43
|
+
}
|
|
44
|
+
if (signal?.aborted) {
|
|
45
|
+
throw new CursorModelDiscoveryError("Aborted", "Cursor model discovery was aborted.");
|
|
46
|
+
}
|
|
47
|
+
throw new CursorModelDiscoveryError("ProtocolError", error instanceof Error ? error.message : "Cursor model discovery failed.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fallbackCatalog(): CursorModelCatalog {
|
|
52
|
+
return createEstimatedCursorCatalog(this.#now());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
export const CURSOR_H2_NATIVE_PACKAGE = "@bastani/atomic-natives";
|
|
4
|
+
|
|
5
|
+
export interface CursorH2NativeUnaryResponse {
|
|
6
|
+
readonly statusCode?: number;
|
|
7
|
+
readonly status_code?: number;
|
|
8
|
+
readonly headersJson?: string;
|
|
9
|
+
readonly headers_json?: string;
|
|
10
|
+
readonly body: Uint8Array;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CursorH2NativeStream {
|
|
14
|
+
write(data: Uint8Array, timeoutMs?: number | null): Promise<void>;
|
|
15
|
+
finishInput(): Promise<void>;
|
|
16
|
+
nextFrame(): Promise<Uint8Array | null>;
|
|
17
|
+
cancel(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CursorH2NativeBinding {
|
|
21
|
+
cursorH2RequestUnary(configJson: string, body: Uint8Array): Promise<CursorH2NativeUnaryResponse>;
|
|
22
|
+
cursorH2OpenStream(configJson: string, initialBody?: Uint8Array | null): Promise<CursorH2NativeStream>;
|
|
23
|
+
cursorH2CancelOperation(operationId: string): Promise<void> | void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CursorH2NativeLoadResult =
|
|
27
|
+
| { readonly ok: true; readonly binding: CursorH2NativeBinding; readonly packageName: string }
|
|
28
|
+
| { readonly ok: false; readonly error: Error; readonly packageName: string };
|
|
29
|
+
|
|
30
|
+
let cachedLoadResult: CursorH2NativeLoadResult | undefined;
|
|
31
|
+
|
|
32
|
+
export function resetCursorH2NativeBindingCache(): void {
|
|
33
|
+
cachedLoadResult = undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadCursorH2NativeBinding(): CursorH2NativeLoadResult {
|
|
37
|
+
if (cachedLoadResult) return cachedLoadResult;
|
|
38
|
+
const requireNative = createRequire(import.meta.url);
|
|
39
|
+
try {
|
|
40
|
+
const loaded = requireNative(CURSOR_H2_NATIVE_PACKAGE) as Partial<CursorH2NativeBinding>;
|
|
41
|
+
if (typeof loaded.cursorH2RequestUnary !== "function" || typeof loaded.cursorH2OpenStream !== "function" || typeof loaded.cursorH2CancelOperation !== "function") {
|
|
42
|
+
cachedLoadResult = {
|
|
43
|
+
ok: false,
|
|
44
|
+
packageName: CURSOR_H2_NATIVE_PACKAGE,
|
|
45
|
+
error: new Error(`Cursor HTTP/2 native package ${CURSOR_H2_NATIVE_PACKAGE} is missing required N-API exports.`),
|
|
46
|
+
};
|
|
47
|
+
return cachedLoadResult;
|
|
48
|
+
}
|
|
49
|
+
cachedLoadResult = { ok: true, binding: loaded as CursorH2NativeBinding, packageName: CURSOR_H2_NATIVE_PACKAGE };
|
|
50
|
+
return cachedLoadResult;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
cachedLoadResult = {
|
|
53
|
+
ok: false,
|
|
54
|
+
packageName: CURSOR_H2_NATIVE_PACKAGE,
|
|
55
|
+
error: new Error(`Cursor HTTP/2 native package ${CURSOR_H2_NATIVE_PACKAGE} is unavailable for ${process.platform}-${process.arch}: ${error instanceof Error ? error.message : String(error)}`),
|
|
56
|
+
};
|
|
57
|
+
return cachedLoadResult;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatCursorH2NativeLoadFailure(result: Extract<CursorH2NativeLoadResult, { ok: false }>): string {
|
|
62
|
+
return [
|
|
63
|
+
result.error.message,
|
|
64
|
+
`Install or rebuild ${result.packageName} so its NAPI-RS optional dependency for ${process.platform}-${process.arch} is available.`,
|
|
65
|
+
"If you installed Atomic from npm/Bun, reinstall @bastani/atomic. If you are developing locally, run `bun --cwd packages/natives run build`."
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function cursorH2NativeLoadSummary(result: CursorH2NativeLoadResult): string {
|
|
70
|
+
return result.ok ? `loaded ${result.packageName}` : result.error.message;
|
|
71
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Cursor protocol notes
|
|
2
|
+
|
|
3
|
+
This directory contains the isolated Cursor protobuf protocol codec and vendored generated protobuf descriptors. The codec intentionally follows the MIT-licensed [`ndraiman/pi-cursor-provider`](https://github.com/ndraiman/pi-cursor-provider) implementation at commit `82fc4e73f9ae820d87b34ac36713b18989910a36`: request and control messages are built with the generated `agent_pb.ts` descriptors and `@bufbuild/protobuf`, not with hand-maintained field concatenation.
|
|
4
|
+
|
|
5
|
+
Known private endpoints:
|
|
6
|
+
|
|
7
|
+
- Browser login: `https://cursor.com/loginDeepControl?challenge=<pkce>&uuid=<uuid>&mode=login&redirectTarget=cli`
|
|
8
|
+
- Login poll: `https://api2.cursor.sh/auth/poll?uuid=<uuid>&verifier=<verifier>`
|
|
9
|
+
- Refresh: `POST https://api2.cursor.sh/auth/exchange_user_api_key`
|
|
10
|
+
- Model discovery: `POST https://api2.cursor.sh/agent.v1.AgentService/GetUsableModels`
|
|
11
|
+
- Agent stream: `POST https://api2.cursor.sh/agent.v1.AgentService/Run`
|
|
12
|
+
|
|
13
|
+
`src/transport.ts` exposes an injectable HTTP/2 client and protocol codec seam plus buffered Connect frame helpers. Production defaults use `CursorProtobufProtocolCodec` and the Rust/N-API HTTP/2 native binding in `@bastani/atomic-natives` (`crates/atomic-natives`) because Bun's `node:http2` behavior is not reliable against Cursor's private API.
|
|
14
|
+
|
|
15
|
+
Protocol behavior intentionally copied from the reference provider:
|
|
16
|
+
|
|
17
|
+
- `AgentClientMessage.run_request = 1`, `exec_client_message = 2`, `kv_client_message = 3`, `conversation_action = 4`, `client_heartbeat = 7`.
|
|
18
|
+
- Run requests use generated `AgentRunRequest`, `ConversationStateStructure`, `ConversationAction`, `UserMessage`, and `ModelDetails` messages.
|
|
19
|
+
- `UserMessage.message_id`, `UserMessage.correlation_id`, and reconstructed historical turn request ids are UUIDs generated the same way as the reference provider.
|
|
20
|
+
- Conversation ids are deterministic UUIDs derived from the hashed conversation key (`conv:<session-or-first-user-text>`), matching the reference provider rather than sending raw Atomic session ids to Cursor.
|
|
21
|
+
- Static fallback models are the reference `cursor-models-raw.json`; live model discovery is opportunistic and only replaces the registered catalog when Cursor returns usable models.
|
|
22
|
+
- Tool definitions are returned in response to `ExecServerMessage.request_context_args = 10`; `McpArgs` messages become Atomic tool calls and active tool results are sent back as generated `ExecClientMessage.mcp_result` frames.
|
|
23
|
+
- Checkpoint and blob-store state is persisted per Cursor conversation id and discarded on Cursor end-stream errors such as `not_found`.
|
|
24
|
+
- `InteractionUpdate.turn_ended` is non-terminal; the stream closes on the Connect stream ending.
|
|
25
|
+
|
|
26
|
+
Manual smoke-test procedure after Cursor releases:
|
|
27
|
+
|
|
28
|
+
1. Sign in to the current Cursor CLI/app and capture a successful `api2.cursor.sh` model discovery or agent `Run` request.
|
|
29
|
+
2. Update `CURSOR_CLIENT_VERSION` in `src/config.ts` from the captured `x-cursor-client-version` header if it changed.
|
|
30
|
+
3. In Atomic, run `/login`, select **Cursor**, complete browser auth, then confirm `/model` lists `cursor/<model-id>` entries from live discovery.
|
|
31
|
+
4. Select a Cursor model and run one chat turn plus one tool-using turn; verify the process exits cleanly for a one-shot/noninteractive run.
|
|
32
|
+
5. Re-run the Cursor unit tests and update these notes for any changed protobuf paths.
|
|
33
|
+
|
|
34
|
+
If Cursor changes the private protocol, update the vendored generated protobuf descriptors from the reference/source protocol before changing the codec behavior.
|