@simonepri/refined-antigravity-acp 0.1.2 → 0.2.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.
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
2
+ <path d="M2.1 19.4C6.3 15.3 5.4 2 12 2s5.7 13.3 9.9 17.4c1.5 1.5-.3 2.2-1.5 1.3C16 17.6 16.3 12.8 12 12.8s-4 4.8-8.4 7.9c-1.2.9-3 .2-1.5-1.3Z"/>
3
+ </svg>
@@ -0,0 +1,62 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 640 170" width="640" height="170">
2
+ <defs>
3
+ <!-- Antigravity-style gradient: a warm horizontal sweep confined to the top of the mark,
4
+ faded over a blue body (proportions and stops sampled from the official Antigravity logo).
5
+ Coordinates are in the ACP path box (0..576 x 0..220). -->
6
+
7
+ <!-- Blue body: saturated core held through the legs, lightening into the feet
8
+ (reference #3A8CF2 / #4589F8 core, #5395FC bars, #96C1EE feet). -->
9
+ <linearGradient id="agyBody" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="220">
10
+ <stop offset="0" stop-color="#3A8CF2"/>
11
+ <stop offset="0.62" stop-color="#3E8DF5"/>
12
+ <stop offset="0.74" stop-color="#4589F8"/>
13
+ <stop offset="0.80" stop-color="#4E98F3"/>
14
+ <stop offset="0.90" stop-color="#74ADF3"/>
15
+ <stop offset="1" stop-color="#96C1EE"/>
16
+ </linearGradient>
17
+
18
+ <!-- Warm band: horizontal left->right sweep, green -> lime -> tan -> orange -> coral,
19
+ stops sampled from the reference top rows (y=170 / y=210). -->
20
+ <linearGradient id="agyWarm" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="576" y2="0">
21
+ <stop offset="0" stop-color="#8FC466"/>
22
+ <stop offset="0.25" stop-color="#A9BC55"/>
23
+ <stop offset="0.5" stop-color="#C5A65C"/>
24
+ <stop offset="0.7" stop-color="#DC8A5F"/>
25
+ <stop offset="0.82" stop-color="#EA705B"/>
26
+ <stop offset="1" stop-color="#E65E5E"/>
27
+ </linearGradient>
28
+
29
+ <!-- Vertical fade for the warm band: fully warm until y=55 (top 25%), eased out by y=108 (49%),
30
+ at the A/C valley (y=106). Plain alpha fade over blue yields the teal (left) and
31
+ lavender (right) midtones of the reference. -->
32
+ <linearGradient id="agyFade" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="0" y2="220">
33
+ <stop offset="0" stop-color="#FFFFFF"/>
34
+ <stop offset="0.25" stop-color="#FFFFFF"/>
35
+ <stop offset="0.33" stop-color="#B3B3B3"/>
36
+ <stop offset="0.41" stop-color="#4D4D4D"/>
37
+ <stop offset="0.49" stop-color="#000000"/>
38
+ <stop offset="1" stop-color="#000000"/>
39
+ </linearGradient>
40
+ <mask id="agyWarmMask" maskUnits="userSpaceOnUse" x="0" y="0" width="576" height="220">
41
+ <rect x="0" y="0" width="576" height="220" fill="url(#agyFade)"/>
42
+ </mask>
43
+
44
+ <!-- ACP logomark outline, drawn twice: blue body, then the masked warm band on top. -->
45
+ <path id="agyMark" d="M568.003 115.821L517.278 27.9661C507.183 10.4816 489.084 0.0227367 468.894 0.0227367C448.727 0.0227367 430.674 10.4361 420.556 27.8752L343.251 161.749H242.755C236.23 161.749 230.386 158.384 227.135 152.745C223.861 147.106 223.861 140.376 227.135 134.715L277.861 46.8603C281.112 41.2216 286.955 37.8338 293.481 37.8338C300.006 37.8338 305.827 41.1988 309.101 46.8603L312.125 52.0897C313.353 54.2042 315.604 55.5002 318.036 55.5002C320.469 55.5002 322.743 54.1815 323.948 52.067L337.385 28.5118C338.795 26.0335 338.522 22.9413 336.703 20.7586C325.699 7.57131 309.874 0 293.322 0C292.662 0 292.003 0 291.321 0.0454733C272.04 0.75031 254.76 11.1864 245.074 27.9434L200.215 105.657L155.81 29.1484C145.465 11.2092 126.594 0.0227367 106.608 0.0227367C105.949 0.0227367 105.289 0.0227367 104.607 0.06821C85.3265 0.773047 68.0467 11.2092 58.3608 27.9661L7.65806 115.821C-6.25678 139.899 -0.868187 168.82 21.05 187.759C29.8945 195.422 41.6039 199.628 54.0181 199.628H148.648C151.081 199.628 153.332 198.332 154.56 196.217L168.52 172.026C169.748 169.911 169.748 167.319 168.52 165.205C167.292 163.09 165.041 161.794 162.608 161.794H56.0417C49.5163 161.794 43.6729 158.429 40.4216 152.79C37.1475 147.152 37.1475 140.422 40.4216 134.76L91.1471 46.9057C94.3985 41.2671 100.242 37.8793 106.767 37.8793C113.293 37.8793 119.113 41.2443 122.387 46.9057L194.826 172.526C195.031 172.89 195.258 173.208 195.531 173.526C198.76 178.665 202.83 183.485 207.786 187.782C216.631 195.444 228.34 199.651 240.754 199.651H321.424L315.581 209.769C314.353 211.883 314.353 214.475 315.581 216.59C316.809 218.704 319.059 220 321.492 220H349.436C351.868 220 354.119 218.704 355.347 216.59L364.396 200.901L367.17 196.468C367.17 196.468 367.261 196.331 367.284 196.263L453.274 46.9285C456.525 41.2898 462.369 37.902 468.894 37.902C475.42 37.902 481.263 41.2671 484.514 46.9285L535.24 134.783C538.491 140.422 538.514 147.174 535.24 152.813C531.988 158.452 526.145 161.84 519.62 161.84H418.669C416.236 161.84 413.985 163.136 412.757 165.25L398.774 189.442C397.546 191.556 397.546 194.148 398.774 196.263C400.002 198.377 402.253 199.673 404.686 199.673H518.21C539.81 199.673 559.295 188.237 569.026 169.843C578.053 152.79 577.666 132.623 567.981 115.843L568.003 115.821Z"/>
46
+ </defs>
47
+
48
+ <!-- Clean dark card -->
49
+ <rect width="640" height="170" rx="16" fill="#0D1117"/>
50
+ <rect x="0.5" y="0.5" width="639" height="169" rx="15.5" fill="none" stroke="#21262D" stroke-width="1"/>
51
+
52
+ <!-- Scaled up ACP Logomark (scale 0.35 vs 0.24 previously) -->
53
+ <g transform="translate(320, 58)">
54
+ <g transform="translate(-100, -38) scale(0.35)">
55
+ <use xlink:href="#agyMark" href="#agyMark" fill="url(#agyBody)"/>
56
+ <use xlink:href="#agyMark" href="#agyMark" fill="url(#agyWarm)" mask="url(#agyWarmMask)"/>
57
+ </g>
58
+ </g>
59
+
60
+ <!-- Typography -->
61
+ <text x="320" y="132" text-anchor="middle" fill="#F0F6FC" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif" font-size="16" font-weight="700" letter-spacing="2.5">REFINED ANTIGRAVITY ACP</text>
62
+ </svg>
@@ -0,0 +1,9 @@
1
+ import type { PluginServerContext } from "@getpaseo/plugin/server";
2
+ import { createAntigravityProvider } from "./server/provider.js";
3
+
4
+ export { createAntigravityProvider } from "./server/provider.js";
5
+
6
+ export default function contribute(server: PluginServerContext) {
7
+ server.registerProvider(createAntigravityProvider());
8
+ return () => {};
9
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "refined-antigravity-acp",
3
+ "requirements": {
4
+ "paseo": "^0.9.0"
5
+ },
6
+ "description": "Hardened wrapper for Google Antigravity ACP with bugfixes, hang mitigation, and steering."
7
+ }
@@ -0,0 +1,445 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import zlib from "node:zlib";
6
+ import { execFileSync } from "node:child_process";
7
+
8
+ export const EVENT_PROCESSOR_ENTRY =
9
+ "google3/third_party/py/google/antigravity/connections/local/event_processor.py";
10
+
11
+ export const PYCACHE_PREFIX =
12
+ "google3/third_party/py/google/antigravity/connections/local/__pycache__/event_processor.";
13
+
14
+ export const ZIP_EOCD_MAGIC = 0x06054b50;
15
+ export const ZIP_CD_MAGIC = 0x02014b50;
16
+ export const ZIP_LOCAL_MAGIC = 0x04034b50;
17
+
18
+ export const ORIGINAL_ANCHOR = ` elif (
19
+ tsu.state
20
+ == localharness_pb2.TrajectoryStateUpdate.State.STATE_FULLY_IDLE
21
+ ):`;
22
+
23
+ export const REPLACEMENT_CODE = ` elif tsu.state in (
24
+ localharness_pb2.TrajectoryStateUpdate.State.STATE_FULLY_IDLE,
25
+ localharness_pb2.TrajectoryStateUpdate.State.STATE_WAITING_FOR_TASKS,
26
+ ):`;
27
+
28
+ export const PROMPT_ANCHOR = `elif (
29
+ tsu.state
30
+ == localharness_pb2.TrajectoryStateUpdate.State.STATE_FULLY_IDLE):`;
31
+
32
+ export const PROMPT_REPLACEMENT = ` elif tsu.state in (
33
+ localharness_pb2.TrajectoryStateUpdate.State.STATE_FULLY_IDLE,
34
+ localharness_pb2.TrajectoryStateUpdate.State.STATE_WAITING_FOR_TASKS,
35
+ ):`;
36
+
37
+ export type PatchClassification = "patchable" | "already-fixed" | "unrecognized" | "entry-missing";
38
+
39
+ export interface ZipEntryInfo {
40
+ name: string;
41
+ method: number;
42
+ crc32: number;
43
+ compSize: number;
44
+ uncompSize: number;
45
+ localHeaderOffset: number;
46
+ cdEntryOffset: number;
47
+ nameLength: number;
48
+ extraLength: number;
49
+ commentLength: number;
50
+ }
51
+
52
+ export interface ZipArchiveInfo {
53
+ eocdOffset: number;
54
+ cdOffset: number;
55
+ cdSize: number;
56
+ cdStart: number;
57
+ baseOffset: number;
58
+ entries: Map<string, ZipEntryInfo>;
59
+ }
60
+
61
+ export interface PatchOptions {
62
+ cacheBaseDir?: string;
63
+ force?: boolean;
64
+ skipSign?: boolean;
65
+ }
66
+
67
+ export interface PatchResult {
68
+ path: string;
69
+ patched: boolean;
70
+ classification: PatchClassification;
71
+ cached: boolean;
72
+ }
73
+
74
+ function isValidEocd(buf: Buffer, pos: number): boolean {
75
+ if (pos + 22 > buf.length) return false;
76
+ const cdSize = buf.readUInt32LE(pos + 12);
77
+ const cdStart = pos - cdSize;
78
+ if (cdStart < 0) return false;
79
+ if (cdSize === 0) return true;
80
+ if (cdStart + 4 > buf.length) return false;
81
+ return buf.readUInt32LE(cdStart) === ZIP_CD_MAGIC;
82
+ }
83
+
84
+ export function findEocd(buf: Buffer): number {
85
+ if (buf.length < 22) return -1;
86
+ const signature = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
87
+ let pos = buf.length - 22;
88
+ while (pos >= 0) {
89
+ pos = buf.lastIndexOf(signature, pos);
90
+ if (pos === -1) return -1;
91
+ if (isValidEocd(buf, pos)) return pos;
92
+ pos -= 1;
93
+ }
94
+ return -1;
95
+ }
96
+
97
+ function parseCdEntries(buf: Buffer, cdStart: number, cdSize: number): Map<string, ZipEntryInfo> {
98
+ const entries = new Map<string, ZipEntryInfo>();
99
+ let pos = cdStart;
100
+ const cdEnd = cdStart + cdSize;
101
+
102
+ while (pos + 46 <= cdEnd) {
103
+ if (buf.readUInt32LE(pos) !== ZIP_CD_MAGIC) break;
104
+ const method = buf.readUInt16LE(pos + 10);
105
+ const crc32 = buf.readUInt32LE(pos + 16);
106
+ const compSize = buf.readUInt32LE(pos + 20);
107
+ const uncompSize = buf.readUInt32LE(pos + 24);
108
+ const nameLength = buf.readUInt16LE(pos + 28);
109
+ const extraLength = buf.readUInt16LE(pos + 30);
110
+ const commentLength = buf.readUInt16LE(pos + 32);
111
+ const localHeaderOffset = buf.readUInt32LE(pos + 42);
112
+ const nameStart = pos + 46;
113
+ const nameEnd = nameStart + nameLength;
114
+ if (nameEnd > cdEnd) break;
115
+ const name = buf.toString("utf8", nameStart, nameEnd);
116
+
117
+ entries.set(name, {
118
+ name,
119
+ method,
120
+ crc32,
121
+ compSize,
122
+ uncompSize,
123
+ localHeaderOffset,
124
+ cdEntryOffset: pos,
125
+ nameLength,
126
+ extraLength,
127
+ commentLength,
128
+ });
129
+
130
+ pos += 46 + nameLength + extraLength + commentLength;
131
+ }
132
+
133
+ return entries;
134
+ }
135
+
136
+ export function parseZipArchive(buf: Buffer): ZipArchiveInfo | null {
137
+ const eocdOffset = findEocd(buf);
138
+ if (eocdOffset < 0) return null;
139
+
140
+ const cdSize = buf.readUInt32LE(eocdOffset + 12);
141
+ const cdOffset = buf.readUInt32LE(eocdOffset + 16);
142
+ const cdStart = eocdOffset - cdSize;
143
+ const baseOffset = cdStart - cdOffset;
144
+ const entries = parseCdEntries(buf, cdStart, cdSize);
145
+
146
+ return {
147
+ eocdOffset,
148
+ cdOffset,
149
+ cdSize,
150
+ cdStart,
151
+ baseOffset,
152
+ entries,
153
+ };
154
+ }
155
+
156
+ export function getEntryData(buf: Buffer, zip: ZipArchiveInfo, entry: ZipEntryInfo): Buffer | null {
157
+ const absLocalHdr = zip.baseOffset + entry.localHeaderOffset;
158
+ if (absLocalHdr < 0 || absLocalHdr + 30 > buf.length) return null;
159
+ if (buf.readUInt32LE(absLocalHdr) !== ZIP_LOCAL_MAGIC) return null;
160
+
161
+ const localNameLen = buf.readUInt16LE(absLocalHdr + 26);
162
+ const localExtraLen = buf.readUInt16LE(absLocalHdr + 28);
163
+ const dataOffset = absLocalHdr + 30 + localNameLen + localExtraLen;
164
+ const dataEnd = dataOffset + entry.uncompSize;
165
+
166
+ if (dataEnd > buf.length) return null;
167
+ return buf.subarray(dataOffset, dataEnd);
168
+ }
169
+
170
+ export function findAnchorAndReplacement(
171
+ text: string,
172
+ ): { anchor: string; replacement: string } | null {
173
+ if (text.includes(ORIGINAL_ANCHOR)) {
174
+ return { anchor: ORIGINAL_ANCHOR, replacement: REPLACEMENT_CODE };
175
+ }
176
+ if (text.includes(PROMPT_ANCHOR)) {
177
+ return { anchor: PROMPT_ANCHOR, replacement: PROMPT_REPLACEMENT };
178
+ }
179
+ return null;
180
+ }
181
+
182
+ export function shortenDecorativeComments(text: string, delta: number): string | null {
183
+ if (delta <= 0) return text;
184
+ const matches = [...text.matchAll(/([ \t]*# -{20,})/g)];
185
+ if (matches.length === 0) return null;
186
+
187
+ if (matches.length >= 2) {
188
+ const d1 = Math.ceil(delta / 2);
189
+ const d2 = Math.floor(delta / 2);
190
+ const m1 = matches[0];
191
+ const m2 = matches[1];
192
+ if (m1[0].length <= d1 + 5 || m2[0].length <= d2 + 5) return null;
193
+ const s1 = m1[0].slice(0, m1[0].length - d1);
194
+ const s2 = m2[0].slice(0, m2[0].length - d2);
195
+ return (
196
+ text.slice(0, m1.index) +
197
+ s1 +
198
+ text.slice(m1.index + m1[0].length, m2.index) +
199
+ s2 +
200
+ text.slice(m2.index + m2[0].length)
201
+ );
202
+ }
203
+
204
+ const m = matches[0];
205
+ if (m[0].length <= delta + 5) return null;
206
+ const s = m[0].slice(0, m[0].length - delta);
207
+ return text.slice(0, m.index) + s + text.slice(m.index + m[0].length);
208
+ }
209
+
210
+ export function patchEventProcessorContent(content: string): string | null {
211
+ const pair = findAnchorAndReplacement(content);
212
+ if (!pair) return null;
213
+ const delta =
214
+ Buffer.byteLength(pair.replacement, "utf8") - Buffer.byteLength(pair.anchor, "utf8");
215
+ const shortened = shortenDecorativeComments(content, delta);
216
+ if (!shortened) return null;
217
+ const patched = shortened.replace(pair.anchor, pair.replacement);
218
+ if (Buffer.byteLength(patched, "utf8") !== Buffer.byteLength(content, "utf8")) {
219
+ return null;
220
+ }
221
+ return patched;
222
+ }
223
+
224
+ export function classifyBuffer(buf: Buffer): PatchClassification {
225
+ const zip = parseZipArchive(buf);
226
+ if (!zip) return "unrecognized";
227
+
228
+ const entry = zip.entries.get(EVENT_PROCESSOR_ENTRY);
229
+ if (!entry) return "entry-missing";
230
+ if (entry.method !== 0) return "unrecognized";
231
+
232
+ const data = getEntryData(buf, zip, entry);
233
+ if (!data) return "unrecognized";
234
+
235
+ const text = data.toString("utf8");
236
+ if (text.includes("STATE_WAITING_FOR_TASKS")) {
237
+ return "already-fixed";
238
+ }
239
+
240
+ const pair = findAnchorAndReplacement(text);
241
+ if (!pair) return "unrecognized";
242
+
243
+ const delta =
244
+ Buffer.byteLength(pair.replacement, "utf8") - Buffer.byteLength(pair.anchor, "utf8");
245
+ const shortened = shortenDecorativeComments(text, delta);
246
+ if (!shortened) return "unrecognized";
247
+
248
+ return "patchable";
249
+ }
250
+
251
+ export function classifyBinary(filePath: string): PatchClassification {
252
+ try {
253
+ const buf = fs.readFileSync(filePath);
254
+ return classifyBuffer(buf);
255
+ } catch {
256
+ return "unrecognized";
257
+ }
258
+ }
259
+
260
+ function renameSiblingPyc(buf: Buffer, zip: ZipArchiveInfo): void {
261
+ for (const entry of zip.entries.values()) {
262
+ if (!entry.name.startsWith(PYCACHE_PREFIX) || !entry.name.endsWith(".pyc")) {
263
+ continue;
264
+ }
265
+
266
+ const cdExtPos = entry.cdEntryOffset + 46 + entry.nameLength - 4;
267
+ if (buf.subarray(cdExtPos, cdExtPos + 4).toString("utf8") === ".pyc") {
268
+ buf.write(".bak", cdExtPos, "utf8");
269
+ }
270
+
271
+ const localHdrPos = zip.baseOffset + entry.localHeaderOffset;
272
+ const localNameLen = buf.readUInt16LE(localHdrPos + 26);
273
+ const localExtPos = localHdrPos + 30 + localNameLen - 4;
274
+ if (buf.subarray(localExtPos, localExtPos + 4).toString("utf8") === ".pyc") {
275
+ buf.write(".bak", localExtPos, "utf8");
276
+ }
277
+ }
278
+ }
279
+
280
+ export function patchBuffer(buf: Buffer): {
281
+ patched: boolean;
282
+ classification: PatchClassification;
283
+ } {
284
+ const classification = classifyBuffer(buf);
285
+ if (classification !== "patchable") {
286
+ return { patched: false, classification };
287
+ }
288
+
289
+ const zip = parseZipArchive(buf)!;
290
+ const entry = zip.entries.get(EVENT_PROCESSOR_ENTRY)!;
291
+ const data = getEntryData(buf, zip, entry)!;
292
+ const text = data.toString("utf8");
293
+
294
+ const patchedText = patchEventProcessorContent(text);
295
+ if (!patchedText) {
296
+ return { patched: false, classification: "unrecognized" };
297
+ }
298
+
299
+ const localHdrPos = zip.baseOffset + entry.localHeaderOffset;
300
+ const localNameLen = buf.readUInt16LE(localHdrPos + 26);
301
+ const localExtraLen = buf.readUInt16LE(localHdrPos + 28);
302
+ const dataOffset = localHdrPos + 30 + localNameLen + localExtraLen;
303
+
304
+ buf.write(patchedText, dataOffset, "utf8");
305
+
306
+ const newCrc = zlib.crc32(buf.subarray(dataOffset, dataOffset + entry.uncompSize));
307
+ buf.writeUInt32LE(newCrc, localHdrPos + 14);
308
+ buf.writeUInt32LE(newCrc, entry.cdEntryOffset + 16);
309
+
310
+ renameSiblingPyc(buf, zip);
311
+
312
+ return { patched: true, classification: "already-fixed" };
313
+ }
314
+
315
+ export function getCacheKey(realpath: string, stat?: { size: number; mtimeMs: number }): string {
316
+ const s = stat ?? fs.statSync(realpath);
317
+ return crypto
318
+ .createHash("sha256")
319
+ .update(`${realpath}:${s.size}:${s.mtimeMs}`)
320
+ .digest("hex")
321
+ .slice(0, 32);
322
+ }
323
+
324
+ export function getCacheBaseDir(customDir?: string): string {
325
+ if (customDir) return customDir;
326
+ const pluginData =
327
+ process.env.PASEO_PLUGIN_DATA_DIR ?? path.join(os.homedir(), ".paseo", "plugin-data");
328
+ return path.join(pluginData, "antigravity", "bin");
329
+ }
330
+
331
+ function cloneFile(src: string, dst: string): void {
332
+ try {
333
+ fs.copyFileSync(src, dst, fs.constants.COPYFILE_FICLONE);
334
+ } catch {
335
+ fs.copyFileSync(src, dst);
336
+ }
337
+ }
338
+
339
+ function reSignBinary(dst: string): void {
340
+ if (process.platform !== "darwin") return;
341
+ try {
342
+ execFileSync(
343
+ "codesign",
344
+ ["--force", "-s", "-", "--preserve-metadata=entitlements,flags", dst],
345
+ { stdio: "ignore" },
346
+ );
347
+ } catch {
348
+ // Gracefully ignore codesign errors
349
+ }
350
+ }
351
+
352
+ export function gcCache(baseDir: string, currentKey?: string): void {
353
+ if (!fs.existsSync(baseDir)) return;
354
+ try {
355
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
356
+ for (const entry of entries) {
357
+ if (!entry.isDirectory()) continue;
358
+ if (currentKey && entry.name === currentKey) continue;
359
+ fs.rmSync(path.join(baseDir, entry.name), {
360
+ recursive: true,
361
+ force: true,
362
+ });
363
+ }
364
+ } catch {
365
+ // Non-fatal if GC fails
366
+ }
367
+ }
368
+
369
+ interface CacheTarget {
370
+ realPath: string;
371
+ key: string;
372
+ cacheBase: string;
373
+ cacheDir: string;
374
+ dstPath: string;
375
+ }
376
+
377
+ function resolveCacheTarget(srcPath: string, customBase?: string): CacheTarget {
378
+ const realPath = fs.realpathSync(srcPath);
379
+ const stat = fs.statSync(realPath);
380
+ const key = getCacheKey(realPath, stat);
381
+ const cacheBase = getCacheBaseDir(customBase);
382
+ const cacheDir = path.join(cacheBase, key);
383
+ const dstPath = path.join(cacheDir, path.basename(realPath));
384
+ return { realPath, key, cacheBase, cacheDir, dstPath };
385
+ }
386
+
387
+ function cloneAndPatch(
388
+ realPath: string,
389
+ dstPath: string,
390
+ cacheDir: string,
391
+ skipSign?: boolean,
392
+ ): boolean {
393
+ fs.mkdirSync(cacheDir, { recursive: true, mode: 0o755 });
394
+ const tempDst = `${dstPath}.tmp.${Date.now()}`;
395
+ cloneFile(realPath, tempDst);
396
+ fs.chmodSync(tempDst, 0o755);
397
+
398
+ const buf = fs.readFileSync(tempDst);
399
+ const patchRes = patchBuffer(buf);
400
+ if (!patchRes.patched) {
401
+ fs.rmSync(tempDst, { force: true });
402
+ return false;
403
+ }
404
+
405
+ fs.writeFileSync(tempDst, buf);
406
+ if (!skipSign) {
407
+ reSignBinary(tempDst);
408
+ }
409
+ fs.renameSync(tempDst, dstPath);
410
+ return true;
411
+ }
412
+
413
+ export function patchBinary(srcPath: string, options?: PatchOptions): PatchResult {
414
+ if (process.env.PASEO_AGY_NO_PATCH === "1") {
415
+ return { path: srcPath, patched: false, classification: "unrecognized", cached: false };
416
+ }
417
+
418
+ const { realPath, key, cacheBase, cacheDir, dstPath } = resolveCacheTarget(
419
+ srcPath,
420
+ options?.cacheBaseDir,
421
+ );
422
+
423
+ if (!options?.force && fs.existsSync(dstPath)) {
424
+ gcCache(cacheBase, key);
425
+ return { path: dstPath, patched: false, classification: "already-fixed", cached: true };
426
+ }
427
+
428
+ const classification = classifyBinary(realPath);
429
+ if (classification !== "patchable") {
430
+ return { path: srcPath, patched: false, classification, cached: false };
431
+ }
432
+
433
+ const ok = cloneAndPatch(realPath, dstPath, cacheDir, options?.skipSign);
434
+ if (!ok) {
435
+ return { path: srcPath, patched: false, classification: "unrecognized", cached: false };
436
+ }
437
+
438
+ gcCache(cacheBase, key);
439
+ return { path: dstPath, patched: true, classification: "already-fixed", cached: false };
440
+ }
441
+
442
+ export function ensurePatchedBinary(srcPath: string, options?: PatchOptions): string {
443
+ const res = patchBinary(srcPath, options);
444
+ return res.path;
445
+ }
@@ -0,0 +1,122 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+
6
+ /**
7
+ * Resolves the path to the ACP token JSON file under the Gemini home directory.
8
+ */
9
+ export function getAcpTokenPath(): string {
10
+ const geminiHome = process.env.GEMINI_HOME || path.join(os.homedir(), ".gemini");
11
+ return path.join(geminiHome, "antigravity-acp", "acp_token.json");
12
+ }
13
+
14
+ /**
15
+ * Checks if the local ACP token file contains an active refresh token.
16
+ */
17
+ export function hasValidRefreshToken(tokenPath: string = getAcpTokenPath()): boolean {
18
+ if (!fs.existsSync(tokenPath)) return false;
19
+ try {
20
+ const existing = JSON.parse(fs.readFileSync(tokenPath, "utf-8"));
21
+ return typeof existing.refresh_token === "string" && existing.refresh_token.length > 0;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Extracts the CLI refresh token from macOS Keychain if available.
29
+ */
30
+ function extractClientId(idToken: unknown): string | undefined {
31
+ if (typeof idToken !== "string") return undefined;
32
+ try {
33
+ const parts = idToken.split(".");
34
+ if (parts.length < 2) return undefined;
35
+ const payload = JSON.parse(Buffer.from(parts[1], "base64").toString("utf-8"));
36
+ return typeof payload.aud === "string" ? payload.aud : undefined;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ export function readKeychainCredentials(): { refreshToken: string; clientId?: string } | null {
43
+ if (process.platform !== "darwin") return null;
44
+ try {
45
+ const raw = execFileSync(
46
+ "security",
47
+ ["find-generic-password", "-s", "gemini", "-a", "antigravity", "-w"],
48
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 },
49
+ ).trim();
50
+
51
+ if (!raw) return null;
52
+ const b64 = raw.replace(/^go-keyring-base64:/, "");
53
+ const parsed = JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
54
+ const refreshToken = parsed?.token?.refresh_token;
55
+ if (!refreshToken) return null;
56
+
57
+ const clientId = extractClientId(parsed.id_token);
58
+ return { refreshToken, clientId };
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Writes an authorized_user credential blob to the ACP token path with mode 0600.
66
+ */
67
+ export function writeAcpTokenFile(
68
+ tokenPath: string,
69
+ refreshToken: string,
70
+ clientId?: string,
71
+ ): void {
72
+ const acpBlob: Record<string, unknown> = {
73
+ refresh_token: refreshToken,
74
+ type: "authorized_user",
75
+ };
76
+ if (clientId) {
77
+ acpBlob.client_id = clientId;
78
+ }
79
+ fs.mkdirSync(path.dirname(tokenPath), { recursive: true, mode: 0o700 });
80
+ fs.writeFileSync(tokenPath, JSON.stringify(acpBlob, null, 2), { mode: 0o600 });
81
+ try {
82
+ fs.chmodSync(tokenPath, 0o600);
83
+ } catch {
84
+ // Ignore chmod failure on filesystems that do not support POSIX modes
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Syncs credentials from the official `agy` CLI's Keychain entry (`antigravity`)
90
+ * into `~/.gemini/antigravity-acp/acp_token.json` if the token file is missing or invalid.
91
+ */
92
+ export function syncAgyCredentials(): boolean {
93
+ const tokenPath = getAcpTokenPath();
94
+ if (hasValidRefreshToken(tokenPath)) {
95
+ return true;
96
+ }
97
+
98
+ const creds = readKeychainCredentials();
99
+ if (!creds) {
100
+ return false;
101
+ }
102
+
103
+ try {
104
+ writeAcpTokenFile(tokenPath, creds.refreshToken, creds.clientId);
105
+ return true;
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Ensures the user is authenticated with Antigravity before starting the ACP server.
113
+ * Throws an actionable error directing the user to run `agy` in their terminal.
114
+ */
115
+ export function ensureAuthenticated(): void {
116
+ const ok = syncAgyCredentials();
117
+ if (!ok && !hasValidRefreshToken(getAcpTokenPath())) {
118
+ throw new Error(
119
+ "Not logged in to Google Antigravity. Please run 'agy' in your terminal to log in.",
120
+ );
121
+ }
122
+ }