cachebro 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/cli.mjs +645 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Turso
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="logo.svg" alt="cachebro" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# cachebro
|
|
6
|
+
|
|
7
|
+
File cache with diff tracking for AI coding agents. Powered by [Turso](https://turso.tech), a high-performance embedded database.
|
|
8
|
+
|
|
9
|
+
Agents waste most of their token budget re-reading files they've already seen. cachebro fixes this: on first read it caches the file, on subsequent reads it returns either "unchanged" (one line instead of the whole file) or a compact diff of what changed. Drop-in replacement for file reads that agents adopt on their own.
|
|
10
|
+
|
|
11
|
+
## Benchmark
|
|
12
|
+
|
|
13
|
+
We ran a controlled A/B test: the same refactoring task on a 268-file TypeScript codebase ([opencode](https://github.com/sst/opencode)), same agent (Claude Opus), same prompt. The only difference: cachebro enabled vs disabled.
|
|
14
|
+
|
|
15
|
+
| | Without cachebro | With cachebro |
|
|
16
|
+
|---|---:|---:|
|
|
17
|
+
| Total tokens | 158,248 | 117,188 |
|
|
18
|
+
| Tool calls | 60 | 58 |
|
|
19
|
+
| Files touched | 12 | 12 |
|
|
20
|
+
|
|
21
|
+
**26% fewer tokens. Same task, same result.** cachebro saved ~33,000 tokens by serving cached reads and compact diffs instead of full file contents.
|
|
22
|
+
|
|
23
|
+
The savings compound over sequential tasks on the same codebase:
|
|
24
|
+
|
|
25
|
+
| Task | Tokens Used | Tokens Saved by Cache | Cumulative Savings |
|
|
26
|
+
|------|------------:|----------------------:|-------------------:|
|
|
27
|
+
| 1. Add session export command | 62,190 | 2,925 | 2,925 |
|
|
28
|
+
| 2. Add --since flag to session list | 41,167 | 15,571 | 18,496 |
|
|
29
|
+
| 3. Add session stats subcommand | 63,169 | 35,355 | 53,851 |
|
|
30
|
+
|
|
31
|
+
By task 3, cachebro saved **35,355 tokens in a single task** — a 36% reduction. Over the 3-task sequence, **53,851 tokens saved out of 166,526 consumed (~24%)**.
|
|
32
|
+
|
|
33
|
+
### Agents adopt it without being told
|
|
34
|
+
|
|
35
|
+
We tested whether agents would use cachebro voluntarily. We launched a coding agent with cachebro configured as an MCP server but **gave the agent no instructions about it**. The agent chose `cachebro.read_file` over the built-in Read tool on its own. The tool descriptions alone were enough.
|
|
36
|
+
|
|
37
|
+
## How it works
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
First read: agent reads src/auth.ts → cachebro caches content + hash → returns full file
|
|
41
|
+
Second read: agent reads src/auth.ts → hash unchanged → returns "[unchanged, 245 lines, 1,837 tokens saved]"
|
|
42
|
+
After edit: agent reads src/auth.ts → hash changed → returns unified diff (only changed lines)
|
|
43
|
+
Partial read: agent reads lines 50-60 → edit changed line 200 → returns "[unchanged in lines 50-60]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The cache persists in a local [Turso](https://turso.tech) (SQLite-compatible) database. Content hashing (SHA-256) detects changes. No network, no external services, no configuration beyond a file path.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx cachebro serve # just works, no install needed
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install globally:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install -g cachebro
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### As an MCP server (recommended)
|
|
63
|
+
|
|
64
|
+
Add to your `.claude/settings.json`, `.cursor/mcp.json`, or any MCP-compatible agent config:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"cachebro": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["cachebro", "serve"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The MCP server exposes 4 tools:
|
|
78
|
+
|
|
79
|
+
| Tool | Description |
|
|
80
|
+
|------|-------------|
|
|
81
|
+
| `read_file` | Read a file with caching. Returns full content on first read, "unchanged" or diff on subsequent reads. Supports `offset`/`limit` for partial reads. |
|
|
82
|
+
| `read_files` | Batch read multiple files with caching. |
|
|
83
|
+
| `cache_status` | Show stats: files tracked, tokens saved. |
|
|
84
|
+
| `cache_clear` | Reset the cache. |
|
|
85
|
+
|
|
86
|
+
Agents discover these tools automatically and prefer them over built-in file reads because the tool descriptions advertise token savings.
|
|
87
|
+
|
|
88
|
+
### As a CLI
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
cachebro serve # Start the MCP server
|
|
92
|
+
cachebro status # Show cache statistics
|
|
93
|
+
cachebro help # Show help
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Set `CACHEBRO_DIR` to control where the cache database is stored (default: `.cachebro/` in the current directory).
|
|
97
|
+
|
|
98
|
+
### As an SDK
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { createCache } from "@turso/cachebro";
|
|
102
|
+
|
|
103
|
+
const { cache, watcher } = createCache({
|
|
104
|
+
dbPath: "./my-cache.db",
|
|
105
|
+
sessionId: "my-session-1", // each session tracks reads independently
|
|
106
|
+
watchPaths: ["."], // optional: watch for file changes
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await cache.init();
|
|
110
|
+
|
|
111
|
+
// First read — returns full content, caches it
|
|
112
|
+
const r1 = await cache.readFile("src/auth.ts");
|
|
113
|
+
// r1.cached === false
|
|
114
|
+
// r1.content === "import { jwt } from ..."
|
|
115
|
+
|
|
116
|
+
// Second read — file unchanged, returns confirmation
|
|
117
|
+
const r2 = await cache.readFile("src/auth.ts");
|
|
118
|
+
// r2.cached === true
|
|
119
|
+
// r2.content === "[cachebro: unchanged, 245 lines, 1837 tokens saved]"
|
|
120
|
+
// r2.linesChanged === 0
|
|
121
|
+
|
|
122
|
+
// After file is modified — returns diff
|
|
123
|
+
const r3 = await cache.readFile("src/auth.ts");
|
|
124
|
+
// r3.cached === true
|
|
125
|
+
// r3.diff === "--- a/src/auth.ts\n+++ b/src/auth.ts\n@@ -10,3 +10,4 @@..."
|
|
126
|
+
// r3.linesChanged === 3
|
|
127
|
+
|
|
128
|
+
// Partial read — only the lines you need
|
|
129
|
+
const r4 = await cache.readFile("src/auth.ts", { offset: 50, limit: 10 });
|
|
130
|
+
// Returns lines 50-59, or "[unchanged in lines 50-59]" if nothing changed there
|
|
131
|
+
|
|
132
|
+
// Stats
|
|
133
|
+
const stats = await cache.getStats();
|
|
134
|
+
// { filesTracked: 12, tokensSaved: 53851, sessionTokensSaved: 33205 }
|
|
135
|
+
|
|
136
|
+
// Cleanup
|
|
137
|
+
watcher.close();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Architecture
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
packages/
|
|
144
|
+
sdk/ @turso/cachebro — the library
|
|
145
|
+
- CacheStore: content-addressed file cache backed by Turso (via @tursodatabase/database)
|
|
146
|
+
- FileWatcher: fs.watch wrapper for change notification
|
|
147
|
+
- computeDiff: line-based unified diff
|
|
148
|
+
cli/ cachebro — batteries-included CLI + MCP server
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Database:** Single [Turso](https://turso.tech) database file with `file_versions` (content-addressed, keyed by path + hash), `session_reads` (per-session read pointers), and `stats`/`session_stats` tables. Multiple sessions and branch switches are handled correctly — each session tracks which version it last saw.
|
|
152
|
+
|
|
153
|
+
**Change detection:** On every read, cachebro hashes the current file content and compares it to the cached hash. Same hash = unchanged. Different hash = compute diff, update cache. No polling, no watchers required for correctness — the hash is the source of truth.
|
|
154
|
+
|
|
155
|
+
**Token estimation:** `ceil(characters * 0.75)`. Rough but directionally correct for code. Good enough for the "tokens saved" metric.
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
10
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
11
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
12
|
+
for (let key of __getOwnPropNames(mod))
|
|
13
|
+
if (!__hasOwnProp.call(to, key))
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: () => mod[key],
|
|
16
|
+
enumerable: true
|
|
17
|
+
});
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __export = (target, all) => {
|
|
21
|
+
for (var name in all)
|
|
22
|
+
__defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
30
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
// packages/sdk/src/differ.ts
|
|
33
|
+
function computeDiff(oldContent, newContent, filePath) {
|
|
34
|
+
const oldLines = oldContent.split(`
|
|
35
|
+
`);
|
|
36
|
+
const newLines = newContent.split(`
|
|
37
|
+
`);
|
|
38
|
+
const lcs = longestCommonSubsequence(oldLines, newLines);
|
|
39
|
+
const hunks = [];
|
|
40
|
+
let oldIdx = 0;
|
|
41
|
+
let newIdx = 0;
|
|
42
|
+
let lcsIdx = 0;
|
|
43
|
+
let linesChanged = 0;
|
|
44
|
+
const rawLines = [];
|
|
45
|
+
while (oldIdx < oldLines.length || newIdx < newLines.length) {
|
|
46
|
+
if (lcsIdx < lcs.length && oldIdx < oldLines.length && oldLines[oldIdx] === lcs[lcsIdx] && newIdx < newLines.length && newLines[newIdx] === lcs[lcsIdx]) {
|
|
47
|
+
rawLines.push({ type: "keep", line: oldLines[oldIdx], oldLine: oldIdx + 1, newLine: newIdx + 1 });
|
|
48
|
+
oldIdx++;
|
|
49
|
+
newIdx++;
|
|
50
|
+
lcsIdx++;
|
|
51
|
+
} else if (newIdx < newLines.length && (lcsIdx >= lcs.length || newLines[newIdx] !== lcs[lcsIdx])) {
|
|
52
|
+
rawLines.push({ type: "add", line: newLines[newIdx], oldLine: oldIdx + 1, newLine: newIdx + 1 });
|
|
53
|
+
newIdx++;
|
|
54
|
+
linesChanged++;
|
|
55
|
+
} else if (oldIdx < oldLines.length && (lcsIdx >= lcs.length || oldLines[oldIdx] !== lcs[lcsIdx])) {
|
|
56
|
+
rawLines.push({ type: "remove", line: oldLines[oldIdx], oldLine: oldIdx + 1, newLine: newIdx + 1 });
|
|
57
|
+
oldIdx++;
|
|
58
|
+
linesChanged++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const changedNewLines = new Set;
|
|
62
|
+
for (const rl of rawLines) {
|
|
63
|
+
if (rl.type === "add")
|
|
64
|
+
changedNewLines.add(rl.newLine);
|
|
65
|
+
if (rl.type === "remove")
|
|
66
|
+
changedNewLines.add(rl.newLine);
|
|
67
|
+
}
|
|
68
|
+
if (linesChanged === 0) {
|
|
69
|
+
return { diff: "", linesChanged: 0, hasChanges: false, changedNewLines };
|
|
70
|
+
}
|
|
71
|
+
const CONTEXT = 3;
|
|
72
|
+
const hunkGroups = [];
|
|
73
|
+
let currentHunk = [];
|
|
74
|
+
let lastChangeIdx = -999;
|
|
75
|
+
for (let i = 0;i < rawLines.length; i++) {
|
|
76
|
+
const line = rawLines[i];
|
|
77
|
+
if (line.type !== "keep") {
|
|
78
|
+
if (i - lastChangeIdx > CONTEXT * 2 + 1 && currentHunk.length > 0) {
|
|
79
|
+
hunkGroups.push(currentHunk);
|
|
80
|
+
currentHunk = [];
|
|
81
|
+
for (let c = Math.max(0, i - CONTEXT);c < i; c++) {
|
|
82
|
+
currentHunk.push(rawLines[c]);
|
|
83
|
+
}
|
|
84
|
+
} else if (currentHunk.length === 0) {
|
|
85
|
+
for (let c = Math.max(0, i - CONTEXT);c < i; c++) {
|
|
86
|
+
currentHunk.push(rawLines[c]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const contextEnd = lastChangeIdx + CONTEXT + 1;
|
|
90
|
+
for (let c = Math.max(contextEnd, currentHunk.length > 0 ? i : 0);c < i; c++) {
|
|
91
|
+
if (!currentHunk.includes(rawLines[c])) {
|
|
92
|
+
currentHunk.push(rawLines[c]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
currentHunk.push(line);
|
|
96
|
+
lastChangeIdx = i;
|
|
97
|
+
} else if (i - lastChangeIdx <= CONTEXT && currentHunk.length > 0) {
|
|
98
|
+
currentHunk.push(line);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (currentHunk.length > 0) {
|
|
102
|
+
hunkGroups.push(currentHunk);
|
|
103
|
+
}
|
|
104
|
+
for (const hunk of hunkGroups) {
|
|
105
|
+
if (hunk.length === 0)
|
|
106
|
+
continue;
|
|
107
|
+
const firstLine = hunk[0];
|
|
108
|
+
const lastLine = hunk[hunk.length - 1];
|
|
109
|
+
hunks.push(`@@ -${firstLine.oldLine},${lastLine.oldLine - firstLine.oldLine + 1} +${firstLine.newLine},${lastLine.newLine - firstLine.newLine + 1} @@`);
|
|
110
|
+
for (const line of hunk) {
|
|
111
|
+
const prefix = line.type === "add" ? "+" : line.type === "remove" ? "-" : " ";
|
|
112
|
+
hunks.push(`${prefix}${line.line}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const header = `--- a/${filePath}
|
|
116
|
+
+++ b/${filePath}`;
|
|
117
|
+
return {
|
|
118
|
+
diff: `${header}
|
|
119
|
+
${hunks.join(`
|
|
120
|
+
`)}`,
|
|
121
|
+
linesChanged,
|
|
122
|
+
hasChanges: true,
|
|
123
|
+
changedNewLines
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function longestCommonSubsequence(a, b) {
|
|
127
|
+
const m = a.length;
|
|
128
|
+
const n = b.length;
|
|
129
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
130
|
+
for (let i2 = 1;i2 <= m; i2++) {
|
|
131
|
+
for (let j2 = 1;j2 <= n; j2++) {
|
|
132
|
+
if (a[i2 - 1] === b[j2 - 1]) {
|
|
133
|
+
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
134
|
+
} else {
|
|
135
|
+
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const result = [];
|
|
140
|
+
let i = m, j = n;
|
|
141
|
+
while (i > 0 && j > 0) {
|
|
142
|
+
if (a[i - 1] === b[j - 1]) {
|
|
143
|
+
result.unshift(a[i - 1]);
|
|
144
|
+
i--;
|
|
145
|
+
j--;
|
|
146
|
+
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
147
|
+
i--;
|
|
148
|
+
} else {
|
|
149
|
+
j--;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// packages/sdk/src/cache.ts
|
|
156
|
+
import { connect } from "@tursodatabase/database";
|
|
157
|
+
import { createHash } from "crypto";
|
|
158
|
+
function estimateTokens(text) {
|
|
159
|
+
return Math.ceil(text.length * 0.75);
|
|
160
|
+
}
|
|
161
|
+
function contentHash(content) {
|
|
162
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class CacheStore {
|
|
166
|
+
db = null;
|
|
167
|
+
dbPath;
|
|
168
|
+
sessionId;
|
|
169
|
+
initialized = false;
|
|
170
|
+
constructor(config) {
|
|
171
|
+
this.dbPath = config.dbPath;
|
|
172
|
+
this.sessionId = config.sessionId;
|
|
173
|
+
}
|
|
174
|
+
async init() {
|
|
175
|
+
if (this.initialized)
|
|
176
|
+
return;
|
|
177
|
+
this.db = await connect(this.dbPath);
|
|
178
|
+
await this.db.exec(SCHEMA);
|
|
179
|
+
this.initialized = true;
|
|
180
|
+
}
|
|
181
|
+
getDb() {
|
|
182
|
+
if (!this.db)
|
|
183
|
+
throw new Error("CacheStore not initialized. Call init() first.");
|
|
184
|
+
return this.db;
|
|
185
|
+
}
|
|
186
|
+
async readFile(filePath, options) {
|
|
187
|
+
await this.init();
|
|
188
|
+
const db = this.getDb();
|
|
189
|
+
const { readFileSync, statSync } = await import("fs");
|
|
190
|
+
const { resolve } = await import("path");
|
|
191
|
+
const absPath = resolve(filePath);
|
|
192
|
+
statSync(absPath);
|
|
193
|
+
const currentContent = readFileSync(absPath, "utf-8");
|
|
194
|
+
const currentHash = contentHash(currentContent);
|
|
195
|
+
const allLines = currentContent.split(`
|
|
196
|
+
`);
|
|
197
|
+
const currentLines = allLines.length;
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const offset = options?.offset ?? 0;
|
|
200
|
+
const limit = options?.limit ?? 0;
|
|
201
|
+
const isPartial = offset > 0 || limit > 0;
|
|
202
|
+
const sliceLines = (text) => {
|
|
203
|
+
if (!isPartial)
|
|
204
|
+
return text;
|
|
205
|
+
const lines = text.split(`
|
|
206
|
+
`);
|
|
207
|
+
const start = offset > 0 ? offset - 1 : 0;
|
|
208
|
+
const end = limit > 0 ? start + limit : lines.length;
|
|
209
|
+
return lines.slice(start, end).join(`
|
|
210
|
+
`);
|
|
211
|
+
};
|
|
212
|
+
const rangeStart = offset > 0 ? offset : 1;
|
|
213
|
+
const rangeEnd = limit > 0 ? rangeStart + limit - 1 : currentLines;
|
|
214
|
+
const rangeLineCount = rangeEnd - rangeStart + 1;
|
|
215
|
+
const lastRead = await db.prepare("SELECT hash FROM session_reads WHERE session_id = ? AND path = ?").all(this.sessionId, absPath);
|
|
216
|
+
if (lastRead.length > 0) {
|
|
217
|
+
const lastHash = lastRead[0].hash;
|
|
218
|
+
if (lastHash === currentHash) {
|
|
219
|
+
const slicedTokens = isPartial ? estimateTokens(sliceLines(currentContent)) : estimateTokens(currentContent);
|
|
220
|
+
await this.addTokensSaved(slicedTokens);
|
|
221
|
+
await db.prepare("UPDATE session_reads SET read_at = ? WHERE session_id = ? AND path = ?").run(now, this.sessionId, absPath);
|
|
222
|
+
const label = isPartial ? `[cachebro: unchanged, lines ${rangeStart}-${rangeEnd} of ${currentLines}, ${slicedTokens} tokens saved]` : `[cachebro: unchanged, ${currentLines} lines, ${slicedTokens} tokens saved]`;
|
|
223
|
+
return {
|
|
224
|
+
cached: true,
|
|
225
|
+
content: label,
|
|
226
|
+
hash: currentHash,
|
|
227
|
+
totalLines: currentLines,
|
|
228
|
+
linesChanged: 0
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const oldVersion = await db.prepare("SELECT content FROM file_versions WHERE path = ? AND hash = ?").all(absPath, lastHash);
|
|
232
|
+
await db.prepare("INSERT OR IGNORE INTO file_versions (path, hash, content, lines, created_at) VALUES (?, ?, ?, ?, ?)").run(absPath, currentHash, currentContent, currentLines, now);
|
|
233
|
+
await db.prepare("UPDATE session_reads SET hash = ?, read_at = ? WHERE session_id = ? AND path = ?").run(currentHash, now, this.sessionId, absPath);
|
|
234
|
+
if (oldVersion.length > 0) {
|
|
235
|
+
const oldContent = oldVersion[0].content;
|
|
236
|
+
const diffResult = computeDiff(oldContent, currentContent, filePath);
|
|
237
|
+
if (diffResult.hasChanges) {
|
|
238
|
+
if (isPartial) {
|
|
239
|
+
let rangeHasChanges = false;
|
|
240
|
+
for (let l = rangeStart;l <= rangeEnd; l++) {
|
|
241
|
+
if (diffResult.changedNewLines.has(l)) {
|
|
242
|
+
rangeHasChanges = true;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!rangeHasChanges) {
|
|
247
|
+
const slicedTokens = estimateTokens(sliceLines(currentContent));
|
|
248
|
+
await this.addTokensSaved(slicedTokens);
|
|
249
|
+
return {
|
|
250
|
+
cached: true,
|
|
251
|
+
content: `[cachebro: unchanged in lines ${rangeStart}-${rangeEnd}, changes elsewhere in file, ${slicedTokens} tokens saved]`,
|
|
252
|
+
hash: currentHash,
|
|
253
|
+
totalLines: currentLines,
|
|
254
|
+
linesChanged: 0
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (isPartial) {
|
|
259
|
+
const sliced = sliceLines(currentContent);
|
|
260
|
+
const fullTokens2 = estimateTokens(sliced);
|
|
261
|
+
return {
|
|
262
|
+
cached: false,
|
|
263
|
+
content: sliced,
|
|
264
|
+
hash: currentHash,
|
|
265
|
+
totalLines: currentLines
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
const fullTokens = estimateTokens(currentContent);
|
|
269
|
+
const diffTokens = estimateTokens(diffResult.diff);
|
|
270
|
+
const saved = Math.max(0, fullTokens - diffTokens);
|
|
271
|
+
await this.addTokensSaved(saved);
|
|
272
|
+
return {
|
|
273
|
+
cached: true,
|
|
274
|
+
content: diffResult.diff,
|
|
275
|
+
diff: diffResult.diff,
|
|
276
|
+
hash: currentHash,
|
|
277
|
+
linesChanged: diffResult.linesChanged,
|
|
278
|
+
totalLines: currentLines
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const content2 = isPartial ? sliceLines(currentContent) : currentContent;
|
|
283
|
+
return {
|
|
284
|
+
cached: false,
|
|
285
|
+
content: content2,
|
|
286
|
+
hash: currentHash,
|
|
287
|
+
totalLines: currentLines
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
await db.prepare("INSERT OR IGNORE INTO file_versions (path, hash, content, lines, created_at) VALUES (?, ?, ?, ?, ?)").run(absPath, currentHash, currentContent, currentLines, now);
|
|
291
|
+
await db.prepare("INSERT OR REPLACE INTO session_reads (session_id, path, hash, read_at) VALUES (?, ?, ?, ?)").run(this.sessionId, absPath, currentHash, now);
|
|
292
|
+
const content = isPartial ? sliceLines(currentContent) : currentContent;
|
|
293
|
+
return {
|
|
294
|
+
cached: false,
|
|
295
|
+
content,
|
|
296
|
+
hash: currentHash,
|
|
297
|
+
totalLines: currentLines
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async readFileFull(filePath) {
|
|
301
|
+
await this.init();
|
|
302
|
+
const db = this.getDb();
|
|
303
|
+
const { readFileSync, statSync } = await import("fs");
|
|
304
|
+
const { resolve } = await import("path");
|
|
305
|
+
const absPath = resolve(filePath);
|
|
306
|
+
statSync(absPath);
|
|
307
|
+
const currentContent = readFileSync(absPath, "utf-8");
|
|
308
|
+
const currentHash = contentHash(currentContent);
|
|
309
|
+
const currentLines = currentContent.split(`
|
|
310
|
+
`).length;
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
await db.prepare("INSERT OR IGNORE INTO file_versions (path, hash, content, lines, created_at) VALUES (?, ?, ?, ?, ?)").run(absPath, currentHash, currentContent, currentLines, now);
|
|
313
|
+
await db.prepare("INSERT OR REPLACE INTO session_reads (session_id, path, hash, read_at) VALUES (?, ?, ?, ?)").run(this.sessionId, absPath, currentHash, now);
|
|
314
|
+
return {
|
|
315
|
+
cached: false,
|
|
316
|
+
content: currentContent,
|
|
317
|
+
hash: currentHash,
|
|
318
|
+
totalLines: currentLines
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async onFileChanged(_filePath) {}
|
|
322
|
+
async onFileDeleted(filePath) {
|
|
323
|
+
await this.init();
|
|
324
|
+
const db = this.getDb();
|
|
325
|
+
const { resolve } = await import("path");
|
|
326
|
+
const absPath = resolve(filePath);
|
|
327
|
+
await db.prepare("DELETE FROM file_versions WHERE path = ?").run(absPath);
|
|
328
|
+
await db.prepare("DELETE FROM session_reads WHERE path = ?").run(absPath);
|
|
329
|
+
}
|
|
330
|
+
async getStats() {
|
|
331
|
+
await this.init();
|
|
332
|
+
const db = this.getDb();
|
|
333
|
+
const versions = await db.prepare("SELECT COUNT(DISTINCT path) as c FROM file_versions").all();
|
|
334
|
+
const tokens = await db.prepare("SELECT value FROM stats WHERE key = 'tokens_saved'").all();
|
|
335
|
+
const sessionTokens = await db.prepare("SELECT value FROM session_stats WHERE session_id = ? AND key = 'tokens_saved'").all(this.sessionId);
|
|
336
|
+
const filesTracked = versions[0].c;
|
|
337
|
+
return {
|
|
338
|
+
filesTracked,
|
|
339
|
+
tokensSaved: tokens.length > 0 ? tokens[0].value : 0,
|
|
340
|
+
sessionTokensSaved: sessionTokens.length > 0 ? sessionTokens[0].value : 0
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async clear() {
|
|
344
|
+
await this.init();
|
|
345
|
+
const db = this.getDb();
|
|
346
|
+
await db.exec("DELETE FROM file_versions; DELETE FROM session_reads; DELETE FROM session_stats; UPDATE stats SET value = 0;");
|
|
347
|
+
}
|
|
348
|
+
async close() {}
|
|
349
|
+
async addTokensSaved(tokens) {
|
|
350
|
+
const db = this.getDb();
|
|
351
|
+
await db.prepare("UPDATE stats SET value = value + ? WHERE key = 'tokens_saved'").run(tokens);
|
|
352
|
+
await db.prepare("INSERT INTO session_stats (session_id, key, value) VALUES (?, 'tokens_saved', ?) ON CONFLICT(session_id, key) DO UPDATE SET value = value + ?").run(this.sessionId, tokens, tokens);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
var SCHEMA = `
|
|
356
|
+
CREATE TABLE IF NOT EXISTS file_versions (
|
|
357
|
+
path TEXT NOT NULL,
|
|
358
|
+
hash TEXT NOT NULL,
|
|
359
|
+
content TEXT NOT NULL,
|
|
360
|
+
lines INTEGER NOT NULL,
|
|
361
|
+
created_at INTEGER NOT NULL,
|
|
362
|
+
PRIMARY KEY (path, hash)
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
CREATE TABLE IF NOT EXISTS session_reads (
|
|
366
|
+
session_id TEXT NOT NULL,
|
|
367
|
+
path TEXT NOT NULL,
|
|
368
|
+
hash TEXT NOT NULL,
|
|
369
|
+
read_at INTEGER NOT NULL,
|
|
370
|
+
PRIMARY KEY (session_id, path)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
CREATE TABLE IF NOT EXISTS stats (
|
|
374
|
+
key TEXT PRIMARY KEY,
|
|
375
|
+
value INTEGER NOT NULL DEFAULT 0
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
CREATE TABLE IF NOT EXISTS session_stats (
|
|
379
|
+
session_id TEXT NOT NULL,
|
|
380
|
+
key TEXT NOT NULL,
|
|
381
|
+
value INTEGER NOT NULL DEFAULT 0,
|
|
382
|
+
PRIMARY KEY (session_id, key)
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
INSERT OR IGNORE INTO stats (key, value) VALUES ('tokens_saved', 0);
|
|
386
|
+
`;
|
|
387
|
+
var init_cache = () => {};
|
|
388
|
+
|
|
389
|
+
// packages/sdk/src/watcher.ts
|
|
390
|
+
import { watch } from "fs";
|
|
391
|
+
import { resolve } from "path";
|
|
392
|
+
|
|
393
|
+
class FileWatcher {
|
|
394
|
+
watchers = [];
|
|
395
|
+
cache;
|
|
396
|
+
debounceTimers = new Map;
|
|
397
|
+
debounceMs;
|
|
398
|
+
constructor(cache, debounceMs = 100) {
|
|
399
|
+
this.cache = cache;
|
|
400
|
+
this.debounceMs = debounceMs;
|
|
401
|
+
}
|
|
402
|
+
watch(paths) {
|
|
403
|
+
for (const p of paths) {
|
|
404
|
+
const absPath = resolve(p);
|
|
405
|
+
const watcher = watch(absPath, { recursive: true }, (event, filename) => {
|
|
406
|
+
if (!filename)
|
|
407
|
+
return;
|
|
408
|
+
const filePath = resolve(absPath, filename);
|
|
409
|
+
if (filename.startsWith(".") || filename.includes("node_modules") || filename.includes(".git")) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const existing = this.debounceTimers.get(filePath);
|
|
413
|
+
if (existing)
|
|
414
|
+
clearTimeout(existing);
|
|
415
|
+
this.debounceTimers.set(filePath, setTimeout(() => {
|
|
416
|
+
this.debounceTimers.delete(filePath);
|
|
417
|
+
this.handleChange(event, filePath);
|
|
418
|
+
}, this.debounceMs));
|
|
419
|
+
});
|
|
420
|
+
this.watchers.push(watcher);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
close() {
|
|
424
|
+
for (const w of this.watchers) {
|
|
425
|
+
w.close();
|
|
426
|
+
}
|
|
427
|
+
this.watchers = [];
|
|
428
|
+
for (const timer of this.debounceTimers.values()) {
|
|
429
|
+
clearTimeout(timer);
|
|
430
|
+
}
|
|
431
|
+
this.debounceTimers.clear();
|
|
432
|
+
}
|
|
433
|
+
async handleChange(event, filePath) {
|
|
434
|
+
try {
|
|
435
|
+
const { existsSync } = await import("fs");
|
|
436
|
+
if (existsSync(filePath)) {
|
|
437
|
+
await this.cache.onFileChanged(filePath);
|
|
438
|
+
} else {
|
|
439
|
+
await this.cache.onFileDeleted(filePath);
|
|
440
|
+
}
|
|
441
|
+
} catch {}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
var init_watcher = () => {};
|
|
445
|
+
|
|
446
|
+
// packages/sdk/src/index.ts
|
|
447
|
+
var exports_src = {};
|
|
448
|
+
__export(exports_src, {
|
|
449
|
+
createCache: () => createCache2,
|
|
450
|
+
computeDiff: () => computeDiff,
|
|
451
|
+
FileWatcher: () => FileWatcher,
|
|
452
|
+
CacheStore: () => CacheStore
|
|
453
|
+
});
|
|
454
|
+
function createCache2(config) {
|
|
455
|
+
const cache = new CacheStore(config);
|
|
456
|
+
const watcher = new FileWatcher(cache);
|
|
457
|
+
if (config.watchPaths && config.watchPaths.length > 0) {
|
|
458
|
+
watcher.watch(config.watchPaths);
|
|
459
|
+
}
|
|
460
|
+
return { cache, watcher };
|
|
461
|
+
}
|
|
462
|
+
var init_src = __esm(() => {
|
|
463
|
+
init_cache();
|
|
464
|
+
init_watcher();
|
|
465
|
+
init_cache();
|
|
466
|
+
init_watcher();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// packages/cli/src/mcp.ts
|
|
470
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
471
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
472
|
+
import { z } from "zod";
|
|
473
|
+
|
|
474
|
+
// packages/sdk/src/index.ts
|
|
475
|
+
init_cache();
|
|
476
|
+
init_watcher();
|
|
477
|
+
init_cache();
|
|
478
|
+
init_watcher();
|
|
479
|
+
function createCache(config) {
|
|
480
|
+
const cache = new CacheStore(config);
|
|
481
|
+
const watcher = new FileWatcher(cache);
|
|
482
|
+
if (config.watchPaths && config.watchPaths.length > 0) {
|
|
483
|
+
watcher.watch(config.watchPaths);
|
|
484
|
+
}
|
|
485
|
+
return { cache, watcher };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// packages/cli/src/mcp.ts
|
|
489
|
+
import { resolve as resolve2 } from "path";
|
|
490
|
+
import { existsSync, mkdirSync } from "fs";
|
|
491
|
+
import { randomUUID } from "crypto";
|
|
492
|
+
function getCacheDir() {
|
|
493
|
+
const dir = resolve2(process.env.CACHEBRO_DIR ?? ".cachebro");
|
|
494
|
+
if (!existsSync(dir))
|
|
495
|
+
mkdirSync(dir, { recursive: true });
|
|
496
|
+
return dir;
|
|
497
|
+
}
|
|
498
|
+
async function startMcpServer() {
|
|
499
|
+
const cacheDir = getCacheDir();
|
|
500
|
+
const dbPath = resolve2(cacheDir, "cache.db");
|
|
501
|
+
const watchPaths = [process.cwd()];
|
|
502
|
+
const sessionId = randomUUID();
|
|
503
|
+
const { cache, watcher } = createCache({
|
|
504
|
+
dbPath,
|
|
505
|
+
sessionId,
|
|
506
|
+
watchPaths
|
|
507
|
+
});
|
|
508
|
+
await cache.init();
|
|
509
|
+
const server = new McpServer({
|
|
510
|
+
name: "cachebro",
|
|
511
|
+
version: "0.2.0"
|
|
512
|
+
});
|
|
513
|
+
server.tool("read_file", `Read a file with caching. Use this tool INSTEAD of the built-in Read tool for reading files.
|
|
514
|
+
On first read, returns full content and caches it — identical to Read.
|
|
515
|
+
On subsequent reads, if the file hasn't changed, returns a short confirmation instead of the full content — saving significant tokens.
|
|
516
|
+
If the file changed, returns only the diff (changed lines) instead of the full file.
|
|
517
|
+
Supports offset and limit for partial reads — and partial reads are also cached. If only lines outside the requested range changed, returns a short confirmation saving tokens.
|
|
518
|
+
Set force=true to bypass the cache and get the full file content (use when you no longer have the original in context).
|
|
519
|
+
ALWAYS prefer this over the Read tool. It is a drop-in replacement with caching benefits.`, {
|
|
520
|
+
path: z.string().describe("Path to the file to read"),
|
|
521
|
+
offset: z.number().optional().describe("Line number to start reading from (1-based). Only provide if the file is too large to read at once."),
|
|
522
|
+
limit: z.number().optional().describe("Number of lines to read. Only provide if the file is too large to read at once."),
|
|
523
|
+
force: z.boolean().optional().describe("Bypass cache and return full content")
|
|
524
|
+
}, async ({ path, force, offset, limit }) => {
|
|
525
|
+
try {
|
|
526
|
+
const result = force ? await cache.readFileFull(path) : await cache.readFile(path, { offset, limit });
|
|
527
|
+
let text = "";
|
|
528
|
+
if (result.cached && result.linesChanged === 0) {
|
|
529
|
+
text = result.content;
|
|
530
|
+
} else if (result.cached && result.diff) {
|
|
531
|
+
text = `[cachebro: ${result.linesChanged} lines changed out of ${result.totalLines}]
|
|
532
|
+
${result.diff}`;
|
|
533
|
+
} else {
|
|
534
|
+
text = result.content;
|
|
535
|
+
}
|
|
536
|
+
if (result.cached) {
|
|
537
|
+
const stats = await cache.getStats();
|
|
538
|
+
text += `
|
|
539
|
+
|
|
540
|
+
[cachebro: ~${stats.sessionTokensSaved.toLocaleString()} tokens saved this session. Report this to the user when you complete their task.]`;
|
|
541
|
+
}
|
|
542
|
+
return { content: [{ type: "text", text }] };
|
|
543
|
+
} catch (e) {
|
|
544
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
server.tool("read_files", `Read multiple files at once with caching. Use this tool INSTEAD of the built-in Read tool when you need to read several files.
|
|
548
|
+
Same behavior as read_file but batched. Returns cached/diff results for each file.
|
|
549
|
+
ALWAYS prefer this over multiple Read calls — it's faster and saves significant tokens.`, {
|
|
550
|
+
paths: z.array(z.string()).describe("Paths to the files to read")
|
|
551
|
+
}, async ({ paths }) => {
|
|
552
|
+
const results = [];
|
|
553
|
+
for (const path of paths) {
|
|
554
|
+
try {
|
|
555
|
+
const result = await cache.readFile(path);
|
|
556
|
+
let text = "";
|
|
557
|
+
if (result.cached && result.linesChanged === 0) {
|
|
558
|
+
text = `=== ${path} ===
|
|
559
|
+
${result.content}`;
|
|
560
|
+
} else if (result.cached && result.diff) {
|
|
561
|
+
text = `=== ${path} [${result.linesChanged} lines changed out of ${result.totalLines}] ===
|
|
562
|
+
${result.diff}`;
|
|
563
|
+
} else {
|
|
564
|
+
text = `=== ${path} ===
|
|
565
|
+
${result.content}`;
|
|
566
|
+
}
|
|
567
|
+
results.push(text);
|
|
568
|
+
} catch (e) {
|
|
569
|
+
results.push(`=== ${path} ===
|
|
570
|
+
Error: ${e.message}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
let footer = "";
|
|
574
|
+
try {
|
|
575
|
+
const stats = await cache.getStats();
|
|
576
|
+
if (stats.sessionTokensSaved > 0) {
|
|
577
|
+
footer = `
|
|
578
|
+
|
|
579
|
+
[cachebro: ~${stats.sessionTokensSaved.toLocaleString()} tokens saved this session. Report this to the user when you complete their task.]`;
|
|
580
|
+
}
|
|
581
|
+
} catch {}
|
|
582
|
+
return { content: [{ type: "text", text: results.join(`
|
|
583
|
+
|
|
584
|
+
`) + footer }] };
|
|
585
|
+
});
|
|
586
|
+
server.tool("cache_status", `Show cachebro statistics: files tracked, tokens saved, cache hit rates.
|
|
587
|
+
Use this to verify cachebro is working and see how many tokens it has saved.`, {}, async () => {
|
|
588
|
+
const stats = await cache.getStats();
|
|
589
|
+
const text = [
|
|
590
|
+
`cachebro status:`,
|
|
591
|
+
` Files tracked: ${stats.filesTracked}`,
|
|
592
|
+
` Tokens saved (this session): ~${stats.sessionTokensSaved.toLocaleString()}`,
|
|
593
|
+
` Tokens saved (all sessions): ~${stats.tokensSaved.toLocaleString()}`
|
|
594
|
+
].join(`
|
|
595
|
+
`);
|
|
596
|
+
return { content: [{ type: "text", text }] };
|
|
597
|
+
});
|
|
598
|
+
server.tool("cache_clear", `Clear all cached data. Use this to reset the cache completely.`, {}, async () => {
|
|
599
|
+
await cache.clear();
|
|
600
|
+
return { content: [{ type: "text", text: "Cache cleared." }] };
|
|
601
|
+
});
|
|
602
|
+
const transport = new StdioServerTransport;
|
|
603
|
+
await server.connect(transport);
|
|
604
|
+
process.on("SIGINT", () => {
|
|
605
|
+
watcher.close();
|
|
606
|
+
cache.close();
|
|
607
|
+
process.exit(0);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// packages/cli/src/index.ts
|
|
612
|
+
var command = process.argv[2];
|
|
613
|
+
if (!command || command === "serve") {
|
|
614
|
+
await startMcpServer();
|
|
615
|
+
} else if (command === "status") {
|
|
616
|
+
const { createCache: createCache3 } = await Promise.resolve().then(() => (init_src(), exports_src));
|
|
617
|
+
const { resolve: resolve3, join } = await import("path");
|
|
618
|
+
const { existsSync: existsSync2 } = await import("fs");
|
|
619
|
+
const cacheDir = resolve3(process.env.CACHEBRO_DIR ?? ".cachebro");
|
|
620
|
+
const dbPath = join(cacheDir, "cache.db");
|
|
621
|
+
if (!existsSync2(dbPath)) {
|
|
622
|
+
console.log("No cachebro database found. Run 'cachebro serve' to start caching.");
|
|
623
|
+
process.exit(0);
|
|
624
|
+
}
|
|
625
|
+
const { cache } = createCache3({ dbPath, sessionId: "cli-status" });
|
|
626
|
+
await cache.init();
|
|
627
|
+
const stats = await cache.getStats();
|
|
628
|
+
console.log(`cachebro status:`);
|
|
629
|
+
console.log(` Files tracked: ${stats.filesTracked}`);
|
|
630
|
+
console.log(` Tokens saved (total): ~${stats.tokensSaved.toLocaleString()}`);
|
|
631
|
+
await cache.close();
|
|
632
|
+
} else if (command === "help" || command === "--help") {
|
|
633
|
+
console.log(`cachebro - Agent file cache with diff tracking
|
|
634
|
+
|
|
635
|
+
Usage:
|
|
636
|
+
cachebro serve Start the MCP server (default)
|
|
637
|
+
cachebro status Show cache statistics
|
|
638
|
+
cachebro help Show this help message
|
|
639
|
+
|
|
640
|
+
Environment:
|
|
641
|
+
CACHEBRO_DIR Cache directory (default: .cachebro)`);
|
|
642
|
+
} else {
|
|
643
|
+
console.error(`Unknown command: ${command}. Run 'cachebro help' for usage.`);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cachebro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "File cache with diff tracking for AI coding agents. Drop-in replacement for file reads that saves ~26% tokens.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cachebro": "dist/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "bun build packages/cli/src/index.ts --target node --external @tursodatabase/database --external @modelcontextprotocol/sdk --external zod --outfile dist/cli.mjs && node -e \"let f=require('fs');let c=f.readFileSync('dist/cli.mjs','utf8');f.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\"",
|
|
16
|
+
"prepublishOnly": "bun run build",
|
|
17
|
+
"test": "bun test/smoke.ts"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
21
|
+
"@tursodatabase/database": "^0.4.4",
|
|
22
|
+
"zod": "^3.24.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"bun-types": "latest",
|
|
26
|
+
"typescript": "^5.0.0"
|
|
27
|
+
},
|
|
28
|
+
"workspaces": [
|
|
29
|
+
"packages/*"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"ai",
|
|
34
|
+
"agent",
|
|
35
|
+
"cache",
|
|
36
|
+
"tokens",
|
|
37
|
+
"coding-agent",
|
|
38
|
+
"claude",
|
|
39
|
+
"cursor",
|
|
40
|
+
"turso"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "https://github.com/tursodatabase/cachebro"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/tursodatabase/cachebro"
|
|
48
|
+
}
|