agy-context-saver 1.8.1 → 2.0.1

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.
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Agy-Context-Saver Registration & Installation Engine
5
- * Zero-delay cross-platform installer with intelligent existing-installation detection.
4
+ * Agy-Context-Saver Registration & Installation Engine (v2.0.0)
5
+ * Zero-delay cross-platform installer with CLI-first shim provisioning,
6
+ * unified hook registration, and legacy MCP retirement/sweep.
6
7
  *
7
8
  * Capabilities:
8
- * - Detects existing native plugin link, hook registration, MCP server, and tool schemas
9
- * - Re-verifies and updates in ~10-15ms without redundant configuration churn
10
- * - Uses asynchronous file operations (fs.promises) with parallel schema writes
11
- * - Supports silent mode for stdio MCP JSON-RPC compatibility
12
- * - Supports CLI flags: install, install --check, uninstall, status
9
+ * - Detects native plugin link, hook registration (agy-context-saver), CLI shims, RTK (>= 1.1.0), and MCP cleanup
10
+ * - Re-verifies and updates without redundant disk churn
11
+ * - Retires legacy MCP registration in mcp_config.json and sweeps agy-context-saver* schema dirs (ACS-HOOK-11)
12
+ * - Supports silent mode and CLI flags: install, install --check, uninstall, status, --restore-backups
13
13
  */
14
14
 
15
15
  import fs from "node:fs";
@@ -18,10 +18,18 @@ import path from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
19
  import { ensureRtkInstalled, findExistingRtk } from "./provision-rtk.mjs";
20
20
  import { getStateDir, removeLegacyTmpState } from "./state-dir.mjs";
21
+ import { formatTranscriptAccessStatus } from "../lib/transcript-core.mjs";
21
22
 
22
23
  const fsPromises = fs.promises;
23
24
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
25
  const repoRoot = path.resolve(__dirname, "..");
26
+ const PACKAGE_VERSION = (() => {
27
+ try {
28
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version || "2.0.1";
29
+ } catch {
30
+ return "2.0.1";
31
+ }
32
+ })();
25
33
 
26
34
  const homeDir = process.env.AGY_TEST_PROFILE_DIR || os.homedir();
27
35
  const geminiConfigDir = path.join(homeDir, ".gemini", "config");
@@ -30,15 +38,96 @@ const pluginDest = path.join(pluginsDir, "agy-context-saver");
30
38
  const scriptsDir = path.join(geminiConfigDir, "scripts");
31
39
  const hooksJsonPath = path.join(geminiConfigDir, "hooks.json");
32
40
  const hooksBakPath = path.join(geminiConfigDir, "hooks.json.bak");
41
+ const configJsonPath = path.join(geminiConfigDir, "config.json");
33
42
  const mcpJsonPath = path.join(geminiConfigDir, "mcp_config.json");
34
43
  const mcpBakPath = path.join(geminiConfigDir, "mcp_config.json.bak");
35
- const antigravityMcpDir = path.join(homeDir, ".gemini", "antigravity", "mcp", "agy-context-saver");
44
+ const antigravityMcpRootDir = path.join(homeDir, ".gemini", "antigravity", "mcp");
45
+ const antigravityMcpDir = path.join(antigravityMcpRootDir, "agy-context-saver");
46
+ const antigravityBinDir = path.join(homeDir, ".gemini", "antigravity", "bin");
36
47
 
37
48
  const sourceHook = path.join(repoRoot, "scripts", "execution-guard-hook.mjs");
49
+ const repoHooksJsonPath = path.join(repoRoot, "hooks.json");
38
50
  const destHook = path.join(scriptsDir, "execution-guard-hook.mjs");
39
- const mcpIndexPath = path.join(pluginDest, "mcp", "index.js").replace(/\\/g, "/");
40
51
  const destHookNormalized = destHook.replace(/\\/g, "/");
41
52
 
53
+ const HOOK_KEY = "agy-context-saver";
54
+ const LEGACY_HOOK_KEY = "execution-guard";
55
+ const CLI_SHIM_NAMES = ["agy-context", "agy-context-saver"];
56
+ const DEFAULT_HOOK_MATCHER =
57
+ "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir|replace_file_content|write_file|write_to_file|edit_file";
58
+
59
+ function getExpectedHookMatcher() {
60
+ try {
61
+ const cfg = JSON.parse(fs.readFileSync(repoHooksJsonPath, "utf-8"));
62
+ const matcher = cfg?.[HOOK_KEY]?.PreToolUse?.[0]?.matcher;
63
+ if (typeof matcher === "string" && matcher.trim()) return matcher;
64
+ } catch {}
65
+ return DEFAULT_HOOK_MATCHER;
66
+ }
67
+
68
+ function isLegacyAgyMcpKey(key) {
69
+ return key === "agy-context-saver" || key.startsWith("agy-context-saver");
70
+ }
71
+
72
+ function isExecutableFile(filePath) {
73
+ if (!fs.existsSync(filePath)) return false;
74
+ if (os.platform() === "win32") return true;
75
+ try {
76
+ fs.accessSync(filePath, fs.constants.X_OK);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function detectCliShims() {
84
+ const isWin = os.platform() === "win32";
85
+ const expectedFiles = isWin
86
+ ? CLI_SHIM_NAMES.map((name) => `${name}.cmd`)
87
+ : CLI_SHIM_NAMES;
88
+ const present = expectedFiles.filter((name) => isExecutableFile(path.join(antigravityBinDir, name)));
89
+ return {
90
+ exists: present.length === expectedFiles.length,
91
+ dir: antigravityBinDir,
92
+ shims: present
93
+ };
94
+ }
95
+
96
+ function findLegacyMcpArtifacts() {
97
+ const legacyMcpKeys = [];
98
+ if (fs.existsSync(mcpJsonPath)) {
99
+ try {
100
+ const cfg = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
101
+ if (cfg && typeof cfg.mcpServers === "object" && cfg.mcpServers !== null) {
102
+ for (const key of Object.keys(cfg.mcpServers)) {
103
+ if (isLegacyAgyMcpKey(key)) {
104
+ legacyMcpKeys.push(key);
105
+ }
106
+ }
107
+ }
108
+ } catch {}
109
+ }
110
+
111
+ const legacySchemaDirs = [];
112
+ if (fs.existsSync(antigravityMcpRootDir)) {
113
+ try {
114
+ for (const entry of fs.readdirSync(antigravityMcpRootDir, { withFileTypes: true })) {
115
+ if (entry.name.startsWith("agy-context-saver")) {
116
+ legacySchemaDirs.push(path.join(antigravityMcpRootDir, entry.name));
117
+ }
118
+ }
119
+ } catch {}
120
+ }
121
+
122
+ const duplicateCount = legacyMcpKeys.length + legacySchemaDirs.length;
123
+ return {
124
+ clean: duplicateCount === 0,
125
+ duplicateCount,
126
+ legacyMcpKeys,
127
+ legacySchemaDirs
128
+ };
129
+ }
130
+
42
131
  /**
43
132
  * Write file only if content has changed, eliminating disk churn.
44
133
  */
@@ -75,7 +164,7 @@ export async function safeCopyFileIfChanged(sourcePath, targetPath) {
75
164
  }
76
165
 
77
166
  /**
78
- * Detects whether Agy-Context-Saver is already installed across all Antigravity integration points.
167
+ * Detects whether Agy-Context-Saver is already installed across all Antigravity 2.0 integration points.
79
168
  */
80
169
  export function detectExistingInstallation() {
81
170
  const pluginExists = fs.existsSync(pluginDest);
@@ -88,52 +177,86 @@ export function detectExistingInstallation() {
88
177
  }
89
178
  }
90
179
 
91
- let hookRegistered = false;
92
- let hookCommand = null;
93
- if (fs.existsSync(hooksJsonPath)) {
180
+ let pluginHookRegistered = false;
181
+ let pluginHookCommand = null;
182
+ const pluginHooksPath = path.join(pluginDest, "hooks.json");
183
+ if (fs.existsSync(pluginHooksPath)) {
94
184
  try {
95
- const cfg = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
96
- const h = cfg["execution-guard"] || cfg["agy-context-saver"];
185
+ const pCfg = JSON.parse(fs.readFileSync(pluginHooksPath, "utf-8"));
186
+ const h = pCfg?.[HOOK_KEY];
97
187
  if (h) {
98
- hookRegistered = true;
99
- hookCommand = h.PreToolUse?.[0]?.hooks?.[0]?.command;
188
+ pluginHookCommand = h.PreToolUse?.[0]?.hooks?.[0]?.command ?? null;
189
+ pluginHookRegistered = Boolean(pluginHookCommand) && !String(pluginHookCommand).includes("ANTIGRAVITY_PLUGIN_ROOT");
100
190
  }
101
191
  } catch {}
102
192
  }
103
193
 
104
- let mcpRegistered = false;
105
- let mcpArgs = null;
106
- if (fs.existsSync(mcpJsonPath)) {
194
+ let legacyHookPresent = false;
195
+ let globalHookRegistered = false;
196
+ let globalHookCommand = null;
197
+ const expectedMatcher = getExpectedHookMatcher();
198
+ if (fs.existsSync(hooksJsonPath)) {
107
199
  try {
108
- const cfg = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
109
- if (cfg.mcpServers?.["agy-context-saver"]) {
110
- mcpRegistered = true;
111
- mcpArgs = cfg.mcpServers["agy-context-saver"].args;
200
+ const cfg = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
201
+ legacyHookPresent = Boolean(cfg?.[LEGACY_HOOK_KEY]);
202
+ const gh = cfg?.[HOOK_KEY];
203
+ if (gh) {
204
+ globalHookCommand = gh.PreToolUse?.[0]?.hooks?.[0]?.command ?? null;
205
+ const globalMatcher = gh.PreToolUse?.[0]?.matcher ?? "";
206
+ globalHookRegistered =
207
+ Boolean(globalHookCommand) &&
208
+ String(globalHookCommand).replace(/\\/g, "/").includes(destHookNormalized) &&
209
+ globalMatcher === expectedMatcher;
112
210
  }
113
211
  } catch {}
114
212
  }
115
213
 
116
- const scriptExists = fs.existsSync(destHook);
117
- const schemasExist =
118
- fs.existsSync(path.join(antigravityMcpDir, "check_context_health.json")) &&
119
- fs.existsSync(path.join(antigravityMcpDir, "get_installation_status.json")) &&
120
- fs.existsSync(path.join(antigravityMcpDir, "sync_installation.json")) &&
121
- fs.existsSync(path.join(antigravityMcpDir, "read_transcript.json")) &&
122
- fs.existsSync(path.join(antigravityMcpDir, "query_transcript.json"));
123
-
214
+ const pluginHookScript = path.join(pluginDest, "scripts", "execution-guard-hook.mjs");
215
+ const scriptExists = fs.existsSync(pluginHookScript);
216
+ const mirroredHookExists = fs.existsSync(destHook);
217
+ const cliShim = detectCliShims();
218
+ const mcp = findLegacyMcpArtifacts();
124
219
  const rtkInfo = findExistingRtk();
125
- const isInstalled = pluginExists || (hookRegistered && mcpRegistered);
126
- const isComplete = (pluginExists || (hookRegistered && mcpRegistered && scriptExists)) && schemasExist && Boolean(rtkInfo);
220
+ const hookRegistered = pluginHookRegistered && globalHookRegistered && mirroredHookExists;
221
+
222
+ const isInstalled =
223
+ pluginExists ||
224
+ pluginHookRegistered ||
225
+ globalHookRegistered ||
226
+ legacyHookPresent ||
227
+ mirroredHookExists ||
228
+ cliShim.exists ||
229
+ mcp.legacyMcpKeys.length > 0 ||
230
+ mcp.legacySchemaDirs.length > 0;
231
+
232
+ const isComplete =
233
+ pluginExists &&
234
+ hookRegistered &&
235
+ scriptExists &&
236
+ !legacyHookPresent &&
237
+ cliShim.exists &&
238
+ Boolean(rtkInfo) &&
239
+ mcp.clean;
127
240
 
128
241
  return {
129
242
  isInstalled,
130
243
  isComplete,
131
244
  details: {
132
245
  plugin: { exists: pluginExists, target: pluginTarget },
133
- hook: { registered: hookRegistered, command: hookCommand, scriptExists },
134
- mcp: { registered: mcpRegistered, args: mcpArgs },
135
- schemas: { exists: schemasExist, dir: antigravityMcpDir },
136
- rtk: rtkInfo
246
+ hook: {
247
+ key: HOOK_KEY,
248
+ registered: hookRegistered,
249
+ pluginHookRegistered,
250
+ globalHookRegistered,
251
+ mirroredHookExists,
252
+ legacyHookPresent,
253
+ command: globalHookCommand || pluginHookCommand,
254
+ scriptExists
255
+ },
256
+ cliShim,
257
+ mcp,
258
+ rtk: rtkInfo,
259
+ transcriptAccess: formatTranscriptAccessStatus()
137
260
  }
138
261
  };
139
262
  }
@@ -145,19 +268,142 @@ export function parseAndValidateJsonConfig(filePath, configName) {
145
268
  try {
146
269
  parsed = JSON.parse(content);
147
270
  } catch (err) {
148
- throw new Error(`Configuration error: ${filePath} contains invalid JSON (${err.message}). Installation aborted to prevent data loss. Original file is unchanged.`);
271
+ throw new Error(
272
+ `Configuration error: ${filePath} contains invalid JSON (${err.message}). Installation aborted to prevent data loss. Original file is unchanged.`
273
+ );
149
274
  }
150
275
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
151
- throw new Error(`Configuration error: ${filePath} must contain a JSON object, but found ${parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed}. Installation aborted to prevent data loss. Original file is unchanged.`);
276
+ throw new Error(
277
+ `Configuration error: ${filePath} must contain a JSON object, but found ${parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed}. Installation aborted to prevent data loss. Original file is unchanged.`
278
+ );
152
279
  }
153
280
  if (configName === "mcp" && parsed.mcpServers !== undefined) {
154
281
  if (typeof parsed.mcpServers !== "object" || parsed.mcpServers === null || Array.isArray(parsed.mcpServers)) {
155
- throw new Error(`Configuration error: ${filePath} property "mcpServers" must be a JSON object. Installation aborted to prevent data loss. Original file is unchanged.`);
282
+ throw new Error(
283
+ `Configuration error: ${filePath} property "mcpServers" must be a JSON object. Installation aborted to prevent data loss. Original file is unchanged.`
284
+ );
156
285
  }
157
286
  }
158
287
  return parsed;
159
288
  }
160
289
 
290
+ async function stripLegacyMcpRegistrations(parsedMcp, { log, warn }) {
291
+ if (!fs.existsSync(mcpJsonPath) || !parsedMcp?.mcpServers || typeof parsedMcp.mcpServers !== "object") {
292
+ return;
293
+ }
294
+
295
+ const keysToStrip = Object.keys(parsedMcp.mcpServers).filter(isLegacyAgyMcpKey);
296
+ if (keysToStrip.length === 0) {
297
+ return;
298
+ }
299
+
300
+ if (!fs.existsSync(mcpBakPath)) {
301
+ try {
302
+ await fsPromises.copyFile(mcpJsonPath, mcpBakPath);
303
+ log(`[OK] Created configuration backup: ${mcpBakPath}`);
304
+ } catch (err) {
305
+ warn(`[WARN] Could not create backup of ${mcpJsonPath}: ${err.message}`);
306
+ }
307
+ }
308
+
309
+ const nextMcpConfig = {
310
+ ...parsedMcp,
311
+ mcpServers: { ...parsedMcp.mcpServers }
312
+ };
313
+ for (const key of keysToStrip) {
314
+ delete nextMcpConfig.mcpServers[key];
315
+ }
316
+
317
+ await safeWriteFileIfChanged(mcpJsonPath, JSON.stringify(nextMcpConfig, null, 2));
318
+ log(`[OK] Retired legacy MCP registration(s) (${keysToStrip.join(", ")}) from: ${mcpJsonPath}`);
319
+ }
320
+
321
+ async function sweepLegacyMcpSchemas({ log, warn }) {
322
+ if (!fs.existsSync(antigravityMcpRootDir)) {
323
+ return;
324
+ }
325
+ let entries = [];
326
+ try {
327
+ entries = await fsPromises.readdir(antigravityMcpRootDir, { withFileTypes: true });
328
+ } catch (err) {
329
+ warn(`[WARN] Could not inspect ${antigravityMcpRootDir}: ${err.message}`);
330
+ return;
331
+ }
332
+
333
+ for (const entry of entries) {
334
+ if (!entry.name.startsWith("agy-context-saver")) continue;
335
+ const targetPath = path.join(antigravityMcpRootDir, entry.name);
336
+ try {
337
+ await fsPromises.rm(targetPath, { recursive: true, force: true });
338
+ log(`[OK] Swept legacy MCP schema directory: ${targetPath}`);
339
+ } catch (err) {
340
+ warn(`[WARN] Could not remove legacy MCP schema directory ${targetPath}: ${err.message}`);
341
+ }
342
+ }
343
+ }
344
+
345
+ async function installCliShims({ log }) {
346
+ const sourceCli = path.join(repoRoot, "bin", "agy-context.mjs");
347
+ const pluginCli = path.join(pluginDest, "bin", "agy-context.mjs");
348
+
349
+ for (const cliPath of [sourceCli, pluginCli]) {
350
+ if (fs.existsSync(cliPath)) {
351
+ try {
352
+ fs.chmodSync(cliPath, 0o755);
353
+ } catch {}
354
+ }
355
+ }
356
+
357
+ const targetCli = fs.existsSync(pluginCli) ? pluginCli : sourceCli;
358
+ const targetCliNormalized = targetCli.replace(/\\/g, "/");
359
+ const execPathNormalized = process.execPath.replace(/\\/g, "/");
360
+
361
+ await fsPromises.mkdir(antigravityBinDir, { recursive: true });
362
+
363
+ const posixShimBody = [
364
+ "#!/bin/sh",
365
+ "set -eu",
366
+ "if command -v node >/dev/null 2>&1; then",
367
+ ` exec node "${targetCliNormalized}" "$@"`,
368
+ "fi",
369
+ `exec "${execPathNormalized}" "${targetCliNormalized}" "$@"`,
370
+ ""
371
+ ].join("\n");
372
+
373
+ const winShimBody = [
374
+ "@echo off",
375
+ "setlocal",
376
+ "where node >nul 2>nul",
377
+ "if %errorlevel% equ 0 (",
378
+ ` node "${targetCli}" %*`,
379
+ " exit /b %errorlevel%",
380
+ ")",
381
+ `"${process.execPath}" "${targetCli}" %*`,
382
+ "exit /b %errorlevel%",
383
+ ""
384
+ ].join("\r\n");
385
+
386
+ let changed = false;
387
+ for (const name of CLI_SHIM_NAMES) {
388
+ const posixShimPath = path.join(antigravityBinDir, name);
389
+ if (await safeWriteFileIfChanged(posixShimPath, posixShimBody)) {
390
+ changed = true;
391
+ }
392
+ try {
393
+ fs.chmodSync(posixShimPath, 0o755);
394
+ } catch {}
395
+
396
+ if (os.platform() === "win32") {
397
+ const winShimPath = path.join(antigravityBinDir, `${name}.cmd`);
398
+ if (await safeWriteFileIfChanged(winShimPath, winShimBody)) {
399
+ changed = true;
400
+ }
401
+ }
402
+ }
403
+
404
+ log(`[OK] CLI shims installed in: ${antigravityBinDir}${changed ? "" : " (up-to-date)"}`);
405
+ }
406
+
161
407
  export async function runInstall(options = {}) {
162
408
  const log = options.silent ? () => {} : console.log;
163
409
  const warn = options.silent ? () => {} : console.warn;
@@ -167,50 +413,58 @@ export async function runInstall(options = {}) {
167
413
  // Pre-flight Fail-Closed Validation: Verify existing configs BEFORE ANY DISK MUTATION OR CHECK RETURN
168
414
  const validatedHooks = parseAndValidateJsonConfig(hooksJsonPath, "hooks");
169
415
  const validatedMcp = parseAndValidateJsonConfig(mcpJsonPath, "mcp");
416
+ const validatedConfig = parseAndValidateJsonConfig(configJsonPath, "config");
170
417
 
171
418
  const existing = detectExistingInstallation();
172
419
 
173
420
  // If checkOnly is requested, execute strictly read-only inspection (zero disk writes, zero network calls)
174
421
  if (options.checkOnly) {
175
422
  if (existing.isInstalled) {
176
- log("šŸ” Existing installation detected:");
423
+ log("Existing installation detected:");
177
424
  if (existing.details.plugin.exists) {
178
- log(` āœ“ Native Plugin Link: ${pluginDest}`);
425
+ log(` [OK] Native Plugin Link: ${pluginDest}`);
179
426
  }
180
427
  if (existing.details.hook.registered) {
181
- log(` āœ“ Governor Hook: Registered in ${hooksJsonPath}`);
428
+ log(` [OK] Governor Hook (${HOOK_KEY}): Active via plugin + global hooks.json`);
429
+ } else if (existing.details.hook.pluginHookRegistered) {
430
+ log(` [WARN] Global hooks.json entry missing/stale for /swarm workers (run setup to synchronize)`);
431
+ }
432
+ if (existing.details.hook.legacyHookPresent) {
433
+ log(` [WARN] Global hooks.json has legacy 'execution-guard' entry (run setup to clean up)`);
434
+ }
435
+ if (existing.details.cliShim.exists) {
436
+ log(` [OK] CLI Shims (agy-context): Installed in ${antigravityBinDir}`);
182
437
  }
183
- if (existing.details.mcp.registered) {
184
- log(` āœ“ Universal MCP Server: Configured in ${mcpJsonPath}`);
438
+ if (existing.details.rtk) {
439
+ log(` [OK] RTK Command Optimizer: ${existing.details.rtk.version} (${existing.details.rtk.path})`);
185
440
  }
186
- if (existing.details.schemas.exists) {
187
- log(` āœ“ Antigravity Tool Schemas: Present in ${antigravityMcpDir}`);
441
+ if (existing.details.mcp.clean) {
442
+ log(" [OK] MCP Cleanup: 0 duplicate MCP configs/schemas");
443
+ } else {
444
+ log(` [WARN] MCP Cleanup: ${existing.details.mcp.duplicateCount} legacy MCP config/schema entries found`);
188
445
  }
189
- log("\n[OK] Pre-flight check complete. Installation is active.");
446
+ log("\n[OK] Pre-flight check complete. Zero changes written.");
190
447
  } else {
191
- log("šŸ” Pre-flight check: No active installation detected (clean profile).");
448
+ log("Pre-flight check: No active installation detected (clean profile).");
192
449
  log("\n[OK] Pre-flight check complete. Zero changes written.");
193
450
  }
194
451
  return existing;
195
452
  }
196
453
 
197
454
  if (existing.isInstalled) {
198
- log("šŸ” Existing installation detected:");
455
+ log("Existing installation detected:");
199
456
  if (existing.details.plugin.exists) {
200
- log(` āœ“ Native Plugin Link: ${pluginDest}`);
457
+ log(` [OK] Native Plugin Link: ${pluginDest}`);
201
458
  }
202
459
  if (existing.details.hook.registered) {
203
- log(` āœ“ Governor Hook: Registered in ${hooksJsonPath}`);
204
- }
205
- if (existing.details.mcp.registered) {
206
- log(` āœ“ Universal MCP Server: Configured in ${mcpJsonPath}`);
460
+ log(` [OK] Governor Hook (${HOOK_KEY}): Active via plugin + global hooks.json`);
207
461
  }
208
- if (existing.details.schemas.exists) {
209
- log(` āœ“ Antigravity Tool Schemas: Present in ${antigravityMcpDir}`);
462
+ if (existing.details.cliShim.exists) {
463
+ log(` [OK] CLI Shims (agy-context): Installed in ${antigravityBinDir}`);
210
464
  }
211
- log("\n⚔ Re-verifying and synchronizing to latest version...");
465
+ log(`\nRe-verifying and synchronizing to v${PACKAGE_VERSION}...`);
212
466
  } else {
213
- log("⚔ Fresh installation of Agy-Context-Saver (Zero-Delay Mode)...");
467
+ log(`Fresh installation of Agy-Context-Saver v${PACKAGE_VERSION}...`);
214
468
  }
215
469
 
216
470
  // 0. Pre-flight Validation: Verify configuration directory write access
@@ -220,10 +474,12 @@ export async function runInstall(options = {}) {
220
474
  await fsPromises.writeFile(probeFile, "ok", "utf-8");
221
475
  await fsPromises.unlink(probeFile);
222
476
  } catch (err) {
223
- throw new Error(`Pre-flight check failed: cannot write to Antigravity configuration directory (${geminiConfigDir}): ${err.message}`);
477
+ throw new Error(
478
+ `Pre-flight check failed: cannot write to Antigravity configuration directory (${geminiConfigDir}): ${err.message}`
479
+ );
224
480
  }
225
481
 
226
- // 0a. Ensure RTK is provisioned and available
482
+ // 0a. Ensure RTK (>= 1.1.0) is provisioned and available
227
483
  try {
228
484
  const rtkResult = await ensureRtkInstalled({ silent: options.silent, existingRtk: existing.details.rtk });
229
485
  log(`[OK] RTK Command Optimizer verified: ${rtkResult.version} (${rtkResult.path})`);
@@ -231,14 +487,17 @@ export async function runInstall(options = {}) {
231
487
  if (options.allowDegraded) {
232
488
  warn(`[WARN] RTK provisioning check encountered an issue: ${err.message}. Continuing in degraded mode.`);
233
489
  } else {
234
- throw new Error(`RTK provisioning failed: ${err.message}. To install without RTK (degraded mode), re-run with --allow-degraded.`);
490
+ throw new Error(
491
+ `RTK provisioning failed: ${err.message}. To install without RTK (degraded mode), re-run with --allow-degraded.`
492
+ );
235
493
  }
236
494
  }
237
495
 
238
- // 1. Instant Native Plugin Junction / Symlink or Permanent Copy (Takes ~5ms)
496
+ // 1. Native Plugin Junction / Symlink or Permanent Copy
239
497
  await fsPromises.mkdir(pluginsDir, { recursive: true });
240
498
  const isGitRepo = fs.existsSync(path.join(repoRoot, ".git"));
241
- const isTempOrNpx = !isGitRepo || repoRoot.includes("_npx") || repoRoot.includes("npm-cache") || repoRoot.includes("node_modules");
499
+ const isTempOrNpx =
500
+ !isGitRepo || repoRoot.includes("_npx") || repoRoot.includes("npm-cache") || repoRoot.includes("node_modules");
242
501
  const copyFilter = (src) => {
243
502
  const rel = path.relative(repoRoot, src);
244
503
  if (!rel) return true;
@@ -255,7 +514,7 @@ export async function runInstall(options = {}) {
255
514
  const isWin = os.platform() === "win32";
256
515
  fs.symlinkSync(repoRoot, pluginDest, isWin ? "junction" : "dir");
257
516
  log(`[OK] Native Antigravity Plugin linked: ${pluginDest} -> ${repoRoot}`);
258
- } catch (err) {
517
+ } catch {
259
518
  await fsPromises.cp(repoRoot, pluginDest, { recursive: true, filter: copyFilter });
260
519
  log(`[OK] Native Antigravity Plugin copied to: ${pluginDest}`);
261
520
  }
@@ -269,246 +528,85 @@ export async function runInstall(options = {}) {
269
528
  log(`[OK] Native Antigravity Plugin verified: ${pluginDest}`);
270
529
  }
271
530
 
272
- // 2. Ensure scripts dir exists & copy hook script asynchronously
531
+ // 2. Dual-surface hook governance (plugin hooks.json + global hooks.json for /swarm workers):
532
+ // Mirror execution-guard-hook.mjs to ~/.gemini/config/scripts/execution-guard-hook.mjs and register
533
+ // "agy-context-saver" in ~/.gemini/config/hooks.json so native /swarm workers (which load global hooks.json
534
+ // rather than plugin hooks.json) execute PreToolUse reliably alongside coordinator idempotency deduplication.
273
535
  await fsPromises.mkdir(scriptsDir, { recursive: true });
274
536
  const hookUpdated = await safeCopyFileIfChanged(sourceHook, destHook);
275
- log(`[OK] Governor hook script mirrored to: ${destHook}${hookUpdated ? "" : " (up-to-date)"}`);
537
+ log(`[OK] Mirrored hook script ${hookUpdated ? "updated" : "verified"}: ${destHook}`);
276
538
 
277
- // 3. Register Hook in ~/.gemini/config/hooks.json (Preserves existing hooks + creates backup)
278
- let hooksConfig = validatedHooks ? { ...validatedHooks } : {};
279
- if (fs.existsSync(hooksJsonPath) && !fs.existsSync(hooksBakPath)) {
280
- try {
281
- await fsPromises.copyFile(hooksJsonPath, hooksBakPath);
282
- log(`[OK] Created configuration backup: ${hooksBakPath}`);
283
- } catch (err) {
284
- warn(`[WARN] Could not create backup of ${hooksJsonPath}: ${err.message}`);
285
- }
286
- }
287
-
288
- const alreadyHadHook = Boolean(hooksConfig["execution-guard"]);
289
- if (hooksConfig["agy-context-saver"]) {
290
- delete hooksConfig["agy-context-saver"];
291
- }
292
- hooksConfig["execution-guard"] = {
539
+ const hookMatcher = getExpectedHookMatcher();
540
+ const nextHooks = validatedHooks ? { ...validatedHooks } : {};
541
+ const hadLegacyHook = Boolean(nextHooks[LEGACY_HOOK_KEY]);
542
+ delete nextHooks[LEGACY_HOOK_KEY];
543
+ nextHooks[HOOK_KEY] = {
293
544
  PreToolUse: [
294
545
  {
295
- matcher: "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir|replace_file_content|write_to_file",
546
+ matcher: hookMatcher,
296
547
  hooks: [
297
548
  {
298
549
  type: "command",
299
550
  command: `node "${destHookNormalized}"`,
300
- timeout: 5
551
+ timeout: 10
301
552
  }
302
553
  ]
303
554
  }
304
555
  ]
305
556
  };
306
- const hooksUpdated = await safeWriteFileIfChanged(hooksJsonPath, JSON.stringify(hooksConfig, null, 2));
307
- log(`[OK] Lifecycle hook ${alreadyHadHook ? "re-verified" : "registered"} in: ${hooksJsonPath}${hooksUpdated ? "" : " (up-to-date)"}`);
308
-
309
- // 4. Register MCP Server in ~/.gemini/config/mcp_config.json (Preserves existing servers + creates backup)
310
- let mcpConfig = validatedMcp ? { ...validatedMcp } : { mcpServers: {} };
311
- if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object") {
312
- mcpConfig.mcpServers = {};
313
- } else {
314
- mcpConfig.mcpServers = { ...mcpConfig.mcpServers };
557
+ const nextHooksRaw = JSON.stringify(nextHooks, null, 2);
558
+ let hooksNeedsBackup = false;
559
+ if (fs.existsSync(hooksJsonPath) && !fs.existsSync(hooksBakPath)) {
560
+ try {
561
+ const currentRaw = fs.readFileSync(hooksJsonPath, "utf-8");
562
+ hooksNeedsBackup = currentRaw !== nextHooksRaw && (hadLegacyHook || Object.keys(validatedHooks || {}).length > 0);
563
+ } catch {}
315
564
  }
316
- if (fs.existsSync(mcpJsonPath) && !fs.existsSync(mcpBakPath)) {
565
+ if (hooksNeedsBackup) {
317
566
  try {
318
- await fsPromises.copyFile(mcpJsonPath, mcpBakPath);
319
- log(`[OK] Created configuration backup: ${mcpBakPath}`);
567
+ await fsPromises.copyFile(hooksJsonPath, hooksBakPath);
568
+ log(`[OK] Created configuration backup: ${hooksBakPath}`);
320
569
  } catch (err) {
321
- warn(`[WARN] Could not create backup of ${mcpJsonPath}: ${err.message}`);
570
+ warn(`[WARN] Could not create backup of ${hooksJsonPath}: ${err.message}`);
322
571
  }
323
572
  }
324
-
325
- const alreadyHadMcp = Boolean(mcpConfig.mcpServers["agy-context-saver"]);
326
- mcpConfig.mcpServers["agy-context-saver"] = {
327
- command: "node",
328
- args: [mcpIndexPath]
329
- };
330
- const mcpUpdated = await safeWriteFileIfChanged(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
331
- log(`[OK] MCP Server ${alreadyHadMcp ? "re-verified" : "registered"} in: ${mcpJsonPath}${mcpUpdated ? "" : " (up-to-date)"}`);
332
-
333
- // 5. Mirror Antigravity Tool Schemas in parallel
334
- await fsPromises.mkdir(antigravityMcpDir, { recursive: true });
335
-
336
- // Clean up retired schemas if present
337
- const retiredSchemas = [
338
- "safe_command.json",
339
- "get_spillover_content.json",
340
- "read_task_output.json",
341
- "get_step_detail.json",
342
- // Subagent tools moved to the separate Agy-Squad package (2026-10-15)
343
- "subagent_brief.json",
344
- "dispatch_squad.json"
345
- ];
346
- for (const s of retiredSchemas) {
347
- const sPath = path.join(antigravityMcpDir, s);
348
- if (fs.existsSync(sPath)) {
349
- try {
350
- await fsPromises.unlink(sPath);
351
- log(`[OK] Cleaned up retired schema from: ${sPath}`);
352
- } catch {}
573
+ const hooksChanged = await safeWriteFileIfChanged(hooksJsonPath, nextHooksRaw);
574
+ log(`[OK] Lifecycle hook (${HOOK_KEY}) ${hooksChanged ? "registered" : "verified"} in: ${hooksJsonPath} + plugin hooks.json`);
575
+
576
+ // 3. Ensure ~/.gemini/config/config.json enables plugin if present
577
+ if (validatedConfig && typeof validatedConfig === "object") {
578
+ const prevPluginEntry = validatedConfig.plugins?.[HOOK_KEY];
579
+ if (!prevPluginEntry || prevPluginEntry.enabled !== true) {
580
+ const nextConfig = {
581
+ ...validatedConfig,
582
+ plugins: {
583
+ ...(validatedConfig.plugins && typeof validatedConfig.plugins === "object" ? validatedConfig.plugins : {}),
584
+ [HOOK_KEY]: {
585
+ ...(prevPluginEntry && typeof prevPluginEntry === "object" ? prevPluginEntry : {}),
586
+ enabled: true
587
+ }
588
+ }
589
+ };
590
+ await safeWriteFileIfChanged(configJsonPath, JSON.stringify(nextConfig, null, 2));
591
+ log(`[OK] Enabled plugin '${HOOK_KEY}' in: ${configJsonPath}`);
353
592
  }
354
593
  }
355
594
 
356
- // Versions up to 1.7.0 kept hook state in the shared temp folder; it now lives in getStateDir()
595
+ // 4. Retire default MCP registration & sweep all agy-context-saver* schema directories (ACS-HOOK-11)
596
+ await stripLegacyMcpRegistrations(validatedMcp, { log, warn });
597
+ await sweepLegacyMcpSchemas({ log, warn });
598
+
599
+ // 5. Install executable CLI shims (agy-context, agy-context-saver) into ~/.gemini/antigravity/bin/
600
+ await installCliShims({ log });
601
+
602
+ // 6. Clean up legacy temp-folder state from versions <= 1.7.0
357
603
  for (const legacy of removeLegacyTmpState()) {
358
604
  log(`[OK] Removed legacy temp-folder state: ${legacy}`);
359
605
  }
360
606
 
361
- const instructionsSourcePath = path.join(repoRoot, "mcp", "instructions.md");
362
- const instructionsContent = await fsPromises.readFile(instructionsSourcePath, "utf-8");
363
-
364
- const checkHealthSchema = {
365
- name: "check_context_health",
366
- description: "Inspects a conversation transcript (transcript.jsonl) to diagnose turn count, payload size, tool polling loops, and context degradation risk. Accepts full file path, conversation UUID, or empty for active session.",
367
- parameters: {
368
- type: "object",
369
- properties: {
370
- transcriptPath: {
371
- type: "string",
372
- description: "Path to transcript.jsonl file, conversation UUID, or empty for active conversation."
373
- },
374
- conversationId: {
375
- type: "string",
376
- description: "Conversation UUID or folder name (alternative to transcriptPath)."
377
- }
378
- }
379
- }
380
- };
381
-
382
- const getInstallationStatusSchema = {
383
- name: "get_installation_status",
384
- description: "Inspect the live installation status of Agy-Context-Saver across all Antigravity integration points (Native Plugin Link, Governor Hook, MCP Server, and Tool Schemas).",
385
- parameters: {
386
- type: "object",
387
- properties: {},
388
- required: []
389
- }
390
- };
391
-
392
- const syncInstallationSchema = {
393
- name: "sync_installation",
394
- description: "Re-verify and synchronize Agy-Context-Saver installation, updating the governor hook, plugin link, and tool schemas in ~25ms without terminal shell commands.",
395
- parameters: {
396
- type: "object",
397
- properties: {
398
- checkOnly: {
399
- type: "boolean",
400
- description: "If true, performs a pre-flight audit without writing changes (default: false)."
401
- }
402
- },
403
- required: []
404
- }
405
- };
406
-
407
- const readTranscriptSchema = {
408
- name: "read_transcript",
409
- description: "Quickly read recent conversation history from an Antigravity transcript in clean Markdown format with zero JSON noise. Supports 'compact' (transcript.jsonl) and 'full' (transcript_full.jsonl) modes with minimal parameters.",
410
- parameters: {
411
- type: "object",
412
- properties: {
413
- conversationId: {
414
- type: "string",
415
- description: "Conversation ID (UUID), folder name, or full path to transcript.jsonl. If omitted, defaults to active conversation."
416
- },
417
- mode: {
418
- type: "string",
419
- enum: ["compact", "full"],
420
- description: "Transcript mode: 'compact' reads transcript.jsonl; 'full' reads transcript_full.jsonl (default: 'compact')."
421
- },
422
- lastTurns: {
423
- type: "number",
424
- description: "Number of most recent conversation turns to display (default: 3). Set to 0 to read all."
425
- },
426
- includeThinking: {
427
- type: "boolean",
428
- description: "Whether to include model thinking / internal reasoning blocks (default: false)."
429
- }
430
- },
431
- required: ["conversationId"]
432
- }
433
- };
434
-
435
- const queryTranscriptSchema = {
436
- name: "query_transcript",
437
- description: "Granular query and forensic filtering engine for Antigravity conversation transcripts. Searches by keyword or regex, filters by role (user, assistant, subagent, tool, error), slices step ranges, and automatically dereferences full content when truncated.",
438
- parameters: {
439
- type: "object",
440
- properties: {
441
- conversationId: {
442
- type: "string",
443
- description: "Conversation ID (UUID), folder name, or full path to transcript.jsonl."
444
- },
445
- query: {
446
- type: "string",
447
- description: "Search keyword or regex pattern to match across message content, tool calls, and thinking."
448
- },
449
- roles: {
450
- type: "array",
451
- items: { type: "string" },
452
- description: "Filter steps by role/source: 'user', 'assistant', 'subagent', 'tool', 'error', or 'all' (default: ['user', 'assistant'])."
453
- },
454
- mode: {
455
- type: "string",
456
- enum: ["auto", "compact", "full"],
457
- description: "Mode: 'auto' streams transcript.jsonl and dereferences matched truncated lines from transcript_full.jsonl; 'compact' reads transcript.jsonl only; 'full' reads transcript_full.jsonl only (default: 'auto')."
458
- },
459
- startStep: {
460
- type: "number",
461
- description: "Start step_index (inclusive)."
462
- },
463
- endStep: {
464
- type: "number",
465
- description: "End step_index (inclusive)."
466
- },
467
- lastTurns: {
468
- type: "number",
469
- description: "Limit to last N matched turns."
470
- },
471
- includeThinking: {
472
- type: "boolean",
473
- description: "Include model thinking blocks in output (default: false)."
474
- },
475
- includeToolCalls: {
476
- type: "boolean",
477
- description: "Include tool call arguments and results in output (default: false)."
478
- },
479
- maxResults: {
480
- type: "number",
481
- description: "Maximum number of matched steps to return (default: 5)."
482
- },
483
- summaryOnly: {
484
- type: "boolean",
485
- description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
486
- },
487
- stepIndex: {
488
- type: "number",
489
- description: "Optional step_index to surgically retrieve full un-truncated content for a single step from transcript_full.jsonl."
490
- },
491
- field: {
492
- type: "string",
493
- description: "Optional specific field to extract when stepIndex is provided (e.g. 'content', 'thinking', 'tool_calls', 'tool_args', or 'tool_args:Message'). Defaults to full step."
494
- }
495
- },
496
- required: ["conversationId"]
497
- }
498
- };
499
-
500
- await Promise.all([
501
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "instructions.md"), instructionsContent),
502
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "check_context_health.json"), JSON.stringify(checkHealthSchema, null, 2)),
503
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "get_installation_status.json"), JSON.stringify(getInstallationStatusSchema, null, 2)),
504
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "sync_installation.json"), JSON.stringify(syncInstallationSchema, null, 2)),
505
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "read_transcript.json"), JSON.stringify(readTranscriptSchema, null, 2)),
506
- safeWriteFileIfChanged(path.join(antigravityMcpDir, "query_transcript.json"), JSON.stringify(queryTranscriptSchema, null, 2))
507
- ]);
508
-
509
607
  const elapsed = (performance.now() - startTime).toFixed(1);
510
608
  const statusVerb = existing.isInstalled ? "synchronized & up-to-date" : "completed";
511
- log(`\nāœ“ Installation ${statusVerb} in ${elapsed}ms (Zero Delay)!\n`);
609
+ log(`\nInstallation ${statusVerb} in ${elapsed}ms!\n`);
512
610
  const postInstall = detectExistingInstallation();
513
611
  return { ...postInstall, isInstalled: true, elapsedMs: elapsed };
514
612
  }
@@ -538,40 +636,56 @@ export async function runUninstall(options = {}) {
538
636
  }
539
637
  }
540
638
 
541
- // 3. Remove from hooks.json
639
+ // 3. Remove both current and legacy hook keys from hooks.json, and plugin toggle from config.json
542
640
  if (fs.existsSync(hooksJsonPath)) {
543
641
  try {
544
642
  const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
545
- delete hooksConfig["execution-guard"];
546
- delete hooksConfig["agy-context-saver"];
547
- await fsPromises.writeFile(hooksJsonPath, JSON.stringify(hooksConfig, null, 2), "utf-8");
548
- log(`[OK] Removed from: ${hooksJsonPath}`);
643
+ if (hooksConfig && typeof hooksConfig === "object") {
644
+ delete hooksConfig[LEGACY_HOOK_KEY];
645
+ delete hooksConfig[HOOK_KEY];
646
+ await safeWriteFileIfChanged(hooksJsonPath, JSON.stringify(hooksConfig, null, 2));
647
+ log(`[OK] Removed hook registration from: ${hooksJsonPath}`);
648
+ }
549
649
  } catch {}
550
650
  }
551
-
552
- // 4. Remove from mcp_config.json
553
- if (fs.existsSync(mcpJsonPath)) {
651
+ if (fs.existsSync(configJsonPath)) {
554
652
  try {
555
- const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
556
- if (mcpConfig.mcpServers) {
557
- delete mcpConfig.mcpServers["agy-context-saver"];
653
+ const cfg = JSON.parse(fs.readFileSync(configJsonPath, "utf-8"));
654
+ if (cfg && typeof cfg === "object" && cfg.plugins && typeof cfg.plugins === "object" && HOOK_KEY in cfg.plugins) {
655
+ delete cfg.plugins[HOOK_KEY];
656
+ await safeWriteFileIfChanged(configJsonPath, JSON.stringify(cfg, null, 2));
657
+ log(`[OK] Removed '${HOOK_KEY}' plugin entry from: ${configJsonPath}`);
558
658
  }
559
- await fsPromises.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
560
- log(`[OK] Removed from: ${mcpJsonPath}`);
561
659
  } catch {}
562
660
  }
563
661
 
564
- // 5. Remove mirrored Antigravity MCP schemas directory
565
- if (fs.existsSync(antigravityMcpDir)) {
662
+ // 4. Strip any agy-context-saver* keys from mcp_config.json (preserving foreign servers, creating .bak if absent)
663
+ if (fs.existsSync(mcpJsonPath)) {
566
664
  try {
567
- await fsPromises.rm(antigravityMcpDir, { recursive: true, force: true });
568
- log(`[OK] Removed mirrored MCP schema directory: ${antigravityMcpDir}`);
569
- } catch (err) {
570
- warn(`[WARN] Could not remove schema directory: ${err.message}`);
665
+ const parsedMcp = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
666
+ await stripLegacyMcpRegistrations(parsedMcp, { log, warn });
667
+ } catch {}
668
+ }
669
+
670
+ // 5. Sweep ALL agy-context-saver* directories under ~/.gemini/antigravity/mcp/
671
+ await sweepLegacyMcpSchemas({ log, warn });
672
+
673
+ // 6. Remove CLI shims from ~/.gemini/antigravity/bin/
674
+ for (const name of CLI_SHIM_NAMES) {
675
+ for (const candidate of [name, `${name}.cmd`]) {
676
+ const shimPath = path.join(antigravityBinDir, candidate);
677
+ if (fs.existsSync(shimPath)) {
678
+ try {
679
+ await fsPromises.unlink(shimPath);
680
+ log(`[OK] Removed CLI shim: ${shimPath}`);
681
+ } catch (err) {
682
+ warn(`[WARN] Could not remove CLI shim ${shimPath}: ${err.message}`);
683
+ }
684
+ }
571
685
  }
572
686
  }
573
687
 
574
- // 6. Remove hook state: the per-user state folder and anything older versions left in the temp folder
688
+ // 7. Remove hook state: the per-user state folder and anything older versions left in the temp folder
575
689
  const stateDir = getStateDir();
576
690
  if (fs.existsSync(stateDir)) {
577
691
  try {
@@ -585,12 +699,12 @@ export async function runUninstall(options = {}) {
585
699
  log(`[OK] Removed legacy temp-folder state: ${legacy}`);
586
700
  }
587
701
 
588
- // 7. Optional: Restore configuration files from backups if requested
702
+ // 8. Optional: Restore configuration files from backups if requested
589
703
  if (options.restoreBackups) {
590
704
  await runRestoreBackups(options);
591
705
  }
592
706
 
593
- log("\nāœ“ Uninstallation complete.\n");
707
+ log("\nUninstallation complete.\n");
594
708
  }
595
709
 
596
710
  export async function runRestoreBackups(options = {}) {
@@ -630,12 +744,31 @@ export function runStatus(options = {}) {
630
744
  const existing = detectExistingInstallation();
631
745
  log("Checking Agy-Context-Saver installation status...\n");
632
746
  log(`- Native Plugin Link: ${existing.details.plugin.exists ? "ACTIVE (" + pluginDest + ")" : "NOT LINKED"}`);
633
- log(`- Execution Governor Hook: ${existing.details.hook.registered ? "REGISTERED in hooks.json" : "NOT REGISTERED"}`);
634
- log(`- Universal MCP Server: ${existing.details.mcp.registered ? "CONFIGURED in mcp_config.json" : "NOT CONFIGURED"}`);
635
- log(`- Antigravity Tool Schemas: ${existing.details.schemas.exists ? "PRESENT (5 Schemas)" : "MISSING"}`);
636
- log(`- RTK Command Optimizer: ${existing.details.rtk ? "INSTALLED (" + existing.details.rtk.version + ")" : "MISSING"}`);
637
-
638
- log(`\nOverall Status: ${existing.isComplete ? "HEALTHY & ACTIVE šŸ›”ļø" : existing.isInstalled ? "PARTIAL INSTALLATION" : "NOT INSTALLED"}\n`);
747
+ const hookStatusLabel = existing.details.hook.registered
748
+ ? "ACTIVE (plugin + global hooks.json)"
749
+ : existing.details.hook.pluginHookRegistered
750
+ ? "PARTIAL (plugin hooks.json only; global /swarm hook missing)"
751
+ : "NOT REGISTERED";
752
+ log(
753
+ `- Execution Governor Hook (${HOOK_KEY}): ${hookStatusLabel}${existing.details.hook.legacyHookPresent ? " (legacy 'execution-guard' in global hooks.json)" : ""}`
754
+ );
755
+ log(
756
+ `- CLI Shim (agy-context): ${existing.details.cliShim.exists ? "INSTALLED (" + antigravityBinDir + ")" : "MISSING"}`
757
+ );
758
+ log(
759
+ `- RTK Command Optimizer (>= 1.1.0): ${existing.details.rtk ? "INSTALLED (" + existing.details.rtk.version + ")" : "MISSING"}`
760
+ );
761
+ log(
762
+ `- MCP Cleanup: ${existing.details.mcp.clean ? "CLEAN (0 duplicate MCP configs/schemas)" : existing.details.mcp.duplicateCount + " legacy MCP config/schema entries present"}`
763
+ );
764
+ const ta = existing.details.transcriptAccess || formatTranscriptAccessStatus();
765
+ log(
766
+ `- Transcript Access Policy: ${ta.active ? `${ta.mode.toUpperCase()} (owner: ${ta.ownerId})` : "OPEN (default)"}`
767
+ );
768
+
769
+ log(
770
+ `\nOverall Status: ${existing.isComplete ? "HEALTHY & ACTIVE" : existing.isInstalled ? "PARTIAL INSTALLATION" : "NOT INSTALLED"}\n`
771
+ );
639
772
  return existing;
640
773
  }
641
774
 
@@ -646,24 +779,26 @@ export {
646
779
  hooksBakPath,
647
780
  mcpJsonPath,
648
781
  mcpBakPath,
649
- antigravityMcpDir
782
+ antigravityMcpRootDir,
783
+ antigravityMcpDir,
784
+ antigravityBinDir
650
785
  };
651
786
 
652
787
  function printCliHelp() {
653
788
  console.log(`
654
- Agy-Context-Saver Registration & Lifecycle Engine
789
+ Agy-Context-Saver Registration & Lifecycle Engine (v${PACKAGE_VERSION})
655
790
 
656
791
  Usage:
657
792
  node scripts/install-register.mjs [command/option]
658
793
 
659
794
  Commands & Options:
660
- --status, status Check live integration status across all layers + RTK
661
- --check, -c Run pre-flight validation check without writing changes
662
- --uninstall, uninstall Remove governor hook, plugin link, and MCP server
795
+ --status, status Check live integration status (plugin, hook, CLI shim, RTK >= 1.1.0, MCP cleanup)
796
+ --check, -c Run read-only pre-flight validation check without writing changes
797
+ --uninstall, uninstall Remove governor hook, plugin link, CLI shims, and legacy MCP configs/schemas
663
798
  --restore-backups Restore original configuration backup files (*.bak) (standalone or during uninstall)
664
799
  --allow-degraded Allow installation to proceed even if RTK optimizer provisioning fails
665
800
  --help, -h, help Show this help message
666
- (no arguments) Synchronize and verify full 4-layer installation
801
+ (no arguments) Synchronize and verify v${PACKAGE_VERSION} CLI-first installation
667
802
  `);
668
803
  }
669
804
 
@@ -684,7 +819,9 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
684
819
 
685
820
  // Reject conflicting read-only and mutating flags explicitly
686
821
  if ((isCheck || isStatus) && (isUninstall || isRestoreBackups)) {
687
- console.error("Error: Read-only flags (--check, --status) cannot be combined with mutating flags (--uninstall, --restore-backups).");
822
+ console.error(
823
+ "Error: Read-only flags (--check, --status) cannot be combined with mutating flags (--uninstall, --restore-backups)."
824
+ );
688
825
  process.exit(1);
689
826
  }
690
827
  if (isCheck && isStatus) {
@@ -692,15 +829,20 @@ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.me
692
829
  process.exit(1);
693
830
  }
694
831
 
695
- if (isCheck) {
696
- await runInstall({ checkOnly: true, allowDegraded });
697
- } else if (isStatus) {
698
- runStatus();
699
- } else if (isUninstall) {
700
- await runUninstall({ restoreBackups: isRestoreBackups });
701
- } else if (isRestoreBackups) {
702
- await runRestoreBackups();
703
- } else {
704
- await runInstall({ allowDegraded });
832
+ try {
833
+ if (isCheck) {
834
+ await runInstall({ checkOnly: true, allowDegraded });
835
+ } else if (isStatus) {
836
+ runStatus();
837
+ } else if (isUninstall) {
838
+ await runUninstall({ restoreBackups: isRestoreBackups });
839
+ } else if (isRestoreBackups) {
840
+ await runRestoreBackups();
841
+ } else {
842
+ await runInstall({ allowDegraded });
843
+ }
844
+ } catch (err) {
845
+ console.error(`Error: ${err && err.message ? err.message : String(err)}`);
846
+ process.exit(1);
705
847
  }
706
848
  }