context-mode 1.0.79 → 1.0.81

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 (44) 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/cli.js +57 -0
  6. package/build/server.js +94 -1
  7. package/build/store.js +28 -10
  8. package/cli.bundle.mjs +109 -102
  9. package/insight/components.json +25 -0
  10. package/insight/index.html +13 -0
  11. package/insight/package.json +54 -0
  12. package/insight/server.mjs +624 -0
  13. package/insight/src/components/analytics.tsx +112 -0
  14. package/insight/src/components/ui/badge.tsx +52 -0
  15. package/insight/src/components/ui/button.tsx +58 -0
  16. package/insight/src/components/ui/card.tsx +103 -0
  17. package/insight/src/components/ui/chart.tsx +371 -0
  18. package/insight/src/components/ui/collapsible.tsx +19 -0
  19. package/insight/src/components/ui/input.tsx +20 -0
  20. package/insight/src/components/ui/progress.tsx +83 -0
  21. package/insight/src/components/ui/scroll-area.tsx +55 -0
  22. package/insight/src/components/ui/separator.tsx +23 -0
  23. package/insight/src/components/ui/table.tsx +114 -0
  24. package/insight/src/components/ui/tabs.tsx +82 -0
  25. package/insight/src/components/ui/tooltip.tsx +64 -0
  26. package/insight/src/lib/api.ts +71 -0
  27. package/insight/src/lib/utils.ts +6 -0
  28. package/insight/src/main.tsx +22 -0
  29. package/insight/src/routeTree.gen.ts +189 -0
  30. package/insight/src/router.tsx +19 -0
  31. package/insight/src/routes/__root.tsx +55 -0
  32. package/insight/src/routes/enterprise.tsx +316 -0
  33. package/insight/src/routes/index.tsx +914 -0
  34. package/insight/src/routes/knowledge.tsx +221 -0
  35. package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +137 -0
  36. package/insight/src/routes/search.tsx +97 -0
  37. package/insight/src/routes/sessions.tsx +179 -0
  38. package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +181 -0
  39. package/insight/src/styles.css +104 -0
  40. package/insight/tsconfig.json +29 -0
  41. package/insight/vite.config.ts +19 -0
  42. package/openclaw.plugin.json +1 -1
  43. package/package.json +2 -1
  44. package/server.bundle.mjs +79 -75
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.79"
9
+ "version": "1.0.81"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "context-mode",
14
14
  "source": "./",
15
15
  "description": "Claude Code MCP plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
16
- "version": "1.0.79",
16
+ "version": "1.0.81",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "description": "MCP server that saves 98% of your context window with session continuity. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and automatic state restore across compactions.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
@@ -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.79",
6
+ "version": "1.0.81",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "1.0.79",
3
+ "version": "1.0.81",
4
4
  "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.",
5
5
  "author": {
6
6
  "name": "Mert Koseoğlu",
package/build/cli.js CHANGED
@@ -93,6 +93,9 @@ else if (args[0] === "upgrade") {
93
93
  else if (args[0] === "hook") {
94
94
  hookDispatch(args[1], args[2]);
95
95
  }
96
+ else if (args[0] === "insight") {
97
+ insight(args[1] ? Number(args[1]) : 4747);
98
+ }
96
99
  else {
97
100
  // Default: start MCP server
98
101
  import("./server.js");
@@ -353,6 +356,60 @@ async function doctor() {
353
356
  : color.yellow("Some checks need attention — see above for details"));
354
357
  return 0;
355
358
  }
359
+ /* -------------------------------------------------------
360
+ * Insight — analytics dashboard
361
+ * ------------------------------------------------------- */
362
+ async function insight(port) {
363
+ const { execSync, spawn } = await import("node:child_process");
364
+ const { statSync, mkdirSync, cpSync } = await import("node:fs");
365
+ const insightSource = resolve(__dirname, "insight");
366
+ // Adapter-agnostic cache: use ~/.claude/context-mode/insight-cache as default
367
+ // (matches server.ts pattern but CLI doesn't have adapter detection)
368
+ const cacheDir = join(homedir(), ".claude", "context-mode", "insight-cache");
369
+ if (!existsSync(join(insightSource, "server.mjs"))) {
370
+ console.error("Error: Insight source not found. Try upgrading context-mode.");
371
+ process.exit(1);
372
+ }
373
+ mkdirSync(cacheDir, { recursive: true });
374
+ // Copy source if newer
375
+ const srcMtime = statSync(join(insightSource, "server.mjs")).mtimeMs;
376
+ const cacheMtime = existsSync(join(cacheDir, "server.mjs"))
377
+ ? statSync(join(cacheDir, "server.mjs")).mtimeMs : 0;
378
+ if (srcMtime > cacheMtime) {
379
+ console.log("Copying Insight source...");
380
+ cpSync(insightSource, cacheDir, { recursive: true, force: true });
381
+ }
382
+ // Install deps
383
+ if (!existsSync(join(cacheDir, "node_modules"))) {
384
+ console.log("Installing dependencies (first run)...");
385
+ execSync("npm install --production=false", { cwd: cacheDir, stdio: "inherit", timeout: 120000 });
386
+ }
387
+ // Build
388
+ console.log("Building dashboard...");
389
+ execSync("npx vite build", { cwd: cacheDir, stdio: "pipe", timeout: 30000 });
390
+ // Start server
391
+ const url = `http://localhost:${port}`;
392
+ console.log(`\n context-mode Insight\n ${url}\n`);
393
+ const child = spawn("node", [join(cacheDir, "server.mjs")], {
394
+ cwd: cacheDir,
395
+ env: { ...process.env, PORT: String(port) },
396
+ stdio: "inherit",
397
+ });
398
+ // Open browser
399
+ const platform = process.platform;
400
+ try {
401
+ if (platform === "darwin")
402
+ execSync(`open "${url}"`, { stdio: "pipe" });
403
+ else if (platform === "win32")
404
+ execSync(`start "" "${url}"`, { stdio: "pipe" });
405
+ else
406
+ execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null`, { stdio: "pipe" });
407
+ }
408
+ catch { /* best effort */ }
409
+ // Keep alive until Ctrl+C
410
+ process.on("SIGINT", () => { child.kill(); process.exit(0); });
411
+ process.on("SIGTERM", () => { child.kill(); process.exit(0); });
412
+ }
356
413
  /* -------------------------------------------------------
357
414
  * Upgrade — adapter-aware hook configuration
358
415
  * ------------------------------------------------------- */
package/build/server.js CHANGED
@@ -3,7 +3,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { createHash } from "node:crypto";
6
- import { existsSync, unlinkSync, readdirSync, readFileSync, writeFileSync, rmSync, mkdirSync } from "node:fs";
6
+ import { existsSync, unlinkSync, readdirSync, readFileSync, writeFileSync, rmSync, mkdirSync, cpSync, statSync } from "node:fs";
7
+ import { execSync } from "node:child_process";
7
8
  import { join, dirname, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
10
  import { homedir, tmpdir } from "node:os";
@@ -1853,6 +1854,98 @@ server.registerTool("ctx_purge", {
1853
1854
  }],
1854
1855
  });
1855
1856
  });
1857
+ // ── ctx-insight: analytics dashboard ──────────────────────────────────────────
1858
+ server.registerTool("ctx_insight", {
1859
+ title: "Open Insight Dashboard",
1860
+ description: "Opens the context-mode Insight dashboard in the browser. " +
1861
+ "Shows personal analytics: session activity, tool usage, error rate, " +
1862
+ "parallel work patterns, project focus, and actionable insights. " +
1863
+ "First run installs dependencies (~30s). Subsequent runs open instantly.",
1864
+ inputSchema: z.object({
1865
+ port: z.number().optional().describe("Port to serve on (default: 4747)"),
1866
+ }),
1867
+ }, async ({ port: userPort }) => {
1868
+ const port = userPort || 4747;
1869
+ const insightSource = resolve(__pkg_dir, "insight");
1870
+ // Use adapter-aware path: derive from sessions dir (works across all 12 adapters)
1871
+ const sessDir = getSessionDir();
1872
+ const cacheDir = join(dirname(sessDir), "insight-cache");
1873
+ // Verify source exists
1874
+ if (!existsSync(join(insightSource, "server.mjs"))) {
1875
+ return trackResponse("ctx_insight", {
1876
+ content: [{ type: "text", text: "Error: Insight source not found in plugin. Try upgrading context-mode." }],
1877
+ });
1878
+ }
1879
+ try {
1880
+ const steps = [];
1881
+ // Ensure cache dir
1882
+ mkdirSync(cacheDir, { recursive: true });
1883
+ // Copy source files if needed (check by comparing server.mjs mtime)
1884
+ const srcMtime = statSync(join(insightSource, "server.mjs")).mtimeMs;
1885
+ const cacheMtime = existsSync(join(cacheDir, "server.mjs"))
1886
+ ? statSync(join(cacheDir, "server.mjs")).mtimeMs : 0;
1887
+ if (srcMtime > cacheMtime) {
1888
+ steps.push("Copying source files...");
1889
+ cpSync(insightSource, cacheDir, { recursive: true, force: true });
1890
+ steps.push("Source files copied.");
1891
+ }
1892
+ // Install deps if needed
1893
+ const hasNodeModules = existsSync(join(cacheDir, "node_modules"));
1894
+ if (!hasNodeModules) {
1895
+ steps.push("Installing dependencies (first run, ~30s)...");
1896
+ execSync("npm install --production=false", {
1897
+ cwd: cacheDir,
1898
+ stdio: "pipe",
1899
+ timeout: 120000,
1900
+ });
1901
+ steps.push("Dependencies installed.");
1902
+ }
1903
+ // Build
1904
+ steps.push("Building dashboard...");
1905
+ execSync("npx vite build", {
1906
+ cwd: cacheDir,
1907
+ stdio: "pipe",
1908
+ timeout: 30000,
1909
+ });
1910
+ steps.push("Build complete.");
1911
+ // Start server in background
1912
+ const { spawn } = await import("node:child_process");
1913
+ const child = spawn("node", [join(cacheDir, "server.mjs")], {
1914
+ cwd: cacheDir,
1915
+ env: { ...process.env, PORT: String(port) },
1916
+ detached: true,
1917
+ stdio: "ignore",
1918
+ });
1919
+ child.unref();
1920
+ // Wait for server to be ready
1921
+ await new Promise(r => setTimeout(r, 1500));
1922
+ // Open browser (cross-platform)
1923
+ const url = `http://localhost:${port}`;
1924
+ const platform = process.platform;
1925
+ try {
1926
+ if (platform === "darwin")
1927
+ execSync(`open "${url}"`, { stdio: "pipe" });
1928
+ else if (platform === "win32")
1929
+ execSync(`start "" "${url}"`, { stdio: "pipe" });
1930
+ else
1931
+ execSync(`xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null`, { stdio: "pipe" });
1932
+ }
1933
+ catch { /* browser open is best-effort */ }
1934
+ steps.push(`Dashboard running at ${url}`);
1935
+ return trackResponse("ctx_insight", {
1936
+ content: [{
1937
+ type: "text",
1938
+ text: steps.map(s => `- ${s}`).join("\n") + `\n\nOpen: ${url}\nPID: ${child.pid} · Stop: kill ${child.pid}`,
1939
+ }],
1940
+ });
1941
+ }
1942
+ catch (err) {
1943
+ const msg = err instanceof Error ? err.message : String(err);
1944
+ return trackResponse("ctx_insight", {
1945
+ content: [{ type: "text", text: `Insight setup failed: ${msg}` }],
1946
+ });
1947
+ }
1948
+ });
1856
1949
  // ─────────────────────────────────────────────────────────
1857
1950
  // Server startup
1858
1951
  // ─────────────────────────────────────────────────────────
package/build/store.js CHANGED
@@ -41,7 +41,12 @@ function sanitizeQuery(query, mode = "AND") {
41
41
  !["AND", "OR", "NOT", "NEAR"].includes(w.toUpperCase()));
42
42
  if (words.length === 0)
43
43
  return '""';
44
- return words.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
44
+ // Filter stopwords to improve BM25 ranking common terms like "update",
45
+ // "test", "fix" appear everywhere and dilute relevance scoring.
46
+ // Fall back to unfiltered words if ALL terms are stopwords.
47
+ const meaningful = words.filter((w) => !STOPWORDS.has(w.toLowerCase()));
48
+ const final = meaningful.length > 0 ? meaningful : words;
49
+ return final.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
45
50
  }
46
51
  function sanitizeTrigramQuery(query, mode = "AND") {
47
52
  const cleaned = query.replace(/["'(){}[\]*:^~]/g, "").trim();
@@ -50,7 +55,9 @@ function sanitizeTrigramQuery(query, mode = "AND") {
50
55
  const words = cleaned.split(/\s+/).filter((w) => w.length >= 3);
51
56
  if (words.length === 0)
52
57
  return "";
53
- return words.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
58
+ const meaningful = words.filter((w) => !STOPWORDS.has(w.toLowerCase()));
59
+ const final = meaningful.length > 0 ? meaningful : words;
60
+ return final.map((w) => `"${w}"`).join(mode === "OR" ? " OR " : " ");
54
61
  }
55
62
  function levenshtein(a, b) {
56
63
  if (a.length === 0)
@@ -264,6 +271,10 @@ export class ContentStore {
264
271
  #stmtChunkContent;
265
272
  #stmtStats;
266
273
  #stmtSourceMeta;
274
+ // Cleanup path
275
+ #stmtCleanupChunks;
276
+ #stmtCleanupChunksTrigram;
277
+ #stmtCleanupSources;
267
278
  // FTS5 optimization: track inserts and optimize periodically to defragment
268
279
  // the index. FTS5 b-trees fragment over many insert/delete cycles, degrading
269
280
  // search performance. SQLite's built-in 'optimize' merges b-tree segments.
@@ -547,6 +558,10 @@ export class ContentStore {
547
558
  (SELECT COUNT(*) FROM chunks) AS chunks,
548
559
  (SELECT COUNT(*) FROM chunks WHERE content_type = 'code') AS codeChunks
549
560
  `);
561
+ // Cleanup path — cached to avoid recompiling SQL on each periodic call
562
+ this.#stmtCleanupChunks = this.#db.prepare("DELETE FROM chunks WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
563
+ this.#stmtCleanupChunksTrigram = this.#db.prepare("DELETE FROM chunks_trigram WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
564
+ this.#stmtCleanupSources = this.#db.prepare("DELETE FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days')");
550
565
  }
551
566
  // ── Index ──
552
567
  index(options) {
@@ -768,10 +783,14 @@ export class ContentStore {
768
783
  }
769
784
  // ── Proximity Reranking ──
770
785
  #applyProximityReranking(results, query) {
771
- const terms = query
786
+ const allTerms = query
772
787
  .toLowerCase()
773
788
  .split(/\s+/)
774
789
  .filter((w) => w.length >= 2);
790
+ // Exclude stopwords from proximity/title scoring — they match everywhere
791
+ // and inflate boosts for irrelevant chunks. Keep all terms as fallback.
792
+ const filtered = allTerms.filter((w) => !STOPWORDS.has(w));
793
+ const terms = filtered.length > 0 ? filtered : allTerms;
775
794
  return results
776
795
  .map((r) => {
777
796
  // Title-match boost: query terms found in the chunk title get a boost.
@@ -806,11 +825,13 @@ export class ContentStore {
806
825
  return reranked.map((r) => ({ ...r, matchLayer: "rrf" }));
807
826
  }
808
827
  // Step 2: Fuzzy correction → RRF re-run
828
+ // Skip stopwords — they'll be filtered by sanitizeQuery anyway, and each
829
+ // fuzzyCorrect call hits the vocab DB + runs levenshtein comparisons.
809
830
  const words = query
810
831
  .toLowerCase()
811
832
  .trim()
812
833
  .split(/\s+/)
813
- .filter((w) => w.length >= 3);
834
+ .filter((w) => w.length >= 3 && !STOPWORDS.has(w));
814
835
  const original = words.join(" ");
815
836
  const correctedWords = words.map((w) => this.fuzzyCorrect(w) ?? w);
816
837
  const correctedQuery = correctedWords.join(" ");
@@ -898,13 +919,10 @@ export class ContentStore {
898
919
  * Returns count of deleted sources.
899
920
  */
900
921
  cleanupStaleSources(maxAgeDays) {
901
- const deleteChunks = this.#db.prepare("DELETE FROM chunks WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
902
- const deleteChunksTrigram = this.#db.prepare("DELETE FROM chunks_trigram WHERE source_id IN (SELECT id FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days'))");
903
- const deleteSources = this.#db.prepare("DELETE FROM sources WHERE datetime(indexed_at) < datetime('now', '-' || ? || ' days')");
904
922
  const cleanup = this.#db.transaction((days) => {
905
- deleteChunks.run(days);
906
- deleteChunksTrigram.run(days);
907
- return deleteSources.run(days);
923
+ this.#stmtCleanupChunks.run(days);
924
+ this.#stmtCleanupChunksTrigram.run(days);
925
+ return this.#stmtCleanupSources.run(days);
908
926
  });
909
927
  const info = cleanup(maxAgeDays);
910
928
  return info.changes;