@thecat69/cache-ctrl 1.0.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 +558 -0
- package/cache_ctrl.ts +153 -0
- package/package.json +35 -0
- package/skills/cache-ctrl-caller/SKILL.md +154 -0
- package/skills/cache-ctrl-external/SKILL.md +130 -0
- package/skills/cache-ctrl-local/SKILL.md +213 -0
- package/src/cache/cacheManager.ts +241 -0
- package/src/cache/externalCache.ts +127 -0
- package/src/cache/localCache.ts +9 -0
- package/src/commands/checkFiles.ts +83 -0
- package/src/commands/checkFreshness.ts +123 -0
- package/src/commands/flush.ts +55 -0
- package/src/commands/inspect.ts +184 -0
- package/src/commands/install.ts +13 -0
- package/src/commands/invalidate.ts +53 -0
- package/src/commands/list.ts +83 -0
- package/src/commands/prune.ts +110 -0
- package/src/commands/search.ts +57 -0
- package/src/commands/touch.ts +47 -0
- package/src/commands/write.ts +170 -0
- package/src/files/changeDetector.ts +122 -0
- package/src/files/gitFiles.ts +41 -0
- package/src/files/openCodeInstaller.ts +66 -0
- package/src/http/freshnessChecker.ts +116 -0
- package/src/index.ts +557 -0
- package/src/search/keywordSearch.ts +59 -0
- package/src/types/cache.ts +91 -0
- package/src/types/commands.ts +192 -0
- package/src/types/result.ts +36 -0
- package/src/utils/fileStem.ts +7 -0
- package/src/utils/validate.ts +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# cache-ctrl
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
It handles advisory locking for safe concurrent writes, keyword search across all entries, HTTP freshness checking for external URLs, and file-change detection for local scans.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### npm (recommended)
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install -g @thecat69/cache-ctrl
|
|
15
|
+
cache-ctrl install
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`cache-ctrl install` configures the OpenCode integration in one step:
|
|
19
|
+
- Writes an OpenCode tool wrapper at `~/.config/opencode/tools/cache_ctrl.ts` (Linux/macOS) or `%APPDATA%\opencode\tools\cache_ctrl.ts` (Windows).
|
|
20
|
+
- Copies 3 skill SKILL.md files to `~/.config/opencode/skills/`.
|
|
21
|
+
|
|
22
|
+
**Prerequisites**: `bun` ≥ 1.0.0 must be in `PATH` (Bun executes the TypeScript files natively — no build step).
|
|
23
|
+
|
|
24
|
+
### Local development (from source)
|
|
25
|
+
|
|
26
|
+
Run from inside the `cache-ctrl/` directory:
|
|
27
|
+
|
|
28
|
+
```zsh
|
|
29
|
+
zsh install.sh
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates two symlinks:
|
|
33
|
+
- `~/.local/bin/cache-ctrl` → `src/index.ts` — global CLI command (executed directly by Bun)
|
|
34
|
+
- `.opencode/tools/cache-ctrl.ts` → `cache_ctrl.ts` — auto-discovered by OpenCode as a native plugin
|
|
35
|
+
|
|
36
|
+
`install.sh` is for local development only. For end-user installation, use `npm install -g @thecat69/cache-ctrl`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Architecture
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
CLI (cache-ctrl) opencode Plugin
|
|
44
|
+
src/index.ts cache_ctrl.ts
|
|
45
|
+
│ │
|
|
46
|
+
└──────────┬──────────────┘
|
|
47
|
+
│
|
|
48
|
+
Command Layer
|
|
49
|
+
src/commands/{list, inspect, flush,
|
|
50
|
+
invalidate, touch, prune,
|
|
51
|
+
checkFreshness, checkFiles, search,
|
|
52
|
+
write}.ts
|
|
53
|
+
│
|
|
54
|
+
Core Services
|
|
55
|
+
cacheManager ← read/write + advisory lock
|
|
56
|
+
externalCache ← external staleness logic
|
|
57
|
+
localCache ← local scan path logic
|
|
58
|
+
freshnessChecker ← HTTP HEAD requests
|
|
59
|
+
changeDetector ← mtime/hash comparison
|
|
60
|
+
keywordSearch ← scoring engine
|
|
61
|
+
│
|
|
62
|
+
Cache Directories (on disk)
|
|
63
|
+
.ai/external-context-gatherer_cache/
|
|
64
|
+
├── <subject>.json
|
|
65
|
+
└── <subject>.json.lock (advisory)
|
|
66
|
+
.ai/local-context-gatherer_cache/
|
|
67
|
+
├── context.json
|
|
68
|
+
└── context.json.lock (advisory)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Key design decisions:**
|
|
72
|
+
- All commands funnel through `cacheManager` for reads/writes — no direct filesystem access from command handlers.
|
|
73
|
+
- The CLI and plugin share the same command functions — no duplicated business logic.
|
|
74
|
+
- All operations return `Result<T, CacheError>` — nothing throws into the caller.
|
|
75
|
+
- `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.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## CLI Reference
|
|
80
|
+
|
|
81
|
+
**Output format**: JSON (single line) by default. Add `--pretty` to any command for indented output.
|
|
82
|
+
**Errors**: Written to stderr as `{ "ok": false, "error": "...", "code": "..." }`. Exit code `1` on error, `2` on bad arguments.
|
|
83
|
+
**Help**: Run `cache-ctrl --help` or `cache-ctrl help` for the full command reference. Run `cache-ctrl help <command>` for per-command usage, arguments, and options. Help output is plain text written to stdout; exit code `0` on success, `1` for unknown command.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### `install`
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
cache-ctrl install [--config-dir <path>]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Configures OpenCode integration after `npm install -g @thecat69/cache-ctrl`. Does two things:
|
|
94
|
+
|
|
95
|
+
1. **Generates an OpenCode tool wrapper** at `<opencode-config>/tools/cache_ctrl.ts` — a one-line re-export that points back to the installed package so Bun resolves all relative imports correctly.
|
|
96
|
+
2. **Copies 3 skill files** (`cache-ctrl-caller`, `cache-ctrl-local`, `cache-ctrl-external`) to `<opencode-config>/skills/<name>/SKILL.md`.
|
|
97
|
+
|
|
98
|
+
Both operations are idempotent — re-running `cache-ctrl install` after `npm update -g @thecat69/cache-ctrl` regenerates the wrapper with the new package path.
|
|
99
|
+
|
|
100
|
+
**OpenCode config directory resolution** (in priority order):
|
|
101
|
+
1. `--config-dir <path>` flag (explicit override)
|
|
102
|
+
2. `$XDG_CONFIG_HOME/opencode` (Linux/macOS, if `XDG_CONFIG_HOME` is set)
|
|
103
|
+
3. `~/.config/opencode` (Linux/macOS default)
|
|
104
|
+
4. `%APPDATA%\opencode` (Windows)
|
|
105
|
+
|
|
106
|
+
**Options:**
|
|
107
|
+
|
|
108
|
+
| Flag | Description |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `--config-dir <path>` | Override the detected OpenCode config directory |
|
|
111
|
+
|
|
112
|
+
```jsonc
|
|
113
|
+
// cache-ctrl install --pretty
|
|
114
|
+
{
|
|
115
|
+
"ok": true,
|
|
116
|
+
"value": {
|
|
117
|
+
"configDir": "/home/user/.config/opencode",
|
|
118
|
+
"toolPath": "/home/user/.config/opencode/tools/cache_ctrl.ts",
|
|
119
|
+
"skillPaths": [
|
|
120
|
+
"/home/user/.config/opencode/skills/cache-ctrl-caller/SKILL.md",
|
|
121
|
+
"/home/user/.config/opencode/skills/cache-ctrl-local/SKILL.md",
|
|
122
|
+
"/home/user/.config/opencode/skills/cache-ctrl-external/SKILL.md"
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Error codes**: `FILE_WRITE_ERROR` if the tool wrapper or a skill file cannot be written.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `help`
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
cache-ctrl help [<command>]
|
|
136
|
+
cache-ctrl --help
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Prints human-readable usage information and exits. No JSON output.
|
|
140
|
+
|
|
141
|
+
- `cache-ctrl --help` — print full command reference (all commands with descriptions)
|
|
142
|
+
- `cache-ctrl help` — same as `--help`
|
|
143
|
+
- `cache-ctrl help <command>` — print per-command usage, arguments, and options
|
|
144
|
+
- `cache-ctrl help help` — same as `cache-ctrl help` (full reference)
|
|
145
|
+
|
|
146
|
+
Exit code: `0` on success, `1` if `<command>` is not recognized.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### `list`
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
cache-ctrl list [--agent external|local|all] [--pretty]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Lists all cache entries. Shows age, human-readable age string, and staleness flag.
|
|
157
|
+
|
|
158
|
+
- External entries are stale if `fetched_at` is empty or older than 24 hours.
|
|
159
|
+
- Local entries show `is_stale: true` only when `cache_ctrl_check_files` detects actual changes (changed files, new non-ignored files, or deleted files). A freshly-written cache with no subsequent file changes shows `is_stale: false`.
|
|
160
|
+
|
|
161
|
+
**Default**: `--agent all`
|
|
162
|
+
|
|
163
|
+
```jsonc
|
|
164
|
+
// cache-ctrl list --pretty
|
|
165
|
+
{
|
|
166
|
+
"ok": true,
|
|
167
|
+
"value": [
|
|
168
|
+
{
|
|
169
|
+
"file": "/path/to/.ai/external-context-gatherer_cache/opencode-skills.json",
|
|
170
|
+
"agent": "external",
|
|
171
|
+
"subject": "opencode-skills",
|
|
172
|
+
"description": "opencode skill file index",
|
|
173
|
+
"fetched_at": "2026-04-04T10:00:00Z",
|
|
174
|
+
"age_human": "2 hours ago",
|
|
175
|
+
"is_stale": false
|
|
176
|
+
}
|
|
177
|
+
]
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### `inspect`
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
cache-ctrl inspect <agent> <subject-keyword> [--filter <kw>[,<kw>...]] [--folder <path>] [--search-facts <kw>[,<kw>...]] [--pretty]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Prints the full JSON content of the best-matching cache entry. Uses the same keyword scoring as `search`. Returns `AMBIGUOUS_MATCH` if two results score identically.
|
|
190
|
+
|
|
191
|
+
Three complementary filters are available for `agent: "local"` — they are AND-ed when combined:
|
|
192
|
+
|
|
193
|
+
**`--filter <kw>[,<kw>...]`** (local agent only): restricts `facts` to entries whose **file path** contains at least one keyword (case-insensitive substring). Ignored for the external agent.
|
|
194
|
+
|
|
195
|
+
**`--folder <path>`** (local agent only): restricts `facts` to entries whose **file path** equals the given folder prefix or starts with `<folder>/` (recursive subtree match). Returns `INVALID_ARGS` if used with the external agent.
|
|
196
|
+
|
|
197
|
+
**`--search-facts <kw>[,<kw>...]`** (local agent only): restricts `facts` to entries where **at least one fact string** contains any keyword (case-insensitive substring). Silently ignored for the external agent.
|
|
198
|
+
|
|
199
|
+
`global_facts` and all other metadata fields are always included regardless of which filters are set.
|
|
200
|
+
|
|
201
|
+
**`tracked_files` is never returned** for `agent: "local"` — it is internal operational metadata consumed by `check-files` and is always stripped from inspect responses.
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
cache-ctrl inspect external opencode-skills --pretty
|
|
205
|
+
cache-ctrl inspect local context --pretty
|
|
206
|
+
cache-ctrl inspect local context --filter lsp,nvim --pretty
|
|
207
|
+
cache-ctrl inspect local context --folder src/commands --pretty
|
|
208
|
+
cache-ctrl inspect local context --search-facts "Result<" --pretty
|
|
209
|
+
cache-ctrl inspect local context --folder src --filter commands --search-facts async --pretty
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### `flush`
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
cache-ctrl flush <agent|all> --confirm [--pretty]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Deletes cache files. The `--confirm` flag is **required** as a safeguard.
|
|
221
|
+
|
|
222
|
+
- `external` → deletes all `*.json` files in the external cache directory (not `.lock` files)
|
|
223
|
+
- `local` → deletes `context.json`
|
|
224
|
+
- `all` → both
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
cache-ctrl flush external --confirm
|
|
228
|
+
cache-ctrl flush all --confirm --pretty
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `invalidate`
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
cache-ctrl invalidate <agent> [subject-keyword] [--pretty]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Zeros out the timestamp (`fetched_at` for external, `timestamp` for local), marking the entry as stale without deleting its content. Agents will treat it as a cache miss on next run.
|
|
240
|
+
|
|
241
|
+
- With a keyword: invalidates the best-matching file.
|
|
242
|
+
- Without a keyword on `external`: invalidates **all** external entries.
|
|
243
|
+
- Without a keyword on `local`: invalidates `context.json`.
|
|
244
|
+
|
|
245
|
+
> If the local cache file does not exist, returns `FILE_NOT_FOUND` — the command is a no-op in that case.
|
|
246
|
+
|
|
247
|
+
```
|
|
248
|
+
cache-ctrl invalidate external opencode-skills
|
|
249
|
+
cache-ctrl invalidate external # all external entries
|
|
250
|
+
cache-ctrl invalidate local
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### `touch`
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
cache-ctrl touch <agent> [subject-keyword] [--pretty]
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Resets the timestamp to the current UTC time — the inverse of `invalidate`. Marks the entry as fresh.
|
|
262
|
+
|
|
263
|
+
- Without a keyword on `external`: touches **all** external entries.
|
|
264
|
+
|
|
265
|
+
```
|
|
266
|
+
cache-ctrl touch external opencode-skills
|
|
267
|
+
cache-ctrl touch local
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### `prune`
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
cache-ctrl prune [--agent external|local|all] [--max-age <duration>] [--delete] [--pretty]
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Finds entries older than `--max-age` and invalidates them (default) or deletes them (`--delete`).
|
|
279
|
+
|
|
280
|
+
**Duration format**: `<number><unit>` — `h` for hours, `d` for days. Examples: `24h`, `7d`, `1d`.
|
|
281
|
+
|
|
282
|
+
**Defaults**: `--agent all`, `--max-age 24h` for external. Local cache **always** matches (no TTL).
|
|
283
|
+
|
|
284
|
+
> If the local cache does not exist and `--delete` is not set, the local entry is skipped silently (not added to `matched`).
|
|
285
|
+
|
|
286
|
+
> ⚠️ `prune --agent all --delete` will **always** delete the local cache. Use `--agent external` to avoid this.
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
cache-ctrl prune --agent external --max-age 7d
|
|
290
|
+
cache-ctrl prune --agent external --max-age 1d --delete
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
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
|
+
### `check-files`
|
|
326
|
+
|
|
327
|
+
```
|
|
328
|
+
cache-ctrl check-files [--pretty]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Reads `tracked_files[]` from the local cache and compares each file's current `mtime` (and `hash` if stored) against the saved values.
|
|
332
|
+
|
|
333
|
+
**Comparison logic:**
|
|
334
|
+
1. Read current `mtime` via `lstat()` (reflects the symlink node itself, not the target).
|
|
335
|
+
2. If stored `hash` is present and `mtime` changed → recompute SHA-256. Hash match → `unchanged` (touch-only). Hash differs → `changed`.
|
|
336
|
+
3. No stored `hash` → mtime change alone marks the file as `changed`.
|
|
337
|
+
4. File missing on disk → `missing`.
|
|
338
|
+
|
|
339
|
+
If `tracked_files` is absent or empty → returns `{ status: "unchanged", ... }` (not an error).
|
|
340
|
+
|
|
341
|
+
```jsonc
|
|
342
|
+
// cache-ctrl check-files --pretty
|
|
343
|
+
{
|
|
344
|
+
"ok": true,
|
|
345
|
+
"value": {
|
|
346
|
+
"status": "unchanged",
|
|
347
|
+
"changed_files": [],
|
|
348
|
+
"unchanged_files": ["lua/plugins/ui/bufferline.lua"],
|
|
349
|
+
"missing_files": []
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
### `search`
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
cache-ctrl search <keyword> [<keyword>...] [--pretty]
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Searches all cache files across both namespaces. Case-insensitive. Returns results ranked by score (descending).
|
|
363
|
+
|
|
364
|
+
**Scoring matrix** (per keyword, additive across multiple keywords):
|
|
365
|
+
|
|
366
|
+
| Match type | Score |
|
|
367
|
+
|---|---|
|
|
368
|
+
| Exact match on file stem | 100 |
|
|
369
|
+
| Substring match on file stem | 80 |
|
|
370
|
+
| Exact word match on `subject`/`topic` | 70 |
|
|
371
|
+
| Substring match on `subject`/`topic` | 50 |
|
|
372
|
+
| Keyword match on `description` | 30 |
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
cache-ctrl search opencode skills
|
|
376
|
+
cache-ctrl search neovim --pretty
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
### `write`
|
|
382
|
+
|
|
383
|
+
```
|
|
384
|
+
cache-ctrl write external <subject> --data '<json>' [--pretty]
|
|
385
|
+
cache-ctrl write local --data '<json>' [--pretty]
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
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`.
|
|
389
|
+
|
|
390
|
+
- `external`: `subject` is required as a positional argument. After validation, unknown fields from the existing file on disk are preserved (merge write).
|
|
391
|
+
- `local`: no subject argument; `timestamp` is **auto-set** to the current UTC time server-side — any value supplied in `--data` is silently overridden. `mtime` for each entry in `tracked_files[]` is **auto-populated** by the write command via filesystem `lstat()` — agents do not need to supply it. Local writes use per-path merge: submitted `tracked_files` entries replace existing entries for the same path; entries for other paths are preserved; entries for files deleted from disk are evicted automatically. On cold start (no existing cache), submit all relevant files for a full write; on subsequent writes, submit only new or changed files.
|
|
392
|
+
- `local`: facts paths are validated against submitted `tracked_files` — submitting a facts key outside that set returns `VALIDATION_ERROR`.
|
|
393
|
+
|
|
394
|
+
> `VALIDATION_ERROR` messages include the offending field path (e.g., `facts.src/foo.ts.2: write concise observations, not file content (max 800 chars per fact)`), making it straightforward to locate the violating value.
|
|
395
|
+
|
|
396
|
+
> 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
|
+
|
|
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.
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
// cache-ctrl write external mysubject --data '{"subject":"mysubject","description":"...","fetched_at":"2026-04-05T10:00:00Z","sources":[],"header_metadata":{}}' --pretty
|
|
402
|
+
{ "ok": true, "value": { "file": "/path/to/.ai/external-context-gatherer_cache/mysubject.json" } }
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## opencode Plugin Tools
|
|
408
|
+
|
|
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:
|
|
410
|
+
|
|
411
|
+
| Tool | Description |
|
|
412
|
+
|---|---|
|
|
413
|
+
| `cache_ctrl_search` | Search all cache entries by keyword |
|
|
414
|
+
| `cache_ctrl_list` | List entries with age and staleness flags |
|
|
415
|
+
| `cache_ctrl_inspect` | Return full content of a specific entry |
|
|
416
|
+
| `cache_ctrl_invalidate` | Zero out a cache entry's timestamp |
|
|
417
|
+
| `cache_ctrl_check_freshness` | HTTP HEAD check for external source URLs |
|
|
418
|
+
| `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 |
|
|
420
|
+
|
|
421
|
+
No bash permission is required for agents that use the plugin tools directly.
|
|
422
|
+
|
|
423
|
+
All 7 plugin tool responses include a `server_time` field at the outer JSON level:
|
|
424
|
+
|
|
425
|
+
```json
|
|
426
|
+
{ "ok": true, "value": { ... }, "server_time": "2026-04-05T12:34:56.789Z" }
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Use `server_time` to assess how stale stored timestamps are without requiring bash or system access.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Agent Integration
|
|
434
|
+
|
|
435
|
+
### `external-context-gatherer`
|
|
436
|
+
|
|
437
|
+
```zsh
|
|
438
|
+
# Before fetching — check if cache is still fresh
|
|
439
|
+
cache-ctrl list --agent external --pretty
|
|
440
|
+
# If is_stale: false → skip fetch
|
|
441
|
+
|
|
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
|
+
# After writing new cache content — mark entry fresh
|
|
447
|
+
cache-ctrl touch external <subject>
|
|
448
|
+
|
|
449
|
+
# Force a re-fetch
|
|
450
|
+
cache-ctrl invalidate external <subject>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### `local-context-gatherer`
|
|
454
|
+
|
|
455
|
+
```zsh
|
|
456
|
+
# Before deciding whether to re-scan
|
|
457
|
+
cache-ctrl check-files
|
|
458
|
+
# If status: "changed" → invalidate and re-scan
|
|
459
|
+
cache-ctrl invalidate local
|
|
460
|
+
# If status: "unchanged" → use cached context
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
**Requirement**: The agent MUST populate `tracked_files[]` (with `path` and optionally `hash`) when writing its cache file. `mtime` per entry is auto-populated server-side via filesystem `lstat()` — agents do not need to supply it. `check-files` returns `unchanged` silently if `tracked_files` is absent.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## Cache File Schemas
|
|
468
|
+
|
|
469
|
+
### External: `.ai/external-context-gatherer_cache/<subject>.json`
|
|
470
|
+
|
|
471
|
+
```jsonc
|
|
472
|
+
{
|
|
473
|
+
"subject": "opencode-skills", // Must match the file stem
|
|
474
|
+
"description": "opencode skill index", // One-liner for keyword search
|
|
475
|
+
"fetched_at": "2026-04-04T12:00:00Z", // "" when invalidated
|
|
476
|
+
"sources": [
|
|
477
|
+
{ "type": "github_api", "url": "https://..." }
|
|
478
|
+
],
|
|
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
|
+
// Any additional agent fields are preserved unchanged
|
|
488
|
+
}
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Local: `.ai/local-context-gatherer_cache/context.json`
|
|
492
|
+
|
|
493
|
+
> `timestamp` is **auto-set** by the write command to the current UTC time. Do not include it in agent-supplied content — any value provided is silently overridden. `mtime` values in `tracked_files[]` are **auto-populated** by the write command via filesystem `lstat()` — agents only need to supply `path` (and optionally `hash`). Local writes use per-path merge: submitted `tracked_files` entries replace existing entries for the same path; entries for other paths are preserved; entries for files deleted from disk are evicted automatically. On cold start (no existing cache), submit all relevant files; on subsequent writes, submit only new or changed files.
|
|
494
|
+
|
|
495
|
+
```jsonc
|
|
496
|
+
{
|
|
497
|
+
"timestamp": "2026-04-04T12:00:00Z", // auto-set on write; "" when invalidated
|
|
498
|
+
"topic": "neovim plugin configuration",
|
|
499
|
+
"description": "Scan of nvim lua plugins",
|
|
500
|
+
"cache_miss_reason": "files changed", // optional: why the previous cache was discarded
|
|
501
|
+
"tracked_files": [
|
|
502
|
+
{ "path": "lua/plugins/ui/bufferline.lua", "mtime": 1743768000000, "hash": "sha256hex..." }
|
|
503
|
+
// mtime is auto-populated by the write command; agents only need to supply path (and optionally hash)
|
|
504
|
+
],
|
|
505
|
+
"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)"
|
|
508
|
+
],
|
|
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
|
|
512
|
+
}
|
|
513
|
+
// Any additional agent fields are preserved unchanged
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## Error Codes
|
|
520
|
+
|
|
521
|
+
| Code | Meaning |
|
|
522
|
+
|---|---|
|
|
523
|
+
| `FILE_NOT_FOUND` | Cache file does not exist |
|
|
524
|
+
| `FILE_READ_ERROR` | Cannot read file |
|
|
525
|
+
| `FILE_WRITE_ERROR` | Cannot write file |
|
|
526
|
+
| `PARSE_ERROR` | File is not valid JSON |
|
|
527
|
+
| `LOCK_TIMEOUT` | Could not acquire lock within 5 seconds |
|
|
528
|
+
| `LOCK_ERROR` | Unexpected lock file error |
|
|
529
|
+
| `INVALID_ARGS` | Missing or invalid CLI arguments |
|
|
530
|
+
| `CONFIRMATION_REQUIRED` | `flush` called without `--confirm` |
|
|
531
|
+
| `VALIDATION_ERROR` | Schema validation failed (e.g., missing required field or type mismatch in `write`) |
|
|
532
|
+
| `NO_MATCH` | No cache file matched the keyword |
|
|
533
|
+
| `AMBIGUOUS_MATCH` | Multiple files with identical top score |
|
|
534
|
+
| `HTTP_REQUEST_FAILED` | Network error during HEAD request |
|
|
535
|
+
| `URL_NOT_FOUND` | `--url` value not found in `sources[]` |
|
|
536
|
+
| `UNKNOWN` | Unexpected internal error |
|
|
537
|
+
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
## Development
|
|
541
|
+
|
|
542
|
+
```zsh
|
|
543
|
+
# Run unit tests
|
|
544
|
+
bun run test
|
|
545
|
+
|
|
546
|
+
# Watch mode
|
|
547
|
+
bun run test:watch
|
|
548
|
+
|
|
549
|
+
# Run E2E tests (requires Docker)
|
|
550
|
+
bun run test:e2e
|
|
551
|
+
|
|
552
|
+
# Re-run installer (idempotent)
|
|
553
|
+
zsh install.sh
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
Unit tests live in `tests/` and use Vitest. Filesystem operations use real temp directories; HTTP calls are mocked with `vi.mock`.
|
|
557
|
+
|
|
558
|
+
E2E tests live in `e2e/tests/` and run inside Docker via `docker compose -f e2e/docker-compose.yml run --rm e2e`. They spawn the actual CLI binary as a subprocess and verify exit codes, stdout/stderr JSON shape, and cross-command behaviour. Docker must be running; no other host dependencies are required.
|