agent-file-stash 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -8
- package/dist/cli.mjs +812 -196
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -7,39 +7,111 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
|
|
8
8
|
// packages/sdk/src/differ.ts
|
|
9
9
|
var CONTEXT = 3;
|
|
10
|
+
var MAX_EDIT_DISTANCE = 2500;
|
|
11
|
+
var MAX_SNAKE_STEPS = 2e7;
|
|
10
12
|
function computeDiff(oldContent, newContent, filePath) {
|
|
11
13
|
const oldLines = oldContent.split("\n");
|
|
12
14
|
const newLines = newContent.split("\n");
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
+
const shortest = Math.min(oldLines.length, newLines.length);
|
|
16
|
+
let prefix = 0;
|
|
17
|
+
while (prefix < shortest && oldLines[prefix] === newLines[prefix]) prefix++;
|
|
18
|
+
let suffix = 0;
|
|
19
|
+
while (suffix < shortest - prefix && oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]) suffix++;
|
|
20
|
+
const n = oldLines.length - prefix - suffix;
|
|
21
|
+
const m = newLines.length - prefix - suffix;
|
|
22
|
+
if (n === 0 && m === 0) {
|
|
23
|
+
return { diff: "", linesChanged: 0, hasChanges: false, changedNewLines: /* @__PURE__ */ new Set() };
|
|
24
|
+
}
|
|
25
|
+
const edits = myersEdits(oldLines, newLines, prefix, n, m) ?? {
|
|
26
|
+
removed: new Uint8Array(n).fill(1),
|
|
27
|
+
added: new Uint8Array(m).fill(1)
|
|
28
|
+
};
|
|
29
|
+
const { rawLines, linesChanged } = buildRawLines(oldLines, newLines, prefix, suffix, edits);
|
|
15
30
|
const changedNewLines = collectChangedLines(rawLines);
|
|
16
|
-
if (linesChanged === 0) {
|
|
17
|
-
return { diff: "", linesChanged: 0, hasChanges: false, changedNewLines };
|
|
18
|
-
}
|
|
19
31
|
const hunkGroups = groupIntoHunks(rawLines);
|
|
20
32
|
const diff = formatDiff(hunkGroups, filePath);
|
|
21
33
|
return { diff, linesChanged, hasChanges: true, changedNewLines };
|
|
22
34
|
}
|
|
23
|
-
function
|
|
35
|
+
function myersEdits(oldLines, newLines, off, n, m) {
|
|
36
|
+
const removed = new Uint8Array(n);
|
|
37
|
+
const added = new Uint8Array(m);
|
|
38
|
+
if (n === 0 || m === 0) return { removed: removed.fill(1), added: added.fill(1) };
|
|
39
|
+
if (Math.abs(n - m) > MAX_EDIT_DISTANCE) return null;
|
|
40
|
+
const max = Math.min(n + m, MAX_EDIT_DISTANCE);
|
|
41
|
+
const base = max + 1;
|
|
42
|
+
const v = new Int32Array(2 * max + 3);
|
|
43
|
+
const trace = [];
|
|
44
|
+
let steps = 0;
|
|
45
|
+
let found = -1;
|
|
46
|
+
for (let d = 0; d <= max && found < 0; d++) {
|
|
47
|
+
for (let k = -d; k <= d; k += 2) {
|
|
48
|
+
let x2 = k === -d || k !== d && v[base + k - 1] < v[base + k + 1] ? v[base + k + 1] : v[base + k - 1] + 1;
|
|
49
|
+
let y2 = x2 - k;
|
|
50
|
+
while (x2 < n && y2 < m && oldLines[off + x2] === newLines[off + y2]) {
|
|
51
|
+
x2++;
|
|
52
|
+
y2++;
|
|
53
|
+
steps++;
|
|
54
|
+
}
|
|
55
|
+
if (steps > MAX_SNAKE_STEPS) return null;
|
|
56
|
+
v[base + k] = x2;
|
|
57
|
+
if (x2 >= n && y2 >= m) {
|
|
58
|
+
found = d;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (found >= 0) break;
|
|
63
|
+
const snapshot = new Int32Array(d + 1);
|
|
64
|
+
for (let k = -d, i = 0; k <= d; k += 2, i++) snapshot[i] = v[base + k];
|
|
65
|
+
trace.push(snapshot);
|
|
66
|
+
}
|
|
67
|
+
if (found < 0) return null;
|
|
68
|
+
let x = n, y = m;
|
|
69
|
+
for (let d = found; d > 0; d--) {
|
|
70
|
+
const k = x - y;
|
|
71
|
+
const prev = trace[d - 1];
|
|
72
|
+
const at = (diagonal) => prev[diagonal + d - 1 >> 1];
|
|
73
|
+
const down = k === -d || k !== d && at(k - 1) < at(k + 1);
|
|
74
|
+
const prevK = down ? k + 1 : k - 1;
|
|
75
|
+
const prevX = at(prevK);
|
|
76
|
+
const prevY = prevX - prevK;
|
|
77
|
+
if (down) added[prevY] = 1;
|
|
78
|
+
else removed[prevX] = 1;
|
|
79
|
+
x = prevX;
|
|
80
|
+
y = prevY;
|
|
81
|
+
}
|
|
82
|
+
return { removed, added };
|
|
83
|
+
}
|
|
84
|
+
function buildRawLines(oldLines, newLines, prefix, suffix, edits) {
|
|
24
85
|
const rawLines = [];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
86
|
+
const { removed, added } = edits;
|
|
87
|
+
let linesChanged = 0;
|
|
88
|
+
for (let i = 0; i < prefix; i++) {
|
|
89
|
+
rawLines.push({ type: "keep", line: oldLines[i], oldLine: i + 1, newLine: i + 1 });
|
|
90
|
+
}
|
|
91
|
+
let oi = 0, ni = 0;
|
|
92
|
+
while (oi < removed.length || ni < added.length) {
|
|
93
|
+
if (oi < removed.length && ni < added.length && !removed[oi] && !added[ni]) {
|
|
94
|
+
rawLines.push({ type: "keep", line: oldLines[prefix + oi], oldLine: prefix + oi + 1, newLine: prefix + ni + 1 });
|
|
95
|
+
oi++;
|
|
96
|
+
ni++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
while (ni < added.length && added[ni]) {
|
|
100
|
+
rawLines.push({ type: "add", line: newLines[prefix + ni], oldLine: prefix + oi + 1, newLine: prefix + ni + 1 });
|
|
101
|
+
ni++;
|
|
36
102
|
linesChanged++;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
103
|
+
}
|
|
104
|
+
while (oi < removed.length && removed[oi]) {
|
|
105
|
+
rawLines.push({ type: "remove", line: oldLines[prefix + oi], oldLine: prefix + oi + 1, newLine: prefix + ni + 1 });
|
|
106
|
+
oi++;
|
|
40
107
|
linesChanged++;
|
|
41
108
|
}
|
|
42
109
|
}
|
|
110
|
+
const oldTail = prefix + removed.length;
|
|
111
|
+
const newTail = prefix + added.length;
|
|
112
|
+
for (let i = 0; i < suffix; i++) {
|
|
113
|
+
rawLines.push({ type: "keep", line: oldLines[oldTail + i], oldLine: oldTail + i + 1, newLine: newTail + i + 1 });
|
|
114
|
+
}
|
|
43
115
|
return { rawLines, linesChanged };
|
|
44
116
|
}
|
|
45
117
|
function collectChangedLines(rawLines) {
|
|
@@ -83,13 +155,8 @@ function groupIntoHunks(rawLines) {
|
|
|
83
155
|
}
|
|
84
156
|
function fillContextGap(current, rawLines, lastChangeIdx, upTo) {
|
|
85
157
|
const contextEnd = lastChangeIdx + CONTEXT + 1;
|
|
86
|
-
const inCurrent = new Set(current);
|
|
87
158
|
for (let c = contextEnd; c < upTo; c++) {
|
|
88
|
-
|
|
89
|
-
if (!inCurrent.has(line)) {
|
|
90
|
-
current.push(line);
|
|
91
|
-
inCurrent.add(line);
|
|
92
|
-
}
|
|
159
|
+
current.push(rawLines[c]);
|
|
93
160
|
}
|
|
94
161
|
}
|
|
95
162
|
function formatDiff(hunkGroups, filePath) {
|
|
@@ -108,38 +175,6 @@ function formatDiff(hunkGroups, filePath) {
|
|
|
108
175
|
}
|
|
109
176
|
return lines.join("\n");
|
|
110
177
|
}
|
|
111
|
-
var LCS_LINE_LIMIT = 5e3;
|
|
112
|
-
function longestCommonSubsequence(a, b) {
|
|
113
|
-
const m = a.length;
|
|
114
|
-
const n = b.length;
|
|
115
|
-
if (m > LCS_LINE_LIMIT || n > LCS_LINE_LIMIT) {
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
119
|
-
for (let i2 = 1; i2 <= m; i2++) {
|
|
120
|
-
for (let j2 = 1; j2 <= n; j2++) {
|
|
121
|
-
if (a[i2 - 1] === b[j2 - 1]) {
|
|
122
|
-
dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
123
|
-
} else {
|
|
124
|
-
dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
const result = [];
|
|
129
|
-
let i = m, j = n;
|
|
130
|
-
while (i > 0 && j > 0) {
|
|
131
|
-
if (a[i - 1] === b[j - 1]) {
|
|
132
|
-
result.unshift(a[i - 1]);
|
|
133
|
-
i--;
|
|
134
|
-
j--;
|
|
135
|
-
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
136
|
-
i--;
|
|
137
|
-
} else {
|
|
138
|
-
j--;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return result;
|
|
142
|
-
}
|
|
143
178
|
|
|
144
179
|
// packages/sdk/src/exclude.ts
|
|
145
180
|
import { basename, dirname } from "node:path";
|
|
@@ -173,8 +208,8 @@ function isExcludedPath(absPath, extraPatterns = []) {
|
|
|
173
208
|
return dirname(absPath).split(/[\\/]/).some((seg) => SECRET_DIRS.has(seg.toLowerCase()));
|
|
174
209
|
}
|
|
175
210
|
|
|
176
|
-
// packages/sdk/src/
|
|
177
|
-
var
|
|
211
|
+
// packages/sdk/src/migrations.ts
|
|
212
|
+
var SCHEMA_V1 = `
|
|
178
213
|
CREATE TABLE IF NOT EXISTS file_versions (
|
|
179
214
|
path TEXT NOT NULL,
|
|
180
215
|
hash TEXT NOT NULL,
|
|
@@ -221,11 +256,62 @@ CREATE TABLE IF NOT EXISTS session_stats (
|
|
|
221
256
|
|
|
222
257
|
INSERT OR IGNORE INTO stats (key, value) VALUES ('tokens_saved', 0);
|
|
223
258
|
`;
|
|
259
|
+
var MIGRATIONS = [{ version: 1, up: (db) => db.exec(SCHEMA_V1) }];
|
|
260
|
+
var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
|
261
|
+
var SchemaTooNewError = class extends Error {
|
|
262
|
+
constructor(found, supported) {
|
|
263
|
+
super(`database schema version ${found} is newer than this release supports (${supported}); upgrade agent-file-stash`);
|
|
264
|
+
this.name = "SchemaTooNewError";
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
function readUserVersion(db) {
|
|
268
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
269
|
+
return row.user_version;
|
|
270
|
+
}
|
|
271
|
+
function runMigrations(db, migrations = MIGRATIONS) {
|
|
272
|
+
const target = migrations[migrations.length - 1].version;
|
|
273
|
+
const check = (version) => {
|
|
274
|
+
if (version > target) throw new SchemaTooNewError(version, target);
|
|
275
|
+
};
|
|
276
|
+
check(readUserVersion(db));
|
|
277
|
+
for (const step of migrations) {
|
|
278
|
+
if (readUserVersion(db) >= step.version) continue;
|
|
279
|
+
db.exec("BEGIN IMMEDIATE");
|
|
280
|
+
try {
|
|
281
|
+
const current = readUserVersion(db);
|
|
282
|
+
check(current);
|
|
283
|
+
if (current < step.version) {
|
|
284
|
+
step.up(db);
|
|
285
|
+
db.exec(`PRAGMA user_version = ${step.version}`);
|
|
286
|
+
}
|
|
287
|
+
db.exec("COMMIT");
|
|
288
|
+
} catch (err) {
|
|
289
|
+
try {
|
|
290
|
+
db.exec("ROLLBACK");
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
throw err;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// packages/sdk/src/stash.ts
|
|
224
299
|
var DEFAULT_MAX_LINES = 2e3;
|
|
225
300
|
var DEFAULT_MAX_CHARS = 1e5;
|
|
226
301
|
var DEFAULT_MAX_STORE_BYTES = 1e6;
|
|
227
302
|
var HARD_MAX_READ_BYTES = 64 * 1024 * 1024;
|
|
228
303
|
var BINARY_SNIFF_CHARS = 8192;
|
|
304
|
+
var SCOPE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
305
|
+
var SCOPE_DELIMITER = "::";
|
|
306
|
+
var MAX_SCOPES_PER_SESSION = 32;
|
|
307
|
+
var LIFETIME_KEYS = [
|
|
308
|
+
"reads_total",
|
|
309
|
+
"baseline_tokens_total",
|
|
310
|
+
"sent_tokens_total",
|
|
311
|
+
"sessions_total",
|
|
312
|
+
"overhead_tokens_total",
|
|
313
|
+
"counters_since"
|
|
314
|
+
];
|
|
229
315
|
function positiveOr(value, fallback) {
|
|
230
316
|
return Number.isInteger(value) && value > 0 ? value : fallback;
|
|
231
317
|
}
|
|
@@ -330,6 +416,7 @@ async function waitForLockRelease(lockPath) {
|
|
|
330
416
|
const deadline = Date.now() + RECOVERY_LOCK_WAIT_MS;
|
|
331
417
|
while (existsSync(lockPath) && Date.now() < deadline) await sleep(RECOVERY_LOCK_POLL_MS);
|
|
332
418
|
}
|
|
419
|
+
var BASE_SESSION_SQL = `(CASE WHEN instr(session_id, '${SCOPE_DELIMITER}') > 0 THEN substr(session_id, 1, instr(session_id, '${SCOPE_DELIMITER}') - 1) ELSE session_id END)`;
|
|
333
420
|
function queryOne(db, sql, params = []) {
|
|
334
421
|
const rows = db.prepare(sql).all(...params);
|
|
335
422
|
return rows[0];
|
|
@@ -348,9 +435,10 @@ var StashStore = class {
|
|
|
348
435
|
recoveryAttempted = false;
|
|
349
436
|
recoveredFrom;
|
|
350
437
|
reason;
|
|
438
|
+
sessionCounted = false;
|
|
351
439
|
constructor(config) {
|
|
352
440
|
this.dbPath = config.dbPath;
|
|
353
|
-
this.sessionId = config.sessionId;
|
|
441
|
+
this.sessionId = config.sessionId.replace(/:{2,}/g, "_");
|
|
354
442
|
this.exclude = config.exclude ?? [];
|
|
355
443
|
this.recoverCorrupt = config.recoverCorrupt ?? true;
|
|
356
444
|
this.quiet = config.quiet ?? false;
|
|
@@ -390,7 +478,7 @@ var StashStore = class {
|
|
|
390
478
|
try {
|
|
391
479
|
db.exec("PRAGMA busy_timeout=5000");
|
|
392
480
|
db.exec("PRAGMA journal_mode=WAL");
|
|
393
|
-
db
|
|
481
|
+
runMigrations(db);
|
|
394
482
|
this.registerSession(db);
|
|
395
483
|
this.pruneClosedSessions(db);
|
|
396
484
|
} catch (err) {
|
|
@@ -478,8 +566,9 @@ var StashStore = class {
|
|
|
478
566
|
db.prepare("DELETE FROM sessions WHERE session_id = ?").run(s.session_id);
|
|
479
567
|
}
|
|
480
568
|
}
|
|
481
|
-
|
|
482
|
-
|
|
569
|
+
for (const table of ["session_reads", "session_ranges"]) {
|
|
570
|
+
db.exec(`DELETE FROM ${table} WHERE ${BASE_SESSION_SQL} NOT IN (SELECT session_id FROM sessions)`);
|
|
571
|
+
}
|
|
483
572
|
db.exec("DELETE FROM session_stats WHERE session_id NOT IN (SELECT session_id FROM sessions)");
|
|
484
573
|
db.exec(
|
|
485
574
|
"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)"
|
|
@@ -506,26 +595,26 @@ var StashStore = class {
|
|
|
506
595
|
const size = statSync(absPath).size;
|
|
507
596
|
const now = Date.now();
|
|
508
597
|
if (size > HARD_MAX_READ_BYTES) {
|
|
509
|
-
const
|
|
510
|
-
return { absPath, size, content: "", hash: "", lines: 0, now, early: { result, countRead: false } };
|
|
598
|
+
const result2 = { stashed: false, content: `[filestash: file too large (${size} bytes); not read]`, hash: "" };
|
|
599
|
+
return { absPath, size, content: "", hash: "", lines: 0, now, early: { result: result2, countRead: false } };
|
|
511
600
|
}
|
|
512
601
|
const content = readFileSync(absPath, "utf-8");
|
|
513
602
|
const hash = contentHash(content);
|
|
514
603
|
const lines = content.split("\n").length;
|
|
515
604
|
const snap = { absPath, size, content, hash, lines, now };
|
|
516
605
|
if (content.slice(0, BINARY_SNIFF_CHARS).includes("\0")) {
|
|
517
|
-
const
|
|
518
|
-
snap.early = { result, countRead: true };
|
|
606
|
+
const result2 = { stashed: false, content: `[filestash: binary file (${size} bytes), not shown]`, hash, totalLines: lines };
|
|
607
|
+
snap.early = { result: result2, countRead: true };
|
|
519
608
|
}
|
|
520
609
|
return snap;
|
|
521
610
|
}
|
|
522
611
|
earlyResult(snap) {
|
|
523
|
-
const { result, countRead } = snap.early;
|
|
612
|
+
const { result: result2, countRead } = snap.early;
|
|
524
613
|
if (countRead) {
|
|
525
|
-
const tokens = estimateTokens(
|
|
614
|
+
const tokens = estimateTokens(result2.content);
|
|
526
615
|
this.guarded((db) => this.recordRead(db, tokens, tokens));
|
|
527
616
|
}
|
|
528
|
-
return
|
|
617
|
+
return result2;
|
|
529
618
|
}
|
|
530
619
|
isStoreable(snap) {
|
|
531
620
|
return snap.size <= this.maxStoreBytes && !isExcludedPath(snap.absPath, this.exclude);
|
|
@@ -551,7 +640,8 @@ var StashStore = class {
|
|
|
551
640
|
now,
|
|
552
641
|
size: snap.size,
|
|
553
642
|
slice: slice.text,
|
|
554
|
-
midLine: slice.midLine
|
|
643
|
+
midLine: slice.midLine,
|
|
644
|
+
tid: this.trackingId(options?.scope)
|
|
555
645
|
};
|
|
556
646
|
if (this.db) {
|
|
557
647
|
try {
|
|
@@ -562,6 +652,22 @@ var StashStore = class {
|
|
|
562
652
|
}
|
|
563
653
|
return this.plainResult(state);
|
|
564
654
|
}
|
|
655
|
+
trackingId(scope) {
|
|
656
|
+
return scope !== void 0 && SCOPE_PATTERN.test(scope) ? `${this.sessionId}${SCOPE_DELIMITER}${scope}` : this.sessionId;
|
|
657
|
+
}
|
|
658
|
+
ensureScopeCapacity(db, tid) {
|
|
659
|
+
if (tid === this.sessionId) return;
|
|
660
|
+
if (queryOne(db, "SELECT 1 FROM session_reads WHERE session_id = ? LIMIT 1", [tid])) return;
|
|
661
|
+
const prefix = `${this.sessionId}${SCOPE_DELIMITER}`;
|
|
662
|
+
const scopes = db.prepare(
|
|
663
|
+
"SELECT session_id, MAX(read_at) AS last FROM session_reads WHERE substr(session_id, 1, ?) = ? GROUP BY session_id ORDER BY last ASC, session_id ASC"
|
|
664
|
+
).all(prefix.length, prefix);
|
|
665
|
+
const excess = scopes.length - MAX_SCOPES_PER_SESSION + 1;
|
|
666
|
+
for (const { session_id } of scopes.slice(0, Math.max(0, excess))) {
|
|
667
|
+
db.prepare("DELETE FROM session_reads WHERE session_id = ?").run(session_id);
|
|
668
|
+
db.prepare("DELETE FROM session_ranges WHERE session_id = ?").run(session_id);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
565
671
|
plainResult(s) {
|
|
566
672
|
return { stashed: false, content: s.slice, hash: s.currentHash, totalLines: s.currentLines };
|
|
567
673
|
}
|
|
@@ -574,9 +680,10 @@ var StashStore = class {
|
|
|
574
680
|
const lastRead = queryOne(
|
|
575
681
|
db,
|
|
576
682
|
"SELECT hash FROM session_reads WHERE session_id = ? AND path = ?",
|
|
577
|
-
[
|
|
683
|
+
[state.tid, state.absPath]
|
|
578
684
|
);
|
|
579
685
|
if (!lastRead) {
|
|
686
|
+
this.ensureScopeCapacity(db, state.tid);
|
|
580
687
|
return this.handleFirstRead(db, state);
|
|
581
688
|
}
|
|
582
689
|
const lastHash = lastRead.hash;
|
|
@@ -589,43 +696,43 @@ var StashStore = class {
|
|
|
589
696
|
this.storeVersion(db, s.absPath, s.currentHash, s.currentContent, s.currentLines, s.now);
|
|
590
697
|
db.prepare(
|
|
591
698
|
"INSERT OR REPLACE INTO session_reads (session_id, path, hash, read_at) VALUES (?, ?, ?, ?)"
|
|
592
|
-
).run(
|
|
593
|
-
this.clearRanges(db, s.absPath);
|
|
699
|
+
).run(s.tid, s.absPath, s.currentHash, s.now);
|
|
700
|
+
this.clearRanges(db, s.tid, s.absPath);
|
|
594
701
|
return this.fullSlice(db, s);
|
|
595
702
|
}
|
|
596
703
|
requestedEnd(s) {
|
|
597
704
|
return Math.min(s.rangeEnd, s.currentLines);
|
|
598
705
|
}
|
|
599
|
-
getRanges(db, absPath, hash) {
|
|
706
|
+
getRanges(db, tid, absPath, hash) {
|
|
600
707
|
const rows = db.prepare(
|
|
601
708
|
"SELECT start_line, end_line FROM session_ranges WHERE session_id = ? AND path = ? AND hash = ?"
|
|
602
|
-
).all(
|
|
709
|
+
).all(tid, absPath, hash);
|
|
603
710
|
return rows.map((r) => [r.start_line, r.end_line]);
|
|
604
711
|
}
|
|
605
712
|
coversRequest(db, s, hash) {
|
|
606
|
-
return coversRange(this.getRanges(db, s.absPath, hash), s.rangeStart, this.requestedEnd(s));
|
|
713
|
+
return coversRange(this.getRanges(db, s.tid, s.absPath, hash), s.rangeStart, this.requestedEnd(s));
|
|
607
714
|
}
|
|
608
|
-
clearRanges(db, absPath) {
|
|
609
|
-
db.prepare("DELETE FROM session_ranges WHERE session_id = ? AND path = ?").run(
|
|
715
|
+
clearRanges(db, tid, absPath) {
|
|
716
|
+
db.prepare("DELETE FROM session_ranges WHERE session_id = ? AND path = ?").run(tid, absPath);
|
|
610
717
|
}
|
|
611
|
-
addRange(db, absPath, hash, start, end) {
|
|
718
|
+
addRange(db, tid, absPath, hash, start, end) {
|
|
612
719
|
if (end < start) return;
|
|
613
|
-
const merged = mergeIntervals([...this.getRanges(db, absPath, hash), [start, end]]);
|
|
614
|
-
db.prepare("DELETE FROM session_ranges WHERE session_id = ? AND path = ? AND hash = ?").run(
|
|
720
|
+
const merged = mergeIntervals([...this.getRanges(db, tid, absPath, hash), [start, end]]);
|
|
721
|
+
db.prepare("DELETE FROM session_ranges WHERE session_id = ? AND path = ? AND hash = ?").run(tid, absPath, hash);
|
|
615
722
|
const insert = db.prepare(
|
|
616
723
|
"INSERT INTO session_ranges (session_id, path, hash, start_line, end_line) VALUES (?, ?, ?, ?, ?)"
|
|
617
724
|
);
|
|
618
|
-
for (const [a, b] of merged) insert.run(
|
|
725
|
+
for (const [a, b] of merged) insert.run(tid, absPath, hash, a, b);
|
|
619
726
|
}
|
|
620
727
|
handleUncovered(db, s) {
|
|
621
728
|
db.prepare(
|
|
622
729
|
"UPDATE session_reads SET read_at = ? WHERE session_id = ? AND path = ?"
|
|
623
|
-
).run(s.now,
|
|
730
|
+
).run(s.now, s.tid, s.absPath);
|
|
624
731
|
return this.fullSlice(db, s);
|
|
625
732
|
}
|
|
626
733
|
fullSlice(db, s) {
|
|
627
734
|
const content = s.slice;
|
|
628
|
-
this.addRange(db, s.absPath, s.currentHash, s.rangeStart, this.requestedEnd(s));
|
|
735
|
+
this.addRange(db, s.tid, s.absPath, s.currentHash, s.rangeStart, this.requestedEnd(s));
|
|
629
736
|
this.recordRead(db, estimateTokens(content), estimateTokens(content));
|
|
630
737
|
return { stashed: false, content, hash: s.currentHash, totalLines: s.currentLines };
|
|
631
738
|
}
|
|
@@ -636,7 +743,7 @@ var StashStore = class {
|
|
|
636
743
|
const text = label(saved);
|
|
637
744
|
const sent = estimateTokens(text);
|
|
638
745
|
const base = { stashed: true, hash: s.currentHash, totalLines: s.currentLines, linesChanged: 0 };
|
|
639
|
-
this.addRange(db, s.absPath, s.currentHash, s.rangeStart, this.requestedEnd(s));
|
|
746
|
+
this.addRange(db, s.tid, s.absPath, s.currentHash, s.rangeStart, this.requestedEnd(s));
|
|
640
747
|
if (sent >= baseline) {
|
|
641
748
|
this.recordRead(db, baseline, baseline);
|
|
642
749
|
return { ...base, content };
|
|
@@ -647,7 +754,7 @@ var StashStore = class {
|
|
|
647
754
|
handleUnchanged(db, s) {
|
|
648
755
|
db.prepare(
|
|
649
756
|
"UPDATE session_reads SET read_at = ? WHERE session_id = ? AND path = ?"
|
|
650
|
-
).run(s.now,
|
|
757
|
+
).run(s.now, s.tid, s.absPath);
|
|
651
758
|
return this.unchangedLabel(
|
|
652
759
|
db,
|
|
653
760
|
s,
|
|
@@ -660,12 +767,12 @@ var StashStore = class {
|
|
|
660
767
|
"SELECT content, lines FROM file_versions WHERE path = ? AND hash = ?",
|
|
661
768
|
[s.absPath, lastHash]
|
|
662
769
|
);
|
|
663
|
-
const oldFullyDelivered = !!oldVersion && coversRange(this.getRanges(db, s.absPath, lastHash), 1, oldVersion.lines);
|
|
770
|
+
const oldFullyDelivered = !!oldVersion && coversRange(this.getRanges(db, s.tid, s.absPath, lastHash), 1, oldVersion.lines);
|
|
664
771
|
this.storeVersion(db, s.absPath, s.currentHash, s.currentContent, s.currentLines, s.now);
|
|
665
772
|
db.prepare(
|
|
666
773
|
"UPDATE session_reads SET hash = ?, read_at = ? WHERE session_id = ? AND path = ?"
|
|
667
|
-
).run(s.currentHash, s.now,
|
|
668
|
-
this.clearRanges(db, s.absPath);
|
|
774
|
+
).run(s.currentHash, s.now, s.tid, s.absPath);
|
|
775
|
+
this.clearRanges(db, s.tid, s.absPath);
|
|
669
776
|
if (oldVersion && oldFullyDelivered) {
|
|
670
777
|
const diffResult = computeDiff(oldVersion.content, s.currentContent, s.absPath);
|
|
671
778
|
if (diffResult.hasChanges) {
|
|
@@ -688,7 +795,7 @@ var StashStore = class {
|
|
|
688
795
|
const contentTokens = estimateTokens(s.currentContent);
|
|
689
796
|
const diffTokens = estimateTokens(diffResult.diff);
|
|
690
797
|
if (diffTokens >= contentTokens) return this.fullSlice(db, s);
|
|
691
|
-
this.addRange(db, s.absPath, s.currentHash, 1, s.currentLines);
|
|
798
|
+
this.addRange(db, s.tid, s.absPath, s.currentHash, 1, s.currentLines);
|
|
692
799
|
this.recordRead(db, contentTokens, diffTokens);
|
|
693
800
|
return {
|
|
694
801
|
stashed: true,
|
|
@@ -711,29 +818,31 @@ var StashStore = class {
|
|
|
711
818
|
).run(absPath, hash, content, lines, now);
|
|
712
819
|
}
|
|
713
820
|
// Resets session tracking for the file and returns the first slice regardless of what was delivered before. Counts as a plain read (baseline equals sent).
|
|
714
|
-
async readFileFull(filePath) {
|
|
821
|
+
async readFileFull(filePath, scope) {
|
|
715
822
|
await this.init();
|
|
716
823
|
const snap = this.readFileSnapshot(filePath);
|
|
717
824
|
if (snap.early) return this.earlyResult(snap);
|
|
718
825
|
const { absPath, content, hash, lines, now } = snap;
|
|
719
826
|
const slice = sliceLines(content, 0, 0, this.maxLines, this.maxChars);
|
|
720
|
-
const
|
|
721
|
-
if (!this.db) return
|
|
827
|
+
const result2 = { stashed: false, content: slice.text, hash, totalLines: lines };
|
|
828
|
+
if (!this.db) return result2;
|
|
722
829
|
try {
|
|
723
830
|
const tokens = estimateTokens(slice.text);
|
|
724
831
|
if (this.isStoreable(snap)) {
|
|
832
|
+
const tid = this.trackingId(scope);
|
|
833
|
+
this.ensureScopeCapacity(this.db, tid);
|
|
725
834
|
this.storeVersion(this.db, absPath, hash, content, lines, now);
|
|
726
835
|
this.db.prepare(
|
|
727
836
|
"INSERT OR REPLACE INTO session_reads (session_id, path, hash, read_at) VALUES (?, ?, ?, ?)"
|
|
728
|
-
).run(
|
|
729
|
-
this.clearRanges(this.db, absPath);
|
|
730
|
-
this.addRange(this.db, absPath, hash, slice.start, slice.end);
|
|
837
|
+
).run(tid, absPath, hash, now);
|
|
838
|
+
this.clearRanges(this.db, tid, absPath);
|
|
839
|
+
this.addRange(this.db, tid, absPath, hash, slice.start, slice.end);
|
|
731
840
|
}
|
|
732
841
|
this.recordRead(this.db, tokens, tokens);
|
|
733
842
|
} catch (err) {
|
|
734
843
|
this.degrade(err);
|
|
735
844
|
}
|
|
736
|
-
return
|
|
845
|
+
return result2;
|
|
737
846
|
}
|
|
738
847
|
async onFileDeleted(filePath) {
|
|
739
848
|
await this.init();
|
|
@@ -759,6 +868,8 @@ var StashStore = class {
|
|
|
759
868
|
const versionRow = queryOne(db, "SELECT COUNT(DISTINCT path) as c FROM file_versions");
|
|
760
869
|
const tokenRow = queryOne(db, "SELECT value FROM stats WHERE key = 'tokens_saved'");
|
|
761
870
|
const sessionStat = (key) => queryOne(db, "SELECT value FROM session_stats WHERE session_id = ? AND key = ?", [this.sessionId, key])?.value ?? 0;
|
|
871
|
+
const lifetime = (key) => queryOne(db, "SELECT value FROM stats WHERE key = ?", [key])?.value ?? 0;
|
|
872
|
+
const since = queryOne(db, "SELECT value FROM stats WHERE key = 'counters_since'")?.value;
|
|
762
873
|
return {
|
|
763
874
|
filesTracked: versionRow?.c ?? 0,
|
|
764
875
|
tokensSaved: tokenRow?.value ?? 0,
|
|
@@ -766,6 +877,12 @@ var StashStore = class {
|
|
|
766
877
|
sessionReads: sessionStat("reads"),
|
|
767
878
|
sessionBaselineTokens: sessionStat("baseline_tokens"),
|
|
768
879
|
sessionSentTokens: sessionStat("sent_tokens"),
|
|
880
|
+
lifetimeSessions: lifetime("sessions_total"),
|
|
881
|
+
lifetimeReads: lifetime("reads_total"),
|
|
882
|
+
lifetimeBaselineTokens: lifetime("baseline_tokens_total"),
|
|
883
|
+
lifetimeSentTokens: lifetime("sent_tokens_total"),
|
|
884
|
+
lifetimeOverheadTokens: lifetime("overhead_tokens_total"),
|
|
885
|
+
...since !== void 0 && since > 0 && { countersSince: since },
|
|
769
886
|
degraded: false,
|
|
770
887
|
...this.recoveredFrom && { recoveredFrom: this.recoveredFrom }
|
|
771
888
|
};
|
|
@@ -778,6 +895,11 @@ var StashStore = class {
|
|
|
778
895
|
sessionReads: 0,
|
|
779
896
|
sessionBaselineTokens: 0,
|
|
780
897
|
sessionSentTokens: 0,
|
|
898
|
+
lifetimeSessions: 0,
|
|
899
|
+
lifetimeReads: 0,
|
|
900
|
+
lifetimeBaselineTokens: 0,
|
|
901
|
+
lifetimeSentTokens: 0,
|
|
902
|
+
lifetimeOverheadTokens: 0,
|
|
781
903
|
degraded: true,
|
|
782
904
|
...this.reason && { degradedReason: this.reason }
|
|
783
905
|
};
|
|
@@ -790,6 +912,7 @@ var StashStore = class {
|
|
|
790
912
|
db.prepare("DELETE FROM session_ranges").run();
|
|
791
913
|
db.prepare("DELETE FROM session_stats").run();
|
|
792
914
|
db.prepare("UPDATE stats SET value = 0").run();
|
|
915
|
+
db.prepare(`DELETE FROM stats WHERE key IN (${LIFETIME_KEYS.map(() => "?").join(", ")})`).run(...LIFETIME_KEYS);
|
|
793
916
|
});
|
|
794
917
|
}
|
|
795
918
|
async resetReads() {
|
|
@@ -817,6 +940,19 @@ var StashStore = class {
|
|
|
817
940
|
this.bumpSessionStat(db, "baseline_tokens", baseline);
|
|
818
941
|
this.bumpSessionStat(db, "sent_tokens", sent);
|
|
819
942
|
if (baseline > sent) this.addTokensSaved(db, baseline - sent);
|
|
943
|
+
this.bumpLifetime(db, { reads_total: 1, baseline_tokens_total: baseline, sent_tokens_total: sent });
|
|
944
|
+
}
|
|
945
|
+
bumpLifetime(db, counters) {
|
|
946
|
+
db.prepare("INSERT OR IGNORE INTO stats (key, value) VALUES ('counters_since', ?)").run(Date.now());
|
|
947
|
+
const bump = db.prepare("INSERT INTO stats (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = value + ?");
|
|
948
|
+
for (const [key, n] of Object.entries(counters)) bump.run(key, n, n);
|
|
949
|
+
}
|
|
950
|
+
async recordSessionStart(overheadTokens) {
|
|
951
|
+
await this.init();
|
|
952
|
+
if (this.sessionCounted) return;
|
|
953
|
+
this.sessionCounted = true;
|
|
954
|
+
const overhead = Number.isFinite(overheadTokens) && overheadTokens > 0 ? Math.round(overheadTokens) : 0;
|
|
955
|
+
this.guarded((db) => this.bumpLifetime(db, { sessions_total: 1, overhead_tokens_total: overhead }));
|
|
820
956
|
}
|
|
821
957
|
bumpSessionStat(db, key, n) {
|
|
822
958
|
db.prepare(
|
|
@@ -901,6 +1037,23 @@ var FileWatcher = class {
|
|
|
901
1037
|
}
|
|
902
1038
|
};
|
|
903
1039
|
|
|
1040
|
+
// packages/sdk/src/lifetime.ts
|
|
1041
|
+
function lifetimeView(c) {
|
|
1042
|
+
const grossSaved = c.lifetimeBaselineTokens - c.lifetimeSentTokens;
|
|
1043
|
+
const netSaved = grossSaved - c.lifetimeOverheadTokens;
|
|
1044
|
+
return {
|
|
1045
|
+
empty: c.lifetimeSessions === 0 && c.lifetimeReads === 0,
|
|
1046
|
+
sessions: c.lifetimeSessions,
|
|
1047
|
+
reads: c.lifetimeReads,
|
|
1048
|
+
baselineTokens: c.lifetimeBaselineTokens,
|
|
1049
|
+
sentTokens: c.lifetimeSentTokens,
|
|
1050
|
+
overheadTokens: c.lifetimeOverheadTokens,
|
|
1051
|
+
grossSaved,
|
|
1052
|
+
netSaved,
|
|
1053
|
+
netPerSession: c.lifetimeSessions > 0 ? Math.round(netSaved / c.lifetimeSessions) || 0 : null
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
904
1057
|
// packages/sdk/src/index.ts
|
|
905
1058
|
function createStash(config) {
|
|
906
1059
|
const stash = new StashStore(config);
|
|
@@ -912,13 +1065,11 @@ function createStash(config) {
|
|
|
912
1065
|
}
|
|
913
1066
|
|
|
914
1067
|
// packages/cli/src/index.ts
|
|
915
|
-
import { resolve as
|
|
916
|
-
import { existsSync as
|
|
917
|
-
import { homedir } from "node:os";
|
|
1068
|
+
import { resolve as resolve5, join as join5 } from "node:path";
|
|
1069
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync2, copyFileSync, mkdirSync as mkdirSync2 } from "node:fs";
|
|
1070
|
+
import { homedir as homedir2 } from "node:os";
|
|
918
1071
|
|
|
919
1072
|
// packages/cli/src/mcp.ts
|
|
920
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
921
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
922
1073
|
import { z } from "zod";
|
|
923
1074
|
import { resolve as resolve3, join, relative, isAbsolute } from "node:path";
|
|
924
1075
|
import { readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
@@ -954,32 +1105,34 @@ function isPathAllowed(absPath, cwd) {
|
|
|
954
1105
|
const rel = relative(cwd, realPath);
|
|
955
1106
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
956
1107
|
}
|
|
957
|
-
function formatReadResult(
|
|
958
|
-
if (
|
|
959
|
-
return `[filestash: ${
|
|
960
|
-
${
|
|
1108
|
+
function formatReadResult(result2) {
|
|
1109
|
+
if (result2.stashed && result2.diff) {
|
|
1110
|
+
return `[filestash: ${result2.linesChanged} lines changed out of ${result2.totalLines}]
|
|
1111
|
+
${result2.diff}`;
|
|
961
1112
|
}
|
|
962
|
-
return
|
|
1113
|
+
return result2.content;
|
|
963
1114
|
}
|
|
964
|
-
function formatFileEntry(path,
|
|
965
|
-
if (
|
|
966
|
-
return `=== ${path} [${
|
|
967
|
-
${
|
|
1115
|
+
function formatFileEntry(path, result2) {
|
|
1116
|
+
if (result2.stashed && result2.diff) {
|
|
1117
|
+
return `=== ${path} [${result2.linesChanged} lines changed out of ${result2.totalLines}] ===
|
|
1118
|
+
${result2.diff}`;
|
|
968
1119
|
}
|
|
969
1120
|
return `=== ${path} ===
|
|
970
|
-
${
|
|
1121
|
+
${result2.content}`;
|
|
971
1122
|
}
|
|
972
1123
|
var readFileShape = {
|
|
973
1124
|
path: z.string().describe("File path"),
|
|
974
1125
|
offset: z.number().optional().describe("Start line (1-based)"),
|
|
975
1126
|
limit: z.number().optional().describe("Max lines"),
|
|
976
|
-
force: z.boolean().optional().describe("Bypass stash")
|
|
1127
|
+
force: z.boolean().optional().describe("Bypass stash"),
|
|
1128
|
+
agent: z.string().optional()
|
|
977
1129
|
};
|
|
978
1130
|
var readFilesShape = {
|
|
979
1131
|
paths: z.preprocess(
|
|
980
1132
|
(val) => typeof val === "string" ? JSON.parse(val) : val,
|
|
981
1133
|
z.array(z.string())
|
|
982
|
-
).describe("File paths")
|
|
1134
|
+
).describe("File paths"),
|
|
1135
|
+
agent: z.string().optional()
|
|
983
1136
|
};
|
|
984
1137
|
var TOOL_DEFS = {
|
|
985
1138
|
read_file: {
|
|
@@ -1029,8 +1182,24 @@ function toolDefinitionTokens() {
|
|
|
1029
1182
|
}));
|
|
1030
1183
|
return Math.ceil(JSON.stringify(defs).length / 4);
|
|
1031
1184
|
}
|
|
1032
|
-
function degradedLine(
|
|
1033
|
-
return `Mode: DEGRADED (${
|
|
1185
|
+
function degradedLine(reason2 = "unknown") {
|
|
1186
|
+
return `Mode: DEGRADED (${reason2}) - files are read normally, nothing is stashed`;
|
|
1187
|
+
}
|
|
1188
|
+
function formatLifetime(c, since, saved) {
|
|
1189
|
+
const v = lifetimeView(c);
|
|
1190
|
+
if (v.empty) return [` Lifetime counters start with this version; ${saved} above includes older history.`];
|
|
1191
|
+
const fmt = (n) => `~${n.toLocaleString()}`;
|
|
1192
|
+
const date = since ? ` (since ${new Date(since).toISOString().slice(0, 10)})` : "";
|
|
1193
|
+
const perSession = v.netPerSession === null ? "" : `, ${fmt(v.netPerSession)} per session`;
|
|
1194
|
+
return [
|
|
1195
|
+
` Lifetime${date}:`,
|
|
1196
|
+
` Sessions: ${v.sessions}, reads: ${v.reads}`,
|
|
1197
|
+
` Would have sent (plain reads): ${fmt(v.baselineTokens)} tokens`,
|
|
1198
|
+
` Actually sent: ${fmt(v.sentTokens)} tokens`,
|
|
1199
|
+
` Gross saved: ${fmt(v.grossSaved)} tokens`,
|
|
1200
|
+
` Tool definitions overhead: ${fmt(v.overheadTokens)} tokens (est., ${v.sessions} sessions)`,
|
|
1201
|
+
` Net saved: ${fmt(v.netSaved)} tokens (est.)${perSession}`
|
|
1202
|
+
];
|
|
1034
1203
|
}
|
|
1035
1204
|
function formatStatus(stats, overheadTokens) {
|
|
1036
1205
|
if (stats.degraded) return degradedLine(stats.degradedReason);
|
|
@@ -1046,7 +1215,8 @@ function formatStatus(stats, overheadTokens) {
|
|
|
1046
1215
|
` Gross saved: ${fmt(stats.sessionTokensSaved)} tokens`,
|
|
1047
1216
|
` Tool definitions overhead: ${fmt(overheadTokens)} tokens (est.)`,
|
|
1048
1217
|
` Net saved: ${fmt(net)} tokens (est.)`,
|
|
1049
|
-
` Gross saved (all sessions): ${fmt(stats.tokensSaved)} tokens
|
|
1218
|
+
` Gross saved (all sessions): ${fmt(stats.tokensSaved)} tokens`,
|
|
1219
|
+
...formatLifetime(stats, stats.countersSince, `"Gross saved (all sessions)"`)
|
|
1050
1220
|
].join("\n");
|
|
1051
1221
|
}
|
|
1052
1222
|
function statsSuffix(grossSaved) {
|
|
@@ -1054,7 +1224,7 @@ function statsSuffix(grossSaved) {
|
|
|
1054
1224
|
|
|
1055
1225
|
[filestash: net ~${(grossSaved - toolDefinitionTokens()).toLocaleString()} tokens this session (est.)]`;
|
|
1056
1226
|
}
|
|
1057
|
-
async function readSingleFile(path, cwd, stash) {
|
|
1227
|
+
async function readSingleFile(path, cwd, stash, scope) {
|
|
1058
1228
|
const absPath = resolve3(path);
|
|
1059
1229
|
if (!isPathAllowed(absPath, cwd)) {
|
|
1060
1230
|
return {
|
|
@@ -1064,8 +1234,8 @@ Error: Path must be within the working directory (${cwd})`,
|
|
|
1064
1234
|
};
|
|
1065
1235
|
}
|
|
1066
1236
|
try {
|
|
1067
|
-
const
|
|
1068
|
-
return { text: formatFileEntry(path,
|
|
1237
|
+
const result2 = await stash.readFile(path, { ...scope !== void 0 && { scope } });
|
|
1238
|
+
return { text: formatFileEntry(path, result2), ok: true };
|
|
1069
1239
|
} catch (e) {
|
|
1070
1240
|
const message = e instanceof Error ? e.message : String(e);
|
|
1071
1241
|
return { text: `=== ${path} ===
|
|
@@ -1093,6 +1263,9 @@ async function startMcpServer() {
|
|
|
1093
1263
|
...limitOptions()
|
|
1094
1264
|
});
|
|
1095
1265
|
await stash.init();
|
|
1266
|
+
await stash.recordSessionStart(toolDefinitionTokens());
|
|
1267
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
1268
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
1096
1269
|
const server = new McpServer({
|
|
1097
1270
|
name: "filestash",
|
|
1098
1271
|
version: packageJson.version ?? "0.0.0"
|
|
@@ -1100,7 +1273,7 @@ async function startMcpServer() {
|
|
|
1100
1273
|
server.registerTool(
|
|
1101
1274
|
"read_file",
|
|
1102
1275
|
TOOL_DEFS.read_file,
|
|
1103
|
-
async ({ path, force, offset, limit }) => {
|
|
1276
|
+
async ({ path, force, offset, limit, agent }) => {
|
|
1104
1277
|
const absPath = resolve3(path);
|
|
1105
1278
|
if (!isPathAllowed(absPath, cwd)) {
|
|
1106
1279
|
return {
|
|
@@ -1109,12 +1282,13 @@ async function startMcpServer() {
|
|
|
1109
1282
|
};
|
|
1110
1283
|
}
|
|
1111
1284
|
try {
|
|
1112
|
-
const
|
|
1285
|
+
const result2 = force ? await stash.readFileFull(path, agent) : await stash.readFile(path, {
|
|
1113
1286
|
...offset !== void 0 && { offset },
|
|
1114
|
-
...limit !== void 0 && { limit }
|
|
1287
|
+
...limit !== void 0 && { limit },
|
|
1288
|
+
...agent !== void 0 && { scope: agent }
|
|
1115
1289
|
});
|
|
1116
|
-
let text = formatReadResult(
|
|
1117
|
-
if (
|
|
1290
|
+
let text = formatReadResult(result2);
|
|
1291
|
+
if (result2.stashed) {
|
|
1118
1292
|
const stats = await stash.getStats();
|
|
1119
1293
|
text += statsSuffix(stats.sessionTokensSaved);
|
|
1120
1294
|
}
|
|
@@ -1134,8 +1308,8 @@ async function startMcpServer() {
|
|
|
1134
1308
|
server.registerTool(
|
|
1135
1309
|
"read_files",
|
|
1136
1310
|
TOOL_DEFS.read_files,
|
|
1137
|
-
async ({ paths }) => {
|
|
1138
|
-
const results = await Promise.all(paths.map((p) => readSingleFile(p, cwd, stash)));
|
|
1311
|
+
async ({ paths, agent }) => {
|
|
1312
|
+
const results = await Promise.all(paths.map((p) => readSingleFile(p, cwd, stash, agent)));
|
|
1139
1313
|
const successfulPaths = paths.filter((_, i) => results[i].ok);
|
|
1140
1314
|
const combined = results.map((r) => r.text).join("\n\n");
|
|
1141
1315
|
let footer = "";
|
|
@@ -1165,7 +1339,8 @@ async function startMcpServer() {
|
|
|
1165
1339
|
[`${META_NAMESPACE}/stats`]: {
|
|
1166
1340
|
...stats,
|
|
1167
1341
|
toolDefinitionTokens: overhead,
|
|
1168
|
-
netTokensSaved: stats.sessionTokensSaved - overhead
|
|
1342
|
+
netTokensSaved: stats.sessionTokensSaved - overhead,
|
|
1343
|
+
lifetime: lifetimeView(stats)
|
|
1169
1344
|
}
|
|
1170
1345
|
}
|
|
1171
1346
|
};
|
|
@@ -1223,6 +1398,449 @@ function findStashDatabases(root, depth = 0) {
|
|
|
1223
1398
|
return found;
|
|
1224
1399
|
}
|
|
1225
1400
|
|
|
1401
|
+
// packages/cli/src/editors.ts
|
|
1402
|
+
import { join as join3 } from "node:path";
|
|
1403
|
+
function editorTargets(home) {
|
|
1404
|
+
const mcpServersEntry = {
|
|
1405
|
+
command: "npx",
|
|
1406
|
+
args: ["agent-file-stash", "serve"]
|
|
1407
|
+
};
|
|
1408
|
+
const opencodeMcpEntry = {
|
|
1409
|
+
type: "local",
|
|
1410
|
+
command: ["npx", "agent-file-stash", "serve"]
|
|
1411
|
+
};
|
|
1412
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || join3(home, ".config");
|
|
1413
|
+
return [
|
|
1414
|
+
{ name: "Claude Code", path: join3(home, ".claude.json"), key: "mcpServers", entry: mcpServersEntry },
|
|
1415
|
+
{ name: "Cursor", path: join3(home, ".cursor", "mcp.json"), key: "mcpServers", entry: mcpServersEntry },
|
|
1416
|
+
{ name: "OpenCode", path: join3(xdgConfig, "opencode", "opencode.json"), key: "mcp", entry: opencodeMcpEntry }
|
|
1417
|
+
];
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// packages/cli/src/doctor.ts
|
|
1421
|
+
import { accessSync, constants, existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
|
|
1422
|
+
import { homedir } from "node:os";
|
|
1423
|
+
import { delimiter, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
|
|
1424
|
+
import { pathToFileURL } from "node:url";
|
|
1425
|
+
import { spawnSync } from "node:child_process";
|
|
1426
|
+
var PACKAGE_NAME = "agent-file-stash";
|
|
1427
|
+
var RECOVERY_LOCK_STALE_MS2 = 15e3;
|
|
1428
|
+
var DEFAULT_MIN_NODE = "24";
|
|
1429
|
+
var NPM_TIMEOUT_MS = 5e3;
|
|
1430
|
+
var MAX_SCRIPT_BYTES = 64 * 1024;
|
|
1431
|
+
var SESSION_START_TARGETS = ["clear", "compact"];
|
|
1432
|
+
var result = (id, status, message, hint) => ({
|
|
1433
|
+
id,
|
|
1434
|
+
status,
|
|
1435
|
+
message,
|
|
1436
|
+
...hint !== void 0 && { hint }
|
|
1437
|
+
});
|
|
1438
|
+
var reason = (e) => e instanceof Error ? e.message : String(e);
|
|
1439
|
+
function readJson(path) {
|
|
1440
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
1441
|
+
}
|
|
1442
|
+
function asRecord(v) {
|
|
1443
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
|
|
1444
|
+
}
|
|
1445
|
+
function ownPackageJson() {
|
|
1446
|
+
for (const rel of ["../package.json", "../../../package.json"]) {
|
|
1447
|
+
try {
|
|
1448
|
+
const pkg = readJson(join4(import.meta.dirname, rel));
|
|
1449
|
+
if (pkg.name === PACKAGE_NAME) return pkg;
|
|
1450
|
+
} catch {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return void 0;
|
|
1455
|
+
}
|
|
1456
|
+
function wide(mode, mask) {
|
|
1457
|
+
return (mode & mask) !== 0;
|
|
1458
|
+
}
|
|
1459
|
+
function octal(mode) {
|
|
1460
|
+
return (mode & 511).toString(8).padStart(3, "0");
|
|
1461
|
+
}
|
|
1462
|
+
function checkNode(o) {
|
|
1463
|
+
const range = ownPackageJson()?.engines?.node;
|
|
1464
|
+
const required = Number(/\d+/.exec(range ?? "")?.[0] ?? DEFAULT_MIN_NODE);
|
|
1465
|
+
const major = Number(o.nodeVersion.split(".")[0]);
|
|
1466
|
+
if (major >= required) return [result("node", "ok", `Node ${o.nodeVersion} (requires >=${required})`)];
|
|
1467
|
+
return [
|
|
1468
|
+
result("node", "error", `Node ${o.nodeVersion} is older than the required >=${required}`, `install Node ${required} or newer`)
|
|
1469
|
+
];
|
|
1470
|
+
}
|
|
1471
|
+
function checkStashDir(o) {
|
|
1472
|
+
const dir = resolveStashDir(o.cwd);
|
|
1473
|
+
if (!existsSync4(dir)) return [result("stash-dir", "info", `stash directory ${dir} does not exist yet; it will be created on first read`)];
|
|
1474
|
+
const st = statSync2(dir);
|
|
1475
|
+
if (!st.isDirectory()) {
|
|
1476
|
+
return [result("stash-dir", "error", `${dir} exists but is not a directory`, `remove it or point FILESTASH_DIR elsewhere`)];
|
|
1477
|
+
}
|
|
1478
|
+
if (o.platform !== "win32" && wide(st.mode, 63)) {
|
|
1479
|
+
return [result("stash-dir", "warn", `stash directory ${dir} has mode ${octal(st.mode)}, expected 700`, `run: chmod 700 ${dir}`)];
|
|
1480
|
+
}
|
|
1481
|
+
return [result("stash-dir", "info", `stash directory ${dir}`)];
|
|
1482
|
+
}
|
|
1483
|
+
function isCorruptMessage(err) {
|
|
1484
|
+
const code = err.errcode;
|
|
1485
|
+
if (typeof code === "number" && [11, 26].includes(code & 255)) return true;
|
|
1486
|
+
return /not a database|malformed|corrupt/i.test(reason(err));
|
|
1487
|
+
}
|
|
1488
|
+
function pidAlive(pid) {
|
|
1489
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
1490
|
+
try {
|
|
1491
|
+
process.kill(pid, 0);
|
|
1492
|
+
return true;
|
|
1493
|
+
} catch (e) {
|
|
1494
|
+
return e.code === "EPERM";
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function formatBytes(n) {
|
|
1498
|
+
return n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
1499
|
+
}
|
|
1500
|
+
async function inspectDatabase(dbPath) {
|
|
1501
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
1502
|
+
const url = pathToFileURL(dbPath);
|
|
1503
|
+
url.search = existsSync4(`${dbPath}-wal`) || existsSync4(`${dbPath}-shm`) ? "mode=ro" : "mode=ro&immutable=1";
|
|
1504
|
+
const db = new DatabaseSync(url.href, { readOnly: true });
|
|
1505
|
+
try {
|
|
1506
|
+
const userVersion = db.prepare("PRAGMA user_version").get().user_version;
|
|
1507
|
+
const first = db.prepare("PRAGMA quick_check").all()[0] ?? {};
|
|
1508
|
+
const integrity = String(Object.values(first)[0] ?? "no result");
|
|
1509
|
+
const facts = { userVersion, integrity };
|
|
1510
|
+
if (integrity === "ok" && userVersion >= 1 && userVersion <= SCHEMA_VERSION) {
|
|
1511
|
+
facts.filesTracked = db.prepare("SELECT COUNT(DISTINCT path) AS c FROM file_versions").get().c;
|
|
1512
|
+
facts.pids = db.prepare("SELECT pid FROM sessions").all().map((r) => r.pid);
|
|
1513
|
+
}
|
|
1514
|
+
return facts;
|
|
1515
|
+
} finally {
|
|
1516
|
+
db.close();
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function checkDatabaseFiles(dir, now) {
|
|
1520
|
+
const out = [];
|
|
1521
|
+
const names = readdirSync2(dir);
|
|
1522
|
+
const leftovers = names.filter((n) => /^stash\.db\.corrupt-\d+$/.test(n));
|
|
1523
|
+
if (leftovers.length > 0) {
|
|
1524
|
+
out.push(result("database-leftovers", "info", `${leftovers.length} moved-aside corrupt database file(s) in ${dir} (stash.db.corrupt-*); delete them when no longer needed`));
|
|
1525
|
+
}
|
|
1526
|
+
if (names.includes("stash.db.recover.lock")) {
|
|
1527
|
+
const lock = join4(dir, "stash.db.recover.lock");
|
|
1528
|
+
const age = now - statSync2(lock).mtimeMs;
|
|
1529
|
+
if (age > RECOVERY_LOCK_STALE_MS2) {
|
|
1530
|
+
out.push(result("database-lock", "warn", `stale recovery lock ${lock} (${Math.round(age / 1e3)}s old)`, `remove ${lock}; the next start also clears it`));
|
|
1531
|
+
} else {
|
|
1532
|
+
out.push(result("database-lock", "info", `a recovery is in progress (${lock})`));
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return out;
|
|
1536
|
+
}
|
|
1537
|
+
async function checkDatabase(o) {
|
|
1538
|
+
const dir = resolveStashDir(o.cwd);
|
|
1539
|
+
const dbPath = join4(dir, "stash.db");
|
|
1540
|
+
const out = [];
|
|
1541
|
+
if (existsSync4(dir)) out.push(...checkDatabaseFiles(dir, o.now));
|
|
1542
|
+
if (!existsSync4(dbPath)) return [result("database", "info", `no database at ${dbPath} yet; it will be created on first read`), ...out];
|
|
1543
|
+
const st = statSync2(dbPath);
|
|
1544
|
+
let facts;
|
|
1545
|
+
try {
|
|
1546
|
+
facts = await inspectDatabase(dbPath);
|
|
1547
|
+
} catch (e) {
|
|
1548
|
+
if (isCorruptMessage(e)) {
|
|
1549
|
+
return [
|
|
1550
|
+
result("database", "error", `${dbPath} is corrupt (${reason(e)})`, `it will be moved aside to stash.db.corrupt-<ms> on next start; or delete it`),
|
|
1551
|
+
...out
|
|
1552
|
+
];
|
|
1553
|
+
}
|
|
1554
|
+
return [result("database", "warn", `could not inspect ${dbPath} without writing (${reason(e)})`, `stop running servers and re-run doctor`), ...out];
|
|
1555
|
+
}
|
|
1556
|
+
if (facts.integrity !== "ok") {
|
|
1557
|
+
out.unshift(result("database", "error", `${dbPath} failed quick_check (${facts.integrity})`, `it will be moved aside to stash.db.corrupt-<ms> on next start; or delete it`));
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
if (facts.userVersion > SCHEMA_VERSION) {
|
|
1561
|
+
out.unshift(result("database-schema", "error", new SchemaTooNewError(facts.userVersion, SCHEMA_VERSION).message, "upgrade agent-file-stash or point FILESTASH_DIR at another directory"));
|
|
1562
|
+
} else if (facts.userVersion < SCHEMA_VERSION) {
|
|
1563
|
+
out.unshift(result("database-schema", "info", `schema version ${facts.userVersion} will be upgraded to ${SCHEMA_VERSION} on next start`));
|
|
1564
|
+
} else {
|
|
1565
|
+
out.unshift(result("database-schema", "ok", `database schema version ${facts.userVersion}, quick_check passed`));
|
|
1566
|
+
}
|
|
1567
|
+
if (o.platform !== "win32" && wide(st.mode, 63)) {
|
|
1568
|
+
out.push(result("database-mode", "warn", `${dbPath} has mode ${octal(st.mode)}, expected 600`, `run: chmod 600 ${dbPath}`));
|
|
1569
|
+
}
|
|
1570
|
+
const parts = [formatBytes(st.size)];
|
|
1571
|
+
if (facts.filesTracked !== void 0) parts.push(`${facts.filesTracked} files tracked`);
|
|
1572
|
+
if (facts.pids) {
|
|
1573
|
+
const live = facts.pids.filter(pidAlive).length;
|
|
1574
|
+
parts.push(`${live} live and ${facts.pids.length - live} dead session(s)`);
|
|
1575
|
+
}
|
|
1576
|
+
out.push(result("database-stats", "info", `database ${parts.join(", ")}`));
|
|
1577
|
+
return out;
|
|
1578
|
+
}
|
|
1579
|
+
function isExecutable(path) {
|
|
1580
|
+
try {
|
|
1581
|
+
if (!statSync2(path).isFile()) return false;
|
|
1582
|
+
accessSync(path, constants.X_OK);
|
|
1583
|
+
return true;
|
|
1584
|
+
} catch {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function commandResolves(command2, platform) {
|
|
1589
|
+
if (isAbsolute2(command2) || command2.includes("/") || command2.includes("\\")) return isExecutable(command2);
|
|
1590
|
+
const exts = platform === "win32" ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
|
|
1591
|
+
return (process.env.PATH ?? "").split(delimiter).filter(Boolean).some((dir) => exts.some((ext) => isExecutable(join4(dir, command2 + ext))));
|
|
1592
|
+
}
|
|
1593
|
+
function splitCommand(entry) {
|
|
1594
|
+
const raw = entry.command;
|
|
1595
|
+
if (Array.isArray(raw) && typeof raw[0] === "string") return { command: raw[0], args: raw.slice(1).map(String) };
|
|
1596
|
+
if (typeof raw !== "string") return void 0;
|
|
1597
|
+
return { command: raw, args: Array.isArray(entry.args) ? entry.args.map(String) : [] };
|
|
1598
|
+
}
|
|
1599
|
+
function checkMcpRegistration(o) {
|
|
1600
|
+
const out = [];
|
|
1601
|
+
let found = 0;
|
|
1602
|
+
for (const t of editorTargets(o.home)) {
|
|
1603
|
+
if (!existsSync4(t.path)) continue;
|
|
1604
|
+
found++;
|
|
1605
|
+
const id = `mcp-${t.name.toLowerCase().replace(/\s+/g, "-")}`;
|
|
1606
|
+
let config;
|
|
1607
|
+
try {
|
|
1608
|
+
config = readJson(t.path);
|
|
1609
|
+
} catch {
|
|
1610
|
+
out.push(result(id, "warn", `${t.name}: ${t.path} is not valid JSON`, `fix the file, then run: agent-file-stash init`));
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const entry = asRecord(asRecord(asRecord(config)?.[t.key])?.filestash);
|
|
1614
|
+
if (!entry) {
|
|
1615
|
+
out.push(result(id, "warn", `${t.name}: no filestash entry in ${t.path}`, "run: agent-file-stash init"));
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
const cmd = splitCommand(entry);
|
|
1619
|
+
if (!cmd) {
|
|
1620
|
+
out.push(result(id, "warn", `${t.name}: the filestash entry in ${t.path} has no command`, "run: agent-file-stash init"));
|
|
1621
|
+
} else if (!commandResolves(cmd.command, o.platform)) {
|
|
1622
|
+
out.push(result(id, "warn", `${t.name}: command "${cmd.command}" was not found or is not executable`, `install it or fix the command in ${t.path}`));
|
|
1623
|
+
} else if (!cmd.args.includes("serve")) {
|
|
1624
|
+
out.push(result(id, "warn", `${t.name}: the filestash entry does not run "serve"`, `set the arguments to: agent-file-stash serve`));
|
|
1625
|
+
} else {
|
|
1626
|
+
out.push(result(id, "ok", `${t.name}: filestash registered in ${t.path}`));
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
if (found === 0) return [result("mcp", "info", "no editor configuration found (Claude Code, Cursor, OpenCode)")];
|
|
1630
|
+
return out;
|
|
1631
|
+
}
|
|
1632
|
+
function matcherCovers(matcher, event) {
|
|
1633
|
+
if (matcher === void 0 || matcher === "" || matcher === "*") return true;
|
|
1634
|
+
if (typeof matcher !== "string") return false;
|
|
1635
|
+
try {
|
|
1636
|
+
return new RegExp(`^(?:${matcher})$`).test(event);
|
|
1637
|
+
} catch {
|
|
1638
|
+
return matcher.split("|").includes(event);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function commandRuns(command2, o, subcommand) {
|
|
1642
|
+
const mentions = (text) => text.includes(PACKAGE_NAME) && subcommand.test(text);
|
|
1643
|
+
if (mentions(command2)) return true;
|
|
1644
|
+
const target = command2.trim().replace(/^(?:ba|z)?sh\s+/, "").split(/\s+/)[0].replace(/^["']|["']$/g, "").replace(/^~(?=\/)/, o.home).replace(/\$\{?(?:HOME)\}?/, o.home).replace(/\$\{?CLAUDE_PROJECT_DIR\}?/, o.cwd);
|
|
1645
|
+
try {
|
|
1646
|
+
const path = resolve4(o.cwd, target);
|
|
1647
|
+
if (!statSync2(path).isFile() || statSync2(path).size > MAX_SCRIPT_BYTES) return false;
|
|
1648
|
+
return mentions(readFileSync3(path, "utf-8"));
|
|
1649
|
+
} catch {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function checkResetHook(o) {
|
|
1654
|
+
const out = [];
|
|
1655
|
+
const covered = /* @__PURE__ */ new Set();
|
|
1656
|
+
let hookFile;
|
|
1657
|
+
let hookFound = false;
|
|
1658
|
+
const files = [
|
|
1659
|
+
join4(o.home, ".claude", "settings.json"),
|
|
1660
|
+
join4(o.cwd, ".claude", "settings.json"),
|
|
1661
|
+
join4(o.cwd, ".claude", "settings.local.json")
|
|
1662
|
+
];
|
|
1663
|
+
for (const file of files) {
|
|
1664
|
+
if (!existsSync4(file)) continue;
|
|
1665
|
+
let settings;
|
|
1666
|
+
try {
|
|
1667
|
+
settings = readJson(file);
|
|
1668
|
+
} catch {
|
|
1669
|
+
out.push(result("hook-settings", "warn", `${file} is not valid JSON`, "fix the file so hooks in it can be checked"));
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
const sessionStart = asRecord(asRecord(settings)?.hooks)?.SessionStart;
|
|
1673
|
+
for (const entry of Array.isArray(sessionStart) ? sessionStart : []) {
|
|
1674
|
+
const e = asRecord(entry);
|
|
1675
|
+
const hooks = Array.isArray(e?.hooks) ? e.hooks : [];
|
|
1676
|
+
const runs = hooks.some((h) => typeof asRecord(h)?.command === "string" && commandRuns(asRecord(h).command, o, /\breset\b/));
|
|
1677
|
+
if (!runs) continue;
|
|
1678
|
+
hookFound = true;
|
|
1679
|
+
hookFile ??= file;
|
|
1680
|
+
for (const ev of SESSION_START_TARGETS) if (matcherCovers(e?.matcher, ev)) covered.add(ev);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
if (!hookFound) {
|
|
1684
|
+
out.push(result("hook", "warn", "no SessionStart hook runs 'agent-file-stash reset'", "run: agent-file-stash init --hooks (without it a read after /clear or /compact can come back as unchanged)"));
|
|
1685
|
+
return out;
|
|
1686
|
+
}
|
|
1687
|
+
const missing = SESSION_START_TARGETS.filter((ev) => !covered.has(ev));
|
|
1688
|
+
if (missing.length > 0) {
|
|
1689
|
+
out.push(result("hook", "warn", `reset hook in ${hookFile} does not match ${missing.join(" or ")}`, `set the matcher to "clear|compact"`));
|
|
1690
|
+
} else {
|
|
1691
|
+
out.push(result("hook", "ok", `reset hook configured for clear and compact (${hookFile})`));
|
|
1692
|
+
}
|
|
1693
|
+
return out;
|
|
1694
|
+
}
|
|
1695
|
+
function checkSubagentScopeHook(o) {
|
|
1696
|
+
const files = [
|
|
1697
|
+
join4(o.home, ".claude", "settings.json"),
|
|
1698
|
+
join4(o.cwd, ".claude", "settings.json"),
|
|
1699
|
+
join4(o.cwd, ".claude", "settings.local.json")
|
|
1700
|
+
];
|
|
1701
|
+
for (const file of files) {
|
|
1702
|
+
if (!existsSync4(file)) continue;
|
|
1703
|
+
let settings;
|
|
1704
|
+
try {
|
|
1705
|
+
settings = readJson(file);
|
|
1706
|
+
} catch {
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
const preToolUse = asRecord(asRecord(settings)?.hooks)?.PreToolUse;
|
|
1710
|
+
for (const entry of Array.isArray(preToolUse) ? preToolUse : []) {
|
|
1711
|
+
const e = asRecord(entry);
|
|
1712
|
+
const hooks = Array.isArray(e?.hooks) ? e.hooks : [];
|
|
1713
|
+
const runs = hooks.some((h) => typeof asRecord(h)?.command === "string" && commandRuns(asRecord(h).command, o, /\bhook\s+subagent-scope\b/));
|
|
1714
|
+
if (runs && matcherCovers(e?.matcher, "mcp__filestash__read_file")) {
|
|
1715
|
+
return [result("hook-subagent-scope", "ok", `subagent scope hook configured (${file})`)];
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
return [result("hook-subagent-scope", "warn", "no PreToolUse hook runs 'agent-file-stash hook subagent-scope' for the stash read tools", "run: agent-file-stash init --hooks (without it subagents share read tracking with the main agent)")];
|
|
1720
|
+
}
|
|
1721
|
+
function checkEnv() {
|
|
1722
|
+
const out = [];
|
|
1723
|
+
const exclude = process.env.FILESTASH_EXCLUDE;
|
|
1724
|
+
if (exclude !== void 0 && exclude !== "") {
|
|
1725
|
+
const n = exclude.split(",").filter((p) => p.trim()).length;
|
|
1726
|
+
out.push(result("env-exclude", "ok", `FILESTASH_EXCLUDE: ${n} pattern(s)`));
|
|
1727
|
+
}
|
|
1728
|
+
for (const name of ["FILESTASH_MAX_LINES", "FILESTASH_MAX_CHARS"]) {
|
|
1729
|
+
const raw = process.env[name];
|
|
1730
|
+
if (raw === void 0 || raw === "") continue;
|
|
1731
|
+
const valid = /^[1-9]\d*$/.test(raw) && Number.isSafeInteger(Number(raw));
|
|
1732
|
+
out.push(
|
|
1733
|
+
valid ? result(`env-${name.toLowerCase()}`, "ok", `${name}=${raw}`) : result(`env-${name.toLowerCase()}`, "warn", `${name}=${JSON.stringify(raw)} is not a positive integer and is ignored`, `set ${name} to a positive integer or unset it`)
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
return out;
|
|
1737
|
+
}
|
|
1738
|
+
function npmLatestVersion() {
|
|
1739
|
+
const res = spawnSync("npm", ["view", PACKAGE_NAME, "version"], {
|
|
1740
|
+
encoding: "utf-8",
|
|
1741
|
+
timeout: NPM_TIMEOUT_MS,
|
|
1742
|
+
shell: process.platform === "win32"
|
|
1743
|
+
});
|
|
1744
|
+
const version = res.status === 0 ? res.stdout.trim() : "";
|
|
1745
|
+
return /^\d+\.\d+\.\d+/.test(version) ? version : void 0;
|
|
1746
|
+
}
|
|
1747
|
+
function newer(a, b) {
|
|
1748
|
+
const pa = a.split(/[.-]/).map(Number);
|
|
1749
|
+
const pb = b.split(/[.-]/).map(Number);
|
|
1750
|
+
for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
function checkUpdates(o) {
|
|
1754
|
+
if (!o.checkUpdates) return [];
|
|
1755
|
+
const installed = ownPackageJson()?.version;
|
|
1756
|
+
const latest = o.fetchLatest();
|
|
1757
|
+
if (!installed || !latest) return [result("updates", "info", "could not check for updates")];
|
|
1758
|
+
if (newer(latest, installed)) return [result("updates", "info", `update available: ${installed} -> ${latest} (npm install -g ${PACKAGE_NAME})`)];
|
|
1759
|
+
return [result("updates", "ok", `agent-file-stash ${installed} is up to date`)];
|
|
1760
|
+
}
|
|
1761
|
+
var CHECKS = [
|
|
1762
|
+
["node", checkNode],
|
|
1763
|
+
["stash-dir", checkStashDir],
|
|
1764
|
+
["database", checkDatabase],
|
|
1765
|
+
["mcp", checkMcpRegistration],
|
|
1766
|
+
["hook", checkResetHook],
|
|
1767
|
+
["hook-subagent-scope", checkSubagentScopeHook],
|
|
1768
|
+
["env", checkEnv],
|
|
1769
|
+
["updates", checkUpdates]
|
|
1770
|
+
];
|
|
1771
|
+
function defaultOptions(argv = []) {
|
|
1772
|
+
return {
|
|
1773
|
+
nodeVersion: process.versions.node,
|
|
1774
|
+
cwd: process.cwd(),
|
|
1775
|
+
home: homedir(),
|
|
1776
|
+
platform: process.platform,
|
|
1777
|
+
now: Date.now(),
|
|
1778
|
+
checkUpdates: argv.includes("--check-updates"),
|
|
1779
|
+
fetchLatest: npmLatestVersion
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
async function runChecks(o, checks = CHECKS) {
|
|
1783
|
+
const results = [];
|
|
1784
|
+
for (const [id, check] of checks) {
|
|
1785
|
+
try {
|
|
1786
|
+
results.push(...await check(o));
|
|
1787
|
+
} catch (e) {
|
|
1788
|
+
results.push(result(id, "warn", `check failed: ${reason(e)}`));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return results;
|
|
1792
|
+
}
|
|
1793
|
+
function summarize(results) {
|
|
1794
|
+
const count = (s) => results.filter((r) => r.status === s).length;
|
|
1795
|
+
return { ok: count("ok"), warnings: count("warn"), errors: count("error") };
|
|
1796
|
+
}
|
|
1797
|
+
function formatHuman(results) {
|
|
1798
|
+
const lines = results.flatMap((r) => [`[${r.status}] ${r.message}`, ...r.hint ? [` ${r.hint}`] : []]);
|
|
1799
|
+
const s = summarize(results);
|
|
1800
|
+
return [...lines, "", `${s.ok} ok, ${s.warnings} warnings, ${s.errors} errors`].join("\n");
|
|
1801
|
+
}
|
|
1802
|
+
async function runDoctor(argv, overrides = {}) {
|
|
1803
|
+
const results = await runChecks({ ...defaultOptions(argv), ...overrides });
|
|
1804
|
+
console.log(argv.includes("--json") ? JSON.stringify(results, null, 2) : formatHuman(results));
|
|
1805
|
+
return summarize(results).errors > 0 ? 1 : 0;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
// packages/cli/src/hook.ts
|
|
1809
|
+
var HOOK_STDIN_TIMEOUT_MS = 1e3;
|
|
1810
|
+
var AGENT_ID_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1811
|
+
var STASH_READ_TOOL = /__read_files?$/;
|
|
1812
|
+
async function readHookStdin() {
|
|
1813
|
+
if (process.stdin.isTTY) return "";
|
|
1814
|
+
const read = (async () => {
|
|
1815
|
+
let raw = "";
|
|
1816
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
1817
|
+
return raw;
|
|
1818
|
+
})();
|
|
1819
|
+
const timeout = new Promise((res) => setTimeout(() => res(""), HOOK_STDIN_TIMEOUT_MS).unref());
|
|
1820
|
+
return (await Promise.race([read, timeout])).trim();
|
|
1821
|
+
}
|
|
1822
|
+
function subagentScopeOutput(raw) {
|
|
1823
|
+
try {
|
|
1824
|
+
const input = JSON.parse(raw);
|
|
1825
|
+
const { agent_id: agentId, tool_name: toolName, tool_input: toolInput } = input;
|
|
1826
|
+
if (typeof agentId !== "string" || !AGENT_ID_PATTERN.test(agentId)) return void 0;
|
|
1827
|
+
if (typeof toolName !== "string" || !STASH_READ_TOOL.test(toolName)) return void 0;
|
|
1828
|
+
if (typeof toolInput !== "object" || toolInput === null || Array.isArray(toolInput)) return void 0;
|
|
1829
|
+
return JSON.stringify({
|
|
1830
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", updatedInput: { ...toolInput, agent: agentId } }
|
|
1831
|
+
});
|
|
1832
|
+
} catch {
|
|
1833
|
+
return void 0;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
async function runSubagentScopeHook() {
|
|
1837
|
+
try {
|
|
1838
|
+
const out = subagentScopeOutput(await readHookStdin());
|
|
1839
|
+
if (out) process.stdout.write(out + "\n");
|
|
1840
|
+
} catch {
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1226
1844
|
// packages/cli/src/index.ts
|
|
1227
1845
|
var _origEmitWarning = process.emitWarning;
|
|
1228
1846
|
process.emitWarning = function(msg, ...args) {
|
|
@@ -1235,19 +1853,16 @@ var RESET_HOOK_ENTRY = {
|
|
|
1235
1853
|
matcher: "clear|compact",
|
|
1236
1854
|
hooks: [{ type: "command", command: RESET_HOOK_COMMAND }]
|
|
1237
1855
|
};
|
|
1238
|
-
var
|
|
1856
|
+
var SCOPE_HOOK_COMMAND = "npx agent-file-stash hook subagent-scope";
|
|
1857
|
+
var SCOPE_HOOK_ENTRY = {
|
|
1858
|
+
matcher: "mcp__(filestash|agent-file-stash)__read_files?",
|
|
1859
|
+
hooks: [{ type: "command", command: SCOPE_HOOK_COMMAND }]
|
|
1860
|
+
};
|
|
1239
1861
|
function assertHealthy(stash) {
|
|
1240
1862
|
if (stash.isDegraded) throw new Error(stash.degradedReason);
|
|
1241
1863
|
}
|
|
1242
1864
|
async function readHookCwd() {
|
|
1243
|
-
|
|
1244
|
-
const read = (async () => {
|
|
1245
|
-
let raw2 = "";
|
|
1246
|
-
for await (const chunk of process.stdin) raw2 += chunk;
|
|
1247
|
-
return raw2;
|
|
1248
|
-
})();
|
|
1249
|
-
const timeout = new Promise((res) => setTimeout(() => res(""), HOOK_STDIN_TIMEOUT_MS).unref());
|
|
1250
|
-
const raw = (await Promise.race([read, timeout])).trim();
|
|
1865
|
+
const raw = await readHookStdin();
|
|
1251
1866
|
if (!raw) return void 0;
|
|
1252
1867
|
try {
|
|
1253
1868
|
const cwd = JSON.parse(raw)?.cwd;
|
|
@@ -1259,8 +1874,8 @@ async function readHookCwd() {
|
|
|
1259
1874
|
async function runReset(fromHook) {
|
|
1260
1875
|
try {
|
|
1261
1876
|
const hookCwd = fromHook ? await readHookCwd() : void 0;
|
|
1262
|
-
const dbPath =
|
|
1263
|
-
if (!
|
|
1877
|
+
const dbPath = join5(resolveStashDir(hookCwd), "stash.db");
|
|
1878
|
+
if (!existsSync5(dbPath)) {
|
|
1264
1879
|
if (!fromHook) console.log("No filestash database found. Nothing to reset.");
|
|
1265
1880
|
return;
|
|
1266
1881
|
}
|
|
@@ -1280,36 +1895,47 @@ async function runReset(fromHook) {
|
|
|
1280
1895
|
}
|
|
1281
1896
|
}
|
|
1282
1897
|
function hooksSnippet() {
|
|
1283
|
-
return JSON.stringify({ hooks: { SessionStart: [RESET_HOOK_ENTRY] } }, null, 2);
|
|
1898
|
+
return JSON.stringify({ hooks: { SessionStart: [RESET_HOOK_ENTRY], PreToolUse: [SCOPE_HOOK_ENTRY] } }, null, 2);
|
|
1899
|
+
}
|
|
1900
|
+
var HOOK_INSTALLS = [
|
|
1901
|
+
{ event: "SessionStart", entry: RESET_HOOK_ENTRY },
|
|
1902
|
+
{ event: "PreToolUse", entry: SCOPE_HOOK_ENTRY }
|
|
1903
|
+
];
|
|
1904
|
+
function hasHookCommand(entries, command2) {
|
|
1905
|
+
return Array.isArray(entries) && entries.some((e) => e?.hooks?.some((h) => h?.command === command2));
|
|
1284
1906
|
}
|
|
1285
1907
|
function installClaudeHook(home) {
|
|
1286
|
-
const settingsPath =
|
|
1908
|
+
const settingsPath = join5(home, ".claude", "settings.json");
|
|
1287
1909
|
let settings = {};
|
|
1288
|
-
const existed =
|
|
1910
|
+
const existed = existsSync5(settingsPath);
|
|
1289
1911
|
if (existed) {
|
|
1290
1912
|
try {
|
|
1291
|
-
settings = JSON.parse(
|
|
1913
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
|
|
1292
1914
|
} catch {
|
|
1293
|
-
console.error(` Claude Code
|
|
1915
|
+
console.error(` Claude Code hooks: ${settingsPath} is not valid JSON, left untouched`);
|
|
1294
1916
|
return;
|
|
1295
1917
|
}
|
|
1296
1918
|
}
|
|
1297
1919
|
const hooks = settings.hooks ?? {};
|
|
1298
|
-
const
|
|
1299
|
-
if (
|
|
1300
|
-
console.log(" Claude Code
|
|
1920
|
+
const missing = HOOK_INSTALLS.filter(({ event, entry }) => !hasHookCommand(hooks[event], entry.hooks[0].command));
|
|
1921
|
+
if (missing.length === 0) {
|
|
1922
|
+
console.log(" Claude Code hooks: already configured");
|
|
1301
1923
|
return;
|
|
1302
1924
|
}
|
|
1303
1925
|
if (existed) copyFileSync(settingsPath, `${settingsPath}.bak`);
|
|
1304
|
-
else mkdirSync2(
|
|
1305
|
-
|
|
1926
|
+
else mkdirSync2(join5(home, ".claude"), { recursive: true });
|
|
1927
|
+
const merged = { ...hooks };
|
|
1928
|
+
for (const { event, entry } of missing) {
|
|
1929
|
+
merged[event] = [...Array.isArray(hooks[event]) ? hooks[event] : [], entry];
|
|
1930
|
+
}
|
|
1931
|
+
settings.hooks = merged;
|
|
1306
1932
|
writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1307
|
-
console.log(` Claude Code
|
|
1933
|
+
console.log(` Claude Code hooks: configured (${settingsPath})`);
|
|
1308
1934
|
}
|
|
1309
1935
|
async function runStatus() {
|
|
1310
|
-
const stashDir =
|
|
1311
|
-
const dbPath =
|
|
1312
|
-
if (!
|
|
1936
|
+
const stashDir = resolve5(process.env.FILESTASH_DIR ?? ".file-stash");
|
|
1937
|
+
const dbPath = join5(stashDir, "stash.db");
|
|
1938
|
+
if (!existsSync5(dbPath)) {
|
|
1313
1939
|
console.log("No filestash database found. Run 'agent-file-stash serve' to start stashing.");
|
|
1314
1940
|
process.exit(0);
|
|
1315
1941
|
}
|
|
@@ -1322,6 +1948,7 @@ async function runStatus() {
|
|
|
1322
1948
|
console.log(`filestash status:`);
|
|
1323
1949
|
console.log(` Files tracked: ${stats.filesTracked}`);
|
|
1324
1950
|
console.log(` Tokens saved (total): ~${stats.tokensSaved.toLocaleString()}`);
|
|
1951
|
+
console.log(formatLifetime(stats, stats.countersSince, `"Tokens saved (total)"`).join("\n"));
|
|
1325
1952
|
} catch (e) {
|
|
1326
1953
|
console.error(`filestash status failed for ${dbPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1327
1954
|
process.exitCode = 1;
|
|
@@ -1330,19 +1957,27 @@ async function runStatus() {
|
|
|
1330
1957
|
}
|
|
1331
1958
|
}
|
|
1332
1959
|
async function runStatusAll(root) {
|
|
1333
|
-
const databases = findStashDatabases(
|
|
1960
|
+
const databases = findStashDatabases(resolve5(root));
|
|
1334
1961
|
if (databases.length === 0) {
|
|
1335
|
-
console.log(`No filestash databases found under ${
|
|
1962
|
+
console.log(`No filestash databases found under ${resolve5(root)}.`);
|
|
1336
1963
|
return;
|
|
1337
1964
|
}
|
|
1338
1965
|
const rows = [];
|
|
1966
|
+
const lifetime = { lifetimeSessions: 0, lifetimeReads: 0, lifetimeBaselineTokens: 0, lifetimeSentTokens: 0, lifetimeOverheadTokens: 0 };
|
|
1967
|
+
let since;
|
|
1339
1968
|
for (const dbPath of databases) {
|
|
1340
1969
|
const { stash } = createStash({ dbPath, sessionId: CLI_STATUS_SESSION, recoverCorrupt: false, quiet: true });
|
|
1341
1970
|
try {
|
|
1342
1971
|
await stash.init();
|
|
1343
1972
|
const stats = await stash.getStats();
|
|
1344
1973
|
assertHealthy(stash);
|
|
1345
|
-
|
|
1974
|
+
lifetime.lifetimeSessions += stats.lifetimeSessions;
|
|
1975
|
+
lifetime.lifetimeReads += stats.lifetimeReads;
|
|
1976
|
+
lifetime.lifetimeBaselineTokens += stats.lifetimeBaselineTokens;
|
|
1977
|
+
lifetime.lifetimeSentTokens += stats.lifetimeSentTokens;
|
|
1978
|
+
lifetime.lifetimeOverheadTokens += stats.lifetimeOverheadTokens;
|
|
1979
|
+
if (stats.countersSince !== void 0) since = Math.min(since ?? Infinity, stats.countersSince);
|
|
1980
|
+
rows.push({ project: resolve5(dbPath, "../..").replace(/\/\.vscode$/, ""), files: stats.filesTracked, tokens: stats.tokensSaved });
|
|
1346
1981
|
} catch (e) {
|
|
1347
1982
|
console.error(` skipped ${dbPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
1348
1983
|
} finally {
|
|
@@ -1352,53 +1987,26 @@ async function runStatusAll(root) {
|
|
|
1352
1987
|
rows.sort((a, b) => b.tokens - a.tokens);
|
|
1353
1988
|
const totalTokens = rows.reduce((sum, r) => sum + r.tokens, 0);
|
|
1354
1989
|
const totalFiles = rows.reduce((sum, r) => sum + r.files, 0);
|
|
1355
|
-
console.log(`filestash status (${rows.length} databases under ${
|
|
1990
|
+
console.log(`filestash status (${rows.length} databases under ${resolve5(root)}):`);
|
|
1356
1991
|
for (const r of rows) {
|
|
1357
1992
|
console.log(` ~${r.tokens.toLocaleString().padStart(10)} tokens ${String(r.files).padStart(5)} files ${r.project}`);
|
|
1358
1993
|
}
|
|
1359
1994
|
console.log(` ~${totalTokens.toLocaleString().padStart(10)} tokens ${String(totalFiles).padStart(5)} files TOTAL`);
|
|
1995
|
+
console.log(formatLifetime(lifetime, since, "the per-project totals").join("\n"));
|
|
1360
1996
|
console.log(`
|
|
1361
1997
|
Savings count re-reads within a session (unchanged files and diffs); a new session always receives full content.`);
|
|
1362
1998
|
}
|
|
1363
1999
|
async function runInit(withHooks) {
|
|
1364
|
-
const home =
|
|
1365
|
-
const
|
|
1366
|
-
command: "npx",
|
|
1367
|
-
args: ["agent-file-stash", "serve"]
|
|
1368
|
-
};
|
|
1369
|
-
const opencodeMcpEntry = {
|
|
1370
|
-
type: "local",
|
|
1371
|
-
command: ["npx", "agent-file-stash", "serve"]
|
|
1372
|
-
};
|
|
1373
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME || join3(home, ".config");
|
|
1374
|
-
const targets = [
|
|
1375
|
-
{
|
|
1376
|
-
name: "Claude Code",
|
|
1377
|
-
path: join3(home, ".claude.json"),
|
|
1378
|
-
key: "mcpServers",
|
|
1379
|
-
entry: mcpServersEntry
|
|
1380
|
-
},
|
|
1381
|
-
{
|
|
1382
|
-
name: "Cursor",
|
|
1383
|
-
path: join3(home, ".cursor", "mcp.json"),
|
|
1384
|
-
key: "mcpServers",
|
|
1385
|
-
entry: mcpServersEntry
|
|
1386
|
-
},
|
|
1387
|
-
{
|
|
1388
|
-
name: "OpenCode",
|
|
1389
|
-
path: join3(xdgConfig, "opencode", "opencode.json"),
|
|
1390
|
-
key: "mcp",
|
|
1391
|
-
entry: opencodeMcpEntry
|
|
1392
|
-
}
|
|
1393
|
-
];
|
|
2000
|
+
const home = homedir2();
|
|
2001
|
+
const targets = editorTargets(home);
|
|
1394
2002
|
let configured = 0;
|
|
1395
2003
|
for (const target of targets) {
|
|
1396
|
-
const dir =
|
|
1397
|
-
if (!
|
|
2004
|
+
const dir = join5(target.path, "..");
|
|
2005
|
+
if (!existsSync5(dir)) continue;
|
|
1398
2006
|
let config = {};
|
|
1399
|
-
if (
|
|
2007
|
+
if (existsSync5(target.path)) {
|
|
1400
2008
|
try {
|
|
1401
|
-
config = JSON.parse(
|
|
2009
|
+
config = JSON.parse(readFileSync4(target.path, "utf-8"));
|
|
1402
2010
|
} catch {
|
|
1403
2011
|
config = {};
|
|
1404
2012
|
}
|
|
@@ -1416,7 +2024,7 @@ async function runInit(withHooks) {
|
|
|
1416
2024
|
}
|
|
1417
2025
|
if (configured === 0) {
|
|
1418
2026
|
console.log("No supported tools detected. You can manually add agent-file-stash to your MCP config:");
|
|
1419
|
-
console.log(JSON.stringify({ mcpServers: { "agent-file-stash":
|
|
2027
|
+
console.log(JSON.stringify({ mcpServers: { "agent-file-stash": targets[0].entry } }, null, 2));
|
|
1420
2028
|
} else {
|
|
1421
2029
|
console.log(`
|
|
1422
2030
|
Done! Restart your editor to pick up agent-file-stash.`);
|
|
@@ -1445,13 +2053,17 @@ function runHelp() {
|
|
|
1445
2053
|
Usage:
|
|
1446
2054
|
agent-file-stash init Auto-configure for your editor
|
|
1447
2055
|
agent-file-stash init --hooks
|
|
1448
|
-
Also add the Claude Code context
|
|
2056
|
+
Also add the Claude Code hooks (context reset, subagent read scope) to ~/.claude/settings.json
|
|
1449
2057
|
agent-file-stash reset Forget what was read so the next read returns full content
|
|
1450
2058
|
(--from-hook: quiet mode for Claude Code hooks)
|
|
2059
|
+
agent-file-stash hook subagent-scope
|
|
2060
|
+
PreToolUse hook that gives each subagent its own read tracking (no output otherwise)
|
|
1451
2061
|
agent-file-stash serve Start the MCP server (default)
|
|
1452
2062
|
agent-file-stash status Show stash statistics
|
|
1453
2063
|
agent-file-stash status --all [dir]
|
|
1454
2064
|
Sum statistics of every stash under dir (default: home)
|
|
2065
|
+
agent-file-stash doctor Check the install and report what to fix (read-only)
|
|
2066
|
+
(--json: machine-readable output, --check-updates: ask npm for the latest version)
|
|
1455
2067
|
agent-file-stash help Show this help message
|
|
1456
2068
|
|
|
1457
2069
|
Environment:
|
|
@@ -1464,13 +2076,17 @@ var command = process.argv[2];
|
|
|
1464
2076
|
if (!command || command === "serve") {
|
|
1465
2077
|
await startMcpServer();
|
|
1466
2078
|
} else if (command === "status" && process.argv[3] === "--all") {
|
|
1467
|
-
await runStatusAll(process.argv[4] ??
|
|
2079
|
+
await runStatusAll(process.argv[4] ?? homedir2());
|
|
1468
2080
|
} else if (command === "status") {
|
|
1469
2081
|
await runStatus();
|
|
1470
2082
|
} else if (command === "init") {
|
|
1471
2083
|
await runInit(process.argv.includes("--hooks"));
|
|
1472
2084
|
} else if (command === "reset") {
|
|
1473
2085
|
await runReset(process.argv.includes("--from-hook"));
|
|
2086
|
+
} else if (command === "hook" && process.argv[3] === "subagent-scope") {
|
|
2087
|
+
await runSubagentScopeHook();
|
|
2088
|
+
} else if (command === "doctor") {
|
|
2089
|
+
process.exitCode = await runDoctor(process.argv.slice(3));
|
|
1474
2090
|
} else if (command === "help" || command === "--help") {
|
|
1475
2091
|
runHelp();
|
|
1476
2092
|
} else {
|