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
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agy-Context-Saver CLI (v2.0.0)
|
|
5
|
+
*
|
|
6
|
+
* Local-first CLI for streaming transcript inspection, forensic querying,
|
|
7
|
+
* context health diagnostics, and lifecycle management.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
readTranscript,
|
|
15
|
+
queryTranscript,
|
|
16
|
+
getStepDetail,
|
|
17
|
+
checkContextHealth,
|
|
18
|
+
getBrainDir,
|
|
19
|
+
manageTranscriptAccessPolicy,
|
|
20
|
+
verifyTranscriptCoreAccess,
|
|
21
|
+
consumeTranscriptAccessGrant
|
|
22
|
+
} from "../lib/transcript-core.mjs";
|
|
23
|
+
import {
|
|
24
|
+
runInstall,
|
|
25
|
+
runUninstall,
|
|
26
|
+
runStatus,
|
|
27
|
+
runRestoreBackups
|
|
28
|
+
} from "../scripts/install-register.mjs";
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = path.dirname(__filename);
|
|
32
|
+
const pkgPath = path.resolve(__dirname, "../package.json");
|
|
33
|
+
|
|
34
|
+
function getVersion() {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
37
|
+
return pkg.version || "2.0.0";
|
|
38
|
+
} catch {
|
|
39
|
+
return "2.0.0";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printHelp() {
|
|
44
|
+
const version = getVersion();
|
|
45
|
+
console.log(`Agy-Context-Saver CLI (v${version})
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
agy-context <command> [options]
|
|
49
|
+
|
|
50
|
+
Transcript Commands:
|
|
51
|
+
agy-context transcript read [--id <conv>] [--mode compact|full] [--last-turns N] [--offset N] [--limit N] [--outline] [--include-thinking] [--json] [--file <path>]
|
|
52
|
+
agy-context transcript query [--id <conv>] [--query <regex>] [--roles csv] [--mode auto|compact|full] [--start N] [--end N] [--last-turns N] [--limit N] [--summary] [--include-tools] [--full-args] [--field content|thinking|tool_calls|tool_args|tool_args:<Key>|all] [--include-thinking] [--json] [--file <path>]
|
|
53
|
+
agy-context transcript step [--id <conv>] (--step <N|N,M|N-M|N..M> | --start N --end M) [--field content|thinking|tool_calls|tool_args|tool_args:<Key>|all] [--head N] [--tail N] [--full] [--json] [--file <path>]
|
|
54
|
+
agy-context transcript health [--id <conv>] [--json] [--file <path>]
|
|
55
|
+
agy-context transcript list [--limit N] [--json]
|
|
56
|
+
agy-context transcript access [--status | --off | --on | --reset | --self-only | --mode <open|allowlist|self-only|deny> | --allow <id,@roster*...> | --deny <id,@roster*...> | --ttl <sec>] [--json]
|
|
57
|
+
Aliases: grep/search/find -> query | stats/info -> health | tools -> query --roles assistant --field tool_args | outline/summary -> query --summary --include-tools | ls -> list | policy -> access
|
|
58
|
+
|
|
59
|
+
Lifecycle Commands:
|
|
60
|
+
agy-context status [--json]
|
|
61
|
+
agy-context setup [--check] [--allow-degraded]
|
|
62
|
+
agy-context uninstall [--restore-backups]
|
|
63
|
+
agy-context restore-backups
|
|
64
|
+
agy-context --help | -h | --version | -v
|
|
65
|
+
`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseFlags(argv, spec) {
|
|
69
|
+
const opts = {};
|
|
70
|
+
const positional = [];
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < argv.length; i++) {
|
|
73
|
+
const token = argv[i];
|
|
74
|
+
if (!token.startsWith("-") || token === "-") {
|
|
75
|
+
positional.push(token);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (token === "-h") {
|
|
80
|
+
opts.help = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (token === "-v") {
|
|
84
|
+
opts.version = true;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (token === "-c") {
|
|
88
|
+
if (!spec.booleans.has("check")) {
|
|
89
|
+
throw new Error(`Unknown flag '-c' for this command.`);
|
|
90
|
+
}
|
|
91
|
+
opts.check = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!token.startsWith("--")) {
|
|
96
|
+
throw new Error(`Unknown option '${token}'.`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const eqIdx = token.indexOf("=");
|
|
100
|
+
const key = eqIdx === -1 ? token.slice(2) : token.slice(2, eqIdx);
|
|
101
|
+
const inlineVal = eqIdx === -1 ? null : token.slice(eqIdx + 1);
|
|
102
|
+
|
|
103
|
+
if (key === "help") {
|
|
104
|
+
opts.help = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (key === "version") {
|
|
108
|
+
opts.version = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (spec.booleans.has(key)) {
|
|
113
|
+
if (inlineVal !== null) {
|
|
114
|
+
if (inlineVal !== "true" && inlineVal !== "false") {
|
|
115
|
+
throw new Error(`Boolean flag '--${key}' does not accept value '${inlineVal}'.`);
|
|
116
|
+
}
|
|
117
|
+
opts[key] = inlineVal === "true";
|
|
118
|
+
} else {
|
|
119
|
+
opts[key] = true;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (spec.values.has(key)) {
|
|
125
|
+
let val = inlineVal;
|
|
126
|
+
if (val === null) {
|
|
127
|
+
if (i + 1 >= argv.length) {
|
|
128
|
+
throw new Error(`Missing value for flag '--${key}'.`);
|
|
129
|
+
}
|
|
130
|
+
val = argv[++i];
|
|
131
|
+
}
|
|
132
|
+
opts[key] = val;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(`Unknown flag '--${key}'.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { opts, positional };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseOptionalInt(val, flagName) {
|
|
143
|
+
if (val === undefined) return undefined;
|
|
144
|
+
const num = Number(val);
|
|
145
|
+
if (!Number.isFinite(num) || !Number.isInteger(num)) {
|
|
146
|
+
throw new Error(`Flag '--${flagName}' must be an integer (received: '${val}').`);
|
|
147
|
+
}
|
|
148
|
+
return num;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateExplicitFileFlag(fileArg) {
|
|
152
|
+
if (fileArg === undefined) return undefined;
|
|
153
|
+
const trimmed = String(fileArg).trim();
|
|
154
|
+
if (!trimmed || !trimmed.toLowerCase().endsWith(".jsonl")) {
|
|
155
|
+
throw new Error(`Invalid --file '${fileArg}': must be an existing .jsonl transcript file.`);
|
|
156
|
+
}
|
|
157
|
+
const resolved = path.resolve(trimmed);
|
|
158
|
+
if (!fs.existsSync(resolved)) {
|
|
159
|
+
throw new Error(`Transcript file not found at --file '${resolved}'.`);
|
|
160
|
+
}
|
|
161
|
+
const real = fs.realpathSync(resolved);
|
|
162
|
+
if (!fs.statSync(real).isFile()) {
|
|
163
|
+
throw new Error(`Invalid --file '${resolved}': target is not a regular file.`);
|
|
164
|
+
}
|
|
165
|
+
return real;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function emitError(message, asJson = false, extra = {}) {
|
|
169
|
+
consumeTranscriptAccessGrant();
|
|
170
|
+
if (asJson) {
|
|
171
|
+
console.error(JSON.stringify({ isError: true, error: message, ...extra }, null, 2));
|
|
172
|
+
} else {
|
|
173
|
+
console.error(`Error: ${message}`);
|
|
174
|
+
}
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function handleTranscriptCommand(argv) {
|
|
179
|
+
const sub = argv[0];
|
|
180
|
+
if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
|
|
181
|
+
printHelp();
|
|
182
|
+
process.exit(0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const rest = argv.slice(1);
|
|
186
|
+
|
|
187
|
+
if (sub === "read" || sub === "outline" || sub === "summary") {
|
|
188
|
+
const { opts, positional } = parseFlags(rest, {
|
|
189
|
+
booleans: new Set(["include-thinking", "outline", "summary", "include-tools", "full-args", "json"]),
|
|
190
|
+
values: new Set(["id", "mode", "last-turns", "last", "tail", "offset", "limit", "start", "end", "file"])
|
|
191
|
+
});
|
|
192
|
+
if (opts.help) {
|
|
193
|
+
printHelp();
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
if (opts.id === undefined && positional.length === 1) {
|
|
197
|
+
opts.id = positional.shift();
|
|
198
|
+
}
|
|
199
|
+
if (positional.length > 0) {
|
|
200
|
+
throw new Error(`Unexpected positional arguments for 'transcript ${sub}': ${positional.join(" ")}`);
|
|
201
|
+
}
|
|
202
|
+
const explicitFile = validateExplicitFileFlag(opts.file);
|
|
203
|
+
const limit = parseOptionalInt(opts.limit, "limit");
|
|
204
|
+
|
|
205
|
+
if (sub === "outline" || sub === "summary" || opts.outline || opts.summary) {
|
|
206
|
+
const startStep = parseOptionalInt(opts.start, "start");
|
|
207
|
+
const endStep = parseOptionalInt(opts.end, "end");
|
|
208
|
+
const res = await queryTranscript({
|
|
209
|
+
conversationId: opts.id,
|
|
210
|
+
filePath: explicitFile,
|
|
211
|
+
startStep: startStep !== undefined ? startStep : 0,
|
|
212
|
+
endStep: endStep !== undefined ? endStep : 999999,
|
|
213
|
+
maxResults: limit !== undefined ? limit : 500,
|
|
214
|
+
summaryOnly: true,
|
|
215
|
+
includeToolCalls: true,
|
|
216
|
+
fullArgs: Boolean(opts["full-args"]),
|
|
217
|
+
includeThinking: Boolean(opts["include-thinking"]),
|
|
218
|
+
allowExternalPaths: false,
|
|
219
|
+
subcommand: sub
|
|
220
|
+
});
|
|
221
|
+
if (res.isError) {
|
|
222
|
+
emitError(res.text, Boolean(opts.json), res.candidates ? { candidates: res.candidates } : {});
|
|
223
|
+
}
|
|
224
|
+
if (opts.json) {
|
|
225
|
+
const { text, ...payload } = res;
|
|
226
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
227
|
+
} else {
|
|
228
|
+
console.log(res.text);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const mode = opts.mode || "compact";
|
|
234
|
+
if (mode !== "compact" && mode !== "full") {
|
|
235
|
+
throw new Error(`Invalid --mode '${mode}' for 'transcript read' (expected 'compact' or 'full').`);
|
|
236
|
+
}
|
|
237
|
+
const rawLastTurns = opts["last-turns"] !== undefined ? opts["last-turns"] : (opts.last !== undefined ? opts.last : opts.tail);
|
|
238
|
+
const lastTurns = parseOptionalInt(rawLastTurns, "last-turns");
|
|
239
|
+
const offset = parseOptionalInt(opts.offset, "offset");
|
|
240
|
+
|
|
241
|
+
const res = await readTranscript({
|
|
242
|
+
conversationId: opts.id,
|
|
243
|
+
filePath: explicitFile,
|
|
244
|
+
mode,
|
|
245
|
+
lastTurns: lastTurns !== undefined ? lastTurns : 3,
|
|
246
|
+
offset,
|
|
247
|
+
limit,
|
|
248
|
+
includeThinking: Boolean(opts["include-thinking"]),
|
|
249
|
+
allowExternalPaths: false
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (res.isError) {
|
|
253
|
+
emitError(res.text, Boolean(opts.json), res.candidates ? { candidates: res.candidates } : {});
|
|
254
|
+
}
|
|
255
|
+
if (opts.json) {
|
|
256
|
+
const { text, ...payload } = res;
|
|
257
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
258
|
+
} else {
|
|
259
|
+
console.log(res.text);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (sub === "query" || sub === "grep" || sub === "search" || sub === "find" || sub === "tools") {
|
|
265
|
+
const { opts, positional } = parseFlags(rest, {
|
|
266
|
+
booleans: new Set(["summary", "outline", "include-tools", "full-args", "include-thinking", "json"]),
|
|
267
|
+
values: new Set(["id", "query", "roles", "mode", "start", "end", "last-turns", "last", "tail", "limit", "field", "file"])
|
|
268
|
+
});
|
|
269
|
+
if (opts.help) {
|
|
270
|
+
printHelp();
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
if (sub === "grep" || sub === "search" || sub === "find") {
|
|
274
|
+
opts["include-tools"] = true;
|
|
275
|
+
}
|
|
276
|
+
if (sub === "tools") {
|
|
277
|
+
if (!opts.roles) opts.roles = "assistant";
|
|
278
|
+
if (!opts.field) opts.field = "tool_args";
|
|
279
|
+
opts["include-tools"] = true;
|
|
280
|
+
if (opts.start === undefined && opts.last === undefined && opts["last-turns"] === undefined && opts.tail === undefined) {
|
|
281
|
+
opts.start = "0";
|
|
282
|
+
if (opts.end === undefined) opts.end = "999999";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (opts.query === undefined && positional.length >= 1) {
|
|
286
|
+
opts.query = positional.shift();
|
|
287
|
+
}
|
|
288
|
+
if (opts.id === undefined && positional.length >= 1) {
|
|
289
|
+
opts.id = positional.shift();
|
|
290
|
+
}
|
|
291
|
+
if (positional.length > 0) {
|
|
292
|
+
throw new Error(`Unexpected positional arguments for 'transcript ${sub}': ${positional.join(" ")}`);
|
|
293
|
+
}
|
|
294
|
+
const mode = opts.mode || "auto";
|
|
295
|
+
if (mode !== "auto" && mode !== "compact" && mode !== "full") {
|
|
296
|
+
throw new Error(`Invalid --mode '${mode}' for 'transcript query' (expected 'auto', 'compact', or 'full').`);
|
|
297
|
+
}
|
|
298
|
+
const explicitFile = validateExplicitFileFlag(opts.file);
|
|
299
|
+
const startStep = parseOptionalInt(opts.start, "start");
|
|
300
|
+
const endStep = parseOptionalInt(opts.end, "end");
|
|
301
|
+
const rawLastTurns = opts["last-turns"] !== undefined ? opts["last-turns"] : (opts.last !== undefined ? opts.last : opts.tail);
|
|
302
|
+
const lastTurns = parseOptionalInt(rawLastTurns, "last-turns");
|
|
303
|
+
const maxResults = parseOptionalInt(opts.limit, "limit");
|
|
304
|
+
const roles = opts.roles
|
|
305
|
+
? opts.roles.split(",").map((r) => r.trim()).filter(Boolean)
|
|
306
|
+
: ["all"];
|
|
307
|
+
|
|
308
|
+
const res = await queryTranscript({
|
|
309
|
+
conversationId: opts.id,
|
|
310
|
+
filePath: explicitFile,
|
|
311
|
+
query: opts.query,
|
|
312
|
+
roles,
|
|
313
|
+
mode,
|
|
314
|
+
startStep,
|
|
315
|
+
endStep,
|
|
316
|
+
lastTurns,
|
|
317
|
+
maxResults,
|
|
318
|
+
summaryOnly: Boolean(opts.summary || opts.outline),
|
|
319
|
+
includeToolCalls: Boolean(opts["include-tools"]),
|
|
320
|
+
fullArgs: Boolean(opts["full-args"]),
|
|
321
|
+
field: opts.field,
|
|
322
|
+
includeThinking: Boolean(opts["include-thinking"]),
|
|
323
|
+
allowExternalPaths: false,
|
|
324
|
+
subcommand: sub
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
if (res.isError) {
|
|
328
|
+
emitError(res.text, Boolean(opts.json), res.candidates ? { candidates: res.candidates } : {});
|
|
329
|
+
}
|
|
330
|
+
if (opts.json) {
|
|
331
|
+
const { text, ...payload } = res;
|
|
332
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
333
|
+
} else {
|
|
334
|
+
console.log(res.text);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (sub === "step") {
|
|
340
|
+
const { opts, positional } = parseFlags(rest, {
|
|
341
|
+
booleans: new Set(["json", "full"]),
|
|
342
|
+
values: new Set(["id", "step", "start", "end", "field", "mode", "tail", "tail-lines", "head", "head-lines", "file"])
|
|
343
|
+
});
|
|
344
|
+
if (opts.help) {
|
|
345
|
+
printHelp();
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
if (opts.step === undefined && positional.length >= 1) {
|
|
349
|
+
opts.step = positional.shift();
|
|
350
|
+
}
|
|
351
|
+
if (opts.id === undefined && positional.length >= 1) {
|
|
352
|
+
opts.id = positional.shift();
|
|
353
|
+
}
|
|
354
|
+
if (positional.length > 0) {
|
|
355
|
+
throw new Error(`Unexpected positional arguments for 'transcript step': ${positional.join(" ")}`);
|
|
356
|
+
}
|
|
357
|
+
const startStep = parseOptionalInt(opts.start, "start");
|
|
358
|
+
const endStep = parseOptionalInt(opts.end, "end");
|
|
359
|
+
const tailLines = parseOptionalInt(opts["tail-lines"] !== undefined ? opts["tail-lines"] : opts.tail, "tail");
|
|
360
|
+
const headLines = parseOptionalInt(opts["head-lines"] !== undefined ? opts["head-lines"] : opts.head, "head");
|
|
361
|
+
if (opts.step === undefined && startStep === undefined && endStep === undefined) {
|
|
362
|
+
throw new Error(`Missing required flag '--step <N|N,M|N-M>' (or '--start N --end M') for 'transcript step'.`);
|
|
363
|
+
}
|
|
364
|
+
const explicitFile = validateExplicitFileFlag(opts.file);
|
|
365
|
+
|
|
366
|
+
const res = await getStepDetail({
|
|
367
|
+
conversationId: opts.id,
|
|
368
|
+
filePath: explicitFile,
|
|
369
|
+
stepIndex: opts.step,
|
|
370
|
+
startStep,
|
|
371
|
+
endStep,
|
|
372
|
+
field: opts.field || "all",
|
|
373
|
+
headLines,
|
|
374
|
+
tailLines,
|
|
375
|
+
allowExternalPaths: false
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (res.isError) {
|
|
379
|
+
emitError(res.text, Boolean(opts.json), res.candidates ? { candidates: res.candidates } : {});
|
|
380
|
+
}
|
|
381
|
+
if (opts.json) {
|
|
382
|
+
const { text, ...payload } = res;
|
|
383
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
384
|
+
} else {
|
|
385
|
+
console.log(res.text);
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (sub === "health" || sub === "stats" || sub === "info") {
|
|
391
|
+
const { opts, positional } = parseFlags(rest, {
|
|
392
|
+
booleans: new Set(["json"]),
|
|
393
|
+
values: new Set(["id", "file"])
|
|
394
|
+
});
|
|
395
|
+
if (opts.help) {
|
|
396
|
+
printHelp();
|
|
397
|
+
process.exit(0);
|
|
398
|
+
}
|
|
399
|
+
if (opts.id === undefined && positional.length === 1) {
|
|
400
|
+
opts.id = positional.shift();
|
|
401
|
+
}
|
|
402
|
+
if (positional.length > 0) {
|
|
403
|
+
throw new Error(`Unexpected positional arguments for 'transcript ${sub}': ${positional.join(" ")}`);
|
|
404
|
+
}
|
|
405
|
+
const explicitFile = validateExplicitFileFlag(opts.file);
|
|
406
|
+
|
|
407
|
+
const res = await checkContextHealth({
|
|
408
|
+
conversationId: opts.id,
|
|
409
|
+
filePath: explicitFile,
|
|
410
|
+
allowExternalPaths: false
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (res.isError) {
|
|
414
|
+
emitError(res.text, Boolean(opts.json), res.candidates ? { candidates: res.candidates } : {});
|
|
415
|
+
}
|
|
416
|
+
if (opts.json) {
|
|
417
|
+
const { text, reportText, ...payload } = res;
|
|
418
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
419
|
+
} else {
|
|
420
|
+
console.log(res.reportText);
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (sub === "list" || sub === "ls") {
|
|
426
|
+
const { opts } = parseFlags(rest, {
|
|
427
|
+
booleans: new Set(["json"]),
|
|
428
|
+
values: new Set(["limit"])
|
|
429
|
+
});
|
|
430
|
+
if (opts.help) {
|
|
431
|
+
printHelp();
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
const accessCheck = verifyTranscriptCoreAccess({ subcommand: sub });
|
|
435
|
+
if (!accessCheck.allowed) {
|
|
436
|
+
emitError(accessCheck.error, Boolean(opts.json));
|
|
437
|
+
}
|
|
438
|
+
const brainDir = getBrainDir();
|
|
439
|
+
const limit = opts.limit !== undefined ? Math.max(1, Number(opts.limit) || 10) : 10;
|
|
440
|
+
const entries = [];
|
|
441
|
+
if (brainDir && fs.existsSync(brainDir)) {
|
|
442
|
+
for (const name of fs.readdirSync(brainDir)) {
|
|
443
|
+
const convDir = path.join(brainDir, name);
|
|
444
|
+
const transcriptFile = path.join(convDir, ".system_generated", "logs", "transcript.jsonl");
|
|
445
|
+
if (fs.existsSync(transcriptFile)) {
|
|
446
|
+
try {
|
|
447
|
+
const stat = fs.statSync(transcriptFile);
|
|
448
|
+
entries.push({
|
|
449
|
+
conversationId: name,
|
|
450
|
+
updatedAt: stat.mtime.toISOString(),
|
|
451
|
+
mtimeMs: stat.mtimeMs,
|
|
452
|
+
bytes: stat.size
|
|
453
|
+
});
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
458
|
+
}
|
|
459
|
+
const sliced = entries.slice(0, limit).map(({ mtimeMs, ...restItem }) => restItem);
|
|
460
|
+
if (opts.json) {
|
|
461
|
+
console.log(JSON.stringify({ total: entries.length, conversations: sliced }, null, 2));
|
|
462
|
+
} else {
|
|
463
|
+
const lines = [`Recent Conversations (${sliced.length} of ${entries.length} in ${brainDir}):`];
|
|
464
|
+
for (const item of sliced) {
|
|
465
|
+
lines.push(`- ${item.conversationId} | updated: ${item.updatedAt} | ${(item.bytes / 1024).toFixed(1)} KB`);
|
|
466
|
+
}
|
|
467
|
+
console.log(lines.join("\n"));
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (sub === "access" || sub === "policy") {
|
|
473
|
+
const { opts, positional } = parseFlags(rest, {
|
|
474
|
+
booleans: new Set(["status", "off", "deny-children", "on", "reset", "self-only", "json"]),
|
|
475
|
+
values: new Set(["mode", "allow", "deny", "ttl"])
|
|
476
|
+
});
|
|
477
|
+
if (opts.help) {
|
|
478
|
+
printHelp();
|
|
479
|
+
process.exit(0);
|
|
480
|
+
}
|
|
481
|
+
if (positional.length === 1 && !opts.status && !opts.off && !opts["deny-children"] && !opts.on && !opts.reset && !opts["self-only"] && opts.mode === undefined) {
|
|
482
|
+
const p0 = positional[0].toLowerCase();
|
|
483
|
+
if (p0 === "status") opts.status = true;
|
|
484
|
+
else if (p0 === "off" || p0 === "deny" || p0 === "lock") opts.off = true;
|
|
485
|
+
else if (p0 === "on" || p0 === "open" || p0 === "reset" || p0 === "unlock") opts.on = true;
|
|
486
|
+
else if (p0 === "self-only") opts["self-only"] = true;
|
|
487
|
+
else throw new Error(`Unexpected positional argument '${positional[0]}' for 'transcript ${sub}'.`);
|
|
488
|
+
positional.shift();
|
|
489
|
+
}
|
|
490
|
+
if (positional.length > 0) {
|
|
491
|
+
throw new Error(`Unexpected positional arguments for 'transcript ${sub}': ${positional.join(" ")}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const hasMutationFlag =
|
|
495
|
+
Boolean(opts.off) ||
|
|
496
|
+
Boolean(opts["deny-children"]) ||
|
|
497
|
+
Boolean(opts.on) ||
|
|
498
|
+
Boolean(opts.reset) ||
|
|
499
|
+
Boolean(opts["self-only"]) ||
|
|
500
|
+
opts.mode !== undefined ||
|
|
501
|
+
opts.allow !== undefined ||
|
|
502
|
+
opts.deny !== undefined ||
|
|
503
|
+
opts.ttl !== undefined;
|
|
504
|
+
|
|
505
|
+
const res = manageTranscriptAccessPolicy({
|
|
506
|
+
status: Boolean(opts.status) || !hasMutationFlag,
|
|
507
|
+
off: Boolean(opts.off),
|
|
508
|
+
denyChildren: Boolean(opts["deny-children"]),
|
|
509
|
+
on: Boolean(opts.on),
|
|
510
|
+
reset: Boolean(opts.reset),
|
|
511
|
+
selfOnly: Boolean(opts["self-only"]),
|
|
512
|
+
mode: opts.mode,
|
|
513
|
+
allow: opts.allow,
|
|
514
|
+
deny: opts.deny,
|
|
515
|
+
ttl: opts.ttl
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
if (opts.json) {
|
|
519
|
+
const { text, ...payload } = res;
|
|
520
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
521
|
+
} else {
|
|
522
|
+
console.log(res.text);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
throw new Error(`Unknown transcript subcommand '${sub}'. Expected read, query, step, health, list, or access.`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function main() {
|
|
531
|
+
const argv = process.argv.slice(2);
|
|
532
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
|
|
533
|
+
printHelp();
|
|
534
|
+
process.exit(0);
|
|
535
|
+
}
|
|
536
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
537
|
+
console.log(getVersion());
|
|
538
|
+
process.exit(0);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const cmd = argv[0];
|
|
542
|
+
const rest = argv.slice(1);
|
|
543
|
+
|
|
544
|
+
if (cmd === "transcript") {
|
|
545
|
+
await handleTranscriptCommand(rest);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (cmd === "status" || cmd === "--status") {
|
|
550
|
+
const { opts, positional } = parseFlags(rest, {
|
|
551
|
+
booleans: new Set(["json"]),
|
|
552
|
+
values: new Set()
|
|
553
|
+
});
|
|
554
|
+
if (opts.help) {
|
|
555
|
+
printHelp();
|
|
556
|
+
process.exit(0);
|
|
557
|
+
}
|
|
558
|
+
if (positional.length > 0) {
|
|
559
|
+
throw new Error(`Unexpected arguments for 'status': ${positional.join(" ")}`);
|
|
560
|
+
}
|
|
561
|
+
const status = runStatus({ silent: Boolean(opts.json) });
|
|
562
|
+
if (opts.json) {
|
|
563
|
+
console.log(JSON.stringify(status, null, 2));
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (cmd === "setup" || cmd === "install" || cmd === "--install") {
|
|
569
|
+
const { opts, positional } = parseFlags(rest, {
|
|
570
|
+
booleans: new Set(["check", "allow-degraded"]),
|
|
571
|
+
values: new Set()
|
|
572
|
+
});
|
|
573
|
+
if (opts.help) {
|
|
574
|
+
printHelp();
|
|
575
|
+
process.exit(0);
|
|
576
|
+
}
|
|
577
|
+
if (positional.length > 0) {
|
|
578
|
+
throw new Error(`Unexpected arguments for 'setup': ${positional.join(" ")}`);
|
|
579
|
+
}
|
|
580
|
+
await runInstall({
|
|
581
|
+
checkOnly: Boolean(opts.check),
|
|
582
|
+
allowDegraded: Boolean(opts["allow-degraded"])
|
|
583
|
+
});
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (cmd === "uninstall" || cmd === "--uninstall") {
|
|
588
|
+
const { opts, positional } = parseFlags(rest, {
|
|
589
|
+
booleans: new Set(["restore-backups"]),
|
|
590
|
+
values: new Set()
|
|
591
|
+
});
|
|
592
|
+
if (opts.help) {
|
|
593
|
+
printHelp();
|
|
594
|
+
process.exit(0);
|
|
595
|
+
}
|
|
596
|
+
if (positional.length > 0) {
|
|
597
|
+
throw new Error(`Unexpected arguments for 'uninstall': ${positional.join(" ")}`);
|
|
598
|
+
}
|
|
599
|
+
await runUninstall({
|
|
600
|
+
restoreBackups: Boolean(opts["restore-backups"])
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (cmd === "restore-backups" || cmd === "--restore-backups") {
|
|
606
|
+
await runRestoreBackups();
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
throw new Error(`Unknown command '${cmd}'. Run 'agy-context --help' for usage.`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
main().catch((err) => {
|
|
614
|
+
const wantsJson = process.argv.includes("--json");
|
|
615
|
+
emitError(err?.message || String(err), wantsJson);
|
|
616
|
+
});
|
package/hooks.json
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"agy-context-saver": {
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "manage_task|schedule|run_command|view_file|read_file|read_many_files|grep_search|find_by_name|list_dir|replace_file_content|write_to_file",
|
|
5
|
+
"matcher": "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",
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "node scripts/execution-guard-hook.mjs",
|
|
10
|
-
"timeout":
|
|
9
|
+
"command": "node ./scripts/execution-guard-hook.mjs",
|
|
10
|
+
"timeout": 10
|
|
11
11
|
}
|
|
12
12
|
]
|
|
13
13
|
}
|
package/install.ps1
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Agy-Context-Saver Universal Installer for Windows
|
|
2
|
-
# Installs
|
|
2
|
+
# Installs plugin link, CLI shims, RTK >= 1.1.0, and configures node.cmd for hooks
|
|
3
3
|
|
|
4
4
|
$ErrorActionPreference = "Stop"
|
|
5
5
|
|