@tt-a1i/openpi 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -20
- package/SETUP.md +10 -4
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +65 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +105 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1271 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
- package/extensions/ai-providers/cursor/provider.ts +1431 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +39 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +64 -7
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +130 -48
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +7 -71
- package/extensions/subagents/src/prompt.ts +19 -5
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +147 -21
- package/extensions/workflows/index.ts +75 -20
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +11 -233
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +9 -9
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +5 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import * as http2 from "node:http2";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as tls from "node:tls";
|
|
4
|
+
|
|
5
|
+
export interface CursorHttp2ConnectOptions {
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isLocalOrMetadataHost(hostname: string): boolean {
|
|
11
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
12
|
+
if (
|
|
13
|
+
host === "localhost" ||
|
|
14
|
+
host.endsWith(".localhost") ||
|
|
15
|
+
host === "metadata.google.internal"
|
|
16
|
+
) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (host === "::" || host === "::1" || /^f[cd]/.test(host)) return true;
|
|
20
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\./.exec(host);
|
|
21
|
+
if (!ipv4) return false;
|
|
22
|
+
const first = Number(ipv4[1]);
|
|
23
|
+
const second = Number(ipv4[2]);
|
|
24
|
+
return (
|
|
25
|
+
first === 0 ||
|
|
26
|
+
first === 10 ||
|
|
27
|
+
first === 127 ||
|
|
28
|
+
(first === 169 && second === 254) ||
|
|
29
|
+
(first === 172 && second >= 16 && second <= 31) ||
|
|
30
|
+
(first === 192 && second === 168)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function shouldBypassProxy(target: URL): boolean {
|
|
35
|
+
if (isLocalOrMetadataHost(target.hostname)) return true;
|
|
36
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
|
|
37
|
+
if (!noProxy) return false;
|
|
38
|
+
const targetHost = target.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
39
|
+
const targetPort =
|
|
40
|
+
target.port || (target.protocol === "https:" ? "443" : "80");
|
|
41
|
+
for (const rawRule of noProxy.split(/[,\s]+/)) {
|
|
42
|
+
let rule = rawRule.trim().toLowerCase();
|
|
43
|
+
if (!rule) continue;
|
|
44
|
+
if (rule === "*") return true;
|
|
45
|
+
let rulePort: string | undefined;
|
|
46
|
+
const portMatch = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(rule);
|
|
47
|
+
if (portMatch) {
|
|
48
|
+
rule = portMatch[1]!;
|
|
49
|
+
rulePort = portMatch[2];
|
|
50
|
+
}
|
|
51
|
+
if (rulePort && rulePort !== targetPort) continue;
|
|
52
|
+
rule = rule.replace(/^\[|\]$/g, "").replace(/^\./, "");
|
|
53
|
+
if (targetHost === rule || targetHost.endsWith(`.${rule}`)) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve Cursor's provider override first, then the standard proxy variables. */
|
|
59
|
+
export function resolveCursorProxy(target: URL): string | undefined {
|
|
60
|
+
if (shouldBypassProxy(target)) return undefined;
|
|
61
|
+
const protocolProxy =
|
|
62
|
+
target.protocol === "https:"
|
|
63
|
+
? process.env.HTTPS_PROXY || process.env.https_proxy
|
|
64
|
+
: process.env.HTTP_PROXY || process.env.http_proxy;
|
|
65
|
+
return [
|
|
66
|
+
process.env.PI_PROXY_CURSOR,
|
|
67
|
+
process.env.PI_PROXY,
|
|
68
|
+
protocolProxy,
|
|
69
|
+
process.env.ALL_PROXY || process.env.all_proxy,
|
|
70
|
+
]
|
|
71
|
+
.map((value) => value?.trim())
|
|
72
|
+
.find((value): value is string => Boolean(value));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function connectProxyTunnel(
|
|
76
|
+
proxyUrl: URL,
|
|
77
|
+
targetUrl: URL,
|
|
78
|
+
options: CursorHttp2ConnectOptions,
|
|
79
|
+
): Promise<net.Socket> {
|
|
80
|
+
if (!["http:", "https:"].includes(proxyUrl.protocol)) {
|
|
81
|
+
return Promise.reject(
|
|
82
|
+
new Error(`Unsupported Cursor proxy protocol: ${proxyUrl.protocol}`),
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (options.signal?.aborted) {
|
|
86
|
+
return Promise.reject(new Error("Cursor proxy tunnel aborted"));
|
|
87
|
+
}
|
|
88
|
+
const proxyTls = proxyUrl.protocol === "https:";
|
|
89
|
+
const proxyPort = Number(proxyUrl.port || (proxyTls ? 443 : 80));
|
|
90
|
+
const targetPort = Number(
|
|
91
|
+
targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
92
|
+
);
|
|
93
|
+
const targetAuthority = `${targetUrl.hostname}:${targetPort}`;
|
|
94
|
+
let proxyAuthorization: string | undefined;
|
|
95
|
+
if (proxyUrl.username || proxyUrl.password) {
|
|
96
|
+
try {
|
|
97
|
+
const credentials = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
|
|
98
|
+
proxyAuthorization = Buffer.from(credentials).toString("base64");
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
return Promise.reject(
|
|
101
|
+
new Error("Cursor proxy credentials contain invalid percent-encoding", {
|
|
102
|
+
cause,
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const { promise, resolve, reject } = Promise.withResolvers<net.Socket>();
|
|
108
|
+
let rawSocket: net.Socket | undefined;
|
|
109
|
+
let targetSocket: net.Socket | undefined;
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
let response = Buffer.alloc(0);
|
|
112
|
+
let settled = false;
|
|
113
|
+
|
|
114
|
+
const cleanup = () => {
|
|
115
|
+
if (timer) clearTimeout(timer);
|
|
116
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
117
|
+
rawSocket?.removeListener("error", onError);
|
|
118
|
+
rawSocket?.removeListener(proxyTls ? "secureConnect" : "connect", onReady);
|
|
119
|
+
rawSocket?.removeListener("data", onData);
|
|
120
|
+
targetSocket?.removeListener("error", onError);
|
|
121
|
+
targetSocket?.removeListener("secureConnect", onTargetReady);
|
|
122
|
+
};
|
|
123
|
+
const fail = (error: Error) => {
|
|
124
|
+
if (settled) return;
|
|
125
|
+
settled = true;
|
|
126
|
+
cleanup();
|
|
127
|
+
targetSocket?.destroy();
|
|
128
|
+
rawSocket?.destroy();
|
|
129
|
+
reject(error);
|
|
130
|
+
};
|
|
131
|
+
const succeed = (socket: net.Socket) => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
134
|
+
cleanup();
|
|
135
|
+
resolve(socket);
|
|
136
|
+
};
|
|
137
|
+
const onAbort = () => fail(new Error("Cursor proxy tunnel aborted"));
|
|
138
|
+
const onError = (error: Error) => fail(error);
|
|
139
|
+
const onTargetReady = () => {
|
|
140
|
+
if (targetSocket) succeed(targetSocket);
|
|
141
|
+
};
|
|
142
|
+
const onData = (chunk: Buffer) => {
|
|
143
|
+
if (!rawSocket) return;
|
|
144
|
+
response = Buffer.concat([response, chunk]);
|
|
145
|
+
if (response.length > 64 * 1024) {
|
|
146
|
+
fail(new Error("Cursor proxy response headers exceed 64 KiB"));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const headerEnd = response.indexOf("\r\n\r\n");
|
|
150
|
+
if (headerEnd === -1) return;
|
|
151
|
+
const statusLine = response
|
|
152
|
+
.subarray(0, headerEnd)
|
|
153
|
+
.toString("latin1")
|
|
154
|
+
.split("\r\n")[0];
|
|
155
|
+
if (!/^HTTP\/1\.[01] 200\b/.test(statusLine ?? "")) {
|
|
156
|
+
fail(
|
|
157
|
+
new Error(
|
|
158
|
+
`Cursor proxy tunnel failed: ${statusLine || "invalid response"}`,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
rawSocket.removeListener("data", onData);
|
|
164
|
+
if (targetUrl.protocol !== "https:") {
|
|
165
|
+
succeed(rawSocket);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
targetSocket = tls.connect({
|
|
169
|
+
socket: rawSocket,
|
|
170
|
+
servername: targetUrl.hostname,
|
|
171
|
+
ALPNProtocols: ["h2"],
|
|
172
|
+
});
|
|
173
|
+
targetSocket.once("error", onError);
|
|
174
|
+
targetSocket.once("secureConnect", onTargetReady);
|
|
175
|
+
};
|
|
176
|
+
const onReady = () => {
|
|
177
|
+
if (!rawSocket) return;
|
|
178
|
+
let request = `CONNECT ${targetAuthority} HTTP/1.1\r\nHost: ${targetAuthority}\r\n`;
|
|
179
|
+
if (proxyAuthorization) {
|
|
180
|
+
request += `Proxy-Authorization: Basic ${proxyAuthorization}\r\n`;
|
|
181
|
+
}
|
|
182
|
+
rawSocket.on("data", onData);
|
|
183
|
+
rawSocket.write(`${request}\r\n`);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
187
|
+
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
188
|
+
const timeoutMs = Math.floor(options.timeoutMs);
|
|
189
|
+
timer = setTimeout(
|
|
190
|
+
() =>
|
|
191
|
+
fail(new Error(`Cursor proxy tunnel timed out after ${timeoutMs}ms`)),
|
|
192
|
+
timeoutMs,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
rawSocket = proxyTls
|
|
196
|
+
? tls.connect({ host: proxyUrl.hostname, port: proxyPort })
|
|
197
|
+
: net.connect({ host: proxyUrl.hostname, port: proxyPort });
|
|
198
|
+
rawSocket.once("error", onError);
|
|
199
|
+
rawSocket.once(proxyTls ? "secureConnect" : "connect", onReady);
|
|
200
|
+
return promise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Open Cursor's HTTP/2 session directly or through an HTTP CONNECT proxy. */
|
|
204
|
+
export async function connectCursorHttp2(
|
|
205
|
+
baseUrl: string,
|
|
206
|
+
options: CursorHttp2ConnectOptions = {},
|
|
207
|
+
): Promise<http2.ClientHttp2Session> {
|
|
208
|
+
const target = new URL(baseUrl);
|
|
209
|
+
const proxy = resolveCursorProxy(target);
|
|
210
|
+
if (!proxy) return http2.connect(target);
|
|
211
|
+
const socket = await connectProxyTunnel(new URL(proxy), target, options);
|
|
212
|
+
return http2.connect(target, { createConnection: () => socket });
|
|
213
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Translate Cursor MCP requests into Pi calls; execution stays in Pi's loop. */
|
|
2
|
+
import type { Context, ToolCall } from "@earendil-works/pi-ai/compat";
|
|
3
|
+
import { type McpArgs, McpToolDefinitionSchema } from "./proto.ts";
|
|
4
|
+
import {
|
|
5
|
+
create,
|
|
6
|
+
decodeJsonValue,
|
|
7
|
+
encodeJsonValue,
|
|
8
|
+
type JsonValue,
|
|
9
|
+
} from "./protobuf.ts";
|
|
10
|
+
|
|
11
|
+
export const CURSOR_PI_PROVIDER = "openpi";
|
|
12
|
+
export const CURSOR_PI_TOOLS_SYSTEM_PROMPT =
|
|
13
|
+
"Use only the provided openpi MCP tools. These are the active Pi tools and Pi owns their execution and permissions. Do not use Cursor-native filesystem, shell, editing, web, task, or interaction tools. When tool results appear in conversation history, continue from those results. Do not repeat a completed tool call.";
|
|
14
|
+
|
|
15
|
+
export function buildCursorTools(tools: Context["tools"]) {
|
|
16
|
+
return (tools ?? []).map((tool) => {
|
|
17
|
+
const schema: JsonValue = JSON.parse(JSON.stringify(tool.parameters));
|
|
18
|
+
return create(McpToolDefinitionSchema, {
|
|
19
|
+
name: tool.name,
|
|
20
|
+
providerIdentifier: CURSOR_PI_PROVIDER,
|
|
21
|
+
toolName: tool.name,
|
|
22
|
+
description: tool.description,
|
|
23
|
+
inputSchema: encodeJsonValue(schema),
|
|
24
|
+
inputSchemaJson: JSON.stringify(schema),
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decodeCursorTool(
|
|
30
|
+
args: McpArgs,
|
|
31
|
+
tools: Context["tools"],
|
|
32
|
+
): ToolCall {
|
|
33
|
+
const name = args.toolName || args.name;
|
|
34
|
+
if (
|
|
35
|
+
args.providerIdentifier !== CURSOR_PI_PROVIDER ||
|
|
36
|
+
(args.serverIdentifier && args.serverIdentifier !== CURSOR_PI_PROVIDER) ||
|
|
37
|
+
!name ||
|
|
38
|
+
(args.name && args.name !== name) ||
|
|
39
|
+
!tools?.some((tool) => tool.name === name)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error("Cursor requested an unadvertised Pi tool identity");
|
|
42
|
+
}
|
|
43
|
+
if (!args.toolCallId.trim())
|
|
44
|
+
throw new Error("Cursor MCP tool call has no identity");
|
|
45
|
+
const values: Record<string, unknown> = {};
|
|
46
|
+
for (const [key, value] of Object.entries(args.args)) {
|
|
47
|
+
// google.protobuf.Value, not JSON text. Define own properties so keys such
|
|
48
|
+
// as __proto__ cannot alter the decoded argument object's prototype.
|
|
49
|
+
if (!value.length || ![8, 17, 26, 32, 42, 50].includes(value[0]!)) {
|
|
50
|
+
throw new Error("Cursor MCP argument is not a protobuf JSON value");
|
|
51
|
+
}
|
|
52
|
+
const decoded = decodeJsonValue(value);
|
|
53
|
+
const validateJson = (item: JsonValue): void => {
|
|
54
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
55
|
+
throw new Error("Cursor MCP argument contains a non-finite number");
|
|
56
|
+
if (item && typeof item === "object")
|
|
57
|
+
for (const child of Object.values(item)) validateJson(child);
|
|
58
|
+
};
|
|
59
|
+
validateJson(decoded);
|
|
60
|
+
Object.defineProperty(values, key, {
|
|
61
|
+
value: decoded,
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { type: "toolCall", id: args.toolCallId, name, arguments: values };
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Node >=22 provides Promise.withResolvers; the repo's ES2022 lib needs a shim. */
|
|
2
|
+
declare global {
|
|
3
|
+
interface PromiseConstructor {
|
|
4
|
+
withResolvers<T>(): {
|
|
5
|
+
promise: Promise<T>;
|
|
6
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
7
|
+
reject: (reason?: unknown) => void;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai-providers — OAuth-backed model providers for pi.
|
|
3
|
+
*
|
|
4
|
+
* Adds OAuth-backed Google Antigravity and Cursor model providers. Both are
|
|
5
|
+
* inert until the user logs in and selects one of their models. Cursor uses
|
|
6
|
+
* AgentService/Run with an experimental bridge to normal Pi tool calls.
|
|
7
|
+
* Cursor-native coding tools are not exposed or executed by this extension.
|
|
8
|
+
*
|
|
9
|
+
* Wire protocol: Cloud Code Assist `v1internal:streamGenerateContent` over
|
|
10
|
+
* SSE (see antigravity/provider.ts). Reference implementation: oh-my-pi's
|
|
11
|
+
* google-gemini-cli provider (shared google-gemini-cli/google-antigravity).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { encodeApiKey } from "./antigravity/credentials.ts";
|
|
17
|
+
import { fetchAntigravityModels } from "./antigravity/discovery.ts";
|
|
18
|
+
import {
|
|
19
|
+
ANTIGRAVITY_API_URL,
|
|
20
|
+
ANTIGRAVITY_MODELS,
|
|
21
|
+
} from "./antigravity/models.ts";
|
|
22
|
+
import {
|
|
23
|
+
loginAntigravity,
|
|
24
|
+
refreshAntigravityToken,
|
|
25
|
+
} from "./antigravity/oauth.ts";
|
|
26
|
+
import { streamAntigravity } from "./antigravity/provider.ts";
|
|
27
|
+
import { getCursorApiKey } from "./cursor/credentials.ts";
|
|
28
|
+
import { fetchCursorModels } from "./cursor/discovery.ts";
|
|
29
|
+
import { transformCursorImageInput } from "./cursor/input-images.ts";
|
|
30
|
+
import { CURSOR_MODELS } from "./cursor/models.ts";
|
|
31
|
+
import { loginCursor, refreshCursorToken } from "./cursor/oauth.ts";
|
|
32
|
+
import { streamCursor } from "./cursor/provider.ts";
|
|
33
|
+
import { createOAuthAuth } from "./oauth-adapter.ts";
|
|
34
|
+
|
|
35
|
+
function providerStreams(
|
|
36
|
+
streamSimple: ProviderStreams["streamSimple"],
|
|
37
|
+
): ProviderStreams {
|
|
38
|
+
return {
|
|
39
|
+
stream: (model, context, options) => streamSimple(model, context, options),
|
|
40
|
+
streamSimple,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function authProviders(pi: ExtensionAPI) {
|
|
45
|
+
pi.on("input", transformCursorImageInput);
|
|
46
|
+
|
|
47
|
+
pi.registerProvider(
|
|
48
|
+
createProvider({
|
|
49
|
+
id: "google-antigravity",
|
|
50
|
+
name: "Google Antigravity",
|
|
51
|
+
baseUrl: ANTIGRAVITY_API_URL,
|
|
52
|
+
api: providerStreams(streamAntigravity),
|
|
53
|
+
auth: {
|
|
54
|
+
oauth: createOAuthAuth({
|
|
55
|
+
name: "Google (Antigravity)",
|
|
56
|
+
isSubscription: true,
|
|
57
|
+
login: loginAntigravity,
|
|
58
|
+
refreshToken: refreshAntigravityToken,
|
|
59
|
+
getApiKey: encodeApiKey,
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
models: ANTIGRAVITY_MODELS,
|
|
63
|
+
fetchModels: fetchAntigravityModels,
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
pi.registerProvider(
|
|
68
|
+
createProvider({
|
|
69
|
+
id: "cursor",
|
|
70
|
+
name: "Cursor",
|
|
71
|
+
baseUrl: "https://api2.cursor.sh",
|
|
72
|
+
api: providerStreams(streamCursor),
|
|
73
|
+
auth: {
|
|
74
|
+
oauth: createOAuthAuth({
|
|
75
|
+
name: "Cursor",
|
|
76
|
+
isSubscription: true,
|
|
77
|
+
login: loginCursor,
|
|
78
|
+
refreshToken: refreshCursorToken,
|
|
79
|
+
getApiKey: getCursorApiKey,
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
models: CURSOR_MODELS,
|
|
83
|
+
fetchModels: fetchCursorModels,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ModelAuth,
|
|
3
|
+
OAuthAuth,
|
|
4
|
+
OAuthCredential,
|
|
5
|
+
OAuthCredentials,
|
|
6
|
+
OAuthLoginCallbacks,
|
|
7
|
+
ProviderAuthInteraction,
|
|
8
|
+
} from "@earendil-works/pi-ai";
|
|
9
|
+
|
|
10
|
+
interface LegacyOAuthImplementation {
|
|
11
|
+
name: string;
|
|
12
|
+
isSubscription?: boolean;
|
|
13
|
+
login(callbacks: CancellableOAuthLoginCallbacks): Promise<OAuthCredentials>;
|
|
14
|
+
refreshToken(
|
|
15
|
+
credential: OAuthCredentials,
|
|
16
|
+
signal: AbortSignal,
|
|
17
|
+
): Promise<OAuthCredentials>;
|
|
18
|
+
getApiKey(credential: OAuthCredentials): string | Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CancellableOAuthLoginCallbacks = Omit<
|
|
22
|
+
OAuthLoginCallbacks,
|
|
23
|
+
"onManualCodeInput"
|
|
24
|
+
> & {
|
|
25
|
+
onManualCodeInput?(signal?: AbortSignal): Promise<string>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function legacyCallbacks(
|
|
29
|
+
interaction: ProviderAuthInteraction,
|
|
30
|
+
): CancellableOAuthLoginCallbacks {
|
|
31
|
+
return {
|
|
32
|
+
signal: interaction.signal,
|
|
33
|
+
onAuth: (info) => interaction.notify({ type: "auth_url", ...info }),
|
|
34
|
+
onDeviceCode: (info) =>
|
|
35
|
+
interaction.notify({ type: "device_code", ...info }),
|
|
36
|
+
onProgress: (message) => interaction.notify({ type: "progress", message }),
|
|
37
|
+
onPrompt: (prompt) =>
|
|
38
|
+
interaction.prompt({
|
|
39
|
+
type: "text",
|
|
40
|
+
message: prompt.message,
|
|
41
|
+
placeholder: prompt.placeholder,
|
|
42
|
+
}),
|
|
43
|
+
onManualCodeInput: (signal) =>
|
|
44
|
+
interaction.prompt({
|
|
45
|
+
type: "manual_code",
|
|
46
|
+
message: "Paste the authorization callback URL or code",
|
|
47
|
+
signal,
|
|
48
|
+
}),
|
|
49
|
+
onSelect: (prompt) =>
|
|
50
|
+
interaction.prompt({
|
|
51
|
+
type: "select",
|
|
52
|
+
message: prompt.message,
|
|
53
|
+
options: prompt.options,
|
|
54
|
+
}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function canonicalCredential(credentials: OAuthCredentials): OAuthCredential {
|
|
59
|
+
return { ...credentials, type: "oauth" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Adapt pi's retained extension OAuth callbacks to the native Provider API. */
|
|
63
|
+
export function createOAuthAuth(
|
|
64
|
+
implementation: LegacyOAuthImplementation,
|
|
65
|
+
): OAuthAuth {
|
|
66
|
+
return {
|
|
67
|
+
name: implementation.name,
|
|
68
|
+
isSubscription: implementation.isSubscription,
|
|
69
|
+
login: async (interaction) =>
|
|
70
|
+
canonicalCredential(
|
|
71
|
+
await implementation.login(legacyCallbacks(interaction)),
|
|
72
|
+
),
|
|
73
|
+
refresh: async (credential, signal) =>
|
|
74
|
+
canonicalCredential(
|
|
75
|
+
await implementation.refreshToken(credential, signal),
|
|
76
|
+
),
|
|
77
|
+
toAuth: async (credential): Promise<ModelAuth> => ({
|
|
78
|
+
apiKey: await implementation.getApiKey(credential),
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
OPENPI_TOOL_SURFACE,
|
|
33
33
|
patchOwnedTools,
|
|
34
34
|
} from "../shared/tool-surface.ts";
|
|
35
|
+
import { completionOwnerFor } from "../shared/completion-inbox.ts";
|
|
35
36
|
import {
|
|
36
37
|
projectBackgroundTerminalCapability,
|
|
37
38
|
registerWebCapability,
|
|
@@ -105,7 +106,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
105
106
|
let ui: ExtensionUIContext | undefined;
|
|
106
107
|
let unsubStatus: (() => void) | undefined;
|
|
107
108
|
let startReservations = 0;
|
|
108
|
-
const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>(
|
|
109
|
+
const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>({
|
|
110
|
+
owner: () =>
|
|
111
|
+
sessionContext
|
|
112
|
+
? completionOwnerFor(sessionContext.sessionManager)
|
|
113
|
+
: undefined,
|
|
114
|
+
});
|
|
109
115
|
const hideLifecycleTools = () =>
|
|
110
116
|
patchOwnedTools(pi, "background", {
|
|
111
117
|
disable: OPENPI_TOOL_SURFACE.background.deferred,
|
|
@@ -245,6 +251,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
245
251
|
const flushResults = (wake: boolean) => {
|
|
246
252
|
const snaps = resultDelivery.drain(MAX_RUNNING);
|
|
247
253
|
if (!deliverResults(snaps, wake)) resultDelivery.restore(snaps);
|
|
254
|
+
else resultDelivery.acknowledge(snaps);
|
|
248
255
|
};
|
|
249
256
|
|
|
250
257
|
const idleResultBatcher = createIdleResultBatcher({
|
|
@@ -395,11 +395,9 @@ export async function signalWindowsProcessTree(
|
|
|
395
395
|
: attempt.outcome === "timed_out"
|
|
396
396
|
? `taskkill timed out after ${attempt.timeoutMs}ms; helper ${attempt.helperClosed ? "closed after SIGKILL" : `did not close within an additional ${attempt.helperCloseTimeoutMs}ms`}`
|
|
397
397
|
: `taskkill exited ${attempt.exitCode ?? "without a code"}${attempt.signal ? ` (${attempt.signal})` : ""}`;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
!targetExited()
|
|
402
|
-
) {
|
|
398
|
+
// Closing the helper or observing the shell exit does not prove that all
|
|
399
|
+
// descendants exited. Preserve uncertainty instead of killing only the shell.
|
|
400
|
+
if (attempt.outcome === "timed_out") {
|
|
403
401
|
return { outcome: "unresolved", detail };
|
|
404
402
|
}
|
|
405
403
|
// A failed graceful taskkill must leave the shell PID alive for the
|
|
@@ -1,44 +1,64 @@
|
|
|
1
1
|
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
import {
|
|
3
|
+
type CompletionOwner,
|
|
4
|
+
createCompletionInbox,
|
|
5
|
+
} from "../../shared/completion-inbox.ts";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
|
-
* Deferred one-shot delivery
|
|
5
|
-
* terminal's result is held
|
|
6
|
-
* message or consumed by a tool call (bg_kill /
|
|
7
|
-
* returned the settlement itself.
|
|
8
|
-
* structurally impossible — whoever
|
|
8
|
+
* Deferred one-shot delivery adapter (same semantics as subagents'): a
|
|
9
|
+
* settled terminal's result is held in the shared inbox until it is either
|
|
10
|
+
* drained into a follow-up message or consumed by a tool call (bg_kill /
|
|
11
|
+
* bg_status) that already returned the settlement itself. Stable ids make
|
|
12
|
+
* double delivery structurally impossible — whoever claims first wins.
|
|
9
13
|
*/
|
|
10
|
-
export function createDeferredResultDelivery<T extends { id: string }>(
|
|
11
|
-
|
|
14
|
+
export function createDeferredResultDelivery<T extends { id: string }>(
|
|
15
|
+
options: { readonly owner?: () => CompletionOwner | undefined } = {},
|
|
16
|
+
) {
|
|
17
|
+
const inbox = createCompletionInbox<T>();
|
|
18
|
+
const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
|
|
12
19
|
|
|
13
20
|
const queue = {
|
|
14
21
|
defer(result: T) {
|
|
15
|
-
|
|
16
|
-
|
|
22
|
+
const currentOwner = owner();
|
|
23
|
+
inbox.defer(
|
|
24
|
+
{
|
|
25
|
+
deliveryId: `background:${result.id}`,
|
|
26
|
+
owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
|
|
27
|
+
producer: "background",
|
|
28
|
+
producerId: result.id,
|
|
29
|
+
terminalRef: { kind: "terminal-snapshot", id: result.id },
|
|
30
|
+
wake: "producer-policy",
|
|
31
|
+
payload: result,
|
|
32
|
+
},
|
|
33
|
+
currentOwner,
|
|
34
|
+
);
|
|
35
|
+
return inbox.size();
|
|
17
36
|
},
|
|
18
37
|
consume(ids: Iterable<string>) {
|
|
19
|
-
|
|
38
|
+
inbox.consume("background", ids);
|
|
20
39
|
},
|
|
21
40
|
drain(maxResults = Number.POSITIVE_INFINITY) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
results.push(result);
|
|
26
|
-
pending.delete(id);
|
|
27
|
-
}
|
|
28
|
-
return results;
|
|
41
|
+
return inbox
|
|
42
|
+
.claim(owner(), maxResults)
|
|
43
|
+
.map((envelope) => envelope.payload);
|
|
29
44
|
},
|
|
30
45
|
restore(results: readonly T[]) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
inbox.retryClaimed(
|
|
47
|
+
"background",
|
|
48
|
+
results.map((result) => result.id),
|
|
49
|
+
owner(),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
acknowledge(results: readonly T[]) {
|
|
53
|
+
inbox.acknowledge(results.map((result) => `background:${result.id}`));
|
|
35
54
|
},
|
|
36
55
|
size() {
|
|
37
|
-
return
|
|
56
|
+
return inbox.size();
|
|
38
57
|
},
|
|
39
58
|
clear() {
|
|
40
|
-
|
|
59
|
+
inbox.clear();
|
|
41
60
|
},
|
|
61
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
42
62
|
};
|
|
43
63
|
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
44
64
|
}
|