@benjamolina/pi-antigravity-guard 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.
package/dist/sse.js ADDED
@@ -0,0 +1,110 @@
1
+ const MAX_RECORD_BYTES = 1024 * 1024;
2
+ export class SseFrameError extends Error {
3
+ }
4
+ export class SseFramer {
5
+ decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
6
+ data = [];
7
+ line = [];
8
+ recordBytes = 0;
9
+ carriageReturn = false;
10
+ bom = true;
11
+ push(bytes) {
12
+ const records = [];
13
+ let start = 0;
14
+ for (let index = 0; index < bytes.length; index++) {
15
+ if (this.carriageReturn) {
16
+ if (bytes[index] === 10) {
17
+ this.count(1);
18
+ this.endLine(records);
19
+ this.carriageReturn = false;
20
+ start = index + 1;
21
+ continue;
22
+ }
23
+ this.endLine(records);
24
+ this.carriageReturn = false;
25
+ }
26
+ const byte = bytes[index];
27
+ if (byte === 10 || byte === 13) {
28
+ this.append(bytes.subarray(start, index));
29
+ this.count(1);
30
+ if (byte === 13)
31
+ this.carriageReturn = true;
32
+ else
33
+ this.endLine(records);
34
+ start = index + 1;
35
+ }
36
+ }
37
+ this.append(bytes.subarray(start));
38
+ return records;
39
+ }
40
+ finish() {
41
+ const records = [];
42
+ if (this.carriageReturn) {
43
+ this.endLine(records);
44
+ this.carriageReturn = false;
45
+ }
46
+ this.flush();
47
+ if (this.line.length || this.data.length || this.recordBytes)
48
+ throw new SseFrameError("SSE stream ended with an unterminated record.");
49
+ return records;
50
+ }
51
+ append(bytes) {
52
+ if (!bytes.length)
53
+ return;
54
+ this.count(bytes.length);
55
+ try {
56
+ let text = this.decoder.decode(bytes, { stream: true });
57
+ if (this.bom && text) {
58
+ this.bom = false;
59
+ if (text.startsWith("\ufeff"))
60
+ text = text.slice(1);
61
+ }
62
+ if (text)
63
+ this.line.push(text);
64
+ }
65
+ catch {
66
+ throw new SseFrameError("SSE stream contains invalid UTF-8.");
67
+ }
68
+ }
69
+ flush() {
70
+ try {
71
+ const text = this.decoder.decode();
72
+ if (this.bom) {
73
+ this.bom = false;
74
+ if (text.startsWith("\ufeff"))
75
+ this.line.push(text.slice(1));
76
+ }
77
+ else if (text)
78
+ this.line.push(text);
79
+ }
80
+ catch {
81
+ throw new SseFrameError("SSE stream contains invalid UTF-8.");
82
+ }
83
+ }
84
+ count(bytes) {
85
+ this.recordBytes += bytes;
86
+ if (this.recordBytes > MAX_RECORD_BYTES)
87
+ throw new SseFrameError("SSE record is too large.");
88
+ }
89
+ endLine(records) {
90
+ this.flush();
91
+ const line = this.line.join("");
92
+ if (!line) {
93
+ if (this.data.length)
94
+ records.push(this.data.join("\n"));
95
+ this.data = [];
96
+ this.recordBytes = 0;
97
+ }
98
+ else if (!line.startsWith(":")) {
99
+ const separator = line.indexOf(":");
100
+ const field = separator === -1 ? line : line.slice(0, separator);
101
+ let value = separator === -1 ? "" : line.slice(separator + 1);
102
+ if (value.startsWith(" "))
103
+ value = value.slice(1);
104
+ if (field === "data")
105
+ this.data.push(value);
106
+ }
107
+ this.line = [];
108
+ this.decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
109
+ }
110
+ }
@@ -0,0 +1,36 @@
1
+ import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
2
+ import type { ResponseSemantic } from "./response.ts";
3
+ type StreamErrorKind = "aborted" | "access" | "model" | "quota" | "response" | "transport" | "callback";
4
+ export declare class StreamTransportError extends Error {
5
+ readonly kind: StreamErrorKind;
6
+ readonly status?: number | undefined;
7
+ constructor(kind: StreamErrorKind, message: string, status?: number | undefined);
8
+ }
9
+ export interface StreamTransportInput {
10
+ accessToken: string;
11
+ context: Context;
12
+ fetch: typeof globalThis.fetch;
13
+ generationOptions?: SimpleStreamOptions;
14
+ headers?: Record<string, string>;
15
+ inactivityTimeoutMs?: number;
16
+ model: Model<string>;
17
+ projectId: string;
18
+ now: () => number;
19
+ onSemantic: (semantic: ResponseSemantic) => void | Promise<void>;
20
+ platform: string;
21
+ requestId: string;
22
+ signal?: AbortSignal;
23
+ timeoutMs?: number;
24
+ }
25
+ export interface PiStreamLifecycleInput {
26
+ model: Model<string>;
27
+ now: () => number;
28
+ signal?: AbortSignal;
29
+ runTransport: (input: {
30
+ onSemantic: (semantic: ResponseSemantic) => void;
31
+ signal: AbortSignal;
32
+ }) => Promise<void>;
33
+ }
34
+ export declare function createPiLifecycleStream(input: PiStreamLifecycleInput): import("@earendil-works/pi-ai").AssistantMessageEventStream;
35
+ export declare function executeStreamTransport(input: StreamTransportInput): Promise<void>;
36
+ export {};
package/dist/stream.js ADDED
@@ -0,0 +1,269 @@
1
+ import { calculateCost, createAssistantMessageEventStream } from "@earendil-works/pi-ai";
2
+ import { ANTIGRAVITY_ENDPOINTS } from "@benjamolina/antigravity-guard-core";
3
+ import { serializeTextContext } from "./context.js";
4
+ import { ResponseSemanticError, ResponseSemantics } from "./response.js";
5
+ import { SseFrameError, SseFramer } from "./sse.js";
6
+ const TOTAL_TIMEOUT_MS = 120_000;
7
+ const INACTIVITY_TIMEOUT_MS = 30_000;
8
+ const MAX_ERROR_BYTES = 64 * 1024;
9
+ const ANTIGRAVITY_USER_AGENT = "antigravity/cli/1.1.23 (aidev_client; os_type=linux; arch=amd64; cl=974125021; auth_method=consumer)";
10
+ const ENDPOINT = `${ANTIGRAVITY_ENDPOINTS.daily}/v1internal:streamGenerateContent?alt=sse`;
11
+ const API = "antigravity-guard-sse";
12
+ const MODEL = "antigravity-gemini-3.8-flash";
13
+ const PROTECTED_HEADERS = new Set(["authorization", "host", "content-type", "content-length"]);
14
+ const LOCAL_ERRORS = new WeakSet();
15
+ export class StreamTransportError extends Error {
16
+ kind;
17
+ status;
18
+ constructor(kind, message, status) {
19
+ super(message);
20
+ this.kind = kind;
21
+ this.status = status;
22
+ }
23
+ }
24
+ export function createPiLifecycleStream(input) {
25
+ const stream = createAssistantMessageEventStream();
26
+ const controller = new AbortController();
27
+ const signal = input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal;
28
+ const output = {
29
+ role: "assistant",
30
+ content: [],
31
+ api: input.model.api,
32
+ provider: input.model.provider,
33
+ model: input.model.id,
34
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
35
+ stopReason: "pending",
36
+ timestamp: input.now(),
37
+ };
38
+ let complete = false;
39
+ let textStarted = false;
40
+ let removeAbort = () => { };
41
+ const finalize = (reason, errorMessage) => {
42
+ if (complete)
43
+ return;
44
+ complete = true;
45
+ removeAbort();
46
+ if (reason === "error" || reason === "aborted")
47
+ controller.abort();
48
+ output.stopReason = reason;
49
+ if (reason === "stop" || reason === "length") {
50
+ if (textStarted)
51
+ stream.push({ type: "text_end", contentIndex: 0, content: output.content[0]?.type === "text" ? output.content[0].text : "", partial: output });
52
+ stream.push({ type: "done", reason, message: output });
53
+ }
54
+ else {
55
+ output.errorMessage = errorMessage ?? "Antigravity generation failed.";
56
+ stream.push({ type: "error", reason, error: output });
57
+ }
58
+ stream.end(output);
59
+ };
60
+ const onSemantic = (semantic) => {
61
+ if (complete)
62
+ return;
63
+ if (semantic.type === "text" && semantic.text) {
64
+ if (!textStarted) {
65
+ textStarted = true;
66
+ output.content.push({ type: "text", text: "" });
67
+ stream.push({ type: "text_start", contentIndex: 0, partial: output });
68
+ }
69
+ const text = output.content[0];
70
+ if (text?.type === "text")
71
+ text.text += semantic.text;
72
+ stream.push({ type: "text_delta", contentIndex: 0, delta: semantic.text, partial: output });
73
+ }
74
+ else if (semantic.type === "usage") {
75
+ output.usage.input = semantic.input;
76
+ output.usage.output = semantic.output;
77
+ output.usage.cacheRead = semantic.cacheRead;
78
+ output.usage.cacheWrite = semantic.cacheWrite;
79
+ output.usage.totalTokens = semantic.input + semantic.output + semantic.cacheRead + semantic.cacheWrite;
80
+ output.usage.cost = calculateCost(input.model, output.usage);
81
+ }
82
+ else if (semantic.type === "finish")
83
+ finalize(semantic.reason);
84
+ };
85
+ stream.push({ type: "start", partial: output });
86
+ if (input.signal) {
87
+ const abort = () => finalize("aborted");
88
+ input.signal.addEventListener("abort", abort, { once: true });
89
+ removeAbort = () => input.signal?.removeEventListener("abort", abort);
90
+ if (input.signal.aborted)
91
+ abort();
92
+ }
93
+ if (!complete)
94
+ void Promise.resolve().then(() => input.runTransport({ onSemantic, signal })).then(() => { if (!complete)
95
+ finalize(signal.aborted ? "aborted" : "error"); }, (error) => finalize(signal.aborted || input.signal?.aborted ? "aborted" : "error", isLocalStreamError(error) ? error.message : undefined));
96
+ return stream;
97
+ }
98
+ export async function executeStreamTransport(input) {
99
+ validateInput(input);
100
+ const signal = totalSignal(input);
101
+ const headers = requestHeaders(input);
102
+ let responseBody = null;
103
+ try {
104
+ const original = serializeTextContext({ context: input.context, model: input.model, options: input.generationOptions, project: input.projectId, requestId: input.requestId });
105
+ const payload = await payloadHook(input, original, signal);
106
+ const response = await abortable(input.fetch(ENDPOINT, { method: "POST", redirect: "error", headers, body: JSON.stringify(payload), signal }), signal);
107
+ await responseHook(input, response, signal);
108
+ responseBody = response.body;
109
+ if (!response.ok)
110
+ throw await httpError(response, signal);
111
+ if (!response.body || !isSse(response.headers.get("content-type"))) {
112
+ await boundedBody(response.body, signal);
113
+ throw streamError("response", "Antigravity did not return an SSE response.");
114
+ }
115
+ await consume(response.body, signal, input.onSemantic, inactivityTimeout(input));
116
+ }
117
+ catch (error) {
118
+ void responseBody?.cancel().catch(() => undefined);
119
+ if (isLocalStreamError(error))
120
+ throw error;
121
+ if (signal.aborted)
122
+ throw streamError("aborted", "Generation was cancelled.");
123
+ if (error instanceof SseFrameError || error instanceof ResponseSemanticError)
124
+ throw streamError("response", "Antigravity returned an invalid stream.");
125
+ throw streamError("transport", "Antigravity generation request failed.");
126
+ }
127
+ }
128
+ function validateInput(input) {
129
+ if (input.model.id !== MODEL || input.model.api !== API)
130
+ throw streamError("response", "The selected Antigravity model or API is unsupported.");
131
+ }
132
+ async function payloadHook(input, payload, signal) {
133
+ try {
134
+ const replacement = await abortable(Promise.resolve().then(() => input.generationOptions?.onPayload?.(payload, input.model)), signal);
135
+ if (replacement !== undefined && JSON.stringify(replacement) !== JSON.stringify(payload))
136
+ throw streamError("callback", "Payload replacement cannot alter the fixed request.");
137
+ return replacement ?? payload;
138
+ }
139
+ catch (error) {
140
+ if (isLocalStreamError(error))
141
+ throw error;
142
+ throw streamError("callback", "Payload handling failed.");
143
+ }
144
+ }
145
+ async function responseHook(input, response, signal) {
146
+ const headers = {};
147
+ response.headers.forEach((value, key) => { headers[key] = value; });
148
+ try {
149
+ await abortable(Promise.resolve().then(() => input.generationOptions?.onResponse?.({ status: response.status, headers }, input.model)), signal);
150
+ }
151
+ catch (error) {
152
+ if (isLocalStreamError(error))
153
+ throw error;
154
+ throw streamError("callback", "Response handling failed.");
155
+ }
156
+ }
157
+ function requestHeaders(input) {
158
+ for (const name of Object.keys(input.headers ?? {}))
159
+ if (PROTECTED_HEADERS.has(name.toLowerCase()))
160
+ throw streamError("response", "Custom headers cannot replace protected request headers.");
161
+ return {
162
+ ...input.headers,
163
+ Authorization: `Bearer ${input.accessToken}`,
164
+ Accept: "text/event-stream",
165
+ "Content-Type": "application/json",
166
+ "User-Agent": ANTIGRAVITY_USER_AGENT,
167
+ };
168
+ }
169
+ async function consume(body, signal, onSemantic, inactivityMs) {
170
+ const reader = body.getReader();
171
+ const framer = new SseFramer();
172
+ const semantics = new ResponseSemantics();
173
+ try {
174
+ while (true) {
175
+ const next = await abortable(reader.read(), AbortSignal.any([signal, AbortSignal.timeout(inactivityMs)]));
176
+ if (next.done)
177
+ break;
178
+ for (const record of framer.push(next.value))
179
+ for (const semantic of semantics.push(record))
180
+ await deliver(onSemantic, semantic);
181
+ }
182
+ for (const record of framer.finish())
183
+ for (const semantic of semantics.push(record))
184
+ await deliver(onSemantic, semantic);
185
+ semantics.finish();
186
+ }
187
+ catch (error) {
188
+ void reader.cancel().catch(() => undefined);
189
+ throw error;
190
+ }
191
+ finally {
192
+ reader.releaseLock();
193
+ }
194
+ }
195
+ async function deliver(callback, semantic) {
196
+ try {
197
+ await callback(semantic);
198
+ }
199
+ catch {
200
+ throw streamError("callback", "Semantic delivery failed.");
201
+ }
202
+ }
203
+ async function httpError(response, signal) {
204
+ const body = await boundedBody(response.body, signal);
205
+ if (response.status === 401)
206
+ return streamError("access", "Authentication expired. Run /login antigravity-guard.", 401);
207
+ if (response.status === 403)
208
+ return streamError("access", "Antigravity access was denied. Check your entitlement.", 403);
209
+ if (response.status === 404)
210
+ return streamError("model", "The requested Antigravity model is unavailable.", 404);
211
+ if (response.status === 429 || body.includes("RESOURCE_EXHAUSTED"))
212
+ return streamError("quota", "Antigravity quota or rate limit was reached.", response.status);
213
+ return streamError("response", "Antigravity generation request was rejected.", response.status);
214
+ }
215
+ async function boundedBody(body, signal) {
216
+ if (!body)
217
+ return "";
218
+ const reader = body.getReader();
219
+ const chunks = [];
220
+ let size = 0;
221
+ try {
222
+ while (true) {
223
+ const next = await abortable(reader.read(), signal);
224
+ if (next.done || size + next.value.byteLength > MAX_ERROR_BYTES)
225
+ break;
226
+ chunks.push(next.value);
227
+ size += next.value.byteLength;
228
+ }
229
+ const output = new Uint8Array(size);
230
+ let offset = 0;
231
+ for (const chunk of chunks) {
232
+ output.set(chunk, offset);
233
+ offset += chunk.byteLength;
234
+ }
235
+ return new TextDecoder().decode(output);
236
+ }
237
+ finally {
238
+ void reader.cancel().catch(() => undefined);
239
+ reader.releaseLock();
240
+ }
241
+ }
242
+ function totalSignal(input) {
243
+ const timeout = remaining(input, TOTAL_TIMEOUT_MS);
244
+ if (timeout <= 0 || input.signal?.aborted)
245
+ throw streamError("aborted", "Generation was cancelled.");
246
+ return input.signal ? AbortSignal.any([input.signal, AbortSignal.timeout(timeout)]) : AbortSignal.timeout(timeout);
247
+ }
248
+ function remaining(input, fallback) {
249
+ return input.timeoutMs === undefined ? fallback : input.timeoutMs > 0 && Number.isFinite(input.timeoutMs) ? Math.min(input.timeoutMs, fallback) : 0;
250
+ }
251
+ function isSse(value) { return value?.split(";", 1)[0]?.trim().toLowerCase() === "text/event-stream"; }
252
+ function inactivityTimeout(input) {
253
+ return input.inactivityTimeoutMs === undefined ? INACTIVITY_TIMEOUT_MS : input.inactivityTimeoutMs > 0 && Number.isFinite(input.inactivityTimeoutMs) ? Math.min(input.inactivityTimeoutMs, INACTIVITY_TIMEOUT_MS) : 0;
254
+ }
255
+ function streamError(kind, message, status) {
256
+ const error = new StreamTransportError(kind, message, status);
257
+ LOCAL_ERRORS.add(error);
258
+ return error;
259
+ }
260
+ function isLocalStreamError(error) { return error instanceof StreamTransportError && LOCAL_ERRORS.has(error); }
261
+ function abortable(promise, signal) {
262
+ if (signal.aborted)
263
+ return Promise.reject(streamError("aborted", "Generation was cancelled."));
264
+ return new Promise((resolve, reject) => {
265
+ const abort = () => reject(streamError("aborted", "Generation was cancelled."));
266
+ signal.addEventListener("abort", abort, { once: true });
267
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
268
+ });
269
+ }
@@ -0,0 +1,13 @@
1
+ export interface PiCredentials {
2
+ refresh: string;
3
+ access: string;
4
+ expires: number;
5
+ projectId?: string;
6
+ email?: string;
7
+ }
8
+ export interface AuthHttpDependencies {
9
+ fetch: typeof globalThis.fetch;
10
+ now: () => number;
11
+ signal?: AbortSignal;
12
+ deadlineMs?: number;
13
+ }
package/dist/types.js ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@benjamolina/pi-antigravity-guard",
3
+ "version": "0.1.0",
4
+ "description": "Pi adapter for Antigravity Guard",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/BenjaMolina/opencode-antigravity-guard.git",
10
+ "directory": "packages/pi"
11
+ },
12
+ "keywords": [
13
+ "pi-package",
14
+ "pi",
15
+ "antigravity"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.19.0"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "!dist/**/*.test.*",
23
+ "LICENSE",
24
+ "README.md"
25
+ ],
26
+ "pi": {
27
+ "extensions": ["./dist/extension.js"]
28
+ },
29
+ "dependencies": {
30
+ "@benjamolina/antigravity-guard-core": "0.1.0"
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-ai": "*",
34
+ "@earendil-works/pi-coding-agent": "*"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "prepack": "npm run build"
40
+ },
41
+ "devDependencies": {
42
+ "@modelcontextprotocol/sdk": "1.30.0"
43
+ }
44
+ }