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.
- package/CHANGELOG.md +27 -1
- package/README.md +52 -220
- package/assets/architecture-overview.svg +39 -0
- package/bin/agy-context.mjs +616 -0
- package/hooks.json +3 -3
- package/install.ps1 +1 -1
- package/lib/transcript-core.mjs +2310 -0
- package/package.json +11 -10
- package/plugin.json +6 -6
- package/rules/AGENTS.md +47 -79
- package/scripts/execution-guard-hook.mjs +3239 -375
- package/scripts/install-register.mjs +465 -323
- package/scripts/provision-rtk.mjs +35 -26
- package/scripts/release-preflight.mjs +75 -11
- package/scripts/state-dir.mjs +1 -0
- package/skills/context-governance/SKILL.md +85 -79
- package/mcp/index.js +0 -1332
- package/mcp/instructions.md +0 -33
- package/mcp_config.json +0 -8
|
@@ -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
|
|
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
|
|
9
|
-
* - Re-verifies and updates
|
|
10
|
-
* -
|
|
11
|
-
* - Supports silent mode
|
|
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
|
|
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
|
|
92
|
-
let
|
|
93
|
-
|
|
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
|
|
96
|
-
const h =
|
|
185
|
+
const pCfg = JSON.parse(fs.readFileSync(pluginHooksPath, "utf-8"));
|
|
186
|
+
const h = pCfg?.[HOOK_KEY];
|
|
97
187
|
if (h) {
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
105
|
-
let
|
|
106
|
-
|
|
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(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
126
|
-
|
|
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: {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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("
|
|
423
|
+
log("Existing installation detected:");
|
|
177
424
|
if (existing.details.plugin.exists) {
|
|
178
|
-
log(`
|
|
425
|
+
log(` [OK] Native Plugin Link: ${pluginDest}`);
|
|
179
426
|
}
|
|
180
427
|
if (existing.details.hook.registered) {
|
|
181
|
-
log(`
|
|
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.
|
|
184
|
-
log(`
|
|
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.
|
|
187
|
-
log(
|
|
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.
|
|
446
|
+
log("\n[OK] Pre-flight check complete. Zero changes written.");
|
|
190
447
|
} else {
|
|
191
|
-
log("
|
|
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("
|
|
455
|
+
log("Existing installation detected:");
|
|
199
456
|
if (existing.details.plugin.exists) {
|
|
200
|
-
log(`
|
|
457
|
+
log(` [OK] Native Plugin Link: ${pluginDest}`);
|
|
201
458
|
}
|
|
202
459
|
if (existing.details.hook.registered) {
|
|
203
|
-
log(`
|
|
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.
|
|
209
|
-
log(`
|
|
462
|
+
if (existing.details.cliShim.exists) {
|
|
463
|
+
log(` [OK] CLI Shims (agy-context): Installed in ${antigravityBinDir}`);
|
|
210
464
|
}
|
|
211
|
-
log(
|
|
465
|
+
log(`\nRe-verifying and synchronizing to v${PACKAGE_VERSION}...`);
|
|
212
466
|
} else {
|
|
213
|
-
log(
|
|
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(
|
|
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(
|
|
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.
|
|
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 =
|
|
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
|
|
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.
|
|
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]
|
|
537
|
+
log(`[OK] Mirrored hook script ${hookUpdated ? "updated" : "verified"}: ${destHook}`);
|
|
276
538
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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:
|
|
546
|
+
matcher: hookMatcher,
|
|
296
547
|
hooks: [
|
|
297
548
|
{
|
|
298
549
|
type: "command",
|
|
299
550
|
command: `node "${destHookNormalized}"`,
|
|
300
|
-
timeout:
|
|
551
|
+
timeout: 10
|
|
301
552
|
}
|
|
302
553
|
]
|
|
303
554
|
}
|
|
304
555
|
]
|
|
305
556
|
};
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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 (
|
|
565
|
+
if (hooksNeedsBackup) {
|
|
317
566
|
try {
|
|
318
|
-
await fsPromises.copyFile(
|
|
319
|
-
log(`[OK] Created configuration backup: ${
|
|
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 ${
|
|
570
|
+
warn(`[WARN] Could not create backup of ${hooksJsonPath}: ${err.message}`);
|
|
322
571
|
}
|
|
323
572
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
//
|
|
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(`\
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
|
556
|
-
if (
|
|
557
|
-
delete
|
|
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
|
-
//
|
|
565
|
-
if (fs.existsSync(
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
} catch
|
|
570
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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("\
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
log(
|
|
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
|
-
|
|
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
|
|
661
|
-
--check, -c Run pre-flight validation check without writing changes
|
|
662
|
-
--uninstall, uninstall Remove governor hook, plugin link, and MCP
|
|
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
|
|
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(
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
}
|