context-mode 1.0.118 → 1.0.119

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 (43) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  4. package/.openclaw-plugin/package.json +1 -1
  5. package/build/adapters/pi/mcp-bridge.d.ts +28 -3
  6. package/build/adapters/pi/mcp-bridge.js +127 -14
  7. package/build/adapters/qwen-code/index.js +6 -2
  8. package/build/cli.js +93 -5
  9. package/build/opencode-plugin.js +2 -5
  10. package/build/server.js +25 -8
  11. package/build/util/project-dir.js +9 -5
  12. package/cli.bundle.mjs +148 -139
  13. package/hooks/core/routing.mjs +13 -0
  14. package/openclaw.plugin.json +1 -1
  15. package/package.json +5 -6
  16. package/scripts/heal-better-sqlite3.mjs +53 -6
  17. package/scripts/heal-installed-plugins.mjs +104 -0
  18. package/scripts/postinstall.mjs +35 -1
  19. package/server.bundle.mjs +88 -88
  20. package/skills/UPSTREAM-CREDITS.md +51 -0
  21. package/skills/diagnose/SKILL.md +122 -0
  22. package/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  23. package/skills/grill-me/SKILL.md +15 -0
  24. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  25. package/skills/grill-with-docs/CONTEXT-FORMAT.md +77 -0
  26. package/skills/grill-with-docs/SKILL.md +93 -0
  27. package/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  28. package/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +44 -0
  29. package/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  30. package/skills/improve-codebase-architecture/SKILL.md +76 -0
  31. package/skills/tdd/SKILL.md +114 -0
  32. package/skills/tdd/deep-modules.md +33 -0
  33. package/skills/tdd/interface-design.md +31 -0
  34. package/skills/tdd/mocking.md +59 -0
  35. package/skills/tdd/refactoring.md +10 -0
  36. package/skills/tdd/tests.md +61 -0
  37. package/start.mjs +25 -1
  38. package/build/cache-heal.d.ts +0 -48
  39. package/build/cache-heal.js +0 -150
  40. package/build/routing-block.d.ts +0 -8
  41. package/build/routing-block.js +0 -86
  42. package/build/tool-naming.d.ts +0 -4
  43. package/build/tool-naming.js +0 -24
@@ -157,6 +157,12 @@ const SAFE_COMMAND_PATTERNS = [
157
157
  /^pwd$/,
158
158
  /^whoami$/,
159
159
  /^hostname(?:\s+-[a-zA-Z]+)?$/,
160
+ // uname (#517): short-flag probes only (`-a`, `-srm`). No path operands —
161
+ // uname doesn't take any, and refusing them keeps the pattern strict.
162
+ /^uname(?:\s+-[a-zA-Z]+)?$/,
163
+ // id (#517): bare `id`, single short flag (`-u`, `-g`), or single user
164
+ // operand (`id mksglu`). Output is one line — bounded by definition.
165
+ /^id(?:\s+\S+)?$/,
160
166
  /^date(?:\s+[^\r\n]+)?$/,
161
167
  /^echo\s/,
162
168
  /^printf\s/,
@@ -166,6 +172,10 @@ const SAFE_COMMAND_PATTERNS = [
166
172
  /^readlink(?:\s+[^\r\n]+)?$/,
167
173
  /^basename(?:\s+[^\r\n]+)?$/,
168
174
  /^dirname(?:\s+[^\r\n]+)?$/,
175
+ // realpath (#517): canonical path resolution prints one line per operand.
176
+ // Same shape as readlink — single-line `[^\r\n]+` to mirror the operator-gate
177
+ // defense-in-depth from #470.
178
+ /^realpath(?:\s+[^\r\n]+)?$/,
169
179
  // Filesystem ops (silent on success, errors on stderr only).
170
180
  // For cp / mv / rm we explicitly refuse `-v` / `--verbose`: verbose
171
181
  // mode prints one line per file and can flood on big trees
@@ -177,6 +187,9 @@ const SAFE_COMMAND_PATTERNS = [
177
187
  /^mv(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
178
188
  /^cp(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
179
189
  /^rm(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
190
+ // ln (#517): silent on success — same `-v` / `--verbose` carve-out as
191
+ // cp/mv/rm. Bulk symlink operations with -v flood one line per link.
192
+ /^ln(?!\s+-[a-zA-Z]*v\b)(?!\s+--verbose\b)\s+[^\r\n]+$/,
180
193
  // ls — refuse recursive (-R / --recursive) to keep output bounded.
181
194
  /^ls(?!\s+-[a-zA-Z]*R)(?!\s+--recursive)(?:\s+[^\r\n]+)?$/,
182
195
  // git read-only / status subcommands
@@ -3,7 +3,7 @@
3
3
  "name": "Context Mode",
4
4
  "kind": "tool",
5
5
  "description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
6
- "version": "1.0.118",
6
+ "version": "1.0.119",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.118",
3
+ "version": "1.0.119",
4
4
  "type": "module",
5
5
  "description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
6
6
  "author": "Mert Koseoğlu",
@@ -84,10 +84,11 @@
84
84
  "LICENSE"
85
85
  ],
86
86
  "scripts": {
87
- "build": "tsc && node -e \"if(process.platform!=='win32'){require('fs').chmodSync('build/cli.js',0o755)}\" && npm run bundle",
87
+ "build": "tsc && node -e \"if(process.platform!=='win32'){require('fs').chmodSync('build/cli.js',0o755)}\" && npm run bundle && npm run assert-bundle",
88
+ "assert-bundle": "node scripts/assert-bundle.mjs server.bundle.mjs cli.bundle.mjs hooks/session-extract.bundle.mjs hooks/session-snapshot.bundle.mjs hooks/session-db.bundle.mjs",
88
89
  "bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify && esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=cli.bundle.mjs --external:better-sqlite3 --minify && esbuild src/session/extract.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-extract.bundle.mjs --minify && esbuild src/session/snapshot.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-snapshot.bundle.mjs --minify && esbuild src/session/db.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-db.bundle.mjs --external:better-sqlite3 --minify",
89
90
  "version-sync": "node scripts/version-sync.mjs",
90
- "version": "node scripts/version-sync.mjs && git add package.json .claude-plugin/plugin.json .claude-plugin/marketplace.json .openclaw-plugin/openclaw.plugin.json .openclaw-plugin/package.json openclaw.plugin.json .pi/extensions/context-mode/package.json",
91
+ "version": "node scripts/version-sync.mjs && git add package.json .claude-plugin/plugin.json .claude-plugin/marketplace.json .cursor-plugin/plugin.json .codex-plugin/plugin.json .codex-plugin/marketplace.json .openclaw-plugin/openclaw.plugin.json .openclaw-plugin/package.json openclaw.plugin.json .pi/extensions/context-mode/package.json",
91
92
  "prepublishOnly": "npm run build",
92
93
  "dev": "npx tsx src/server.ts",
93
94
  "setup": "npx tsx src/cli.ts setup",
@@ -107,14 +108,12 @@
107
108
  "@clack/prompts": "^1.0.1",
108
109
  "@mixmark-io/domino": "^2.2.0",
109
110
  "@modelcontextprotocol/sdk": "^1.26.0",
111
+ "better-sqlite3": "^12.6.2",
110
112
  "picocolors": "^1.1.1",
111
113
  "turndown": "^7.2.0",
112
114
  "turndown-plugin-gfm": "^1.0.2",
113
115
  "zod": "^3.25.0"
114
116
  },
115
- "optionalDependencies": {
116
- "better-sqlite3": "^12.6.2"
117
- },
118
117
  "devDependencies": {
119
118
  "@types/better-sqlite3": "^7.6.13",
120
119
  "@types/node": "^22.19.11",
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  import { existsSync } from "node:fs";
29
- import { execSync, spawnSync } from "node:child_process";
29
+ import { execSync, execFileSync, spawnSync } from "node:child_process";
30
30
  import { resolve } from "node:path";
31
31
  import { createRequire } from "node:module";
32
32
 
@@ -39,17 +39,64 @@ import { createRequire } from "node:module";
39
39
  export function healBetterSqlite3Binding(pkgRoot) {
40
40
  try {
41
41
  const bsqRoot = resolve(pkgRoot, "node_modules", "better-sqlite3");
42
+ const bindingPath = resolve(bsqRoot, "build", "Release", "better_sqlite3.node");
43
+ const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
44
+
42
45
  if (!existsSync(bsqRoot)) {
43
- // No package at all — caller (ensure-deps install branch) handles this.
44
- return { healed: false, reason: "package-missing" };
46
+ // ── Package itself missing (#514) ───────────────────────────────
47
+ // npm@7+ silently drops optionalDependencies whose engines field
48
+ // does not match the running Node version (Node 26 vs
49
+ // better-sqlite3@12.x → silent skip, package never written).
50
+ // Even after promoting the package back to dependencies, an
51
+ // existing install where the package directory was previously
52
+ // skipped will still have an empty slot. Take ownership and
53
+ // install the package by name with --no-optional, which forces
54
+ // npm to install the named package even if it would otherwise
55
+ // be filtered out as an optional dep.
56
+ try {
57
+ execFileSync(
58
+ npmBin,
59
+ [
60
+ "install",
61
+ "better-sqlite3",
62
+ "--no-optional",
63
+ "--no-save",
64
+ "--no-audit",
65
+ "--no-fund",
66
+ ],
67
+ {
68
+ cwd: pkgRoot,
69
+ stdio: "pipe",
70
+ timeout: 180000,
71
+ shell: process.platform === "win32",
72
+ },
73
+ );
74
+ } catch {
75
+ // Install failed — surface the cause via the manual-required
76
+ // exit so the caller (cli.ts upgrade verifier) reports it.
77
+ return { healed: false, reason: "package-missing" };
78
+ }
79
+ // Re-check after install. If npm wrote the package AND its
80
+ // postinstall produced the binding, we're done. Otherwise fall
81
+ // through into the binding-missing flow below.
82
+ if (existsSync(bindingPath)) {
83
+ return { healed: true, reason: "package-installed" };
84
+ }
85
+ if (!existsSync(bsqRoot)) {
86
+ // npm reported success but the directory is still absent.
87
+ // This indicates the engine-mismatch silent-skip is still in
88
+ // effect (e.g. npm < 7 or pnpm without --shamefully-hoist).
89
+ return { healed: false, reason: "package-missing" };
90
+ }
91
+ // Package present but binding still missing — recurse into
92
+ // the existing 3-layer heal that owns prebuild-install / npm
93
+ // install / actionable-stderr.
45
94
  }
46
- const bindingPath = resolve(bsqRoot, "build", "Release", "better_sqlite3.node");
95
+
47
96
  if (existsSync(bindingPath)) {
48
97
  return { healed: true, reason: "binding-present" };
49
98
  }
50
99
 
51
- const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
52
-
53
100
  // ── Layer A: spawn prebuild-install directly via process.execPath ──
54
101
  // Bypasses cmd.exe PATH and MSVC requirement.
55
102
  try {
@@ -179,3 +179,107 @@ export function healSettingsEnabledPlugins({ settingsPath, pluginKey }) {
179
179
 
180
180
  return { healed };
181
181
  }
182
+
183
+ // ─────────────────────────────────────────────────────────────────────────
184
+ // Issue #523 (v1.0.119) — Layer 5 heal: plugin.json mcpServers args
185
+ //
186
+ // /ctx-upgrade in v1.0.118 wrote `.mcp.json` with the literal
187
+ // `${CLAUDE_PLUGIN_ROOT}` placeholder (#411) but did NOT touch
188
+ // `.claude-plugin/plugin.json`. On Windows, start.mjs's `normalizeHooksOnStartup`
189
+ // (#378) rewrites that file's `mcpServers["context-mode"].args[0]` to an
190
+ // absolute path. If `pluginRoot` happens to be the upgrade tmpdir at the time
191
+ // of normalization (or an earlier upgrade left absolute paths in place), the
192
+ // resulting plugin.json carries a `<tmpdir>/context-mode-upgrade-<epoch>/start.mjs`
193
+ // path. After Node tmpdir cleanup, MCP fails to spawn with ENOENT and the user
194
+ // has no /ctx-upgrade escape hatch.
195
+ //
196
+ // This heal is the sibling of #411's `.mcp.json` fix:
197
+ // - Detects tmpdir-prefixed args[0] (epoch-pattern, OS-agnostic)
198
+ // - Rewrites to literal `${CLAUDE_PLUGIN_ROOT}/start.mjs` placeholder
199
+ // - Never touches sibling mcpServers entries (only `pluginKey`'s server)
200
+ // - Refuses to write outside `pluginCacheRoot` (path-traversal guard)
201
+ //
202
+ // Single source of truth shared by:
203
+ // - `start.mjs` HEAL 5b (every MCP boot)
204
+ // - `scripts/postinstall.mjs` (every `npm install -g context-mode`)
205
+ // - `src/cli.ts` upgrade() (post-bump)
206
+ // ─────────────────────────────────────────────────────────────────────────
207
+
208
+ /** Matches `<sep>context-mode-upgrade-<digits><sep>`. OS-agnostic. */
209
+ const TMPDIR_UPGRADE_RE = /[/\\]context-mode-upgrade-\d+[/\\]/;
210
+ const PLACEHOLDER_ARG = "${CLAUDE_PLUGIN_ROOT}/start.mjs";
211
+
212
+ /**
213
+ * Heal `<pluginRoot>/.claude-plugin/plugin.json` mcpServers args.
214
+ *
215
+ * @param {{
216
+ * pluginRoot: string,
217
+ * pluginCacheRoot: string,
218
+ * pluginKey: string,
219
+ * }} opts
220
+ * @returns {HealResult}
221
+ */
222
+ export function healPluginJsonMcpServers({ pluginRoot, pluginCacheRoot, pluginKey }) {
223
+ if (!pluginRoot || !pluginCacheRoot || !pluginKey) {
224
+ return { healed: [], skipped: "missing-args" };
225
+ }
226
+
227
+ // Path-traversal guard: refuse to touch a plugin root that escapes the
228
+ // declared cache root. Mirrors HEAL 3's guard.
229
+ const resolvedRoot = resolve(pluginRoot);
230
+ const cacheRootWithSep = resolve(pluginCacheRoot) + sep;
231
+ if (!resolvedRoot.startsWith(cacheRootWithSep)) {
232
+ return { healed: [], skipped: "outside-cache-root" };
233
+ }
234
+
235
+ const pluginJsonPath = resolve(pluginRoot, ".claude-plugin", "plugin.json");
236
+ if (!existsSync(pluginJsonPath)) {
237
+ return { healed: [], skipped: "no-plugin-json" };
238
+ }
239
+
240
+ let raw;
241
+ try { raw = readFileSync(pluginJsonPath, "utf-8"); }
242
+ catch (err) { return { healed: [], error: `read-failed: ${(err && err.message) || err}` }; }
243
+
244
+ let parsed;
245
+ try { parsed = JSON.parse(raw); }
246
+ catch (err) { return { healed: [], error: `parse-failed: ${(err && err.message) || err}` }; }
247
+
248
+ const servers = parsed && parsed.mcpServers;
249
+ if (!servers || typeof servers !== "object") {
250
+ return { healed: [], skipped: "no-mcp-servers" };
251
+ }
252
+
253
+ // Derive our server name from pluginKey ("context-mode@context-mode" → "context-mode").
254
+ const ourServerName = pluginKey.split("@")[0];
255
+ const ours = servers[ourServerName];
256
+ if (!ours || typeof ours !== "object" || !Array.isArray(ours.args)) {
257
+ return { healed: [], skipped: "no-our-server" };
258
+ }
259
+
260
+ /** @type {string[]} */
261
+ const healed = [];
262
+ const before = ours.args;
263
+ const after = before.map((a) => {
264
+ if (typeof a !== "string") return a;
265
+ // Detect tmpdir-prefixed `context-mode-upgrade-<digits>` paths and
266
+ // rewrite to the literal placeholder that survives upgrades. Only
267
+ // rewrites when the trailing component is `start.mjs` (our entrypoint).
268
+ if (TMPDIR_UPGRADE_RE.test(a) && /[/\\]start\.mjs$/.test(a)) {
269
+ return PLACEHOLDER_ARG;
270
+ }
271
+ return a;
272
+ });
273
+ const changed = after.some((v, i) => v !== before[i]);
274
+ if (changed) {
275
+ ours.args = after;
276
+ healed.push("plugin-json-args");
277
+ try {
278
+ writeFileSync(pluginJsonPath, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
279
+ } catch (err) {
280
+ return { healed: [], error: `write-failed: ${(err && err.message) || err}` };
281
+ }
282
+ }
283
+
284
+ return { healed };
285
+ }
@@ -14,7 +14,7 @@ import { dirname, resolve, join, sep } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { homedir } from "node:os";
16
16
  import { healBetterSqlite3Binding } from "./heal-better-sqlite3.mjs";
17
- import { healInstalledPlugins, healSettingsEnabledPlugins } from "./heal-installed-plugins.mjs";
17
+ import { healInstalledPlugins, healSettingsEnabledPlugins, healPluginJsonMcpServers } from "./heal-installed-plugins.mjs";
18
18
 
19
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
20
  const pkgRoot = resolve(__dirname, "..");
@@ -96,6 +96,40 @@ if (isGlobalInstall()) {
96
96
  }
97
97
  // skipped/error: silent — already covered by the prior heal's stderr line.
98
98
  } catch { /* never block install */ }
99
+
100
+ // v1.0.119: Layer 5b (Issue #523). Heal .claude-plugin/plugin.json's
101
+ // mcpServers["context-mode"].args[0] when /ctx-upgrade left a tmpdir-prefixed
102
+ // path baked in. Iterates EVERY installed cache entry's installPath so
103
+ // already-broken users self-recover the next time `npm install -g context-mode`
104
+ // runs. Best effort, never blocks install.
105
+ try {
106
+ const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
107
+ const cacheRoot = resolve(homedir(), ".claude", "plugins", "cache");
108
+ if (existsSync(ipPath)) {
109
+ const ip = JSON.parse(readFileSync(ipPath, "utf-8"));
110
+ const entries = (ip && ip.plugins && ip.plugins["context-mode@context-mode"]) || [];
111
+ let healedAny = false;
112
+ if (Array.isArray(entries)) {
113
+ for (const entry of entries) {
114
+ const installPath = entry && entry.installPath;
115
+ if (typeof installPath !== "string" || !installPath) continue;
116
+ try {
117
+ const r = healPluginJsonMcpServers({
118
+ pluginRoot: installPath,
119
+ pluginCacheRoot: cacheRoot,
120
+ pluginKey: "context-mode@context-mode",
121
+ });
122
+ if (r && Array.isArray(r.healed) && r.healed.length > 0) {
123
+ healedAny = true;
124
+ }
125
+ } catch { /* per-entry best effort */ }
126
+ }
127
+ }
128
+ if (healedAny) {
129
+ process.stderr.write("context-mode: healed plugin.json mcpServers args (Issue #523)\n");
130
+ }
131
+ }
132
+ } catch { /* never block install */ }
99
133
  }
100
134
 
101
135
  // ── 0. Self-heal Layer 3: Backward symlink for stale registry (anthropics/claude-code#46915) ──