context-mode 1.0.111 → 1.0.112

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.
Files changed (150) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/index.ts +3 -2
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +152 -34
  7. package/bin/statusline.mjs +144 -127
  8. package/build/adapters/base.d.ts +8 -5
  9. package/build/adapters/base.js +8 -18
  10. package/build/adapters/claude-code/index.d.ts +24 -3
  11. package/build/adapters/claude-code/index.js +44 -11
  12. package/build/adapters/codex/hooks.d.ts +10 -5
  13. package/build/adapters/codex/hooks.js +10 -5
  14. package/build/adapters/codex/index.d.ts +17 -5
  15. package/build/adapters/codex/index.js +337 -37
  16. package/build/adapters/codex/paths.d.ts +1 -0
  17. package/build/adapters/codex/paths.js +12 -0
  18. package/build/adapters/cursor/index.d.ts +6 -0
  19. package/build/adapters/cursor/index.js +83 -2
  20. package/build/adapters/detect.d.ts +1 -1
  21. package/build/adapters/detect.js +29 -6
  22. package/build/adapters/omp/index.d.ts +65 -0
  23. package/build/adapters/omp/index.js +182 -0
  24. package/build/adapters/omp/plugin.d.ts +75 -0
  25. package/build/adapters/omp/plugin.js +220 -0
  26. package/build/adapters/openclaw/mcp-tools.d.ts +54 -0
  27. package/build/adapters/openclaw/mcp-tools.js +198 -0
  28. package/build/adapters/openclaw/plugin.d.ts +130 -0
  29. package/build/adapters/openclaw/plugin.js +629 -0
  30. package/build/adapters/openclaw/workspace-router.d.ts +29 -0
  31. package/build/adapters/openclaw/workspace-router.js +64 -0
  32. package/build/adapters/opencode/plugin.d.ts +145 -0
  33. package/build/adapters/opencode/plugin.js +457 -0
  34. package/build/adapters/pi/extension.d.ts +26 -0
  35. package/build/adapters/pi/extension.js +552 -0
  36. package/build/adapters/pi/index.d.ts +57 -0
  37. package/build/adapters/pi/index.js +173 -0
  38. package/build/adapters/pi/mcp-bridge.d.ts +113 -0
  39. package/build/adapters/pi/mcp-bridge.js +251 -0
  40. package/build/adapters/types.d.ts +11 -6
  41. package/build/cli.js +186 -170
  42. package/build/db-base.d.ts +15 -2
  43. package/build/db-base.js +50 -5
  44. package/build/executor.d.ts +2 -0
  45. package/build/executor.js +15 -2
  46. package/build/runPool.d.ts +36 -0
  47. package/build/runPool.js +51 -0
  48. package/build/runtime.js +64 -5
  49. package/build/search/auto-memory.js +6 -4
  50. package/build/security.js +30 -10
  51. package/build/server.d.ts +23 -1
  52. package/build/server.js +652 -174
  53. package/build/session/analytics.d.ts +404 -1
  54. package/build/session/analytics.js +1347 -42
  55. package/build/session/db.d.ts +114 -5
  56. package/build/session/db.js +275 -27
  57. package/build/session/event-emit.d.ts +48 -0
  58. package/build/session/event-emit.js +101 -0
  59. package/build/session/extract.d.ts +1 -0
  60. package/build/session/extract.js +79 -12
  61. package/build/session/purge.d.ts +111 -0
  62. package/build/session/purge.js +138 -0
  63. package/build/store.d.ts +7 -0
  64. package/build/store.js +69 -6
  65. package/build/util/claude-config.d.ts +26 -0
  66. package/build/util/claude-config.js +91 -0
  67. package/build/util/hook-config.d.ts +4 -0
  68. package/build/util/hook-config.js +39 -0
  69. package/cli.bundle.mjs +411 -208
  70. package/configs/antigravity/GEMINI.md +0 -3
  71. package/configs/claude-code/CLAUDE.md +1 -4
  72. package/configs/codex/AGENTS.md +1 -4
  73. package/configs/codex/config.toml +3 -0
  74. package/configs/codex/hooks.json +8 -0
  75. package/configs/cursor/context-mode.mdc +0 -3
  76. package/configs/gemini-cli/GEMINI.md +0 -3
  77. package/configs/jetbrains-copilot/copilot-instructions.md +0 -3
  78. package/configs/kilo/AGENTS.md +0 -3
  79. package/configs/kiro/KIRO.md +0 -3
  80. package/configs/omp/SYSTEM.md +85 -0
  81. package/configs/omp/mcp.json +7 -0
  82. package/configs/openclaw/AGENTS.md +0 -3
  83. package/configs/opencode/AGENTS.md +0 -3
  84. package/configs/pi/AGENTS.md +0 -3
  85. package/configs/qwen-code/QWEN.md +1 -4
  86. package/configs/vscode-copilot/copilot-instructions.md +0 -3
  87. package/configs/zed/AGENTS.md +0 -3
  88. package/hooks/codex/posttooluse.mjs +9 -2
  89. package/hooks/codex/precompact.mjs +69 -0
  90. package/hooks/codex/sessionstart.mjs +13 -9
  91. package/hooks/codex/stop.mjs +1 -2
  92. package/hooks/codex/userpromptsubmit.mjs +1 -2
  93. package/hooks/core/routing.mjs +237 -18
  94. package/hooks/cursor/afteragentresponse.mjs +1 -1
  95. package/hooks/cursor/hooks.json +31 -0
  96. package/hooks/cursor/posttooluse.mjs +1 -1
  97. package/hooks/cursor/sessionstart.mjs +5 -5
  98. package/hooks/cursor/stop.mjs +1 -1
  99. package/hooks/ensure-deps.mjs +12 -13
  100. package/hooks/gemini-cli/aftertool.mjs +1 -1
  101. package/hooks/gemini-cli/beforeagent.mjs +1 -1
  102. package/hooks/gemini-cli/precompress.mjs +3 -2
  103. package/hooks/gemini-cli/sessionstart.mjs +9 -9
  104. package/hooks/jetbrains-copilot/posttooluse.mjs +1 -1
  105. package/hooks/jetbrains-copilot/precompact.mjs +3 -2
  106. package/hooks/jetbrains-copilot/sessionstart.mjs +9 -9
  107. package/hooks/kiro/agentspawn.mjs +5 -5
  108. package/hooks/kiro/posttooluse.mjs +2 -2
  109. package/hooks/kiro/userpromptsubmit.mjs +1 -1
  110. package/hooks/posttooluse.mjs +45 -0
  111. package/hooks/precompact.mjs +17 -0
  112. package/hooks/pretooluse.mjs +23 -0
  113. package/hooks/routing-block.mjs +0 -12
  114. package/hooks/run-hook.mjs +16 -3
  115. package/hooks/session-db.bundle.mjs +27 -18
  116. package/hooks/session-extract.bundle.mjs +2 -2
  117. package/hooks/session-helpers.mjs +101 -64
  118. package/hooks/sessionstart.mjs +51 -2
  119. package/hooks/vscode-copilot/posttooluse.mjs +1 -1
  120. package/hooks/vscode-copilot/precompact.mjs +3 -2
  121. package/hooks/vscode-copilot/sessionstart.mjs +9 -9
  122. package/openclaw.plugin.json +1 -1
  123. package/package.json +14 -8
  124. package/server.bundle.mjs +349 -147
  125. package/skills/UPSTREAM-CREDITS.md +0 -51
  126. package/skills/context-mode-ops/SKILL.md +0 -299
  127. package/skills/context-mode-ops/agent-teams.md +0 -198
  128. package/skills/context-mode-ops/communication.md +0 -224
  129. package/skills/context-mode-ops/marketing.md +0 -124
  130. package/skills/context-mode-ops/release.md +0 -214
  131. package/skills/context-mode-ops/review-pr.md +0 -269
  132. package/skills/context-mode-ops/tdd.md +0 -329
  133. package/skills/context-mode-ops/triage-issue.md +0 -266
  134. package/skills/context-mode-ops/validation.md +0 -307
  135. package/skills/diagnose/SKILL.md +0 -122
  136. package/skills/diagnose/scripts/hitl-loop.template.sh +0 -41
  137. package/skills/grill-me/SKILL.md +0 -15
  138. package/skills/grill-with-docs/ADR-FORMAT.md +0 -47
  139. package/skills/grill-with-docs/CONTEXT-FORMAT.md +0 -77
  140. package/skills/grill-with-docs/SKILL.md +0 -93
  141. package/skills/improve-codebase-architecture/DEEPENING.md +0 -37
  142. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +0 -44
  143. package/skills/improve-codebase-architecture/LANGUAGE.md +0 -53
  144. package/skills/improve-codebase-architecture/SKILL.md +0 -76
  145. package/skills/tdd/SKILL.md +0 -114
  146. package/skills/tdd/deep-modules.md +0 -33
  147. package/skills/tdd/interface-design.md +0 -31
  148. package/skills/tdd/mocking.md +0 -59
  149. package/skills/tdd/refactoring.md +0 -10
  150. package/skills/tdd/tests.md +0 -61
package/build/cli.js CHANGED
@@ -20,6 +20,22 @@ import { resolve, dirname, join } from "node:path";
20
20
  import { tmpdir, devNull, homedir } from "node:os";
21
21
  import { fileURLToPath, pathToFileURL } from "node:url";
22
22
  import { detectRuntimes, getRuntimeSummary, hasBunRuntime, getAvailableLanguages, } from "./runtime.js";
23
+ import { getHookScriptPaths } from "./util/hook-config.js";
24
+ import { resolveClaudeConfigDir } from "./util/claude-config.js";
25
+ // Private 16-LOC copy of browserOpenArgv. Canonical version lives in src/server.ts;
26
+ // duplicated here so the cli bundle does not pull server.ts top-level boot side effects.
27
+ // Keep in sync — pure data, no I/O.
28
+ function browserOpenArgv(url, platform) {
29
+ if (platform === "darwin")
30
+ return [{ cmd: "open", args: [url] }];
31
+ if (platform === "win32") {
32
+ return [{ cmd: "cmd", args: ["/c", "start", "", url] }];
33
+ }
34
+ return [
35
+ { cmd: "xdg-open", args: [url] },
36
+ { cmd: "sensible-browser", args: [url] },
37
+ ];
38
+ }
23
39
  // ── Adapter imports ──────────────────────────────────────
24
40
  import { detectPlatform, getAdapter } from "./adapters/detect.js";
25
41
  /* -------------------------------------------------------
@@ -55,6 +71,7 @@ const HOOK_MAP = {
55
71
  "codex": {
56
72
  pretooluse: "hooks/codex/pretooluse.mjs",
57
73
  posttooluse: "hooks/codex/posttooluse.mjs",
74
+ precompact: "hooks/codex/precompact.mjs",
58
75
  sessionstart: "hooks/codex/sessionstart.mjs",
59
76
  userpromptsubmit: "hooks/codex/userpromptsubmit.mjs",
60
77
  stop: "hooks/codex/stop.mjs",
@@ -104,7 +121,11 @@ if (args[0] === "doctor") {
104
121
  doctor().then((code) => process.exit(code));
105
122
  }
106
123
  else if (args[0] === "upgrade") {
107
- upgrade();
124
+ upgrade().catch((err) => {
125
+ const message = err instanceof Error ? err.message : String(err);
126
+ p.log.error(color.red(message));
127
+ process.exit(1);
128
+ });
108
129
  }
109
130
  else if (args[0] === "hook") {
110
131
  hookDispatch(args[1], args[2]);
@@ -151,33 +172,20 @@ export function npmExec(command, opts = {}) {
151
172
  export function openInBrowser(url, platform = process.platform, runner = nodeExecFile) {
152
173
  const opts = { stdio: "ignore" };
153
174
  const hint = () => console.error(`\nCould not auto-open browser. Open manually: ${url}`);
154
- try {
155
- if (platform === "darwin") {
156
- runner("open", [url], opts);
157
- }
158
- else if (platform === "win32") {
159
- // `start` is a cmd.exe builtin; first arg after `start` is the
160
- // window title — pass empty so the URL isn't consumed as a title.
161
- runner("cmd", ["/c", "start", "", url], opts);
162
- }
163
- else {
164
- // linux/bsd: try xdg-open, fall back to sensible-browser.
165
- try {
166
- runner("xdg-open", [url], opts);
167
- }
168
- catch {
169
- try {
170
- runner("sensible-browser", [url], opts);
171
- }
172
- catch {
173
- hint();
174
- }
175
- }
175
+ // Platform→argv mapping is canonical in src/server.ts; mirrored privately
176
+ // above to avoid pulling server boot side effects into the cli bundle.
177
+ const attempts = browserOpenArgv(url, platform);
178
+ let opened = false;
179
+ for (const { cmd, args } of attempts) {
180
+ try {
181
+ runner(cmd, args, opts);
182
+ opened = true;
183
+ break;
176
184
  }
185
+ catch { /* try next fallback */ }
177
186
  }
178
- catch {
187
+ if (!opened)
179
188
  hint();
180
- }
181
189
  }
182
190
  function defaultPluginRoot() {
183
191
  const __filename = fileURLToPath(import.meta.url);
@@ -331,22 +339,35 @@ async function doctor() {
331
339
  if (result.status === "pass") {
332
340
  p.log.success(color.green(`${result.check}: PASS`) + ` — ${result.message}`);
333
341
  }
342
+ else if (result.status === "warn") {
343
+ p.log.warn(color.yellow(`${result.check}: WARN`) +
344
+ ` — ${result.message}` +
345
+ (result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
346
+ }
334
347
  else {
335
348
  p.log.error(color.red(`${result.check}: FAIL`) +
336
349
  ` — ${result.message}` +
337
350
  (result.fix ? color.dim(`\n Run: ${result.fix}`) : ""));
338
351
  }
339
352
  }
340
- // Hook script exists
341
- p.log.step("Checking hook script...");
342
- const hookScriptPath = resolve(pluginRoot, "hooks", "pretooluse.mjs");
343
- try {
344
- accessSync(hookScriptPath, constants.R_OK);
345
- p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${hookScriptPath}`));
353
+ // Hook scripts exist
354
+ p.log.step("Checking hook scripts...");
355
+ const hookScriptPaths = getHookScriptPaths(adapter, pluginRoot);
356
+ if (hookScriptPaths.length === 0) {
357
+ p.log.success(color.green("Hook scripts: PASS") + color.dim(" — no direct .mjs script paths to verify"));
346
358
  }
347
- catch {
348
- p.log.error(color.red("Hook script exists: FAIL") +
349
- color.dim(` not found at ${hookScriptPath}`));
359
+ else {
360
+ for (const scriptPath of hookScriptPaths) {
361
+ const absolutePath = resolve(pluginRoot, scriptPath);
362
+ try {
363
+ accessSync(absolutePath, constants.R_OK);
364
+ p.log.success(color.green("Hook script exists: PASS") + color.dim(` — ${absolutePath}`));
365
+ }
366
+ catch {
367
+ p.log.error(color.red("Hook script exists: FAIL") +
368
+ color.dim(` — not found at ${absolutePath}`));
369
+ }
370
+ }
350
371
  }
351
372
  // Plugin registration — adapter-aware
352
373
  p.log.step(`Checking ${adapter.name} plugin registration...`);
@@ -568,7 +589,9 @@ async function upgrade() {
568
589
  // commit and CC keeps reporting the old version even after our cache dir is
569
590
  // updated — users then see "ctx-upgrade succeeded" but nothing actually
570
591
  // changed at the plugin-system level.
571
- const marketplaceDir = resolve(homedir(), ".claude", "plugins", "marketplaces", "context-mode");
592
+ // Issue #460 round-3: route through resolveClaudeConfigDir so users who
593
+ // relocate their CC config root keep the marketplace clone in the same tree.
594
+ const marketplaceDir = resolve(resolveClaudeConfigDir(), "plugins", "marketplaces", "context-mode");
572
595
  if (existsSync(join(marketplaceDir, ".git"))) {
573
596
  s.start("Syncing marketplace clone");
574
597
  try {
@@ -606,156 +629,140 @@ async function upgrade() {
606
629
  if (newVersion === localVersion) {
607
630
  p.log.success(color.green("Already on latest") + ` — v${localVersion}`);
608
631
  rmSync(tmpDir, { recursive: true, force: true });
609
- return;
610
632
  }
611
633
  else {
612
634
  p.log.info(`Update available: ${color.yellow("v" + localVersion)} → ${color.green("v" + newVersion)}`);
613
- }
614
- // Step 2: Install dependencies + build
615
- s.start("Installing dependencies & building");
616
- npmExecFile(["install", "--no-audit", "--no-fund"], {
617
- cwd: srcDir,
618
- stdio: "pipe",
619
- timeout: 120000,
620
- });
621
- npmExecFile(["run", "build"], {
622
- cwd: srcDir,
623
- stdio: "pipe",
624
- timeout: 60000,
625
- });
626
- s.stop("Built successfully");
627
- // Step 3: Update in-place
628
- s.start("Updating files in-place");
629
- // Old version dirs are cleaned lazily by sessionstart.mjs (age-gated >1h)
630
- // to avoid breaking active sessions that still reference them (#181).
631
- // Read files list from cloned repo's package.json so new directories
632
- // (like insight/) are automatically included without chicken-and-egg issues
633
- // where the old CLI doesn't know about new directories.
634
- const clonedPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
635
- const items = [
636
- ...(clonedPkg.files || []),
637
- "src", "package.json",
638
- ];
639
- for (const item of items) {
640
- try {
641
- rmSync(resolve(pluginRoot, item), { recursive: true, force: true });
642
- cpSync(resolve(srcDir, item), resolve(pluginRoot, item), { recursive: true });
635
+ // Step 2: Install dependencies + build
636
+ s.start("Installing dependencies & building");
637
+ npmExecFile(["install", "--no-audit", "--no-fund"], {
638
+ cwd: srcDir,
639
+ stdio: "pipe",
640
+ timeout: 120000,
641
+ });
642
+ npmExecFile(["run", "build"], {
643
+ cwd: srcDir,
644
+ stdio: "pipe",
645
+ timeout: 60000,
646
+ });
647
+ s.stop("Built successfully");
648
+ // Step 3: Update in-place
649
+ s.start("Updating files in-place");
650
+ // Old version dirs are cleaned lazily by sessionstart.mjs (age-gated >1h)
651
+ // to avoid breaking active sessions that still reference them (#181).
652
+ // Read files list from cloned repo's package.json so new directories
653
+ // (like insight/) are automatically included without chicken-and-egg issues
654
+ // where the old CLI doesn't know about new directories.
655
+ const clonedPkg = JSON.parse(readFileSync(resolve(srcDir, "package.json"), "utf-8"));
656
+ const items = [
657
+ ...(clonedPkg.files || []),
658
+ "src", "package.json",
659
+ ];
660
+ for (const item of items) {
661
+ try {
662
+ rmSync(resolve(pluginRoot, item), { recursive: true, force: true });
663
+ cpSync(resolve(srcDir, item), resolve(pluginRoot, item), { recursive: true });
664
+ }
665
+ catch { /* some files may not exist in source */ }
643
666
  }
644
- catch { /* some files may not exist in source */ }
645
- }
646
- // Write .mcp.json with CLAUDE_PLUGIN_ROOT placeholder (fixes #411).
647
- // Absolute paths bake-in the current pluginRoot dir, which sessionstart.mjs
648
- // (#181) deletes after upgrade breaking MCP server resolution. The literal
649
- // ${CLAUDE_PLUGIN_ROOT} placeholder is resolved by Claude at load-time and
650
- // stays valid across version cleanups. Matches .claude-plugin/plugin.json.
651
- const mcpConfig = {
652
- mcpServers: {
653
- "context-mode": {
654
- command: "node",
655
- args: ["${CLAUDE_PLUGIN_ROOT}/start.mjs"],
667
+ // Write .mcp.json with CLAUDE_PLUGIN_ROOT placeholder (fixes #411).
668
+ // Absolute paths bake-in the current pluginRoot dir, which sessionstart.mjs
669
+ // (#181) deletes after upgrade breaking MCP server resolution. The literal
670
+ // ${CLAUDE_PLUGIN_ROOT} placeholder is resolved by Claude at load-time and
671
+ // stays valid across version cleanups. Matches .claude-plugin/plugin.json.
672
+ const mcpConfig = {
673
+ mcpServers: {
674
+ "context-mode": {
675
+ command: "node",
676
+ args: ["${CLAUDE_PLUGIN_ROOT}/start.mjs"],
677
+ },
656
678
  },
657
- },
658
- };
659
- writeFileSync(resolve(pluginRoot, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
660
- s.stop(color.green(`Updated in-place to v${newVersion}`));
661
- // Fix registry — adapter-aware
662
- adapter.updatePluginRegistry(pluginRoot, newVersion);
663
- p.log.info(color.dim(" Registry synced to " + pluginRoot));
664
- // Install production deps
665
- s.start("Installing production dependencies");
666
- npmExecFile(["install", "--production", "--no-audit", "--no-fund"], {
667
- cwd: pluginRoot,
668
- stdio: "pipe",
669
- timeout: 60000,
670
- });
671
- s.stop("Dependencies ready");
672
- if (detection.platform !== 'opencode' && detection.platform !== 'kilo') {
673
- // Rebuild native addons for current Node.js ABI (fixes #131)
674
- s.start("Rebuilding native addons");
675
- const bsqBindingPath = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release", "better_sqlite3.node");
676
- // Skip rebuild when the binding from `npm install --production` is
677
- // already present. Earlier code ran `npm rebuild better-sqlite3`
678
- // unconditionally — its internal prebuild-install spawn raced with
679
- // the prior install's tree-prune, intermittently failing to resolve
680
- // `rc/index.js` and printing a scary "rebuild warning" even though
681
- // the binding was healthy. Pre-check eliminates the race for the
682
- // 99% case (binding survived install).
683
- if (existsSync(bsqBindingPath)) {
684
- s.stop(color.green("Native addons OK") + color.dim(" — binding present"));
685
- changes.push("better-sqlite3 binding already present (no rebuild needed)");
686
- }
687
- else {
688
- // Binding actually missing — delegate to the shared 3-layer heal
689
- // (scripts/heal-better-sqlite3.mjs, PR #410) instead of raw
690
- // `npm rebuild`. Single source of truth across postinstall +
691
- // ensure-deps + cli upgrade. Layer A spawns prebuild-install
692
- // directly via process.execPath, bypassing PATH/MSVC and the
693
- // npm-internal rc-resolution race that bit `npm rebuild`.
679
+ };
680
+ writeFileSync(resolve(pluginRoot, ".mcp.json"), JSON.stringify(mcpConfig, null, 2) + "\n");
681
+ s.stop(color.green(`Updated in-place to v${newVersion}`));
682
+ // Fix registry — adapter-aware
683
+ adapter.updatePluginRegistry(pluginRoot, newVersion);
684
+ p.log.info(color.dim(" Registry synced to " + pluginRoot));
685
+ // Install production deps
686
+ s.start("Installing production dependencies");
687
+ npmExecFile(["install", "--production", "--no-audit", "--no-fund"], {
688
+ cwd: pluginRoot,
689
+ stdio: "pipe",
690
+ timeout: 60000,
691
+ });
692
+ s.stop("Dependencies ready");
693
+ if (detection.platform !== 'opencode' && detection.platform !== 'kilo') {
694
+ // Verify native addons through the same bootstrap start.mjs imports.
695
+ // On modern Node, the ABI-specific cache file is the compatibility marker;
696
+ // the active binding alone may be stale from a previous Node ABI.
697
+ s.start("Verifying native addon ABI");
698
+ const bsqAbiCachePath = resolve(pluginRoot, "node_modules", "better-sqlite3", "build", "Release", `better_sqlite3.abi${process.versions.modules}.node`);
694
699
  try {
695
- const healUrl = pathToFileURL(resolve(pluginRoot, "scripts", "heal-better-sqlite3.mjs")).href;
696
- const { healBetterSqlite3Binding } = await import(healUrl);
697
- const result = healBetterSqlite3Binding(pluginRoot);
698
- if (result?.healed) {
699
- s.stop(color.green("Native addons healed") + color.dim(` (${result.reason})`));
700
- changes.push(`Healed better-sqlite3 binding via ${result.reason}`);
700
+ const ensureDepsPath = resolve(pluginRoot, "hooks", "ensure-deps.mjs");
701
+ if (!existsSync(ensureDepsPath)) {
702
+ throw new Error(`missing ${ensureDepsPath}`);
703
+ }
704
+ await import(`${pathToFileURL(ensureDepsPath).href}?upgrade=${Date.now()}`);
705
+ if (existsSync(bsqAbiCachePath)) {
706
+ s.stop(color.green("Native addons OK") + color.dim(" — ABI cache present"));
707
+ changes.push(`better-sqlite3 ABI ${process.versions.modules} cache ready`);
701
708
  }
702
709
  else {
703
- s.stop(color.yellow("Native addon heal needs manual step"));
704
- p.log.warn(color.dim(` Run: cd "${pluginRoot}" && npm install better-sqlite3`));
710
+ s.stop(color.yellow("Native addon ABI cache missing"));
711
+ p.log.warn(color.dim(` Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
705
712
  }
706
713
  }
707
714
  catch (err) {
708
715
  const message = err instanceof Error ? err.message : String(err);
709
- s.stop(color.yellow("Native addon heal unavailable"));
710
- p.log.warn(color.yellow("better-sqlite3 heal helper missing") +
716
+ s.stop(color.yellow("Native addon ABI bootstrap unavailable"));
717
+ p.log.warn(color.yellow("better-sqlite3 ABI repair did not run") +
711
718
  ` — ${message}` +
712
719
  color.dim(`\n Try manually: cd "${pluginRoot}" && npm rebuild better-sqlite3`));
713
720
  }
714
721
  }
715
- }
716
- // Update global npm
717
- s.start("Updating npm global package");
718
- try {
719
- npmExecFile(["install", "-g", pluginRoot, "--no-audit", "--no-fund"], {
720
- stdio: "pipe",
721
- timeout: 30000,
722
- });
723
- s.stop(color.green("npm global updated"));
724
- changes.push("Updated npm global package");
725
- }
726
- catch {
727
- s.stop(color.yellow("npm global update skipped"));
728
- p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
729
- }
730
- // Cleanup
731
- rmSync(tmpDir, { recursive: true, force: true });
732
- // Sync skills to the active install path from installed_plugins.json (#228).
733
- // Only targets the ACTUAL directory Claude Code reads from — not spraying everywhere.
734
- try {
735
- const registryPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
736
- if (existsSync(registryPath)) {
737
- const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
738
- const entries = registry?.plugins?.["context-mode@context-mode"];
739
- if (Array.isArray(entries)) {
740
- for (const entry of entries) {
741
- const installPath = entry.installPath;
742
- if (installPath && installPath !== pluginRoot && existsSync(installPath)) {
743
- const srcSkills = resolve(srcDir, "skills");
744
- if (existsSync(srcSkills)) {
745
- cpSync(srcSkills, resolve(installPath, "skills"), { recursive: true });
746
- changes.push(`Synced skills to active install path`);
722
+ // Update global npm
723
+ s.start("Updating npm global package");
724
+ try {
725
+ npmExecFile(["install", "-g", pluginRoot, "--no-audit", "--no-fund"], {
726
+ stdio: "pipe",
727
+ timeout: 30000,
728
+ });
729
+ s.stop(color.green("npm global updated"));
730
+ changes.push("Updated npm global package");
731
+ }
732
+ catch {
733
+ s.stop(color.yellow("npm global update skipped"));
734
+ p.log.info(color.dim(" Could not update global npm — may need sudo or standalone install"));
735
+ }
736
+ // Cleanup
737
+ rmSync(tmpDir, { recursive: true, force: true });
738
+ // Sync skills to the active install path from installed_plugins.json (#228).
739
+ // Only targets the ACTUAL directory Claude Code reads from not spraying everywhere.
740
+ // Issue #460 round-3: honor $CLAUDE_CONFIG_DIR so the registry lookup
741
+ // tracks relocated CC config trees.
742
+ try {
743
+ const registryPath = resolve(resolveClaudeConfigDir(), "plugins", "installed_plugins.json");
744
+ if (existsSync(registryPath)) {
745
+ const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
746
+ const entries = registry?.plugins?.["context-mode@context-mode"];
747
+ if (Array.isArray(entries)) {
748
+ for (const entry of entries) {
749
+ const installPath = entry.installPath;
750
+ if (installPath && installPath !== pluginRoot && existsSync(installPath)) {
751
+ const srcSkills = resolve(srcDir, "skills");
752
+ if (existsSync(srcSkills)) {
753
+ cpSync(srcSkills, resolve(installPath, "skills"), { recursive: true });
754
+ changes.push(`Synced skills to active install path`);
755
+ }
747
756
  }
748
757
  }
749
758
  }
750
759
  }
751
760
  }
761
+ catch { /* best effort — registry may not exist or be malformed */ }
762
+ changes.push(`Updated v${localVersion} → v${newVersion}`);
763
+ p.log.success(color.green("Plugin reinstalled from GitHub!") +
764
+ color.dim(` — v${newVersion}`));
752
765
  }
753
- catch { /* best effort — registry may not exist or be malformed */ }
754
- changes.push(newVersion !== localVersion
755
- ? `Updated v${localVersion} → v${newVersion}`
756
- : `Reinstalled v${localVersion} from GitHub`);
757
- p.log.success(color.green("Plugin reinstalled from GitHub!") +
758
- color.dim(` — v${newVersion}`));
759
766
  }
760
767
  catch (err) {
761
768
  const message = err instanceof Error ? err.message : String(err);
@@ -783,12 +790,18 @@ async function upgrade() {
783
790
  }
784
791
  // Step 4: Configure hooks — adapter-aware
785
792
  p.log.step(`Configuring ${adapter.name} hooks...`);
786
- const hookChanges = adapter.configureAllHooks(pluginRoot);
787
- for (const change of hookChanges) {
788
- p.log.info(color.dim(` ${change}`));
789
- changes.push(change);
793
+ try {
794
+ const hookChanges = adapter.configureAllHooks(pluginRoot);
795
+ for (const change of hookChanges) {
796
+ p.log.info(color.dim(` ${change}`));
797
+ changes.push(change);
798
+ }
799
+ p.log.success(color.green("Hooks configured") + color.dim(` — ${adapter.name}`));
800
+ }
801
+ catch (err) {
802
+ const message = err instanceof Error ? err.message : String(err);
803
+ throw new Error(`Hook configuration failed: ${message}`);
790
804
  }
791
- p.log.success(color.green("Hooks configured") + color.dim(` — ${adapter.name}`));
792
805
  // Step 5: Set hook script permissions — adapter-aware
793
806
  p.log.step("Setting hook script permissions...");
794
807
  const permSet = adapter.setHookPermissions(pluginRoot);
@@ -854,14 +867,17 @@ function statuslineForward() {
854
867
  // marketplace clone (#418-synced, stable across upgrades) and to the path
855
868
  // Claude Code itself loads from (installed_plugins.json) keeps the bar
856
869
  // alive instead of silently going blank.
870
+ // Issue #460 round-3: marketplace + registry paths must follow
871
+ // $CLAUDE_CONFIG_DIR so relocated CC trees still find the statusline binary.
872
+ const claudeRoot = resolveClaudeConfigDir();
857
873
  const candidates = [
858
874
  resolve(getPluginRoot(), "bin", "statusline.mjs"),
859
- resolve(homedir(), ".claude", "plugins", "marketplaces", "context-mode", "bin", "statusline.mjs"),
875
+ resolve(claudeRoot, "plugins", "marketplaces", "context-mode", "bin", "statusline.mjs"),
860
876
  ];
861
877
  // installed_plugins.json may list one or more install paths CC actually
862
878
  // loads from. Prefer those if they exist.
863
879
  try {
864
- const registryPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
880
+ const registryPath = resolve(claudeRoot, "plugins", "installed_plugins.json");
865
881
  if (existsSync(registryPath)) {
866
882
  const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
867
883
  const entries = registry?.plugins?.["context-mode@context-mode"];
@@ -48,11 +48,24 @@ export declare class NodeSQLiteAdapter {
48
48
  transaction(fn: (...args: any[]) => any): any;
49
49
  close(): void;
50
50
  }
51
+ /**
52
+ * Probe whether the supplied node:sqlite DatabaseSync constructor links a
53
+ * SQLite build that includes the FTS5 module. Some Node.js Linux builds
54
+ * (e.g. v22.14.0 on Ubuntu) ship node:sqlite without FTS5 even though the
55
+ * import succeeds, which silently breaks ctx_search/ctx_batch_execute and
56
+ * the doctor's FTS5 check (issue #461).
57
+ *
58
+ * Returns true only when a `CREATE VIRTUAL TABLE … USING fts5(x)` statement
59
+ * succeeds. Always returns false on any failure (constructor throw, missing
60
+ * module, etc.) so the caller can fall through to better-sqlite3, whose
61
+ * bundled SQLite always ships with FTS5.
62
+ */
63
+ export declare function nodeSqliteHasFts5(DatabaseSync: any): boolean;
51
64
  /**
52
65
  * Lazy-load the SQLite driver for the current runtime.
53
66
  * Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
54
- * Linux Node → node:sqlite via NodeSQLiteAdapter (issue #228).
55
- * Other Node → better-sqlite3 (native addon).
67
+ * Linux Node → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461).
68
+ * Other Node (or Linux Node without FTS5) → better-sqlite3 (native addon).
56
69
  */
57
70
  export declare function loadDatabase(): typeof DatabaseConstructor;
58
71
  /**
package/build/db-base.js CHANGED
@@ -155,11 +155,40 @@ export class NodeSQLiteAdapter {
155
155
  // Lazy loader
156
156
  // ─────────────────────────────────────────────────────────
157
157
  let _Database = null;
158
+ /**
159
+ * Probe whether the supplied node:sqlite DatabaseSync constructor links a
160
+ * SQLite build that includes the FTS5 module. Some Node.js Linux builds
161
+ * (e.g. v22.14.0 on Ubuntu) ship node:sqlite without FTS5 even though the
162
+ * import succeeds, which silently breaks ctx_search/ctx_batch_execute and
163
+ * the doctor's FTS5 check (issue #461).
164
+ *
165
+ * Returns true only when a `CREATE VIRTUAL TABLE … USING fts5(x)` statement
166
+ * succeeds. Always returns false on any failure (constructor throw, missing
167
+ * module, etc.) so the caller can fall through to better-sqlite3, whose
168
+ * bundled SQLite always ships with FTS5.
169
+ */
170
+ export function nodeSqliteHasFts5(DatabaseSync) {
171
+ let probe = null;
172
+ try {
173
+ probe = new DatabaseSync(":memory:");
174
+ probe.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)");
175
+ return true;
176
+ }
177
+ catch {
178
+ return false;
179
+ }
180
+ finally {
181
+ try {
182
+ probe?.close();
183
+ }
184
+ catch { /* probe never opened or already closed */ }
185
+ }
186
+ }
158
187
  /**
159
188
  * Lazy-load the SQLite driver for the current runtime.
160
189
  * Bun → bun:sqlite via BunSQLiteAdapter (issue #45).
161
- * Linux Node → node:sqlite via NodeSQLiteAdapter (issue #228).
162
- * Other Node → better-sqlite3 (native addon).
190
+ * Linux Node → node:sqlite via NodeSQLiteAdapter when it ships FTS5 (#228, #461).
191
+ * Other Node (or Linux Node without FTS5) → better-sqlite3 (native addon).
163
192
  */
164
193
  export function loadDatabase() {
165
194
  if (!_Database) {
@@ -185,8 +214,20 @@ export function loadDatabase() {
185
214
  else if (process.platform === "linux") {
186
215
  // Linux — try node:sqlite to avoid native addon SIGSEGV (nodejs/node#62515).
187
216
  // node:sqlite is built into Node >= 22.5, no flag needed since 22.13.
217
+ // Probe FTS5 support before committing — some Linux Node builds ship
218
+ // node:sqlite without FTS5, which would silently break ctx_search (#461).
219
+ // The probe runs at most once per process (cached via _Database below),
220
+ // so the cost of opening an in-memory DatabaseSync is negligible.
221
+ let DatabaseSync = null;
188
222
  try {
189
- const { DatabaseSync } = require(["node", "sqlite"].join(":"));
223
+ // Array.join() prevents esbuild from resolving the specifier at bundle time
224
+ // (mirrors the bun:sqlite branch above).
225
+ ({ DatabaseSync } = require(["node", "sqlite"].join(":")));
226
+ }
227
+ catch {
228
+ DatabaseSync = null;
229
+ }
230
+ if (DatabaseSync && nodeSqliteHasFts5(DatabaseSync)) {
190
231
  _Database = function NodeDatabaseFactory(path, opts) {
191
232
  const raw = new DatabaseSync(path, {
192
233
  readOnly: opts?.readonly ?? false,
@@ -194,8 +235,12 @@ export function loadDatabase() {
194
235
  return new NodeSQLiteAdapter(raw);
195
236
  };
196
237
  }
197
- catch {
198
- // node:sqlite not available — fall through to better-sqlite3
238
+ else {
239
+ // node:sqlite missing or built without FTS5 — fall through to better-sqlite3.
240
+ // Trade-off: this reintroduces the native-addon path that #228 routed
241
+ // around (Linux SIGSEGV per nodejs/node#62515). Deliberate — a visible
242
+ // crash on the rare unstable build is preferable to a silent
243
+ // "no such module: fts5" on every ctx_search call.
199
244
  _Database = require("better-sqlite3");
200
245
  }
201
246
  }
@@ -11,6 +11,8 @@ export declare function buildScriptFilename(language: Language, platform: NodeJS
11
11
  export declare function buildSpawnOptions(platform: NodeJS.Platform): {
12
12
  windowsHide: boolean;
13
13
  };
14
+ /** Pure helper — exported for unit testing. Restores parent PATH after shell startup. */
15
+ export declare function buildShellScriptContent(code: string, inheritedPath: string | undefined, platform: NodeJS.Platform): string;
14
16
  interface ExecuteOptions {
15
17
  language: Language;
16
18
  code: string;
package/build/executor.js CHANGED
@@ -42,6 +42,15 @@ export function buildScriptFilename(language, platform, shellPath) {
42
42
  export function buildSpawnOptions(platform) {
43
43
  return { windowsHide: platform === "win32" };
44
44
  }
45
+ function quoteForPosixShell(value) {
46
+ return `'${value.replace(/'/g, `'\\''`)}'`;
47
+ }
48
+ /** Pure helper — exported for unit testing. Restores parent PATH after shell startup. */
49
+ export function buildShellScriptContent(code, inheritedPath, platform) {
50
+ if (platform === "win32" || !inheritedPath)
51
+ return code;
52
+ return `export PATH=${quoteForPosixShell(inheritedPath)}\n${code}`;
53
+ }
45
54
  /**
46
55
  * Resolve the real OS temp directory, bypassing any TMPDIR env override.
47
56
  * os.tmpdir() reads TMPDIR from the environment, which some shells/tools
@@ -173,7 +182,7 @@ export class PolyglotExecutor {
173
182
  }
174
183
  const fp = join(tmpDir, buildScriptFilename(language, process.platform, language === "shell" ? this.#runtimes.shell : null));
175
184
  if (language === "shell") {
176
- writeFileSync(fp, code, { encoding: "utf-8", mode: 0o700 });
185
+ writeFileSync(fp, buildShellScriptContent(code, process.env.PATH, process.platform), { encoding: "utf-8", mode: 0o700 });
177
186
  }
178
187
  else {
179
188
  writeFileSync(fp, code, "utf-8");
@@ -210,7 +219,11 @@ export class PolyglotExecutor {
210
219
  return new Promise((res) => {
211
220
  // Only .cmd/.bat shims need shell on Windows; real executables don't.
212
221
  // Using shell: true globally causes process-tree kill issues with MSYS2/Git Bash.
213
- const needsShell = isWin && ["tsx", "ts-node", "elixir"].includes(cmd[0]);
222
+ // "bun" is included as defense-in-depth: bunCommand() prefers absolute
223
+ // .exe paths now (#506), but if it falls back to the bare "bun" string
224
+ // on Windows that resolution typically goes through a `bun.cmd` shim
225
+ // (npm i -g bun) which CreateProcess can't execute without cmd.exe.
226
+ const needsShell = isWin && ["tsx", "ts-node", "elixir", "bun"].includes(cmd[0]);
214
227
  // On Windows with Git Bash, pass the script as `bash -c "source /posix/path"`
215
228
  // rather than `bash /path/to/script.sh`. This avoids MSYS2 path mangling
216
229
  // while still allowing MSYS_NO_PATHCONV to protect non-ASCII paths in commands.