context-mode 1.0.69 → 1.0.71

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code plugins by Mert Koseoğlu",
9
- "version": "1.0.69"
9
+ "version": "1.0.71"
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.69",
16
+ "version": "1.0.71",
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.69",
3
+ "version": "1.0.71",
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.69",
6
+ "version": "1.0.71",
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.69",
3
+ "version": "1.0.71",
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/server.js CHANGED
@@ -7,6 +7,7 @@ import { existsSync, unlinkSync, readdirSync, readFileSync, rmSync, mkdirSync }
7
7
  import { join, dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { homedir, tmpdir } from "node:os";
10
+ import { request as httpsRequest } from "node:https";
10
11
  import { z } from "zod";
11
12
  import { PolyglotExecutor } from "./executor.js";
12
13
  import { ContentStore, cleanupStaleDBs, cleanupStaleContentDBs } from "./store.js";
@@ -180,7 +181,72 @@ const sessionStats = {
180
181
  cacheBytesSaved: 0, // bytes avoided by TTL cache hits
181
182
  sessionStart: Date.now(),
182
183
  };
184
+ // ── Version outdated warning ──────────────────────────────────────────────
185
+ // Non-blocking npm check at startup. trackResponse prepends warning
186
+ // using a burst cadence: 3 warnings → 1h silent → 3 warnings → repeat.
187
+ let _latestVersion = null;
188
+ let _warningBurstCount = 0;
189
+ let _lastBurstStart = 0;
190
+ const VERSION_BURST_SIZE = 3;
191
+ const VERSION_SILENT_MS = 60 * 60 * 1000; // 1 hour
192
+ async function fetchLatestVersion() {
193
+ return new Promise((res) => {
194
+ const req = httpsRequest("https://registry.npmjs.org/context-mode/latest", { headers: { Connection: "close" } }, (resp) => {
195
+ let raw = "";
196
+ resp.on("data", (chunk) => { raw += chunk; });
197
+ resp.on("end", () => {
198
+ try {
199
+ const data = JSON.parse(raw);
200
+ res(data.version ?? "unknown");
201
+ }
202
+ catch {
203
+ res("unknown");
204
+ }
205
+ });
206
+ });
207
+ req.on("error", () => res("unknown"));
208
+ req.setTimeout(5000, () => { req.destroy(); res("unknown"); });
209
+ req.end();
210
+ });
211
+ }
212
+ function getUpgradeHint() {
213
+ const name = _detectedAdapter?.name;
214
+ if (name === "Claude Code")
215
+ return "/ctx-upgrade";
216
+ if (name === "OpenClaw")
217
+ return "npm run install:openclaw";
218
+ if (name === "Pi")
219
+ return "npm run build";
220
+ return "npm update -g context-mode";
221
+ }
222
+ function isOutdated() {
223
+ if (!_latestVersion || _latestVersion === "unknown")
224
+ return false;
225
+ return _latestVersion !== VERSION;
226
+ }
227
+ function shouldShowVersionWarning() {
228
+ if (!isOutdated())
229
+ return false;
230
+ const now = Date.now();
231
+ // Start of a new burst?
232
+ if (_warningBurstCount >= VERSION_BURST_SIZE) {
233
+ if (now - _lastBurstStart < VERSION_SILENT_MS)
234
+ return false; // still silent
235
+ _warningBurstCount = 0; // silence over, reset burst
236
+ }
237
+ if (_warningBurstCount === 0)
238
+ _lastBurstStart = now;
239
+ _warningBurstCount++;
240
+ return true;
241
+ }
183
242
  function trackResponse(toolName, response) {
243
+ // Prepend version outdated warning if needed
244
+ if (shouldShowVersionWarning() && response.content.length > 0) {
245
+ const hint = getUpgradeHint();
246
+ response.content[0].text =
247
+ `⚠️ context-mode v${VERSION} outdated → v${_latestVersion} available. Upgrade: ${hint}\n\n` +
248
+ response.content[0].text;
249
+ }
184
250
  const bytes = response.content.reduce((sum, c) => sum + Buffer.byteLength(c.text), 0);
185
251
  sessionStats.calls[toolName] = (sessionStats.calls[toolName] || 0) + 1;
186
252
  sessionStats.bytesReturned[toolName] =
@@ -465,6 +531,19 @@ server.registerTool("ctx_execute", {
465
531
  // The closure approach (function(__cm_req){ var require=...; })(require) correctly
466
532
  // shadows the CJS require for all code inside, including __cm_main().
467
533
  instrumentedCode = `
534
+ // FS read instrumentation — count bytes read via fs.readFileSync/readFile
535
+ let __cm_fs=0;
536
+ process.on('exit',()=>{if(__cm_fs>0)try{process.stderr.write('__CM_FS__:'+__cm_fs+'\\n')}catch{}});
537
+ (function(){
538
+ try{
539
+ var f=typeof require!=='undefined'?require('fs'):null;
540
+ if(!f)return;
541
+ var ors=f.readFileSync;
542
+ f.readFileSync=function(){var r=ors.apply(this,arguments);if(Buffer.isBuffer(r))__cm_fs+=r.length;else if(typeof r==='string')__cm_fs+=Buffer.byteLength(r);return r;};
543
+ var orf=f.readFile;
544
+ if(orf)f.readFile=function(){var a=Array.from(arguments),cb=a.pop();orf.apply(this,a.concat([function(e,d){if(!e&&d){if(Buffer.isBuffer(d))__cm_fs+=d.length;else if(typeof d==='string')__cm_fs+=Buffer.byteLength(d);}cb(e,d);}]));};
545
+ }catch{}
546
+ })();
468
547
  let __cm_net=0;
469
548
  // Report network bytes on process exit — works with both promise and callback patterns.
470
549
  // process.on('exit') fires after all I/O completes, unlike .finally() which fires
@@ -519,6 +598,12 @@ __cm_main().catch(e=>{console.error(e);process.exitCode=1});${background ? '\nse
519
598
  // Clean the metric line from stderr
520
599
  result.stderr = result.stderr.replace(/\n?__CM_NET__:\d+\n?/g, "");
521
600
  }
601
+ // Parse sandbox FS read metrics from stderr
602
+ const fsMatch = result.stderr?.match(/__CM_FS__:(\d+)/);
603
+ if (fsMatch) {
604
+ sessionStats.bytesSandboxed += parseInt(fsMatch[1]);
605
+ result.stderr = result.stderr.replace(/\n?__CM_FS__:\d+\n?/g, "");
606
+ }
522
607
  if (result.timedOut) {
523
608
  const partialOutput = result.stdout?.trim();
524
609
  if (result.backgrounded && partialOutput) {
@@ -1770,6 +1855,9 @@ async function main() {
1770
1855
  }
1771
1856
  }
1772
1857
  catch { /* best effort — _detectedAdapter stays null, falls back to .claude */ }
1858
+ // Non-blocking version check — result stored for trackResponse warnings
1859
+ fetchLatestVersion().then(v => { if (v !== "unknown")
1860
+ _latestVersion = v; });
1773
1861
  console.error(`Context Mode MCP server v${VERSION} running on stdio`);
1774
1862
  console.error(`Detected runtimes:\n${getRuntimeSummary(runtimes)}`);
1775
1863
  if (!hasBunRuntime()) {
@@ -373,9 +373,9 @@ export declare class AnalyticsEngine {
373
373
  queryAll(runtimeStats: RuntimeStats): FullReport;
374
374
  }
375
375
  /**
376
- * Render a FullReport as the same markdown output ctx_stats has always produced.
376
+ * Render a FullReport as a marketing-friendly, outcome-focused session story.
377
377
  *
378
- * Preserves the exact output format: Context Window Protection table,
379
- * TTL Cache section, Session Continuity table, and Analytics JSON block.
378
+ * Framework: Persona -> Metric -> Evidence -> Action -> ROI
379
+ * The output tells a narrative instead of dumping raw numbers.
380
380
  */
381
381
  export declare function formatReport(report: FullReport): string;
@@ -613,62 +613,95 @@ function kb(b) {
613
613
  return `${(b / 1024).toFixed(1)}KB`;
614
614
  }
615
615
  /**
616
- * Render a FullReport as the same markdown output ctx_stats has always produced.
616
+ * Render a FullReport as a marketing-friendly, outcome-focused session story.
617
617
  *
618
- * Preserves the exact output format: Context Window Protection table,
619
- * TTL Cache section, Session Continuity table, and Analytics JSON block.
618
+ * Framework: Persona -> Metric -> Evidence -> Action -> ROI
619
+ * The output tells a narrative instead of dumping raw numbers.
620
620
  */
621
621
  export function formatReport(report) {
622
- const lines = [
623
- `## context-mode \u2014 Session Report (${report.session.uptime_min} min)`,
624
- ];
625
- // ── Feature 1: Context Window Protection ──
626
- lines.push("", `### Context Window Protection`, "");
622
+ const lines = [];
623
+ // ── Hero: headline story ──
624
+ lines.push(`## Think in Code -- Session Report`);
627
625
  if (report.savings.total_calls === 0) {
628
- lines.push(`No context-mode tool calls yet. Use \`batch_execute\`, \`execute\`, or \`fetch_and_index\` to keep raw output out of your context window.`);
629
- }
630
- else {
631
- lines.push(`| Metric | Value |`, `|--------|------:|`, `| Total data processed | **${kb(report.savings.total_processed)}** |`, `| Kept in sandbox (never entered context) | **${kb(report.savings.kept_out)}** |`, `| Entered context | ${kb(report.savings.total_bytes_returned)} |`, `| Estimated tokens saved | ~${Math.round(report.savings.kept_out / 4).toLocaleString()} |`, `| **Context savings** | **${report.savings.savings_ratio.toFixed(1)}x (${report.savings.pct}% reduction)** |`);
632
- // Per-tool breakdown
633
- if (report.savings.by_tool.length > 0) {
634
- lines.push("", `| Tool | Calls | Context | Tokens |`, `|------|------:|--------:|-------:|`);
635
- for (const t of report.savings.by_tool) {
636
- lines.push(`| ${t.tool} | ${t.calls} | ${kb(t.calls > 0 ? (t.tokens * 4) : 0)} | ~${t.tokens.toLocaleString()} |`);
637
- }
638
- lines.push(`| **Total** | **${report.savings.total_calls}** | **${kb(report.savings.total_bytes_returned)}** | **~${Math.round(report.savings.total_bytes_returned / 4).toLocaleString()}** |`);
639
- }
640
- if (report.savings.kept_out > 0) {
641
- lines.push("", `Without context-mode, **${kb(report.savings.total_processed)}** of raw output would flood your context window. Instead, **${report.savings.pct}%** stayed in sandbox.`);
642
- }
643
- // Cache savings section
644
- if (report.cache) {
645
- lines.push("", `### TTL Cache`, "", `| Metric | Value |`, `|--------|------:|`, `| Cache hits | **${report.cache.hits}** |`, `| Data avoided by cache | **${kb(report.cache.bytes_saved)}** |`, `| Network requests saved | **${report.cache.hits}** |`, `| TTL remaining | **~${report.cache.ttl_hours_left}h** |`, "", `Content was already indexed in the knowledge base \u2014 ${report.cache.hits} fetch${report.cache.hits > 1 ? "es" : ""} skipped entirely. **${kb(report.cache.bytes_saved)}** of network I/O avoided. Search results served directly from local FTS5 index.`);
646
- if (report.cache.total_savings_ratio > report.savings.savings_ratio) {
647
- lines.push("", `**Total context savings (sandbox + cache): ${report.cache.total_savings_ratio.toFixed(1)}x** \u2014 ${kb(report.cache.total_with_cache)} processed, only ${kb(report.savings.total_bytes_returned)} entered context.`);
648
- }
626
+ lines.push("", "No sandbox activity yet. Use ctx_execute or ctx_batch_execute to start saving context.", "", "---", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
627
+ return lines.join("\n");
628
+ }
629
+ const totalProcessed = report.savings.kept_out + (report.cache ? report.cache.bytes_saved : 0);
630
+ const totalReturned = report.savings.total_bytes_returned;
631
+ const grandTotal = totalProcessed + totalReturned;
632
+ const savingsPercent = grandTotal > 0
633
+ ? ((1 - totalReturned / grandTotal) * 100).toFixed(1)
634
+ : "0.0";
635
+ // Rough estimate: 4 bytes per token, ~2 min reading time per 1000 tokens
636
+ const tokensSaved = Math.round(totalProcessed / 4);
637
+ const extraMinutes = Math.round((tokensSaved / 1000) * 2);
638
+ lines.push("", `Your agent processed ${kb(totalProcessed)} of data.`, `Only ${kb(totalReturned)} entered your context window.`, "", `**Context saved: ${savingsPercent}% -- session extended by ~${extraMinutes} minutes**`);
639
+ // ── What happened ──
640
+ lines.push("", "### What happened", "");
641
+ // Count per-tool categories
642
+ const toolCallMap = new Map();
643
+ for (const t of report.savings.by_tool) {
644
+ toolCallMap.set(t.tool, t.calls);
645
+ }
646
+ const executeCount = (toolCallMap.get("ctx_execute") ?? 0) +
647
+ (toolCallMap.get("ctx_execute_file") ?? 0);
648
+ const batchCount = toolCallMap.get("ctx_batch_execute") ?? 0;
649
+ const searchCount = toolCallMap.get("ctx_search") ?? 0;
650
+ const fetchCount = toolCallMap.get("ctx_fetch_and_index") ?? 0;
651
+ const fileCount = executeCount + batchCount;
652
+ const networkCount = fetchCount;
653
+ const cacheCount = report.cache ? report.cache.hits : 0;
654
+ if (fileCount > 0) {
655
+ lines.push(`-> ${fileCount} file${fileCount !== 1 ? "s" : ""} analyzed in sandbox (never entered context)`);
656
+ }
657
+ if (networkCount > 0) {
658
+ lines.push(`-> ${networkCount} API call${networkCount !== 1 ? "s" : ""} sandboxed (responses indexed, not dumped)`);
659
+ }
660
+ if (searchCount > 0) {
661
+ lines.push(`-> ${searchCount} search quer${searchCount !== 1 ? "ies" : "y"} served from index`);
662
+ }
663
+ if (cacheCount > 0) {
664
+ lines.push(`-> ${cacheCount} repeat fetch${cacheCount !== 1 ? "es" : ""} avoided (TTL cache)`);
665
+ }
666
+ // ── Per-tool breakdown ──
667
+ const activatedTools = report.savings.by_tool.filter((t) => t.calls > 0);
668
+ if (activatedTools.length > 0) {
669
+ lines.push("", "### Per-tool breakdown", "", "| Tool | Calls | Data processed | Context used | Saved |", "|------|------:|---------------:|-------------:|------:|");
670
+ for (const t of activatedTools) {
671
+ const processed = t.tokens * 4; // bytes approximation from tokens
672
+ const contextUsed = t.context_kb * 1024;
673
+ const savedPct = processed > 0
674
+ ? (((processed - contextUsed) / processed) *
675
+ 100).toFixed(0)
676
+ : "--";
677
+ lines.push(`| ${t.tool} | ${t.calls} | ${kb(processed)} | ${kb(contextUsed)} | ${savedPct}% |`);
649
678
  }
650
679
  }
651
- // ── Session Continuity ──
680
+ // ── Session continuity ──
652
681
  if (report.continuity.total_events > 0) {
653
- lines.push("", "### Session Continuity", "", "| What's preserved | Count | I remember... | Why it matters |", "|------------------|------:|---------------|----------------|");
654
- for (const row of report.continuity.by_category) {
655
- lines.push(`| ${row.label} | ${row.count} | ${row.preview} | ${row.why} |`);
682
+ lines.push("", "### Session continuity", "");
683
+ const parts = [];
684
+ if (report.continuity.compact_count > 0) {
685
+ parts.push(`${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""}`);
686
+ }
687
+ parts.push(`${report.continuity.total_events} event${report.continuity.total_events !== 1 ? "s" : ""} preserved`);
688
+ // Count tasks from continuity categories
689
+ const taskRow = report.continuity.by_category.find((c) => c.category === "task" || c.category === "tasks");
690
+ if (taskRow && taskRow.count > 0) {
691
+ parts.push(`${taskRow.count} task${taskRow.count !== 1 ? "s" : ""} tracked`);
656
692
  }
657
- lines.push(`| **Total** | **${report.continuity.total_events}** | | **Zero knowledge lost on compact** |`);
658
- lines.push("");
693
+ lines.push(parts.join(" | "));
659
694
  if (report.continuity.compact_count > 0) {
660
- lines.push(`Context has been compacted **${report.continuity.compact_count} time(s)** \u2014 session knowledge was preserved each time.`);
695
+ lines.push("", `Session knowledge preserved across ${report.continuity.compact_count} compaction${report.continuity.compact_count !== 1 ? "s" : ""} -- zero context lost.`);
661
696
  }
662
697
  else {
663
- lines.push(`When your context compacts, all of this will restore Claude's awareness \u2014 no starting from scratch.`);
698
+ lines.push("", "When your context compacts, all of this will restore awareness -- no starting from scratch.");
664
699
  }
665
700
  if (report.continuity.resume_ready) {
666
- lines.push(`Resume snapshot ready for the next compaction.`);
701
+ lines.push("Resume snapshot ready for the next compaction.");
667
702
  }
668
- lines.push("");
669
- lines.push(`> **Note:** Previous session data is loaded when you start a new session. Without \`--continue\`, old session history is cleaned up to keep the database lean.`);
670
703
  }
671
- // ── Analytics JSON ──
704
+ // ── Analytics JSON (for power users) ──
672
705
  const analyticsJson = {
673
706
  session: {
674
707
  duration_min: report.session.duration_min,
@@ -702,7 +735,7 @@ export function formatReport(report) {
702
735
  continuity: { total_events: report.continuity.total_events },
703
736
  };
704
737
  lines.push("", "### Analytics (27 metrics)", "", "```json", JSON.stringify(analyticsJson, null, 2), "```");
705
- // Formatting directive
706
- lines.push("", "---", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
738
+ // Footer
739
+ lines.push("", "---", "context-mode -- github.com/mksglu/context-mode", "", "_Display this entire report as-is in your response. Do NOT summarize, collapse, or paraphrase any section._");
707
740
  return lines.join("\n");
708
741
  }