@quantiya/codevibe-antigravity-plugin 1.0.0 → 1.0.2
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/server.js +258 -7
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1457,6 +1457,7 @@ function parseApprovalUISnapshot(stripped, originalSnapshot) {
|
|
|
1457
1457
|
if (options.length < 2) return null;
|
|
1458
1458
|
const submitMap = buildApprovalSubmitMap(options);
|
|
1459
1459
|
const { command, filePath } = extractIdentity(headerText);
|
|
1460
|
+
const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
|
|
1460
1461
|
return {
|
|
1461
1462
|
kind: "approval",
|
|
1462
1463
|
headerText,
|
|
@@ -1465,6 +1466,7 @@ function parseApprovalUISnapshot(stripped, originalSnapshot) {
|
|
|
1465
1466
|
filePath,
|
|
1466
1467
|
options,
|
|
1467
1468
|
submitMap,
|
|
1469
|
+
body,
|
|
1468
1470
|
paneHash: hashPromptSnapshot(originalSnapshot)
|
|
1469
1471
|
};
|
|
1470
1472
|
}
|
|
@@ -1481,6 +1483,7 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
|
|
|
1481
1483
|
const options = parseOptions(belowHeader);
|
|
1482
1484
|
if (options.length < 2) return null;
|
|
1483
1485
|
const submitMap = buildQuestionSubmitMap(options);
|
|
1486
|
+
const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
|
|
1484
1487
|
return {
|
|
1485
1488
|
kind: "question",
|
|
1486
1489
|
headerText: header.body,
|
|
@@ -1489,9 +1492,30 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
|
|
|
1489
1492
|
filePath: void 0,
|
|
1490
1493
|
options,
|
|
1491
1494
|
submitMap,
|
|
1495
|
+
body,
|
|
1492
1496
|
paneHash: hashPromptSnapshot(originalSnapshot)
|
|
1493
1497
|
};
|
|
1494
1498
|
}
|
|
1499
|
+
function extractBodyBetweenHeaderAndFirstOption(belowHeader) {
|
|
1500
|
+
const lines = belowHeader.split("\n");
|
|
1501
|
+
const optionRegex = /^[>\s]*([1-9])\. (.+?)\s*$/;
|
|
1502
|
+
const fenceRegex = /^```/;
|
|
1503
|
+
let firstOptionIdx = -1;
|
|
1504
|
+
let inFence = false;
|
|
1505
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1506
|
+
if (fenceRegex.test(lines[i])) {
|
|
1507
|
+
inFence = !inFence;
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (inFence) continue;
|
|
1511
|
+
if (optionRegex.test(lines[i])) {
|
|
1512
|
+
firstOptionIdx = i;
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (firstOptionIdx <= 0) return "";
|
|
1517
|
+
return lines.slice(0, firstOptionIdx).join("\n").trim();
|
|
1518
|
+
}
|
|
1495
1519
|
function parseOptions(snapshot) {
|
|
1496
1520
|
const recent = snapshot.split("\n").slice(-30);
|
|
1497
1521
|
const optionRegex = /^[>\s]*([1-9])\. (.+?)\s*$/;
|
|
@@ -1720,6 +1744,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
1720
1744
|
submitMap: candidate.submitMap,
|
|
1721
1745
|
matchedPaneHeader: candidate.headerText,
|
|
1722
1746
|
paneDisplayHeader: candidate.fullHeaderLine,
|
|
1747
|
+
body: candidate.body,
|
|
1723
1748
|
emittedAt: Date.now(),
|
|
1724
1749
|
ttlMs: this.promptTtlMs,
|
|
1725
1750
|
paneOptions: candidate.options
|
|
@@ -1904,6 +1929,7 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
1904
1929
|
submitMap: candidate.submitMap,
|
|
1905
1930
|
matchedPaneHeader: candidate.headerText,
|
|
1906
1931
|
paneDisplayHeader: candidate.fullHeaderLine,
|
|
1932
|
+
body: candidate.body,
|
|
1907
1933
|
emittedAt: Date.now(),
|
|
1908
1934
|
ttlMs: this.promptTtlMs
|
|
1909
1935
|
};
|
|
@@ -2826,6 +2852,7 @@ var McpServer = class {
|
|
|
2826
2852
|
this.wrapperPid = options.wrapperPid ?? null;
|
|
2827
2853
|
this.cliLogPath = options.cliLogPath ?? null;
|
|
2828
2854
|
this.appSyncClient = options.appSyncClient ?? new import_codevibe_core4.AppSyncClient();
|
|
2855
|
+
this.workingDirectory = process.cwd();
|
|
2829
2856
|
this.approvalDetector = options.approvalDetector ?? new ApprovalDetector(options.detectorOptions);
|
|
2830
2857
|
this.paneObserver = options.paneObserver ?? new TmuxPaneObserver();
|
|
2831
2858
|
this.transcriptTailer = options.transcriptTailer ?? new TranscriptTailer();
|
|
@@ -3269,12 +3296,36 @@ var McpServer = class {
|
|
|
3269
3296
|
return;
|
|
3270
3297
|
}
|
|
3271
3298
|
const optionsArr = state.paneOptions ? state.paneOptions.map((o) => ({ number: o.number, text: o.text })) : Object.keys(state.submitMap).map((n) => ({ number: n }));
|
|
3299
|
+
const diffParsed = parseFencedDiffFromBody(
|
|
3300
|
+
state.body,
|
|
3301
|
+
state.paneDisplayHeader
|
|
3302
|
+
);
|
|
3303
|
+
if (diffParsed) {
|
|
3304
|
+
logger.info("Parsed fenced diff for INTERACTIVE_PROMPT", {
|
|
3305
|
+
promptId: state.promptId,
|
|
3306
|
+
filePath: diffParsed.filePath,
|
|
3307
|
+
oldLines: diffParsed.oldString.split("\n").length,
|
|
3308
|
+
newLines: diffParsed.newString.split("\n").length,
|
|
3309
|
+
rawDiffLength: diffParsed.rawDiff.length
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
const toolName = diffParsed ? "Edit" : state.pendingCall?.toolType;
|
|
3313
|
+
const filePath = diffParsed?.filePath ?? state.pendingCall?.filePath;
|
|
3314
|
+
const toolInput = diffParsed ? {
|
|
3315
|
+
file_path: diffParsed.filePath,
|
|
3316
|
+
old_string: diffParsed.oldString,
|
|
3317
|
+
new_string: diffParsed.newString,
|
|
3318
|
+
old_start_line: diffParsed.oldStartLine,
|
|
3319
|
+
new_start_line: diffParsed.newStartLine,
|
|
3320
|
+
diff: diffParsed.rawDiff
|
|
3321
|
+
} : void 0;
|
|
3272
3322
|
const optionsForMobile = state.pendingCall ? {
|
|
3273
3323
|
promptId: state.promptId,
|
|
3274
|
-
tool_name:
|
|
3324
|
+
tool_name: toolName,
|
|
3275
3325
|
command: state.pendingCall.command,
|
|
3276
|
-
file_path:
|
|
3277
|
-
options: optionsArr
|
|
3326
|
+
file_path: filePath,
|
|
3327
|
+
options: optionsArr,
|
|
3328
|
+
...toolInput ? { tool_input: toolInput } : {}
|
|
3278
3329
|
} : { promptId: state.promptId };
|
|
3279
3330
|
const content = state.paneDisplayHeader || state.matchedPaneHeader || state.pendingCall?.command || state.pendingCall?.filePath || "approval requested";
|
|
3280
3331
|
const input = {
|
|
@@ -3296,6 +3347,89 @@ var McpServer = class {
|
|
|
3296
3347
|
}
|
|
3297
3348
|
}
|
|
3298
3349
|
// ─── Mobile → desktop flow ──────────────────────────────────────────────
|
|
3350
|
+
/**
|
|
3351
|
+
* Download an attachment from S3 to the working directory and return a
|
|
3352
|
+
* relative path suitable for agy's `@./path` syntax (inherited from
|
|
3353
|
+
* gemini-cli — Antigravity CLI is downstream of Gemini CLI).
|
|
3354
|
+
*
|
|
3355
|
+
* Mirrors codevibe-gemini-plugin's downloadAttachment() including the
|
|
3356
|
+
* AppSync nested-isEncrypted fallback (subscriptions return null on
|
|
3357
|
+
* nested object fields even when DynamoDB has the value).
|
|
3358
|
+
*
|
|
3359
|
+
* Returns the relative path (e.g. `./.codevibe-attachments/attachment-xxx.png`)
|
|
3360
|
+
* on success, or null on failure (logged + swallowed — one bad attachment
|
|
3361
|
+
* shouldn't block the whole prompt).
|
|
3362
|
+
*/
|
|
3363
|
+
async downloadAttachment(attachment, sessionId, sessionKey, eventIsEncrypted) {
|
|
3364
|
+
try {
|
|
3365
|
+
const shouldDecrypt = attachment.isEncrypted ?? eventIsEncrypted ?? false;
|
|
3366
|
+
logger.info("Downloading attachment", {
|
|
3367
|
+
id: attachment.id,
|
|
3368
|
+
type: attachment.type,
|
|
3369
|
+
filename: attachment.filename,
|
|
3370
|
+
s3Key: attachment.s3Key,
|
|
3371
|
+
attachmentIsEncrypted: attachment.isEncrypted,
|
|
3372
|
+
eventIsEncrypted,
|
|
3373
|
+
shouldDecrypt
|
|
3374
|
+
});
|
|
3375
|
+
const { downloadUrl } = await this.appSyncClient.getAttachmentDownloadUrl(attachment.s3Key);
|
|
3376
|
+
const response = await fetch(downloadUrl);
|
|
3377
|
+
if (!response.ok) {
|
|
3378
|
+
throw new Error(`Failed to download attachment: ${response.status} ${response.statusText}`);
|
|
3379
|
+
}
|
|
3380
|
+
let buffer = Buffer.from(await response.arrayBuffer());
|
|
3381
|
+
if (shouldDecrypt && sessionKey) {
|
|
3382
|
+
try {
|
|
3383
|
+
logger.info("Decrypting attachment", { id: attachment.id });
|
|
3384
|
+
buffer = import_codevibe_core4.cryptoService.decryptData(buffer, sessionKey);
|
|
3385
|
+
logger.info("Attachment decrypted successfully", {
|
|
3386
|
+
id: attachment.id,
|
|
3387
|
+
decryptedSize: buffer.length
|
|
3388
|
+
});
|
|
3389
|
+
} catch (decryptError) {
|
|
3390
|
+
logger.error("Failed to decrypt attachment:", { id: attachment.id, error: String(decryptError) });
|
|
3391
|
+
throw new Error("Failed to decrypt attachment");
|
|
3392
|
+
}
|
|
3393
|
+
} else if (shouldDecrypt && !sessionKey) {
|
|
3394
|
+
logger.warn("Cannot decrypt attachment - no session key available", { id: attachment.id });
|
|
3395
|
+
}
|
|
3396
|
+
const attachmentsDir = path5.join(this.workingDirectory, ".codevibe-attachments");
|
|
3397
|
+
if (!fs5.existsSync(attachmentsDir)) {
|
|
3398
|
+
fs5.mkdirSync(attachmentsDir, { recursive: true });
|
|
3399
|
+
}
|
|
3400
|
+
let extension = "";
|
|
3401
|
+
if (attachment.filename) {
|
|
3402
|
+
const ext = path5.extname(attachment.filename);
|
|
3403
|
+
if (ext) extension = ext;
|
|
3404
|
+
}
|
|
3405
|
+
if (!extension) {
|
|
3406
|
+
const mimeToExt = {
|
|
3407
|
+
"image/jpeg": ".jpg",
|
|
3408
|
+
"image/png": ".png",
|
|
3409
|
+
"image/gif": ".gif",
|
|
3410
|
+
"image/webp": ".webp",
|
|
3411
|
+
"image/heic": ".heic",
|
|
3412
|
+
"application/pdf": ".pdf"
|
|
3413
|
+
};
|
|
3414
|
+
extension = mimeToExt[attachment.type] || ".bin";
|
|
3415
|
+
}
|
|
3416
|
+
const filename = `attachment-${attachment.id}${extension}`;
|
|
3417
|
+
const absolutePath = path5.join(attachmentsDir, filename);
|
|
3418
|
+
fs5.writeFileSync(absolutePath, buffer);
|
|
3419
|
+
const relativePath = `./.codevibe-attachments/${filename}`;
|
|
3420
|
+
logger.info("Attachment saved to working directory", {
|
|
3421
|
+
id: attachment.id,
|
|
3422
|
+
sessionId,
|
|
3423
|
+
absolutePath,
|
|
3424
|
+
relativePath,
|
|
3425
|
+
size: buffer.length
|
|
3426
|
+
});
|
|
3427
|
+
return relativePath;
|
|
3428
|
+
} catch (error) {
|
|
3429
|
+
logger.error("Failed to download attachment:", { id: attachment.id, error: String(error) });
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3299
3433
|
async handleMobileEvent(evt) {
|
|
3300
3434
|
if (!this.started) return;
|
|
3301
3435
|
if (this.sessionDisabled) return;
|
|
@@ -3318,10 +3452,42 @@ var McpServer = class {
|
|
|
3318
3452
|
evt = decrypted;
|
|
3319
3453
|
await this.markDelivered(evt);
|
|
3320
3454
|
if (evt.type === import_codevibe_core4.EventType.USER_PROMPT) {
|
|
3321
|
-
|
|
3322
|
-
const
|
|
3455
|
+
let promptContent = evt.content;
|
|
3456
|
+
const attachments = evt.attachments ?? [];
|
|
3457
|
+
if (attachments.length > 0) {
|
|
3458
|
+
logger.info("Downloading attachments for USER_PROMPT", {
|
|
3459
|
+
sessionId: session.sessionId,
|
|
3460
|
+
eventId: evt.eventId,
|
|
3461
|
+
count: attachments.length
|
|
3462
|
+
});
|
|
3463
|
+
const attachmentPaths = [];
|
|
3464
|
+
for (const attachment of attachments) {
|
|
3465
|
+
const relPath = await this.downloadAttachment(
|
|
3466
|
+
attachment,
|
|
3467
|
+
session.sessionId,
|
|
3468
|
+
session.sessionKey ?? null,
|
|
3469
|
+
evt.isEncrypted
|
|
3470
|
+
);
|
|
3471
|
+
if (relPath) attachmentPaths.push(relPath);
|
|
3472
|
+
}
|
|
3473
|
+
if (attachmentPaths.length > 0) {
|
|
3474
|
+
const fileReferences = attachmentPaths.map((p) => `@${p}`).join(" ");
|
|
3475
|
+
if (promptContent) {
|
|
3476
|
+
promptContent = `${fileReferences} ${promptContent}`;
|
|
3477
|
+
} else {
|
|
3478
|
+
promptContent = `${fileReferences} Please analyze the attached file(s).`;
|
|
3479
|
+
}
|
|
3480
|
+
logger.info("Prompt augmented with attachment paths", {
|
|
3481
|
+
sessionId: session.sessionId,
|
|
3482
|
+
eventId: evt.eventId,
|
|
3483
|
+
attachmentCount: attachmentPaths.length
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
this.mobileDeduper.track(session.sessionId, promptContent);
|
|
3488
|
+
const result = await this.promptResponder.sendFreeForm(promptContent);
|
|
3323
3489
|
if (!result.ok) {
|
|
3324
|
-
this.mobileDeduper.forget(session.sessionId,
|
|
3490
|
+
this.mobileDeduper.forget(session.sessionId, promptContent);
|
|
3325
3491
|
logger.warn("sendFreeForm failed", { reason: result.reason, details: result.details });
|
|
3326
3492
|
return;
|
|
3327
3493
|
}
|
|
@@ -3541,11 +3707,96 @@ function parseArgs(argv) {
|
|
|
3541
3707
|
if (require.main === module) {
|
|
3542
3708
|
void main();
|
|
3543
3709
|
}
|
|
3710
|
+
function parseFencedDiffFromBody(body, header) {
|
|
3711
|
+
if (!body) return null;
|
|
3712
|
+
const fenceMatch = body.match(/```[^\n]*\n([\s\S]*?)\n```/i);
|
|
3713
|
+
if (!fenceMatch) return null;
|
|
3714
|
+
const rawDiff = fenceMatch[1];
|
|
3715
|
+
const oldLines = [];
|
|
3716
|
+
const newLines = [];
|
|
3717
|
+
const hunkHeaderPattern = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/;
|
|
3718
|
+
let currentOldLine = 0;
|
|
3719
|
+
let currentNewLine = 0;
|
|
3720
|
+
let oldStartLine;
|
|
3721
|
+
let newStartLine;
|
|
3722
|
+
let hunkCount = 0;
|
|
3723
|
+
let filePath;
|
|
3724
|
+
const stripPrefix = (raw) => {
|
|
3725
|
+
let p = raw.trim();
|
|
3726
|
+
if (p.startsWith("a/") || p.startsWith("b/")) p = p.slice(2);
|
|
3727
|
+
if (p === "/dev/null") return "";
|
|
3728
|
+
return p;
|
|
3729
|
+
};
|
|
3730
|
+
for (const line of rawDiff.split("\n")) {
|
|
3731
|
+
const hunkMatch = line.match(hunkHeaderPattern);
|
|
3732
|
+
if (hunkMatch) {
|
|
3733
|
+
hunkCount += 1;
|
|
3734
|
+
currentOldLine = Number.parseInt(hunkMatch[1], 10);
|
|
3735
|
+
currentNewLine = Number.parseInt(hunkMatch[2], 10);
|
|
3736
|
+
continue;
|
|
3737
|
+
}
|
|
3738
|
+
if (line.startsWith("+++ ")) {
|
|
3739
|
+
const pathVal = stripPrefix(line.slice(4));
|
|
3740
|
+
if (pathVal) filePath = pathVal;
|
|
3741
|
+
continue;
|
|
3742
|
+
}
|
|
3743
|
+
if (line.startsWith("--- ")) {
|
|
3744
|
+
if (!filePath) {
|
|
3745
|
+
const pathVal = stripPrefix(line.slice(4));
|
|
3746
|
+
if (pathVal) filePath = pathVal;
|
|
3747
|
+
}
|
|
3748
|
+
continue;
|
|
3749
|
+
}
|
|
3750
|
+
if (line.startsWith("+")) {
|
|
3751
|
+
if (newStartLine === void 0) newStartLine = currentNewLine;
|
|
3752
|
+
newLines.push(line.slice(1));
|
|
3753
|
+
currentNewLine += 1;
|
|
3754
|
+
} else if (line.startsWith("-")) {
|
|
3755
|
+
if (oldStartLine === void 0) oldStartLine = currentOldLine;
|
|
3756
|
+
oldLines.push(line.slice(1));
|
|
3757
|
+
currentOldLine += 1;
|
|
3758
|
+
} else if (line.startsWith(" ")) {
|
|
3759
|
+
const ctx = line.slice(1);
|
|
3760
|
+
oldLines.push(ctx);
|
|
3761
|
+
newLines.push(ctx);
|
|
3762
|
+
currentOldLine += 1;
|
|
3763
|
+
currentNewLine += 1;
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
if (hunkCount > 1) {
|
|
3767
|
+
oldStartLine = void 0;
|
|
3768
|
+
newStartLine = void 0;
|
|
3769
|
+
}
|
|
3770
|
+
if (!filePath) {
|
|
3771
|
+
const searchTargets = [body, header ?? ""];
|
|
3772
|
+
for (const target of searchTargets) {
|
|
3773
|
+
if (!target) continue;
|
|
3774
|
+
const m = target.match(/\[.*?\]\(file:\/\/([^\s\)]+)\)/) || target.match(/file:\/\/([^\s\)]+)/);
|
|
3775
|
+
if (m) {
|
|
3776
|
+
try {
|
|
3777
|
+
filePath = decodeURIComponent(m[1]);
|
|
3778
|
+
} catch {
|
|
3779
|
+
filePath = m[1];
|
|
3780
|
+
}
|
|
3781
|
+
break;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
return {
|
|
3786
|
+
filePath,
|
|
3787
|
+
oldString: oldLines.join("\n"),
|
|
3788
|
+
newString: newLines.join("\n"),
|
|
3789
|
+
oldStartLine,
|
|
3790
|
+
newStartLine,
|
|
3791
|
+
rawDiff
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3544
3794
|
var __testing = {
|
|
3545
3795
|
generateLaunchSessionId,
|
|
3546
3796
|
parseArgs,
|
|
3547
3797
|
parseMaybeJson,
|
|
3548
|
-
getActiveConversationFromCliLog
|
|
3798
|
+
getActiveConversationFromCliLog,
|
|
3799
|
+
parseFencedDiffFromBody
|
|
3549
3800
|
};
|
|
3550
3801
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3551
3802
|
0 && (module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-antigravity-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Control Antigravity CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"bin": {
|