@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.
Files changed (2) hide show
  1. package/dist/server.js +258 -7
  2. 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: state.pendingCall.toolType,
3324
+ tool_name: toolName,
3275
3325
  command: state.pendingCall.command,
3276
- file_path: state.pendingCall.filePath,
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
- this.mobileDeduper.track(session.sessionId, evt.content);
3322
- const result = await this.promptResponder.sendFreeForm(evt.content);
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, evt.content);
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.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": {