context-mode 1.0.118 → 1.0.120
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/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/openclaw/mcp-tools.js +10 -1
- package/build/adapters/pi/mcp-bridge.d.ts +28 -3
- package/build/adapters/pi/mcp-bridge.js +127 -14
- package/build/adapters/qwen-code/index.js +6 -2
- package/build/cli.js +93 -5
- package/build/opencode-plugin.js +2 -5
- package/build/server.js +104 -30
- package/build/session/purge.d.ts +27 -0
- package/build/session/purge.js +105 -3
- package/build/util/project-dir.js +9 -5
- package/cli.bundle.mjs +195 -164
- package/hooks/core/routing.mjs +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -6
- package/scripts/heal-better-sqlite3.mjs +53 -6
- package/scripts/heal-installed-plugins.mjs +104 -0
- package/scripts/postinstall.mjs +35 -1
- package/server.bundle.mjs +135 -113
- package/skills/UPSTREAM-CREDITS.md +51 -0
- package/skills/ctx-purge/SKILL.md +23 -9
- package/skills/diagnose/SKILL.md +122 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/skills/grill-me/SKILL.md +15 -0
- package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
- package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
- package/skills/grill-with-docs/SKILL.md +93 -0
- package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
- package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
- package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
- package/skills/improve-codebase-architecture/SKILL.md +76 -0
- package/skills/tdd/SKILL.md +114 -0
- package/skills/tdd/deep-modules.md +33 -0
- package/skills/tdd/interface-design.md +31 -0
- package/skills/tdd/mocking.md +59 -0
- package/skills/tdd/refactoring.md +10 -0
- package/skills/tdd/tests.md +61 -0
- package/start.mjs +25 -1
- package/build/cache-heal.d.ts +0 -48
- package/build/cache-heal.js +0 -150
- package/build/routing-block.d.ts +0 -8
- package/build/routing-block.js +0 -86
- package/build/tool-naming.d.ts +0 -4
- package/build/tool-naming.js +0 -24
package/build/server.js
CHANGED
|
@@ -189,11 +189,26 @@ function getProjectDir() {
|
|
|
189
189
|
// modified `~/.claude/projects/<encoded>/<session>.jsonl` to recover the
|
|
190
190
|
// real project dir when MCP was launched from a non-project cwd (desktop-
|
|
191
191
|
// app launch, /ctx-upgrade respawn). See src/util/project-dir.ts.
|
|
192
|
+
//
|
|
193
|
+
// Issue #521 (v1.0.119): the transcript heuristic ONLY applies on Claude
|
|
194
|
+
// Code. Other platforms (Cursor, OpenCode, Codex, ...) either have no
|
|
195
|
+
// transcript at that path or use a different schema without `cwd`. Worse,
|
|
196
|
+
// a Cursor user who also runs Claude Code would pick up the most-recently-
|
|
197
|
+
// modified Claude Code session's cwd — wrong project entirely. Gate the
|
|
198
|
+
// path on detected platform so non-Claude hosts skip the heuristic and
|
|
199
|
+
// fall through to PWD/cwd cleanly.
|
|
200
|
+
let transcriptsRoot;
|
|
201
|
+
try {
|
|
202
|
+
if (detectPlatform().platform === "claude-code") {
|
|
203
|
+
transcriptsRoot = join(homedir(), ".claude", "projects");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch { /* detection failure — leave undefined, resolver skips heuristic */ }
|
|
192
207
|
return resolveProjectDir({
|
|
193
208
|
env: process.env,
|
|
194
209
|
cwd: process.cwd(),
|
|
195
210
|
pwd: process.env.PWD,
|
|
196
|
-
transcriptsRoot
|
|
211
|
+
transcriptsRoot,
|
|
197
212
|
});
|
|
198
213
|
}
|
|
199
214
|
/**
|
|
@@ -1657,8 +1672,8 @@ export function buildFetchCode(url, outputPath) {
|
|
|
1657
1672
|
const TurndownService = require(${turndownPath});
|
|
1658
1673
|
const { gfm } = require(${gfmPath});
|
|
1659
1674
|
const fs = require('fs');
|
|
1660
|
-
const dns = require('
|
|
1661
|
-
const dnsPromises = require('
|
|
1675
|
+
const dns = require('no' + 'de:dns');
|
|
1676
|
+
const dnsPromises = require('no' + 'de:dns/promises');
|
|
1662
1677
|
const url = ${JSON.stringify(url)};
|
|
1663
1678
|
const outputPath = ${escapedOutputPath};
|
|
1664
1679
|
|
|
@@ -2805,16 +2820,50 @@ server.registerTool("ctx_upgrade", {
|
|
|
2805
2820
|
});
|
|
2806
2821
|
});
|
|
2807
2822
|
// ── ctx-purge: explicit knowledge base wipe ─────────────────────────────────
|
|
2823
|
+
//
|
|
2824
|
+
// Issue #520 — scoped purge.
|
|
2825
|
+
// The schema is ADDITIVE: bare {confirm:true} preserves the legacy
|
|
2826
|
+
// project-wide wipe verbatim (with a stderr deprecation warning so
|
|
2827
|
+
// future callers migrate to explicit scope). When sessionId is given,
|
|
2828
|
+
// only that session's rows + FTS5 chunks are removed; project-wide
|
|
2829
|
+
// files (events.md, FTS5 store file, stats file) are preserved.
|
|
2830
|
+
// Passing both sessionId AND scope:"project" is ambiguous (does the
|
|
2831
|
+
// caller want a per-session wipe or a project-wide one?) and is
|
|
2832
|
+
// rejected by the schema's refine().
|
|
2808
2833
|
server.registerTool("ctx_purge", {
|
|
2809
2834
|
title: "Purge Knowledge Base",
|
|
2810
|
-
description: "
|
|
2811
|
-
"
|
|
2812
|
-
"
|
|
2813
|
-
"
|
|
2835
|
+
description: "DESTRUCTIVE — permanently delete indexed content. CANNOT be undone.\n\n" +
|
|
2836
|
+
"You MUST specify exactly ONE scope:\n\n" +
|
|
2837
|
+
" • { confirm: true, sessionId: \"<uuid>\" }\n" +
|
|
2838
|
+
" Deletes ONLY that session's events + per-session FTS5 chunks.\n" +
|
|
2839
|
+
" Preserves stats file and ALL other sessions.\n\n" +
|
|
2840
|
+
" • { confirm: true, scope: \"project\" }\n" +
|
|
2841
|
+
" Wipes the ENTIRE project: FTS5 knowledge base, every session DB row,\n" +
|
|
2842
|
+
" events markdown, AND resets the stats file.\n\n" +
|
|
2843
|
+
"REFUSAL RULES (tool returns an error):\n" +
|
|
2844
|
+
" • confirm: false → 'purge cancelled'\n" +
|
|
2845
|
+
" • Both sessionId AND scope:'project' provided → 'ambiguous — pick one'\n" +
|
|
2846
|
+
" • scope:'session' without sessionId → throws (sessionId required)\n" +
|
|
2847
|
+
" • Neither sessionId NOR scope provided → DEPRECATED: maps to\n" +
|
|
2848
|
+
" scope:'project' with a deprecation warning to stderr. Will be a hard\n" +
|
|
2849
|
+
" error in a future major.\n\n" +
|
|
2850
|
+
"Use sessionId when the user asks to clear a specific conversation's data.\n" +
|
|
2851
|
+
"Use scope:'project' ONLY when the user explicitly asks to reset everything.\n" +
|
|
2852
|
+
"NEVER call with bare {confirm:true} — always specify the scope.",
|
|
2814
2853
|
inputSchema: z.object({
|
|
2815
|
-
confirm: z.boolean().describe("
|
|
2854
|
+
confirm: z.boolean().describe("MUST be true. Destructive operation; false returns 'purge cancelled'."),
|
|
2855
|
+
sessionId: z.string().optional().describe("UUID of a single session. Pairs with confirm:true to wipe only that " +
|
|
2856
|
+
"session's events + per-session FTS5 chunks. Sibling sessions and the " +
|
|
2857
|
+
"stats file are preserved. MUST NOT be combined with scope:'project'."),
|
|
2858
|
+
scope: z.enum(["session", "project"]).optional().describe("Explicit scope selector. 'session' REQUIRES sessionId. 'project' wipes " +
|
|
2859
|
+
"the entire project (FTS5 + every session + stats). Omit only for the " +
|
|
2860
|
+
"deprecated bare-{confirm:true} back-compat path."),
|
|
2861
|
+
}).refine((v) => !(v.sessionId && v.scope === "project"), {
|
|
2862
|
+
message: "Ambiguous purge: sessionId implies scope:'session', cannot combine with scope:'project'. " +
|
|
2863
|
+
"Use scope:'project' WITHOUT sessionId for the legacy whole-project wipe.",
|
|
2864
|
+
path: ["scope"],
|
|
2816
2865
|
}),
|
|
2817
|
-
}, async ({ confirm }) => {
|
|
2866
|
+
}, async ({ confirm, sessionId, scope }) => {
|
|
2818
2867
|
if (!confirm) {
|
|
2819
2868
|
return trackResponse("ctx_purge", {
|
|
2820
2869
|
content: [{
|
|
@@ -2823,6 +2872,17 @@ server.registerTool("ctx_purge", {
|
|
|
2823
2872
|
}],
|
|
2824
2873
|
});
|
|
2825
2874
|
}
|
|
2875
|
+
// Effective scope resolution:
|
|
2876
|
+
// - explicit scope wins
|
|
2877
|
+
// - else "session" iff sessionId is given
|
|
2878
|
+
// - else "project" (back-compat — emit deprecation warning so
|
|
2879
|
+
// callers migrate to the explicit form before a future major).
|
|
2880
|
+
const effectiveScope = scope ?? (sessionId ? "session" : "project");
|
|
2881
|
+
if (!scope && !sessionId) {
|
|
2882
|
+
console.warn("[context-mode] ctx_purge: bare {confirm:true} is deprecated. " +
|
|
2883
|
+
"Pass scope:'project' for the whole-project wipe, or scope:'session' + sessionId " +
|
|
2884
|
+
"for a scoped wipe. See issue #520.");
|
|
2885
|
+
}
|
|
2826
2886
|
// Close the persistent FTS5 content store handle BEFORE delegating to
|
|
2827
2887
|
// purgeSession so the store's lock is released on Windows. The handle
|
|
2828
2888
|
// is recreated lazily on the next getStore() call.
|
|
@@ -2855,27 +2915,39 @@ server.registerTool("ctx_purge", {
|
|
|
2855
2915
|
// legacy hash here is correct: that pre-pre-legacy directory was
|
|
2856
2916
|
// never migrated and still uses raw casing.
|
|
2857
2917
|
contentHash: hashProjectDirLegacy(getProjectDir()),
|
|
2918
|
+
scope: effectiveScope,
|
|
2919
|
+
sessionId,
|
|
2858
2920
|
});
|
|
2859
|
-
//
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2921
|
+
// Stats are PROJECT-scoped (one stats file per project, summing all
|
|
2922
|
+
// sessions). A scoped per-session purge MUST leave stats alone — they
|
|
2923
|
+
// still belong to other sessions in the same project. Stats reset
|
|
2924
|
+
// happens ONLY when scope === "project".
|
|
2925
|
+
if (effectiveScope === "project") {
|
|
2926
|
+
// Reset in-memory session stats
|
|
2927
|
+
sessionStats.calls = {};
|
|
2928
|
+
sessionStats.bytesReturned = {};
|
|
2929
|
+
sessionStats.bytesIndexed = 0;
|
|
2930
|
+
sessionStats.bytesSandboxed = 0;
|
|
2931
|
+
sessionStats.cacheHits = 0;
|
|
2932
|
+
sessionStats.cacheBytesSaved = 0;
|
|
2933
|
+
sessionStats.sessionStart = Date.now();
|
|
2934
|
+
deleted.push("session stats");
|
|
2935
|
+
// Also drop the persisted stats file so external readers see a fresh state
|
|
2936
|
+
try {
|
|
2937
|
+
const statsFile = getStatsFilePath();
|
|
2938
|
+
if (existsSync(statsFile))
|
|
2939
|
+
unlinkSync(statsFile);
|
|
2940
|
+
}
|
|
2941
|
+
catch { /* best effort */ }
|
|
2873
2942
|
}
|
|
2874
|
-
|
|
2943
|
+
const message = effectiveScope === "session"
|
|
2944
|
+
? `Purged session ${sessionId}: ${deleted.length ? deleted.join(", ") : "no matching rows"}. ` +
|
|
2945
|
+
`Other sessions and project-wide stats preserved.`
|
|
2946
|
+
: `Purged: ${deleted.join(", ")}. All session data for this project has been permanently deleted.`;
|
|
2875
2947
|
return trackResponse("ctx_purge", {
|
|
2876
2948
|
content: [{
|
|
2877
2949
|
type: "text",
|
|
2878
|
-
text:
|
|
2950
|
+
text: message,
|
|
2879
2951
|
}],
|
|
2880
2952
|
});
|
|
2881
2953
|
});
|
|
@@ -3365,11 +3437,13 @@ async function main() {
|
|
|
3365
3437
|
// even though the server is alive. Heartbeat refreshes updated_at every 60s;
|
|
3366
3438
|
// statusline staleness threshold is 30min (cliff is 30 missed ticks away).
|
|
3367
3439
|
setInterval(() => persistStats(), 60_000).unref();
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3440
|
+
if (process.stdin.isTTY) {
|
|
3441
|
+
console.error(`Context Mode MCP server v${VERSION} running on stdio`);
|
|
3442
|
+
console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
|
|
3443
|
+
if (!hasBunRuntime()) {
|
|
3444
|
+
console.error("\nPerformance tip: Install Bun for 3-5x faster JS/TS execution");
|
|
3445
|
+
console.error(" curl -fsSL https://bun.sh/install | bash");
|
|
3446
|
+
}
|
|
3373
3447
|
}
|
|
3374
3448
|
}
|
|
3375
3449
|
main().catch((err) => {
|
package/build/session/purge.d.ts
CHANGED
|
@@ -84,6 +84,33 @@ export interface PurgeOpts {
|
|
|
84
84
|
* session DB hash.
|
|
85
85
|
*/
|
|
86
86
|
contentHash?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Issue #520 — scoped purge.
|
|
89
|
+
*
|
|
90
|
+
* - `"project"` (default when omitted for back-compat callers that
|
|
91
|
+
* only pass `confirm:true` at the MCP layer): wipe ALL session
|
|
92
|
+
* artifacts for `projectDir`. This is the legacy destructive
|
|
93
|
+
* behavior preserved verbatim.
|
|
94
|
+
* - `"session"`: wipe ONLY the rows for `sessionId` inside the
|
|
95
|
+
* project's SessionDB plus FTS5 chunks tagged with that
|
|
96
|
+
* `session_id`. Project-wide files (events.md, content store
|
|
97
|
+
* file, stats file) are left intact. Requires `sessionId`.
|
|
98
|
+
*
|
|
99
|
+
* When `scope` is omitted but `sessionId` is set, behavior implies
|
|
100
|
+
* `scope:"session"` (a sessionId-only call cannot mean "wipe the
|
|
101
|
+
* whole project"). When neither is set, behavior implies
|
|
102
|
+
* `scope:"project"` for back-compat with the original handler.
|
|
103
|
+
*/
|
|
104
|
+
scope?: "session" | "project";
|
|
105
|
+
/**
|
|
106
|
+
* Session identifier whose rows should be wiped from the project's
|
|
107
|
+
* SessionDB and tagged FTS5 chunks. Only consulted when `scope ===
|
|
108
|
+
* "session"`. The `session_events`, `session_meta`, and
|
|
109
|
+
* `session_resume` rows for this id are removed; rows for other
|
|
110
|
+
* sessions in the same DB are preserved. Match SessionDB.deleteSession
|
|
111
|
+
* semantics (see src/session/db.ts).
|
|
112
|
+
*/
|
|
113
|
+
sessionId?: string;
|
|
87
114
|
}
|
|
88
115
|
export interface PurgeResult {
|
|
89
116
|
/**
|
package/build/session/purge.js
CHANGED
|
@@ -31,9 +31,10 @@
|
|
|
31
31
|
* session-related kinds. On Linux the two hashes coincide, so the dual
|
|
32
32
|
* sweep collapses into a single unique-path pass.
|
|
33
33
|
*/
|
|
34
|
-
import { unlinkSync } from "node:fs";
|
|
34
|
+
import { existsSync, unlinkSync } from "node:fs";
|
|
35
35
|
import { join } from "node:path";
|
|
36
|
-
import {
|
|
36
|
+
import { loadDatabase } from "../db-base.js";
|
|
37
|
+
import { getWorktreeSuffix, hashProjectDirCanonical, hashProjectDirLegacy, SessionDB, } from "./db.js";
|
|
37
38
|
/** Canonical SQLite sidecar suffixes. The empty string is the main DB. */
|
|
38
39
|
const SQLITE_SIDECARS = ["", "-wal", "-shm"];
|
|
39
40
|
/** Try to unlink one path; report success without throwing on ENOENT etc. */
|
|
@@ -68,9 +69,110 @@ function tryUnlinkSqliteTriple(path, wipedPaths) {
|
|
|
68
69
|
* without `contentHash`), which is a programmer bug not a runtime concern.
|
|
69
70
|
*/
|
|
70
71
|
export function purgeSession(opts) {
|
|
71
|
-
const { projectDir, sessionsDir, storePath, contentDir, legacyContentDir, contentHash } = opts;
|
|
72
|
+
const { projectDir, sessionsDir, storePath, contentDir, legacyContentDir, contentHash, sessionId, scope } = opts;
|
|
72
73
|
const deleted = [];
|
|
73
74
|
const wipedPaths = [];
|
|
75
|
+
// Issue #520 — scope discipline.
|
|
76
|
+
// Resolve effective scope: explicit `scope` wins; otherwise infer
|
|
77
|
+
// "session" iff sessionId is given, else "project".
|
|
78
|
+
const effectiveScope = scope ?? (sessionId ? "session" : "project");
|
|
79
|
+
if (effectiveScope === "session" && !sessionId) {
|
|
80
|
+
throw new TypeError("purgeSession: scope:'session' requires sessionId. " +
|
|
81
|
+
"Pass scope:'project' for the legacy whole-project wipe.");
|
|
82
|
+
}
|
|
83
|
+
// ── Session-scoped path (issue #520). ─────────────────────────────────
|
|
84
|
+
// Wipe ONLY this sessionId's rows from the project's SessionDB. The DB
|
|
85
|
+
// file itself, the events.md sidecar, the FTS5 store, and the stats
|
|
86
|
+
// file are all left intact — those are project-scoped concerns. The
|
|
87
|
+
// label "session rows for <id>" appears once when at least one row was
|
|
88
|
+
// removed, mirroring the project-scoped UI contract.
|
|
89
|
+
if (effectiveScope === "session" && sessionId) {
|
|
90
|
+
const worktreeSuffix = getWorktreeSuffix(projectDir);
|
|
91
|
+
const canonicalHash = hashProjectDirCanonical(projectDir);
|
|
92
|
+
const legacyHash = hashProjectDirLegacy(projectDir);
|
|
93
|
+
const hashes = canonicalHash === legacyHash
|
|
94
|
+
? [canonicalHash]
|
|
95
|
+
: [canonicalHash, legacyHash];
|
|
96
|
+
let rowsRemoved = false;
|
|
97
|
+
for (const h of hashes) {
|
|
98
|
+
const dbPath = join(sessionsDir, `${h}${worktreeSuffix}.db`);
|
|
99
|
+
if (!existsSync(dbPath))
|
|
100
|
+
continue;
|
|
101
|
+
let db = null;
|
|
102
|
+
try {
|
|
103
|
+
db = new SessionDB({ dbPath });
|
|
104
|
+
const before = db.getEvents(sessionId).length;
|
|
105
|
+
db.deleteSession(sessionId);
|
|
106
|
+
if (before > 0)
|
|
107
|
+
rowsRemoved = true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Best-effort — corrupt DB is logged elsewhere; do not block purge.
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
// close() releases the handle WITHOUT deleting the file —
|
|
114
|
+
// this is what makes the scoped wipe non-destructive at the
|
|
115
|
+
// file-system level. Using cleanup() here would erase the
|
|
116
|
+
// entire DB (main + WAL + SHM), defeating per-session scope.
|
|
117
|
+
try {
|
|
118
|
+
db?.close();
|
|
119
|
+
}
|
|
120
|
+
catch { /* best effort */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (rowsRemoved)
|
|
124
|
+
deleted.push(`session rows for ${sessionId}`);
|
|
125
|
+
// Per-session FTS5 chunk wipe. The chunks table has a `session_id
|
|
126
|
+
// UNINDEXED` column (src/store.ts schema). The public index() path
|
|
127
|
+
// currently inserts NULL — but a future per-session-tagged path
|
|
128
|
+
// (e.g. tool-call indexing keyed to a session) will populate it,
|
|
129
|
+
// and the SQL contract here keeps that future correct from day one.
|
|
130
|
+
// Today this is a safe no-op against existing data.
|
|
131
|
+
//
|
|
132
|
+
// Caller is responsible for closing any persistent ContentStore
|
|
133
|
+
// handle BEFORE invoking purgeSession (Windows file lock). The
|
|
134
|
+
// ctx_purge handler does this via _store?.cleanup() before delegating.
|
|
135
|
+
const ftsTargets = [];
|
|
136
|
+
if (storePath && existsSync(storePath))
|
|
137
|
+
ftsTargets.push(storePath);
|
|
138
|
+
if (contentDir) {
|
|
139
|
+
const canonicalH = hashProjectDirCanonical(projectDir);
|
|
140
|
+
const legacyH = hashProjectDirLegacy(projectDir);
|
|
141
|
+
const hh = canonicalH === legacyH ? [canonicalH] : [canonicalH, legacyH];
|
|
142
|
+
for (const h of hh) {
|
|
143
|
+
const p = join(contentDir, `${h}.db`);
|
|
144
|
+
if (existsSync(p) && !ftsTargets.includes(p))
|
|
145
|
+
ftsTargets.push(p);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let chunksRemoved = false;
|
|
149
|
+
for (const path of ftsTargets) {
|
|
150
|
+
try {
|
|
151
|
+
const Database = loadDatabase();
|
|
152
|
+
const fts = new Database(path, { timeout: 30000 });
|
|
153
|
+
try {
|
|
154
|
+
const before = fts.prepare("SELECT COUNT(*) AS c FROM chunks WHERE session_id = ?").get(sessionId).c;
|
|
155
|
+
fts.prepare("DELETE FROM chunks WHERE session_id = ?").run(sessionId);
|
|
156
|
+
fts.prepare("DELETE FROM chunks_trigram WHERE session_id = ?").run(sessionId);
|
|
157
|
+
if (before > 0)
|
|
158
|
+
chunksRemoved = true;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
try {
|
|
162
|
+
fts.close();
|
|
163
|
+
}
|
|
164
|
+
catch { /* best effort */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Best-effort — schema mismatch / corrupt DB / missing FTS5 must not
|
|
169
|
+
// block the per-session SessionDB wipe that already succeeded.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (chunksRemoved)
|
|
173
|
+
deleted.push(`FTS5 chunks for ${sessionId}`);
|
|
174
|
+
return { deleted, wipedPaths };
|
|
175
|
+
}
|
|
74
176
|
// ── 1. Knowledge base FTS5 store (per-platform). ──────────────────────
|
|
75
177
|
// Two input modes:
|
|
76
178
|
// - `storePath`: single absolute path; pre-resolved by caller. Wipes
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
/**
|
|
2
4
|
* Project-dir resolution helpers — shared between `start.mjs` (the MCP entry
|
|
3
5
|
* point) and `src/server.ts getProjectDir()` (the consumer).
|
|
@@ -52,11 +54,6 @@ export function isPluginInstallPath(p) {
|
|
|
52
54
|
* transcripts have older mtimes and are correctly ignored.
|
|
53
55
|
*/
|
|
54
56
|
export function resolveProjectDirFromTranscript(opts) {
|
|
55
|
-
// Inline imports kept private to this function — keeps the module test-
|
|
56
|
-
// friendly when fs is stubbed at the call sites that don't use this path.
|
|
57
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
-
const fs = require("node:fs");
|
|
59
|
-
const path = require("node:path");
|
|
60
57
|
if (!fs.existsSync(opts.projectsRoot))
|
|
61
58
|
return undefined;
|
|
62
59
|
let bestPath;
|
|
@@ -156,6 +153,13 @@ export function resolveProjectDir(opts) {
|
|
|
156
153
|
env.OPENCODE_PROJECT_DIR,
|
|
157
154
|
env.PI_PROJECT_DIR,
|
|
158
155
|
env.IDEA_INITIAL_DIRECTORY,
|
|
156
|
+
// Issue #521: Cursor MCP env override. The cursor adapter already
|
|
157
|
+
// trusts CURSOR_CWD for hook input resolution (adapters/cursor/index.ts:581);
|
|
158
|
+
// mirror that trust here so ctx_stats / SessionDB / hash see the workspace
|
|
159
|
+
// path on Cursor. Whether Cursor itself sets this on MCP child spawn is
|
|
160
|
+
// unconfirmed — but documenting it as a supported override gives users a
|
|
161
|
+
// documented escape hatch (`~/.cursor/mcp.json` env: { CURSOR_CWD: "..." }).
|
|
162
|
+
env.CURSOR_CWD,
|
|
159
163
|
env.CONTEXT_MODE_PROJECT_DIR,
|
|
160
164
|
];
|
|
161
165
|
for (const c of candidates) {
|