@mxalbert/context-mode 2.0.2 → 2.0.4
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/README.md +1 -1
- package/build/adapters/opencode/plugin.d.ts +60 -2
- package/build/adapters/opencode/plugin.js +89 -1
- package/build/adapters/opencode/v2.d.ts +11 -0
- package/build/cli.js +2 -2
- package/build/security.d.ts +44 -10
- package/build/security.js +147 -32
- package/build/server.js +100 -49
- package/build/session/purge.js +2 -1
- package/build/store.d.ts +22 -3
- package/build/store.js +32 -34
- package/cli.bundle.mjs +197 -197
- package/configs/antigravity-cli/plugin.json +1 -1
- package/configs/copilot-cli/.github/plugin/plugin.json +1 -1
- package/hooks/core/routing.mjs +169 -6
- package/hooks/security.bundle.mjs +2 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +134 -134
package/build/server.js
CHANGED
|
@@ -21,7 +21,7 @@ import { detectRuntimes, getRuntimeSummary, getAvailableLanguages, hasBunRuntime
|
|
|
21
21
|
import { classifyNonZeroExit } from "./exit-classify.js";
|
|
22
22
|
import { startLifecycleGuard, noteMcpActivity, noteRequestStart, noteRequestEnd, attachMcpActivityTap } from "./lifecycle.js";
|
|
23
23
|
import { charSafePrefix } from "./truncate.js";
|
|
24
|
-
import { describeStorageDirectorySource, ensureWritableStorageDir, formatStorageDirectoryError, hashProjectDirCanonical, hashProjectDirLegacy, resolveContentStorePath, resolveContentStorageDir, resolveDefaultSessionDir, resolveSessionDbPath, resolveSessionStorageDir, resolveStatsStorageDir, SessionDB, StorageDirectoryError, } from "./session/db.js";
|
|
24
|
+
import { describeStorageDirectorySource, ensureWritableStorageDir, formatStorageDirectoryError, hashProjectDirCanonical, hashProjectDirLegacy, resolveContentStorePath, resolveContentStorageDir, resolveDefaultSessionDir, resolveSessionDbPath, resolveSessionPath, resolveSessionStorageDir, resolveStatsStorageDir, SessionDB, StorageDirectoryError, } from "./session/db.js";
|
|
25
25
|
import { purgeSession } from "./session/purge.js";
|
|
26
26
|
import { NPM_LATEST_URL, PACKAGE_NAME, PLUGIN_KEY } from "./package-identity.js";
|
|
27
27
|
import { emitCacheHitEvent, emitIndexWriteEvent, emitSandboxExecuteEvent, } from "./session/event-emit.js";
|
|
@@ -370,8 +370,19 @@ process.on("exit", () => { try {
|
|
|
370
370
|
unlinkSync(CM_FS_PRELOAD);
|
|
371
371
|
}
|
|
372
372
|
catch { /* best effort */ } });
|
|
373
|
-
// Lazy
|
|
374
|
-
|
|
373
|
+
// Lazy per-project store cache — no DB overhead unless index/search is used.
|
|
374
|
+
// Keyed by resolved dbPath: multi-project hosts (opencode web serves MANY
|
|
375
|
+
// projects inside ONE process, each plugin tool call arriving with its own
|
|
376
|
+
// projectDir via withProjectDirOverride) get one store per project. A single
|
|
377
|
+
// cached instance would funnel every project's reads/writes into whichever
|
|
378
|
+
// project ran the first ctx_* tool — cross-project knowledge-base exposure
|
|
379
|
+
// AND one long-lived connection whose files other processes' startup
|
|
380
|
+
// cleanups may unlink (surfaces as disk I/O error, SQLITE_IOERR_VNODE).
|
|
381
|
+
// Mirrors the #645 SessionDB singleton re-keying pattern applied to the
|
|
382
|
+
// Pi/OMP/OpenClaw plugins.
|
|
383
|
+
const _stores = new Map();
|
|
384
|
+
/** True once the process-wide startup cleanups have run (see getStore). */
|
|
385
|
+
let _startupCleanupsRan = false;
|
|
375
386
|
/**
|
|
376
387
|
* Build the FK-attribution object passed to every ContentStore.index*() call
|
|
377
388
|
* in this process. CLAUDE_SESSION_ID is the only MCP-side handle we have on
|
|
@@ -434,27 +445,34 @@ export function resolveSessionIdFromSessionDB(opts) {
|
|
|
434
445
|
}
|
|
435
446
|
}
|
|
436
447
|
/**
|
|
437
|
-
* Auto-index session events
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
448
|
+
* Auto-index the CURRENT project's session events file written by the
|
|
449
|
+
* SessionStart hook (same per-project path derivation the hook uses —
|
|
450
|
+
* canonical hash + worktree suffix). The file is consumed (deleted) after
|
|
451
|
+
* indexing to prevent double-indexing. Called on every getStore().
|
|
452
|
+
*
|
|
453
|
+
* Scoped to this project deliberately: the events filename is per-project,
|
|
454
|
+
* and a multi-project host (opencode web) shares one sessions directory
|
|
455
|
+
* across projects. The previous implementation glob-scanned the whole
|
|
456
|
+
* directory, so whichever project ran the first ctx_* tool indexed — and
|
|
457
|
+
* consumed — every OTHER project's pending events into its own store.
|
|
443
458
|
*/
|
|
444
459
|
function maybeIndexSessionEvents(store) {
|
|
445
460
|
try {
|
|
446
461
|
const sessionsDir = getSessionDir();
|
|
447
462
|
if (!existsSync(sessionsDir))
|
|
448
463
|
return;
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
464
|
+
const eventsPath = resolveSessionPath({
|
|
465
|
+
projectDir: getProjectDir(),
|
|
466
|
+
sessionsDir,
|
|
467
|
+
ext: "-events.md",
|
|
468
|
+
});
|
|
469
|
+
if (!existsSync(eventsPath))
|
|
470
|
+
return;
|
|
471
|
+
try {
|
|
472
|
+
store.index({ path: eventsPath, source: "session-events", attribution: currentAttribution() });
|
|
473
|
+
unlinkSync(eventsPath);
|
|
457
474
|
}
|
|
475
|
+
catch { /* best-effort */ }
|
|
458
476
|
}
|
|
459
477
|
catch { /* best-effort — session continuity never blocks tools */ }
|
|
460
478
|
}
|
|
@@ -632,19 +650,27 @@ function getStorePath() {
|
|
|
632
650
|
return resolveContentStorePath({ projectDir: getProjectDir(), contentDir: dir });
|
|
633
651
|
}
|
|
634
652
|
function getStore() {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
653
|
+
// Opens whatever DB exists (or creates a new one). Routine retention
|
|
654
|
+
// never deletes per-platform DB FILES — it is row-level only
|
|
655
|
+
// (cleanupStaleSources below) and on-disk reclamation is ctx_purge's
|
|
656
|
+
// job (rationale in the startup block below). The only file-deleting
|
|
657
|
+
// exception is the constructor's corruption-recovery path.
|
|
658
|
+
const dbPath = getStorePath();
|
|
659
|
+
let store = _stores.get(dbPath);
|
|
660
|
+
if (!store) {
|
|
661
|
+
store = new ContentStore(dbPath);
|
|
662
|
+
_stores.set(dbPath, store);
|
|
640
663
|
// Wire deny-policy hook: store re-checks the Read deny list before
|
|
641
664
|
// re-reading any file_path during auto-refresh. Catches policy edits
|
|
642
|
-
// made after a file was originally indexed. See #442 round-3.
|
|
643
|
-
|
|
665
|
+
// made after a file was originally indexed. See #442 round-3. The
|
|
666
|
+
// project dir is pinned at creation — the store is project-scoped, so
|
|
667
|
+
// its deny policy stays this project's even when other projects make
|
|
668
|
+
// calls into the same process.
|
|
669
|
+
const storeProjectDir = getProjectDir();
|
|
670
|
+
store.setDenyChecker((filePath) => {
|
|
644
671
|
try {
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
const r = evaluateFilePath(filePath, denyGlobs, process.platform === "win32", projectDir);
|
|
672
|
+
const denyGlobs = readToolDenyPatterns("Read", storeProjectDir);
|
|
673
|
+
const r = evaluateFilePath(filePath, denyGlobs, undefined, storeProjectDir);
|
|
648
674
|
return r.denied;
|
|
649
675
|
}
|
|
650
676
|
catch {
|
|
@@ -652,22 +678,36 @@ function getStore() {
|
|
|
652
678
|
return true;
|
|
653
679
|
}
|
|
654
680
|
});
|
|
655
|
-
//
|
|
681
|
+
// Per-store: drop this project's sources untouched for 14 days.
|
|
656
682
|
try {
|
|
657
|
-
|
|
658
|
-
cleanupStaleContentDBs(contentDir, 14);
|
|
659
|
-
_store.cleanupStaleSources(14);
|
|
660
|
-
// Also clean legacy shared dir from before platform isolation
|
|
661
|
-
const legacyDir = join(homedir(), ".context-mode", "content");
|
|
662
|
-
if (existsSync(legacyDir))
|
|
663
|
-
cleanupStaleContentDBs(legacyDir, 0);
|
|
683
|
+
store.cleanupStaleSources(14);
|
|
664
684
|
}
|
|
665
685
|
catch { /* best-effort */ }
|
|
666
|
-
//
|
|
667
|
-
|
|
686
|
+
// Process-wide, once: sweep the legacy shared content dir (pre-platform-
|
|
687
|
+
// isolation) and orphaned PID-based tmpdir DBs from the migration. Runs
|
|
688
|
+
// on FIRST store creation only — not once per project.
|
|
689
|
+
//
|
|
690
|
+
// The per-platform content dir gets NO automatic file deletion: mtime
|
|
691
|
+
// cannot distinguish a long-idle LIVE connection (a web host with a
|
|
692
|
+
// project open but untouched for weeks) from an abandoned DB, and
|
|
693
|
+
// unlinking an open store's db/-wal/-shm surfaces as unrecoverable
|
|
694
|
+
// disk I/O errors (SQLITE_IOERR_VNODE on macOS). Retention inside
|
|
695
|
+
// each store is handled logically above (cleanupStaleSources);
|
|
696
|
+
// on-disk reclamation is ctx_purge's job.
|
|
697
|
+
if (!_startupCleanupsRan) {
|
|
698
|
+
_startupCleanupsRan = true;
|
|
699
|
+
try {
|
|
700
|
+
const legacyDir = join(homedir(), ".context-mode", "content");
|
|
701
|
+
if (existsSync(legacyDir))
|
|
702
|
+
cleanupStaleContentDBs(legacyDir, 0);
|
|
703
|
+
}
|
|
704
|
+
catch { /* best-effort */ }
|
|
705
|
+
// Also clean old PID-based DBs from migration
|
|
706
|
+
cleanupStaleDBs();
|
|
707
|
+
}
|
|
668
708
|
}
|
|
669
|
-
maybeIndexSessionEvents(
|
|
670
|
-
return
|
|
709
|
+
maybeIndexSessionEvents(store);
|
|
710
|
+
return store;
|
|
671
711
|
}
|
|
672
712
|
// ─────────────────────────────────────────────────────────
|
|
673
713
|
// Session stats — track context consumption per tool
|
|
@@ -1099,7 +1139,7 @@ function checkFilePathDenyPolicy(filePath, toolName) {
|
|
|
1099
1139
|
try {
|
|
1100
1140
|
const projectDir = getProjectDir();
|
|
1101
1141
|
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
1102
|
-
const result = evaluateFilePath(filePath, denyGlobs,
|
|
1142
|
+
const result = evaluateFilePath(filePath, denyGlobs, undefined, projectDir);
|
|
1103
1143
|
if (result.denied) {
|
|
1104
1144
|
return trackResponse(toolName, {
|
|
1105
1145
|
content: [{
|
|
@@ -2087,10 +2127,9 @@ EXAMPLE: ctx_index(path: "/path/to/large-spec.md", source: "openapi-v2-spec")`,
|
|
|
2087
2127
|
const store = getStore();
|
|
2088
2128
|
const projectDir = getProjectDir();
|
|
2089
2129
|
const denyGlobs = readToolDenyPatterns("Read", projectDir);
|
|
2090
|
-
const isWin32 = process.platform === "win32";
|
|
2091
2130
|
const perFileDeny = (absPath) => {
|
|
2092
2131
|
try {
|
|
2093
|
-
return evaluateFilePath(absPath, denyGlobs,
|
|
2132
|
+
return evaluateFilePath(absPath, denyGlobs, undefined, projectDir).denied;
|
|
2094
2133
|
}
|
|
2095
2134
|
catch {
|
|
2096
2135
|
return false; // fail-open consistent with checkFilePathDenyPolicy
|
|
@@ -4629,12 +4668,17 @@ EXAMPLE: ctx_purge(confirm: true, scope: "project")`,
|
|
|
4629
4668
|
storePathForPurge = getStorePath();
|
|
4630
4669
|
}
|
|
4631
4670
|
catch { /* best effort — store path may be unresolvable on fresh install */ }
|
|
4632
|
-
if (
|
|
4633
|
-
|
|
4634
|
-
|
|
4671
|
+
if (storePathForPurge) {
|
|
4672
|
+
// Close ONLY the calling project's store — other projects' stores in a
|
|
4673
|
+
// multi-project host (opencode web) are neither purged nor disturbed.
|
|
4674
|
+
const openStore = _stores.get(storePathForPurge);
|
|
4675
|
+
if (openStore) {
|
|
4676
|
+
try {
|
|
4677
|
+
openStore.cleanup();
|
|
4678
|
+
}
|
|
4679
|
+
catch { /* best effort */ }
|
|
4680
|
+
_stores.delete(storePathForPurge);
|
|
4635
4681
|
}
|
|
4636
|
-
catch { /* best effort */ }
|
|
4637
|
-
_store = null;
|
|
4638
4682
|
}
|
|
4639
4683
|
// FTS5 store: pass contentDir so purgeSession sweeps BOTH canonical
|
|
4640
4684
|
// and legacy raw-casing variants (dual-hash, mirrors session events).
|
|
@@ -4906,8 +4950,15 @@ async function main() {
|
|
|
4906
4950
|
// Clean up own DB + backgrounded processes + preload script on shutdown
|
|
4907
4951
|
const shutdown = () => {
|
|
4908
4952
|
executor.cleanupBackgrounded();
|
|
4909
|
-
|
|
4910
|
-
|
|
4953
|
+
// Persist every open per-project DB for --continue sessions. Cleared
|
|
4954
|
+
// after close so a repeated shutdown call cannot double-close a store.
|
|
4955
|
+
for (const openStore of _stores.values()) {
|
|
4956
|
+
try {
|
|
4957
|
+
openStore.close();
|
|
4958
|
+
}
|
|
4959
|
+
catch { /* best effort */ }
|
|
4960
|
+
}
|
|
4961
|
+
_stores.clear();
|
|
4911
4962
|
try {
|
|
4912
4963
|
unlinkSync(CM_FS_PRELOAD);
|
|
4913
4964
|
}
|
package/build/session/purge.js
CHANGED
|
@@ -131,7 +131,8 @@ export function purgeSession(opts) {
|
|
|
131
131
|
//
|
|
132
132
|
// Caller is responsible for closing any persistent ContentStore
|
|
133
133
|
// handle BEFORE invoking purgeSession (Windows file lock). The
|
|
134
|
-
// ctx_purge handler does this via
|
|
134
|
+
// ctx_purge handler does this via the per-project store cache's
|
|
135
|
+
// cleanup() before delegating.
|
|
135
136
|
const ftsTargets = [];
|
|
136
137
|
if (storePath && existsSync(storePath))
|
|
137
138
|
ftsTargets.push(storePath);
|
package/build/store.d.ts
CHANGED
|
@@ -19,9 +19,28 @@ export declare function sanitizeTrigramQuery(query: string, mode?: "AND" | "OR")
|
|
|
19
19
|
export declare function cleanupStaleDBs(): number;
|
|
20
20
|
/**
|
|
21
21
|
* Clean up stale per-project content store DBs older than maxAgeDays.
|
|
22
|
-
* Scans the given directory for *.db files and
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Scans the given directory for *.db files and unlinks each (plus its
|
|
23
|
+
* -wal/-shm sidecars) when its EFFECTIVE last-write time — the newer of the
|
|
24
|
+
* main .db mtime and a non-empty -wal mtime — exceeds the cutoff.
|
|
25
|
+
*
|
|
26
|
+
* Why the -wal mtime participates: in WAL mode commits touch only the -wal
|
|
27
|
+
* file; the main .db mtime advances at checkpoint. A main-only check can
|
|
28
|
+
* delete an actively-used store that simply hasn't checkpointed within the
|
|
29
|
+
* window. Conversely, a non-empty -wal older than some threshold is NOT
|
|
30
|
+
* proof the owning process is dead — a live-but-idle connection (a session
|
|
31
|
+
* quiet over lunch) is indistinguishable from a crashed process by mtime
|
|
32
|
+
* alone. The pre-fix heuristic deleted such DBs unconditionally after 1h,
|
|
33
|
+
* which on macOS unlinks the files under an open connection and surfaces
|
|
34
|
+
* as disk I/O error (SQLITE_IOERR_VNODE) on every later write — no amount
|
|
35
|
+
* of retrying recovers an invalidated fd.
|
|
36
|
+
*
|
|
37
|
+
* Contract: a -wal can only EXTEND a DB's life (its mtime counts as the
|
|
38
|
+
* latest write), never shorten it. A WAL never triggers deletion on its
|
|
39
|
+
* own — the way the old 1-hour zombie rule did — and once the whole DB
|
|
40
|
+
* (main + non-empty wal) is beyond maxAgeDays, the caller's retention
|
|
41
|
+
* policy owns the consequences. Callers that cannot tolerate deleting a
|
|
42
|
+
* possibly-live DB (the per-platform content dir — see getStore() in
|
|
43
|
+
* src/server.ts) must not call this with a nonzero maxAgeDays.
|
|
25
44
|
*/
|
|
26
45
|
export declare function cleanupStaleContentDBs(contentDir: string, maxAgeDays: number): number;
|
|
27
46
|
export declare class ContentStore {
|
package/build/store.js
CHANGED
|
@@ -158,24 +158,30 @@ export function cleanupStaleDBs() {
|
|
|
158
158
|
catch { /* ignore readdir errors */ }
|
|
159
159
|
return cleaned;
|
|
160
160
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Check if a PID is still alive (not a zombie holding a WAL lock).
|
|
163
|
-
* Returns true if the process exists, false if it's dead.
|
|
164
|
-
*/
|
|
165
|
-
function isProcessAlive(pid) {
|
|
166
|
-
try {
|
|
167
|
-
process.kill(pid, 0);
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
161
|
/**
|
|
175
162
|
* Clean up stale per-project content store DBs older than maxAgeDays.
|
|
176
|
-
* Scans the given directory for *.db files and
|
|
177
|
-
*
|
|
178
|
-
*
|
|
163
|
+
* Scans the given directory for *.db files and unlinks each (plus its
|
|
164
|
+
* -wal/-shm sidecars) when its EFFECTIVE last-write time — the newer of the
|
|
165
|
+
* main .db mtime and a non-empty -wal mtime — exceeds the cutoff.
|
|
166
|
+
*
|
|
167
|
+
* Why the -wal mtime participates: in WAL mode commits touch only the -wal
|
|
168
|
+
* file; the main .db mtime advances at checkpoint. A main-only check can
|
|
169
|
+
* delete an actively-used store that simply hasn't checkpointed within the
|
|
170
|
+
* window. Conversely, a non-empty -wal older than some threshold is NOT
|
|
171
|
+
* proof the owning process is dead — a live-but-idle connection (a session
|
|
172
|
+
* quiet over lunch) is indistinguishable from a crashed process by mtime
|
|
173
|
+
* alone. The pre-fix heuristic deleted such DBs unconditionally after 1h,
|
|
174
|
+
* which on macOS unlinks the files under an open connection and surfaces
|
|
175
|
+
* as disk I/O error (SQLITE_IOERR_VNODE) on every later write — no amount
|
|
176
|
+
* of retrying recovers an invalidated fd.
|
|
177
|
+
*
|
|
178
|
+
* Contract: a -wal can only EXTEND a DB's life (its mtime counts as the
|
|
179
|
+
* latest write), never shorten it. A WAL never triggers deletion on its
|
|
180
|
+
* own — the way the old 1-hour zombie rule did — and once the whole DB
|
|
181
|
+
* (main + non-empty wal) is beyond maxAgeDays, the caller's retention
|
|
182
|
+
* policy owns the consequences. Callers that cannot tolerate deleting a
|
|
183
|
+
* possibly-live DB (the per-platform content dir — see getStore() in
|
|
184
|
+
* src/server.ts) must not call this with a nonzero maxAgeDays.
|
|
179
185
|
*/
|
|
180
186
|
export function cleanupStaleContentDBs(contentDir, maxAgeDays) {
|
|
181
187
|
let cleaned = 0;
|
|
@@ -187,26 +193,18 @@ export function cleanupStaleContentDBs(contentDir, maxAgeDays) {
|
|
|
187
193
|
for (const file of files) {
|
|
188
194
|
try {
|
|
189
195
|
const filePath = join(contentDir, file);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const walStat = statSync(walPath);
|
|
200
|
-
// If WAL file is non-empty and DB hasn't been modified in >1 hour,
|
|
201
|
-
// the owning process may be dead — check via mtime staleness
|
|
202
|
-
if (walStat.size > 0 && (Date.now() - walStat.mtimeMs) > 3600_000) {
|
|
203
|
-
shouldClean = true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
catch { /* ignore WAL check errors */ }
|
|
196
|
+
// Effective last-write: newer of main .db and non-empty -wal. A fresh
|
|
197
|
+
// WAL is evidence of a live (or recently-live) connection — it can
|
|
198
|
+
// only push the cutoff later, never mark a fresh DB stale.
|
|
199
|
+
let effectiveMs = statSync(filePath).mtimeMs;
|
|
200
|
+
try {
|
|
201
|
+
const walStat = statSync(filePath + "-wal");
|
|
202
|
+
if (walStat.size > 0 && walStat.mtimeMs > effectiveMs) {
|
|
203
|
+
effectiveMs = walStat.mtimeMs;
|
|
207
204
|
}
|
|
208
205
|
}
|
|
209
|
-
|
|
206
|
+
catch { /* no -wal — main mtime stands */ }
|
|
207
|
+
if (effectiveMs < cutoff) {
|
|
210
208
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
211
209
|
try {
|
|
212
210
|
unlinkSync(filePath + suffix);
|