@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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A CLI tool and native opencode plugin that manages the two AI agent caches (`.ai/external-context-gatherer_cache/` and `.ai/local-context-gatherer_cache/`) with a uniform interface.
|
|
4
4
|
|
|
5
|
-
It handles advisory locking for safe concurrent writes, keyword search across all entries,
|
|
5
|
+
It handles advisory locking for safe concurrent writes, keyword search across all entries, and file-change detection for local scans.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -46,26 +46,32 @@ src/index.ts cache_ctrl.ts
|
|
|
46
46
|
└──────────┬──────────────┘
|
|
47
47
|
│
|
|
48
48
|
Command Layer
|
|
49
|
-
src/commands/{list, inspect,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
src/commands/{list, inspect, inspectExternal,
|
|
50
|
+
inspectLocal, flush, invalidate,
|
|
51
|
+
touch, prune,
|
|
52
|
+
checkFiles, search, writeLocal,
|
|
53
|
+
writeExternal, install, update, uninstall,
|
|
54
|
+
graph, map, watch, version}.ts
|
|
55
|
+
│
|
|
56
|
+
Core Services
|
|
57
|
+
cacheManager ← read/write + advisory lock
|
|
58
|
+
externalCache ← external staleness logic
|
|
59
|
+
localCache ← local scan path logic
|
|
60
|
+
graphCache ← graph.json read/write path
|
|
61
|
+
changeDetector ← mtime/hash comparison
|
|
60
62
|
keywordSearch ← scoring engine
|
|
61
|
-
|
|
63
|
+
analysis/symbolExtractor ← import/export AST pass
|
|
64
|
+
analysis/graphBuilder ← dependency graph construction
|
|
65
|
+
analysis/pageRank ← Personalized PageRank ranking
|
|
66
|
+
│
|
|
62
67
|
Cache Directories (on disk)
|
|
63
68
|
.ai/external-context-gatherer_cache/
|
|
64
69
|
├── <subject>.json
|
|
65
70
|
└── <subject>.json.lock (advisory)
|
|
66
71
|
.ai/local-context-gatherer_cache/
|
|
67
72
|
├── context.json
|
|
68
|
-
|
|
73
|
+
├── context.json.lock (advisory)
|
|
74
|
+
└── graph.json (dependency graph; written by watch daemon)
|
|
69
75
|
```
|
|
70
76
|
|
|
71
77
|
**Key design decisions:**
|
|
@@ -73,6 +79,7 @@ src/index.ts cache_ctrl.ts
|
|
|
73
79
|
- The CLI and plugin share the same command functions — no duplicated business logic.
|
|
74
80
|
- All operations return `Result<T, CacheError>` — nothing throws into the caller.
|
|
75
81
|
- `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.
|
|
82
|
+
- `write.ts` and `inspect.ts` are thin routers; all business logic lives in `writeLocal.ts`, `writeExternal.ts`, `inspectLocal.ts`, `inspectExternal.ts`.
|
|
76
83
|
|
|
77
84
|
---
|
|
78
85
|
|
|
@@ -129,6 +136,90 @@ Both operations are idempotent — re-running `cache-ctrl install` after `npm up
|
|
|
129
136
|
|
|
130
137
|
---
|
|
131
138
|
|
|
139
|
+
### `update`
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
cache-ctrl update [--config-dir <path>]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Updates the globally installed npm package to the latest version, then re-runs the OpenCode integration install to refresh the tool wrapper and skill files.
|
|
146
|
+
|
|
147
|
+
1. Runs `npm install -g @thecat69/cache-ctrl@latest`.
|
|
148
|
+
2. Re-runs `cache-ctrl install` (idempotent — regenerates the wrapper with the new package path).
|
|
149
|
+
|
|
150
|
+
If the `npm install` step fails, the error is recorded in `warnings[]` and the integration install still proceeds.
|
|
151
|
+
|
|
152
|
+
**Options:**
|
|
153
|
+
|
|
154
|
+
| Flag | Description |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `--config-dir <path>` | Override the detected OpenCode config directory |
|
|
157
|
+
|
|
158
|
+
```jsonc
|
|
159
|
+
// cache-ctrl update --pretty
|
|
160
|
+
{
|
|
161
|
+
"ok": true,
|
|
162
|
+
"value": {
|
|
163
|
+
"packageUpdated": true,
|
|
164
|
+
"installedPaths": [
|
|
165
|
+
"/home/user/.config/opencode/tools/cache_ctrl.ts",
|
|
166
|
+
"/home/user/.config/opencode/skills/cache-ctrl-caller/SKILL.md",
|
|
167
|
+
"/home/user/.config/opencode/skills/cache-ctrl-local/SKILL.md",
|
|
168
|
+
"/home/user/.config/opencode/skills/cache-ctrl-external/SKILL.md"
|
|
169
|
+
],
|
|
170
|
+
"warnings": []
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Error codes**: `INVALID_ARGS` if `--config-dir` is outside the user home directory. `FILE_WRITE_ERROR` if the integration files cannot be written.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `uninstall`
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
cache-ctrl uninstall [--config-dir <path>]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Removes the cache-ctrl OpenCode integration and uninstalls the global npm package.
|
|
186
|
+
|
|
187
|
+
Removes, in order:
|
|
188
|
+
1. `<configDir>/tools/cache_ctrl.ts`
|
|
189
|
+
2. All `<configDir>/skills/cache-ctrl-*` directories (recursive)
|
|
190
|
+
3. `~/.local/bin/cache-ctrl`
|
|
191
|
+
4. Runs `npm uninstall -g @thecat69/cache-ctrl`
|
|
192
|
+
|
|
193
|
+
Missing files are not treated as errors — they produce a `warnings[]` entry instead. If the `npm uninstall` step fails, the error is recorded in `warnings[]`.
|
|
194
|
+
|
|
195
|
+
**Options:**
|
|
196
|
+
|
|
197
|
+
| Flag | Description |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `--config-dir <path>` | Override the detected OpenCode config directory |
|
|
200
|
+
|
|
201
|
+
```jsonc
|
|
202
|
+
// cache-ctrl uninstall --pretty
|
|
203
|
+
{
|
|
204
|
+
"ok": true,
|
|
205
|
+
"value": {
|
|
206
|
+
"removed": [
|
|
207
|
+
"/home/user/.config/opencode/tools/cache_ctrl.ts",
|
|
208
|
+
"/home/user/.config/opencode/skills/cache-ctrl-caller",
|
|
209
|
+
"/home/user/.config/opencode/skills/cache-ctrl-local",
|
|
210
|
+
"/home/user/.config/opencode/skills/cache-ctrl-external",
|
|
211
|
+
"/home/user/.local/bin/cache-ctrl"
|
|
212
|
+
],
|
|
213
|
+
"packageUninstalled": true,
|
|
214
|
+
"warnings": []
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Error codes**: `INVALID_ARGS` if `--config-dir` is outside the user home directory. `UNKNOWN` for unexpected filesystem errors.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
132
223
|
### `help`
|
|
133
224
|
|
|
134
225
|
```
|
|
@@ -277,7 +368,7 @@ cache-ctrl prune [--agent external|local|all] [--max-age <duration>] [--delete]
|
|
|
277
368
|
|
|
278
369
|
Finds entries older than `--max-age` and invalidates them (default) or deletes them (`--delete`).
|
|
279
370
|
|
|
280
|
-
**Duration format**: `<number><unit>` — `h` for hours, `d` for days. Examples: `
|
|
371
|
+
**Duration format**: `<number><unit>` — `s` for seconds, `m` for minutes, `h` for hours, `d` for days. Examples: `30s`, `15m`, `24h`, `7d`.
|
|
281
372
|
|
|
282
373
|
**Defaults**: `--agent all`, `--max-age 24h` for external. Local cache **always** matches (no TTL).
|
|
283
374
|
|
|
@@ -292,36 +383,6 @@ cache-ctrl prune --agent external --max-age 1d --delete
|
|
|
292
383
|
|
|
293
384
|
---
|
|
294
385
|
|
|
295
|
-
### `check-freshness`
|
|
296
|
-
|
|
297
|
-
```
|
|
298
|
-
cache-ctrl check-freshness <subject-keyword> [--url <url>] [--pretty]
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
Sends HTTP HEAD requests to each URL in the matched external entry's `sources[]`. Uses conditional headers (`If-None-Match`, `If-Modified-Since`) from stored `header_metadata`. Updates `header_metadata` in-place after checking.
|
|
302
|
-
|
|
303
|
-
- HTTP 304 → `fresh`
|
|
304
|
-
- HTTP 200 → `stale` (resource changed)
|
|
305
|
-
- Network / 4xx / 5xx → `error` (does not update metadata for that URL)
|
|
306
|
-
|
|
307
|
-
With `--url`: checks only that specific URL (must exist in `sources[]`).
|
|
308
|
-
|
|
309
|
-
```jsonc
|
|
310
|
-
// cache-ctrl check-freshness opencode-skills --pretty
|
|
311
|
-
{
|
|
312
|
-
"ok": true,
|
|
313
|
-
"value": {
|
|
314
|
-
"subject": "opencode-skills",
|
|
315
|
-
"sources": [
|
|
316
|
-
{ "url": "https://example.com/docs", "status": "fresh", "http_status": 304 }
|
|
317
|
-
],
|
|
318
|
-
"overall": "fresh"
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
386
|
### `check-files`
|
|
326
387
|
|
|
327
388
|
```
|
|
@@ -346,11 +407,15 @@ If `tracked_files` is absent or empty → returns `{ status: "unchanged", ... }`
|
|
|
346
407
|
"status": "unchanged",
|
|
347
408
|
"changed_files": [],
|
|
348
409
|
"unchanged_files": ["lua/plugins/ui/bufferline.lua"],
|
|
349
|
-
"missing_files": []
|
|
410
|
+
"missing_files": [],
|
|
411
|
+
"new_files": [],
|
|
412
|
+
"deleted_git_files": []
|
|
350
413
|
}
|
|
351
414
|
}
|
|
352
415
|
```
|
|
353
416
|
|
|
417
|
+
`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.
|
|
418
|
+
|
|
354
419
|
---
|
|
355
420
|
|
|
356
421
|
### `search`
|
|
@@ -378,11 +443,11 @@ cache-ctrl search neovim --pretty
|
|
|
378
443
|
|
|
379
444
|
---
|
|
380
445
|
|
|
381
|
-
### `write`
|
|
446
|
+
### `write-local` / `write-external`
|
|
382
447
|
|
|
383
448
|
```
|
|
384
|
-
cache-ctrl write
|
|
385
|
-
cache-ctrl write
|
|
449
|
+
cache-ctrl write-external <subject> --data '<json>' [--pretty]
|
|
450
|
+
cache-ctrl write-local --data '<json>' [--pretty]
|
|
386
451
|
```
|
|
387
452
|
|
|
388
453
|
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 +460,148 @@ Writes a validated cache entry to disk. The `--data` argument must be a valid JS
|
|
|
395
460
|
|
|
396
461
|
> 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
462
|
|
|
398
|
-
**Always use
|
|
463
|
+
**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
464
|
|
|
400
465
|
```json
|
|
401
|
-
// cache-ctrl write
|
|
466
|
+
// cache-ctrl write-external mysubject --data '{"subject":"mysubject","description":"...","fetched_at":"2026-04-05T10:00:00Z","sources":[]}' --pretty
|
|
402
467
|
{ "ok": true, "value": { "file": "/path/to/.ai/external-context-gatherer_cache/mysubject.json" } }
|
|
403
468
|
```
|
|
404
469
|
|
|
405
470
|
---
|
|
406
471
|
|
|
472
|
+
### `graph`
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
cache-ctrl graph [--max-tokens <number>] [--seed <path>[,<path>...]] [--pretty]
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
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).
|
|
479
|
+
|
|
480
|
+
**Options:**
|
|
481
|
+
|
|
482
|
+
| Flag | Description |
|
|
483
|
+
|---|---|
|
|
484
|
+
| `--max-tokens <number>` | Token budget for `ranked_files` output (default: 1024, clamped 64–128000) |
|
|
485
|
+
| `--seed <path>[,<path>...]` | Personalize PageRank toward these file paths (repeat `--seed` for multiple values) |
|
|
486
|
+
|
|
487
|
+
Returns `FILE_NOT_FOUND` if `graph.json` does not exist — run `cache-ctrl watch` to generate it.
|
|
488
|
+
|
|
489
|
+
```jsonc
|
|
490
|
+
// cache-ctrl graph --max-tokens 512 --pretty
|
|
491
|
+
{
|
|
492
|
+
"ok": true,
|
|
493
|
+
"value": {
|
|
494
|
+
"ranked_files": [
|
|
495
|
+
{
|
|
496
|
+
"path": "src/cache/cacheManager.ts",
|
|
497
|
+
"rank": 0.142,
|
|
498
|
+
"deps": ["src/utils/validate.ts"],
|
|
499
|
+
"defs": ["readCache", "writeCache", "findRepoRoot"],
|
|
500
|
+
"ref_count": 12
|
|
501
|
+
}
|
|
502
|
+
],
|
|
503
|
+
"total_files": 36,
|
|
504
|
+
"computed_at": "2026-04-11T10:00:00Z",
|
|
505
|
+
"token_estimate": 487
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
### `map`
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
cache-ctrl map [--depth overview|modules|full] [--folder <path-prefix>] [--pretty]
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
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.
|
|
519
|
+
|
|
520
|
+
**Options:**
|
|
521
|
+
|
|
522
|
+
| Flag | Description |
|
|
523
|
+
|---|---|
|
|
524
|
+
| `--depth overview\|modules\|full` | Output depth (default: `overview`) |
|
|
525
|
+
| `--folder <path-prefix>` | Restrict output to files whose path equals or starts with this prefix |
|
|
526
|
+
|
|
527
|
+
**Depth values:**
|
|
528
|
+
- `overview` — includes `summary`, `role`, `importance` per file (no individual facts)
|
|
529
|
+
- `modules` — same as `overview` plus the `modules` grouping from `context.json`
|
|
530
|
+
- `full` — includes all per-file `facts[]` strings
|
|
531
|
+
|
|
532
|
+
Returns `FILE_NOT_FOUND` if `context.json` does not exist.
|
|
533
|
+
|
|
534
|
+
```jsonc
|
|
535
|
+
// cache-ctrl map --depth overview --folder src/commands --pretty
|
|
536
|
+
{
|
|
537
|
+
"ok": true,
|
|
538
|
+
"value": {
|
|
539
|
+
"depth": "overview",
|
|
540
|
+
"global_facts": ["TypeScript CLI, Bun runtime"],
|
|
541
|
+
"files": [
|
|
542
|
+
{
|
|
543
|
+
"path": "src/commands/graph.ts",
|
|
544
|
+
"summary": "Reads graph.json and returns PageRank-ranked file list",
|
|
545
|
+
"role": "implementation",
|
|
546
|
+
"importance": 2
|
|
547
|
+
}
|
|
548
|
+
],
|
|
549
|
+
"total_files": 1,
|
|
550
|
+
"folder_filter": "src/commands"
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
### `watch`
|
|
558
|
+
|
|
559
|
+
```
|
|
560
|
+
cache-ctrl watch [--verbose]
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
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.
|
|
564
|
+
|
|
565
|
+
Writes to `.ai/local-context-gatherer_cache/graph.json`. The graph is then available to `cache-ctrl graph` and `cache_ctrl_graph`.
|
|
566
|
+
|
|
567
|
+
**Options:**
|
|
568
|
+
|
|
569
|
+
| Flag | Description |
|
|
570
|
+
|---|---|
|
|
571
|
+
| `--verbose` | Log watcher lifecycle events and rebuild completion to stdout |
|
|
572
|
+
|
|
573
|
+
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).
|
|
574
|
+
|
|
575
|
+
```sh
|
|
576
|
+
# Start the daemon in the background
|
|
577
|
+
cache-ctrl watch &
|
|
578
|
+
|
|
579
|
+
# Or run it in a dedicated terminal with verbose output
|
|
580
|
+
cache-ctrl watch --verbose
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
### `version`
|
|
586
|
+
|
|
587
|
+
```
|
|
588
|
+
cache-ctrl version
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
Prints the current package version as JSON and exits.
|
|
592
|
+
|
|
593
|
+
No flags or arguments.
|
|
594
|
+
|
|
595
|
+
```jsonc
|
|
596
|
+
// cache-ctrl version
|
|
597
|
+
{ "ok": true, "value": { "version": "1.1.1" } }
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
407
602
|
## opencode Plugin Tools
|
|
408
603
|
|
|
409
|
-
The plugin (`cache_ctrl.ts`) is auto-discovered via `~/.config/opencode/tools/cache_ctrl.ts` and registers
|
|
604
|
+
The plugin (`cache_ctrl.ts`) is auto-discovered via `~/.config/opencode/tools/cache_ctrl.ts` and registers 9 tools that call the same command functions as the CLI:
|
|
410
605
|
|
|
411
606
|
| Tool | Description |
|
|
412
607
|
|---|---|
|
|
@@ -414,13 +609,15 @@ The plugin (`cache_ctrl.ts`) is auto-discovered via `~/.config/opencode/tools/ca
|
|
|
414
609
|
| `cache_ctrl_list` | List entries with age and staleness flags |
|
|
415
610
|
| `cache_ctrl_inspect` | Return full content of a specific entry |
|
|
416
611
|
| `cache_ctrl_invalidate` | Zero out a cache entry's timestamp |
|
|
417
|
-
| `cache_ctrl_check_freshness` | HTTP HEAD check for external source URLs |
|
|
418
612
|
| `cache_ctrl_check_files` | Compare tracked files against stored mtime/hash |
|
|
419
|
-
| `
|
|
613
|
+
| `cache_ctrl_write_local` | Write a validated local cache entry |
|
|
614
|
+
| `cache_ctrl_write_external` | Write a validated external cache entry |
|
|
615
|
+
| `cache_ctrl_graph` | Return a PageRank-ranked dependency graph within a token budget (reads `graph.json`) |
|
|
616
|
+
| `cache_ctrl_map` | Return a semantic map of `context.json` with per-file FileFacts metadata |
|
|
420
617
|
|
|
421
618
|
No bash permission is required for agents that use the plugin tools directly.
|
|
422
619
|
|
|
423
|
-
All
|
|
620
|
+
All 9 plugin tool responses include a `server_time` field at the outer JSON level:
|
|
424
621
|
|
|
425
622
|
```json
|
|
426
623
|
{ "ok": true, "value": { ... }, "server_time": "2026-04-05T12:34:56.789Z" }
|
|
@@ -439,10 +636,6 @@ Use `server_time` to assess how stale stored timestamps are without requiring ba
|
|
|
439
636
|
cache-ctrl list --agent external --pretty
|
|
440
637
|
# If is_stale: false → skip fetch
|
|
441
638
|
|
|
442
|
-
# For a precise HTTP freshness check on a borderline entry
|
|
443
|
-
cache-ctrl check-freshness <subject>
|
|
444
|
-
# If overall: "fresh" → skip re-fetch
|
|
445
|
-
|
|
446
639
|
# After writing new cache content — mark entry fresh
|
|
447
640
|
cache-ctrl touch external <subject>
|
|
448
641
|
|
|
@@ -476,14 +669,6 @@ cache-ctrl invalidate local
|
|
|
476
669
|
"sources": [
|
|
477
670
|
{ "type": "github_api", "url": "https://..." }
|
|
478
671
|
],
|
|
479
|
-
"header_metadata": {
|
|
480
|
-
"https://...": {
|
|
481
|
-
"etag": "\"abc123\"",
|
|
482
|
-
"last_modified": "Fri, 04 Apr 2026 10:00:00 GMT",
|
|
483
|
-
"checked_at": "2026-04-04T12:00:00Z",
|
|
484
|
-
"status": "fresh"
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
672
|
// Any additional agent fields are preserved unchanged
|
|
488
673
|
}
|
|
489
674
|
```
|
|
@@ -495,25 +680,53 @@ cache-ctrl invalidate local
|
|
|
495
680
|
```jsonc
|
|
496
681
|
{
|
|
497
682
|
"timestamp": "2026-04-04T12:00:00Z", // auto-set on write; "" when invalidated
|
|
498
|
-
"topic": "
|
|
499
|
-
"description": "Scan of
|
|
683
|
+
"topic": "cache-ctrl source",
|
|
684
|
+
"description": "Scan of cache-ctrl TypeScript source",
|
|
500
685
|
"cache_miss_reason": "files changed", // optional: why the previous cache was discarded
|
|
501
686
|
"tracked_files": [
|
|
502
|
-
{ "path": "
|
|
687
|
+
{ "path": "src/commands/graph.ts", "mtime": 1743768000000, "hash": "sha256hex..." }
|
|
503
688
|
// mtime is auto-populated by the write command; agents only need to supply path (and optionally hash)
|
|
504
689
|
],
|
|
505
690
|
"global_facts": [ // optional: repo-level facts; last-write-wins; max 20 entries, each ≤ 300 chars
|
|
506
|
-
"
|
|
507
|
-
"
|
|
691
|
+
"TypeScript CLI tool executed by Bun",
|
|
692
|
+
"All errors use Result<T,E> — no thrown exceptions across command boundaries"
|
|
508
693
|
],
|
|
509
|
-
"facts": { // optional: per-file
|
|
510
|
-
"
|
|
511
|
-
|
|
694
|
+
"facts": { // optional: per-file structured FileFacts; per-path merge
|
|
695
|
+
"src/commands/graph.ts": {
|
|
696
|
+
"summary": "Reads graph.json and returns PageRank-ranked file list within a token budget",
|
|
697
|
+
"role": "implementation", // one of: entry-point | interface | implementation | test | config
|
|
698
|
+
"importance": 2, // 1 = critical, 2 = important, 3 = peripheral
|
|
699
|
+
"facts": [ // max 10 entries, each ≤ 300 chars
|
|
700
|
+
"Uses computePageRank with optional seed files for personalized ranking",
|
|
701
|
+
"Token budget clamped to 64–128000; defaults to 1024"
|
|
702
|
+
]
|
|
703
|
+
}
|
|
704
|
+
// FileFacts entries for files deleted from disk are evicted automatically on the next write
|
|
705
|
+
},
|
|
706
|
+
"modules": { // optional: logical groupings of file paths
|
|
707
|
+
"commands": ["src/commands/graph.ts", "src/commands/map.ts"]
|
|
512
708
|
}
|
|
513
709
|
// Any additional agent fields are preserved unchanged
|
|
514
710
|
}
|
|
515
711
|
```
|
|
516
712
|
|
|
713
|
+
### Graph: `.ai/local-context-gatherer_cache/graph.json`
|
|
714
|
+
|
|
715
|
+
Written and maintained by the `watch` daemon. Read by `cache-ctrl graph` and `cache_ctrl_graph`. Agents do not write this file directly.
|
|
716
|
+
|
|
717
|
+
```jsonc
|
|
718
|
+
{
|
|
719
|
+
"computed_at": "2026-04-11T10:00:00Z",
|
|
720
|
+
"files": {
|
|
721
|
+
"src/cache/cacheManager.ts": {
|
|
722
|
+
"rank": 0.0, // stored as 0.0; PageRank is recomputed on every graph command call
|
|
723
|
+
"deps": ["src/utils/validate.ts", "src/types/result.ts"],
|
|
724
|
+
"defs": ["readCache", "writeCache", "findRepoRoot"]
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
517
730
|
---
|
|
518
731
|
|
|
519
732
|
## Error Codes
|
|
@@ -531,9 +744,7 @@ cache-ctrl invalidate local
|
|
|
531
744
|
| `VALIDATION_ERROR` | Schema validation failed (e.g., missing required field or type mismatch in `write`) |
|
|
532
745
|
| `NO_MATCH` | No cache file matched the keyword |
|
|
533
746
|
| `AMBIGUOUS_MATCH` | Multiple files with identical top score |
|
|
534
|
-
| `
|
|
535
|
-
| `URL_NOT_FOUND` | `--url` value not found in `sources[]` |
|
|
536
|
-
| `UNKNOWN` | Unexpected internal error |
|
|
747
|
+
| `UNKNOWN` | Unexpected internal/runtime error (including unexpected HTTP client failures) |
|
|
537
748
|
|
|
538
749
|
---
|
|
539
750
|
|
package/cache_ctrl.ts
CHANGED
|
@@ -2,11 +2,14 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
2
2
|
import { listCommand } from "./src/commands/list.js";
|
|
3
3
|
import { inspectCommand } from "./src/commands/inspect.js";
|
|
4
4
|
import { invalidateCommand } from "./src/commands/invalidate.js";
|
|
5
|
-
import { checkFreshnessCommand } from "./src/commands/checkFreshness.js";
|
|
6
5
|
import { checkFilesCommand } from "./src/commands/checkFiles.js";
|
|
7
6
|
import { searchCommand } from "./src/commands/search.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { writeLocalCommand } from "./src/commands/writeLocal.js";
|
|
8
|
+
import { writeExternalCommand } from "./src/commands/writeExternal.js";
|
|
9
|
+
import { graphCommand } from "./src/commands/graph.js";
|
|
10
|
+
import { mapCommand } from "./src/commands/map.js";
|
|
11
|
+
import { toUnknownResult } from "./src/utils/errors.js";
|
|
12
|
+
import { rejectTraversalKeys } from "./src/utils/traversal.js";
|
|
10
13
|
|
|
11
14
|
const z = tool.schema;
|
|
12
15
|
|
|
@@ -18,8 +21,7 @@ function withServerTime(result: unknown): string {
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
function handleUnknownError(err: unknown): string {
|
|
21
|
-
|
|
22
|
-
return withServerTime({ ok: false, error: message, code: ErrorCode.UNKNOWN });
|
|
24
|
+
return withServerTime(toUnknownResult(err));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export const search = tool({
|
|
@@ -97,17 +99,87 @@ export const invalidate = tool({
|
|
|
97
99
|
},
|
|
98
100
|
});
|
|
99
101
|
|
|
100
|
-
export const
|
|
101
|
-
description:
|
|
102
|
+
export const check_files = tool({
|
|
103
|
+
description:
|
|
104
|
+
"For local cache: compare tracked files against stored mtime/hash values and return which files changed. Also reports new_files (files not excluded by .gitignore that are absent from cache — includes both git-tracked and untracked-non-ignored files) and deleted_git_files (git-tracked files deleted from working tree).",
|
|
105
|
+
args: {},
|
|
106
|
+
async execute(_args) {
|
|
107
|
+
try {
|
|
108
|
+
const result = await checkFilesCommand();
|
|
109
|
+
return withServerTime(result);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return handleUnknownError(err);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export const write_local = tool({
|
|
117
|
+
description:
|
|
118
|
+
"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.",
|
|
102
119
|
args: {
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
topic: z.string(),
|
|
121
|
+
description: z.string(),
|
|
122
|
+
tracked_files: z.array(z.object({ path: z.string() })),
|
|
123
|
+
global_facts: z.array(z.string().max(300)).max(20).optional(),
|
|
124
|
+
facts: z
|
|
125
|
+
.record(
|
|
126
|
+
z.string(),
|
|
127
|
+
z.object({
|
|
128
|
+
summary: z.string().optional(),
|
|
129
|
+
role: z.string().optional(),
|
|
130
|
+
importance: z.union([z.literal(1), z.literal(2), z.literal(3)]).optional(),
|
|
131
|
+
facts: z.array(z.string()).optional(),
|
|
132
|
+
}),
|
|
133
|
+
)
|
|
134
|
+
.superRefine(rejectTraversalKeys)
|
|
135
|
+
.optional(),
|
|
136
|
+
cache_miss_reason: z.string().optional(),
|
|
137
|
+
},
|
|
138
|
+
async execute(args) {
|
|
139
|
+
try {
|
|
140
|
+
const result = await writeLocalCommand({
|
|
141
|
+
agent: "local",
|
|
142
|
+
content: {
|
|
143
|
+
topic: args.topic,
|
|
144
|
+
description: args.description,
|
|
145
|
+
tracked_files: args.tracked_files,
|
|
146
|
+
...(args.global_facts !== undefined ? { global_facts: args.global_facts } : {}),
|
|
147
|
+
...(args.facts !== undefined ? { facts: args.facts } : {}),
|
|
148
|
+
...(args.cache_miss_reason !== undefined ? { cache_miss_reason: args.cache_miss_reason } : {}),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
return withServerTime(result);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return handleUnknownError(err);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const write_external = tool({
|
|
159
|
+
description:
|
|
160
|
+
"Write a validated external cache entry to disk. Uses atomic write-with-merge so unknown fields are preserved.",
|
|
161
|
+
args: {
|
|
162
|
+
subject: z.string(),
|
|
163
|
+
description: z.string(),
|
|
164
|
+
fetched_at: z.string().datetime(),
|
|
165
|
+
sources: z.array(
|
|
166
|
+
z.object({
|
|
167
|
+
type: z.string(),
|
|
168
|
+
url: z.string(),
|
|
169
|
+
version: z.string().optional(),
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
105
172
|
},
|
|
106
173
|
async execute(args) {
|
|
107
174
|
try {
|
|
108
|
-
const result = await
|
|
175
|
+
const result = await writeExternalCommand({
|
|
176
|
+
agent: "external",
|
|
109
177
|
subject: args.subject,
|
|
110
|
-
|
|
178
|
+
content: {
|
|
179
|
+
description: args.description,
|
|
180
|
+
fetched_at: args.fetched_at,
|
|
181
|
+
sources: args.sources,
|
|
182
|
+
},
|
|
111
183
|
});
|
|
112
184
|
return withServerTime(result);
|
|
113
185
|
} catch (err) {
|
|
@@ -116,13 +188,22 @@ export const check_freshness = tool({
|
|
|
116
188
|
},
|
|
117
189
|
});
|
|
118
190
|
|
|
119
|
-
export const
|
|
191
|
+
export const graph = tool({
|
|
120
192
|
description:
|
|
121
|
-
"
|
|
122
|
-
args: {
|
|
123
|
-
|
|
193
|
+
"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'.",
|
|
194
|
+
args: {
|
|
195
|
+
maxTokens: z.number().optional().describe("Token budget for the response (default: 1024)"),
|
|
196
|
+
seed: z
|
|
197
|
+
.array(z.string())
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("File paths to personalize PageRank toward (e.g. recently changed files)"),
|
|
200
|
+
},
|
|
201
|
+
async execute(args) {
|
|
124
202
|
try {
|
|
125
|
-
const result = await
|
|
203
|
+
const result = await graphCommand({
|
|
204
|
+
...(args.maxTokens !== undefined ? { maxTokens: args.maxTokens } : {}),
|
|
205
|
+
...(args.seed !== undefined ? { seed: args.seed } : {}),
|
|
206
|
+
});
|
|
126
207
|
return withServerTime(result);
|
|
127
208
|
} catch (err) {
|
|
128
209
|
return handleUnknownError(err);
|
|
@@ -130,20 +211,21 @@ export const check_files = tool({
|
|
|
130
211
|
},
|
|
131
212
|
});
|
|
132
213
|
|
|
133
|
-
export const
|
|
214
|
+
export const map = tool({
|
|
134
215
|
description:
|
|
135
|
-
"
|
|
216
|
+
"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.",
|
|
136
217
|
args: {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
218
|
+
depth: z
|
|
219
|
+
.enum(["overview", "modules", "full"])
|
|
220
|
+
.optional()
|
|
221
|
+
.describe("Map depth (default: 'overview')"),
|
|
222
|
+
folder: z.string().optional().describe("Restrict map to files under this path prefix"),
|
|
140
223
|
},
|
|
141
224
|
async execute(args) {
|
|
142
225
|
try {
|
|
143
|
-
const result = await
|
|
144
|
-
|
|
145
|
-
...(args.
|
|
146
|
-
content: args.content,
|
|
226
|
+
const result = await mapCommand({
|
|
227
|
+
...(args.depth !== undefined ? { depth: args.depth } : {}),
|
|
228
|
+
...(args.folder !== undefined ? { folder: args.folder } : {}),
|
|
147
229
|
});
|
|
148
230
|
return withServerTime(result);
|
|
149
231
|
} catch (err) {
|