paseo-acp-agy 1.2.15 → 1.3.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/README.md CHANGED
@@ -92,24 +92,29 @@ To include `paseo-acp-agy` in Paseo's built-in provider store (`ACP_PROVIDER_CAT
92
92
 
93
93
  ## Plan Usage & Context Window Meter Setup
94
94
 
95
- To display the **Google Antigravity** quota breakdown in Paseo's `Settings -> Usage` (Plan usage) and activate the real-time context-window meter ("bolinha") in the chat composer:
95
+ ### Recommended (Paseo >= 0.11.0): Official Update-Proof Paseo Plugin
96
96
 
97
- ```bash
98
- npx -y paseo-acp-agy setup
99
- ```
97
+ Install the bundled **`antigravity-telemetry`** Paseo plugin once. It registers in `~/.paseo/config.json` and survives all future `Paseo.app`, Web, iPhone, and CLI updates without modifying signed `.app` bundles:
100
98
 
101
- Or if installed globally:
102
99
  ```bash
103
- paseo-acp-agy setup
100
+ paseo plugin install "$(paseo-acp-agy --plugin-path)"
104
101
  ```
105
102
 
106
- Then restart your Paseo app or daemon:
103
+ What this provides across Desktop, Web, and Mobile (iPhone):
104
+ - Native **Google Antigravity** quota gauges in Paseo's Sidebar and `Settings -> Usage` (`5h` window, weekly pool, credits)
105
+ - Live per-agent **Composer Pill** (`$cost · ctx X% · 5h Y%`) with interactive Popover / Mobile Bottom Sheet
106
+ - Dedicated **Antigravity Telemetry** workspace panel, Command Center action, and `/agy-telemetry` slash command
107
+ - Combined with native ACP `usage_update` notifications so Paseo's built-in `ContextWindowMeter` ("bolinha") stays deterministic on every session open, resume, model switch, and turn completion
108
+
109
+ ### Standalone Server Patch (Optional, Linux / Paseo < 0.11.0)
110
+
111
+ For standalone `@getpaseo/cli` servers where you prefer direct server-level patching:
112
+
107
113
  ```bash
114
+ paseo-acp-agy setup
108
115
  paseo daemon restart
109
116
  ```
110
117
 
111
- > **Note**: `paseo-acp-agy` also checks and auto-configures your local Paseo server automatically whenever it launches via `--acp`. Running `setup` manually ensures your existing daemon instance picks up the telemetry hooks immediately upon restart.
112
-
113
118
  ---
114
119
 
115
120
  ## Requirements
@@ -10,6 +10,9 @@ export declare class ACPServer extends EventEmitter {
10
10
  private skipNarration;
11
11
  private rl;
12
12
  private isRunning;
13
+ private pendingRequests;
14
+ private usagePublishTimers;
15
+ private nextRequestId;
13
16
  constructor(options?: {
14
17
  input?: Readable;
15
18
  output?: Writable;
@@ -22,8 +25,17 @@ export declare class ACPServer extends EventEmitter {
22
25
  private sendNotification;
23
26
  private sendSuccess;
24
27
  private sendError;
28
+ sendRequest<T = any>(method: string, params: unknown): Promise<T>;
25
29
  private publishCommands;
26
30
  private publishUsageUpdate;
31
+ private clearUsagePublishTimers;
32
+ /**
33
+ * Schedules coalesced, unref'd usage_update notifications so ACP hosts (like Paseo)
34
+ * deterministically receive context window & session telemetry:
35
+ * - after awaiting newSession/loadSession + applyConfiguredOverrides() + session.subscribe()
36
+ * - after turn_completed overwrites or initializes currentTurnUsage in Paseo 0.10..0.13+
37
+ */
38
+ private scheduleUsagePublishBurst;
27
39
  private sessionState;
28
40
  private requireSession;
29
41
  private validateModel;
@@ -6,7 +6,7 @@ import { SessionManager } from "./session.js";
6
6
  import { executeSlashCommand, AVAILABLE_SLASH_COMMANDS } from "./slash-commands.js";
7
7
  import { getShortVersion } from "./version.js";
8
8
  import { resolveDefaultAgyBinary } from "./antigravity-process.js";
9
- import { loadConversationTranscript } from "./transcript.js";
9
+ import { loadConversationTranscript, estimateReplayContextTokens } from "./transcript.js";
10
10
  import { normalizeToolCall, normalizeToolResult, extractPlanEntries } from "./tool-formatter.js";
11
11
  import { McpBridge } from "./mcp-bridge.js";
12
12
  function splitModelAndEffort(modelInput) {
@@ -29,6 +29,9 @@ export class ACPServer extends EventEmitter {
29
29
  skipNarration;
30
30
  rl = null;
31
31
  isRunning = false;
32
+ pendingRequests = new Map();
33
+ usagePublishTimers = new Map();
34
+ nextRequestId = 1;
32
35
  constructor(options = {}) {
33
36
  super();
34
37
  this.input = options.input || process.stdin;
@@ -98,6 +101,13 @@ export class ACPServer extends EventEmitter {
98
101
  };
99
102
  this.send(response);
100
103
  }
104
+ sendRequest(method, params) {
105
+ return new Promise((resolve, reject) => {
106
+ const id = this.nextRequestId++;
107
+ this.pendingRequests.set(id, { resolve, reject });
108
+ this.send({ jsonrpc: "2.0", id, method, params });
109
+ });
110
+ }
101
111
  publishCommands(sessionId) {
102
112
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
103
113
  sessionId,
@@ -129,6 +139,53 @@ export class ACPServer extends EventEmitter {
129
139
  },
130
140
  });
131
141
  }
142
+ clearUsagePublishTimers(sessionId) {
143
+ if (sessionId !== undefined) {
144
+ const timers = this.usagePublishTimers.get(sessionId);
145
+ if (timers) {
146
+ for (const t of timers)
147
+ clearTimeout(t);
148
+ this.usagePublishTimers.delete(sessionId);
149
+ }
150
+ return;
151
+ }
152
+ for (const timers of this.usagePublishTimers.values()) {
153
+ for (const t of timers)
154
+ clearTimeout(t);
155
+ }
156
+ this.usagePublishTimers.clear();
157
+ }
158
+ /**
159
+ * Schedules coalesced, unref'd usage_update notifications so ACP hosts (like Paseo)
160
+ * deterministically receive context window & session telemetry:
161
+ * - after awaiting newSession/loadSession + applyConfiguredOverrides() + session.subscribe()
162
+ * - after turn_completed overwrites or initializes currentTurnUsage in Paseo 0.10..0.13+
163
+ */
164
+ scheduleUsagePublishBurst(sessionId, delaysMs) {
165
+ this.clearUsagePublishTimers(sessionId);
166
+ if (!this.isRunning || delaysMs.length === 0)
167
+ return;
168
+ const timers = new Set();
169
+ this.usagePublishTimers.set(sessionId, timers);
170
+ for (const delayMs of delaysMs) {
171
+ const timer = setTimeout(() => {
172
+ timers.delete(timer);
173
+ if (timers.size === 0) {
174
+ this.usagePublishTimers.delete(sessionId);
175
+ }
176
+ if (!this.isRunning)
177
+ return;
178
+ const current = this.sessionManager.getSession(sessionId);
179
+ if (current) {
180
+ this.publishUsageUpdate(current);
181
+ }
182
+ }, delayMs);
183
+ if (typeof timer.unref === "function") {
184
+ timer.unref();
185
+ }
186
+ timers.add(timer);
187
+ }
188
+ }
132
189
  async sessionState(session, forceModels = false) {
133
190
  const availableModels = await fetchAvailableModels(this.binaryPath, forceModels);
134
191
  return {
@@ -198,6 +255,20 @@ export class ACPServer extends EventEmitter {
198
255
  const isNotification = id === undefined || id === null;
199
256
  const params = (parsed.params || {});
200
257
  logger.debug("Received ACP message", { method, isNotification, id });
258
+ // Handle responses to requests initiated by the server (e.g. session/request_permission)
259
+ if (id !== undefined && !method) {
260
+ const pending = this.pendingRequests.get(id);
261
+ if (pending) {
262
+ this.pendingRequests.delete(id);
263
+ if (parsed.error) {
264
+ pending.reject(parsed.error);
265
+ }
266
+ else {
267
+ pending.resolve(parsed.result);
268
+ }
269
+ return;
270
+ }
271
+ }
201
272
  try {
202
273
  switch (method) {
203
274
  case ACP_METHODS.INITIALIZE: {
@@ -242,17 +313,9 @@ export class ACPServer extends EventEmitter {
242
313
  this.sendSuccess(id, await this.sessionState(session, true));
243
314
  this.publishCommands(session.id);
244
315
  this.publishUsageUpdate(session);
245
- // Re-publish usage after ACPAgent completes awaiting newSession and stores this.sessionId
246
- setTimeout(() => {
247
- if (this.sessionManager.getSession(session.id)) {
248
- this.publishUsageUpdate(session);
249
- }
250
- }, 150);
251
- setTimeout(() => {
252
- if (this.sessionManager.getSession(session.id)) {
253
- this.publishUsageUpdate(session);
254
- }
255
- }, 500);
316
+ // Re-publish usage across startup window so ACPAgent receives it after
317
+ // awaiting newSession + applyConfiguredOverrides() + session.subscribe()
318
+ this.scheduleUsagePublishBurst(session.id, [120, 450, 1200, 2800]);
256
319
  }
257
320
  break;
258
321
  }
@@ -288,6 +351,9 @@ export class ACPServer extends EventEmitter {
288
351
  session.setTitle(firstUser.text.slice(0, 80).replace(/\r?\n/g, " ").trim());
289
352
  }
290
353
  }
354
+ if (replayItems.length > 0) {
355
+ session.seedEstimatedContextUsage(estimateReplayContextTokens(replayItems));
356
+ }
291
357
  for (const item of replayItems) {
292
358
  switch (item.type) {
293
359
  case "user_message":
@@ -355,12 +421,7 @@ export class ACPServer extends EventEmitter {
355
421
  this.sendSuccess(id, await this.sessionState(session));
356
422
  this.publishCommands(session.id);
357
423
  this.publishUsageUpdate(session);
358
- const activeSession = session;
359
- setTimeout(() => {
360
- if (this.sessionManager.getSession(activeSession.id)) {
361
- this.publishUsageUpdate(activeSession);
362
- }
363
- }, 150);
424
+ this.scheduleUsagePublishBurst(session.id, [120, 450, 1200, 2800]);
364
425
  }
365
426
  }
366
427
  catch (err) {
@@ -391,10 +452,23 @@ export class ACPServer extends EventEmitter {
391
452
  created = true;
392
453
  }
393
454
  await session.ensureReadyForResume();
455
+ const resumeConversationId = session.process.currentConversationId;
456
+ if (resumeConversationId &&
457
+ session.usage.contextWindowUsedTokens === 0 &&
458
+ session.usage.totalTokens === 0) {
459
+ try {
460
+ const replayItems = await loadConversationTranscript(resumeConversationId);
461
+ if (replayItems.length > 0) {
462
+ session.seedEstimatedContextUsage(estimateReplayContextTokens(replayItems));
463
+ }
464
+ }
465
+ catch { }
466
+ }
394
467
  if (!isNotification) {
395
468
  this.sendSuccess(id, await this.sessionState(session));
396
469
  this.publishCommands(session.id);
397
470
  this.publishUsageUpdate(session);
471
+ this.scheduleUsagePublishBurst(session.id, [120, 450, 1200, 2800]);
398
472
  }
399
473
  }
400
474
  catch (err) {
@@ -435,6 +509,17 @@ export class ACPServer extends EventEmitter {
435
509
  mode: sourceSession?.mode,
436
510
  permission: sourceSession?.permission,
437
511
  conversationId: sourceSession?.process.currentConversationId,
512
+ usage: sourceSession
513
+ ? {
514
+ inputTokens: 0,
515
+ outputTokens: 0,
516
+ cachedInputTokens: 0,
517
+ totalTokens: 0,
518
+ totalCostUsd: 0,
519
+ contextWindowUsedTokens: sourceSession.usage.contextWindowUsedTokens,
520
+ contextWindowMaxTokens: sourceSession.usage.contextWindowMaxTokens,
521
+ }
522
+ : undefined,
438
523
  binaryPath: this.binaryPath,
439
524
  });
440
525
  if (sourceSession?.title) {
@@ -444,6 +529,7 @@ export class ACPServer extends EventEmitter {
444
529
  this.sendSuccess(id, await this.sessionState(newSession));
445
530
  this.publishCommands(newSession.id);
446
531
  this.publishUsageUpdate(newSession);
532
+ this.scheduleUsagePublishBurst(newSession.id, [120, 450, 1200]);
447
533
  }
448
534
  break;
449
535
  }
@@ -494,8 +580,11 @@ export class ACPServer extends EventEmitter {
494
580
  },
495
581
  });
496
582
  }
497
- if (!isNotification)
583
+ this.publishUsageUpdate(session);
584
+ if (!isNotification) {
498
585
  this.sendSuccess(id, { stopReason: "end_turn" });
586
+ this.scheduleUsagePublishBurst(session.id, [25, 150]);
587
+ }
499
588
  break;
500
589
  }
501
590
  let hasSentNonNarration = false;
@@ -559,13 +648,70 @@ export class ACPServer extends EventEmitter {
559
648
  rawInput: toolCall.rawInput,
560
649
  },
561
650
  });
562
- if (toolCall.questionCard) {
563
- this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
651
+ if (toolName === "ask_question") {
652
+ const rawParams = step.tool_info.parameters || {};
653
+ const rawQuestions = Array.isArray(rawParams.questions)
654
+ ? rawParams.questions
655
+ : [];
656
+ const options = [];
657
+ for (let qIdx = 0; qIdx < rawQuestions.length; qIdx++) {
658
+ const q = rawQuestions[qIdx];
659
+ const opts = Array.isArray(q.options) ? q.options : [];
660
+ for (let optIdx = 0; optIdx < opts.length; optIdx++) {
661
+ const opt = opts[optIdx];
662
+ const clean = typeof opt === "string" ? opt.replace(/^\(Recommended\)\s*/i, "").trim() : String(opt);
663
+ const isRec = typeof opt === "string" && (opt.includes("(Recommended)") || opt.includes("(Recomendado)"));
664
+ options.push({
665
+ optionId: `q${qIdx + 1}_opt${optIdx + 1}`,
666
+ name: `${clean}${isRec ? " (Recomendado)" : ""}`,
667
+ kind: "allow_once",
668
+ });
669
+ }
670
+ }
671
+ options.push({
672
+ optionId: "cancel",
673
+ name: "Cancelar",
674
+ kind: "reject_once",
675
+ });
676
+ const promptText = rawQuestions.map((q) => q.question).filter(Boolean).join(" / ") || "Pergunta do Assistente";
677
+ this.sendRequest("session/request_permission", {
564
678
  sessionId: session.id,
565
- update: {
566
- sessionUpdate: "agent_message_chunk",
567
- content: { type: "text", text: `\n\n${toolCall.questionCard}\n\n` },
679
+ toolCall: {
680
+ toolCallId: toolCall.toolCallId,
681
+ title: toolCall.title,
682
+ kind: "question",
683
+ content: [{ type: "text", text: promptText }],
684
+ rawInput: toolCall.rawInput,
568
685
  },
686
+ options,
687
+ questions: rawQuestions,
688
+ }).then((permRes) => {
689
+ logger.info("Interactive question answered in Paseo", { permRes });
690
+ const outcome = permRes?.outcome;
691
+ let answerText = "";
692
+ if (outcome?._meta?.answers && typeof outcome._meta.answers === "object") {
693
+ answerText = Object.entries(outcome._meta.answers)
694
+ .map(([k, v]) => `• **${k}**: ${v}`)
695
+ .join("\n");
696
+ }
697
+ else if (outcome?.optionId && outcome.optionId !== "cancel") {
698
+ const matched = options.find((o) => o.optionId === outcome.optionId);
699
+ answerText = matched ? matched.name : outcome.optionId;
700
+ }
701
+ if (answerText) {
702
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
703
+ sessionId: session.id,
704
+ update: {
705
+ sessionUpdate: "agent_message_chunk",
706
+ content: {
707
+ type: "text",
708
+ text: `\n\n> 💡 **Opção Selecionada:**\n${answerText}\n\n`,
709
+ },
710
+ },
711
+ });
712
+ }
713
+ }).catch((err) => {
714
+ logger.debug("Interactive question cancelled or dismissed", { error: err.message });
569
715
  });
570
716
  }
571
717
  }
@@ -674,6 +820,7 @@ export class ACPServer extends EventEmitter {
674
820
  if (session.isCancelled) {
675
821
  if (!isNotification) {
676
822
  this.sendSuccess(id, { stopReason: "cancelled", usage: usagePayload });
823
+ this.scheduleUsagePublishBurst(session.id, [25, 150]);
677
824
  }
678
825
  }
679
826
  else if (resultEvent.result?.status === "ERROR") {
@@ -688,10 +835,12 @@ export class ACPServer extends EventEmitter {
688
835
  });
689
836
  if (!isNotification) {
690
837
  this.sendSuccess(id, { stopReason: "refusal", usage: usagePayload });
838
+ this.scheduleUsagePublishBurst(session.id, [25, 150]);
691
839
  }
692
840
  }
693
841
  else if (!isNotification) {
694
842
  this.sendSuccess(id, { stopReason: "end_turn", usage: usagePayload });
843
+ this.scheduleUsagePublishBurst(session.id, [25, 150]);
695
844
  }
696
845
  }
697
846
  catch (promptErr) {
@@ -699,8 +848,10 @@ export class ACPServer extends EventEmitter {
699
848
  if (session.isCancelled ||
700
849
  errorMsg.includes("cancelled") ||
701
850
  errorMsg.includes("Turn was cancelled")) {
702
- if (!isNotification)
851
+ if (!isNotification) {
703
852
  this.sendSuccess(id, { stopReason: "cancelled" });
853
+ this.scheduleUsagePublishBurst(session.id, [25, 150]);
854
+ }
704
855
  }
705
856
  else {
706
857
  logger.error("Error executing prompt turn", { error: errorMsg, sessionId });
@@ -711,8 +862,10 @@ export class ACPServer extends EventEmitter {
711
862
  content: { type: "text", text: `\n\n**Error:** ${errorMsg}\n` },
712
863
  },
713
864
  });
714
- if (!isNotification)
865
+ if (!isNotification) {
715
866
  this.sendSuccess(id, { stopReason: "refusal" });
867
+ this.scheduleUsagePublishBurst(session.id, [25, 150]);
868
+ }
716
869
  }
717
870
  }
718
871
  }
@@ -736,6 +889,7 @@ export class ACPServer extends EventEmitter {
736
889
  case ACP_METHODS.SESSION_CLOSE:
737
890
  case ACP_METHODS.SESSION_CLOSE_ALIAS: {
738
891
  const sessionId = String(params.sessionId || "");
892
+ this.clearUsagePublishTimers(sessionId);
739
893
  await this.sessionManager.closeSession(sessionId);
740
894
  if (!isNotification)
741
895
  this.sendSuccess(id, {});
@@ -758,8 +912,11 @@ export class ACPServer extends EventEmitter {
758
912
  break;
759
913
  }
760
914
  session.setMode(modeId);
761
- if (!isNotification)
915
+ if (!isNotification) {
762
916
  this.sendSuccess(id, {});
917
+ this.publishUsageUpdate(session);
918
+ this.scheduleUsagePublishBurst(session.id, [80, 350, 1000]);
919
+ }
763
920
  break;
764
921
  }
765
922
  case ACP_METHODS.SESSION_SET_MODEL:
@@ -801,8 +958,11 @@ export class ACPServer extends EventEmitter {
801
958
  currentPermission: session.permission,
802
959
  availableModels: models,
803
960
  });
804
- if (!isNotification)
961
+ if (!isNotification) {
805
962
  this.sendSuccess(id, { configOptions });
963
+ this.publishUsageUpdate(session);
964
+ this.scheduleUsagePublishBurst(session.id, [80, 350, 1000]);
965
+ }
806
966
  break;
807
967
  }
808
968
  case ACP_METHODS.SESSION_SET_CONFIG_OPTION:
@@ -869,8 +1029,11 @@ export class ACPServer extends EventEmitter {
869
1029
  currentPermission: session.permission,
870
1030
  availableModels: models,
871
1031
  });
872
- if (!isNotification)
1032
+ if (!isNotification) {
873
1033
  this.sendSuccess(id, { configOptions });
1034
+ this.publishUsageUpdate(session);
1035
+ this.scheduleUsagePublishBurst(session.id, [80, 350, 1000]);
1036
+ }
874
1037
  break;
875
1038
  }
876
1039
  case "provider/usage":
@@ -897,6 +1060,7 @@ export class ACPServer extends EventEmitter {
897
1060
  if (!this.isRunning)
898
1061
  return;
899
1062
  this.isRunning = false;
1063
+ this.clearUsagePublishTimers();
900
1064
  if (this.rl) {
901
1065
  this.rl.close();
902
1066
  this.rl = null;
@@ -135,9 +135,20 @@ export function resolveDefaultAgyBinary(force = false) {
135
135
  }
136
136
  }
137
137
  else {
138
- const localPath = path.join(home, ".local", "bin", "agy");
139
- if (fs.existsSync(localPath))
140
- resolved = localPath;
138
+ const posixCandidates = [
139
+ path.join(home, ".local", "bin", "agy"),
140
+ "/opt/homebrew/bin/agy",
141
+ "/usr/local/bin/agy",
142
+ "/usr/bin/agy",
143
+ path.join(home, ".antigravity", "bin", "agy"),
144
+ path.join(home, ".gemini", "antigravity-cli", "bin", "agy"),
145
+ ];
146
+ for (const cand of posixCandidates) {
147
+ if (fs.existsSync(cand)) {
148
+ resolved = cand;
149
+ break;
150
+ }
151
+ }
141
152
  }
142
153
  }
143
154
  }
package/dist/asar.d.ts CHANGED
@@ -21,6 +21,11 @@ export declare function readArchiveHeader(archivePath: string): {
21
21
  * Lists all file paths inside an ASAR archive.
22
22
  */
23
23
  export declare function listPackage(archivePath: string): string[];
24
+ /**
25
+ * Reads a single UTF-8 text file directly from an ASAR archive without extracting
26
+ * the entire archive to disk. Returns null if the archive or entry cannot be read.
27
+ */
28
+ export declare function readFileFromPackage(archivePath: string, relPath: string): string | null;
24
29
  /**
25
30
  * Pure TypeScript, zero-dependency ASAR archive extractor.
26
31
  * Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
package/dist/asar.js CHANGED
@@ -54,6 +54,40 @@ export function listPackage(archivePath) {
54
54
  walk(header, "");
55
55
  return result;
56
56
  }
57
+ /**
58
+ * Reads a single UTF-8 text file directly from an ASAR archive without extracting
59
+ * the entire archive to disk. Returns null if the archive or entry cannot be read.
60
+ */
61
+ export function readFileFromPackage(archivePath, relPath) {
62
+ try {
63
+ const { header, dataBaseOffset } = readArchiveHeader(archivePath);
64
+ const parts = relPath.replace(/^\/+/, "").split("/").filter(Boolean);
65
+ let node = header;
66
+ for (const part of parts) {
67
+ node = node?.files?.[part];
68
+ if (!node)
69
+ return null;
70
+ }
71
+ if (node.unpacked || node.size === undefined || node.offset === undefined) {
72
+ return null;
73
+ }
74
+ if (node.size === 0)
75
+ return "";
76
+ const fileOffset = dataBaseOffset + parseInt(node.offset, 10);
77
+ const fd = fs.openSync(archivePath, "r");
78
+ try {
79
+ const buf = Buffer.alloc(node.size);
80
+ const bytesRead = fs.readSync(fd, buf, 0, node.size, fileOffset);
81
+ return buf.subarray(0, bytesRead).toString("utf8");
82
+ }
83
+ finally {
84
+ fs.closeSync(fd);
85
+ }
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
57
91
  /**
58
92
  * Pure TypeScript, zero-dependency ASAR archive extractor.
59
93
  * Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { execFileSync } from "node:child_process";
4
6
  import { ACPServer } from "./acp-server.js";
5
7
  import { logger } from "./logger.js";
@@ -8,6 +10,11 @@ import { ensurePaseoIntegration, restorePaseoIntegration, findPaseoServerInstall
8
10
  import { resolveDefaultAgyBinary, isWindowsBatchScript } from "./antigravity-process.js";
9
11
  import { parseAgyQuotaOutput } from "./protocol.js";
10
12
  const args = process.argv.slice(2);
13
+ const bundledPluginDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "paseo-plugin");
14
+ if (args.includes("plugin-path") || args.includes("--plugin-path")) {
15
+ process.stdout.write(`${bundledPluginDir}\n`);
16
+ process.exit(0);
17
+ }
11
18
  if (args.includes("--version") || args.includes("-v")) {
12
19
  if (args.includes("--json")) {
13
20
  process.stdout.write(JSON.stringify(resolveBuildMetadata(), null, 2) + "\n");
@@ -31,12 +38,14 @@ Commands:
31
38
  setup, patch Configure and integrate Antigravity telemetry with Paseo
32
39
  doctor Diagnose Antigravity binary, quota provider, and Paseo status
33
40
  restore Restore original Paseo files from backup (.bak)
41
+ plugin-path Print path to the update-proof Paseo telemetry plugin directory
34
42
 
35
43
  Options:
36
44
  --acp Start ACP server over stdio (default)
37
45
  --setup Integrate Antigravity with local Paseo server installation
38
46
  --doctor Run environment, binary, and telemetry diagnostics
39
47
  --restore Restore original Paseo files from backup
48
+ --plugin-path Print bundled Paseo plugin directory for 'paseo plugin install'
40
49
  --force-macos-app Force patching inside macOS .app bundle (re-signs with ad-hoc)
41
50
  --skip-naration Drop leading narration-only assistant chunks (e.g. 'I will...')
42
51
  --skip-narration Alias for --skip-naration
@@ -139,6 +148,22 @@ if (args.includes("doctor") || args.includes("--doctor")) {
139
148
  process.stdout.write("\n3. Paseo Installation Targets:\n");
140
149
  const serverPaths = findPaseoServerInstallations();
141
150
  const asarPaths = findPaseoAsarPaths();
151
+ let telemetryPluginEnabled = false;
152
+ try {
153
+ const paseoHome = process.env.PASEO_HOME || path.join(process.env.HOME || process.env.USERPROFILE || "", ".paseo");
154
+ const cfgPath = path.join(paseoHome, "config.json");
155
+ if (fs.existsSync(cfgPath)) {
156
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf-8"));
157
+ if (cfg?.pluginsEnabled === true && cfg?.plugins) {
158
+ const p = cfg.plugins["antigravity-telemetry"];
159
+ if (p && p.enabled !== false) {
160
+ telemetryPluginEnabled = true;
161
+ }
162
+ }
163
+ }
164
+ }
165
+ catch { }
166
+ process.stdout.write(` - [Plugin] antigravity-telemetry -> ${telemetryPluginEnabled ? "[ENABLED]" : "[NOT INSTALLED]"}\n`);
142
167
  const totalFound = serverPaths.length + asarPaths.length;
143
168
  if (totalFound === 0) {
144
169
  process.stdout.write(" [!] No Paseo server or desktop installations discovered in standard paths.\n");
@@ -185,21 +210,22 @@ if (args.includes("doctor") || args.includes("--doctor")) {
185
210
  process.stdout.write(" - Antigravity CLI ('agy') executable was not found.\n");
186
211
  process.stdout.write(" Action: Install Antigravity CLI or set AGY_BIN_PATH environment variable.\n");
187
212
  }
188
- if (totalFound === 0) {
213
+ if (totalFound === 0 && !telemetryPluginEnabled) {
189
214
  hasIssue = true;
190
215
  process.stdout.write(" - No Paseo installations found.\n");
191
216
  process.stdout.write(" Action: Set PASEO_SERVER_PATH or PASEO_ASAR_PATH to your Paseo install directory if located elsewhere.\n");
192
217
  }
193
- else if (unpatchedCount > 0) {
218
+ else if (unpatchedCount > 0 && !telemetryPluginEnabled) {
194
219
  hasIssue = true;
195
- if (paseoRunning) {
196
- process.stdout.write(` - Found ${unpatchedCount} unpatched Paseo target(s), but Paseo is currently running.\n`);
197
- process.stdout.write(" Action: Close Paseo completely (Stop-Process -Name \"Paseo\" -Force), then run:\n");
220
+ process.stdout.write(` - Found ${unpatchedCount} unpatched Paseo target(s) and 'antigravity-telemetry' plugin is not yet installed.\n`);
221
+ process.stdout.write(" Recommended update-proof action (survives Paseo updates without touching .app signatures):\n");
222
+ process.stdout.write(` paseo plugin install "${bundledPluginDir}"\n`);
223
+ if (isWin) {
224
+ process.stdout.write(" Or to patch standalone Paseo after closing it (Stop-Process -Name \"Paseo\" -Force):\n");
198
225
  process.stdout.write(" npx -y paseo-acp-agy setup\n");
199
226
  }
200
227
  else {
201
- process.stdout.write(` - Found ${unpatchedCount} unpatched Paseo target(s).\n`);
202
- process.stdout.write(" Action: Run setup to patch Paseo for Antigravity telemetry:\n");
228
+ process.stdout.write(" Or for standalone @getpaseo/cli servers:\n");
203
229
  process.stdout.write(" npx -y paseo-acp-agy setup\n");
204
230
  }
205
231
  }
@@ -272,8 +298,9 @@ if (args.includes("setup") ||
272
298
  process.stdout.write(`\n🛡️ Preserved macOS Code Signature:\n` +
273
299
  ` Skipped ${res.skippedPaths.length} target(s) inside macOS .app bundle to protect official Apple Developer ID signature\n` +
274
300
  ` and prevent Gatekeeper "damaged app" warnings.\n` +
275
- ` (Antigravity ACP runs standalone and does not require modifying Paseo.app).\n` +
276
- ` If you specifically wish to patch the .app bundle, run with: paseo-acp-agy patch --force-macos-app\n\n`);
301
+ ` Recommended update-proof telemetry setup (survives every Paseo update):\n` +
302
+ ` paseo plugin install "${bundledPluginDir}"\n` +
303
+ ` (Or if you specifically wish to patch the .app bundle: paseo-acp-agy patch --force-macos-app)\n\n`);
277
304
  }
278
305
  if (isPaseoRunning()) {
279
306
  const isWin = process.platform === "win32";
@@ -58,7 +58,17 @@ export declare function ensurePaseoIntegration(options?: {
58
58
  forceMacosApp?: boolean;
59
59
  }): Promise<PatchResult>;
60
60
  /**
61
- * Restores original unpatched Paseo files from backup (.bak) archives.
61
+ * Reads the Paseo version embedded inside an ASAR archive's package.json.
62
+ */
63
+ export declare function resolveAsarArchiveVersion(archivePath: string): string | null;
64
+ /**
65
+ * Resolves the authoritative Paseo version of a target app.asar installation,
66
+ * preferring macOS bundle Info.plist (CFBundleShortVersionString) when inside an .app bundle.
67
+ */
68
+ export declare function resolveTargetPaseoVersion(asarPath: string): string | null;
69
+ /**
70
+ * Restores original unpatched Paseo files from backup (.bak) archives,
71
+ * enforcing strict version matching so an older backup can never downgrade a newer Paseo app.
62
72
  */
63
73
  export declare function restorePaseoIntegration(): Promise<{
64
74
  restored: string[];