context-mode 1.0.146 → 1.0.147

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/build/cli.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * context-mode doctor → Diagnose runtime issues, hooks, FTS5, version
8
8
  * context-mode upgrade → Fix hooks, permissions, and settings
9
9
  * context-mode hook <platform> <event> → Dispatch a hook script (used by platform hook configs)
10
+ * CONTEXT_MODE_DIR=/abs/path context-mode → Override sessions/content storage root
11
+ * Empty/whitespace is ignored; non-empty values must be absolute.
10
12
  *
11
13
  * Platform auto-detection: CLI detects which platform is running
12
14
  * (Claude Code, Gemini CLI, OpenCode, etc.) and uses the appropriate adapter.
@@ -22,6 +24,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
22
24
  import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
23
25
  import { getHookScriptPaths } from "./util/hook-config.js";
24
26
  import { resolveClaudeConfigDir } from "./util/claude-config.js";
27
+ import { ensureWritableStorageDir, formatStorageDirectoryError, resolveContentStorageDir, resolveSessionStorageDir, resolveStatsStorageDir, StorageDirectoryError, } from "./session/db.js";
25
28
  // v1.0.128 — Issue #559 sibling MCP kill helpers (see PR-559-560-FIX-DESIGN.md).
26
29
  import { discoverSiblingMcpPids, killSiblingMcpServers } from "./util/sibling-mcp.js";
27
30
  // v1.0.119 — Issue #523 Layer 5 heal: post-bump assertion on .claude-plugin/plugin.json
@@ -125,8 +128,26 @@ async function hookDispatch(platform, event) {
125
128
  /* -------------------------------------------------------
126
129
  * Entry point
127
130
  * ------------------------------------------------------- */
131
+ const IN_PROCESS_PLUGIN_PLATFORMS = new Set(["opencode", "kilo"]);
132
+ const isInProcessPluginPlatform = (p) => p ? IN_PROCESS_PLUGIN_PLATFORMS.has(p) : false;
128
133
  const args = process.argv.slice(2);
129
- if (args[0] === "doctor") {
134
+ function printHelp() {
135
+ console.log([
136
+ "Usage:",
137
+ " context-mode Start MCP server (stdio)",
138
+ " context-mode doctor Diagnose runtime issues, hooks, FTS5, version",
139
+ " context-mode upgrade Fix hooks, permissions, and settings",
140
+ " context-mode hook <platform> <event> Dispatch a configured hook script",
141
+ " context-mode statusline Print Claude Code status line",
142
+ "",
143
+ "Environment:",
144
+ " CONTEXT_MODE_DIR=/absolute/path Override sessions/content storage root; empty is ignored, non-empty must be absolute",
145
+ ].join("\n"));
146
+ }
147
+ if (args[0] === "--help" || args[0] === "-h" || args[0] === "help") {
148
+ printHelp();
149
+ }
150
+ else if (args[0] === "doctor") {
130
151
  doctor().then((code) => process.exit(code));
131
152
  }
132
153
  else if (args[0] === "upgrade") {
@@ -236,7 +257,7 @@ function cachePluginRoot(platform) {
236
257
  }
237
258
  function getPluginRoot() {
238
259
  const platform = detectPlatform().platform;
239
- if (platform === 'opencode' || platform === 'kilo') {
260
+ if (isInProcessPluginPlatform(platform)) {
240
261
  return cachePluginRoot(platform);
241
262
  }
242
263
  return defaultPluginRoot();
@@ -276,6 +297,25 @@ async function fetchLatestVersion() {
276
297
  /* -------------------------------------------------------
277
298
  * Doctor — adapter-aware diagnostics
278
299
  * ------------------------------------------------------- */
300
+ function describeStorageSource(dir) {
301
+ return dir.envVar ? dir.envVar : "adapter default";
302
+ }
303
+ function logStorageDir(dir) {
304
+ try {
305
+ ensureWritableStorageDir(dir);
306
+ p.log.success(color.green(`Storage ${dir.kind}: PASS`) +
307
+ color.dim(` — ${dir.path} (${describeStorageSource(dir)})`));
308
+ return 0;
309
+ }
310
+ catch (err) {
311
+ if (err instanceof StorageDirectoryError) {
312
+ p.log.error(color.red(`Storage ${dir.kind}: FAIL`) +
313
+ color.dim(` — ${formatStorageDirectoryError(err)}`));
314
+ return 1;
315
+ }
316
+ throw err;
317
+ }
318
+ }
279
319
  async function doctor() {
280
320
  if (process.stdout.isTTY)
281
321
  console.clear();
@@ -286,6 +326,29 @@ async function doctor() {
286
326
  p.log.info(`Platform: ${color.cyan(adapter.name)}` +
287
327
  color.dim(` (${detection.confidence} confidence — ${detection.reason})`));
288
328
  let criticalFails = 0;
329
+ try {
330
+ const sessionDir = resolveSessionStorageDir(() => adapter.getSessionDir());
331
+ const contentDir = resolveContentStorageDir(() => sessionDir.path);
332
+ const statsDir = resolveStatsStorageDir(() => sessionDir.path);
333
+ p.note([
334
+ `sessions: ${sessionDir.path} (${describeStorageSource(sessionDir)})`,
335
+ `content: ${contentDir.path} (${describeStorageSource(contentDir)})`,
336
+ `stats: ${statsDir.path} (${describeStorageSource(statsDir)})`,
337
+ ].join("\n"), "Storage paths");
338
+ criticalFails += logStorageDir(sessionDir);
339
+ criticalFails += logStorageDir(contentDir);
340
+ criticalFails += logStorageDir(statsDir);
341
+ }
342
+ catch (err) {
343
+ if (err instanceof StorageDirectoryError) {
344
+ criticalFails++;
345
+ p.log.error(color.red(`Storage ${err.kind}: FAIL`) +
346
+ color.dim(` — ${formatStorageDirectoryError(err)}`));
347
+ }
348
+ else {
349
+ throw err;
350
+ }
351
+ }
289
352
  const s = p.spinner();
290
353
  s.start("Running diagnostics");
291
354
  let runtimes;
@@ -738,8 +801,8 @@ async function insight(port) {
738
801
  // Detect platform + adapter for correct session/content paths
739
802
  const detection = detectPlatform();
740
803
  const adapter = await getAdapter(detection.platform);
741
- const sessDir = adapter.getSessionDir();
742
- const contentDir = join(dirname(sessDir), "content");
804
+ const sessDir = ensureWritableStorageDir(resolveSessionStorageDir(() => adapter.getSessionDir()));
805
+ const contentDir = ensureWritableStorageDir(resolveContentStorageDir(() => sessDir));
743
806
  const cacheDir = join(dirname(sessDir), "insight-cache");
744
807
  if (!existsSync(join(insightSource, "server.mjs"))) {
745
808
  console.error("Error: Insight source not found. Try upgrading context-mode.");
@@ -1141,7 +1204,7 @@ async function upgrade(opts) {
1141
1204
  timeout: 60000,
1142
1205
  });
1143
1206
  s.stop("Dependencies ready");
1144
- if (detection.platform !== 'opencode' && detection.platform !== 'kilo') {
1207
+ if (!isInProcessPluginPlatform(detection.platform)) {
1145
1208
  // Verify native addons through the same bootstrap start.mjs imports.
1146
1209
  // On modern Node, the ABI-specific cache file is the compatibility marker;
1147
1210
  // the active binding alone may be stale from a previous Node ABI.
@@ -1203,20 +1266,20 @@ async function upgrade(opts) {
1203
1266
  color.dim(`\n Try (primary): cd "${pluginRoot}" && npm install better-sqlite3 --no-optional`) +
1204
1267
  color.dim("\n Try (fallback): /context-mode:ctx-doctor"));
1205
1268
  }
1206
- }
1207
- // Update global npm
1208
- s.start("Updating npm global package");
1209
- try {
1210
- npmExecFile(["install", "-g", pluginRoot, "--no-audit", "--no-fund"], {
1211
- stdio: "pipe",
1212
- timeout: 30000,
1213
- });
1214
- s.stop(color.green("npm global updated"));
1215
- changes.push("Updated npm global package");
1216
- }
1217
- catch {
1218
- s.stop(color.yellow("npm global update skipped"));
1219
- p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
1269
+ // Update global npm
1270
+ s.start("Updating npm global package");
1271
+ try {
1272
+ npmExecFile(["install", "-g", pluginRoot, "--no-audit", "--no-fund"], {
1273
+ stdio: "pipe",
1274
+ timeout: 30000,
1275
+ });
1276
+ s.stop(color.green("npm global updated"));
1277
+ changes.push("Updated npm global package");
1278
+ }
1279
+ catch {
1280
+ s.stop(color.yellow("npm global update skipped"));
1281
+ p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
1282
+ }
1220
1283
  }
1221
1284
  // Cleanup
1222
1285
  rmSync(tmpDir, { recursive: true, force: true });
@@ -19,7 +19,12 @@ export interface AutoMemoryResult {
19
19
  export interface AutoMemoryAdapter {
20
20
  getConfigDir(): string;
21
21
  getInstructionFiles(): string[];
22
- getMemoryDir(): string;
22
+ /**
23
+ * `projectDir` is optional for backwards compatibility with legacy
24
+ * callers — when supplied, adapters MUST return a project-scoped path
25
+ * (see HookAdapter.getMemoryDir contract, issue #663).
26
+ */
27
+ getMemoryDir(projectDir?: string): string;
23
28
  }
24
29
  /**
25
30
  * Search auto-memory files for content matching any of the given queries.
@@ -8,6 +8,7 @@
8
8
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
9
9
  import { join, isAbsolute } from "node:path";
10
10
  import { resolveClaudeConfigDir } from "../util/claude-config.js";
11
+ import { hashProjectDirCanonical } from "../session/db.js";
11
12
  const DEBUG = process.env.DEBUG?.includes("context-mode");
12
13
  /**
13
14
  * Search auto-memory files for content matching any of the given queries.
@@ -38,10 +39,18 @@ export function searchAutoMemory(queries, limit = 5, projectDir, configDir, adap
38
39
  // CC config trees (and empty/whitespace env doesn't poison the path).
39
40
  const adapterRelative = adapterConfigDir ? resolveAgainst(projectDir, adapterConfigDir) : null;
40
41
  const effectiveConfigDir = adapterRelative ?? configDir ?? resolveClaudeConfigDir();
41
- const adapterMemoryDir = adapter?.getMemoryDir();
42
+ // Issue #663: scope memory dir by projectDir so parallel projects can't
43
+ // read each other's auto-memory. Adapter-aware path delegates the
44
+ // scoping to the adapter; legacy adapterless fallback applies the same
45
+ // hash directly so the contract holds at both call sites.
46
+ const adapterMemoryDir = adapter?.getMemoryDir(projectDir);
47
+ const fallbackMemoryBase = join(effectiveConfigDir, "memory");
48
+ const fallbackMemoryDir = projectDir
49
+ ? join(fallbackMemoryBase, hashProjectDirCanonical(projectDir))
50
+ : fallbackMemoryBase;
42
51
  const memoryDir = adapterMemoryDir
43
52
  ? resolveAgainst(projectDir, adapterMemoryDir)
44
- : join(effectiveConfigDir, "memory");
53
+ : fallbackMemoryDir;
45
54
  // Collect candidate files
46
55
  const candidates = [];
47
56
  // 1. Project-level instruction files