@herbertgao/pi-extensions 2026.9.9 → 2026.9.11
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/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/node_modules/@herbertgao/pi-bark/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +12 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +427 -120
- package/node_modules/@herbertgao/pi-subagents/docs/rpc.md +184 -0
- package/node_modules/@herbertgao/pi-subagents/docs/workflows.md +466 -0
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +6 -6
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/compose.js +52 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/fan-out-audit.js +56 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/gated-fix.js +60 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/lib/count-child.js +30 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/review-panel.js +68 -0
- package/node_modules/@herbertgao/pi-subagents/examples/workflows/structured-findings.js +81 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +12 -10
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +52 -12
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +837 -146
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +213 -39
- package/node_modules/@herbertgao/pi-subagents/src/cross-extension-rpc.ts +73 -14
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +101 -47
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +2249 -914
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +13 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +215 -0
- package/node_modules/@herbertgao/pi-subagents/src/mention.ts +147 -0
- package/node_modules/@herbertgao/pi-subagents/src/model-resolver.ts +9 -1
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +40 -26
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +18 -8
- package/node_modules/@herbertgao/pi-subagents/src/prompts.ts +46 -9
- package/node_modules/@herbertgao/pi-subagents/src/schedule.ts +21 -16
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +137 -7
- package/node_modules/@herbertgao/pi-subagents/src/structured-output.ts +136 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +126 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-mention.ts +274 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +20 -5
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +10 -4
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +167 -22
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-card.ts +555 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-dialog.ts +1304 -0
- package/node_modules/@herbertgao/pi-subagents/src/ui/workflow-menu.ts +226 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +122 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/entry.ts +47 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/host.ts +463 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/journal.ts +164 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +142 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/meta.ts +401 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/progress.ts +622 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/runtime.ts +1399 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/saved.ts +230 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/task.ts +333 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/tool-description.ts +200 -0
- package/node_modules/@herbertgao/pi-subagents/src/workflow/worker-source.ts +781 -0
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +97 -95
- package/node_modules/@herbertgao/pi-subagents/src/xml.ts +13 -0
- package/node_modules/@herbertgao/resume-from/package.json +1 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
- package/node_modules/@herbertgao/sol-pi/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +4 -4
- package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
- package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
- package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
- package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
- package/node_modules/pi-multi-account/LICENSE +21 -0
- package/node_modules/pi-multi-account/README.md +197 -0
- package/node_modules/pi-multi-account/SECURITY.md +27 -0
- package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
- package/node_modules/pi-multi-account/child-usability.ts +233 -0
- package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
- package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
- package/node_modules/pi-multi-account/context-guard.ts +420 -0
- package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
- package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
- package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
- package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
- package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
- package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
- package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
- package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
- package/node_modules/pi-multi-account/cursor/index.ts +572 -0
- package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
- package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
- package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
- package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
- package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
- package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
- package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
- package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
- package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
- package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
- package/node_modules/pi-multi-account/index.ts +11825 -0
- package/node_modules/pi-multi-account/model-catalog.ts +354 -0
- package/node_modules/pi-multi-account/package.json +101 -0
- package/node_modules/pi-multi-account/pi-contract.ts +281 -0
- package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
- package/node_modules/pi-multi-account/provider-priority.ts +189 -0
- package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
- package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
- package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
- package/node_modules/pi-multi-account/usage.ts +1099 -0
- package/node_modules/pi-typesafe/README.md +6 -2
- package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
- package/node_modules/pi-typesafe/dist/client.js +45 -10
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/package.json +2 -2
- package/node_modules/pi-web-access/CHANGELOG.md +36 -0
- package/node_modules/pi-web-access/README.md +75 -18
- package/node_modules/pi-web-access/anysearch.ts +4 -15
- package/node_modules/pi-web-access/bocha.ts +3 -22
- package/node_modules/pi-web-access/brave.ts +3 -21
- package/node_modules/pi-web-access/brightdata.ts +5 -32
- package/node_modules/pi-web-access/content-find.ts +168 -53
- package/node_modules/pi-web-access/curator-page.ts +4 -1
- package/node_modules/pi-web-access/curator-run.ts +2 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +24620 -0
- package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
- package/node_modules/pi-web-access/duckduckgo.ts +3 -21
- package/node_modules/pi-web-access/extract.ts +3 -1
- package/node_modules/pi-web-access/firecrawl.ts +5 -29
- package/node_modules/pi-web-access/gemini-search.ts +81 -32
- package/node_modules/pi-web-access/index.ts +149 -148
- package/node_modules/pi-web-access/jina-search.ts +4 -15
- package/node_modules/pi-web-access/kagi.ts +4 -13
- package/node_modules/pi-web-access/kimi-search.ts +5 -30
- package/node_modules/pi-web-access/mistral-search.ts +1 -15
- package/node_modules/pi-web-access/ollama.ts +2 -7
- package/node_modules/pi-web-access/openai-search.ts +174 -36
- package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
- package/node_modules/pi-web-access/package.json +10 -4
- package/node_modules/pi-web-access/page-query.ts +10 -2
- package/node_modules/pi-web-access/parallel.ts +1 -15
- package/node_modules/pi-web-access/pdf-extract.ts +3 -0
- package/node_modules/pi-web-access/querit.ts +5 -29
- package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
- package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
- package/node_modules/pi-web-access/search1api.ts +5 -29
- package/node_modules/pi-web-access/searchinfinity.ts +5 -29
- package/node_modules/pi-web-access/searxng.ts +3 -21
- package/node_modules/pi-web-access/serpapi.ts +5 -28
- package/node_modules/pi-web-access/serpbase.ts +3 -22
- package/node_modules/pi-web-access/serpdive.ts +3 -21
- package/node_modules/pi-web-access/serper.ts +5 -28
- package/node_modules/pi-web-access/serply.ts +197 -0
- package/node_modules/pi-web-access/source-check.ts +11 -47
- package/node_modules/pi-web-access/summary-review.ts +7 -3
- package/node_modules/pi-web-access/tavily.ts +3 -21
- package/node_modules/pi-web-access/tinyfish.ts +5 -29
- package/node_modules/pi-web-access/utils.ts +9 -1
- package/node_modules/pi-web-access/valyu.ts +5 -28
- package/node_modules/pi-web-access/xai-search.ts +1 -15
- package/node_modules/pi-web-access/xcrawl.ts +5 -32
- package/package.json +17 -11
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account surface for the Cursor provider.
|
|
3
|
+
*
|
|
4
|
+
* The single-account extension (`index.ts`) keeps ONE module-level access token and
|
|
5
|
+
* registers ONE provider called `cursor`. Rotation needs N providers — `cursor`,
|
|
6
|
+
* `cursor-account-2`, ... — that share one local proxy while each authenticates as its
|
|
7
|
+
* own Cursor subscription.
|
|
8
|
+
*
|
|
9
|
+
* The account is carried by the request itself: every slot's `getApiKey` returns THAT
|
|
10
|
+
* slot's access token, Pi puts it in the `Authorization` header of the call to the local
|
|
11
|
+
* proxy, and the proxy hands the very same token to Cursor. Nothing is cached between
|
|
12
|
+
* requests, so a failover mid-session takes effect on the next call instead of silently
|
|
13
|
+
* re-using the spent account's token.
|
|
14
|
+
*
|
|
15
|
+
* Consumed by pi-multi-account's `cursor-bridge.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { cursorPayloadStream } from "../provider-payload-stream.ts";
|
|
20
|
+
import {
|
|
21
|
+
generateCursorAuthParams,
|
|
22
|
+
getTokenExpiry,
|
|
23
|
+
pollCursorAuth,
|
|
24
|
+
refreshCursorToken,
|
|
25
|
+
} from "./auth.ts";
|
|
26
|
+
import { FALLBACK_MODELS, modelConfig, processModels } from "./index.ts";
|
|
27
|
+
import { getCursorModels, startProxy, type CursorModel } from "./proxy.ts";
|
|
28
|
+
|
|
29
|
+
export const CURSOR_BASE = "cursor";
|
|
30
|
+
|
|
31
|
+
/** Resolves the stored access token for a provider id (reads the host's auth.json). */
|
|
32
|
+
type TokenResolver = (providerId: string) => Promise<string>;
|
|
33
|
+
|
|
34
|
+
type AuthEntryLike = {
|
|
35
|
+
type?: string;
|
|
36
|
+
access?: string;
|
|
37
|
+
refresh?: string;
|
|
38
|
+
expires?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type RegisterOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* Host guard that refuses a login when the SAME real Cursor account is already stored
|
|
44
|
+
* in another slot. Throwing here is correct: two slots on one account is not a rotation,
|
|
45
|
+
* it is one account that would be benched twice.
|
|
46
|
+
*/
|
|
47
|
+
rejectDuplicateLogin?: (slot: string, credentials: AuthEntryLike) => AuthEntryLike;
|
|
48
|
+
/** Called with the real catalog once a token can read it, so the host can re-register. */
|
|
49
|
+
onModelsDiscovered?: (models: CursorModel[]) => void;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// `var`, deliberately: these are hoisted, so even a caller that reaches this module while it
|
|
53
|
+
// is still initializing (a concurrent import) sees `undefined` instead of a temporal-dead-zone
|
|
54
|
+
// crash. The module is shared by every account slot and must never be the reason a session
|
|
55
|
+
// loses Cursor.
|
|
56
|
+
// eslint-disable-next-line no-var
|
|
57
|
+
var proxyPromise: Promise<number> | undefined;
|
|
58
|
+
// eslint-disable-next-line no-var
|
|
59
|
+
var tokenResolver: TokenResolver | undefined;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The bearer token Pi attached to this proxy call, i.e. the identity of the slot that
|
|
63
|
+
* made it. `cursor-proxy` is the single-account extension's placeholder and carries no
|
|
64
|
+
* identity, so it is treated as absent.
|
|
65
|
+
*/
|
|
66
|
+
function bearerFromRequest(req?: {
|
|
67
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
68
|
+
}): string | undefined {
|
|
69
|
+
const raw = req?.headers?.authorization;
|
|
70
|
+
const header = Array.isArray(raw) ? raw[0] : raw;
|
|
71
|
+
if (typeof header !== "string") return undefined;
|
|
72
|
+
const match = /^Bearer\s+(.+)$/i.exec(header.trim());
|
|
73
|
+
const token = match?.[1]?.trim();
|
|
74
|
+
if (!token || token === "cursor-proxy") return undefined;
|
|
75
|
+
return token;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Start (once) the shared local proxy. Idempotent: later calls only refresh the fallback
|
|
80
|
+
* resolver and return the port already bound, so every slot registers against one port.
|
|
81
|
+
*/
|
|
82
|
+
export async function ensureCursorProxy(resolve: TokenResolver): Promise<number> {
|
|
83
|
+
tokenResolver = resolve;
|
|
84
|
+
proxyPromise ??= startProxy(async (req) => {
|
|
85
|
+
const fromRequest = bearerFromRequest(req);
|
|
86
|
+
if (fromRequest) return fromRequest;
|
|
87
|
+
// No usable token on the request (a legacy placeholder, or a caller that sends none):
|
|
88
|
+
// fall back to the base slot's stored credential rather than failing the call.
|
|
89
|
+
const fallback = (await tokenResolver?.(CURSOR_BASE)) ?? "";
|
|
90
|
+
if (!fallback) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"No Cursor account is logged in for this request. Run /login and pick a cursor slot.",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return fallback;
|
|
96
|
+
});
|
|
97
|
+
return proxyPromise;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function displayName(id: string): string {
|
|
101
|
+
return id === CURSOR_BASE ? "Cursor" : `Cursor (${id})`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function discoverModels(
|
|
105
|
+
accessToken: string,
|
|
106
|
+
onModelsDiscovered?: (models: CursorModel[]) => void,
|
|
107
|
+
): Promise<void> {
|
|
108
|
+
if (!onModelsDiscovered) return;
|
|
109
|
+
try {
|
|
110
|
+
const discovered = await getCursorModels(accessToken);
|
|
111
|
+
if (discovered.length > 0) onModelsDiscovered(discovered);
|
|
112
|
+
} catch {
|
|
113
|
+
// Catalog discovery is an enhancement over FALLBACK_MODELS. A login must never fail
|
|
114
|
+
// because Cursor's model list could not be read.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register one Cursor account slot as its own Pi provider.
|
|
120
|
+
*
|
|
121
|
+
* Safe to call repeatedly for the same id — that is how a freshly discovered catalog
|
|
122
|
+
* replaces the fallback list.
|
|
123
|
+
*/
|
|
124
|
+
export function registerCursorProvider(
|
|
125
|
+
pi: ExtensionAPI,
|
|
126
|
+
id: string,
|
|
127
|
+
proxyPort: number,
|
|
128
|
+
rawModels: CursorModel[] = FALLBACK_MODELS,
|
|
129
|
+
options: RegisterOptions = {},
|
|
130
|
+
): void {
|
|
131
|
+
const name = displayName(id);
|
|
132
|
+
const models = processModels(rawModels).map(modelConfig);
|
|
133
|
+
|
|
134
|
+
(pi as any).registerProvider(id, {
|
|
135
|
+
name,
|
|
136
|
+
baseUrl: `http://127.0.0.1:${proxyPort}/v1`,
|
|
137
|
+
api: "openai-completions",
|
|
138
|
+
streamSimple: cursorPayloadStream,
|
|
139
|
+
models,
|
|
140
|
+
oauth: {
|
|
141
|
+
name,
|
|
142
|
+
isSubscription: true,
|
|
143
|
+
|
|
144
|
+
async login(callbacks: any): Promise<AuthEntryLike> {
|
|
145
|
+
const { verifier, uuid, loginUrl } = await generateCursorAuthParams();
|
|
146
|
+
callbacks.onAuth({ url: loginUrl });
|
|
147
|
+
const { accessToken, refreshToken } = await pollCursorAuth(uuid, verifier);
|
|
148
|
+
const credentials: AuthEntryLike = {
|
|
149
|
+
type: "oauth",
|
|
150
|
+
access: accessToken,
|
|
151
|
+
refresh: refreshToken,
|
|
152
|
+
expires: getTokenExpiry(accessToken),
|
|
153
|
+
};
|
|
154
|
+
// Reject BEFORE the catalog call: a duplicate must cost nothing and change nothing.
|
|
155
|
+
const verified = options.rejectDuplicateLogin
|
|
156
|
+
? options.rejectDuplicateLogin(id, credentials)
|
|
157
|
+
: credentials;
|
|
158
|
+
await discoverModels(accessToken, options.onModelsDiscovered);
|
|
159
|
+
return verified;
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async refreshToken(credentials: AuthEntryLike): Promise<AuthEntryLike> {
|
|
163
|
+
const refreshed = await refreshCursorToken(credentials.refresh as string);
|
|
164
|
+
await discoverModels(refreshed.access, options.onModelsDiscovered);
|
|
165
|
+
return { type: "oauth", ...refreshed };
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* THIS slot's token, not a shared placeholder — it is what tells the proxy which
|
|
170
|
+
* Cursor account the request belongs to.
|
|
171
|
+
*/
|
|
172
|
+
getApiKey(credentials: AuthEntryLike): string {
|
|
173
|
+
return credentials.access ?? "";
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { FALLBACK_MODELS };
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Read the account's real model catalog.
|
|
183
|
+
*
|
|
184
|
+
* THROWS the underlying error: callers (login, refresh, startup discovery) each decide
|
|
185
|
+
* whether a failed catalog read is fatal — swallowing it here left every layer above
|
|
186
|
+
* guessing why the fallback list was still in effect.
|
|
187
|
+
*/
|
|
188
|
+
export async function discoverCursorModels(
|
|
189
|
+
accessToken: string,
|
|
190
|
+
): Promise<CursorModel[]> {
|
|
191
|
+
return getCursorModels(accessToken);
|
|
192
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Dumb HTTP/2 bidirectional pipe for Cursor gRPC.
|
|
4
|
+
*
|
|
5
|
+
* Originally from https://github.com/ephraimduncan/opencode-cursor by Ephraim Duncan (MIT).
|
|
6
|
+
*
|
|
7
|
+
* Bun's node:http2 is broken. This Node script acts as a transparent
|
|
8
|
+
* HTTP/2 proxy: it opens a single bidirectional stream and ferries
|
|
9
|
+
* raw bytes between the parent process (via stdin/stdout) and Cursor.
|
|
10
|
+
*
|
|
11
|
+
* Protocol (length-prefixed framing over stdin/stdout):
|
|
12
|
+
* [4 bytes big-endian length][payload]
|
|
13
|
+
*
|
|
14
|
+
* First message on stdin is JSON config:
|
|
15
|
+
* { "accessToken": "...", "url": "...", "path": "...", "unary": false }
|
|
16
|
+
*
|
|
17
|
+
* When unary=true, the bridge uses application/proto (raw protobuf) instead
|
|
18
|
+
* of application/connect+proto (Connect streaming). The single stdin message
|
|
19
|
+
* is written as the request body and the stream is ended immediately.
|
|
20
|
+
* After config, subsequent stdin messages are raw bytes to write to the H2 stream.
|
|
21
|
+
* H2 response data is written to stdout using the same length-prefixed framing.
|
|
22
|
+
*/
|
|
23
|
+
import http2 from "node:http2";
|
|
24
|
+
import crypto from "node:crypto";
|
|
25
|
+
|
|
26
|
+
const CURSOR_CLIENT_VERSION = "cli-2026.01.09-231024f";
|
|
27
|
+
|
|
28
|
+
/** Write one length-prefixed message to stdout. */
|
|
29
|
+
function writeMessage(data) {
|
|
30
|
+
const lenBuf = Buffer.alloc(4);
|
|
31
|
+
lenBuf.writeUInt32BE(data.length, 0);
|
|
32
|
+
process.stdout.write(lenBuf);
|
|
33
|
+
process.stdout.write(data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Buffered stdin reader ---
|
|
37
|
+
|
|
38
|
+
let stdinBuf = Buffer.alloc(0);
|
|
39
|
+
let stdinResolve = null;
|
|
40
|
+
let stdinEnded = false;
|
|
41
|
+
|
|
42
|
+
process.stdin.on("data", (chunk) => {
|
|
43
|
+
stdinBuf = Buffer.concat([stdinBuf, chunk]);
|
|
44
|
+
if (stdinResolve) {
|
|
45
|
+
const r = stdinResolve;
|
|
46
|
+
stdinResolve = null;
|
|
47
|
+
r();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.stdin.on("end", () => {
|
|
52
|
+
stdinEnded = true;
|
|
53
|
+
if (stdinResolve) {
|
|
54
|
+
const r = stdinResolve;
|
|
55
|
+
stdinResolve = null;
|
|
56
|
+
r();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function waitForData() {
|
|
61
|
+
return new Promise((resolve) => { stdinResolve = resolve; });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function readExact(n) {
|
|
65
|
+
while (stdinBuf.length < n) {
|
|
66
|
+
if (stdinEnded) return null;
|
|
67
|
+
await waitForData();
|
|
68
|
+
}
|
|
69
|
+
const result = stdinBuf.subarray(0, n);
|
|
70
|
+
stdinBuf = stdinBuf.subarray(n);
|
|
71
|
+
return Buffer.from(result);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readMessage() {
|
|
75
|
+
const lenBuf = await readExact(4);
|
|
76
|
+
if (!lenBuf) return null;
|
|
77
|
+
const len = lenBuf.readUInt32BE(0);
|
|
78
|
+
if (len === 0) return Buffer.alloc(0);
|
|
79
|
+
return readExact(len);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Main ---
|
|
83
|
+
|
|
84
|
+
const configBuf = await readMessage();
|
|
85
|
+
if (!configBuf) process.exit(1);
|
|
86
|
+
|
|
87
|
+
const config = JSON.parse(configBuf.toString("utf8"));
|
|
88
|
+
const { accessToken, url, path: rpcPath, unary } = config;
|
|
89
|
+
|
|
90
|
+
const client = http2.connect(url || "https://api2.cursor.sh");
|
|
91
|
+
|
|
92
|
+
// Guard against initial connection failure. Reset on any h2 activity
|
|
93
|
+
// so long-running agent conversations (with tool call round-trips) survive.
|
|
94
|
+
let timeout = setTimeout(killBridge, 30_000);
|
|
95
|
+
|
|
96
|
+
function resetTimeout() {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
timeout = setTimeout(killBridge, 120_000);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function killBridge() {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
client.destroy();
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
client.on("error", () => {
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const headers = {
|
|
113
|
+
":method": "POST",
|
|
114
|
+
":path": rpcPath || "/agent.v1.AgentService/Run",
|
|
115
|
+
"content-type": unary ? "application/proto" : "application/connect+proto",
|
|
116
|
+
te: "trailers",
|
|
117
|
+
authorization: `Bearer ${accessToken}`,
|
|
118
|
+
"x-ghost-mode": "true",
|
|
119
|
+
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
120
|
+
"x-cursor-client-type": "cli",
|
|
121
|
+
"x-request-id": crypto.randomUUID(),
|
|
122
|
+
};
|
|
123
|
+
if (!unary) {
|
|
124
|
+
headers["connect-protocol-version"] = "1";
|
|
125
|
+
}
|
|
126
|
+
const h2Stream = client.request(headers);
|
|
127
|
+
|
|
128
|
+
// Forward H2 response data → stdout (length-prefixed)
|
|
129
|
+
h2Stream.on("data", (chunk) => {
|
|
130
|
+
resetTimeout();
|
|
131
|
+
writeMessage(chunk);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
h2Stream.on("end", () => {
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
client.close();
|
|
137
|
+
// Give stdout time to flush
|
|
138
|
+
setTimeout(() => process.exit(0), 100);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
h2Stream.on("error", () => {
|
|
142
|
+
clearTimeout(timeout);
|
|
143
|
+
client.close();
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Forward stdin → H2 stream (after config message)
|
|
148
|
+
if (unary) {
|
|
149
|
+
// Unary mode: read a single body message, write it, and end the stream.
|
|
150
|
+
const body = await readMessage();
|
|
151
|
+
if (body && body.length > 0 && !h2Stream.closed && !h2Stream.destroyed) {
|
|
152
|
+
h2Stream.end(body);
|
|
153
|
+
} else {
|
|
154
|
+
h2Stream.end();
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Streaming mode: forward all stdin messages as Connect frames.
|
|
158
|
+
(async () => {
|
|
159
|
+
while (true) {
|
|
160
|
+
const msg = await readMessage();
|
|
161
|
+
if (!msg || msg.length === 0) {
|
|
162
|
+
// EOF or zero-length = done writing
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
166
|
+
resetTimeout();
|
|
167
|
+
h2Stream.write(msg);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!h2Stream.closed && !h2Stream.destroyed) {
|
|
172
|
+
h2Stream.end();
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
}
|