supergravity-mcp 0.1.3 → 0.1.5

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.
@@ -32,34 +32,53 @@ GPT-OSS 120B (~$0.03-0.09/$0.10-0.40 — far cheaper than the rest) — open-wei
32
32
 
33
33
  Gemini models and Claude+GPT models draw from SEPARATE Antigravity quota pools — call getAntigravityQuota() first if unsure which pool has room.
34
34
  `.trim();
35
+ // Antigravity is a single window shared by every call. Two delegations
36
+ // running at once would type into and read from the same conversation,
37
+ // corrupting both. Every entry point below is serialized through this lock
38
+ // so calls queue up instead of racing each other.
39
+ let lock = Promise.resolve();
40
+ function withLock(fn) {
41
+ const run = lock.then(fn, fn);
42
+ lock = run.then(() => undefined, () => undefined);
43
+ return run;
44
+ }
35
45
  export async function delegateToAntigravity(opts) {
36
- const { task, model, timeoutMs = 120_000 } = opts;
37
- await ensureAntigravityReady();
38
- const target = await getMainPageTarget();
39
- const session = new CdpSession(target.webSocketDebuggerUrl);
40
- try {
41
- await startNewConversation(session);
42
- if (model)
43
- await selectModel(session, model);
44
- await pasteAndSend(session, task);
45
- return await waitForReply(session, task, timeoutMs);
46
- }
47
- finally {
48
- session.close();
49
- }
46
+ return withLock(async () => {
47
+ const { task, model, timeoutMs = 120_000 } = opts;
48
+ await ensureAntigravityReady();
49
+ const target = await getMainPageTarget();
50
+ const session = new CdpSession(target.webSocketDebuggerUrl);
51
+ try {
52
+ // The CDP debug port opens before Antigravity's own UI finishes mounting
53
+ // (its window loads content from a local server that itself needs a
54
+ // moment to boot). On a cold start this can leave the chat input
55
+ // missing for several seconds after we've already connected.
56
+ await waitForChatReady(session);
57
+ await startNewConversation(session);
58
+ if (model)
59
+ await selectModel(session, model);
60
+ await pasteAndSend(session, task);
61
+ return await waitForReply(session, task, timeoutMs);
62
+ }
63
+ finally {
64
+ session.close();
65
+ }
66
+ });
50
67
  }
51
68
  export async function getAntigravityQuota() {
52
- await ensureAntigravityReady();
53
- const target = await getMainPageTarget();
54
- const session = new CdpSession(target.webSocketDebuggerUrl);
55
- try {
56
- const raw = await openSettingsModelsTab(session);
57
- return parseQuotaText(raw);
58
- }
59
- finally {
60
- await closeSettings(session);
61
- session.close();
62
- }
69
+ return withLock(async () => {
70
+ await ensureAntigravityReady();
71
+ const target = await getMainPageTarget();
72
+ const session = new CdpSession(target.webSocketDebuggerUrl);
73
+ try {
74
+ const raw = await openSettingsModelsTab(session);
75
+ return parseQuotaText(raw);
76
+ }
77
+ finally {
78
+ await closeSettings(session);
79
+ session.close();
80
+ }
81
+ });
63
82
  }
64
83
  async function openSettingsModelsTab(session) {
65
84
  await session.evaluate(`
@@ -258,6 +277,26 @@ class CdpSession {
258
277
  }
259
278
  }
260
279
  // ---------- UI actions (selectors verified against Antigravity 2.2.1) ----------
280
+ async function waitForChatReady(session, timeoutMs = 45_000) {
281
+ const start = Date.now();
282
+ while (Date.now() - start < timeoutMs) {
283
+ const state = await session.evaluate(`
284
+ (() => {
285
+ if (document.querySelector('[contenteditable="true"][aria-label="Message input"]')) return 'ready';
286
+ const text = document.body.innerText || '';
287
+ if (text.includes('Continue with Google') || text.includes('Welcome to Antigravity')) return 'needs-signin';
288
+ return 'loading';
289
+ })()
290
+ `);
291
+ if (state === "ready")
292
+ return;
293
+ if (state === "needs-signin") {
294
+ throw new Error("Antigravity needs you to sign in. Open the app and click \"Continue with Google\", then try again — this can't be automated (it's your Google login).");
295
+ }
296
+ await sleep(500);
297
+ }
298
+ throw new Error(`Antigravity's chat UI didn't finish loading within ${timeoutMs}ms of launch. It may still be starting up — try again in a few seconds.`);
299
+ }
261
300
  async function startNewConversation(session) {
262
301
  await session.evaluate(`
263
302
  (() => {
@@ -377,10 +416,15 @@ async function waitForReply(session, taskText, timeoutMs) {
377
416
  continue;
378
417
  if (threadText === lastText) {
379
418
  stableCount++;
380
- // Require the "Thought for Ns" marker so we don't return early on the
381
- // brief window right after sending, before Antigravity starts replying.
382
- if (stableCount >= 2 && /Thought for/.test(threadText)) {
383
- return extractReply(threadText, taskText);
419
+ // Two consecutive stable reads plus actual reply content (not just our
420
+ // own sent message sitting there) means it's done. "Thought for Ns" is
421
+ // NOT a reliable signal Antigravity skips it entirely for fast/simple
422
+ // answers, so requiring it caused real replies to be missed until
423
+ // timeout even though the answer was already on screen.
424
+ if (stableCount >= 2) {
425
+ const candidate = extractReply(threadText, taskText);
426
+ if (candidate)
427
+ return candidate;
384
428
  }
385
429
  }
386
430
  else {
@@ -391,13 +435,26 @@ async function waitForReply(session, taskText, timeoutMs) {
391
435
  throw new Error(`Timed out after ${timeoutMs}ms waiting for Antigravity's reply.`);
392
436
  }
393
437
  function extractReply(threadText, taskText) {
438
+ // We only search on the first 200 chars (waitForReply's DOM lookup does the
439
+ // same, to keep every poll's evaluate() call cheap on huge tasks) - but
440
+ // slicing by that same truncated length left a tail of our own message
441
+ // behind for anything longer than 200 chars. Instead, find where OUR
442
+ // message actually ends by locating the timestamp Antigravity renders
443
+ // immediately after it, which appears regardless of message length.
394
444
  const needle = taskText.slice(0, 200);
395
445
  const idx = threadText.lastIndexOf(needle);
396
- const after = idx >= 0 ? threadText.slice(idx) : threadText;
397
- const thoughtMatch = after.match(/Thought for[^\n]*\n+/);
398
- let rest = thoughtMatch ? after.slice(after.indexOf(thoughtMatch[0]) + thoughtMatch[0].length) : after;
399
- rest = rest.replace(/\n+\d{1,2}:\d{2}\s*$/, "").trim();
400
- return rest;
446
+ const searchFrom = idx >= 0 ? idx + needle.length : 0;
447
+ const closingTimestamp = threadText.slice(searchFrom).match(/\d{1,2}:\d{2}/);
448
+ let after = closingTimestamp
449
+ ? threadText.slice(searchFrom + closingTimestamp.index + closingTimestamp[0].length)
450
+ : threadText.slice(searchFrom);
451
+ // strip the "Thought for Ns" line when present - not always there
452
+ const thoughtMatch = after.match(/^\n*Thought for[^\n]*\n+/);
453
+ if (thoughtMatch)
454
+ after = after.slice(thoughtMatch[0].length);
455
+ // strip the trailing timestamp under the reply itself
456
+ after = after.replace(/\n+\d{1,2}:\d{2}\s*$/, "");
457
+ return after.trim();
401
458
  }
402
459
  function sleep(ms) {
403
460
  return new Promise((resolve) => setTimeout(resolve, ms));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supergravity-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "🚀 MCP server that lets Claude (or any MCP client) delegate tasks to Google's Antigravity desktop app 🌌. Built for Intel Macs 💻 that Antigravity's own CLI doesn't support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,9 +6,10 @@ description: Use when the user wants to delegate a task to Google's Antigravity
6
6
  # delegate
7
7
 
8
8
  Antigravity is a separate AI desktop app (Google's). This skill runs tasks
9
- through it via the `supergravity-mcp` MCP server, instead of answering
10
- yourself useful when the user explicitly asks to use it, wants a second
11
- opinion from a different model family, or wants to offload work.
9
+ through it via the `supergravity` MCP server's `delegate_to_antigravity` /
10
+ `get_antigravity_quota` tools, instead of answering yourself useful when
11
+ the user explicitly asks to use it, wants a second opinion from a different
12
+ model family, or wants to offload work.
12
13
 
13
14
  ## When to trigger
14
15
 
@@ -18,6 +19,21 @@ Any explicit ask to use Antigravity or one of the models it hosts:
18
19
  generic requests that don't name Antigravity or one of its models — just
19
20
  answer normally.
20
21
 
22
+ Load this skill *before* calling either tool the first time in a
23
+ conversation, not after something already went wrong — the model-picking and
24
+ quota-pool guidance below changes which tool call you should even make.
25
+
26
+ ## You don't need to check if Antigravity is running
27
+
28
+ `delegate_to_antigravity` launches Antigravity itself if it isn't already
29
+ open, and waits for its chat UI to finish loading before typing anything —
30
+ you don't need to check first or tell the user to open it. Just call the
31
+ tool. The only implication: the very first call in a session can take
32
+ 15-30+ seconds longer than normal (cold app launch) before anything visible
33
+ happens. If a call fails specifically because the app couldn't be found at
34
+ all, the error names the exact env var (`ANTIGRAVITY_APP_PATH`) to fix it —
35
+ that's a real install problem, not something to retry.
36
+
21
37
  ## Picking a model
22
38
 
23
39
  If the user names a specific model ("use claude opus", "use gemini flash"),
@@ -44,6 +60,22 @@ until it refreshes — don't retry the same model. Instead:
44
60
  3. Otherwise, tell the user which pool is out and when it refreshes — don't
45
61
  silently fall back without saying so if they asked for a specific model.
46
62
 
63
+ ## Code-editing tasks: verify with the filesystem, not the reply text
64
+
65
+ If the task involves editing code, Antigravity works inside a real project
66
+ folder on disk (its "Projects" sidebar is literal directories, same
67
+ filesystem you already have access to) — it actually writes the files, it
68
+ doesn't just describe changes in chat. Its reply text is a narrative summary
69
+ written by the model, not the deliverable, and can be vague or incomplete
70
+ ("Updated the function as requested").
71
+
72
+ So after a code-editing delegation, don't parse the reply for specifics —
73
+ go check the real files yourself: `git diff`, `git status`, or just read the
74
+ file. You have direct filesystem access on this machine; use it instead of
75
+ trusting prose. Only fall back to the reply text for context Antigravity
76
+ wouldn't have written to disk (its reasoning, or a plain Q&A task with no
77
+ files involved).
78
+
47
79
  ## Other failure modes
48
80
 
49
81
  - Antigravity not installed / not found: the error will say so — tell the