pi-provider-cursor-ask 0.1.0

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 (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +87 -0
  4. package/README.zh-CN.md +87 -0
  5. package/UPSTREAM_CHANGELOG.md +368 -0
  6. package/UPSTREAM_SOURCE.md +23 -0
  7. package/dist/index.js +54 -0
  8. package/package.json +97 -0
  9. package/src/auth/cli-credentials.ts +275 -0
  10. package/src/auth/consent.ts +25 -0
  11. package/src/auth/index.ts +23 -0
  12. package/src/auth/oauth.ts +282 -0
  13. package/src/auth/refresh-guard.ts +93 -0
  14. package/src/client/bridge.ts +673 -0
  15. package/src/client/cursor-wire.ts +213 -0
  16. package/src/client/h2-unary.ts +142 -0
  17. package/src/client/index.ts +18 -0
  18. package/src/config/index.ts +69 -0
  19. package/src/diagnostics/diagnostics.ts +116 -0
  20. package/src/diagnostics/index.ts +1 -0
  21. package/src/extension/auth.ts +99 -0
  22. package/src/extension/commands.ts +163 -0
  23. package/src/extension/compaction-guard.ts +86 -0
  24. package/src/extension/debug-hooks.ts +359 -0
  25. package/src/extension/index.ts +8 -0
  26. package/src/extension/provider.ts +277 -0
  27. package/src/extension/quota-adapter.ts +175 -0
  28. package/src/extension/report-dashboard.ts +133 -0
  29. package/src/identity.ts +16 -0
  30. package/src/index.ts +186 -0
  31. package/src/models/ask-catalog.ts +384 -0
  32. package/src/models/catalog.json +1163 -0
  33. package/src/models/cost.ts +126 -0
  34. package/src/models/index.ts +6 -0
  35. package/src/models/limits.ts +36 -0
  36. package/src/models/parameterized.ts +416 -0
  37. package/src/models/processing.ts +313 -0
  38. package/src/proto/agent_pb.ts +14577 -0
  39. package/src/stream/bridge-session.ts +215 -0
  40. package/src/stream/client-transcript.ts +51 -0
  41. package/src/stream/config.ts +5 -0
  42. package/src/stream/context-normalize.ts +308 -0
  43. package/src/stream/context-usage.ts +168 -0
  44. package/src/stream/debug-log.ts +316 -0
  45. package/src/stream/drift.ts +122 -0
  46. package/src/stream/images.ts +201 -0
  47. package/src/stream/index.ts +68 -0
  48. package/src/stream/interaction-query.ts +369 -0
  49. package/src/stream/message-parsing.ts +402 -0
  50. package/src/stream/model-cache.ts +100 -0
  51. package/src/stream/model-discovery.ts +242 -0
  52. package/src/stream/model-routing.ts +100 -0
  53. package/src/stream/native-core.ts +2121 -0
  54. package/src/stream/pi-adapter.ts +414 -0
  55. package/src/stream/protocol.ts +63 -0
  56. package/src/stream/recovery.ts +494 -0
  57. package/src/stream/request-build.ts +668 -0
  58. package/src/stream/root-prompt.ts +184 -0
  59. package/src/stream/run-journal.ts +474 -0
  60. package/src/stream/run-usage.ts +107 -0
  61. package/src/stream/server-messages.ts +777 -0
  62. package/src/stream/session-state.ts +499 -0
  63. package/src/stream/stream-writer.ts +211 -0
  64. package/src/stream/thinking-filter.ts +63 -0
  65. package/src/stream/tool-schema.ts +185 -0
  66. package/src/stream/transport-errors.ts +150 -0
  67. package/src/stream/tuning.ts +250 -0
  68. package/src/stream/types.ts +330 -0
  69. package/src/types/enums.ts +103 -0
  70. package/src/types/index.ts +4 -0
  71. package/src/usage.ts +262 -0
  72. package/src/utils/cache-dir.ts +39 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/security.ts +68 -0
  75. package/src/utils/util.ts +43 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Live model discovery over in-process Node HTTP/2 unary RPCs.
3
+ *
4
+ * Cursor exposes the account's usable models through `GetUsableModels` plus a
5
+ * parameterized-metadata variant. Responses may be raw protobuf or a Connect
6
+ * frame that `decodeConnectUnaryBody` unwraps.
7
+ *
8
+ * Results are memoized per access token: a token hash keys the cache so a
9
+ * re-login or account switch invalidates it without a manual reset.
10
+ */
11
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
12
+ import { createHash } from "node:crypto";
13
+
14
+ import { GetUsableModelsRequestSchema, GetUsableModelsResponseSchema } from "../proto/agent_pb.js";
15
+ import {
16
+ decodeAvailableModelsResponse,
17
+ encodeAvailableModelsRequest,
18
+ type CursorModelParameter,
19
+ type CursorParameterizedModel,
20
+ } from "../client/cursor-wire.js";
21
+ import { callUnaryOverH2, UnaryH2TimeoutError } from "../client/h2-unary.js";
22
+ import { getCursorAgentUrl } from "./config.js";
23
+ import { writeCachedCatalog } from "./model-cache.js";
24
+ import { inferCursorContextWindow, inferCursorMaxOutputTokens } from "../models/limits.js";
25
+ import { lifecycleLog, reportCursorAnomaly } from "./debug-log.js";
26
+
27
+ // Re-exported so existing importers of the model-discovery surface keep working.
28
+ export {
29
+ DEFAULT_CONTEXT_WINDOW,
30
+ DEFAULT_MAX_OUTPUT_TOKENS,
31
+ inferCursorContextWindow,
32
+ inferCursorMaxOutputTokens,
33
+ } from "../models/limits.js";
34
+
35
+ export async function callCursorUnaryRpc(options: {
36
+ accessToken: string;
37
+ rpcPath: string;
38
+ requestBody: Uint8Array;
39
+ url?: string;
40
+ timeoutMs?: number;
41
+ signal?: AbortSignal;
42
+ }): Promise<{ body: Uint8Array; exitCode: number; timedOut: boolean }> {
43
+ try {
44
+ const result = await callUnaryOverH2({
45
+ accessToken: options.accessToken,
46
+ rpcPath: options.rpcPath,
47
+ requestBody: options.requestBody,
48
+ url: options.url,
49
+ timeoutMs: options.timeoutMs ?? 5_000,
50
+ signal: options.signal,
51
+ });
52
+ const ok = result.status >= 200 && result.status < 300;
53
+ return { body: result.body, exitCode: ok ? 0 : 1, timedOut: false };
54
+ } catch (error) {
55
+ const timedOut = options.signal?.aborted === true || error instanceof UnaryH2TimeoutError;
56
+ return { body: new Uint8Array(0), exitCode: 1, timedOut };
57
+ }
58
+ }
59
+
60
+ export interface CursorModel {
61
+ id: string;
62
+ name: string;
63
+ reasoning: boolean;
64
+ contextWindow: number;
65
+ maxTokens: number;
66
+ requestedModelId?: string;
67
+ parameters?: CursorModelParameter[];
68
+ requiresMaxMode?: boolean;
69
+ requestedMaxMode?: boolean;
70
+ supportsImages?: boolean;
71
+ }
72
+
73
+ let cachedModels: { tokenHash: string; models: CursorModel[]; expiresAt: number } | null = null;
74
+
75
+ let cachedParameterizedModels: {
76
+ tokenHash: string;
77
+ models: CursorParameterizedModel[];
78
+ expiresAt: number;
79
+ } | null = null;
80
+
81
+ /** Model list cache TTL: 5 minutes. Re-fetches on token change or TTL expiry. */
82
+ const MODEL_CACHE_TTL_MS = 5 * 60 * 1000;
83
+
84
+ function tokenCacheHash(apiKey: string): string {
85
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
86
+ }
87
+
88
+ export async function getCursorModels(
89
+ apiKey: string,
90
+ options?: { signal?: AbortSignal },
91
+ ): Promise<CursorModel[]> {
92
+ const tokenHash = tokenCacheHash(apiKey);
93
+ if (cachedModels?.tokenHash === tokenHash && Date.now() < cachedModels.expiresAt)
94
+ return cachedModels.models;
95
+ try {
96
+ const requestPayload = create(GetUsableModelsRequestSchema, {});
97
+ const requestBody = toBinary(GetUsableModelsRequestSchema, requestPayload);
98
+ const response = await callCursorUnaryRpc({
99
+ accessToken: apiKey,
100
+ rpcPath: "/agent.v1.AgentService/GetUsableModels",
101
+ requestBody,
102
+ url: getCursorAgentUrl(),
103
+ signal: options?.signal,
104
+ });
105
+ if (!response.timedOut && response.exitCode === 0 && response.body.length > 0) {
106
+ let decoded: any = null;
107
+ try {
108
+ decoded = fromBinary(GetUsableModelsResponseSchema, response.body);
109
+ } catch {
110
+ // Try Connect framing after plain protobuf decode fails.
111
+ const body = decodeConnectUnaryBody(response.body);
112
+ if (body) {
113
+ try {
114
+ decoded = fromBinary(GetUsableModelsResponseSchema, body);
115
+ } catch {
116
+ decoded = null;
117
+ }
118
+ }
119
+ }
120
+ if (decoded?.models?.length) {
121
+ const models = normalizeCursorModels(decoded.models);
122
+ if (models.length > 0) {
123
+ cachedModels = { tokenHash, models, expiresAt: Date.now() + MODEL_CACHE_TTL_MS };
124
+ return models;
125
+ }
126
+ }
127
+ }
128
+ } catch (err) {
129
+ if (options?.signal?.aborted) return [];
130
+ reportCursorAnomaly(
131
+ "model_discovery_failed",
132
+ "Cursor model discovery failed",
133
+ { message: err instanceof Error ? err.message : String(err) },
134
+ { level: "error", stderrIfNoSink: true },
135
+ );
136
+ return [];
137
+ }
138
+ if (options?.signal?.aborted) return [];
139
+ reportCursorAnomaly(
140
+ "model_discovery_failed",
141
+ "Cursor model discovery failed",
142
+ { reason: "no_models" },
143
+ { level: "warning", stderrIfNoSink: true },
144
+ );
145
+ return [];
146
+ }
147
+
148
+ function decodeConnectUnaryBody(payload: Uint8Array): Uint8Array | null {
149
+ if (payload.length < 5) return null;
150
+ let offset = 0;
151
+ while (offset + 5 <= payload.length) {
152
+ const flags = payload[offset]!;
153
+ const view = new DataView(
154
+ payload.buffer,
155
+ payload.byteOffset + offset,
156
+ payload.byteLength - offset,
157
+ );
158
+ const messageLength = view.getUint32(1, false);
159
+ const frameEnd = offset + 5 + messageLength;
160
+ if (frameEnd > payload.length) return null;
161
+ if ((flags & 0b0000_0001) !== 0) return null;
162
+ if ((flags & 0b0000_0010) === 0) return payload.subarray(offset + 5, frameEnd);
163
+ offset = frameEnd;
164
+ }
165
+ return null;
166
+ }
167
+
168
+ export async function getCursorParameterizedModels(
169
+ apiKey: string,
170
+ options?: { signal?: AbortSignal },
171
+ ): Promise<CursorParameterizedModel[]> {
172
+ const tokenHash = tokenCacheHash(apiKey);
173
+ if (
174
+ cachedParameterizedModels?.tokenHash === tokenHash &&
175
+ Date.now() < cachedParameterizedModels.expiresAt
176
+ )
177
+ return cachedParameterizedModels.models;
178
+ try {
179
+ const response = await callCursorUnaryRpc({
180
+ accessToken: apiKey,
181
+ rpcPath: "/aiserver.v1.AiService/AvailableModels",
182
+ requestBody: encodeAvailableModelsRequest(),
183
+ signal: options?.signal,
184
+ });
185
+ if (response.timedOut || response.exitCode !== 0 || response.body.length === 0) return [];
186
+ const body = decodeConnectUnaryBody(response.body) ?? response.body;
187
+ const models = decodeAvailableModelsResponse(body);
188
+ cachedParameterizedModels = { tokenHash, models, expiresAt: Date.now() + MODEL_CACHE_TTL_MS };
189
+ return models;
190
+ } catch (err) {
191
+ if (options?.signal?.aborted) return [];
192
+ lifecycleLog("model_discovery_failed", {
193
+ kind: "parameterized",
194
+ message: err instanceof Error ? err.message : String(err),
195
+ });
196
+ return [];
197
+ }
198
+ }
199
+
200
+ export interface CursorCatalog {
201
+ rawModels: CursorModel[];
202
+ parameterizedModels: CursorParameterizedModel[];
203
+ }
204
+
205
+ /**
206
+ * Run both discovery RPCs and persist the result for the next process.
207
+ *
208
+ * This is the only path that writes the cross-process catalog cache, so a
209
+ * partial failure (one RPC empty) still records whatever did come back rather
210
+ * than leaving the next launch on the bundled fallback list.
211
+ */
212
+ export async function discoverCursorCatalog(
213
+ apiKey: string,
214
+ options?: { signal?: AbortSignal },
215
+ ): Promise<CursorCatalog> {
216
+ const [rawModels, parameterizedModels] = await Promise.all([
217
+ getCursorModels(apiKey, options),
218
+ getCursorParameterizedModels(apiKey, options),
219
+ ]);
220
+ if (rawModels.length > 0 || parameterizedModels.length > 0) {
221
+ writeCachedCatalog({ tokenHash: tokenCacheHash(apiKey), rawModels, parameterizedModels });
222
+ }
223
+ return { rawModels, parameterizedModels };
224
+ }
225
+
226
+ function normalizeCursorModels(models: readonly unknown[]): CursorModel[] {
227
+ const byId = new Map<string, CursorModel>();
228
+ for (const model of models) {
229
+ const m = model as any;
230
+ const id = m?.modelId?.trim?.();
231
+ if (!id) continue;
232
+ const name = m.displayName || m.displayNameShort || m.displayModelId || id;
233
+ byId.set(id, {
234
+ id,
235
+ name,
236
+ reasoning: Boolean(m.thinkingDetails),
237
+ contextWindow: inferCursorContextWindow(id, name),
238
+ maxTokens: inferCursorMaxOutputTokens(id, name),
239
+ });
240
+ }
241
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
242
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Model ID effort suffix routing for Cursor runtime variants.
3
+ */
4
+
5
+ export interface CursorNativeModelRouting {
6
+ modelId: string;
7
+ parameters?: Array<{ id: string; value: string }>;
8
+ requiresMaxMode?: boolean;
9
+ requestedMaxMode?: boolean;
10
+ }
11
+
12
+ export interface ResolvedCursorModelRouting extends CursorNativeModelRouting {
13
+ maxMode: boolean;
14
+ }
15
+
16
+ type CursorModelRoutingByEffort = Record<string, CursorNativeModelRouting>;
17
+
18
+ export interface CursorResolvableModel {
19
+ id: string;
20
+ [key: string]: unknown;
21
+ }
22
+
23
+ /**
24
+ * Insert reasoning effort into model ID, before -fast/-thinking suffix.
25
+ * e.g. model="gpt-5.4" + effort="medium" → "gpt-5.4-medium"
26
+ * model="gpt-5.4-fast" + effort="high" → "gpt-5.4-high-fast"
27
+ * If no effort provided, returns model as-is.
28
+ */
29
+ export function resolveModelId(model: string, reasoningEffort?: string): string {
30
+ if (!reasoningEffort) return model;
31
+
32
+ let suffix = "";
33
+ let base = model;
34
+ if (base.endsWith("-fast")) {
35
+ suffix = "-fast";
36
+ base = base.slice(0, -5);
37
+ } else if (base.endsWith("-thinking")) {
38
+ suffix = "-thinking";
39
+ base = base.slice(0, -9);
40
+ }
41
+
42
+ return `${base}-${reasoningEffort}${suffix}`;
43
+ }
44
+
45
+ function isCursorModelRouting(value: unknown): value is CursorNativeModelRouting {
46
+ return (
47
+ !!value &&
48
+ typeof value === "object" &&
49
+ typeof (value as { modelId?: unknown }).modelId === "string"
50
+ );
51
+ }
52
+
53
+ export function resolveRequestedModelId(
54
+ model: string,
55
+ reasoningEffort?: string,
56
+ cursorModelId?: string,
57
+ ): string;
58
+ export function resolveRequestedModelId(
59
+ model: CursorResolvableModel,
60
+ reasoningEffort?: string,
61
+ routingByModelId?: Map<string, CursorModelRoutingByEffort | CursorNativeModelRouting>,
62
+ ): ResolvedCursorModelRouting;
63
+ export function resolveRequestedModelId(
64
+ model: string | CursorResolvableModel,
65
+ reasoningEffort?: string,
66
+ cursorModelIdOrRoutingByModelId?:
67
+ string | Map<string, CursorModelRoutingByEffort | CursorNativeModelRouting>,
68
+ ): string | ResolvedCursorModelRouting {
69
+ if (typeof model === "string") {
70
+ const trimmedCursorModelId =
71
+ typeof cursorModelIdOrRoutingByModelId === "string"
72
+ ? cursorModelIdOrRoutingByModelId.trim()
73
+ : "";
74
+ if (trimmedCursorModelId) return trimmedCursorModelId;
75
+ return resolveModelId(model, reasoningEffort);
76
+ }
77
+
78
+ const routingByModelId =
79
+ cursorModelIdOrRoutingByModelId instanceof Map ? cursorModelIdOrRoutingByModelId : undefined;
80
+ const configured = routingByModelId?.get(model.id);
81
+ let routing: CursorNativeModelRouting | undefined;
82
+ if (isCursorModelRouting(configured)) {
83
+ routing = configured;
84
+ } else if (configured) {
85
+ routing =
86
+ configured[reasoningEffort ?? ""] ??
87
+ configured.none ??
88
+ configured.medium ??
89
+ configured.high ??
90
+ Object.values(configured).find(isCursorModelRouting);
91
+ }
92
+
93
+ return {
94
+ modelId: routing?.modelId ?? resolveModelId(model.id, reasoningEffort),
95
+ maxMode: Boolean(routing?.requestedMaxMode ?? routing?.requiresMaxMode),
96
+ parameters: routing?.parameters,
97
+ requestedMaxMode: routing?.requestedMaxMode,
98
+ requiresMaxMode: routing?.requiresMaxMode,
99
+ };
100
+ }