paseo-acp-agy 1.2.12 → 1.2.14

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.
@@ -3,6 +3,7 @@ import { EventEmitter } from "node:events";
3
3
  import { SessionManager } from "./session.js";
4
4
  export declare class ACPServer extends EventEmitter {
5
5
  private sessionManager;
6
+ private mcpBridge;
6
7
  private input;
7
8
  private output;
8
9
  private binaryPath;
@@ -1,12 +1,14 @@
1
1
  import readline from "node:readline";
2
2
  import { EventEmitter } from "node:events";
3
3
  import { logger } from "./logger.js";
4
- import { ACP_METHODS, AVAILABLE_MODES, AVAILABLE_PERMISSIONS, fetchAvailableModels, buildConfigOptionsForSession, extractPromptText, mapToolNameToKind, extractImageMarkdownLink, isNarrationText, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
4
+ import { ACP_METHODS, AVAILABLE_MODES, AVAILABLE_PERMISSIONS, fetchAvailableModels, buildConfigOptionsForSession, extractPromptText, extractImageMarkdownLink, isNarrationText, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
5
5
  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
9
  import { loadConversationTranscript } from "./transcript.js";
10
+ import { normalizeToolCall, normalizeToolResult, extractPlanEntries } from "./tool-formatter.js";
11
+ import { McpBridge } from "./mcp-bridge.js";
10
12
  function splitModelAndEffort(modelInput) {
11
13
  if (!modelInput)
12
14
  return {};
@@ -20,6 +22,7 @@ function splitModelAndEffort(modelInput) {
20
22
  }
21
23
  export class ACPServer extends EventEmitter {
22
24
  sessionManager;
25
+ mcpBridge;
23
26
  input;
24
27
  output;
25
28
  binaryPath;
@@ -31,6 +34,7 @@ export class ACPServer extends EventEmitter {
31
34
  this.input = options.input || process.stdin;
32
35
  this.output = options.output || process.stdout;
33
36
  this.binaryPath = options.binaryPath || resolveDefaultAgyBinary();
37
+ this.mcpBridge = new McpBridge(this.binaryPath);
34
38
  this.skipNarration =
35
39
  options.skipNarration ?? (process.env.AGY_ACP_SKIP_NARRATION === "true");
36
40
  this.sessionManager =
@@ -223,6 +227,10 @@ export class ACPServer extends EventEmitter {
223
227
  const requestedModel = typeof params.model === "string" ? params.model : undefined;
224
228
  const parsedModel = splitModelAndEffort(requestedModel);
225
229
  const mode = typeof params.mode === "string" ? params.mode : undefined;
230
+ const mcpServers = params.mcpServers;
231
+ if (mcpServers) {
232
+ void this.mcpBridge.syncServers(mcpServers);
233
+ }
226
234
  const session = this.sessionManager.createSession({
227
235
  cwd,
228
236
  model: parsedModel.model,
@@ -257,6 +265,10 @@ export class ACPServer extends EventEmitter {
257
265
  this.sendError(id, -32602, "sessionId is required");
258
266
  break;
259
267
  }
268
+ const mcpServers = params.mcpServers;
269
+ if (mcpServers) {
270
+ void this.mcpBridge.syncServers(mcpServers);
271
+ }
260
272
  let session = this.sessionManager.getSession(sessionId);
261
273
  try {
262
274
  if (!session) {
@@ -509,6 +521,17 @@ export class ACPServer extends EventEmitter {
509
521
  content: { type: "text", text: step.text_delta },
510
522
  },
511
523
  });
524
+ // Live Plan / Todo checklist tracker
525
+ const planEntries = extractPlanEntries(step.text_delta);
526
+ if (planEntries) {
527
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
528
+ sessionId: session.id,
529
+ update: {
530
+ sessionUpdate: "plan",
531
+ entries: planEntries,
532
+ },
533
+ });
534
+ }
512
535
  }
513
536
  else if (step.step_type === "thought" && step.text_delta) {
514
537
  hasSentNonNarration = true;
@@ -522,29 +545,40 @@ export class ACPServer extends EventEmitter {
522
545
  }
523
546
  else if (step.step_type === "tool" && step.tool_info) {
524
547
  hasSentNonNarration = true;
525
- const toolCallId = `tool_${step.step_index}`;
526
548
  const toolName = step.tool_name || step.tool_info.name || "tool";
527
- const toolKind = mapToolNameToKind(toolName);
549
+ const toolCall = normalizeToolCall(toolName, step.tool_info.parameters || {}, step.step_index);
528
550
  if (step.state === "ACTIVE") {
529
551
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
530
552
  sessionId: session.id,
531
553
  update: {
532
554
  sessionUpdate: "tool_call",
533
- toolCallId,
534
- title: toolName,
535
- kind: toolKind,
536
- rawInput: step.tool_info.parameters || {},
555
+ toolCallId: toolCall.toolCallId,
556
+ title: toolCall.title,
557
+ kind: toolCall.kind,
558
+ ...(toolCall.locations ? { locations: toolCall.locations } : {}),
559
+ rawInput: toolCall.rawInput,
537
560
  },
538
561
  });
562
+ if (toolCall.questionCard) {
563
+ this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
564
+ sessionId: session.id,
565
+ update: {
566
+ sessionUpdate: "agent_message_chunk",
567
+ content: { type: "text", text: `\n\n${toolCall.questionCard}\n\n` },
568
+ },
569
+ });
570
+ }
539
571
  }
540
572
  else if (step.state === "DONE") {
573
+ const toolRes = normalizeToolResult(toolName, step.tool_info.parameters || {}, step.tool_info.output);
541
574
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
542
575
  sessionId: session.id,
543
576
  update: {
544
577
  sessionUpdate: "tool_call_update",
545
- toolCallId,
546
- status: "completed",
547
- rawOutput: step.tool_info.output ?? "",
578
+ toolCallId: toolCall.toolCallId,
579
+ status: toolRes.status,
580
+ rawOutput: toolRes.rawOutput,
581
+ ...(toolRes.content ? { content: toolRes.content } : {}),
548
582
  },
549
583
  });
550
584
  const imageMarkdown = extractImageMarkdownLink(toolName, step.tool_info.output, step.tool_info.parameters);
@@ -559,13 +593,14 @@ export class ACPServer extends EventEmitter {
559
593
  }
560
594
  }
561
595
  else if (step.state === "ERROR") {
596
+ const toolRes = normalizeToolResult(toolName, step.tool_info.parameters || {}, null, step.tool_info.error || { message: "Tool execution error" });
562
597
  this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
563
598
  sessionId: session.id,
564
599
  update: {
565
600
  sessionUpdate: "tool_call_update",
566
- toolCallId,
601
+ toolCallId: toolCall.toolCallId,
567
602
  status: "failed",
568
- rawOutput: step.tool_info.error?.message || "Tool execution error",
603
+ rawOutput: toolRes.rawOutput,
569
604
  },
570
605
  });
571
606
  }
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { execFileSync } from "node:child_process";
4
4
  import { ACPServer } from "./acp-server.js";
5
5
  import { logger } from "./logger.js";
6
6
  import { formatDiagnosticVersion, resolveBuildMetadata } from "./version.js";
7
- import { ensurePaseoIntegration, findPaseoServerInstallations, findPaseoAsarPaths, isPaseoServerPatched, isPaseoAsarPatched, isPaseoRunning, } from "./paseo-patcher.js";
7
+ import { ensurePaseoIntegration, restorePaseoIntegration, findPaseoServerInstallations, findPaseoAsarPaths, isPaseoServerPatched, isPaseoAsarPatched, isPaseoRunning, } from "./paseo-patcher.js";
8
8
  import { resolveDefaultAgyBinary, isWindowsBatchScript } from "./antigravity-process.js";
9
9
  import { parseAgyQuotaOutput } from "./protocol.js";
10
10
  const args = process.argv.slice(2);
@@ -30,11 +30,14 @@ Usage:
30
30
  Commands:
31
31
  setup, patch Configure and integrate Antigravity telemetry with Paseo
32
32
  doctor Diagnose Antigravity binary, quota provider, and Paseo status
33
+ restore Restore original Paseo files from backup (.bak)
33
34
 
34
35
  Options:
35
36
  --acp Start ACP server over stdio (default)
36
37
  --setup Integrate Antigravity with local Paseo server installation
37
38
  --doctor Run environment, binary, and telemetry diagnostics
39
+ --restore Restore original Paseo files from backup
40
+ --force-macos-app Force patching inside macOS .app bundle (re-signs with ad-hoc)
38
41
  --skip-naration Drop leading narration-only assistant chunks (e.g. 'I will...')
39
42
  --skip-narration Alias for --skip-naration
40
43
  -v, --version Show version
@@ -212,6 +215,28 @@ if (args.includes("doctor") || args.includes("--doctor")) {
212
215
  }
213
216
  process.exit(0);
214
217
  }
218
+ if (args.includes("restore") ||
219
+ args.includes("unpatch") ||
220
+ args.includes("--restore") ||
221
+ args.includes("--unpatch")) {
222
+ process.stdout.write("Restoring original Paseo files from backup...\n");
223
+ try {
224
+ const res = await restorePaseoIntegration();
225
+ if (res.restored.length > 0) {
226
+ process.stdout.write(`Successfully restored original files:\n${res.restored.map((r) => ` - ${r}`).join("\n")}\n\n`);
227
+ }
228
+ else {
229
+ process.stdout.write("No backup archives (.bak) found to restore.\n\n");
230
+ }
231
+ if (res.errors.length > 0) {
232
+ process.stderr.write(`Errors during restore:\n${res.errors.map((e) => ` - ${e}`).join("\n")}\n`);
233
+ }
234
+ }
235
+ catch (err) {
236
+ process.stderr.write(`Restore encountered an issue: ${err instanceof Error ? err.message : String(err)}\n`);
237
+ }
238
+ process.exit(0);
239
+ }
215
240
  if (args.includes("setup") ||
216
241
  args.includes("patch") ||
217
242
  args.includes("--setup") ||
@@ -221,22 +246,35 @@ if (args.includes("setup") ||
221
246
  }
222
247
  process.stdout.write("Checking Paseo installation and configuring Antigravity telemetry...\n");
223
248
  try {
224
- const res = await ensurePaseoIntegration({ verbose: true });
249
+ const forceMacosApp = args.includes("--force-macos-app");
250
+ const res = await ensurePaseoIntegration({ verbose: true, forceMacosApp });
225
251
  if (!res.found) {
226
252
  process.stdout.write("Notice: No active @getpaseo/server installation or app.asar found in standard paths.\n" +
227
253
  "If Paseo is installed in a custom directory, set PASEO_SERVER_PATH or PASEO_ASAR_PATH and run setup again.\n");
228
254
  }
229
255
  else {
230
- const totalFound = res.serverPaths.length + (res.asarPaths?.length || 0);
231
- process.stdout.write(`Found ${totalFound} Paseo installation target(s) (${res.serverPaths.length} server dir(s), ${res.asarPaths?.length || 0} asar package(s)).\n`);
256
+ const totalFound = res.serverPaths.length + (res.asarPaths?.length || 0) + (res.skippedPaths?.length || 0);
257
+ process.stdout.write(`Found ${totalFound} Paseo installation target(s) (${res.serverPaths.length} server dir(s), ${res.asarPaths?.length || 0} asar package(s)` +
258
+ (res.skippedPaths && res.skippedPaths.length > 0 ? `, ${res.skippedPaths.length} protected macOS app bundle(s)` : "") +
259
+ `).\n`);
232
260
  const allPatched = [...res.patchedPaths, ...(res.patchedAsarPaths || [])];
233
261
  if (allPatched.length > 0) {
234
262
  process.stdout.write(`Successfully integrated with: \n${allPatched.map((p) => ` - ${p}`).join("\n")}\n\n` +
235
263
  `Antigravity quota provider and context-window telemetry are now enabled!\n`);
236
264
  }
265
+ else if (res.skippedPaths && res.skippedPaths.length > 0) {
266
+ process.stdout.write("Paseo server is ready. macOS .app bundle signature was preserved.\n");
267
+ }
237
268
  else {
238
269
  process.stdout.write("Paseo is already up-to-date and configured for Antigravity telemetry.\n");
239
270
  }
271
+ if (res.skippedPaths && res.skippedPaths.length > 0) {
272
+ process.stdout.write(`\n🛡️ Preserved macOS Code Signature:\n` +
273
+ ` Skipped ${res.skippedPaths.length} target(s) inside macOS .app bundle to protect official Apple Developer ID signature\n` +
274
+ ` 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`);
277
+ }
240
278
  if (isPaseoRunning()) {
241
279
  const isWin = process.platform === "win32";
242
280
  process.stdout.write(`\n⚠️ IMPORTANT: Paseo is currently running in the background!\n` +
@@ -0,0 +1,27 @@
1
+ export interface McpServerStdioConfig {
2
+ type: "stdio";
3
+ command: string;
4
+ args?: string[];
5
+ env?: Record<string, string>;
6
+ alwaysLoad?: boolean;
7
+ }
8
+ export interface McpServerHttpConfig {
9
+ type: "http" | "sse";
10
+ url: string;
11
+ headers?: Record<string, string>;
12
+ alwaysLoad?: boolean;
13
+ }
14
+ export type McpServerConfig = McpServerStdioConfig | McpServerHttpConfig;
15
+ /**
16
+ * Bridges Paseo session MCP servers into Google Antigravity CLI's MCP system.
17
+ */
18
+ export declare class McpBridge {
19
+ private readonly binaryPath;
20
+ private registered;
21
+ constructor(binaryPath?: string);
22
+ /**
23
+ * Syncs MCP servers configured by Paseo into Antigravity.
24
+ */
25
+ syncServers(servers?: Record<string, McpServerConfig>): Promise<void>;
26
+ private addServer;
27
+ }
@@ -0,0 +1,62 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { logger } from "./logger.js";
4
+ const execFileAsync = promisify(execFile);
5
+ /**
6
+ * Bridges Paseo session MCP servers into Google Antigravity CLI's MCP system.
7
+ */
8
+ export class McpBridge {
9
+ binaryPath;
10
+ registered = new Set();
11
+ constructor(binaryPath = "agy") {
12
+ this.binaryPath = binaryPath;
13
+ }
14
+ /**
15
+ * Syncs MCP servers configured by Paseo into Antigravity.
16
+ */
17
+ async syncServers(servers) {
18
+ if (!servers || typeof servers !== "object")
19
+ return;
20
+ for (const [name, config] of Object.entries(servers)) {
21
+ if (!name || !config || typeof config !== "object")
22
+ continue;
23
+ const key = `${name}:${JSON.stringify(config)}`;
24
+ if (this.registered.has(key))
25
+ continue;
26
+ try {
27
+ await this.addServer(name, config);
28
+ this.registered.add(key);
29
+ logger.info("Successfully synced MCP server from Paseo to Antigravity", { name, type: config.type });
30
+ }
31
+ catch (err) {
32
+ logger.warn("Could not sync MCP server to Antigravity (continuing without it)", {
33
+ name,
34
+ error: err.message,
35
+ });
36
+ }
37
+ }
38
+ }
39
+ async addServer(name, config) {
40
+ const args = ["mcp", "add"];
41
+ if (config.type === "stdio") {
42
+ if (config.env) {
43
+ for (const [k, v] of Object.entries(config.env)) {
44
+ args.push("--env", `${k}=${v}`);
45
+ }
46
+ }
47
+ args.push(name, config.command);
48
+ if (config.args && config.args.length > 0) {
49
+ args.push(...config.args);
50
+ }
51
+ }
52
+ else if (config.type === "http" || config.type === "sse") {
53
+ if (config.headers) {
54
+ for (const [k, v] of Object.entries(config.headers)) {
55
+ args.push("--header", `${k}: ${v}`);
56
+ }
57
+ }
58
+ args.push(name, config.url);
59
+ }
60
+ await execFileAsync(this.binaryPath, args, { timeout: 10_000 });
61
+ }
62
+ }
@@ -4,6 +4,7 @@ export interface PatchResult {
4
4
  patchedPaths: string[];
5
5
  asarPaths?: string[];
6
6
  patchedAsarPaths?: string[];
7
+ skippedPaths?: string[];
7
8
  errors: string[];
8
9
  }
9
10
  /**
@@ -54,7 +55,15 @@ export declare function ensurePaseoIntegration(options?: {
54
55
  verbose?: boolean;
55
56
  targetPaths?: string[];
56
57
  targetAsarPaths?: string[];
58
+ forceMacosApp?: boolean;
57
59
  }): Promise<PatchResult>;
60
+ /**
61
+ * Restores original unpatched Paseo files from backup (.bak) archives.
62
+ */
63
+ export declare function restorePaseoIntegration(): Promise<{
64
+ restored: string[];
65
+ errors: string[];
66
+ }>;
58
67
  /**
59
68
  * Checks whether a @getpaseo/server installation directory is already patched
60
69
  * for Antigravity quota and telemetry support.
@@ -1461,6 +1461,14 @@ export async function patchPaseoAsar(asarPath) {
1461
1461
  timeout: 15000,
1462
1462
  });
1463
1463
  changes.push(`Re-signed macOS bundle at ${appBundlePath}`);
1464
+ try {
1465
+ execFileSync("xattr", ["-cr", appBundlePath], {
1466
+ stdio: "ignore",
1467
+ timeout: 10000,
1468
+ });
1469
+ changes.push(`Cleared quarantine extended attributes at ${appBundlePath}`);
1470
+ }
1471
+ catch { }
1464
1472
  }
1465
1473
  catch (signErr) {
1466
1474
  logger.warn(`Could not re-sign ${appBundlePath}`, { error: String(signErr) });
@@ -1491,11 +1499,34 @@ export async function patchPaseoAsar(asarPath) {
1491
1499
  * Discovers and patches all accessible Paseo installations (both directory and app.asar).
1492
1500
  */
1493
1501
  export async function ensurePaseoIntegration(options) {
1494
- const serverPaths = options?.targetPaths || findPaseoServerInstallations();
1495
- const asarPaths = options?.targetAsarPaths || findPaseoAsarPaths();
1502
+ const rawServerPaths = options?.targetPaths || findPaseoServerInstallations();
1503
+ const rawAsarPaths = options?.targetAsarPaths || findPaseoAsarPaths();
1504
+ const serverPaths = [];
1505
+ const asarPaths = [];
1506
+ const skippedPaths = [];
1496
1507
  const patchedPaths = [];
1497
1508
  const patchedAsarPaths = [];
1498
1509
  const errors = [];
1510
+ for (const sPath of rawServerPaths) {
1511
+ if (process.platform === "darwin" && sPath.includes(".app") && !options?.forceMacosApp) {
1512
+ skippedPaths.push(sPath);
1513
+ if (options?.verbose) {
1514
+ logger.info(`[macOS] Skipping server path inside .app bundle to preserve Apple signature: ${sPath}`);
1515
+ }
1516
+ continue;
1517
+ }
1518
+ serverPaths.push(sPath);
1519
+ }
1520
+ for (const aPath of rawAsarPaths) {
1521
+ if (process.platform === "darwin" && aPath.includes(".app") && !options?.forceMacosApp) {
1522
+ skippedPaths.push(aPath);
1523
+ if (options?.verbose) {
1524
+ logger.info(`[macOS] Skipping app.asar inside .app bundle to preserve Apple signature: ${aPath}`);
1525
+ }
1526
+ continue;
1527
+ }
1528
+ asarPaths.push(aPath);
1529
+ }
1499
1530
  for (const sPath of serverPaths) {
1500
1531
  const res = patchPaseoServer(sPath);
1501
1532
  if (res.success) {
@@ -1525,14 +1556,52 @@ export async function ensurePaseoIntegration(options) {
1525
1556
  }
1526
1557
  }
1527
1558
  return {
1528
- found: serverPaths.length > 0 || asarPaths.length > 0,
1559
+ found: serverPaths.length > 0 || asarPaths.length > 0 || skippedPaths.length > 0,
1529
1560
  serverPaths,
1530
1561
  patchedPaths,
1531
1562
  asarPaths,
1532
1563
  patchedAsarPaths,
1564
+ skippedPaths,
1533
1565
  errors,
1534
1566
  };
1535
1567
  }
1568
+ /**
1569
+ * Restores original unpatched Paseo files from backup (.bak) archives.
1570
+ */
1571
+ export async function restorePaseoIntegration() {
1572
+ const restored = [];
1573
+ const errors = [];
1574
+ const home = os.homedir();
1575
+ const macBackup = path.join(home, ".paseo", "backups", "app.asar.bak");
1576
+ const asarCandidates = findPaseoAsarPaths();
1577
+ for (const asarPath of asarCandidates) {
1578
+ let backupFile = null;
1579
+ if (process.platform === "darwin" && asarPath.includes(".app") && fs.existsSync(macBackup)) {
1580
+ backupFile = macBackup;
1581
+ }
1582
+ else if (fs.existsSync(`${asarPath}.bak`)) {
1583
+ backupFile = `${asarPath}.bak`;
1584
+ }
1585
+ if (backupFile) {
1586
+ try {
1587
+ fs.copyFileSync(backupFile, asarPath);
1588
+ restored.push(asarPath);
1589
+ if (process.platform === "darwin" && asarPath.includes(".app")) {
1590
+ const appIndex = asarPath.indexOf(".app");
1591
+ const appBundlePath = asarPath.slice(0, appIndex + 4);
1592
+ try {
1593
+ execFileSync("xattr", ["-cr", appBundlePath], { stdio: "ignore" });
1594
+ }
1595
+ catch { }
1596
+ }
1597
+ }
1598
+ catch (err) {
1599
+ errors.push(`Failed restoring ${asarPath}: ${err?.message || err}`);
1600
+ }
1601
+ }
1602
+ }
1603
+ return { restored, errors };
1604
+ }
1536
1605
  /**
1537
1606
  * Checks whether a @getpaseo/server installation directory is already patched
1538
1607
  * for Antigravity quota and telemetry support.
@@ -135,7 +135,7 @@ export interface SessionInfo {
135
135
  updatedAt?: string | null;
136
136
  _meta?: Record<string, unknown> | null;
137
137
  }
138
- export type ToolKind = "read" | "edit" | "execute" | "fetch" | "other";
138
+ export type ToolKind = "read" | "edit" | "execute" | "fetch" | "search" | "other";
139
139
  export declare function mapToolNameToKind(toolName: string): ToolKind;
140
140
  export declare function extractPromptText(promptInput: unknown): string;
141
141
  export interface ModelDefinition {
package/dist/protocol.js CHANGED
@@ -40,12 +40,18 @@ export const ACP_METHODS = {
40
40
  };
41
41
  export function mapToolNameToKind(toolName) {
42
42
  const name = toolName.toLowerCase();
43
+ if (name.includes("search") ||
44
+ name.includes("grep") ||
45
+ name.includes("find") ||
46
+ name === "search_web" ||
47
+ name === "grep_search" ||
48
+ name === "find_by_name")
49
+ return "search";
43
50
  if (name.includes("read") ||
44
51
  name.includes("view") ||
45
52
  name.includes("list") ||
46
- name.includes("grep") ||
47
- name.includes("find") ||
48
53
  name === "view_file" ||
54
+ name === "read_file" ||
49
55
  name === "list_dir")
50
56
  return "read";
51
57
  if (name.includes("write") ||
@@ -59,12 +65,11 @@ export function mapToolNameToKind(toolName) {
59
65
  if (name.includes("command") ||
60
66
  name.includes("terminal") ||
61
67
  name.includes("execute") ||
68
+ name.includes("subagent") ||
62
69
  name === "run_command")
63
70
  return "execute";
64
- if (name.includes("search") ||
65
- name.includes("url") ||
66
- name.includes("browser") ||
67
- name === "search_web")
71
+ if (name.includes("url") ||
72
+ name.includes("browser"))
68
73
  return "fetch";
69
74
  return "other";
70
75
  }
@@ -0,0 +1,37 @@
1
+ import { ToolKind } from "./protocol.js";
2
+ export interface NormalizedToolCall {
3
+ toolCallId: string;
4
+ title: string;
5
+ kind: ToolKind;
6
+ locations?: Array<{
7
+ path: string;
8
+ line?: number;
9
+ }>;
10
+ rawInput: Record<string, unknown>;
11
+ questionCard?: string;
12
+ }
13
+ export interface NormalizedToolResult {
14
+ status: "completed" | "failed";
15
+ rawOutput: string;
16
+ content?: Array<Record<string, unknown>>;
17
+ imageMarkdown?: string;
18
+ }
19
+ export interface PlanEntry {
20
+ content: string;
21
+ status: "pending" | "in_progress" | "completed";
22
+ }
23
+ /**
24
+ * Normalizes Antigravity tool calls into standard ACP format with descriptive titles,
25
+ * proper kinds, clickable file locations, and normalized parameter names.
26
+ */
27
+ export declare function normalizeToolCall(toolName: string, rawParams: Record<string, unknown> | undefined, stepIndex: number): NormalizedToolCall;
28
+ /**
29
+ * Normalizes tool execution results into rich timeline content (diffs, terminals, text, images).
30
+ */
31
+ export declare function normalizeToolResult(toolName: string, rawParams: Record<string, unknown> | undefined, output: unknown, error?: {
32
+ message?: string;
33
+ }): NormalizedToolResult;
34
+ /**
35
+ * Extracts markdown checklist / todo plan updates from assistant text or thoughts.
36
+ */
37
+ export declare function extractPlanEntries(text: string): PlanEntry[] | null;
@@ -0,0 +1,219 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Normalizes Antigravity tool calls into standard ACP format with descriptive titles,
4
+ * proper kinds, clickable file locations, and normalized parameter names.
5
+ */
6
+ export function normalizeToolCall(toolName, rawParams = {}, stepIndex) {
7
+ const toolCallId = `tool_${stepIndex}`;
8
+ const normalizedParams = { ...rawParams };
9
+ const name = toolName.toLowerCase();
10
+ // 1. Shell commands: run_command
11
+ if (name === "run_command" ||
12
+ name.includes("command") ||
13
+ name.includes("terminal") ||
14
+ Boolean(rawParams.CommandLine) ||
15
+ Boolean(rawParams.command)) {
16
+ const cmd = String(rawParams.CommandLine || rawParams.command || rawParams.cmd || "").trim();
17
+ const summary = String(rawParams.toolSummary || rawParams.summary || "").trim();
18
+ const title = summary || (cmd ? truncate(cmd, 60) : "run_command");
19
+ normalizedParams.command = cmd;
20
+ normalizedParams.CommandLine = cmd;
21
+ return {
22
+ toolCallId,
23
+ title,
24
+ kind: "execute",
25
+ rawInput: normalizedParams,
26
+ };
27
+ }
28
+ // 2. File reads: read_file, view_file, list_dir
29
+ if (name === "read_file" || name === "view_file" || name === "list_dir" || name.includes("read")) {
30
+ const rawPath = String(rawParams.Path || rawParams.path || rawParams.AbsolutePath || rawParams.filePath || rawParams.file || "").trim();
31
+ const startLine = typeof rawParams.StartLine === "number" ? rawParams.StartLine : undefined;
32
+ const base = rawPath ? path.basename(rawPath) : "read_file";
33
+ const title = rawPath ? (startLine ? `${base}:${startLine}` : base) : "read_file";
34
+ normalizedParams.path = rawPath;
35
+ normalizedParams.filePath = rawPath;
36
+ normalizedParams.Path = rawPath;
37
+ return {
38
+ toolCallId,
39
+ title,
40
+ kind: "read",
41
+ locations: rawPath ? [{ path: rawPath, ...(startLine ? { line: startLine } : {}) }] : undefined,
42
+ rawInput: normalizedParams,
43
+ };
44
+ }
45
+ // 3. File edits / writes: edit_file, write_file, replace_file_content, multi_replace_file_content
46
+ if (name === "edit_file" ||
47
+ name === "write_file" ||
48
+ name === "replace_file_content" ||
49
+ name === "multi_replace_file_content" ||
50
+ name === "sed_file" ||
51
+ name.includes("edit") ||
52
+ name.includes("write")) {
53
+ const rawPath = String(rawParams.Path || rawParams.path || rawParams.AbsolutePath || rawParams.filePath || rawParams.TargetFile || "").trim();
54
+ const base = rawPath ? path.basename(rawPath) : "edit_file";
55
+ const target = String(rawParams.Target || rawParams.oldText || rawParams.oldString || "");
56
+ const replacement = String(rawParams.Replacement || rawParams.newText || rawParams.newString || rawParams.Content || "");
57
+ normalizedParams.path = rawPath;
58
+ normalizedParams.filePath = rawPath;
59
+ normalizedParams.Path = rawPath;
60
+ normalizedParams.oldText = target;
61
+ normalizedParams.oldString = target;
62
+ normalizedParams.newText = replacement;
63
+ normalizedParams.newString = replacement;
64
+ return {
65
+ toolCallId,
66
+ title: base,
67
+ kind: "edit",
68
+ locations: rawPath ? [{ path: rawPath }] : undefined,
69
+ rawInput: normalizedParams,
70
+ };
71
+ }
72
+ // 4. Search tools: search_web, grep_search, find_by_name
73
+ if (name === "search_web" || name === "grep_search" || name === "find_by_name" || name.includes("search")) {
74
+ const query = String(rawParams.query || rawParams.pattern || rawParams.CommandLine || "").trim();
75
+ const title = query ? `Busca: ${truncate(query, 50)}` : "search";
76
+ normalizedParams.query = query;
77
+ return {
78
+ toolCallId,
79
+ title,
80
+ kind: "search",
81
+ rawInput: normalizedParams,
82
+ };
83
+ }
84
+ // 5. Subagents: invoke_subagent, browser_subagent, manage_subagents
85
+ if (name.includes("subagent")) {
86
+ const role = String(rawParams.Role || rawParams.TypeName || rawParams.type || "Subagente").trim();
87
+ const prompt = String(rawParams.Prompt || rawParams.prompt || "").trim();
88
+ const title = `🤖 Subagente: ${role}`;
89
+ normalizedParams.role = role;
90
+ normalizedParams.prompt = prompt;
91
+ return {
92
+ toolCallId,
93
+ title,
94
+ kind: "execute",
95
+ rawInput: normalizedParams,
96
+ };
97
+ }
98
+ // 6. Interactive Questions: ask_question
99
+ if (name === "ask_question") {
100
+ const questions = Array.isArray(rawParams.questions) ? rawParams.questions : [];
101
+ const questionCard = formatQuestionCard(questions);
102
+ return {
103
+ toolCallId,
104
+ title: "❓ Pergunta do Assistente",
105
+ kind: "other",
106
+ rawInput: normalizedParams,
107
+ questionCard: questionCard || undefined,
108
+ };
109
+ }
110
+ // 7. General fallback
111
+ const summary = String(rawParams.toolSummary || rawParams.summary || "").trim();
112
+ return {
113
+ toolCallId,
114
+ title: summary || toolName,
115
+ kind: "other",
116
+ rawInput: normalizedParams,
117
+ };
118
+ }
119
+ /**
120
+ * Normalizes tool execution results into rich timeline content (diffs, terminals, text, images).
121
+ */
122
+ export function normalizeToolResult(toolName, rawParams = {}, output, error) {
123
+ if (error) {
124
+ return {
125
+ status: "failed",
126
+ rawOutput: error.message || "Tool execution error",
127
+ };
128
+ }
129
+ const outputStr = typeof output === "string" ? output : JSON.stringify(output ?? "");
130
+ const name = toolName.toLowerCase();
131
+ const content = [];
132
+ // Edit tools: emit diff block so Paseo displays visual green/red syntax diffs
133
+ if (name === "edit_file" ||
134
+ name === "replace_file_content" ||
135
+ name === "multi_replace_file_content" ||
136
+ name === "write_file") {
137
+ const filePath = String(rawParams.Path || rawParams.path || rawParams.AbsolutePath || "");
138
+ const oldText = String(rawParams.Target || rawParams.oldText || rawParams.oldString || "");
139
+ const newText = String(rawParams.Replacement || rawParams.newText || rawParams.newString || rawParams.Content || "");
140
+ if (oldText || newText) {
141
+ content.push({
142
+ type: "diff",
143
+ path: filePath,
144
+ oldText,
145
+ newText,
146
+ });
147
+ }
148
+ }
149
+ else if (name === "read_file" || name === "view_file") {
150
+ if (outputStr.trim()) {
151
+ content.push({
152
+ type: "text",
153
+ text: outputStr,
154
+ });
155
+ }
156
+ }
157
+ return {
158
+ status: "completed",
159
+ rawOutput: outputStr,
160
+ content: content.length > 0 ? content : undefined,
161
+ };
162
+ }
163
+ /**
164
+ * Extracts markdown checklist / todo plan updates from assistant text or thoughts.
165
+ */
166
+ export function extractPlanEntries(text) {
167
+ if (!text || typeof text !== "string")
168
+ return null;
169
+ const lines = text.split(/\r?\n/);
170
+ const entries = [];
171
+ for (const line of lines) {
172
+ const match = line.match(/^[-*]\s+\[([ xX/\\-])\]\s+(.+)$/);
173
+ if (match) {
174
+ const mark = match[1].trim();
175
+ const content = match[2].trim();
176
+ let status = "pending";
177
+ if (mark === "x" || mark === "X") {
178
+ status = "completed";
179
+ }
180
+ else if (mark === "/" || mark === "-") {
181
+ status = "in_progress";
182
+ }
183
+ entries.push({ content, status });
184
+ }
185
+ }
186
+ return entries.length >= 2 ? entries : null;
187
+ }
188
+ /**
189
+ * Formats Antigravity's ask_question structure into a clear, responsive markdown card.
190
+ */
191
+ function formatQuestionCard(questions) {
192
+ if (!questions || questions.length === 0)
193
+ return "";
194
+ const cards = [];
195
+ for (const q of questions) {
196
+ if (!q || typeof q !== "object")
197
+ continue;
198
+ const prompt = q.question || "Escolha uma opção:";
199
+ const options = Array.isArray(q.options) ? q.options : [];
200
+ let card = `### ❓ Pergunta do Assistente\n> **${prompt}**\n\n`;
201
+ if (options.length > 0) {
202
+ card += `*Responda indicando o número da opção desejada:*\n`;
203
+ card += options
204
+ .map((opt, idx) => {
205
+ const isRec = opt.includes("(Recommended)") || opt.includes("(Recomendado)");
206
+ const clean = opt.replace(/^\(Recommended\)\s*/i, "").replace(/^\(Recomendado\)\s*/i, "");
207
+ return `- **[${idx + 1}]** ${clean}${isRec ? " *(Recomendado)*" : ""}`;
208
+ })
209
+ .join("\n");
210
+ }
211
+ cards.push(card);
212
+ }
213
+ return cards.join("\n\n---\n\n");
214
+ }
215
+ function truncate(text, maxLen) {
216
+ if (text.length <= maxLen)
217
+ return text;
218
+ return text.slice(0, maxLen - 3) + "...";
219
+ }
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import os from "node:os";
4
4
  import { logger } from "./logger.js";
5
- import { mapToolNameToKind } from "./protocol.js";
5
+ import { normalizeToolCall } from "./tool-formatter.js";
6
6
  /**
7
7
  * Strips XML-like tags injected by harness or system prompts (e.g., <USER_REQUEST>, <ADDITIONAL_METADATA>)
8
8
  * to recover the clean user prompt for history display in clients like Paseo.
@@ -106,21 +106,20 @@ export function loadConversationTranscript(conversationId) {
106
106
  for (let i = 0; i < step.tool_calls.length; i++) {
107
107
  const tc = step.tool_calls[i];
108
108
  const rawArgs = (tc.args || tc.parameters || {});
109
- const toolName = String(tc.name || tc.tool_name || rawArgs.toolAction || "tool");
110
- const title = String(rawArgs.toolSummary ||
111
- rawArgs.toolAction ||
112
- tc.toolSummary ||
113
- tc.toolAction ||
114
- tc.name ||
115
- "tool");
116
- const toolCallId = `tool_${stepIndex}_${i}`;
117
- lastToolCallId = toolCallId;
109
+ const toolName = String(tc.name || tc.tool_name || tc.toolAction || rawArgs.toolAction || "tool");
110
+ const mergedParams = {
111
+ ...rawArgs,
112
+ toolSummary: rawArgs.toolSummary || tc.toolSummary,
113
+ toolAction: rawArgs.toolAction || tc.toolAction,
114
+ };
115
+ const toolCall = normalizeToolCall(toolName, mergedParams, stepIndex * 100 + i);
116
+ lastToolCallId = toolCall.toolCallId;
118
117
  replayItems.push({
119
118
  type: "tool_call",
120
- toolCallId,
121
- title,
122
- kind: mapToolNameToKind(toolName),
123
- rawInput: rawArgs,
119
+ toolCallId: toolCall.toolCallId,
120
+ title: toolCall.title,
121
+ kind: toolCall.kind,
122
+ rawInput: toolCall.rawInput,
124
123
  });
125
124
  }
126
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-acp-agy",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "ACP (Agent Client Protocol) provider for Google Antigravity in Paseo and Zed",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",