@thecat69/cache-ctrl 1.0.0 → 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/README.md +289 -78
- package/cache_ctrl.ts +107 -25
- package/package.json +2 -1
- package/skills/cache-ctrl-caller/SKILL.md +53 -114
- package/skills/cache-ctrl-external/SKILL.md +29 -89
- package/skills/cache-ctrl-local/SKILL.md +82 -164
- package/src/analysis/graphBuilder.ts +85 -0
- package/src/analysis/pageRank.ts +164 -0
- package/src/analysis/symbolExtractor.ts +240 -0
- package/src/cache/cacheManager.ts +53 -4
- package/src/cache/externalCache.ts +72 -77
- package/src/cache/graphCache.ts +12 -0
- package/src/cache/localCache.ts +2 -0
- package/src/commands/checkFiles.ts +9 -6
- package/src/commands/flush.ts +9 -2
- package/src/commands/graph.ts +131 -0
- package/src/commands/inspect.ts +13 -181
- package/src/commands/inspectExternal.ts +79 -0
- package/src/commands/inspectLocal.ts +134 -0
- package/src/commands/install.ts +6 -0
- package/src/commands/invalidate.ts +24 -24
- package/src/commands/list.ts +11 -11
- package/src/commands/map.ts +87 -0
- package/src/commands/prune.ts +20 -8
- package/src/commands/search.ts +9 -2
- package/src/commands/touch.ts +15 -25
- package/src/commands/uninstall.ts +103 -0
- package/src/commands/update.ts +65 -0
- package/src/commands/version.ts +14 -0
- package/src/commands/watch.ts +270 -0
- package/src/commands/writeExternal.ts +51 -0
- package/src/commands/writeLocal.ts +121 -0
- package/src/files/changeDetector.ts +15 -0
- package/src/files/gitFiles.ts +15 -0
- package/src/files/openCodeInstaller.ts +21 -2
- package/src/index.ts +314 -58
- package/src/search/keywordSearch.ts +24 -0
- package/src/types/cache.ts +38 -26
- package/src/types/commands.ts +123 -22
- package/src/types/result.ts +26 -9
- package/src/utils/errors.ts +14 -0
- package/src/utils/traversal.ts +42 -0
- package/src/commands/checkFreshness.ts +0 -123
- package/src/commands/write.ts +0 -170
- package/src/http/freshnessChecker.ts +0 -116
package/src/index.ts
CHANGED
|
@@ -5,12 +5,19 @@ import { flushCommand } from "./commands/flush.js";
|
|
|
5
5
|
import { invalidateCommand } from "./commands/invalidate.js";
|
|
6
6
|
import { touchCommand } from "./commands/touch.js";
|
|
7
7
|
import { pruneCommand } from "./commands/prune.js";
|
|
8
|
-
import { checkFreshnessCommand } from "./commands/checkFreshness.js";
|
|
9
8
|
import { checkFilesCommand } from "./commands/checkFiles.js";
|
|
10
9
|
import { searchCommand } from "./commands/search.js";
|
|
11
|
-
import {
|
|
10
|
+
import { writeLocalCommand } from "./commands/writeLocal.js";
|
|
11
|
+
import { writeExternalCommand } from "./commands/writeExternal.js";
|
|
12
12
|
import { installCommand } from "./commands/install.js";
|
|
13
|
+
import { updateCommand } from "./commands/update.js";
|
|
14
|
+
import { uninstallCommand } from "./commands/uninstall.js";
|
|
15
|
+
import { graphCommand } from "./commands/graph.js";
|
|
16
|
+
import { mapCommand } from "./commands/map.js";
|
|
17
|
+
import { watchCommand } from "./commands/watch.js";
|
|
18
|
+
import { versionCommand } from "./commands/version.js";
|
|
13
19
|
import { ErrorCode } from "./types/result.js";
|
|
20
|
+
import { toUnknownResult } from "./utils/errors.js";
|
|
14
21
|
|
|
15
22
|
type CommandName =
|
|
16
23
|
| "list"
|
|
@@ -19,11 +26,17 @@ type CommandName =
|
|
|
19
26
|
| "invalidate"
|
|
20
27
|
| "touch"
|
|
21
28
|
| "prune"
|
|
22
|
-
| "check-freshness"
|
|
23
29
|
| "check-files"
|
|
24
30
|
| "search"
|
|
25
|
-
| "write"
|
|
26
|
-
| "
|
|
31
|
+
| "write-local"
|
|
32
|
+
| "write-external"
|
|
33
|
+
| "install"
|
|
34
|
+
| "update"
|
|
35
|
+
| "uninstall"
|
|
36
|
+
| "graph"
|
|
37
|
+
| "map"
|
|
38
|
+
| "watch"
|
|
39
|
+
| "version";
|
|
27
40
|
|
|
28
41
|
function isKnownCommand(cmd: string): cmd is CommandName {
|
|
29
42
|
return Object.hasOwn(COMMAND_HELP as Record<string, unknown>, cmd);
|
|
@@ -118,19 +131,6 @@ const COMMAND_HELP: Record<CommandName, CommandHelp> = {
|
|
|
118
131
|
" --delete Actually delete the stale entries (dry-run if omitted)",
|
|
119
132
|
].join("\n"),
|
|
120
133
|
},
|
|
121
|
-
"check-freshness": {
|
|
122
|
-
usage: "check-freshness <subject-keyword> [--url <url>]",
|
|
123
|
-
description: "Send HTTP HEAD requests to verify source freshness",
|
|
124
|
-
details: [
|
|
125
|
-
" Arguments:",
|
|
126
|
-
" <subject-keyword> Keyword identifying the cache entry to check",
|
|
127
|
-
"",
|
|
128
|
-
" Options:",
|
|
129
|
-
" --url <url> Override the URL used for the HEAD request",
|
|
130
|
-
"",
|
|
131
|
-
" Output: HTTP response metadata and freshness verdict.",
|
|
132
|
-
].join("\n"),
|
|
133
|
-
},
|
|
134
134
|
"check-files": {
|
|
135
135
|
usage: "check-files",
|
|
136
136
|
description: "Compare tracked local files against stored mtime/hash",
|
|
@@ -152,13 +152,25 @@ const COMMAND_HELP: Record<CommandName, CommandHelp> = {
|
|
|
152
152
|
" Output: Ranked list of matching cache entries.",
|
|
153
153
|
].join("\n"),
|
|
154
154
|
},
|
|
155
|
-
write: {
|
|
156
|
-
usage: "write
|
|
157
|
-
description: "Write a validated cache entry
|
|
155
|
+
"write-local": {
|
|
156
|
+
usage: "write-local --data '<json>'",
|
|
157
|
+
description: "Write a validated local cache entry",
|
|
158
|
+
details: [
|
|
159
|
+
" Arguments:",
|
|
160
|
+
" (none)",
|
|
161
|
+
"",
|
|
162
|
+
" Options:",
|
|
163
|
+
" --data '<json>' JSON string containing the cache entry payload",
|
|
164
|
+
"",
|
|
165
|
+
" Output: Confirmation with the written entry's key.",
|
|
166
|
+
].join("\n"),
|
|
167
|
+
},
|
|
168
|
+
"write-external": {
|
|
169
|
+
usage: "write-external <subject> --data '<json>'",
|
|
170
|
+
description: "Write a validated external cache entry",
|
|
158
171
|
details: [
|
|
159
172
|
" Arguments:",
|
|
160
|
-
" <
|
|
161
|
-
" [subject] Optional subject identifier (required for external agent)",
|
|
173
|
+
" <subject> Subject identifier for the external entry",
|
|
162
174
|
"",
|
|
163
175
|
" Options:",
|
|
164
176
|
" --data '<json>' JSON string containing the cache entry payload",
|
|
@@ -179,6 +191,84 @@ const COMMAND_HELP: Record<CommandName, CommandHelp> = {
|
|
|
179
191
|
" Output: JSON object describing installed tool/skill paths.",
|
|
180
192
|
].join("\n"),
|
|
181
193
|
},
|
|
194
|
+
update: {
|
|
195
|
+
usage: "update [--config-dir <path>]",
|
|
196
|
+
description: "Update npm package globally and refresh OpenCode integration",
|
|
197
|
+
details: [
|
|
198
|
+
" Arguments:",
|
|
199
|
+
" (none)",
|
|
200
|
+
"",
|
|
201
|
+
" Options:",
|
|
202
|
+
" --config-dir <path> Override the OpenCode config directory (default: platform-specific)",
|
|
203
|
+
"",
|
|
204
|
+
" Output: JSON object with package update status, installed paths, and warnings.",
|
|
205
|
+
].join("\n"),
|
|
206
|
+
},
|
|
207
|
+
uninstall: {
|
|
208
|
+
usage: "uninstall [--config-dir <path>]",
|
|
209
|
+
description: "Remove OpenCode integration files and uninstall global npm package",
|
|
210
|
+
details: [
|
|
211
|
+
" Arguments:",
|
|
212
|
+
" (none)",
|
|
213
|
+
"",
|
|
214
|
+
" Options:",
|
|
215
|
+
" --config-dir <path> Override the OpenCode config directory (default: platform-specific)",
|
|
216
|
+
"",
|
|
217
|
+
" Output: JSON object with removed paths, npm uninstall status, and warnings.",
|
|
218
|
+
].join("\n"),
|
|
219
|
+
},
|
|
220
|
+
graph: {
|
|
221
|
+
usage: "graph [--max-tokens <number>] [--seed <path>[,<path>...]]",
|
|
222
|
+
description: "Return a PageRank-ranked dependency graph under a token budget",
|
|
223
|
+
details: [
|
|
224
|
+
" Arguments:",
|
|
225
|
+
" (none)",
|
|
226
|
+
"",
|
|
227
|
+
" Options:",
|
|
228
|
+
" --max-tokens <number> Token budget for ranked_files output (default: 1024)",
|
|
229
|
+
" --seed <path>[,<path>...] Personalize rank toward specific file path(s)",
|
|
230
|
+
" (repeat --seed to provide multiple values)",
|
|
231
|
+
"",
|
|
232
|
+
" Output: Ranked files with deps, defs, and ref_count from graph.json.",
|
|
233
|
+
].join("\n"),
|
|
234
|
+
},
|
|
235
|
+
map: {
|
|
236
|
+
usage: "map [--depth overview|modules|full] [--folder <path-prefix>]",
|
|
237
|
+
description: "Return a semantic map of local context.json",
|
|
238
|
+
details: [
|
|
239
|
+
" Arguments:",
|
|
240
|
+
" (none)",
|
|
241
|
+
"",
|
|
242
|
+
" Options:",
|
|
243
|
+
" --depth overview|modules|full Output depth (default: overview)",
|
|
244
|
+
" --folder <path-prefix> Restrict map to files whose path starts with prefix",
|
|
245
|
+
"",
|
|
246
|
+
" Output: JSON object with global_facts, files, optional modules, and total_files.",
|
|
247
|
+
].join("\n"),
|
|
248
|
+
},
|
|
249
|
+
watch: {
|
|
250
|
+
usage: "watch [--verbose]",
|
|
251
|
+
description: "Watch for file changes and recompute the dependency graph",
|
|
252
|
+
details: [
|
|
253
|
+
" Arguments:",
|
|
254
|
+
" (none)",
|
|
255
|
+
"",
|
|
256
|
+
" Options:",
|
|
257
|
+
" --verbose Log watcher lifecycle and rebuild events",
|
|
258
|
+
"",
|
|
259
|
+
" Output: Long-running daemon process that updates graph.json on source changes.",
|
|
260
|
+
].join("\n"),
|
|
261
|
+
},
|
|
262
|
+
version: {
|
|
263
|
+
usage: "version",
|
|
264
|
+
description: "Show the current cache-ctrl package version",
|
|
265
|
+
details: [
|
|
266
|
+
" Arguments:",
|
|
267
|
+
" (none)",
|
|
268
|
+
"",
|
|
269
|
+
" Output: JSON object containing the current package version.",
|
|
270
|
+
].join("\n"),
|
|
271
|
+
},
|
|
182
272
|
};
|
|
183
273
|
|
|
184
274
|
const GLOBAL_OPTIONS_SECTION = [
|
|
@@ -206,7 +296,7 @@ export function printHelp(command?: string): boolean {
|
|
|
206
296
|
...Object.values(COMMAND_HELP).map((h) => h.usage.length),
|
|
207
297
|
);
|
|
208
298
|
|
|
209
|
-
for (const
|
|
299
|
+
for (const help of Object.values(COMMAND_HELP)) {
|
|
210
300
|
const paddedUsage = help.usage.padEnd(maxUsageLen);
|
|
211
301
|
lines.push(` ${paddedUsage} ${help.description}`);
|
|
212
302
|
}
|
|
@@ -216,12 +306,12 @@ export function printHelp(command?: string): boolean {
|
|
|
216
306
|
return true;
|
|
217
307
|
}
|
|
218
308
|
|
|
219
|
-
const sanitized = command.replace(/[\x00-\x1F\x7F]/g, "");
|
|
220
|
-
|
|
221
309
|
if (command === "help") {
|
|
222
310
|
return printHelp();
|
|
223
311
|
}
|
|
224
312
|
|
|
313
|
+
const sanitized = command.replace(/[\x00-\x1F\x7F]/g, "");
|
|
314
|
+
|
|
225
315
|
if (!isKnownCommand(command)) {
|
|
226
316
|
process.stderr.write(`Unknown command: "${sanitized}". Run 'cache-ctrl help' for available commands.\n`);
|
|
227
317
|
return false;
|
|
@@ -257,6 +347,12 @@ function printError(error: { ok: false; error: string; code: string }, pretty: b
|
|
|
257
347
|
}
|
|
258
348
|
}
|
|
259
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Prints a structured usage error and terminates the process.
|
|
352
|
+
*
|
|
353
|
+
* @param message - Human-readable usage failure detail.
|
|
354
|
+
* @remarks Always exits with process code `2` to distinguish usage failures from runtime errors.
|
|
355
|
+
*/
|
|
260
356
|
function usageError(message: string): never {
|
|
261
357
|
process.stderr.write(JSON.stringify({ ok: false, error: message, code: ErrorCode.INVALID_ARGS }) + "\n");
|
|
262
358
|
process.exit(2);
|
|
@@ -265,8 +361,44 @@ function usageError(message: string): never {
|
|
|
265
361
|
export { usageError };
|
|
266
362
|
|
|
267
363
|
/** Flags that consume the following token as their value. Boolean flags must NOT appear here. */
|
|
268
|
-
const VALUE_FLAGS = new Set([
|
|
364
|
+
const VALUE_FLAGS = new Set([
|
|
365
|
+
"data",
|
|
366
|
+
"agent",
|
|
367
|
+
"max-age",
|
|
368
|
+
"filter",
|
|
369
|
+
"folder",
|
|
370
|
+
"search-facts",
|
|
371
|
+
"config-dir",
|
|
372
|
+
"max-tokens",
|
|
373
|
+
"seed",
|
|
374
|
+
"depth",
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
function collectFlagValues(argv: string[], flagName: string): string[] {
|
|
378
|
+
const values: string[] = [];
|
|
379
|
+
|
|
380
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
381
|
+
if (argv[i] !== `--${flagName}`) {
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const next = argv[i + 1];
|
|
385
|
+
if (next !== undefined) {
|
|
386
|
+
values.push(next);
|
|
387
|
+
i += 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return values;
|
|
392
|
+
}
|
|
269
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Parses raw CLI argv tokens into positional args and flag key/value pairs.
|
|
396
|
+
*
|
|
397
|
+
* @param argv - Raw argument tokens (typically `process.argv.slice(2)`).
|
|
398
|
+
* @returns Parsed positional args and normalized flags map.
|
|
399
|
+
* @remarks Flags listed in `VALUE_FLAGS` consume the following token as their value;
|
|
400
|
+
* all other `--flag` tokens are treated as boolean flags.
|
|
401
|
+
*/
|
|
270
402
|
export function parseArgs(argv: string[]): { args: string[]; flags: Record<string, string | boolean> } {
|
|
271
403
|
const positional: string[] = [];
|
|
272
404
|
const flags: Record<string, string | boolean> = {};
|
|
@@ -305,7 +437,7 @@ async function main(): Promise<void> {
|
|
|
305
437
|
|
|
306
438
|
const command = args[0];
|
|
307
439
|
if (!command) {
|
|
308
|
-
usageError("Usage: cache-ctrl <command> [args]. Commands: list, inspect, flush, invalidate, touch, prune, check-
|
|
440
|
+
usageError("Usage: cache-ctrl <command> [args]. Commands: list, inspect, flush, invalidate, touch, prune, check-files, search, write-local, write-external, install, update, uninstall, graph, map, watch, version");
|
|
309
441
|
}
|
|
310
442
|
|
|
311
443
|
switch (command) {
|
|
@@ -456,13 +588,8 @@ async function main(): Promise<void> {
|
|
|
456
588
|
break;
|
|
457
589
|
}
|
|
458
590
|
|
|
459
|
-
case "check-
|
|
460
|
-
const
|
|
461
|
-
if (!subject) {
|
|
462
|
-
usageError("Usage: cache-ctrl check-freshness <subject-keyword> [--url <url>]");
|
|
463
|
-
}
|
|
464
|
-
const url = typeof flags.url === "string" ? flags.url : undefined;
|
|
465
|
-
const result = await checkFreshnessCommand({ subject, ...(url !== undefined ? { url } : {}) });
|
|
591
|
+
case "check-files": {
|
|
592
|
+
const result = await checkFilesCommand();
|
|
466
593
|
if (result.ok) {
|
|
467
594
|
printResult(result, pretty);
|
|
468
595
|
} else {
|
|
@@ -472,8 +599,12 @@ async function main(): Promise<void> {
|
|
|
472
599
|
break;
|
|
473
600
|
}
|
|
474
601
|
|
|
475
|
-
case "
|
|
476
|
-
const
|
|
602
|
+
case "search": {
|
|
603
|
+
const keywords = args.slice(1);
|
|
604
|
+
if (keywords.length === 0) {
|
|
605
|
+
usageError("Usage: cache-ctrl search <keyword> [<keyword>...]");
|
|
606
|
+
}
|
|
607
|
+
const result = await searchCommand({ keywords });
|
|
477
608
|
if (result.ok) {
|
|
478
609
|
printResult(result, pretty);
|
|
479
610
|
} else {
|
|
@@ -483,12 +614,24 @@ async function main(): Promise<void> {
|
|
|
483
614
|
break;
|
|
484
615
|
}
|
|
485
616
|
|
|
486
|
-
case "
|
|
487
|
-
const
|
|
488
|
-
if (
|
|
489
|
-
usageError("Usage: cache-ctrl
|
|
617
|
+
case "write-local": {
|
|
618
|
+
const dataStr = typeof flags.data === "string" ? flags.data : undefined;
|
|
619
|
+
if (!dataStr) {
|
|
620
|
+
usageError("Usage: cache-ctrl write-local --data '<json>'");
|
|
490
621
|
}
|
|
491
|
-
|
|
622
|
+
let content: Record<string, unknown>;
|
|
623
|
+
try {
|
|
624
|
+
content = JSON.parse(dataStr) as Record<string, unknown>; // JSON.parse returns any; writeLocalCommand validates the payload shape via Zod before use.
|
|
625
|
+
} catch {
|
|
626
|
+
usageError("--data must be valid JSON");
|
|
627
|
+
}
|
|
628
|
+
if (typeof content !== "object" || content === null || Array.isArray(content)) {
|
|
629
|
+
usageError("--data must be a JSON object");
|
|
630
|
+
}
|
|
631
|
+
const result = await writeLocalCommand({
|
|
632
|
+
agent: "local",
|
|
633
|
+
content,
|
|
634
|
+
});
|
|
492
635
|
if (result.ok) {
|
|
493
636
|
printResult(result, pretty);
|
|
494
637
|
} else {
|
|
@@ -498,28 +641,27 @@ async function main(): Promise<void> {
|
|
|
498
641
|
break;
|
|
499
642
|
}
|
|
500
643
|
|
|
501
|
-
case "write": {
|
|
502
|
-
const
|
|
503
|
-
if (!
|
|
504
|
-
usageError("Usage: cache-ctrl write <
|
|
505
|
-
}
|
|
506
|
-
if (agent !== "external" && agent !== "local") {
|
|
507
|
-
usageError(`Invalid agent: "${agent}". Must be external or local`);
|
|
644
|
+
case "write-external": {
|
|
645
|
+
const subject = args[1];
|
|
646
|
+
if (!subject) {
|
|
647
|
+
usageError("Usage: cache-ctrl write-external <subject> --data '<json>'");
|
|
508
648
|
}
|
|
509
649
|
const dataStr = typeof flags.data === "string" ? flags.data : undefined;
|
|
510
650
|
if (!dataStr) {
|
|
511
|
-
usageError("Usage: cache-ctrl write <
|
|
651
|
+
usageError("Usage: cache-ctrl write-external <subject> --data '<json>'");
|
|
512
652
|
}
|
|
513
653
|
let content: Record<string, unknown>;
|
|
514
654
|
try {
|
|
515
|
-
content = JSON.parse(dataStr) as Record<string, unknown>;
|
|
655
|
+
content = JSON.parse(dataStr) as Record<string, unknown>; // JSON.parse returns any; writeExternalCommand validates the payload shape via Zod before use.
|
|
516
656
|
} catch {
|
|
517
657
|
usageError("--data must be valid JSON");
|
|
518
658
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
659
|
+
if (typeof content !== "object" || content === null || Array.isArray(content)) {
|
|
660
|
+
usageError("--data must be a JSON object");
|
|
661
|
+
}
|
|
662
|
+
const result = await writeExternalCommand({
|
|
663
|
+
agent: "external",
|
|
664
|
+
subject,
|
|
523
665
|
content,
|
|
524
666
|
});
|
|
525
667
|
if (result.ok) {
|
|
@@ -532,6 +674,9 @@ async function main(): Promise<void> {
|
|
|
532
674
|
}
|
|
533
675
|
|
|
534
676
|
case "install": {
|
|
677
|
+
if (flags["config-dir"] === true) {
|
|
678
|
+
usageError("--config-dir requires a value: --config-dir <path>");
|
|
679
|
+
}
|
|
535
680
|
const configDir = typeof flags["config-dir"] === "string" ? flags["config-dir"] : undefined;
|
|
536
681
|
const result = await installCommand({ ...(configDir !== undefined ? { configDir } : {}) });
|
|
537
682
|
if (result.ok) {
|
|
@@ -543,15 +688,126 @@ async function main(): Promise<void> {
|
|
|
543
688
|
break;
|
|
544
689
|
}
|
|
545
690
|
|
|
691
|
+
case "update": {
|
|
692
|
+
if (flags["config-dir"] === true) {
|
|
693
|
+
usageError("--config-dir requires a value: --config-dir <path>");
|
|
694
|
+
}
|
|
695
|
+
const configDir = typeof flags["config-dir"] === "string" ? flags["config-dir"] : undefined;
|
|
696
|
+
const result = await updateCommand({ ...(configDir !== undefined ? { configDir } : {}) });
|
|
697
|
+
if (result.ok) {
|
|
698
|
+
printResult(result, pretty);
|
|
699
|
+
} else {
|
|
700
|
+
printError(result, pretty);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
case "uninstall": {
|
|
707
|
+
if (flags["config-dir"] === true) {
|
|
708
|
+
usageError("--config-dir requires a value: --config-dir <path>");
|
|
709
|
+
}
|
|
710
|
+
const configDir = typeof flags["config-dir"] === "string" ? flags["config-dir"] : undefined;
|
|
711
|
+
const result = await uninstallCommand({ ...(configDir !== undefined ? { configDir } : {}) });
|
|
712
|
+
if (result.ok) {
|
|
713
|
+
printResult(result, pretty);
|
|
714
|
+
} else {
|
|
715
|
+
printError(result, pretty);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
case "graph": {
|
|
722
|
+
if (flags["max-tokens"] === true) {
|
|
723
|
+
usageError("--max-tokens requires a numeric value");
|
|
724
|
+
}
|
|
725
|
+
const maxTokensRaw = typeof flags["max-tokens"] === "string" ? flags["max-tokens"] : undefined;
|
|
726
|
+
let maxTokensParsed: number | undefined;
|
|
727
|
+
if (maxTokensRaw !== undefined) {
|
|
728
|
+
const parsed = Number(maxTokensRaw);
|
|
729
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
730
|
+
usageError(`Invalid --max-tokens value: "${maxTokensRaw}". Must be a non-negative number`);
|
|
731
|
+
}
|
|
732
|
+
maxTokensParsed = parsed;
|
|
733
|
+
}
|
|
734
|
+
if (flags.seed === true) {
|
|
735
|
+
usageError("--seed requires a value: --seed <path>[,<path>...]");
|
|
736
|
+
}
|
|
737
|
+
const seedFlagValues = collectFlagValues(rawArgs, "seed");
|
|
738
|
+
const seed = seedFlagValues
|
|
739
|
+
.flatMap((value) => value.split(","))
|
|
740
|
+
.map((value) => value.trim())
|
|
741
|
+
.filter((value) => value.length > 0);
|
|
742
|
+
|
|
743
|
+
const result = await graphCommand({
|
|
744
|
+
...(maxTokensParsed !== undefined ? { maxTokens: maxTokensParsed } : {}),
|
|
745
|
+
...(seed.length > 0 ? { seed } : {}),
|
|
746
|
+
});
|
|
747
|
+
if (result.ok) {
|
|
748
|
+
printResult(result, pretty);
|
|
749
|
+
} else {
|
|
750
|
+
printError(result, pretty);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
case "map": {
|
|
757
|
+
if (flags.depth === true) {
|
|
758
|
+
usageError("--depth requires a value: --depth overview|modules|full");
|
|
759
|
+
}
|
|
760
|
+
const depthRaw = typeof flags.depth === "string" ? flags.depth : undefined;
|
|
761
|
+
if (depthRaw !== undefined && depthRaw !== "overview" && depthRaw !== "modules" && depthRaw !== "full") {
|
|
762
|
+
usageError(`Invalid --depth value: "${depthRaw}". Must be overview, modules, or full`);
|
|
763
|
+
}
|
|
764
|
+
if (flags.folder === true) {
|
|
765
|
+
usageError("--folder requires a value: --folder <path-prefix>");
|
|
766
|
+
}
|
|
767
|
+
const folder = typeof flags.folder === "string" ? flags.folder : undefined;
|
|
768
|
+
|
|
769
|
+
const result = await mapCommand({
|
|
770
|
+
...(depthRaw !== undefined ? { depth: depthRaw } : {}),
|
|
771
|
+
...(folder !== undefined ? { folder } : {}),
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (result.ok) {
|
|
775
|
+
printResult(result, pretty);
|
|
776
|
+
} else {
|
|
777
|
+
printError(result, pretty);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
case "watch": {
|
|
784
|
+
const result = await watchCommand({ verbose: flags.verbose === true });
|
|
785
|
+
if (!result.ok) {
|
|
786
|
+
printError(result, pretty);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
case "version": {
|
|
793
|
+
const result = versionCommand({});
|
|
794
|
+
if (result.ok) {
|
|
795
|
+
printResult(result, pretty);
|
|
796
|
+
} else {
|
|
797
|
+
printError(result, pretty);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
|
|
546
803
|
default:
|
|
547
|
-
usageError(`Unknown command: "${command}". Commands: list, inspect, flush, invalidate, touch, prune, check-
|
|
804
|
+
usageError(`Unknown command: "${command}". Commands: list, inspect, flush, invalidate, touch, prune, check-files, search, write-local, write-external, install, update, uninstall, graph, map, watch, version`);
|
|
548
805
|
}
|
|
549
806
|
}
|
|
550
807
|
|
|
551
808
|
if (import.meta.main) {
|
|
552
809
|
main().catch((err: unknown) => {
|
|
553
|
-
|
|
554
|
-
process.stderr.write(JSON.stringify({ ok: false, error: error.message, code: ErrorCode.UNKNOWN }) + "\n");
|
|
810
|
+
process.stderr.write(JSON.stringify(toUnknownResult(err)) + "\n");
|
|
555
811
|
process.exit(1);
|
|
556
812
|
});
|
|
557
813
|
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { CacheEntry } from "../types/cache.js";
|
|
2
2
|
import { getFileStem } from "../utils/fileStem.js";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Scores a cache entry against one or more keywords.
|
|
6
|
+
*
|
|
7
|
+
* @param entry - Candidate cache entry.
|
|
8
|
+
* @param keywords - Search keywords.
|
|
9
|
+
* @returns Numeric relevance score (higher is better).
|
|
10
|
+
* Scoring matrix uses max-per-keyword weights: exact stem (100), stem substring (80),
|
|
11
|
+
* exact word in subject/topic (70), subject/topic substring (50), description substring (30).
|
|
12
|
+
*/
|
|
4
13
|
export function scoreEntry(entry: CacheEntry, keywords: string[]): number {
|
|
5
14
|
const stem = getFileStem(entry.file).toLowerCase();
|
|
6
15
|
const subject = entry.subject.toLowerCase();
|
|
@@ -37,6 +46,13 @@ export function scoreEntry(entry: CacheEntry, keywords: string[]): number {
|
|
|
37
46
|
return total;
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Ranks cache entries by keyword relevance.
|
|
51
|
+
*
|
|
52
|
+
* @param entries - Candidate entries to rank.
|
|
53
|
+
* @param keywords - Search keywords.
|
|
54
|
+
* @returns Score-sorted entries with zero-score candidates removed.
|
|
55
|
+
*/
|
|
40
56
|
export function rankResults(entries: CacheEntry[], keywords: string[]): CacheEntry[] {
|
|
41
57
|
const scored = entries.map((entry) => ({
|
|
42
58
|
entry,
|
|
@@ -52,6 +68,14 @@ export function rankResults(entries: CacheEntry[], keywords: string[]): CacheEnt
|
|
|
52
68
|
return matched.map((s) => ({ ...s.entry, score: s.score }));
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Checks whether a keyword appears as an exact word in text.
|
|
73
|
+
*
|
|
74
|
+
* @param text - Candidate text.
|
|
75
|
+
* @param keyword - Lowercased keyword to match.
|
|
76
|
+
* @returns `true` when an exact token match exists.
|
|
77
|
+
* @remarks Word matching is based on split boundaries (`space`, `_`, `-`, `.`, `/`).
|
|
78
|
+
*/
|
|
55
79
|
export function isExactWordMatch(text: string, keyword: string): boolean {
|
|
56
80
|
// Match whole words — split on non-alphanumeric chars
|
|
57
81
|
const words = text.split(/[\s\-_./]+/);
|
package/src/types/cache.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
/** Supported cache namespaces exposed by the CLI and plugin tools. */
|
|
3
4
|
export type AgentType = "external" | "local";
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Normalized cache entry summary returned by the `list` command prior to formatting.
|
|
8
|
+
*/
|
|
5
9
|
export interface CacheEntry {
|
|
6
10
|
file: string;
|
|
7
11
|
agent: AgentType;
|
|
@@ -17,27 +21,32 @@ const SourceSchema = z.object({
|
|
|
17
21
|
version: z.string().optional(),
|
|
18
22
|
});
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
checked_at: z.string(),
|
|
24
|
-
status: z.enum(["fresh", "stale", "unchecked"]),
|
|
25
|
-
});
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Validates external context cache JSON files stored under `.ai/external-context-gatherer_cache/`.
|
|
26
|
+
*/
|
|
27
27
|
export const ExternalCacheFileSchema = z.looseObject({
|
|
28
28
|
subject: z.string(),
|
|
29
29
|
description: z.string(),
|
|
30
30
|
fetched_at: z.string(),
|
|
31
31
|
sources: z.array(SourceSchema),
|
|
32
|
-
header_metadata: z.record(z.string(), HeaderMetaSchema),
|
|
33
32
|
});
|
|
34
33
|
|
|
34
|
+
/** Validates one tracked file baseline used by local file-change detection. */
|
|
35
35
|
export const TrackedFileSchema = z.object({
|
|
36
36
|
path: z.string(),
|
|
37
37
|
mtime: z.number(),
|
|
38
38
|
hash: z.string().optional(),
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
const FileFactsSchema = z.object({
|
|
42
|
+
summary: z.string().max(300).optional(),
|
|
43
|
+
role: z
|
|
44
|
+
.enum(["entry-point", "interface", "implementation", "test", "config"])
|
|
45
|
+
.optional(),
|
|
46
|
+
importance: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
|
|
47
|
+
facts: z.array(z.string().max(300)).max(10).optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
41
50
|
/**
|
|
42
51
|
* Zod schema for the local context-gatherer cache file (`context.json`).
|
|
43
52
|
*
|
|
@@ -47,8 +56,8 @@ export const TrackedFileSchema = z.object({
|
|
|
47
56
|
* Size constraints enforced at write time:
|
|
48
57
|
* - `global_facts`: max 20 entries; each string ≤ 300 characters.
|
|
49
58
|
* For cross-cutting structural observations only (e.g. repo layout, toolchain).
|
|
50
|
-
* - `facts`:
|
|
51
|
-
*
|
|
59
|
+
* - `facts`: per-file structured metadata with max 10 concise fact strings
|
|
60
|
+
* (each string ≤ 300 characters).
|
|
52
61
|
*/
|
|
53
62
|
export const LocalCacheFileSchema = z.looseObject({
|
|
54
63
|
timestamp: z.string(),
|
|
@@ -68,24 +77,27 @@ export const LocalCacheFileSchema = z.looseObject({
|
|
|
68
77
|
"max 20 global facts — choose only cross-cutting structural observations",
|
|
69
78
|
})
|
|
70
79
|
.optional(),
|
|
71
|
-
facts: z
|
|
72
|
-
|
|
73
|
-
z.string(),
|
|
74
|
-
z
|
|
75
|
-
.array(
|
|
76
|
-
z.string().max(800, {
|
|
77
|
-
message:
|
|
78
|
-
"write concise observations, not file content (max 800 chars per fact)",
|
|
79
|
-
}),
|
|
80
|
-
)
|
|
81
|
-
.max(30, {
|
|
82
|
-
message:
|
|
83
|
-
"max 30 facts per file — choose the most architecturally meaningful observations",
|
|
84
|
-
}),
|
|
85
|
-
)
|
|
86
|
-
.optional(),
|
|
80
|
+
facts: z.record(z.string(), FileFactsSchema).optional(),
|
|
81
|
+
modules: z.record(z.string(), z.array(z.string())).optional(),
|
|
87
82
|
});
|
|
88
83
|
|
|
89
84
|
export type TrackedFile = z.infer<typeof TrackedFileSchema>;
|
|
85
|
+
export type FileFacts = z.infer<typeof FileFactsSchema>;
|
|
90
86
|
export type ExternalCacheFile = z.infer<typeof ExternalCacheFileSchema>;
|
|
91
87
|
export type LocalCacheFile = z.infer<typeof LocalCacheFileSchema>;
|
|
88
|
+
|
|
89
|
+
const GraphNodeSchema = z.object({
|
|
90
|
+
rank: z.number(),
|
|
91
|
+
deps: z.array(z.string()),
|
|
92
|
+
defs: z.array(z.string()),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validates graph cache payloads written by `watch` and consumed by `graph`.
|
|
97
|
+
*/
|
|
98
|
+
export const GraphCacheFileSchema = z.object({
|
|
99
|
+
files: z.record(z.string(), GraphNodeSchema),
|
|
100
|
+
computed_at: z.string(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export type GraphCacheFile = z.infer<typeof GraphCacheFileSchema>;
|