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
package/mcp/index.js
DELETED
|
@@ -1,1332 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Agy-Context-Saver: Universal Model Context Protocol (MCP) Server
|
|
5
|
-
*
|
|
6
|
-
* Implements standard MCP (JSON-RPC 2.0 over stdio) with zero external dependencies.
|
|
7
|
-
* Purpose-built for Google Antigravity across macOS, Linux, and Windows.
|
|
8
|
-
*
|
|
9
|
-
* Capabilities:
|
|
10
|
-
* - Transparent Command Optimization:
|
|
11
|
-
* - Native shell commands transparently optimized via RTK (Rust Token Killer).
|
|
12
|
-
* - Tools:
|
|
13
|
-
* - check_context_health: Inspects conversation transcripts using async streaming
|
|
14
|
-
* to diagnose turn count, tool polling loops, and context degradation with minimal memory.
|
|
15
|
-
* - get_installation_status: Audits live Antigravity plugin, hook, MCP, and RTK integration.
|
|
16
|
-
* - sync_installation: Re-verifies and synchronizes installation and tool schemas.
|
|
17
|
-
* - read_transcript: Clean Markdown streaming reader for compact and full transcripts.
|
|
18
|
-
* - query_transcript: Forensic filtering and regex search engine for conversation logs.
|
|
19
|
-
* - Prompts:
|
|
20
|
-
* - context_shield: Injects the Context Governance rules.
|
|
21
|
-
* - Resources:
|
|
22
|
-
* - context-saver://rules/governance: Serves the full AGENTS.md rulebook.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import fs from "node:fs";
|
|
26
|
-
import os from "node:os";
|
|
27
|
-
import path from "node:path";
|
|
28
|
-
import readline from "node:readline";
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
-
import { runInstall, runUninstall, runStatus, runRestoreBackups, detectExistingInstallation } from "../scripts/install-register.mjs";
|
|
31
|
-
import { findExistingRtk } from "../scripts/provision-rtk.mjs";
|
|
32
|
-
|
|
33
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
-
const realOrSelf = (p) => { try { return fs.realpathSync(p); } catch { return path.resolve(p); } };
|
|
35
|
-
const isEntryPoint = Boolean(
|
|
36
|
-
process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(__filename)
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
// Handle CLI subcommands (e.g. npx agy-context-saver install)
|
|
40
|
-
if (isEntryPoint) {
|
|
41
|
-
const cliArgs = process.argv.slice(2);
|
|
42
|
-
const cliArg = cliArgs[0];
|
|
43
|
-
const checkFlag = cliArgs.includes("--check") || cliArgs.includes("-c");
|
|
44
|
-
const statusFlag = cliArgs.includes("--status") || cliArgs.includes("status");
|
|
45
|
-
const uninstallFlag = cliArgs.includes("--uninstall") || cliArgs.includes("uninstall");
|
|
46
|
-
const restoreFlag = cliArgs.includes("--restore-backups") || cliArgs.includes("restore-backups");
|
|
47
|
-
const helpFlag = cliArgs.includes("--help") || cliArgs.includes("-h") || cliArgs.includes("help");
|
|
48
|
-
const allowDegraded = cliArgs.includes("--allow-degraded");
|
|
49
|
-
|
|
50
|
-
if (helpFlag) {
|
|
51
|
-
printHelp();
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
if ((checkFlag || statusFlag) && (uninstallFlag || restoreFlag)) {
|
|
55
|
-
console.error("Error: Read-only flags (--check, --status) cannot be combined with mutating flags (--uninstall, --restore-backups).");
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
if (checkFlag && statusFlag) {
|
|
59
|
-
console.error("Error: Conflicting flags: specify either --check or --status, not both.");
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
if (checkFlag) {
|
|
63
|
-
await runInstall({ checkOnly: true, allowDegraded });
|
|
64
|
-
process.exit(0);
|
|
65
|
-
} else if (statusFlag) {
|
|
66
|
-
runStatus();
|
|
67
|
-
process.exit(0);
|
|
68
|
-
} else if (uninstallFlag) {
|
|
69
|
-
await runUninstall({ restoreBackups: restoreFlag });
|
|
70
|
-
process.exit(0);
|
|
71
|
-
} else if (restoreFlag) {
|
|
72
|
-
await runRestoreBackups();
|
|
73
|
-
process.exit(0);
|
|
74
|
-
} else if (cliArg === "install" || cliArg === "--install") {
|
|
75
|
-
await runInstall({ allowDegraded });
|
|
76
|
-
process.exit(0);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function printHelp() {
|
|
82
|
-
console.log(`
|
|
83
|
-
Agy-Context-Saver 🛡️ (v1.8.1)
|
|
84
|
-
Universal MCP Server & Lifecycle Governor for Google Antigravity (Powered by RTK)
|
|
85
|
-
|
|
86
|
-
Usage:
|
|
87
|
-
agy-context-saver [command] [options]
|
|
88
|
-
node mcp/index.js [command] [options]
|
|
89
|
-
|
|
90
|
-
Commands:
|
|
91
|
-
status, --status Audit live registration across plugin, hook, MCP, & RTK
|
|
92
|
-
install, --install Synchronize & register plugin, hook, MCP server, and schemas
|
|
93
|
-
install --check, -c Run pre-flight validation check without writing changes
|
|
94
|
-
uninstall, --uninstall Unregister hook, MCP server, and remove plugin link
|
|
95
|
-
uninstall --restore-backups Unregister and restore original configuration backups
|
|
96
|
-
help, --help, -h Show this help message
|
|
97
|
-
|
|
98
|
-
Options:
|
|
99
|
-
--check, -c Dry-run verification mode for install
|
|
100
|
-
--restore-backups Restore backup files (*.bak) during uninstall
|
|
101
|
-
--silent Suppress console log outputs
|
|
102
|
-
|
|
103
|
-
MCP Server Stdio Mode:
|
|
104
|
-
When launched without subcommands, agy-context-saver runs as a JSON-RPC 2.0
|
|
105
|
-
stdio Model Context Protocol server (intended for Antigravity or MCP clients).
|
|
106
|
-
⚠️ Do not execute interactively in a terminal without piped JSON-RPC input.
|
|
107
|
-
|
|
108
|
-
Testing & Verification Playbook:
|
|
109
|
-
npm run status Verify live 4-layer integration status
|
|
110
|
-
npm test Run complete 10-suite automated test suite
|
|
111
|
-
node tests/test-hook.mjs Run 27 closed-topology governance rule tests
|
|
112
|
-
node tests/test-mcp.mjs Run MCP protocol, method, and schema tests
|
|
113
|
-
|
|
114
|
-
Unified 3-Tier Inspection Directives (Waymark + Hybrid view_file + RTK):
|
|
115
|
-
Tier 1 (Structure): waymark_ask(..., plain: true) across all 23 supported formats
|
|
116
|
-
Tier 2 (Read/Edit): view_file(StartLine, EndLine) (1-150L default; or explicit 1..TotalLines full-file audit <= 800L & <= 45 KiB)
|
|
117
|
-
Tier 3 (RTK CLI):
|
|
118
|
-
- Spillovers/Logs: rtk read <path> (--head-lines / --tail-lines)
|
|
119
|
-
- Directory: rtk find <path> (Windows & POSIX) or rtk ls <path> (POSIX)
|
|
120
|
-
- Prose Search: rtk rg "<pattern>" . (Windows trailing dot required) or rtk grep "<pattern>" (POSIX)
|
|
121
|
-
`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
125
|
-
const rulesPath = path.resolve(__dirname, "../rules/AGENTS.md");
|
|
126
|
-
|
|
127
|
-
const SERVER_NAME = "agy-context-saver";
|
|
128
|
-
const SERVER_VERSION = "1.8.1";
|
|
129
|
-
|
|
130
|
-
// --- Tools Specification ---
|
|
131
|
-
const TOOLS = [
|
|
132
|
-
{
|
|
133
|
-
name: "check_context_health",
|
|
134
|
-
description: "Inspects a conversation transcript (transcript.jsonl) to diagnose turn count, payload size, tool polling loops, and context degradation risk. Accepts a file path, conversation UUID, or empty for active conversation.",
|
|
135
|
-
inputSchema: {
|
|
136
|
-
type: "object",
|
|
137
|
-
properties: {
|
|
138
|
-
transcriptPath: {
|
|
139
|
-
type: "string",
|
|
140
|
-
description: "Path to transcript.jsonl file, conversation UUID, or empty for active conversation."
|
|
141
|
-
},
|
|
142
|
-
conversationId: {
|
|
143
|
-
type: "string",
|
|
144
|
-
description: "Conversation UUID or folder name (alternative to transcriptPath)."
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
name: "get_installation_status",
|
|
151
|
-
description: "Inspect the live installation status of Agy-Context-Saver across all Antigravity integration points (Native Plugin Link, Governor Hook in hooks.json, MCP Server in mcp_config.json, and Tool Schemas).",
|
|
152
|
-
inputSchema: {
|
|
153
|
-
type: "object",
|
|
154
|
-
properties: {},
|
|
155
|
-
required: []
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: "sync_installation",
|
|
160
|
-
description: "Re-verify and synchronize Agy-Context-Saver installation, updating the governor hook, plugin link, and tool schemas in ~25ms without terminal shell commands.",
|
|
161
|
-
inputSchema: {
|
|
162
|
-
type: "object",
|
|
163
|
-
properties: {
|
|
164
|
-
checkOnly: {
|
|
165
|
-
type: "boolean",
|
|
166
|
-
description: "If true, performs a pre-flight audit without writing changes (default: false)."
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
required: []
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: "read_transcript",
|
|
174
|
-
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.",
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: "object",
|
|
177
|
-
properties: {
|
|
178
|
-
conversationId: {
|
|
179
|
-
type: "string",
|
|
180
|
-
description: "Conversation ID (UUID), folder name, or full path to transcript.jsonl. If omitted, defaults to active conversation."
|
|
181
|
-
},
|
|
182
|
-
mode: {
|
|
183
|
-
type: "string",
|
|
184
|
-
enum: ["compact", "full"],
|
|
185
|
-
description: "Transcript mode: 'compact' reads transcript.jsonl; 'full' reads transcript_full.jsonl (default: 'compact')."
|
|
186
|
-
},
|
|
187
|
-
lastTurns: {
|
|
188
|
-
type: "number",
|
|
189
|
-
description: "Number of most recent conversation turns to display (default: 3). Set to 0 to read all."
|
|
190
|
-
},
|
|
191
|
-
includeThinking: {
|
|
192
|
-
type: "boolean",
|
|
193
|
-
description: "Whether to include model thinking / internal reasoning blocks (default: false)."
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
required: ["conversationId"]
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name: "query_transcript",
|
|
201
|
-
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.",
|
|
202
|
-
inputSchema: {
|
|
203
|
-
type: "object",
|
|
204
|
-
properties: {
|
|
205
|
-
conversationId: {
|
|
206
|
-
type: "string",
|
|
207
|
-
description: "Conversation ID (UUID), folder name, or full path to transcript.jsonl."
|
|
208
|
-
},
|
|
209
|
-
query: {
|
|
210
|
-
type: "string",
|
|
211
|
-
description: "Search keyword or regex pattern to match across message content, tool calls, and thinking."
|
|
212
|
-
},
|
|
213
|
-
roles: {
|
|
214
|
-
type: "array",
|
|
215
|
-
items: { type: "string" },
|
|
216
|
-
description: "Filter steps by role/source: 'user', 'assistant', 'subagent', 'tool', 'error', or 'all' (default: ['user', 'assistant'])."
|
|
217
|
-
},
|
|
218
|
-
mode: {
|
|
219
|
-
type: "string",
|
|
220
|
-
enum: ["auto", "compact", "full"],
|
|
221
|
-
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')."
|
|
222
|
-
},
|
|
223
|
-
startStep: {
|
|
224
|
-
type: "number",
|
|
225
|
-
description: "Start step_index (inclusive)."
|
|
226
|
-
},
|
|
227
|
-
endStep: {
|
|
228
|
-
type: "number",
|
|
229
|
-
description: "End step_index (inclusive)."
|
|
230
|
-
},
|
|
231
|
-
lastTurns: {
|
|
232
|
-
type: "number",
|
|
233
|
-
description: "Limit to last N matched turns."
|
|
234
|
-
},
|
|
235
|
-
includeThinking: {
|
|
236
|
-
type: "boolean",
|
|
237
|
-
description: "Include model thinking blocks in output (default: false)."
|
|
238
|
-
},
|
|
239
|
-
includeToolCalls: {
|
|
240
|
-
type: "boolean",
|
|
241
|
-
description: "Include tool call arguments and results in output (default: false)."
|
|
242
|
-
},
|
|
243
|
-
maxResults: {
|
|
244
|
-
type: "number",
|
|
245
|
-
description: "Maximum number of matched steps to return (default: 5)."
|
|
246
|
-
},
|
|
247
|
-
summaryOnly: {
|
|
248
|
-
type: "boolean",
|
|
249
|
-
description: "If true, returns high-density 1-line step summaries instead of full message blocks to minimize UI card height (default: false)."
|
|
250
|
-
},
|
|
251
|
-
stepIndex: {
|
|
252
|
-
type: "number",
|
|
253
|
-
description: "Optional step_index to surgically retrieve full un-truncated content for a single step from transcript_full.jsonl."
|
|
254
|
-
},
|
|
255
|
-
field: {
|
|
256
|
-
type: "string",
|
|
257
|
-
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."
|
|
258
|
-
}
|
|
259
|
-
},
|
|
260
|
-
required: ["conversationId"]
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
// --- Resources Specification ---
|
|
266
|
-
const RESOURCES = [
|
|
267
|
-
{
|
|
268
|
-
uri: "context-saver://rules/governance",
|
|
269
|
-
name: "Antigravity Execution & Context Governance Rulebook",
|
|
270
|
-
description: "Authoritative 3-Layer rules preventing background polling loops, maximizing synchronous execution, and keeping command output compact.",
|
|
271
|
-
mimeType: "text/markdown"
|
|
272
|
-
}
|
|
273
|
-
];
|
|
274
|
-
|
|
275
|
-
// --- Prompts Specification ---
|
|
276
|
-
const PROMPTS = [
|
|
277
|
-
{
|
|
278
|
-
name: "context_shield",
|
|
279
|
-
description: "Apply Agy-Context-Saver 3-Layer governance to the active session to prevent polling loops and transcript degradation.",
|
|
280
|
-
arguments: []
|
|
281
|
-
}
|
|
282
|
-
];
|
|
283
|
-
|
|
284
|
-
// --- Tool Implementations ---
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
async function handleCheckContextHealth(args = {}) {
|
|
289
|
-
try {
|
|
290
|
-
const rawTarget = args.transcriptPath || args.conversationId || "current";
|
|
291
|
-
let transcriptPath = rawTarget;
|
|
292
|
-
|
|
293
|
-
if (!fs.existsSync(transcriptPath)) {
|
|
294
|
-
const resolved = resolveTranscriptPath(rawTarget, "compact");
|
|
295
|
-
if (resolved && resolved.filePath && fs.existsSync(resolved.filePath)) {
|
|
296
|
-
transcriptPath = resolved.filePath;
|
|
297
|
-
} else {
|
|
298
|
-
return {
|
|
299
|
-
isError: true,
|
|
300
|
-
content: [{ type: "text", text: `Transcript not found: ${rawTarget}` }]
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const fileStream = fs.createReadStream(transcriptPath, { encoding: "utf-8" });
|
|
306
|
-
const rl = readline.createInterface({
|
|
307
|
-
input: fileStream,
|
|
308
|
-
crlfDelay: Infinity
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
let totalSteps = 0;
|
|
312
|
-
let userTurns = 0;
|
|
313
|
-
let modelTurns = 0;
|
|
314
|
-
let toolCalls = 0;
|
|
315
|
-
let pollingEvents = 0;
|
|
316
|
-
let totalBytes = 0;
|
|
317
|
-
let corruptLines = 0;
|
|
318
|
-
const MAX_LINES = 100000;
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
for await (const line of rl) {
|
|
322
|
-
if (!line.trim()) continue;
|
|
323
|
-
totalSteps++;
|
|
324
|
-
totalBytes += line.length;
|
|
325
|
-
|
|
326
|
-
if (totalSteps > MAX_LINES) {
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Fast check for common step types without parsing if no tool calls (PERF-16)
|
|
331
|
-
const hasToolCalls = line.includes('"tool_calls"');
|
|
332
|
-
const head = line.slice(0, 300);
|
|
333
|
-
const isUserInput = head.includes('"type":"USER_INPUT"');
|
|
334
|
-
const isPlannerResp = head.includes('"type":"PLANNER_RESPONSE"');
|
|
335
|
-
|
|
336
|
-
if (!hasToolCalls && (isUserInput || isPlannerResp)) {
|
|
337
|
-
if (isUserInput) userTurns++;
|
|
338
|
-
else modelTurns++;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const item = JSON.parse(line);
|
|
344
|
-
if (item.type === "USER_INPUT") userTurns++;
|
|
345
|
-
if (item.type === "PLANNER_RESPONSE") modelTurns++;
|
|
346
|
-
if (item.tool_calls && item.tool_calls.length) {
|
|
347
|
-
toolCalls += item.tool_calls.length;
|
|
348
|
-
for (const tc of item.tool_calls) {
|
|
349
|
-
const name = tc.tool_name || tc.name;
|
|
350
|
-
const args = tc.args || tc.arguments || {};
|
|
351
|
-
const action = String(args.Action || args.action || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
352
|
-
const prompt = String(args.Prompt || args.prompt || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
353
|
-
const cond = String(args.TimerCondition || args.timerCondition || "").replace(/^["']|["']$/g, "").trim().toLowerCase();
|
|
354
|
-
if (name === "manage_task" && action === "status") pollingEvents++;
|
|
355
|
-
if (name === "schedule" && (cond.startsWith("task") || cond.includes("task") || prompt.includes("test") || prompt.includes("check on") || prompt.includes("status"))) pollingEvents++;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
} catch (err) {
|
|
359
|
-
corruptLines++;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
} finally {
|
|
363
|
-
rl.close();
|
|
364
|
-
fileStream.destroy();
|
|
365
|
-
}
|
|
366
|
-
const kb = (totalBytes / 1024).toFixed(1);
|
|
367
|
-
const healthStatus = pollingEvents > 3 ? "CRITICAL (Active Polling Loops Detected)" : userTurns > 40 ? "WARNING (High Turn Budget)" : "HEALTHY";
|
|
368
|
-
|
|
369
|
-
const rtkInfo = findExistingRtk();
|
|
370
|
-
const reportLines = [
|
|
371
|
-
`### Context Health Report: ${healthStatus}`,
|
|
372
|
-
"",
|
|
373
|
-
"#### [Active Session Metrics]",
|
|
374
|
-
`- Total Steps: ${totalSteps}`,
|
|
375
|
-
`- User Turns: ${userTurns}`,
|
|
376
|
-
`- Assistant Responses: ${modelTurns}`,
|
|
377
|
-
`- Tool Calls: ${toolCalls}`,
|
|
378
|
-
`- Detected Busy-Polling Events: ${pollingEvents}`,
|
|
379
|
-
`- Approximate Raw Transcript Size: ${kb} KB`
|
|
380
|
-
];
|
|
381
|
-
|
|
382
|
-
if (corruptLines > 0) {
|
|
383
|
-
reportLines.push(`- Corrupted/Unparsed Lines: ${corruptLines}`);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
reportLines.push("");
|
|
387
|
-
reportLines.push("#### [Local RTK Environment]");
|
|
388
|
-
reportLines.push(`- RTK Command Optimizer: ${rtkInfo ? `Active (${rtkInfo.version})` : "Missing (run sync_installation)"}`);
|
|
389
|
-
reportLines.push(`- Note: Lifetime token gains from 'rtk gain' represent cumulative machine-wide host savings, not single-session metrics.`);
|
|
390
|
-
|
|
391
|
-
reportLines.push("");
|
|
392
|
-
if (pollingEvents > 0) {
|
|
393
|
-
reportLines.push("⚠️ Recommendation: Background polling detected! Cease manage_task(status) calls and yield execution to native Reactive Wakeup.");
|
|
394
|
-
}
|
|
395
|
-
if (userTurns >= 35) {
|
|
396
|
-
reportLines.push("💡 Recommendation: Turn budget approaching threshold. Summarize progress and continue in a fresh conversation to preserve context.");
|
|
397
|
-
} else {
|
|
398
|
-
reportLines.push("✓ Context footprint is well-managed.");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return { content: [{ type: "text", text: reportLines.join("\n") }] };
|
|
402
|
-
} catch (err) {
|
|
403
|
-
return {
|
|
404
|
-
isError: true,
|
|
405
|
-
content: [{ type: "text", text: `Failed to inspect transcript: ${err.message}` }]
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function handleGetInstallationStatus() {
|
|
411
|
-
const existing = detectExistingInstallation();
|
|
412
|
-
const rtkInfo = findExistingRtk();
|
|
413
|
-
const schemaText = existing.details.schemas.exists ? "ALL 5 SCHEMAS PRESENT" : "MISSING";
|
|
414
|
-
const report = [
|
|
415
|
-
`### Agy-Context-Saver Installation Status: ${existing.isComplete ? "HEALTHY & ACTIVE 🛡️" : existing.isInstalled ? "PARTIAL INSTALLATION ⚠️" : "NOT INSTALLED ❌"}`,
|
|
416
|
-
`- Native Plugin Link: ${existing.details.plugin.exists ? `ACTIVE (${existing.details.plugin.target})` : "NOT LINKED"}`,
|
|
417
|
-
`- Governor Lifecycle Hook: ${existing.details.hook.registered ? "REGISTERED in hooks.json" : "NOT REGISTERED"} (Script: ${existing.details.hook.scriptExists ? "Present" : "Missing"})`,
|
|
418
|
-
`- Universal MCP Server: ${existing.details.mcp.registered ? "CONFIGURED in mcp_config.json" : "NOT CONFIGURED"}`,
|
|
419
|
-
`- Antigravity Tool Schemas: ${schemaText} (${existing.details.schemas.dir})`,
|
|
420
|
-
`- RTK Binary: ${rtkInfo ? `INSTALLED (${rtkInfo.version})` : "MISSING"}`,
|
|
421
|
-
`- RTK Hook Optimization: ${existing.details.hook.registered ? "ACTIVE (via PreToolUse)" : "INACTIVE"}`,
|
|
422
|
-
"",
|
|
423
|
-
existing.isComplete && rtkInfo
|
|
424
|
-
? "✓ All Antigravity integration layers and RTK optimizer are fully operational."
|
|
425
|
-
: "⚠️ Recommendation: Run sync_installation to re-verify and repair missing layers."
|
|
426
|
-
].join("\n");
|
|
427
|
-
|
|
428
|
-
return {
|
|
429
|
-
content: [{ type: "text", text: report }]
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async function handleSyncInstallation({ checkOnly = false } = {}) {
|
|
434
|
-
const result = await runInstall({ checkOnly, silent: true });
|
|
435
|
-
const rtkInfo = findExistingRtk();
|
|
436
|
-
const report = [
|
|
437
|
-
`### Agy-Context-Saver Installation Synchronization`,
|
|
438
|
-
`- Status: ${result.isComplete ? "SYNCHRONIZED & HEALTHY 🛡️" : "UPDATED"}`,
|
|
439
|
-
`- Elapsed Time: ${result.elapsedMs || "0"} ms`,
|
|
440
|
-
`- Plugin Link: ${result.details?.plugin?.exists ? "Verified" : "Updated"}`,
|
|
441
|
-
`- Lifecycle Hook: Registered in hooks.json`,
|
|
442
|
-
`- MCP Server: Registered in mcp_config.json`,
|
|
443
|
-
`- Tool Schemas: 5 Schemas mirrored to ~/.gemini/antigravity/mcp/agy-context-saver`,
|
|
444
|
-
`- RTK Binary: ${rtkInfo ? `Verified (${rtkInfo.version})` : "Provisioned"}`,
|
|
445
|
-
"",
|
|
446
|
-
checkOnly ? "✓ Pre-flight check complete (dry-run)." : "✓ Installation fully synchronized and up-to-date in Zero-Delay mode."
|
|
447
|
-
].join("\n");
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
content: [{ type: "text", text: report }]
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// --- Transcript Reader & Forensics Engine Helpers ---
|
|
455
|
-
|
|
456
|
-
const MAX_LINE_CHARS = 1000;
|
|
457
|
-
const MAX_TURN_CHARS = 6000;
|
|
458
|
-
const MAX_OUTPUT_CHARS = 24000;
|
|
459
|
-
|
|
460
|
-
function extractConvId(p) {
|
|
461
|
-
const normalized = p.replace(/\\/g, "/");
|
|
462
|
-
const match = normalized.match(/\/brain\/([^/]+)\//);
|
|
463
|
-
return match ? match[1] : path.basename(normalized, path.extname(normalized));
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
let cachedActiveConv = null;
|
|
467
|
-
let cachedActiveConvTime = 0;
|
|
468
|
-
|
|
469
|
-
function getProfileHomeDir() {
|
|
470
|
-
return process.env.AGY_TEST_PROFILE_DIR || os.homedir();
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function getBrainDir() {
|
|
474
|
-
return process.env.AGY_BRAIN_DIR || path.join(getProfileHomeDir(), ".gemini", "antigravity", "brain");
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function resolveTranscriptPath(target, mode = "compact") {
|
|
478
|
-
const isFull = mode === "full";
|
|
479
|
-
const fileName = isFull ? "transcript_full.jsonl" : "transcript.jsonl";
|
|
480
|
-
|
|
481
|
-
// 1. If target is omitted, "current", or empty: resolve active conversation
|
|
482
|
-
let raw = String(target || "").trim();
|
|
483
|
-
if (!raw || raw.toLowerCase() === "current") {
|
|
484
|
-
if (process.env.GEMINI_CONVERSATION_ID) {
|
|
485
|
-
raw = process.env.GEMINI_CONVERSATION_ID;
|
|
486
|
-
} else {
|
|
487
|
-
const now = Date.now();
|
|
488
|
-
if (cachedActiveConv && (now - cachedActiveConvTime) < 5000) {
|
|
489
|
-
raw = cachedActiveConv;
|
|
490
|
-
} else {
|
|
491
|
-
const brainDir = getBrainDir();
|
|
492
|
-
try {
|
|
493
|
-
const entries = fs.readdirSync(brainDir, { withFileTypes: true });
|
|
494
|
-
let bestMtime = 0;
|
|
495
|
-
let bestName = null;
|
|
496
|
-
for (const d of entries) {
|
|
497
|
-
if (!d.isDirectory()) continue;
|
|
498
|
-
try {
|
|
499
|
-
const transPath = path.join(brainDir, d.name, ".system_generated", "logs", "transcript.jsonl");
|
|
500
|
-
const mtime = fs.statSync(transPath).mtimeMs;
|
|
501
|
-
if (mtime > bestMtime) {
|
|
502
|
-
bestMtime = mtime;
|
|
503
|
-
bestName = d.name;
|
|
504
|
-
}
|
|
505
|
-
} catch {
|
|
506
|
-
try {
|
|
507
|
-
const mtime = fs.statSync(path.join(brainDir, d.name)).mtimeMs;
|
|
508
|
-
if (mtime > bestMtime) {
|
|
509
|
-
bestMtime = mtime;
|
|
510
|
-
bestName = d.name;
|
|
511
|
-
}
|
|
512
|
-
} catch {}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
if (bestName) {
|
|
516
|
-
raw = bestName;
|
|
517
|
-
cachedActiveConv = bestName;
|
|
518
|
-
cachedActiveConvTime = now;
|
|
519
|
-
}
|
|
520
|
-
} catch {}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// 2. Normalize separators
|
|
526
|
-
const normalized = raw.replace(/\\/g, "/");
|
|
527
|
-
|
|
528
|
-
// 3. Direct file path
|
|
529
|
-
if (normalized.endsWith(".jsonl")) {
|
|
530
|
-
if (fs.existsSync(normalized)) {
|
|
531
|
-
if (isFull && normalized.endsWith("transcript.jsonl")) {
|
|
532
|
-
const fullCandidate = normalized.replace(/transcript\.jsonl$/, "transcript_full.jsonl");
|
|
533
|
-
if (fs.existsSync(fullCandidate)) return { filePath: fullCandidate, convId: extractConvId(normalized) };
|
|
534
|
-
}
|
|
535
|
-
if (!isFull && normalized.endsWith("transcript_full.jsonl")) {
|
|
536
|
-
const compactCandidate = normalized.replace(/transcript_full\.jsonl$/, "transcript.jsonl");
|
|
537
|
-
if (fs.existsSync(compactCandidate)) return { filePath: compactCandidate, convId: extractConvId(normalized) };
|
|
538
|
-
}
|
|
539
|
-
return { filePath: normalized, convId: extractConvId(normalized) };
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// 4. UUID / Directory resolution under <brainDir>/<id>
|
|
544
|
-
const primaryBrainDir = getBrainDir();
|
|
545
|
-
const brainCandidate = path.join(primaryBrainDir, normalized, ".system_generated", "logs", fileName);
|
|
546
|
-
if (fs.existsSync(brainCandidate)) {
|
|
547
|
-
return { filePath: brainCandidate, convId: normalized };
|
|
548
|
-
}
|
|
549
|
-
if (isFull) {
|
|
550
|
-
const compactFallback = path.join(primaryBrainDir, normalized, ".system_generated", "logs", "transcript.jsonl");
|
|
551
|
-
if (fs.existsSync(compactFallback)) {
|
|
552
|
-
return { filePath: compactFallback, convId: normalized };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// 5. Try local relative/absolute directory path
|
|
557
|
-
const localCandidate = path.resolve(normalized, ".system_generated", "logs", fileName);
|
|
558
|
-
if (fs.existsSync(localCandidate)) {
|
|
559
|
-
return { filePath: localCandidate, convId: extractConvId(localCandidate) };
|
|
560
|
-
}
|
|
561
|
-
const directCandidate = path.resolve(normalized, fileName);
|
|
562
|
-
if (fs.existsSync(directCandidate)) {
|
|
563
|
-
return { filePath: directCandidate, convId: extractConvId(directCandidate) };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return { error: `Transcript not found for target: '${target}'. Searched: ${brainCandidate}` };
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function cleanMessageContent(content, maxChars = MAX_TURN_CHARS) {
|
|
570
|
-
if (content === null || content === undefined) return "";
|
|
571
|
-
let text = typeof content === "string" ? content : JSON.stringify(content, null, 2);
|
|
572
|
-
|
|
573
|
-
// Fast-path: short text without base64 or long blocks skips heavy line splits and regex
|
|
574
|
-
if (text.length <= 120 && !text.includes("base64")) {
|
|
575
|
-
return text;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const origLength = text.length;
|
|
579
|
-
// Pre-slice huge text to ~4x maxChars before heavy regex and line clamping (PERF-15)
|
|
580
|
-
if (text.length > maxChars * 4) {
|
|
581
|
-
text = text.slice(0, maxChars * 4);
|
|
582
|
-
}
|
|
583
|
-
// Strip huge base64 media blocks only if relevant keywords or large length
|
|
584
|
-
if (text.includes("base64") || text.length > 200) {
|
|
585
|
-
text = text.replace(/data:image\/[^;]+;base64,[A-Za-z0-9+/=]+/g, "[Embedded Media/Binary Omitted]");
|
|
586
|
-
text = text.replace(/[A-Za-z0-9+/=]{200,}/g, "[Binary/Base64 Payload Omitted]");
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// Strip Git CRLF/LF line-ending diagnostic warning noise (wastes context window tokens)
|
|
590
|
-
if (text.includes("will be replaced by")) {
|
|
591
|
-
text = text.replace(/(?:#\s*)?warning:\s*(?:in the working copy of [^,\r\n]+,\s*)?(?:CRLF will be replaced by LF|LF will be replaced by CRLF)[^\r\n]*(?:\r?\n)?/gi, "");
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Clamp lines exceeding MAX_LINE_CHARS only if newline exists and text is long enough
|
|
595
|
-
if (text.length > MAX_LINE_CHARS && text.includes("\n")) {
|
|
596
|
-
const lines = text.split("\n");
|
|
597
|
-
let changed = false;
|
|
598
|
-
const clampedLines = lines.map((l) => {
|
|
599
|
-
if (l.length > MAX_LINE_CHARS) {
|
|
600
|
-
changed = true;
|
|
601
|
-
return l.slice(0, MAX_LINE_CHARS) + " ... [line clamped]";
|
|
602
|
-
}
|
|
603
|
-
return l;
|
|
604
|
-
});
|
|
605
|
-
if (changed) {
|
|
606
|
-
text = clampedLines.join("\n");
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
if (origLength > maxChars || text.length > maxChars) {
|
|
611
|
-
const omitted = Math.max(origLength - maxChars, 0);
|
|
612
|
-
return text.slice(0, maxChars) + `\n\n... [Content Truncated (${omitted} chars omitted to preserve token budget)] ...`;
|
|
613
|
-
}
|
|
614
|
-
return text;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
function formatArgumentDescriptor(k, v) {
|
|
618
|
-
if (typeof v === "string") {
|
|
619
|
-
if (v.length <= 80 && !v.includes("\n")) {
|
|
620
|
-
return `${k}=${JSON.stringify(v)}`;
|
|
621
|
-
}
|
|
622
|
-
const preview = v.replace(/[\r\n\t]+/g, " ").slice(0, 50).trim();
|
|
623
|
-
const sizeKb = (Buffer.byteLength(v, "utf8") / 1024).toFixed(1);
|
|
624
|
-
return `${k}="${preview}..." [${sizeKb} KB]`;
|
|
625
|
-
}
|
|
626
|
-
if (typeof v === "number" || typeof v === "boolean" || v === null) {
|
|
627
|
-
return `${k}=${v}`;
|
|
628
|
-
}
|
|
629
|
-
if (Array.isArray(v)) {
|
|
630
|
-
return `${k}=[Array(${v.length})]`;
|
|
631
|
-
}
|
|
632
|
-
if (typeof v === "object") {
|
|
633
|
-
const keys = Object.keys(v);
|
|
634
|
-
return `${k}={Object(${keys.length} keys)}`;
|
|
635
|
-
}
|
|
636
|
-
return `${k}=${String(v).slice(0, 60)}`;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function formatTranscriptItem(item, options = {}) {
|
|
640
|
-
const { includeThinking = false, includeToolCalls = false, maxContentChars = MAX_TURN_CHARS } = options;
|
|
641
|
-
const stepIdx = item.step_index !== undefined ? item.step_index : "?";
|
|
642
|
-
const type = item.type || "UNKNOWN";
|
|
643
|
-
const time = item.created_at ? item.created_at.replace(/\.\d+Z$/, "Z") : "";
|
|
644
|
-
const timeStr = time ? ` (${time})` : "";
|
|
645
|
-
|
|
646
|
-
let role = "SYSTEM";
|
|
647
|
-
if (type === "USER_INPUT") role = "USER";
|
|
648
|
-
else if (type === "PLANNER_RESPONSE") role = "ASSISTANT";
|
|
649
|
-
else if (type === "SUBAGENT_RESPONSE") role = "SUBAGENT";
|
|
650
|
-
else role = type;
|
|
651
|
-
|
|
652
|
-
const parts = [];
|
|
653
|
-
parts.push(`### [Step ${stepIdx} | ${role}]${timeStr}`);
|
|
654
|
-
|
|
655
|
-
// Thinking block
|
|
656
|
-
if (includeThinking && item.thinking) {
|
|
657
|
-
const cleanThinking = cleanMessageContent(item.thinking, 4000);
|
|
658
|
-
parts.push(`> <thinking>\n> ${cleanThinking.split("\n").join("\n> ")}\n> </thinking>`);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Tool calls
|
|
662
|
-
if ((includeToolCalls || role === "ASSISTANT") && Array.isArray(item.tool_calls) && item.tool_calls.length > 0) {
|
|
663
|
-
const tcSummary = item.tool_calls.map((tc) => {
|
|
664
|
-
const name = tc.name || tc.tool_name || "unknown_tool";
|
|
665
|
-
const args = tc.args || tc.arguments || {};
|
|
666
|
-
const compactArgs = Object.entries(args)
|
|
667
|
-
.map(([k, v]) => formatArgumentDescriptor(k, v))
|
|
668
|
-
.join(", ");
|
|
669
|
-
return `- \`${name}(${compactArgs})\``;
|
|
670
|
-
});
|
|
671
|
-
parts.push(`**Tool Calls (${item.tool_calls.length}):**\n${tcSummary.join("\n")}`);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Content
|
|
675
|
-
if (item.content) {
|
|
676
|
-
const cleaned = cleanMessageContent(item.content, maxContentChars);
|
|
677
|
-
if (cleaned.trim()) {
|
|
678
|
-
parts.push(cleaned.trim());
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
return parts.join("\n\n");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const regexCache = new Map();
|
|
686
|
-
const MAX_REGEX_CACHE_ENTRIES = 100;
|
|
687
|
-
|
|
688
|
-
function getCompiledRegex(pattern) {
|
|
689
|
-
if (!pattern || typeof pattern !== "string") return null;
|
|
690
|
-
const cached = regexCache.get(pattern);
|
|
691
|
-
if (cached) return cached;
|
|
692
|
-
|
|
693
|
-
let rx;
|
|
694
|
-
try {
|
|
695
|
-
rx = new RegExp(pattern, "i");
|
|
696
|
-
} catch {
|
|
697
|
-
rx = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (regexCache.size >= MAX_REGEX_CACHE_ENTRIES) {
|
|
701
|
-
const firstKey = regexCache.keys().next().value;
|
|
702
|
-
regexCache.delete(firstKey);
|
|
703
|
-
}
|
|
704
|
-
regexCache.set(pattern, rx);
|
|
705
|
-
return rx;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
async function batchFindFullSteps(fullPath, stepIndicesSet) {
|
|
709
|
-
const result = new Map();
|
|
710
|
-
if (!stepIndicesSet || stepIndicesSet.size === 0) return result;
|
|
711
|
-
|
|
712
|
-
let fileStream;
|
|
713
|
-
let rl;
|
|
714
|
-
try {
|
|
715
|
-
fileStream = fs.createReadStream(fullPath, { encoding: "utf-8" });
|
|
716
|
-
rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
717
|
-
|
|
718
|
-
for await (const line of rl) {
|
|
719
|
-
if (!line.trim()) continue;
|
|
720
|
-
try {
|
|
721
|
-
const item = JSON.parse(line);
|
|
722
|
-
if (stepIndicesSet.has(item.step_index)) {
|
|
723
|
-
result.set(item.step_index, item);
|
|
724
|
-
if (result.size >= stepIndicesSet.size) {
|
|
725
|
-
break;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
} catch {}
|
|
729
|
-
}
|
|
730
|
-
} catch {} finally {
|
|
731
|
-
if (rl) rl.close();
|
|
732
|
-
if (fileStream) fileStream.destroy();
|
|
733
|
-
}
|
|
734
|
-
return result;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
async function findFullStep(fullPath, targetStepIndex) {
|
|
738
|
-
const map = await batchFindFullSteps(fullPath, new Set([targetStepIndex]));
|
|
739
|
-
return map.get(targetStepIndex) || null;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async function handleReadTranscript({ conversationId, mode = "compact", lastTurns = 3, includeThinking = false } = {}) {
|
|
743
|
-
const resolved = resolveTranscriptPath(conversationId, mode);
|
|
744
|
-
if (resolved.error) {
|
|
745
|
-
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const { filePath, convId } = resolved;
|
|
749
|
-
const rawLines = [];
|
|
750
|
-
let fileStream;
|
|
751
|
-
let rl;
|
|
752
|
-
|
|
753
|
-
let totalStepsCount = 0;
|
|
754
|
-
const WINDOW_SIZE = lastTurns > 0 ? Math.max(100, lastTurns * 15) : Infinity;
|
|
755
|
-
|
|
756
|
-
try {
|
|
757
|
-
fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
758
|
-
rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
759
|
-
for await (const line of rl) {
|
|
760
|
-
const trimmed = line.trim();
|
|
761
|
-
if (!trimmed) continue;
|
|
762
|
-
totalStepsCount++;
|
|
763
|
-
if (WINDOW_SIZE === Infinity) {
|
|
764
|
-
rawLines.push(trimmed);
|
|
765
|
-
} else {
|
|
766
|
-
rawLines.push(trimmed);
|
|
767
|
-
if (rawLines.length > WINDOW_SIZE) {
|
|
768
|
-
rawLines.shift();
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
} catch (err) {
|
|
773
|
-
return { isError: true, content: [{ type: "text", text: `Error reading transcript stream: ${err.message}` }] };
|
|
774
|
-
} finally {
|
|
775
|
-
if (rl) rl.close();
|
|
776
|
-
if (fileStream) fileStream.destroy();
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
if (totalStepsCount === 0) {
|
|
780
|
-
return { content: [{ type: "text", text: `Transcript is empty at: ${filePath}` }] };
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
const items = [];
|
|
784
|
-
for (const line of rawLines) {
|
|
785
|
-
try {
|
|
786
|
-
items.push(JSON.parse(line));
|
|
787
|
-
} catch {
|
|
788
|
-
// Tolerates incomplete trailing flushes during concurrent writes
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
let selected = items;
|
|
793
|
-
if (lastTurns > 0) {
|
|
794
|
-
const turnItems = items.filter((it) => it.type === "USER_INPUT" || it.type === "PLANNER_RESPONSE");
|
|
795
|
-
const cutoff = Math.max(0, turnItems.length - lastTurns);
|
|
796
|
-
const minStep = turnItems[cutoff] ? turnItems[cutoff].step_index : 0;
|
|
797
|
-
selected = items.filter((it) => (it.step_index || 0) >= minStep);
|
|
798
|
-
}
|
|
799
|
-
const header = [
|
|
800
|
-
`# Conversation Transcript: \`${convId}\``,
|
|
801
|
-
`- Mode: **${mode}** (${path.basename(filePath)})`,
|
|
802
|
-
`- Total Steps in Log: ${totalStepsCount}`,
|
|
803
|
-
`- Displayed Steps: ${selected.length} (showing last ${lastTurns > 0 ? `${lastTurns} turns` : "all"})`,
|
|
804
|
-
"",
|
|
805
|
-
"---",
|
|
806
|
-
""
|
|
807
|
-
].join("\n");
|
|
808
|
-
|
|
809
|
-
const formattedBlocks = [];
|
|
810
|
-
let currentLength = header.length;
|
|
811
|
-
let clamped = false;
|
|
812
|
-
const maxWindowChars = mode === "compact" ? 3800 : MAX_OUTPUT_CHARS;
|
|
813
|
-
|
|
814
|
-
for (let i = selected.length - 1; i >= 0; i--) {
|
|
815
|
-
const item = selected[i];
|
|
816
|
-
const isDialogueTurn =
|
|
817
|
-
item.type === "USER_INPUT" ||
|
|
818
|
-
item.type === "PLANNER_RESPONSE" ||
|
|
819
|
-
item.type === "SUBAGENT_RESPONSE";
|
|
820
|
-
const block = formatTranscriptItem(item, {
|
|
821
|
-
includeThinking,
|
|
822
|
-
includeToolCalls: true,
|
|
823
|
-
maxContentChars: mode === "compact" && !isDialogueTurn ? 300 : MAX_TURN_CHARS
|
|
824
|
-
});
|
|
825
|
-
if (currentLength + block.length + 6 > maxWindowChars && formattedBlocks.length > 0) {
|
|
826
|
-
clamped = true;
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
formattedBlocks.unshift(block);
|
|
830
|
-
currentLength += block.length + 6;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
let fullOutput = header + formattedBlocks.join("\n\n---\n\n");
|
|
834
|
-
if (clamped) {
|
|
835
|
-
const ceilingLabel = mode === "compact" ? "3.8 KB inline" : "24 KB";
|
|
836
|
-
fullOutput += `\n\n... [Transcript display clamped at ${ceilingLabel} safety ceiling; use query_transcript(stepIndex: N) for specific steps] ...`;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
return { content: [{ type: "text", text: fullOutput }] };
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
async function handleQueryTranscript(args = {}) {
|
|
843
|
-
const {
|
|
844
|
-
conversationId,
|
|
845
|
-
query,
|
|
846
|
-
roles = ["user", "assistant"],
|
|
847
|
-
mode = "auto",
|
|
848
|
-
startStep,
|
|
849
|
-
endStep,
|
|
850
|
-
lastTurns,
|
|
851
|
-
includeThinking = false,
|
|
852
|
-
includeToolCalls = false,
|
|
853
|
-
maxResults = 5,
|
|
854
|
-
summaryOnly = false,
|
|
855
|
-
stepIndex,
|
|
856
|
-
field
|
|
857
|
-
} = args;
|
|
858
|
-
|
|
859
|
-
// Surgical single-step dereferencing (folded from get_step_detail)
|
|
860
|
-
if (stepIndex !== undefined && stepIndex !== null) {
|
|
861
|
-
const numIndex = Number(stepIndex);
|
|
862
|
-
if (isNaN(numIndex)) {
|
|
863
|
-
return {
|
|
864
|
-
isError: true,
|
|
865
|
-
content: [{ type: "text", text: `Error: 'stepIndex' must be a number (received: ${stepIndex}).` }]
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const resolvedFull = resolveTranscriptPath(conversationId, "full");
|
|
870
|
-
let step = null;
|
|
871
|
-
if (resolvedFull && resolvedFull.filePath) {
|
|
872
|
-
step = await findFullStep(resolvedFull.filePath, numIndex);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
if (!step) {
|
|
876
|
-
const resolvedCompact = resolveTranscriptPath(conversationId, "compact");
|
|
877
|
-
if (resolvedCompact && resolvedCompact.filePath) {
|
|
878
|
-
step = await findFullStep(resolvedCompact.filePath, numIndex);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
if (!step) {
|
|
882
|
-
return {
|
|
883
|
-
isError: true,
|
|
884
|
-
content: [{ type: "text", text: `Step index ${numIndex} not found in transcript.` }]
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const MAX_FIELD_CHARS = 24000;
|
|
889
|
-
|
|
890
|
-
if (field) {
|
|
891
|
-
const f = String(field).trim();
|
|
892
|
-
if (f === "content") {
|
|
893
|
-
const val = step.content || "(No content in step)";
|
|
894
|
-
return {
|
|
895
|
-
content: [{ type: "text", text: `### Step ${numIndex} Content\n\n${val.slice(0, MAX_FIELD_CHARS)}` }]
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
if (f === "thinking") {
|
|
899
|
-
const val = step.thinking || "(No thinking block in step)";
|
|
900
|
-
return {
|
|
901
|
-
content: [{ type: "text", text: `### Step ${numIndex} Thinking\n\n${val.slice(0, MAX_FIELD_CHARS)}` }]
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
if (f === "tool_calls") {
|
|
905
|
-
const val = JSON.stringify(step.tool_calls || [], null, 2);
|
|
906
|
-
return {
|
|
907
|
-
content: [{ type: "text", text: `### Step ${numIndex} Tool Calls\n\`\`\`json\n${val.slice(0, MAX_FIELD_CHARS)}\n\`\`\`` }]
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
if (f === "tool_args" || f.startsWith("tool_args:")) {
|
|
911
|
-
const argName = f.includes(":") ? f.split(":")[1].trim() : null;
|
|
912
|
-
const tc = Array.isArray(step.tool_calls) && step.tool_calls.length > 0 ? step.tool_calls[0] : null;
|
|
913
|
-
if (!tc) {
|
|
914
|
-
return {
|
|
915
|
-
content: [{ type: "text", text: `Step ${numIndex} contains no tool calls.` }]
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
const tcArgs = tc.args || tc.arguments || {};
|
|
919
|
-
if (argName) {
|
|
920
|
-
const argVal = tcArgs[argName];
|
|
921
|
-
const displayVal = typeof argVal === "string" ? argVal : JSON.stringify(argVal, null, 2);
|
|
922
|
-
return {
|
|
923
|
-
content: [
|
|
924
|
-
{
|
|
925
|
-
type: "text",
|
|
926
|
-
text: `### Step ${numIndex} Argument: \`${argName}\`\n\n${String(displayVal).slice(0, MAX_FIELD_CHARS)}`
|
|
927
|
-
}
|
|
928
|
-
]
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
return {
|
|
932
|
-
content: [
|
|
933
|
-
{
|
|
934
|
-
type: "text",
|
|
935
|
-
text: `### Step ${numIndex} Tool Arguments\n\`\`\`json\n${JSON.stringify(tcArgs, null, 2).slice(0, MAX_FIELD_CHARS)}\n\`\`\``
|
|
936
|
-
}
|
|
937
|
-
]
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const formatted = formatTranscriptItem(step, {
|
|
943
|
-
includeThinking: true,
|
|
944
|
-
includeToolCalls: true,
|
|
945
|
-
maxContentChars: MAX_FIELD_CHARS
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
return {
|
|
949
|
-
content: [{ type: "text", text: formatted }]
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const resolved = resolveTranscriptPath(conversationId, mode === "full" ? "full" : "compact");
|
|
954
|
-
if (resolved.error) {
|
|
955
|
-
return { isError: true, content: [{ type: "text", text: resolved.error }] };
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const { filePath, convId } = resolved;
|
|
959
|
-
const fullSiblingPath = filePath.replace(/transcript\.jsonl$/, "transcript_full.jsonl");
|
|
960
|
-
|
|
961
|
-
const roleSet = new Set((Array.isArray(roles) ? roles : [roles]).map((r) => String(r).toLowerCase()));
|
|
962
|
-
const matchAllRoles = roleSet.has("all");
|
|
963
|
-
|
|
964
|
-
const queryRegex = getCompiledRegex(query);
|
|
965
|
-
const isPlainLiteral = query && /^[A-Za-z0-9_ -]+$/.test(query);
|
|
966
|
-
|
|
967
|
-
const effectiveLimit = Math.min(lastTurns > 0 ? lastTurns : Infinity, maxResults > 0 ? maxResults : 5);
|
|
968
|
-
const ringCap = Number.isFinite(effectiveLimit) ? effectiveLimit : 1000;
|
|
969
|
-
const ring = [];
|
|
970
|
-
let matchedCount = 0;
|
|
971
|
-
|
|
972
|
-
let fileStream;
|
|
973
|
-
let rl;
|
|
974
|
-
|
|
975
|
-
try {
|
|
976
|
-
fileStream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
977
|
-
rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
978
|
-
|
|
979
|
-
for await (const line of rl) {
|
|
980
|
-
if (!line.trim()) continue;
|
|
981
|
-
|
|
982
|
-
// PERF-12: Raw-line prefilter for plain literal queries
|
|
983
|
-
if (isPlainLiteral && !queryRegex.test(line)) continue;
|
|
984
|
-
|
|
985
|
-
let item;
|
|
986
|
-
try {
|
|
987
|
-
item = JSON.parse(line);
|
|
988
|
-
} catch {
|
|
989
|
-
continue; // skip corrupted or half-flushed trailing lines
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const stepIdx = item.step_index || 0;
|
|
993
|
-
if (startStep !== undefined && stepIdx < startStep) continue;
|
|
994
|
-
if (endStep !== undefined && stepIdx > endStep) continue;
|
|
995
|
-
|
|
996
|
-
if (!matchAllRoles) {
|
|
997
|
-
const type = String(item.type || "").toUpperCase();
|
|
998
|
-
let roleMatches = false;
|
|
999
|
-
if (roleSet.has("user") && type === "USER_INPUT") roleMatches = true;
|
|
1000
|
-
if (roleSet.has("assistant") && type === "PLANNER_RESPONSE") roleMatches = true;
|
|
1001
|
-
if (roleSet.has("subagent") && type === "SUBAGENT_RESPONSE") roleMatches = true;
|
|
1002
|
-
if (roleSet.has("tool") && Array.isArray(item.tool_calls) && item.tool_calls.length > 0) roleMatches = true;
|
|
1003
|
-
if (roleSet.has("error") && (item.status === "ERROR" || (typeof item.content === "string" && item.content.includes("Error")))) roleMatches = true;
|
|
1004
|
-
if (!roleMatches) continue;
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
if (queryRegex) {
|
|
1008
|
-
let matches = false;
|
|
1009
|
-
if (typeof item.content === "string") {
|
|
1010
|
-
matches = queryRegex.test(item.content);
|
|
1011
|
-
} else if (item.content) {
|
|
1012
|
-
matches = queryRegex.test(JSON.stringify(item.content));
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (!matches && typeof item.thinking === "string") {
|
|
1016
|
-
matches = queryRegex.test(item.thinking);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (!matches && Array.isArray(item.tool_calls) && item.tool_calls.length > 0) {
|
|
1020
|
-
matches = queryRegex.test(JSON.stringify(item.tool_calls));
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
if (!matches) continue;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
matchedCount++;
|
|
1027
|
-
ring.push(item);
|
|
1028
|
-
if (ring.length > ringCap) {
|
|
1029
|
-
ring.shift();
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
return { isError: true, content: [{ type: "text", text: `Error streaming transcript: ${err.message}` }] };
|
|
1034
|
-
} finally {
|
|
1035
|
-
if (rl) rl.close();
|
|
1036
|
-
if (fileStream) fileStream.destroy();
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
if (matchedCount === 0) {
|
|
1040
|
-
return {
|
|
1041
|
-
content: [{
|
|
1042
|
-
type: "text",
|
|
1043
|
-
text: `No matching steps found in transcript for \`${convId}\` (query: "${query || '*'}", roles: [${Array.from(roleSet).join(", ")}]).`
|
|
1044
|
-
}]
|
|
1045
|
-
};
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
let finalItems = ring;
|
|
1049
|
-
if (lastTurns && lastTurns > 0 && finalItems.length > lastTurns) {
|
|
1050
|
-
finalItems = finalItems.slice(-lastTurns);
|
|
1051
|
-
}
|
|
1052
|
-
if (finalItems.length > maxResults) {
|
|
1053
|
-
finalItems = finalItems.slice(-maxResults);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// Auto-dereferencing if mode is "auto" and fields were truncated (single pass)
|
|
1057
|
-
// PERF-17: check fs.existsSync(fullSiblingPath) lazily ONLY if truncatedIndices.size > 0
|
|
1058
|
-
if (mode === "auto") { const truncatedIndices = new Set();
|
|
1059
|
-
for (const it of finalItems) {
|
|
1060
|
-
if (Array.isArray(it.truncated_fields) && it.truncated_fields.length > 0 && it.step_index !== undefined) {
|
|
1061
|
-
truncatedIndices.add(it.step_index);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
if (truncatedIndices.size > 0) {
|
|
1065
|
-
const fullMap = await batchFindFullSteps(fullSiblingPath, truncatedIndices);
|
|
1066
|
-
for (let i = 0; i < finalItems.length; i++) {
|
|
1067
|
-
const it = finalItems[i];
|
|
1068
|
-
if (it && fullMap.has(it.step_index)) {
|
|
1069
|
-
finalItems[i] = fullMap.get(it.step_index);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
if (summaryOnly) {
|
|
1076
|
-
const summaryLines = finalItems.map((it) => {
|
|
1077
|
-
const step = it.step_index ?? "?";
|
|
1078
|
-
const role = it.type === "USER_INPUT" ? "USER" : it.type === "PLANNER_RESPONSE" ? "ASSISTANT" : it.type === "SUBAGENT_RESPONSE" ? "SUBAGENT" : it.type || "SYSTEM";
|
|
1079
|
-
const time = it.created_at ? ` (${it.created_at.replace(/\.\d+Z$/, "Z")})` : "";
|
|
1080
|
-
let preview = "";
|
|
1081
|
-
if (it.content) {
|
|
1082
|
-
preview = cleanMessageContent(it.content, 120).replace(/\s+/g, " ").trim();
|
|
1083
|
-
} else if (Array.isArray(it.tool_calls) && it.tool_calls.length > 0) {
|
|
1084
|
-
preview = `Tool calls: ${it.tool_calls.map(tc => tc.name || tc.tool_name).join(", ")}`;
|
|
1085
|
-
}
|
|
1086
|
-
return `- **[Step ${step} | ${role}]**${time}: ${preview || "(empty)"}`;
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
const header = [
|
|
1090
|
-
`## Transcript Query Summary: \`${convId}\``,
|
|
1091
|
-
`- Filter: query=${query ? `"${query}"` : "NONE"}, roles=[${Array.from(roleSet).join(", ")}], mode=${mode}`,
|
|
1092
|
-
`- Matched Steps: ${matchedCount} (showing ${finalItems.length} in compact summary mode)`,
|
|
1093
|
-
"",
|
|
1094
|
-
"---",
|
|
1095
|
-
""
|
|
1096
|
-
].join("\n");
|
|
1097
|
-
|
|
1098
|
-
let output = header + summaryLines.join("\n");
|
|
1099
|
-
if (output.length > MAX_OUTPUT_CHARS) {
|
|
1100
|
-
output = output.slice(0, MAX_OUTPUT_CHARS) + "\n\n... [Query results clamped at 24 KB safety ceiling] ...";
|
|
1101
|
-
}
|
|
1102
|
-
return { content: [{ type: "text", text: output }] };
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
const header = [
|
|
1106
|
-
`## Transcript Query Results: \`${convId}\``,
|
|
1107
|
-
`- Filter: query=${query ? `"${query}"` : "NONE"}, roles=[${Array.from(roleSet).join(", ")}], mode=${mode}`,
|
|
1108
|
-
`- Matched Steps: ${matchedCount} (showing ${finalItems.length})`,
|
|
1109
|
-
"",
|
|
1110
|
-
"---",
|
|
1111
|
-
""
|
|
1112
|
-
].join("\n");
|
|
1113
|
-
|
|
1114
|
-
const blocks = [];
|
|
1115
|
-
let currentLength = header.length;
|
|
1116
|
-
let clamped = false;
|
|
1117
|
-
|
|
1118
|
-
for (let i = finalItems.length - 1; i >= 0; i--) {
|
|
1119
|
-
const block = formatTranscriptItem(finalItems[i], {
|
|
1120
|
-
includeThinking,
|
|
1121
|
-
includeToolCalls: includeToolCalls || roleSet.has("tool")
|
|
1122
|
-
});
|
|
1123
|
-
if (currentLength + block.length + 6 > MAX_OUTPUT_CHARS) {
|
|
1124
|
-
clamped = true;
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
blocks.unshift(block);
|
|
1128
|
-
currentLength += block.length + 6;
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
let output = header + blocks.join("\n\n---\n\n");
|
|
1132
|
-
if (clamped) {
|
|
1133
|
-
output += "\n\n... [Query results clamped at 24 KB safety ceiling] ...";
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
return { content: [{ type: "text", text: output }] };
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
let cachedRulesContent = null;
|
|
1140
|
-
let cachedRulesMtime = 0;
|
|
1141
|
-
|
|
1142
|
-
function getGovernanceRules() {
|
|
1143
|
-
try {
|
|
1144
|
-
if (fs.existsSync(rulesPath)) {
|
|
1145
|
-
const stat = fs.statSync(rulesPath);
|
|
1146
|
-
if (cachedRulesContent && stat.mtimeMs === cachedRulesMtime) {
|
|
1147
|
-
return cachedRulesContent;
|
|
1148
|
-
}
|
|
1149
|
-
cachedRulesContent = fs.readFileSync(rulesPath, "utf-8");
|
|
1150
|
-
cachedRulesMtime = stat.mtimeMs;
|
|
1151
|
-
return cachedRulesContent;
|
|
1152
|
-
}
|
|
1153
|
-
} catch {}
|
|
1154
|
-
return "# Governance rules not found";
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// --- JSON-RPC 2.0 Dispatcher ---
|
|
1158
|
-
async function handleRequest(request) {
|
|
1159
|
-
const { id, method, params } = request;
|
|
1160
|
-
|
|
1161
|
-
if (method === "initialize") {
|
|
1162
|
-
return {
|
|
1163
|
-
jsonrpc: "2.0",
|
|
1164
|
-
id,
|
|
1165
|
-
result: {
|
|
1166
|
-
protocolVersion: "2024-11-05",
|
|
1167
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
1168
|
-
capabilities: { tools: {}, resources: {}, prompts: {} }
|
|
1169
|
-
}
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
if (method === "notifications/initialized") {
|
|
1174
|
-
return null; // No response for notifications
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
if (method === "tools/list") {
|
|
1178
|
-
return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
if (method === "tools/call") {
|
|
1182
|
-
const { name, arguments: args = {} } = params || {};
|
|
1183
|
-
let toolResult;
|
|
1184
|
-
|
|
1185
|
-
if (name === "check_context_health") {
|
|
1186
|
-
toolResult = await handleCheckContextHealth(args);
|
|
1187
|
-
} else if (name === "get_installation_status") {
|
|
1188
|
-
toolResult = handleGetInstallationStatus();
|
|
1189
|
-
} else if (name === "sync_installation") {
|
|
1190
|
-
toolResult = await handleSyncInstallation(args);
|
|
1191
|
-
} else if (name === "read_transcript") {
|
|
1192
|
-
toolResult = await handleReadTranscript(args);
|
|
1193
|
-
} else if (name === "query_transcript") {
|
|
1194
|
-
toolResult = await handleQueryTranscript(args);
|
|
1195
|
-
} else {
|
|
1196
|
-
return {
|
|
1197
|
-
jsonrpc: "2.0",
|
|
1198
|
-
id,
|
|
1199
|
-
error: { code: -32601, message: `Tool not found: ${name}` }
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
return { jsonrpc: "2.0", id, result: toolResult };
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
if (method === "resources/list") {
|
|
1207
|
-
return { jsonrpc: "2.0", id, result: { resources: RESOURCES } };
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (method === "resources/read") {
|
|
1211
|
-
const { uri } = params || {};
|
|
1212
|
-
if (uri === "context-saver://rules/governance") {
|
|
1213
|
-
const content = getGovernanceRules();
|
|
1214
|
-
return {
|
|
1215
|
-
jsonrpc: "2.0",
|
|
1216
|
-
id,
|
|
1217
|
-
result: {
|
|
1218
|
-
contents: [
|
|
1219
|
-
{
|
|
1220
|
-
uri,
|
|
1221
|
-
mimeType: "text/markdown",
|
|
1222
|
-
text: content
|
|
1223
|
-
}
|
|
1224
|
-
]
|
|
1225
|
-
}
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
return {
|
|
1229
|
-
jsonrpc: "2.0",
|
|
1230
|
-
id,
|
|
1231
|
-
error: { code: -32602, message: `Resource not found: ${uri}` }
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
if (method === "prompts/list") {
|
|
1236
|
-
return { jsonrpc: "2.0", id, result: { prompts: PROMPTS } };
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (method === "prompts/get") {
|
|
1240
|
-
const { name } = params || {};
|
|
1241
|
-
if (name === "context_shield") {
|
|
1242
|
-
const content = getGovernanceRules();
|
|
1243
|
-
return {
|
|
1244
|
-
jsonrpc: "2.0",
|
|
1245
|
-
id,
|
|
1246
|
-
result: {
|
|
1247
|
-
description: "3-Layer Context Governance Rules",
|
|
1248
|
-
messages: [
|
|
1249
|
-
{
|
|
1250
|
-
role: "user",
|
|
1251
|
-
content: {
|
|
1252
|
-
type: "text",
|
|
1253
|
-
text: `Apply the following Antigravity Context Governance rules to this session:\n\n${content}`
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
]
|
|
1257
|
-
}
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1260
|
-
return {
|
|
1261
|
-
jsonrpc: "2.0",
|
|
1262
|
-
id,
|
|
1263
|
-
error: { code: -32602, message: `Prompt not found: ${name}` }
|
|
1264
|
-
};
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
return {
|
|
1268
|
-
jsonrpc: "2.0",
|
|
1269
|
-
id,
|
|
1270
|
-
error: { code: -32601, message: `Method not found: ${method}` }
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
if (isEntryPoint) {
|
|
1275
|
-
// Stdio JSON-RPC line loop
|
|
1276
|
-
const MAX_STDIO_PAYLOAD_BYTES = Number(process.env.AGY_MAX_STDIO_PAYLOAD_BYTES) || (10 * 1024 * 1024); // 10 MB ceiling default
|
|
1277
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
1278
|
-
|
|
1279
|
-
rl.on("line", async (line) => {
|
|
1280
|
-
if (!line || !line.trim()) return;
|
|
1281
|
-
if (line.length > MAX_STDIO_PAYLOAD_BYTES) {
|
|
1282
|
-
const errResp = {
|
|
1283
|
-
jsonrpc: "2.0",
|
|
1284
|
-
id: null,
|
|
1285
|
-
error: { code: -32600, message: "Invalid Request: Payload exceeds 10MB safety ceiling" }
|
|
1286
|
-
};
|
|
1287
|
-
process.stdout.write(JSON.stringify(errResp) + "\n");
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
try {
|
|
1291
|
-
const req = JSON.parse(line);
|
|
1292
|
-
const resp = await handleRequest(req);
|
|
1293
|
-
if (resp) {
|
|
1294
|
-
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
1295
|
-
}
|
|
1296
|
-
} catch (err) {
|
|
1297
|
-
const errResp = {
|
|
1298
|
-
jsonrpc: "2.0",
|
|
1299
|
-
id: null,
|
|
1300
|
-
error: { code: -32700, message: `Parse error: ${err.message}` }
|
|
1301
|
-
};
|
|
1302
|
-
process.stdout.write(JSON.stringify(errResp) + "\n");
|
|
1303
|
-
}
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
// Anti-Orphan Lifecycle Governance: Prevent lingering Node processes when parent IDE closes
|
|
1307
|
-
rl.on("close", () => process.exit(0));
|
|
1308
|
-
process.stdin.on("end", () => process.exit(0));
|
|
1309
|
-
process.stdin.on("close", () => process.exit(0));
|
|
1310
|
-
process.stdout.on("error", (err) => {
|
|
1311
|
-
if (err && err.code === "EPIPE") {
|
|
1312
|
-
process.exit(0);
|
|
1313
|
-
}
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
// Lightweight parent-process watchdog: If parent IDE process terminates, exit child
|
|
1317
|
-
const initialPpid = process.ppid;
|
|
1318
|
-
if (initialPpid && initialPpid > 1) {
|
|
1319
|
-
const watchdog = setInterval(() => {
|
|
1320
|
-
try {
|
|
1321
|
-
process.kill(initialPpid, 0);
|
|
1322
|
-
} catch (err) {
|
|
1323
|
-
if (err && err.code === "ESRCH") {
|
|
1324
|
-
process.exit(0);
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
}, 15000);
|
|
1328
|
-
watchdog.unref();
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
export { handleRequest };
|