@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.
Files changed (43) hide show
  1. package/README.md +202 -28
  2. package/cache_ctrl.ts +125 -13
  3. package/package.json +2 -1
  4. package/skills/cache-ctrl-caller/SKILL.md +45 -31
  5. package/skills/cache-ctrl-external/SKILL.md +20 -45
  6. package/skills/cache-ctrl-local/SKILL.md +95 -86
  7. package/src/analysis/graphBuilder.ts +85 -0
  8. package/src/analysis/pageRank.ts +167 -0
  9. package/src/analysis/symbolExtractor.ts +240 -0
  10. package/src/cache/cacheManager.ts +52 -2
  11. package/src/cache/externalCache.ts +41 -64
  12. package/src/cache/graphCache.ts +12 -0
  13. package/src/cache/localCache.ts +2 -0
  14. package/src/commands/checkFiles.ts +7 -4
  15. package/src/commands/checkFreshness.ts +19 -19
  16. package/src/commands/flush.ts +9 -2
  17. package/src/commands/graph.ts +131 -0
  18. package/src/commands/inspect.ts +13 -181
  19. package/src/commands/inspectExternal.ts +79 -0
  20. package/src/commands/inspectLocal.ts +134 -0
  21. package/src/commands/install.ts +6 -0
  22. package/src/commands/invalidate.ts +19 -2
  23. package/src/commands/list.ts +11 -11
  24. package/src/commands/map.ts +87 -0
  25. package/src/commands/prune.ts +20 -8
  26. package/src/commands/search.ts +9 -2
  27. package/src/commands/touch.ts +9 -2
  28. package/src/commands/version.ts +14 -0
  29. package/src/commands/watch.ts +253 -0
  30. package/src/commands/writeExternal.ts +51 -0
  31. package/src/commands/writeLocal.ts +123 -0
  32. package/src/files/changeDetector.ts +15 -0
  33. package/src/files/gitFiles.ts +15 -0
  34. package/src/files/openCodeInstaller.ts +21 -2
  35. package/src/http/freshnessChecker.ts +23 -1
  36. package/src/index.ts +253 -28
  37. package/src/search/keywordSearch.ts +24 -0
  38. package/src/types/cache.ts +42 -18
  39. package/src/types/commands.ts +99 -1
  40. package/src/types/result.ts +27 -7
  41. package/src/utils/errors.ts +14 -0
  42. package/src/utils/traversal.ts +42 -0
  43. 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, flush,
50
- invalidate, touch, prune,
51
- checkFreshness, checkFiles, search,
52
- write}.ts
53
-
54
- Core Services
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
- └── context.json.lock (advisory)
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: `24h`, `7d`, `1d`.
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 external <subject> --data '<json>' [--pretty]
385
- cache-ctrl write local --data '<json>' [--pretty]
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 this command (or `cache_ctrl_write`) instead of writing cache files directly.** Direct writes skip schema validation and risk corrupting the cache.
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 external mysubject --data '{"subject":"mysubject","description":"...","fetched_at":"2026-04-05T10:00:00Z","sources":[],"header_metadata":{}}' --pretty
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 7 tools that call the same command functions as the CLI:
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
- | `cache_ctrl_write` | Write a validated cache entry; validates against ExternalCacheFile or LocalCacheFile schema |
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 7 plugin tool responses include a `server_time` field at the outer JSON level:
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": "neovim plugin configuration",
499
- "description": "Scan of nvim lua plugins",
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": "lua/plugins/ui/bufferline.lua", "mtime": 1743768000000, "hash": "sha256hex..." }
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
- "Kubuntu dotfiles repo",
507
- "StyLua for Lua (140 col, 2-space indent)"
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 facts; per-path merge; max 30 entries per file, each ≤ 800 chars
510
- "lua/plugins/ui/bufferline.lua": ["lazy-loaded via ft = lua", "uses catppuccin mocha theme"]
511
- // Facts for files deleted from disk are evicted automatically on the next write
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 { writeCommand } from "./src/commands/write.js";
9
- import { ErrorCode } from "./src/types/result.js";
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
- const message = err instanceof Error ? err.message : String(err);
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 write = tool({
136
+ export const write_local = tool({
134
137
  description:
135
- "Write a validated cache entry to disk. Validates the content object against the ExternalCacheFile or LocalCacheFile schema before writing. Returns VALIDATION_ERROR if required fields are missing or have wrong types. For 'external': subject arg is required and must match content.subject (or will be injected if absent). For 'local': omit subject; timestamp is auto-set to current UTC time — do not include it in content. In tracked_files, each entry needs only { path } mtime and hash are auto-computed by the tool; any caller-provided mtime or hash values are stripped. For local: uses per-path merge tracked_files entries are merged by path (submitted paths replace existing entries for those paths; other paths are preserved). Entries for files no longer present on disk are evicted automatically. On cold start (no existing cache), submit all relevant files. On subsequent writes, submit only new and changed files. For 'external': uses atomic write-with-merge — existing unknown fields in the file are preserved. Call cache_ctrl_schema or read the skill to see required fields before calling this.",
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
- agent: AgentRequiredSchema,
138
- subject: z.string().min(1).optional(),
139
- content: z.record(z.string(), z.unknown()),
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 writeCommand({
144
- agent: args.agent,
145
- ...(args.subject !== undefined ? { subject: args.subject } : {}),
146
- content: args.content,
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.0.0",
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 → **use Tier 3** for all operations.
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 | Tier 3 |
125
- |---|---|---|---|
126
- | Check local freshness | `cache_ctrl_check_files` | `cache-ctrl check-files` | read context.json, check timestamp |
127
- | List external entries | `cache_ctrl_list` (agent: "external") | `cache-ctrl list --agent external` | glob + read each JSON |
128
- | Search entries | `cache_ctrl_search` | `cache-ctrl search <kw>...` | scan subject/description fields |
129
- | Read facts (local) | `cache_ctrl_inspect` + `filter` | `cache-ctrl inspect local context --filter <kw>` | read file, extract facts |
130
- | Read entry (external) | `cache_ctrl_inspect` | `cache-ctrl inspect external <subject>` | read file directly |
131
- | Invalidate local | `cache_ctrl_invalidate` (agent: "local") | `cache-ctrl invalidate local` | delete or overwrite file |
132
- | Invalidate external | `cache_ctrl_invalidate` (agent: "external", subject) | `cache-ctrl invalidate external <subject>` | set `fetched_at` to `""` via edit |
133
- | HTTP freshness check | `cache_ctrl_check_freshness` | `cache-ctrl check-freshness <subject>` | compare `fetched_at` with now |
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: