agent-file-stash 0.2.5 → 0.4.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 +16 -0
- package/dist/cli.mjs +113 -11
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -112,6 +112,22 @@ filestash status:
|
|
|
112
112
|
Tokens saved (total): ~53,851
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
When each project keeps its own stash (for example with `FILESTASH_DIR=.vscode/file-stash`), sum all of them with `--all`. It scans the given directory (default: home) for `.file-stash/` and `file-stash/` folders:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx agent-file-stash status --all ~/Projects
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
filestash status (3 databases under /Users/me/Projects):
|
|
123
|
+
~ 252,340 tokens 112 files /Users/me/Projects/api
|
|
124
|
+
~ 45,157 tokens 128 files /Users/me/Projects/web
|
|
125
|
+
~ 7,043 tokens 18 files /Users/me/Projects/legacy
|
|
126
|
+
~ 304,540 tokens 258 files TOTAL
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Savings come from re-reads within a session (unchanged files and diffs). A new session always receives full content, since the file is not in its context yet.
|
|
130
|
+
|
|
115
131
|
#### `help`
|
|
116
132
|
|
|
117
133
|
```bash
|
package/dist/cli.mjs
CHANGED
|
@@ -160,6 +160,11 @@ CREATE TABLE IF NOT EXISTS session_reads (
|
|
|
160
160
|
PRIMARY KEY (session_id, path)
|
|
161
161
|
);
|
|
162
162
|
|
|
163
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
164
|
+
session_id TEXT PRIMARY KEY,
|
|
165
|
+
pid INTEGER NOT NULL
|
|
166
|
+
);
|
|
167
|
+
|
|
163
168
|
CREATE TABLE IF NOT EXISTS stats (
|
|
164
169
|
key TEXT PRIMARY KEY,
|
|
165
170
|
value INTEGER NOT NULL DEFAULT 0
|
|
@@ -181,6 +186,15 @@ var HASH_LENGTH = 16;
|
|
|
181
186
|
function contentHash(content) {
|
|
182
187
|
return createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH);
|
|
183
188
|
}
|
|
189
|
+
function isProcessAlive(pid) {
|
|
190
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
191
|
+
try {
|
|
192
|
+
process.kill(pid, 0);
|
|
193
|
+
return true;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
return err.code === "EPERM";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
184
198
|
function queryOne(db, sql, params = []) {
|
|
185
199
|
const rows = db.prepare(sql).all(...params);
|
|
186
200
|
return rows[0];
|
|
@@ -199,9 +213,35 @@ var StashStore = class {
|
|
|
199
213
|
const { DatabaseSync } = await import("node:sqlite");
|
|
200
214
|
this.db = new DatabaseSync(this.dbPath);
|
|
201
215
|
this.db.exec("PRAGMA journal_mode=WAL");
|
|
216
|
+
this.db.exec("PRAGMA busy_timeout=5000");
|
|
202
217
|
this.db.exec(SCHEMA);
|
|
218
|
+
this.registerSession(this.db);
|
|
219
|
+
this.pruneClosedSessions(this.db);
|
|
203
220
|
this.initialized = true;
|
|
204
221
|
}
|
|
222
|
+
registerSession(db) {
|
|
223
|
+
db.prepare("INSERT OR REPLACE INTO sessions (session_id, pid) VALUES (?, ?)").run(this.sessionId, process.pid);
|
|
224
|
+
}
|
|
225
|
+
pruneClosedSessions(db) {
|
|
226
|
+
db.exec("BEGIN IMMEDIATE");
|
|
227
|
+
try {
|
|
228
|
+
const sessions = db.prepare("SELECT session_id, pid FROM sessions").all();
|
|
229
|
+
for (const s of sessions) {
|
|
230
|
+
if (s.session_id !== this.sessionId && !isProcessAlive(s.pid)) {
|
|
231
|
+
db.prepare("DELETE FROM sessions WHERE session_id = ?").run(s.session_id);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
db.exec("DELETE FROM session_reads WHERE session_id NOT IN (SELECT session_id FROM sessions)");
|
|
235
|
+
db.exec("DELETE FROM session_stats WHERE session_id NOT IN (SELECT session_id FROM sessions)");
|
|
236
|
+
db.exec(
|
|
237
|
+
"DELETE FROM file_versions WHERE NOT EXISTS (SELECT 1 FROM session_reads r WHERE r.path = file_versions.path AND r.hash = file_versions.hash)"
|
|
238
|
+
);
|
|
239
|
+
db.exec("COMMIT");
|
|
240
|
+
} catch (err) {
|
|
241
|
+
db.exec("ROLLBACK");
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
205
245
|
getDb() {
|
|
206
246
|
if (!this.db) throw new Error("StashStore not initialized. Call init() first.");
|
|
207
247
|
return this.db;
|
|
@@ -373,6 +413,7 @@ var StashStore = class {
|
|
|
373
413
|
}
|
|
374
414
|
async close() {
|
|
375
415
|
if (this.db) {
|
|
416
|
+
this.db.prepare("DELETE FROM sessions WHERE session_id = ?").run(this.sessionId);
|
|
376
417
|
this.db.close();
|
|
377
418
|
this.db = null;
|
|
378
419
|
this.initialized = false;
|
|
@@ -456,8 +497,8 @@ function createStash(config) {
|
|
|
456
497
|
}
|
|
457
498
|
|
|
458
499
|
// packages/cli/src/index.ts
|
|
459
|
-
import { resolve as resolve4, join as
|
|
460
|
-
import { existsSync as
|
|
500
|
+
import { resolve as resolve4, join as join3 } from "node:path";
|
|
501
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "node:fs";
|
|
461
502
|
import { homedir } from "node:os";
|
|
462
503
|
|
|
463
504
|
// packages/cli/src/mcp.ts
|
|
@@ -669,6 +710,33 @@ Use this to verify filestash is working and see how many tokens it has saved.`
|
|
|
669
710
|
process.on("SIGTERM", shutdown);
|
|
670
711
|
}
|
|
671
712
|
|
|
713
|
+
// packages/cli/src/scan.ts
|
|
714
|
+
import { existsSync as existsSync3, readdirSync } from "node:fs";
|
|
715
|
+
import { join as join2 } from "node:path";
|
|
716
|
+
var STASH_DIR_NAMES = /* @__PURE__ */ new Set([".file-stash", "file-stash"]);
|
|
717
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "build", "dist", "target", "bin", "obj"]);
|
|
718
|
+
var MAX_SCAN_DEPTH = 8;
|
|
719
|
+
function findStashDatabases(root, depth = 0) {
|
|
720
|
+
if (depth > MAX_SCAN_DEPTH) return [];
|
|
721
|
+
let entries;
|
|
722
|
+
try {
|
|
723
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
724
|
+
} catch {
|
|
725
|
+
return [];
|
|
726
|
+
}
|
|
727
|
+
const found = [];
|
|
728
|
+
for (const entry of entries) {
|
|
729
|
+
if (!entry.isDirectory() || SKIPPED_DIRS.has(entry.name)) continue;
|
|
730
|
+
const dir = join2(root, entry.name);
|
|
731
|
+
if (STASH_DIR_NAMES.has(entry.name) && existsSync3(join2(dir, "stash.db"))) {
|
|
732
|
+
found.push(join2(dir, "stash.db"));
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
found.push(...findStashDatabases(dir, depth + 1));
|
|
736
|
+
}
|
|
737
|
+
return found;
|
|
738
|
+
}
|
|
739
|
+
|
|
672
740
|
// packages/cli/src/index.ts
|
|
673
741
|
var _origEmitWarning = process.emitWarning;
|
|
674
742
|
process.emitWarning = function(msg, ...args) {
|
|
@@ -678,8 +746,8 @@ process.emitWarning = function(msg, ...args) {
|
|
|
678
746
|
var CLI_STATUS_SESSION = "cli-status";
|
|
679
747
|
async function runStatus() {
|
|
680
748
|
const stashDir = resolve4(process.env.FILESTASH_DIR ?? ".file-stash");
|
|
681
|
-
const dbPath =
|
|
682
|
-
if (!
|
|
749
|
+
const dbPath = join3(stashDir, "stash.db");
|
|
750
|
+
if (!existsSync4(dbPath)) {
|
|
683
751
|
console.log("No filestash database found. Run 'filestash serve' to start stashing.");
|
|
684
752
|
process.exit(0);
|
|
685
753
|
}
|
|
@@ -691,6 +759,36 @@ async function runStatus() {
|
|
|
691
759
|
console.log(` Tokens saved (total): ~${stats.tokensSaved.toLocaleString()}`);
|
|
692
760
|
await stash.close();
|
|
693
761
|
}
|
|
762
|
+
async function runStatusAll(root) {
|
|
763
|
+
const databases = findStashDatabases(resolve4(root));
|
|
764
|
+
if (databases.length === 0) {
|
|
765
|
+
console.log(`No filestash databases found under ${resolve4(root)}.`);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const rows = [];
|
|
769
|
+
for (const dbPath of databases) {
|
|
770
|
+
const { stash } = createStash({ dbPath, sessionId: CLI_STATUS_SESSION });
|
|
771
|
+
try {
|
|
772
|
+
await stash.init();
|
|
773
|
+
const stats = await stash.getStats();
|
|
774
|
+
rows.push({ project: resolve4(dbPath, "../..").replace(/\/\.vscode$/, ""), files: stats.filesTracked, tokens: stats.tokensSaved });
|
|
775
|
+
} catch (e) {
|
|
776
|
+
console.error(` skipped ${dbPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
777
|
+
} finally {
|
|
778
|
+
await stash.close();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
rows.sort((a, b) => b.tokens - a.tokens);
|
|
782
|
+
const totalTokens = rows.reduce((sum, r) => sum + r.tokens, 0);
|
|
783
|
+
const totalFiles = rows.reduce((sum, r) => sum + r.files, 0);
|
|
784
|
+
console.log(`filestash status (${rows.length} databases under ${resolve4(root)}):`);
|
|
785
|
+
for (const r of rows) {
|
|
786
|
+
console.log(` ~${r.tokens.toLocaleString().padStart(10)} tokens ${String(r.files).padStart(5)} files ${r.project}`);
|
|
787
|
+
}
|
|
788
|
+
console.log(` ~${totalTokens.toLocaleString().padStart(10)} tokens ${String(totalFiles).padStart(5)} files TOTAL`);
|
|
789
|
+
console.log(`
|
|
790
|
+
Savings count re-reads within a session (unchanged files and diffs); a new session always receives full content.`);
|
|
791
|
+
}
|
|
694
792
|
async function runInit() {
|
|
695
793
|
const home = homedir();
|
|
696
794
|
const mcpServersEntry = {
|
|
@@ -701,33 +799,33 @@ async function runInit() {
|
|
|
701
799
|
type: "local",
|
|
702
800
|
command: ["npx", "agent-file-stash", "serve"]
|
|
703
801
|
};
|
|
704
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ||
|
|
802
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join3(home, ".config");
|
|
705
803
|
const targets = [
|
|
706
804
|
{
|
|
707
805
|
name: "Claude Code",
|
|
708
|
-
path:
|
|
806
|
+
path: join3(home, ".claude.json"),
|
|
709
807
|
key: "mcpServers",
|
|
710
808
|
entry: mcpServersEntry
|
|
711
809
|
},
|
|
712
810
|
{
|
|
713
811
|
name: "Cursor",
|
|
714
|
-
path:
|
|
812
|
+
path: join3(home, ".cursor", "mcp.json"),
|
|
715
813
|
key: "mcpServers",
|
|
716
814
|
entry: mcpServersEntry
|
|
717
815
|
},
|
|
718
816
|
{
|
|
719
817
|
name: "OpenCode",
|
|
720
|
-
path:
|
|
818
|
+
path: join3(xdgConfig, "opencode", "opencode.json"),
|
|
721
819
|
key: "mcp",
|
|
722
820
|
entry: opencodeMcpEntry
|
|
723
821
|
}
|
|
724
822
|
];
|
|
725
823
|
let configured = 0;
|
|
726
824
|
for (const target of targets) {
|
|
727
|
-
const dir =
|
|
728
|
-
if (!
|
|
825
|
+
const dir = join3(target.path, "..");
|
|
826
|
+
if (!existsSync4(dir)) continue;
|
|
729
827
|
let config = {};
|
|
730
|
-
if (
|
|
828
|
+
if (existsSync4(target.path)) {
|
|
731
829
|
try {
|
|
732
830
|
config = JSON.parse(readFileSync3(target.path, "utf-8"));
|
|
733
831
|
} catch {
|
|
@@ -768,6 +866,8 @@ Usage:
|
|
|
768
866
|
agent-file-stash init Auto-configure for your editor
|
|
769
867
|
agent-file-stash serve Start the MCP server (default)
|
|
770
868
|
agent-file-stash status Show stash statistics
|
|
869
|
+
agent-file-stash status --all [dir]
|
|
870
|
+
Sum statistics of every stash under dir (default: home)
|
|
771
871
|
agent-file-stash help Show this help message
|
|
772
872
|
|
|
773
873
|
Environment:
|
|
@@ -776,6 +876,8 @@ Environment:
|
|
|
776
876
|
var command = process.argv[2];
|
|
777
877
|
if (!command || command === "serve") {
|
|
778
878
|
await startMcpServer();
|
|
879
|
+
} else if (command === "status" && process.argv[3] === "--all") {
|
|
880
|
+
await runStatusAll(process.argv[4] ?? homedir());
|
|
779
881
|
} else if (command === "status") {
|
|
780
882
|
await runStatus();
|
|
781
883
|
} else if (command === "init") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-file-stash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"mcpName": "io.github.atilio-ts/agent-file-stash",
|
|
5
5
|
"description": "File stash with diff tracking for AI coding agents. Drop-in replacement for file reads that saves tokens.",
|
|
6
6
|
"type": "module",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"license": "MIT",
|
|
56
56
|
"repository": {
|
|
57
57
|
"type": "git",
|
|
58
|
-
"url": "https://github.com/atilio-ts/agent-file-stash"
|
|
58
|
+
"url": "git+https://github.com/atilio-ts/agent-file-stash.git"
|
|
59
59
|
},
|
|
60
60
|
"homepage": "https://github.com/atilio-ts/agent-file-stash",
|
|
61
61
|
"packageManager": "pnpm@10.29.3"
|