agy-context-saver 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -2
- package/hooks.json +16 -0
- package/install.ps1 +42 -0
- package/install.sh +6 -0
- package/mcp/index.js +1085 -0
- package/mcp/instructions.md +30 -0
- package/mcp_config.json +8 -0
- package/package.json +41 -6
- package/plugin.json +5 -0
- package/rules/AGENTS.md +78 -0
- package/scripts/execution-guard-hook.mjs +575 -0
- package/scripts/install-register.mjs +620 -0
- package/scripts/provision-rtk.mjs +120 -0
- package/skills/context-governance/SKILL.md +130 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agy-Context-Saver Registration & Installation Engine
|
|
5
|
+
* Zero-delay cross-platform installer with intelligent existing-installation detection.
|
|
6
|
+
*
|
|
7
|
+
* 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
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { ensureRtkInstalled, findExistingRtk } from "./provision-rtk.mjs";
|
|
20
|
+
|
|
21
|
+
const fsPromises = fs.promises;
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const repoRoot = path.resolve(__dirname, "..");
|
|
24
|
+
|
|
25
|
+
const homeDir = os.homedir();
|
|
26
|
+
const geminiConfigDir = path.join(homeDir, ".gemini", "config");
|
|
27
|
+
const pluginsDir = path.join(geminiConfigDir, "plugins");
|
|
28
|
+
const pluginDest = path.join(pluginsDir, "agy-context-saver");
|
|
29
|
+
const scriptsDir = path.join(geminiConfigDir, "scripts");
|
|
30
|
+
const hooksJsonPath = path.join(geminiConfigDir, "hooks.json");
|
|
31
|
+
const hooksBakPath = path.join(geminiConfigDir, "hooks.json.bak");
|
|
32
|
+
const mcpJsonPath = path.join(geminiConfigDir, "mcp_config.json");
|
|
33
|
+
const mcpBakPath = path.join(geminiConfigDir, "mcp_config.json.bak");
|
|
34
|
+
const antigravityMcpDir = path.join(homeDir, ".gemini", "antigravity", "mcp", "agy-context-saver");
|
|
35
|
+
|
|
36
|
+
const sourceHook = path.join(repoRoot, "scripts", "execution-guard-hook.mjs");
|
|
37
|
+
const destHook = path.join(scriptsDir, "execution-guard-hook.mjs");
|
|
38
|
+
const mcpIndexPath = path.join(pluginDest, "mcp", "index.js").replace(/\\/g, "/");
|
|
39
|
+
const destHookNormalized = destHook.replace(/\\/g, "/");
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write file only if content has changed, eliminating disk churn.
|
|
43
|
+
*/
|
|
44
|
+
export async function safeWriteFileIfChanged(targetPath, newContent) {
|
|
45
|
+
try {
|
|
46
|
+
if (fs.existsSync(targetPath)) {
|
|
47
|
+
const existing = await fsPromises.readFile(targetPath, "utf-8");
|
|
48
|
+
if (existing === newContent) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {}
|
|
53
|
+
await fsPromises.writeFile(targetPath, newContent, "utf-8");
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Copy file only if content differs.
|
|
59
|
+
*/
|
|
60
|
+
export async function safeCopyFileIfChanged(sourcePath, targetPath) {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(targetPath)) {
|
|
63
|
+
const [srcBuf, destBuf] = await Promise.all([
|
|
64
|
+
fsPromises.readFile(sourcePath),
|
|
65
|
+
fsPromises.readFile(targetPath)
|
|
66
|
+
]);
|
|
67
|
+
if (srcBuf.equals(destBuf)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
await fsPromises.copyFile(sourcePath, targetPath);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detects whether Agy-Context-Saver is already installed across all Antigravity integration points.
|
|
78
|
+
*/
|
|
79
|
+
export function detectExistingInstallation() {
|
|
80
|
+
const pluginExists = fs.existsSync(pluginDest);
|
|
81
|
+
let pluginTarget = null;
|
|
82
|
+
if (pluginExists) {
|
|
83
|
+
try {
|
|
84
|
+
pluginTarget = fs.readlinkSync(pluginDest);
|
|
85
|
+
} catch {
|
|
86
|
+
pluginTarget = "direct folder or junction";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let hookRegistered = false;
|
|
91
|
+
let hookCommand = null;
|
|
92
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const cfg = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
|
|
95
|
+
const h = cfg["execution-guard"] || cfg["agy-context-saver"];
|
|
96
|
+
if (h) {
|
|
97
|
+
hookRegistered = true;
|
|
98
|
+
hookCommand = h.PreToolUse?.[0]?.hooks?.[0]?.command;
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let mcpRegistered = false;
|
|
104
|
+
let mcpArgs = null;
|
|
105
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
106
|
+
try {
|
|
107
|
+
const cfg = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
|
|
108
|
+
if (cfg.mcpServers?.["agy-context-saver"]) {
|
|
109
|
+
mcpRegistered = true;
|
|
110
|
+
mcpArgs = cfg.mcpServers["agy-context-saver"].args;
|
|
111
|
+
}
|
|
112
|
+
} catch {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scriptExists = fs.existsSync(destHook);
|
|
116
|
+
const schemasExist =
|
|
117
|
+
fs.existsSync(path.join(antigravityMcpDir, "check_context_health.json")) &&
|
|
118
|
+
fs.existsSync(path.join(antigravityMcpDir, "subagent_brief.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
|
+
|
|
124
|
+
const rtkInfo = findExistingRtk();
|
|
125
|
+
const isInstalled = pluginExists || (hookRegistered && mcpRegistered);
|
|
126
|
+
const isComplete = (pluginExists || (hookRegistered && mcpRegistered && scriptExists)) && schemasExist && Boolean(rtkInfo);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
isInstalled,
|
|
130
|
+
isComplete,
|
|
131
|
+
details: {
|
|
132
|
+
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
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function runInstall(options = {}) {
|
|
142
|
+
const log = options.silent ? () => {} : console.log;
|
|
143
|
+
const warn = options.silent ? () => {} : console.warn;
|
|
144
|
+
|
|
145
|
+
const startTime = performance.now();
|
|
146
|
+
const existing = detectExistingInstallation();
|
|
147
|
+
|
|
148
|
+
if (existing.isInstalled) {
|
|
149
|
+
log("🔍 Existing installation detected:");
|
|
150
|
+
if (existing.details.plugin.exists) {
|
|
151
|
+
log(` ✓ Native Plugin Link: ${pluginDest}`);
|
|
152
|
+
}
|
|
153
|
+
if (existing.details.hook.registered) {
|
|
154
|
+
log(` ✓ Governor Hook: Registered in ${hooksJsonPath}`);
|
|
155
|
+
}
|
|
156
|
+
if (existing.details.mcp.registered) {
|
|
157
|
+
log(` ✓ Universal MCP Server: Configured in ${mcpJsonPath}`);
|
|
158
|
+
}
|
|
159
|
+
if (existing.details.schemas.exists) {
|
|
160
|
+
log(` ✓ Antigravity Tool Schemas: Present in ${antigravityMcpDir}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.checkOnly) {
|
|
164
|
+
log("\n[OK] Pre-flight check complete. Installation is active.");
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
167
|
+
log("\n⚡ Re-verifying and synchronizing to latest version...");
|
|
168
|
+
} else {
|
|
169
|
+
log("⚡ Fresh installation of Agy-Context-Saver (Zero-Delay Mode)...");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 0. Pre-flight Validation: Verify configuration directory write access
|
|
173
|
+
try {
|
|
174
|
+
await fsPromises.mkdir(geminiConfigDir, { recursive: true });
|
|
175
|
+
const probeFile = path.join(geminiConfigDir, `.preflight-${process.pid}.tmp`);
|
|
176
|
+
await fsPromises.writeFile(probeFile, "ok", "utf-8");
|
|
177
|
+
await fsPromises.unlink(probeFile);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
throw new Error(`Pre-flight check failed: cannot write to Antigravity configuration directory (${geminiConfigDir}): ${err.message}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 0a. Ensure RTK is provisioned and available
|
|
183
|
+
try {
|
|
184
|
+
const rtkResult = await ensureRtkInstalled({ silent: options.silent, existingRtk: existing.details.rtk });
|
|
185
|
+
log(`[OK] RTK Command Optimizer verified: ${rtkResult.version} (${rtkResult.path})`);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
warn(`[WARN] RTK provisioning check encountered an issue: ${err.message}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 1. Instant Native Plugin Junction / Symlink or Permanent Copy (Takes ~5ms)
|
|
191
|
+
await fsPromises.mkdir(pluginsDir, { recursive: true });
|
|
192
|
+
const isGitRepo = fs.existsSync(path.join(repoRoot, ".git"));
|
|
193
|
+
const isTempOrNpx = !isGitRepo || repoRoot.includes("_npx") || repoRoot.includes("npm-cache") || repoRoot.includes("node_modules");
|
|
194
|
+
|
|
195
|
+
if (!fs.existsSync(pluginDest)) {
|
|
196
|
+
if (isTempOrNpx) {
|
|
197
|
+
await fsPromises.cp(repoRoot, pluginDest, { recursive: true });
|
|
198
|
+
log(`[OK] Native Antigravity Plugin installed to: ${pluginDest}`);
|
|
199
|
+
} else {
|
|
200
|
+
try {
|
|
201
|
+
const isWin = os.platform() === "win32";
|
|
202
|
+
fs.symlinkSync(repoRoot, pluginDest, isWin ? "junction" : "dir");
|
|
203
|
+
log(`[OK] Native Antigravity Plugin linked: ${pluginDest} -> ${repoRoot}`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
await fsPromises.cp(repoRoot, pluginDest, { recursive: true });
|
|
206
|
+
log(`[OK] Native Antigravity Plugin copied to: ${pluginDest}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
if (isTempOrNpx) {
|
|
211
|
+
try {
|
|
212
|
+
await fsPromises.cp(repoRoot, pluginDest, { recursive: true });
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
log(`[OK] Native Antigravity Plugin verified: ${pluginDest}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. Ensure scripts dir exists & copy hook script asynchronously
|
|
219
|
+
await fsPromises.mkdir(scriptsDir, { recursive: true });
|
|
220
|
+
const hookUpdated = await safeCopyFileIfChanged(sourceHook, destHook);
|
|
221
|
+
log(`[OK] Governor hook script mirrored to: ${destHook}${hookUpdated ? "" : " (up-to-date)"}`);
|
|
222
|
+
|
|
223
|
+
// 3. Register Hook in ~/.gemini/config/hooks.json (Preserves existing hooks + creates backup)
|
|
224
|
+
let hooksConfig = {};
|
|
225
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
226
|
+
try {
|
|
227
|
+
hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
|
|
228
|
+
// Create backup if none exists
|
|
229
|
+
if (!fs.existsSync(hooksBakPath)) {
|
|
230
|
+
await fsPromises.copyFile(hooksJsonPath, hooksBakPath);
|
|
231
|
+
log(`[OK] Created configuration backup: ${hooksBakPath}`);
|
|
232
|
+
}
|
|
233
|
+
} catch (err) {
|
|
234
|
+
hooksConfig = {};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const alreadyHadHook = Boolean(hooksConfig["execution-guard"]);
|
|
239
|
+
hooksConfig["execution-guard"] = {
|
|
240
|
+
PreToolUse: [
|
|
241
|
+
{
|
|
242
|
+
matcher: "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir",
|
|
243
|
+
hooks: [
|
|
244
|
+
{
|
|
245
|
+
type: "command",
|
|
246
|
+
command: `node "${destHookNormalized}"`,
|
|
247
|
+
timeout: 5
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
const hooksUpdated = await safeWriteFileIfChanged(hooksJsonPath, JSON.stringify(hooksConfig, null, 2));
|
|
254
|
+
log(`[OK] Lifecycle hook ${alreadyHadHook ? "re-verified" : "registered"} in: ${hooksJsonPath}${hooksUpdated ? "" : " (up-to-date)"}`);
|
|
255
|
+
|
|
256
|
+
// 4. Register MCP Server in ~/.gemini/config/mcp_config.json (Preserves existing servers + creates backup)
|
|
257
|
+
let mcpConfig = { mcpServers: {} };
|
|
258
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
259
|
+
try {
|
|
260
|
+
mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
|
|
261
|
+
if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
|
|
262
|
+
// Create backup if none exists
|
|
263
|
+
if (!fs.existsSync(mcpBakPath)) {
|
|
264
|
+
await fsPromises.copyFile(mcpJsonPath, mcpBakPath);
|
|
265
|
+
log(`[OK] Created configuration backup: ${mcpBakPath}`);
|
|
266
|
+
}
|
|
267
|
+
} catch (err) {
|
|
268
|
+
mcpConfig = { mcpServers: {} };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const alreadyHadMcp = Boolean(mcpConfig.mcpServers["agy-context-saver"]);
|
|
273
|
+
mcpConfig.mcpServers["agy-context-saver"] = {
|
|
274
|
+
command: "node",
|
|
275
|
+
args: [mcpIndexPath]
|
|
276
|
+
};
|
|
277
|
+
const mcpUpdated = await safeWriteFileIfChanged(mcpJsonPath, JSON.stringify(mcpConfig, null, 2));
|
|
278
|
+
log(`[OK] MCP Server ${alreadyHadMcp ? "re-verified" : "registered"} in: ${mcpJsonPath}${mcpUpdated ? "" : " (up-to-date)"}`);
|
|
279
|
+
|
|
280
|
+
// 5. Mirror Antigravity Tool Schemas in parallel
|
|
281
|
+
await fsPromises.mkdir(antigravityMcpDir, { recursive: true });
|
|
282
|
+
|
|
283
|
+
// Clean up legacy safe_command schema if present
|
|
284
|
+
const legacySafeCommandPath = path.join(antigravityMcpDir, "safe_command.json");
|
|
285
|
+
if (fs.existsSync(legacySafeCommandPath)) {
|
|
286
|
+
try {
|
|
287
|
+
await fsPromises.unlink(legacySafeCommandPath);
|
|
288
|
+
log(`[OK] Cleaned up legacy safe_command schema from: ${legacySafeCommandPath}`);
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const instructionsContent = `# Agy-Context-Saver MCP Server Instructions
|
|
293
|
+
|
|
294
|
+
## Overview
|
|
295
|
+
Agy-Context-Saver is the universal Model Context Protocol server and lifecycle governor for Google Antigravity, paired with RTK (Rust Token Killer) for transparent CLI output reduction.
|
|
296
|
+
|
|
297
|
+
## Core Rules for Antigravity Agents
|
|
298
|
+
1. **Reactive Wakeup (Zero Task Polling)**:
|
|
299
|
+
- When background tasks run, DO NOT call \`manage_task(Action='status')\` or \`schedule\` polling timers.
|
|
300
|
+
- Stop calling tools and yield the turn. The system notifies you automatically via \`<SYSTEM_MESSAGE>\`.
|
|
301
|
+
- Repeated polling triggers an escalating circuit breaker (frozen at 5 denials).
|
|
302
|
+
2. **Never Read Internal State Directly**:
|
|
303
|
+
- Access to \`.system_generated/\` via \`view_file\` is strictly denied to prevent transcript explosion.
|
|
304
|
+
- Use \`read_transcript\` or \`query_transcript\` to inspect conversation history cleanly.
|
|
305
|
+
3. **Platform-Aware Codebase Inspection**:
|
|
306
|
+
- **Windows**: Use \`rtk read <file>\`, \`rtk find <path>\`, and \`rtk rg "<pattern>" .\` (the trailing \`.\` is required to avoid stdin stalls).
|
|
307
|
+
- **POSIX**: Use \`rtk read <file>\`, \`rtk ls <path>\`, and \`rtk grep "<pattern>"\` or \`rtk rg "<pattern>"\`.
|
|
308
|
+
|
|
309
|
+
## Available MCP Tools
|
|
310
|
+
- \`check_context_health\`: Diagnoses transcript turn counts, byte payload, and polling loops. Accepts \`transcriptPath\` or \`conversationId\`.
|
|
311
|
+
- \`subagent_brief\`: Generates scope-isolated instructions for delegated subagents to prevent parent context bloat.
|
|
312
|
+
- \`read_transcript\`: Streams recent turns from \`transcript.jsonl\` or \`transcript_full.jsonl\` formatted as clean Markdown.
|
|
313
|
+
- \`query_transcript\`: Forensic regex search and filtering engine across transcript steps with optional \`summaryOnly: true\`.
|
|
314
|
+
- \`get_installation_status\`: Audits live health of the plugin link, hook, MCP server, tool schemas, and RTK.
|
|
315
|
+
- \`sync_installation\`: Synchronizes and repairs all 4 integration layers in ~25ms.
|
|
316
|
+
|
|
317
|
+
## Diagnostic & CLI Playbook
|
|
318
|
+
- Audit Status: \`npm run status\` or \`node mcp/index.js status\`
|
|
319
|
+
- Run Test Suite: \`npm test\`
|
|
320
|
+
- CLI Help: \`node mcp/index.js --help\`
|
|
321
|
+
- ⚠️ Warning: Do not run \`npm start\` or \`node mcp/index.js\` without subcommands interactively (it is a stdio JSON-RPC server).
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
const checkHealthSchema = {
|
|
325
|
+
name: "check_context_health",
|
|
326
|
+
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.",
|
|
327
|
+
parameters: {
|
|
328
|
+
type: "object",
|
|
329
|
+
properties: {
|
|
330
|
+
transcriptPath: {
|
|
331
|
+
type: "string",
|
|
332
|
+
description: "Path to transcript.jsonl file, conversation UUID, or empty for active conversation."
|
|
333
|
+
},
|
|
334
|
+
conversationId: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Conversation UUID or folder name (alternative to transcriptPath)."
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const subagentBriefSchema = {
|
|
343
|
+
name: "subagent_brief",
|
|
344
|
+
description: "Generates a scope-isolated, anti-bloat prompt for invoking a subagent. Instructs the subagent to absorb all intermediate file reads/searches and return only high-signal findings.",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
objective: { type: "string", description: "The core research or investigative objective." },
|
|
349
|
+
scopeFiles: { type: "array", items: { type: "string" }, description: "List of files or directories to investigate." },
|
|
350
|
+
expectedDeliverable: { type: "string", description: "The concise format expected back by the parent agent." }
|
|
351
|
+
},
|
|
352
|
+
required: ["objective"]
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const getInstallationStatusSchema = {
|
|
357
|
+
name: "get_installation_status",
|
|
358
|
+
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).",
|
|
359
|
+
parameters: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {},
|
|
362
|
+
required: []
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const syncInstallationSchema = {
|
|
367
|
+
name: "sync_installation",
|
|
368
|
+
description: "Re-verify and synchronize Agy-Context-Saver installation, updating the governor hook, plugin link, and tool schemas in ~25ms without terminal shell commands.",
|
|
369
|
+
parameters: {
|
|
370
|
+
type: "object",
|
|
371
|
+
properties: {
|
|
372
|
+
checkOnly: {
|
|
373
|
+
type: "boolean",
|
|
374
|
+
description: "If true, performs a pre-flight audit without writing changes (default: false)."
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
required: []
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const readTranscriptSchema = {
|
|
382
|
+
name: "read_transcript",
|
|
383
|
+
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.",
|
|
384
|
+
parameters: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
conversationId: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Conversation ID (UUID), folder name, or full path to transcript.jsonl. If omitted, defaults to active conversation."
|
|
390
|
+
},
|
|
391
|
+
mode: {
|
|
392
|
+
type: "string",
|
|
393
|
+
enum: ["compact", "full"],
|
|
394
|
+
description: "Transcript mode: 'compact' reads transcript.jsonl; 'full' reads transcript_full.jsonl (default: 'compact')."
|
|
395
|
+
},
|
|
396
|
+
lastTurns: {
|
|
397
|
+
type: "number",
|
|
398
|
+
description: "Number of most recent conversation turns to display (default: 3). Set to 0 to read all."
|
|
399
|
+
},
|
|
400
|
+
includeThinking: {
|
|
401
|
+
type: "boolean",
|
|
402
|
+
description: "Whether to include model thinking / internal reasoning blocks (default: false)."
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
required: ["conversationId"]
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const queryTranscriptSchema = {
|
|
410
|
+
name: "query_transcript",
|
|
411
|
+
description: "Granular query and forensic filtering engine for Antigravity conversation transcripts. Searches by keyword or regex, filters by role (user, assistant, tool, error), slices step ranges, and automatically dereferences full content when truncated.",
|
|
412
|
+
parameters: {
|
|
413
|
+
type: "object",
|
|
414
|
+
properties: {
|
|
415
|
+
conversationId: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "Conversation ID (UUID), folder name, or full path to transcript.jsonl."
|
|
418
|
+
},
|
|
419
|
+
query: {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "Search keyword or regex pattern to match across message content, tool calls, and thinking."
|
|
422
|
+
},
|
|
423
|
+
roles: {
|
|
424
|
+
type: "array",
|
|
425
|
+
items: { type: "string" },
|
|
426
|
+
description: "Filter steps by role/source: 'user', 'assistant', 'tool', 'error', or 'all' (default: ['user', 'assistant'])."
|
|
427
|
+
},
|
|
428
|
+
mode: {
|
|
429
|
+
type: "string",
|
|
430
|
+
enum: ["auto", "compact", "full"],
|
|
431
|
+
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')."
|
|
432
|
+
},
|
|
433
|
+
startStep: {
|
|
434
|
+
type: "number",
|
|
435
|
+
description: "Start step_index (inclusive)."
|
|
436
|
+
},
|
|
437
|
+
endStep: {
|
|
438
|
+
type: "number",
|
|
439
|
+
description: "End step_index (inclusive)."
|
|
440
|
+
},
|
|
441
|
+
lastTurns: {
|
|
442
|
+
type: "number",
|
|
443
|
+
description: "Limit to last N matched turns."
|
|
444
|
+
},
|
|
445
|
+
includeThinking: {
|
|
446
|
+
type: "boolean",
|
|
447
|
+
description: "Include model thinking blocks in output (default: false)."
|
|
448
|
+
},
|
|
449
|
+
includeToolCalls: {
|
|
450
|
+
type: "boolean",
|
|
451
|
+
description: "Include tool call arguments and results in output (default: false)."
|
|
452
|
+
},
|
|
453
|
+
maxResults: {
|
|
454
|
+
type: "number",
|
|
455
|
+
description: "Maximum number of matched steps to return (default: 5)."
|
|
456
|
+
},
|
|
457
|
+
summaryOnly: {
|
|
458
|
+
type: "boolean",
|
|
459
|
+
description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
required: ["conversationId"]
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
await Promise.all([
|
|
467
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "instructions.md"), instructionsContent),
|
|
468
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "check_context_health.json"), JSON.stringify(checkHealthSchema, null, 2)),
|
|
469
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "subagent_brief.json"), JSON.stringify(subagentBriefSchema, null, 2)),
|
|
470
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "get_installation_status.json"), JSON.stringify(getInstallationStatusSchema, null, 2)),
|
|
471
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "sync_installation.json"), JSON.stringify(syncInstallationSchema, null, 2)),
|
|
472
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "read_transcript.json"), JSON.stringify(readTranscriptSchema, null, 2)),
|
|
473
|
+
safeWriteFileIfChanged(path.join(antigravityMcpDir, "query_transcript.json"), JSON.stringify(queryTranscriptSchema, null, 2))
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
const elapsed = (performance.now() - startTime).toFixed(1);
|
|
477
|
+
const statusVerb = existing.isInstalled ? "synchronized & up-to-date" : "completed";
|
|
478
|
+
log(`\n✓ Installation ${statusVerb} in ${elapsed}ms (Zero Delay)!\n`);
|
|
479
|
+
return { ...existing, isInstalled: true, elapsedMs: elapsed };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function runUninstall(options = {}) {
|
|
483
|
+
const log = options.silent ? () => {} : console.log;
|
|
484
|
+
const warn = options.silent ? () => {} : console.warn;
|
|
485
|
+
log("Uninstalling Agy-Context-Saver...");
|
|
486
|
+
|
|
487
|
+
// 1. Remove plugin link
|
|
488
|
+
if (fs.existsSync(pluginDest)) {
|
|
489
|
+
try {
|
|
490
|
+
await fsPromises.rm(pluginDest, { recursive: true, force: true });
|
|
491
|
+
log(`[OK] Removed plugin link: ${pluginDest}`);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
warn(`[WARN] Could not remove plugin link: ${err.message}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 2. Remove mirrored hook script
|
|
498
|
+
if (fs.existsSync(destHook)) {
|
|
499
|
+
try {
|
|
500
|
+
await fsPromises.unlink(destHook);
|
|
501
|
+
log(`[OK] Removed mirrored hook script: ${destHook}`);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
warn(`[WARN] Could not remove mirrored hook script: ${err.message}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 3. Remove from hooks.json
|
|
508
|
+
if (fs.existsSync(hooksJsonPath)) {
|
|
509
|
+
try {
|
|
510
|
+
const hooksConfig = JSON.parse(fs.readFileSync(hooksJsonPath, "utf-8"));
|
|
511
|
+
delete hooksConfig["execution-guard"];
|
|
512
|
+
delete hooksConfig["agy-context-saver"];
|
|
513
|
+
await fsPromises.writeFile(hooksJsonPath, JSON.stringify(hooksConfig, null, 2), "utf-8");
|
|
514
|
+
log(`[OK] Removed from: ${hooksJsonPath}`);
|
|
515
|
+
} catch {}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 4. Remove from mcp_config.json
|
|
519
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
520
|
+
try {
|
|
521
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
|
|
522
|
+
if (mcpConfig.mcpServers) {
|
|
523
|
+
delete mcpConfig.mcpServers["agy-context-saver"];
|
|
524
|
+
}
|
|
525
|
+
await fsPromises.writeFile(mcpJsonPath, JSON.stringify(mcpConfig, null, 2), "utf-8");
|
|
526
|
+
log(`[OK] Removed from: ${mcpJsonPath}`);
|
|
527
|
+
} catch {}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// 5. Remove mirrored Antigravity MCP schemas directory
|
|
531
|
+
if (fs.existsSync(antigravityMcpDir)) {
|
|
532
|
+
try {
|
|
533
|
+
await fsPromises.rm(antigravityMcpDir, { recursive: true, force: true });
|
|
534
|
+
log(`[OK] Removed mirrored MCP schema directory: ${antigravityMcpDir}`);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
warn(`[WARN] Could not remove schema directory: ${err.message}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 6. Optional: Restore configuration files from backups if requested
|
|
541
|
+
if (options.restoreBackups) {
|
|
542
|
+
if (fs.existsSync(hooksBakPath)) {
|
|
543
|
+
try {
|
|
544
|
+
await fsPromises.copyFile(hooksBakPath, hooksJsonPath);
|
|
545
|
+
log(`[OK] Restored hooks.json from backup: ${hooksBakPath}`);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
warn(`[WARN] Could not restore hooks backup: ${err.message}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (fs.existsSync(mcpBakPath)) {
|
|
551
|
+
try {
|
|
552
|
+
await fsPromises.copyFile(mcpBakPath, mcpJsonPath);
|
|
553
|
+
log(`[OK] Restored mcp_config.json from backup: ${mcpBakPath}`);
|
|
554
|
+
} catch (err) {
|
|
555
|
+
warn(`[WARN] Could not restore mcp backup: ${err.message}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
log("\n✓ Uninstallation complete.\n");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function runStatus(options = {}) {
|
|
564
|
+
const log = options.silent ? () => {} : console.log;
|
|
565
|
+
const existing = detectExistingInstallation();
|
|
566
|
+
log("Checking Agy-Context-Saver installation status...\n");
|
|
567
|
+
log(`- Native Plugin Link: ${existing.details.plugin.exists ? "ACTIVE (" + pluginDest + ")" : "NOT LINKED"}`);
|
|
568
|
+
log(`- Execution Governor Hook: ${existing.details.hook.registered ? "REGISTERED in hooks.json" : "NOT REGISTERED"}`);
|
|
569
|
+
log(`- Universal MCP Server: ${existing.details.mcp.registered ? "CONFIGURED in mcp_config.json" : "NOT CONFIGURED"}`);
|
|
570
|
+
log(`- Antigravity Tool Schemas: ${existing.details.schemas.exists ? "PRESENT (6 Schemas)" : "MISSING"}`);
|
|
571
|
+
log(`- RTK Command Optimizer: ${existing.details.rtk ? "INSTALLED (" + existing.details.rtk.version + ")" : "MISSING"}`);
|
|
572
|
+
|
|
573
|
+
log(`\nOverall Status: ${existing.isComplete ? "HEALTHY & ACTIVE 🛡️" : existing.isInstalled ? "PARTIAL INSTALLATION" : "NOT INSTALLED"}\n`);
|
|
574
|
+
return existing;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export {
|
|
578
|
+
pluginDest,
|
|
579
|
+
destHook,
|
|
580
|
+
hooksJsonPath,
|
|
581
|
+
hooksBakPath,
|
|
582
|
+
mcpJsonPath,
|
|
583
|
+
mcpBakPath,
|
|
584
|
+
antigravityMcpDir
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
function printCliHelp() {
|
|
588
|
+
console.log(`
|
|
589
|
+
Agy-Context-Saver Registration & Lifecycle Engine
|
|
590
|
+
|
|
591
|
+
Usage:
|
|
592
|
+
node scripts/install-register.mjs [command/option]
|
|
593
|
+
|
|
594
|
+
Commands & Options:
|
|
595
|
+
--status, status Check live integration status across all layers + RTK
|
|
596
|
+
--check, -c Run pre-flight validation check without writing changes
|
|
597
|
+
--uninstall, uninstall Remove governor hook, plugin link, and MCP server
|
|
598
|
+
--restore-backups Restore original configuration backup files (*.bak) during uninstall
|
|
599
|
+
--help, -h, help Show this help message
|
|
600
|
+
(no arguments) Synchronize and verify full 4-layer installation
|
|
601
|
+
`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Auto-run if executed directly as script
|
|
605
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
606
|
+
const arg = process.argv[2];
|
|
607
|
+
if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
608
|
+
printCliHelp();
|
|
609
|
+
process.exit(0);
|
|
610
|
+
} else if (arg === "--uninstall" || arg === "uninstall") {
|
|
611
|
+
const restoreBackups = process.argv.includes("--restore-backups");
|
|
612
|
+
await runUninstall({ restoreBackups });
|
|
613
|
+
} else if (arg === "--status" || arg === "status") {
|
|
614
|
+
runStatus();
|
|
615
|
+
} else if (arg === "--check" || arg === "-c") {
|
|
616
|
+
await runInstall({ checkOnly: true });
|
|
617
|
+
} else {
|
|
618
|
+
await runInstall();
|
|
619
|
+
}
|
|
620
|
+
}
|