agent-file-stash 0.5.0 → 0.6.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 +56 -8
- package/dist/cli.mjs +728 -194
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ The MCP server exposes 4 tools that agents discover and use automatically:
|
|
|
57
57
|
|
|
58
58
|
| Tool | Description |
|
|
59
59
|
|------|-------------|
|
|
60
|
-
| `read_file` | Read a file with stashing. Returns full content on first read, an "unchanged" label or diff on subsequent reads. Parameters: `path` (required), `offset` (1-based start line), `limit` (max lines), `force` (bypass the stash and return full content). |
|
|
60
|
+
| `read_file` | Read a file with stashing. Returns full content on first read, an "unchanged" label or diff on subsequent reads. Parameters: `path` (required), `offset` (1-based start line), `limit` (max lines), `force` (bypass the stash and return full content), `agent` (read-tracking scope, normally set by the [subagent hook](#subagents)). |
|
|
61
61
|
| `read_files` | Batch read multiple files at once with stashing. Parameter: `paths` (array of file paths). |
|
|
62
62
|
| `stash_status` | Show files tracked and this session's accounting: reads, tokens a plain read would have sent, tokens actually sent, gross saved, tool-definition overhead (est.) and net saved (est.), plus lifetime gross saved. |
|
|
63
63
|
| `stash_clear` | Clear all stashed content, read tracking and stats. |
|
|
@@ -91,7 +91,7 @@ Done! Restart your editor to pick up agent-file-stash.
|
|
|
91
91
|
|
|
92
92
|
When no supported editor is detected, `init` prints the manual MCP snippet instead.
|
|
93
93
|
|
|
94
|
-
`init --hooks` also merges the context-reset hook (see [Context resets](#context-resets)) into `~/.claude/settings.json`. It is idempotent, keeps all existing settings, and saves the previous file as `settings.json.bak`. Without the flag, `init` only prints the snippet.
|
|
94
|
+
`init --hooks` also merges the context-reset hook (see [Context resets](#context-resets)) and the subagent scope hook (see [Subagents](#subagents)) into `~/.claude/settings.json`. It is idempotent, keeps all existing settings, and saves the previous file as `settings.json.bak`. Without the flag, `init` only prints the snippet.
|
|
95
95
|
|
|
96
96
|
#### `serve`
|
|
97
97
|
|
|
@@ -142,6 +142,24 @@ npx agent-file-stash reset
|
|
|
142
142
|
|
|
143
143
|
Forgets what each session has read, so the next read of every file returns the full content. Stats and stashed versions are kept. It resolves `FILESTASH_DIR` like the server does and exits 0 if no database exists. Meant to be run by a Claude Code hook (see [Context resets](#context-resets)); `--from-hook` reads the hook JSON from stdin, resolves a relative `FILESTASH_DIR` against its `cwd`, and stays silent.
|
|
144
144
|
|
|
145
|
+
#### `hook`
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx agent-file-stash hook subagent-scope
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A Claude Code `PreToolUse` hook, not meant to be run by hand (see [Subagents](#subagents)). It reads the hook JSON from stdin and, when the call comes from a subagent to `read_file` or `read_files`, prints the same arguments plus an `agent` argument so the server tracks that subagent separately. For every other call it prints nothing and exits 0. It never opens the database and never grants or changes permissions.
|
|
152
|
+
|
|
153
|
+
#### `doctor`
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
npx agent-file-stash doctor
|
|
157
|
+
npx agent-file-stash doctor --json
|
|
158
|
+
npx agent-file-stash doctor --check-updates
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Read-only diagnostics that tell you whether the install works and what to fix. It checks the Node version, the stash directory and database (schema version, integrity, permissions, leftover recovery files), the MCP registration in Claude Code, Cursor and OpenCode, the context-reset hook, the subagent scope hook, and the `FILESTASH_*` limits. It never creates, changes or deletes any file or setting (when a running server holds the database, SQLite may refresh the timestamp of its shared-memory index file `stash.db-shm`, as any reader does) and makes no network call unless you pass `--check-updates`, which compares the installed version with `npm view`. Each line is `[ok]`, `[warn]`, `[error]` or `[info]`, followed by a fix hint for warnings and errors; `--json` prints the same results as a JSON array and nothing else. The exit code is 1 when any check reports an error and 0 otherwise.
|
|
162
|
+
|
|
145
163
|
#### `help`
|
|
146
164
|
|
|
147
165
|
```bash
|
|
@@ -195,7 +213,30 @@ The server answers a repeat read of an unchanged file with a short "unchanged" n
|
|
|
195
213
|
}
|
|
196
214
|
```
|
|
197
215
|
|
|
198
|
-
|
|
216
|
+
### Subagents
|
|
217
|
+
|
|
218
|
+
A subagent that uses the MCP server by name shares the parent's server process. Without help, the parent and every subagent share one read tracking: a subagent can be told "unchanged" for a file only the parent has seen, the parent can be told that for a file only a subagent read, and parallel subagents can do it to each other.
|
|
219
|
+
|
|
220
|
+
`init --hooks` also registers a `PreToolUse` hook that fixes this (or paste it into `~/.claude/settings.json`):
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"hooks": {
|
|
225
|
+
"PreToolUse": [
|
|
226
|
+
{
|
|
227
|
+
"matcher": "mcp__(filestash|agent-file-stash)__read_files?",
|
|
228
|
+
"hooks": [{ "type": "command", "command": "npx agent-file-stash hook subagent-scope" }]
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For a call made by a subagent, Claude Code gives the hook the subagent's `agent_id`, and the hook passes it to the tool as the optional `agent` argument. The server then tracks delivered line ranges per `agent` value: a subagent's first read of a file returns real content, a repeat read by the same subagent is "unchanged", and nothing a subagent reads is counted as delivered to the parent or to another subagent. Calls by the main agent carry no `agent_id` and are left untouched. Token accounting (`stash_status`, the read footer) stays per session.
|
|
236
|
+
|
|
237
|
+
The hook only adds an argument. It emits no permission decision, so it never grants or bypasses a permission, and a `permissions.deny` rule still blocks the tool. Without the hook, subagents share the parent's tracking as before, and `force=true` on a subagent's first read of each file is the workaround. `doctor` reports a missing hook.
|
|
238
|
+
|
|
239
|
+
Each session keeps at most 32 `agent` scopes; a read from a new scope beyond that deletes the tracking of the least recently used one, which then gets real content on its next read. Scoped tracking is removed together with its session when that session closes. SDK users pass `scope` to `readFile` and `readFileFull`; a value that is not 1-64 characters of `A-Za-z0-9_.-` is ignored, and session ids must not contain `::`.
|
|
199
240
|
|
|
200
241
|
### How sessions work
|
|
201
242
|
|
|
@@ -216,7 +257,7 @@ When a server starts, it prunes closed sessions: any other session whose process
|
|
|
216
257
|
### Known limitations
|
|
217
258
|
|
|
218
259
|
- Clients do not send a per-conversation id, so the server cannot tell that you ran `/clear` or `/compact`. Without the [reset hook](#context-resets) it can answer "unchanged" for content the model no longer has; `force=true` always returns full content.
|
|
219
|
-
- Subagents
|
|
260
|
+
- Subagents only get their own read tracking when the [subagent hook](#subagents) is installed; without it they share the parent's.
|
|
220
261
|
- The secret denylist matches on file names only: symlinks are not resolved, so a link with an innocent name pointing at a secret file is stashed, and secrets in files with ordinary names are not detected.
|
|
221
262
|
- Extra patterns in `FILESTASH_EXCLUDE` match basenames only, not directories or full paths.
|
|
222
263
|
- Node.js 24 or later is required (`node:sqlite`).
|
|
@@ -228,6 +269,7 @@ The stash never prevents a file from being read. If the database or the stash di
|
|
|
228
269
|
|
|
229
270
|
- A database that is corrupt (not a database, malformed) is treated as a disposable cache: it is renamed to `stash.db.corrupt-<unix-ms>` next to the original (with any `-wal`/`-shm` files given the same suffix), a fresh database is created, and one line is written to stderr. `stash_status` shows `Recovered: corrupt database moved to <path>`. The renamed file can be deleted. Recovery is attempted once per process and is serialised across processes with a `stash.db.recover.lock` file next to the database, so servers starting together on the same corrupt file recover it once.
|
|
230
271
|
- If the stash directory cannot be created or written, or the database cannot be opened or locked, or any database call fails during a session, the stash switches to degraded mode for the rest of the process: reads return the plain file content, nothing is stashed, and one line is written to stderr with the reason. `stash_status` starts with `Mode: DEGRADED (<reason>) - files are read normally, nothing is stashed`, its metadata carries `degraded` and `degradedReason`, and `stash_clear` reports that there is nothing to clear.
|
|
272
|
+
- A database created by a newer release (its `user_version` is higher than this release knows) is not treated as corrupt and is left untouched: the stash degrades with `database schema version N is newer than this release supports (M); upgrade agent-file-stash`.
|
|
231
273
|
- Errors about the file being read (missing, unreadable, a directory) are still reported as errors.
|
|
232
274
|
- If file watching cannot start (for example the OS watcher limit is reached) or fails later, one line is written to stderr and the server continues without it.
|
|
233
275
|
- `status` and `reset` print a one-line error and exit 1 on an unreadable database (`reset --from-hook` exits 0); `status --all` skips it and continues.
|
|
@@ -355,24 +397,27 @@ _Run `pnpm benchmark` to reproduce._
|
|
|
355
397
|
packages/
|
|
356
398
|
├── sdk/src/
|
|
357
399
|
│ ├── index.ts Exports: createStash, StashStore, FileWatcher, computeDiff, isExcludedPath, types
|
|
400
|
+
│ ├── migrations.ts SCHEMA_VERSION and the ordered, transactional schema migrations
|
|
358
401
|
│ ├── stash.ts StashStore — SQLite-backed content-addressed stash with per-session read tracking and pruning
|
|
359
|
-
│ ├── differ.ts computeDiff — line-based
|
|
402
|
+
│ ├── differ.ts computeDiff — line-based Myers diff (unified format, edit distance capped at 2 500 lines)
|
|
360
403
|
│ ├── exclude.ts isExcludedPath — secret-file denylist and FILESTASH_EXCLUDE patterns
|
|
361
404
|
│ ├── watcher.ts FileWatcher — debounced fs.watch wrapper that evicts deleted files from the stash
|
|
362
405
|
│ └── types.ts StashConfig, FileReadResult, StashStats type definitions
|
|
363
406
|
│
|
|
364
407
|
└── cli/src/
|
|
365
408
|
├── index.ts CLI entry point — init, serve, status, reset, help commands
|
|
409
|
+
├── hook.ts The hook subagent-scope command (PreToolUse hook for per-subagent read tracking)
|
|
366
410
|
├── mcp.ts MCP server — registers read_file, read_files, stash_status, stash_clear tools
|
|
367
411
|
└── scan.ts findStashDatabases — locates stash databases for status --all
|
|
368
412
|
|
|
369
413
|
test/
|
|
370
414
|
├── smoke.test.ts End-to-end flows: first read, stash hit, diff on change, partial reads, multi-session isolation
|
|
371
|
-
├── differ.test.ts Unit tests for computeDiff: add/remove/mixed edits, context lines,
|
|
415
|
+
├── differ.test.ts Unit tests for computeDiff: add/remove/mixed edits, context lines, edit distance cap
|
|
372
416
|
├── stash-errors.test.ts Error paths: missing file, clear(), onFileDeleted(), post-close re-init
|
|
373
417
|
├── watcher.test.ts FileWatcher: deletion detection, debounce coalescence, close() cancellation
|
|
374
418
|
├── mcp-tools.test.ts Unit tests for isPathAllowed (path traversal guard) and formatReadResult
|
|
375
419
|
├── mcp-meta.test.ts Validates the _meta field format and reverse-DNS namespace convention
|
|
420
|
+
├── differ-myers.test.ts computeDiff against a reference LCS, patch round-trips, edge cases, edit distance cap, speed bounds
|
|
376
421
|
├── diff-guard.test.ts Full content is returned when a diff is not smaller than the file
|
|
377
422
|
├── prune.test.ts Pruning of closed sessions and their data
|
|
378
423
|
├── scan.test.ts findStashDatabases (status --all)
|
|
@@ -380,6 +425,7 @@ test/
|
|
|
380
425
|
├── session-reset.test.ts resetReads, the reset command, init --hooks, MCP integration
|
|
381
426
|
├── savings-regression.test.ts Session accounting identity, savings workload, tool definition overhead
|
|
382
427
|
├── e2e.test.ts End-to-end suite with real servers: concurrent servers, crash safety, secrets, path restriction, shutdown
|
|
428
|
+
├── subagent-scope.test.ts Per-agent read tracking in the SDK, the MCP server, the hook command, init --hooks and doctor
|
|
383
429
|
├── docs.test.ts README mentions every CLI command, FILESTASH_* variable and MCP tool
|
|
384
430
|
└── benchmark.ts Reproducible two-pass simulation across generated TypeScript files (pnpm benchmark)
|
|
385
431
|
```
|
|
@@ -405,11 +451,13 @@ The SDK has no external dependencies — it uses only Node.js built-ins (`node:s
|
|
|
405
451
|
|
|
406
452
|
WAL mode is enabled with a 5-second busy timeout so several servers can share one database and readers do not block the writer.
|
|
407
453
|
|
|
454
|
+
**Schema versioning:** the schema version is stored in SQLite's `PRAGMA user_version` (currently 1; a database with version 0 is a legacy one created before versioning and is completed with any missing table or index). On open, each pending migration runs in its own `BEGIN IMMEDIATE` transaction that also bumps `user_version`, and servers opening the same database at once re-read the version inside the transaction, so each migration is applied once. Migrations are forward-only and additive when possible; there is no downgrade. A database whose version is newer than the release supports is never modified: the server enters [degraded mode](#degraded-mode) with `database schema version N is newer than this release supports (M); upgrade agent-file-stash`.
|
|
455
|
+
|
|
408
456
|
**Pruning:** on startup each server deletes sessions whose pid is no longer alive, their read pointers, delivered ranges and counters, and any `file_versions` row no remaining session points at. Rows for paths matching the secret denylist are removed at the same time. See [How sessions work](#how-sessions-work).
|
|
409
457
|
|
|
410
458
|
**Change detection:** On every read, the current file content is hashed (SHA-256, truncated to 16 hex chars). Same hash = unchanged. Different hash = compute diff, update stash. No polling or watchers required for correctness — the hash is the source of truth. File watchers are optional and only used to proactively evict deleted files.
|
|
411
459
|
|
|
412
|
-
**Diff algorithm:** Line-based unified diff (`computeDiff`).
|
|
460
|
+
**Diff algorithm:** Line-based unified diff (`computeDiff`). The common leading and trailing lines are trimmed and the rest is diffed with Myers' O(ND) algorithm, so the cost depends on how much changed rather than on file size. Changed lines are grouped into hunks in unified format with 3 lines of context. The diff is returned verbatim to the agent, unless it is not smaller than the file, in which case the full content is returned. When the edit distance exceeds 2 500 changed lines (or the work budget is spent), the differing middle is reported as fully removed and re-added, which in practice makes the guard return the full file; this keeps the diff within a fixed time bound on the single-threaded server.
|
|
413
461
|
|
|
414
462
|
**Token estimation:** `ceil(characters / 4)`. Rough but directionally correct for code. Used for the token metrics and to decide whether a label or diff is actually shorter than the plain content; it never changes what the file contains.
|
|
415
463
|
|
|
@@ -429,7 +477,7 @@ Delete the `"filestash"` key (the one `init` adds; also `"agent-file-stash"` if
|
|
|
429
477
|
|
|
430
478
|
**2. Remove the Claude Code hook**
|
|
431
479
|
|
|
432
|
-
If you ran `init --hooks` (or pasted the snippet from [Context resets](#context-resets)), remove the `SessionStart` entry whose command is `npx agent-file-stash reset --from-hook` from `~/.claude/settings.json`. `init --hooks` also left a backup of the previous file at `~/.claude/settings.json.bak`, which you can delete.
|
|
480
|
+
If you ran `init --hooks` (or pasted the snippet from [Context resets](#context-resets)), remove the `SessionStart` entry whose command is `npx agent-file-stash reset --from-hook` and the `PreToolUse` entry whose command is `npx agent-file-stash hook subagent-scope` from `~/.claude/settings.json`. `init --hooks` also left a backup of the previous file at `~/.claude/settings.json.bak`, which you can delete.
|
|
433
481
|
|
|
434
482
|
**3. Remove the stash database**
|
|
435
483
|
|