@thecat69/cache-ctrl 1.0.0 → 1.1.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/README.md +202 -28
- package/cache_ctrl.ts +125 -13
- package/package.json +2 -1
- package/skills/cache-ctrl-caller/SKILL.md +45 -31
- package/skills/cache-ctrl-external/SKILL.md +20 -45
- package/skills/cache-ctrl-local/SKILL.md +95 -86
- package/src/analysis/graphBuilder.ts +85 -0
- package/src/analysis/pageRank.ts +167 -0
- package/src/analysis/symbolExtractor.ts +240 -0
- package/src/cache/cacheManager.ts +52 -2
- package/src/cache/externalCache.ts +41 -64
- package/src/cache/graphCache.ts +12 -0
- package/src/cache/localCache.ts +2 -0
- package/src/commands/checkFiles.ts +7 -4
- package/src/commands/checkFreshness.ts +19 -19
- 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 +19 -2
- 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 +9 -2
- package/src/commands/version.ts +14 -0
- package/src/commands/watch.ts +253 -0
- package/src/commands/writeExternal.ts +51 -0
- package/src/commands/writeLocal.ts +123 -0
- package/src/files/changeDetector.ts +15 -0
- package/src/files/gitFiles.ts +15 -0
- package/src/files/openCodeInstaller.ts +21 -2
- package/src/http/freshnessChecker.ts +23 -1
- package/src/index.ts +253 -28
- package/src/search/keywordSearch.ts +24 -0
- package/src/types/cache.ts +42 -18
- package/src/types/commands.ts +99 -1
- package/src/types/result.ts +27 -7
- package/src/utils/errors.ts +14 -0
- package/src/utils/traversal.ts +42 -0
- package/src/commands/write.ts +0 -170
package/README.md
CHANGED
|
@@ -46,26 +46,33 @@ src/index.ts cache_ctrl.ts
|
|
|
46
46
|
└──────────┬──────────────┘
|
|
47
47
|
│
|
|
48
48
|
Command Layer
|
|
49
|
-
src/commands/{list, inspect,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
src/commands/{list, inspect, inspectExternal,
|
|
50
|
+
inspectLocal, flush, invalidate,
|
|
51
|
+
touch, prune, checkFreshness,
|
|
52
|
+
checkFiles, search, writeLocal,
|
|
53
|
+
writeExternal, install, graph,
|
|
54
|
+
map, watch, version}.ts
|
|
55
|
+
│
|
|
56
|
+
Core Services
|
|
55
57
|
cacheManager ← read/write + advisory lock
|
|
56
58
|
externalCache ← external staleness logic
|
|
57
59
|
localCache ← local scan path logic
|
|
60
|
+
graphCache ← graph.json read/write path
|
|
58
61
|
freshnessChecker ← HTTP HEAD requests
|
|
59
62
|
changeDetector ← mtime/hash comparison
|
|
60
63
|
keywordSearch ← scoring engine
|
|
61
|
-
|
|
64
|
+
analysis/symbolExtractor ← import/export AST pass
|
|
65
|
+
analysis/graphBuilder ← dependency graph construction
|
|
66
|
+
analysis/pageRank ← Personalized PageRank ranking
|
|
67
|
+
│
|
|
62
68
|
Cache Directories (on disk)
|
|
63
69
|
.ai/external-context-gatherer_cache/
|
|
64
70
|
├── <subject>.json
|
|
65
71
|
└── <subject>.json.lock (advisory)
|
|
66
72
|
.ai/local-context-gatherer_cache/
|
|
67
73
|
├── context.json
|
|
68
|
-
|
|
74
|
+
├── context.json.lock (advisory)
|
|
75
|
+
└── graph.json (dependency graph; written by watch daemon)
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
**Key design decisions:**
|
|
@@ -73,6 +80,7 @@ src/index.ts cache_ctrl.ts
|
|
|
73
80
|
- The CLI and plugin share the same command functions — no duplicated business logic.
|
|
74
81
|
- All operations return `Result<T, CacheError>` — nothing throws into the caller.
|
|
75
82
|
- `writeCache` defaults to merging updates onto the existing object (preserving unknown agent fields). Local writes use per-path merge — submitted `tracked_files` entries replace existing entries for those paths; entries for other paths are preserved; entries for files no longer present on disk are evicted automatically.
|
|
83
|
+
- `write.ts` and `inspect.ts` are thin routers; all business logic lives in `writeLocal.ts`, `writeExternal.ts`, `inspectLocal.ts`, `inspectExternal.ts`.
|
|
76
84
|
|
|
77
85
|
---
|
|
78
86
|
|
|
@@ -277,7 +285,7 @@ cache-ctrl prune [--agent external|local|all] [--max-age <duration>] [--delete]
|
|
|
277
285
|
|
|
278
286
|
Finds entries older than `--max-age` and invalidates them (default) or deletes them (`--delete`).
|
|
279
287
|
|
|
280
|
-
**Duration format**: `<number><unit>` — `h` for hours, `d` for days. Examples: `
|
|
288
|
+
**Duration format**: `<number><unit>` — `s` for seconds, `m` for minutes, `h` for hours, `d` for days. Examples: `30s`, `15m`, `24h`, `7d`.
|
|
281
289
|
|
|
282
290
|
**Defaults**: `--agent all`, `--max-age 24h` for external. Local cache **always** matches (no TTL).
|
|
283
291
|
|
|
@@ -306,6 +314,8 @@ Sends HTTP HEAD requests to each URL in the matched external entry's `sources[]`
|
|
|
306
314
|
|
|
307
315
|
With `--url`: checks only that specific URL (must exist in `sources[]`).
|
|
308
316
|
|
|
317
|
+
Security: before any request, the URL is validated. Non-HTTP(S) schemes and local-network targets (RFC1918/private, loopback, link-local, and IPv4-mapped IPv6 addresses) are blocked and reported with `blocked` status.
|
|
318
|
+
|
|
309
319
|
```jsonc
|
|
310
320
|
// cache-ctrl check-freshness opencode-skills --pretty
|
|
311
321
|
{
|
|
@@ -346,11 +356,15 @@ If `tracked_files` is absent or empty → returns `{ status: "unchanged", ... }`
|
|
|
346
356
|
"status": "unchanged",
|
|
347
357
|
"changed_files": [],
|
|
348
358
|
"unchanged_files": ["lua/plugins/ui/bufferline.lua"],
|
|
349
|
-
"missing_files": []
|
|
359
|
+
"missing_files": [],
|
|
360
|
+
"new_files": [],
|
|
361
|
+
"deleted_git_files": []
|
|
350
362
|
}
|
|
351
363
|
}
|
|
352
364
|
```
|
|
353
365
|
|
|
366
|
+
`new_files` lists non-ignored files absent from cache (includes git-tracked and untracked non-ignored files). `deleted_git_files` lists git-tracked files removed from the working tree.
|
|
367
|
+
|
|
354
368
|
---
|
|
355
369
|
|
|
356
370
|
### `search`
|
|
@@ -378,11 +392,11 @@ cache-ctrl search neovim --pretty
|
|
|
378
392
|
|
|
379
393
|
---
|
|
380
394
|
|
|
381
|
-
### `write`
|
|
395
|
+
### `write-local` / `write-external`
|
|
382
396
|
|
|
383
397
|
```
|
|
384
|
-
cache-ctrl write
|
|
385
|
-
cache-ctrl write
|
|
398
|
+
cache-ctrl write-external <subject> --data '<json>' [--pretty]
|
|
399
|
+
cache-ctrl write-local --data '<json>' [--pretty]
|
|
386
400
|
```
|
|
387
401
|
|
|
388
402
|
Writes a validated cache entry to disk. The `--data` argument must be a valid JSON string matching the ExternalCacheFile or LocalCacheFile schema. Schema validation runs first — all required fields must be present in `--data` or the write is rejected with `VALIDATION_ERROR`.
|
|
@@ -395,18 +409,148 @@ Writes a validated cache entry to disk. The `--data` argument must be a valid JS
|
|
|
395
409
|
|
|
396
410
|
> The `subject` parameter (external agent) must match `/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/` and be at most 128 characters. Returns `INVALID_ARGS` if it fails validation.
|
|
397
411
|
|
|
398
|
-
**Always use
|
|
412
|
+
**Always use these commands (or `cache_ctrl_write_local` / `cache_ctrl_write_external`) instead of writing cache files directly.** Direct writes skip schema validation and risk corrupting the cache.
|
|
399
413
|
|
|
400
414
|
```json
|
|
401
|
-
// cache-ctrl write
|
|
415
|
+
// cache-ctrl write-external mysubject --data '{"subject":"mysubject","description":"...","fetched_at":"2026-04-05T10:00:00Z","sources":[],"header_metadata":{}}' --pretty
|
|
402
416
|
{ "ok": true, "value": { "file": "/path/to/.ai/external-context-gatherer_cache/mysubject.json" } }
|
|
403
417
|
```
|
|
404
418
|
|
|
405
419
|
---
|
|
406
420
|
|
|
421
|
+
### `graph`
|
|
422
|
+
|
|
423
|
+
```
|
|
424
|
+
cache-ctrl graph [--max-tokens <number>] [--seed <path>[,<path>...]] [--pretty]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Returns a PageRank-ranked dependency graph within a token budget. Reads from `graph.json` computed by the `watch` daemon. Files are ranked by their centrality in the import graph; use `--seed` to personalize the ranking toward specific files (e.g. recently changed files).
|
|
428
|
+
|
|
429
|
+
**Options:**
|
|
430
|
+
|
|
431
|
+
| Flag | Description |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `--max-tokens <number>` | Token budget for `ranked_files` output (default: 1024, clamped 64–128000) |
|
|
434
|
+
| `--seed <path>[,<path>...]` | Personalize PageRank toward these file paths (repeat `--seed` for multiple values) |
|
|
435
|
+
|
|
436
|
+
Returns `FILE_NOT_FOUND` if `graph.json` does not exist — run `cache-ctrl watch` to generate it.
|
|
437
|
+
|
|
438
|
+
```jsonc
|
|
439
|
+
// cache-ctrl graph --max-tokens 512 --pretty
|
|
440
|
+
{
|
|
441
|
+
"ok": true,
|
|
442
|
+
"value": {
|
|
443
|
+
"ranked_files": [
|
|
444
|
+
{
|
|
445
|
+
"path": "src/cache/cacheManager.ts",
|
|
446
|
+
"rank": 0.142,
|
|
447
|
+
"deps": ["src/utils/validate.ts"],
|
|
448
|
+
"defs": ["readCache", "writeCache", "findRepoRoot"],
|
|
449
|
+
"ref_count": 12
|
|
450
|
+
}
|
|
451
|
+
],
|
|
452
|
+
"total_files": 36,
|
|
453
|
+
"computed_at": "2026-04-11T10:00:00Z",
|
|
454
|
+
"token_estimate": 487
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### `map`
|
|
462
|
+
|
|
463
|
+
```
|
|
464
|
+
cache-ctrl map [--depth overview|modules|full] [--folder <path-prefix>] [--pretty]
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Returns a semantic map of the local `context.json` using the structured `FileFacts` metadata. Files are sorted by `importance` (ascending) then path. Use `--folder` to scope the output to a subtree.
|
|
468
|
+
|
|
469
|
+
**Options:**
|
|
470
|
+
|
|
471
|
+
| Flag | Description |
|
|
472
|
+
|---|---|
|
|
473
|
+
| `--depth overview\|modules\|full` | Output depth (default: `overview`) |
|
|
474
|
+
| `--folder <path-prefix>` | Restrict output to files whose path equals or starts with this prefix |
|
|
475
|
+
|
|
476
|
+
**Depth values:**
|
|
477
|
+
- `overview` — includes `summary`, `role`, `importance` per file (no individual facts)
|
|
478
|
+
- `modules` — same as `overview` plus the `modules` grouping from `context.json`
|
|
479
|
+
- `full` — includes all per-file `facts[]` strings
|
|
480
|
+
|
|
481
|
+
Returns `FILE_NOT_FOUND` if `context.json` does not exist.
|
|
482
|
+
|
|
483
|
+
```jsonc
|
|
484
|
+
// cache-ctrl map --depth overview --folder src/commands --pretty
|
|
485
|
+
{
|
|
486
|
+
"ok": true,
|
|
487
|
+
"value": {
|
|
488
|
+
"depth": "overview",
|
|
489
|
+
"global_facts": ["TypeScript CLI, Bun runtime"],
|
|
490
|
+
"files": [
|
|
491
|
+
{
|
|
492
|
+
"path": "src/commands/graph.ts",
|
|
493
|
+
"summary": "Reads graph.json and returns PageRank-ranked file list",
|
|
494
|
+
"role": "implementation",
|
|
495
|
+
"importance": 2
|
|
496
|
+
}
|
|
497
|
+
],
|
|
498
|
+
"total_files": 1,
|
|
499
|
+
"folder_filter": "src/commands"
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
### `watch`
|
|
507
|
+
|
|
508
|
+
```
|
|
509
|
+
cache-ctrl watch [--verbose]
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
Long-running daemon that watches the repo for source file changes (`.ts`, `.tsx`, `.js`, `.jsx`) and incrementally rebuilds `graph.json`. On startup it performs an initial full graph build. Subsequent file changes trigger a debounced rebuild (200 ms). Rebuilds are serialized — concurrent changes are queued.
|
|
513
|
+
|
|
514
|
+
Writes to `.ai/local-context-gatherer_cache/graph.json`. The graph is then available to `cache-ctrl graph` and `cache_ctrl_graph`.
|
|
515
|
+
|
|
516
|
+
**Options:**
|
|
517
|
+
|
|
518
|
+
| Flag | Description |
|
|
519
|
+
|---|---|
|
|
520
|
+
| `--verbose` | Log watcher lifecycle events and rebuild completion to stdout |
|
|
521
|
+
|
|
522
|
+
The process runs until `SIGINT` or `SIGTERM`, which trigger a clean shutdown. Exit code `1` on startup failure (e.g., `Bun.watch` unavailable or graph write error).
|
|
523
|
+
|
|
524
|
+
```sh
|
|
525
|
+
# Start the daemon in the background
|
|
526
|
+
cache-ctrl watch &
|
|
527
|
+
|
|
528
|
+
# Or run it in a dedicated terminal with verbose output
|
|
529
|
+
cache-ctrl watch --verbose
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
### `version`
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
cache-ctrl version
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Prints the current package version as JSON and exits.
|
|
541
|
+
|
|
542
|
+
No flags or arguments.
|
|
543
|
+
|
|
544
|
+
```jsonc
|
|
545
|
+
// cache-ctrl version
|
|
546
|
+
{ "ok": true, "value": { "version": "1.1.1" } }
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
407
551
|
## opencode Plugin Tools
|
|
408
552
|
|
|
409
|
-
The plugin (`cache_ctrl.ts`) is auto-discovered via `~/.config/opencode/tools/cache_ctrl.ts` and registers
|
|
553
|
+
The plugin (`cache_ctrl.ts`) is auto-discovered via `~/.config/opencode/tools/cache_ctrl.ts` and registers 10 tools that call the same command functions as the CLI:
|
|
410
554
|
|
|
411
555
|
| Tool | Description |
|
|
412
556
|
|---|---|
|
|
@@ -416,11 +560,14 @@ The plugin (`cache_ctrl.ts`) is auto-discovered via `~/.config/opencode/tools/ca
|
|
|
416
560
|
| `cache_ctrl_invalidate` | Zero out a cache entry's timestamp |
|
|
417
561
|
| `cache_ctrl_check_freshness` | HTTP HEAD check for external source URLs |
|
|
418
562
|
| `cache_ctrl_check_files` | Compare tracked files against stored mtime/hash |
|
|
419
|
-
| `
|
|
563
|
+
| `cache_ctrl_write_local` | Write a validated local cache entry |
|
|
564
|
+
| `cache_ctrl_write_external` | Write a validated external cache entry |
|
|
565
|
+
| `cache_ctrl_graph` | Return a PageRank-ranked dependency graph within a token budget (reads `graph.json`) |
|
|
566
|
+
| `cache_ctrl_map` | Return a semantic map of `context.json` with per-file FileFacts metadata |
|
|
420
567
|
|
|
421
568
|
No bash permission is required for agents that use the plugin tools directly.
|
|
422
569
|
|
|
423
|
-
All
|
|
570
|
+
All 10 plugin tool responses include a `server_time` field at the outer JSON level:
|
|
424
571
|
|
|
425
572
|
```json
|
|
426
573
|
{ "ok": true, "value": { ... }, "server_time": "2026-04-05T12:34:56.789Z" }
|
|
@@ -495,25 +642,53 @@ cache-ctrl invalidate local
|
|
|
495
642
|
```jsonc
|
|
496
643
|
{
|
|
497
644
|
"timestamp": "2026-04-04T12:00:00Z", // auto-set on write; "" when invalidated
|
|
498
|
-
"topic": "
|
|
499
|
-
"description": "Scan of
|
|
645
|
+
"topic": "cache-ctrl source",
|
|
646
|
+
"description": "Scan of cache-ctrl TypeScript source",
|
|
500
647
|
"cache_miss_reason": "files changed", // optional: why the previous cache was discarded
|
|
501
648
|
"tracked_files": [
|
|
502
|
-
{ "path": "
|
|
649
|
+
{ "path": "src/commands/graph.ts", "mtime": 1743768000000, "hash": "sha256hex..." }
|
|
503
650
|
// mtime is auto-populated by the write command; agents only need to supply path (and optionally hash)
|
|
504
651
|
],
|
|
505
652
|
"global_facts": [ // optional: repo-level facts; last-write-wins; max 20 entries, each ≤ 300 chars
|
|
506
|
-
"
|
|
507
|
-
"
|
|
653
|
+
"TypeScript CLI tool executed by Bun",
|
|
654
|
+
"All errors use Result<T,E> — no thrown exceptions across command boundaries"
|
|
508
655
|
],
|
|
509
|
-
"facts": { // optional: per-file
|
|
510
|
-
"
|
|
511
|
-
|
|
656
|
+
"facts": { // optional: per-file structured FileFacts; per-path merge
|
|
657
|
+
"src/commands/graph.ts": {
|
|
658
|
+
"summary": "Reads graph.json and returns PageRank-ranked file list within a token budget",
|
|
659
|
+
"role": "implementation", // one of: entry-point | interface | implementation | test | config
|
|
660
|
+
"importance": 2, // 1 = critical, 2 = important, 3 = peripheral
|
|
661
|
+
"facts": [ // max 10 entries, each ≤ 300 chars
|
|
662
|
+
"Uses computePageRank with optional seed files for personalized ranking",
|
|
663
|
+
"Token budget clamped to 64–128000; defaults to 1024"
|
|
664
|
+
]
|
|
665
|
+
}
|
|
666
|
+
// FileFacts entries for files deleted from disk are evicted automatically on the next write
|
|
667
|
+
},
|
|
668
|
+
"modules": { // optional: logical groupings of file paths
|
|
669
|
+
"commands": ["src/commands/graph.ts", "src/commands/map.ts"]
|
|
512
670
|
}
|
|
513
671
|
// Any additional agent fields are preserved unchanged
|
|
514
672
|
}
|
|
515
673
|
```
|
|
516
674
|
|
|
675
|
+
### Graph: `.ai/local-context-gatherer_cache/graph.json`
|
|
676
|
+
|
|
677
|
+
Written and maintained by the `watch` daemon. Read by `cache-ctrl graph` and `cache_ctrl_graph`. Agents do not write this file directly.
|
|
678
|
+
|
|
679
|
+
```jsonc
|
|
680
|
+
{
|
|
681
|
+
"computed_at": "2026-04-11T10:00:00Z",
|
|
682
|
+
"files": {
|
|
683
|
+
"src/cache/cacheManager.ts": {
|
|
684
|
+
"rank": 0.0, // stored as 0.0; PageRank is recomputed on every graph command call
|
|
685
|
+
"deps": ["src/utils/validate.ts", "src/types/result.ts"],
|
|
686
|
+
"defs": ["readCache", "writeCache", "findRepoRoot"]
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
517
692
|
---
|
|
518
693
|
|
|
519
694
|
## Error Codes
|
|
@@ -531,9 +706,8 @@ cache-ctrl invalidate local
|
|
|
531
706
|
| `VALIDATION_ERROR` | Schema validation failed (e.g., missing required field or type mismatch in `write`) |
|
|
532
707
|
| `NO_MATCH` | No cache file matched the keyword |
|
|
533
708
|
| `AMBIGUOUS_MATCH` | Multiple files with identical top score |
|
|
534
|
-
| `HTTP_REQUEST_FAILED` | Network error during HEAD request |
|
|
535
709
|
| `URL_NOT_FOUND` | `--url` value not found in `sources[]` |
|
|
536
|
-
| `UNKNOWN` | Unexpected internal error |
|
|
710
|
+
| `UNKNOWN` | Unexpected internal/runtime error (including unexpected HTTP client failures) |
|
|
537
711
|
|
|
538
712
|
---
|
|
539
713
|
|
package/cache_ctrl.ts
CHANGED
|
@@ -5,8 +5,12 @@ import { invalidateCommand } from "./src/commands/invalidate.js";
|
|
|
5
5
|
import { checkFreshnessCommand } from "./src/commands/checkFreshness.js";
|
|
6
6
|
import { checkFilesCommand } from "./src/commands/checkFiles.js";
|
|
7
7
|
import { searchCommand } from "./src/commands/search.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { writeLocalCommand } from "./src/commands/writeLocal.js";
|
|
9
|
+
import { writeExternalCommand } from "./src/commands/writeExternal.js";
|
|
10
|
+
import { graphCommand } from "./src/commands/graph.js";
|
|
11
|
+
import { mapCommand } from "./src/commands/map.js";
|
|
12
|
+
import { toUnknownResult } from "./src/utils/errors.js";
|
|
13
|
+
import { rejectTraversalKeys } from "./src/utils/traversal.js";
|
|
10
14
|
|
|
11
15
|
const z = tool.schema;
|
|
12
16
|
|
|
@@ -18,8 +22,7 @@ function withServerTime(result: unknown): string {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function handleUnknownError(err: unknown): string {
|
|
21
|
-
|
|
22
|
-
return withServerTime({ ok: false, error: message, code: ErrorCode.UNKNOWN });
|
|
25
|
+
return withServerTime(toUnknownResult(err));
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
export const search = tool({
|
|
@@ -130,20 +133,129 @@ export const check_files = tool({
|
|
|
130
133
|
},
|
|
131
134
|
});
|
|
132
135
|
|
|
133
|
-
export const
|
|
136
|
+
export const write_local = tool({
|
|
134
137
|
description:
|
|
135
|
-
"Write a validated cache entry
|
|
138
|
+
"Write a validated local cache entry. timestamp is auto-set to current UTC time — do not include it in content. tracked_files entries need only { path }; mtime/hash are computed by the command. Uses per-path merge and evicts entries for files deleted from disk.",
|
|
136
139
|
args: {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
topic: z.string(),
|
|
141
|
+
description: z.string(),
|
|
142
|
+
tracked_files: z.array(z.object({ path: z.string() })),
|
|
143
|
+
global_facts: z.array(z.string().max(300)).max(20).optional(),
|
|
144
|
+
facts: z
|
|
145
|
+
.record(
|
|
146
|
+
z.string(),
|
|
147
|
+
z.object({
|
|
148
|
+
summary: z.string().optional(),
|
|
149
|
+
role: z.string().optional(),
|
|
150
|
+
importance: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
|
|
151
|
+
facts: z.array(z.string()).optional(),
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
.superRefine(rejectTraversalKeys)
|
|
155
|
+
.optional(),
|
|
156
|
+
cache_miss_reason: z.string().optional(),
|
|
140
157
|
},
|
|
141
158
|
async execute(args) {
|
|
142
159
|
try {
|
|
143
|
-
const result = await
|
|
144
|
-
agent:
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
const result = await writeLocalCommand({
|
|
161
|
+
agent: "local",
|
|
162
|
+
content: {
|
|
163
|
+
topic: args.topic,
|
|
164
|
+
description: args.description,
|
|
165
|
+
tracked_files: args.tracked_files,
|
|
166
|
+
...(args.global_facts !== undefined ? { global_facts: args.global_facts } : {}),
|
|
167
|
+
...(args.facts !== undefined ? { facts: args.facts } : {}),
|
|
168
|
+
...(args.cache_miss_reason !== undefined ? { cache_miss_reason: args.cache_miss_reason } : {}),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return withServerTime(result);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
return handleUnknownError(err);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const write_external = tool({
|
|
179
|
+
description:
|
|
180
|
+
"Write a validated external cache entry to disk. Uses atomic write-with-merge so unknown fields are preserved.",
|
|
181
|
+
args: {
|
|
182
|
+
subject: z.string(),
|
|
183
|
+
description: z.string(),
|
|
184
|
+
fetched_at: z.string().datetime(),
|
|
185
|
+
sources: z.array(
|
|
186
|
+
z.object({
|
|
187
|
+
type: z.string(),
|
|
188
|
+
url: z.string(),
|
|
189
|
+
version: z.string().optional(),
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
header_metadata: z.record(
|
|
193
|
+
z.string(),
|
|
194
|
+
z.object({
|
|
195
|
+
etag: z.string().optional(),
|
|
196
|
+
last_modified: z.string().optional(),
|
|
197
|
+
checked_at: z.string(),
|
|
198
|
+
status: z.enum(["fresh", "stale", "unchecked"]),
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
},
|
|
202
|
+
async execute(args) {
|
|
203
|
+
try {
|
|
204
|
+
const result = await writeExternalCommand({
|
|
205
|
+
agent: "external",
|
|
206
|
+
subject: args.subject,
|
|
207
|
+
content: {
|
|
208
|
+
description: args.description,
|
|
209
|
+
fetched_at: args.fetched_at,
|
|
210
|
+
sources: args.sources,
|
|
211
|
+
header_metadata: args.header_metadata,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
return withServerTime(result);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return handleUnknownError(err);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const graph = tool({
|
|
222
|
+
description:
|
|
223
|
+
"Return a PageRank-ranked file dependency graph within a token budget. Use this to understand which files are most central to recent changes. Reads from the pre-computed graph.json updated by 'cache-ctrl watch'.",
|
|
224
|
+
args: {
|
|
225
|
+
maxTokens: z.number().optional().describe("Token budget for the response (default: 1024)"),
|
|
226
|
+
seed: z
|
|
227
|
+
.array(z.string())
|
|
228
|
+
.optional()
|
|
229
|
+
.describe("File paths to personalize PageRank toward (e.g. recently changed files)"),
|
|
230
|
+
},
|
|
231
|
+
async execute(args) {
|
|
232
|
+
try {
|
|
233
|
+
const result = await graphCommand({
|
|
234
|
+
...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
|
|
235
|
+
...(args.seed !== undefined ? { seed: args.seed } : {}),
|
|
236
|
+
});
|
|
237
|
+
return withServerTime(result);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return handleUnknownError(err);
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
export const map = tool({
|
|
245
|
+
description:
|
|
246
|
+
"Return a semantic mental map of the codebase from the local context cache. Use 'overview' (default) for a ~300-token summary of what each file does. Use 'modules' to see logical groupings. Use 'full' to include all per-file facts.",
|
|
247
|
+
args: {
|
|
248
|
+
depth: z
|
|
249
|
+
.enum(["overview", "modules", "full"])
|
|
250
|
+
.optional()
|
|
251
|
+
.describe("Map depth (default: 'overview')"),
|
|
252
|
+
folder: z.string().optional().describe("Restrict map to files under this path prefix"),
|
|
253
|
+
},
|
|
254
|
+
async execute(args) {
|
|
255
|
+
try {
|
|
256
|
+
const result = await mapCommand({
|
|
257
|
+
...(args.depth !== undefined ? { depth: args.depth } : {}),
|
|
258
|
+
...(args.folder !== undefined ? { folder: args.folder } : {}),
|
|
147
259
|
});
|
|
148
260
|
return withServerTime(result);
|
|
149
261
|
} catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thecat69/cache-ctrl",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cache-ctrl": "src/index.ts"
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@opencode-ai/plugin": "latest",
|
|
29
|
+
"@typescript-eslint/typescript-estree": "8.58.1",
|
|
29
30
|
"zod": "4.3.6"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
@@ -10,8 +10,6 @@ For any agent that calls **local-context-gatherer** and **external-context-gathe
|
|
|
10
10
|
The cache avoids expensive subagent calls when their data is already fresh.
|
|
11
11
|
Use `cache_ctrl_*` tools directly for all status checks — **never spawn a subagent just to check cache state**.
|
|
12
12
|
|
|
13
|
-
---
|
|
14
|
-
|
|
15
13
|
## Availability Detection (run once at startup)
|
|
16
14
|
|
|
17
15
|
1. Call `cache_ctrl_list` (built-in tool).
|
|
@@ -19,9 +17,7 @@ Use `cache_ctrl_*` tools directly for all status checks — **never spawn a suba
|
|
|
19
17
|
- Failure (tool not found / permission denied) → try step 2.
|
|
20
18
|
2. Run `bash: "which cache-ctrl"`.
|
|
21
19
|
- Exit 0 → **use Tier 2** for all operations.
|
|
22
|
-
- Not found → **
|
|
23
|
-
|
|
24
|
-
---
|
|
20
|
+
- Not found → **neither Tier is available**. Stop and request environment access to `cache_ctrl_*` tools or the `cache-ctrl` CLI before proceeding.
|
|
25
21
|
|
|
26
22
|
## Before Calling local-context-gatherer
|
|
27
23
|
|
|
@@ -59,10 +55,6 @@ After `local-context-gatherer` returns, verify it actually wrote to cache:
|
|
|
59
55
|
4. Re-invoke the gatherer **once** with the explicit instruction appended: *"IMPORTANT: You MUST call `cache_ctrl_write` before returning. Your previous invocation did not update the cache (timestamp was not advanced)."*
|
|
60
56
|
5. Do not retry more than once.
|
|
61
57
|
|
|
62
|
-
> **Why `timestamp`, not `check-files`?** A `check-files` result of `"changed"` after a successful write is expected — it does not indicate a missing write. Only the `timestamp` advancing is a reliable signal that the write occurred.
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
58
|
## Before Calling external-context-gatherer
|
|
67
59
|
|
|
68
60
|
Check whether external docs for a given subject are already cached and fresh.
|
|
@@ -71,7 +63,6 @@ Check whether external docs for a given subject are already cached and fresh.
|
|
|
71
63
|
|
|
72
64
|
**Tier 1:** Call `cache_ctrl_list` with `agent: "external"`.
|
|
73
65
|
**Tier 2:** `cache-ctrl list --agent external`
|
|
74
|
-
**Tier 3:** `glob` `.ai/external-context-gatherer_cache/*.json` → for each file, `read` and check `fetched_at` (stale if empty or older than 24 hours).
|
|
75
66
|
|
|
76
67
|
### Step 2 — Search for a matching subject
|
|
77
68
|
|
|
@@ -79,7 +70,6 @@ If entries exist, check whether one already covers the topic:
|
|
|
79
70
|
|
|
80
71
|
**Tier 1:** Call `cache_ctrl_search` with relevant keywords.
|
|
81
72
|
**Tier 2:** `cache-ctrl search <keyword> [<keyword>...]`
|
|
82
|
-
**Tier 3:** Scan `subject` and `description` fields in the listed files.
|
|
83
73
|
|
|
84
74
|
### Step 3 — Decide
|
|
85
75
|
|
|
@@ -97,7 +87,38 @@ To **force a re-fetch** for a specific subject:
|
|
|
97
87
|
**Tier 1:** Call `cache_ctrl_invalidate` with `agent: "external"` and the subject keyword.
|
|
98
88
|
**Tier 2:** `cache-ctrl invalidate external <subject>`
|
|
99
89
|
|
|
100
|
-
|
|
90
|
+
## Exploring Local Context: map and graph
|
|
91
|
+
|
|
92
|
+
### `cache_ctrl_map`
|
|
93
|
+
|
|
94
|
+
- **Purpose:** Build a semantic mental map of the codebase (what each file does, plus role/importance metadata).
|
|
95
|
+
- **Params:**
|
|
96
|
+
- `depth` (optional):
|
|
97
|
+
- `overview` (default): ~300-token orientation (summaries + roles)
|
|
98
|
+
- `modules`: adds module/grouping information
|
|
99
|
+
- `full`: includes per-file `facts[]` arrays
|
|
100
|
+
- `folder` (optional): restrict output to a path prefix
|
|
101
|
+
- **When to use:** first call when entering a new task, before deeper inspection.
|
|
102
|
+
|
|
103
|
+
### `cache_ctrl_graph`
|
|
104
|
+
|
|
105
|
+
- **Purpose:** Return a structural dependency graph with PageRank-ranked files by centrality.
|
|
106
|
+
- **Params:**
|
|
107
|
+
- `maxTokens` (optional, default `1024`)
|
|
108
|
+
- `seed` (optional `string[]`): personalize ranking toward specific files (for example changed files)
|
|
109
|
+
- **Requirements:** `cache-ctrl watch` must be running (or must have run recently) to populate `graph.json`.
|
|
110
|
+
- **When to use:** after `cache_ctrl_map`, to identify the most connected/high-leverage files.
|
|
111
|
+
|
|
112
|
+
## Progressive Disclosure protocol (4-step)
|
|
113
|
+
|
|
114
|
+
Use this 4-step sequence to control token usage while preserving accuracy:
|
|
115
|
+
|
|
116
|
+
1. `cache_ctrl_map(depth: "overview")` — orient quickly (~300 tokens)
|
|
117
|
+
2. `cache_ctrl_graph(maxTokens: 1024, seed: [changedFiles])` — structural dependency view
|
|
118
|
+
3. `cache_ctrl_inspect(filter: [...])` — deep facts for specific files
|
|
119
|
+
4. Read only the relevant source files (typically 2–5 files)
|
|
120
|
+
|
|
121
|
+
> See **Reading a Full Cache Entry** below for the three filter targeting options and when to use each.
|
|
101
122
|
|
|
102
123
|
## Reading a Full Cache Entry
|
|
103
124
|
|
|
@@ -105,7 +126,6 @@ Use when you want to pass a cached summary to a subagent or include it inline in
|
|
|
105
126
|
|
|
106
127
|
**Tier 1:** Call `cache_ctrl_inspect` with `agent` and `subject`.
|
|
107
128
|
**Tier 2:** `cache-ctrl inspect external <subject>` or `cache-ctrl inspect local context --filter <kw>[,<kw>...]`
|
|
108
|
-
**Tier 3:** `read` the file directly from `.ai/<agent>_cache/<subject>.json`.
|
|
109
129
|
|
|
110
130
|
> **For `agent: "local"`: always use at least one filter on large codebases.** Three targeting options are available — use the most specific one that fits your task:
|
|
111
131
|
>
|
|
@@ -114,25 +134,21 @@ Use when you want to pass a cached summary to a subagent or include it inline in
|
|
|
114
134
|
> | `filter` | File path contains keyword | When you know which files by name/path segment |
|
|
115
135
|
> | `folder` | File path starts with folder prefix (recursive) | When you need all files in a directory subtree |
|
|
116
136
|
> | `search_facts` | Any fact string contains keyword | When you need files related to a concept, pattern, or API |
|
|
117
|
-
>
|
|
118
|
-
> Unfiltered local inspect returns the **entire facts map**. This is only appropriate for codebases with ≤ ~20 tracked files. On larger codebases, always use at least one of the above.
|
|
119
|
-
|
|
120
|
-
---
|
|
121
137
|
|
|
122
138
|
## Quick Reference
|
|
123
139
|
|
|
124
|
-
| Operation | Tier 1 | Tier 2 |
|
|
125
|
-
|
|
126
|
-
| Check local freshness | `cache_ctrl_check_files` | `cache-ctrl check-files` |
|
|
127
|
-
| List external entries | `cache_ctrl_list` (agent: "external") | `cache-ctrl list --agent external` |
|
|
128
|
-
| Search entries | `cache_ctrl_search` | `cache-ctrl search <kw>...` |
|
|
129
|
-
| Read facts (local) | `cache_ctrl_inspect` + `filter` | `cache-ctrl inspect local context --filter <kw>` |
|
|
130
|
-
| Read entry (external) | `cache_ctrl_inspect` | `cache-ctrl inspect external <subject>` |
|
|
131
|
-
| Invalidate local | `cache_ctrl_invalidate` (agent: "local") | `cache-ctrl invalidate local` |
|
|
132
|
-
| Invalidate external | `cache_ctrl_invalidate` (agent: "external", subject) | `cache-ctrl invalidate external <subject>` |
|
|
133
|
-
| HTTP freshness check | `cache_ctrl_check_freshness` | `cache-ctrl check-freshness <subject>` |
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
| Operation | Tier 1 | Tier 2 |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| Check local freshness | `cache_ctrl_check_files` | `cache-ctrl check-files` |
|
|
143
|
+
| List external entries | `cache_ctrl_list` (agent: "external") | `cache-ctrl list --agent external` |
|
|
144
|
+
| Search entries | `cache_ctrl_search` | `cache-ctrl search <kw>...` |
|
|
145
|
+
| Read facts (local) | `cache_ctrl_inspect` + `filter` | `cache-ctrl inspect local context --filter <kw>` |
|
|
146
|
+
| Read entry (external) | `cache_ctrl_inspect` | `cache-ctrl inspect external <subject>` |
|
|
147
|
+
| Invalidate local | `cache_ctrl_invalidate` (agent: "local") | `cache-ctrl invalidate local` |
|
|
148
|
+
| Invalidate external | `cache_ctrl_invalidate` (agent: "external", subject) | `cache-ctrl invalidate external <subject>` |
|
|
149
|
+
| HTTP freshness check | `cache_ctrl_check_freshness` | `cache-ctrl check-freshness <subject>` |
|
|
150
|
+
| Codebase map | `cache_ctrl_map` | `cache-ctrl map [--depth <overview|modules|full>] [--folder <path>]` |
|
|
151
|
+
| Dependency graph | `cache_ctrl_graph` | `cache-ctrl graph [--max-tokens <n>] [--seed <file1,file2,...>]` |
|
|
136
152
|
|
|
137
153
|
## Anti-Bloat Rules
|
|
138
154
|
|
|
@@ -141,8 +157,6 @@ Use when you want to pass a cached summary to a subagent or include it inline in
|
|
|
141
157
|
- Use `cache_ctrl_inspect` to read only the entries you actually need.
|
|
142
158
|
- Cache entries are the source of truth. Prefer them over re-fetching.
|
|
143
159
|
|
|
144
|
-
---
|
|
145
|
-
|
|
146
160
|
## server_time in Responses
|
|
147
161
|
|
|
148
162
|
Every `cache_ctrl_*` tool call returns a `server_time` field at the outer JSON level:
|