context-mode 1.0.132 → 1.0.133

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.132"
9
+ "version": "1.0.133"
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.132",
16
+ "version": "1.0.133",
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.132",
3
+ "version": "1.0.133",
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.132",
6
+ "version": "1.0.133",
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.132",
3
+ "version": "1.0.133",
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
@@ -1701,7 +1701,24 @@ export function buildFetchCode(url, outputPath) {
1701
1701
  // can serve a public IP for the parent's pre-flight ssrfGuard lookup and
1702
1702
  // then a blocked IP (e.g. 169.254.169.254 IMDS) for the subprocess fetch's
1703
1703
  // own lookup — classic DNS rebinding across the parent/child boundary.
1704
- const classifyIpSrc = classifyIp.toString();
1704
+ //
1705
+ // CRITICAL: bundlers (esbuild) rename top-level identifiers — `classifyIp`
1706
+ // becomes e.g. `_h` in server.bundle.mjs. `classifyIp.toString()` returns
1707
+ // the renamed source `function _h(t){...}`, but the embedded subprocess
1708
+ // template references the literal name `classifyIp` (and the function's
1709
+ // own internal recursion is also `_h(...)`). Result: the subprocess sees
1710
+ // `function _h(t){...; return _h(...)}` injected, then references to
1711
+ // `classifyIp` blow up with `ReferenceError: classifyIp is not defined`.
1712
+ //
1713
+ // Fix: emit `var <fnName> = <fn-expr>; var classifyIp = <fnName>;`. The
1714
+ // named function expression preserves recursion under whatever name the
1715
+ // bundler chose, and the alias re-exposes the canonical `classifyIp`
1716
+ // identifier the rest of the embedded script depends on.
1717
+ const classifyIpInner = classifyIp.toString();
1718
+ const classifyIpFnName = classifyIp.name || "classifyIp";
1719
+ const classifyIpSrc = classifyIpFnName === "classifyIp"
1720
+ ? `var classifyIp = ${classifyIpInner};`
1721
+ : `var ${classifyIpFnName} = ${classifyIpInner};\nvar classifyIp = ${classifyIpFnName};`;
1705
1722
  const strictMode = process.env.CTX_FETCH_STRICT === "1";
1706
1723
  return `
1707
1724
  const TurndownService = require(${turndownPath});
@@ -2597,7 +2614,14 @@ server.registerTool("ctx_stats", {
2597
2614
  }
2598
2615
  if (sid) {
2599
2616
  conversation = getConversationStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash });
2600
- const convReal = getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash });
2617
+ // v1.0.133 Slice 3: pass contentDbPath so getRealBytesStats can
2618
+ // join chunks WHERE session_id = sid and fold the indexed
2619
+ // content bytes into the per-conversation bar. Without this,
2620
+ // Mert's session showed ~200B (event metadata only) even with
2621
+ // 49 MB of indexed content sitting in the content DB.
2622
+ // Render-time read-only — no DB mutation, no backfill.
2623
+ const contentDbPath = getStorePath();
2624
+ const convReal = getRealBytesStats({ sessionId: sid, sessionsDir: getSessionDir(), worktreeHash: dbHash, contentDbPath });
2601
2625
  const lifeReal = getRealBytesStats({ sessionsDir: getSessionDir() });
2602
2626
  realBytes = { conversation: convReal, lifetime: lifeReal };
2603
2627
  }
@@ -358,8 +358,39 @@ export interface RealBytesStats {
358
358
  bytesAvoided: number;
359
359
  bytesReturned: number;
360
360
  snapshotBytes: number;
361
+ /**
362
+ * v1.0.133 Slice 3: bytes attributed to this session in the FTS5 content
363
+ * DB — `SUM(LENGTH(title) + LENGTH(content)) FROM chunks WHERE session_id = ?`.
364
+ *
365
+ * Read-only, render-time computation. Populated only when
366
+ * `getRealBytesStats` is called with both `sessionId` AND `contentDbPath`
367
+ * (i.e. the conversation tier from ctx_stats). Lifetime / project tiers
368
+ * leave this at 0 — aggregating across every adapter's content DB is a
369
+ * separate concern.
370
+ *
371
+ * Legacy chunks with empty `session_id` (pre-Slice-1) are NOT backfilled:
372
+ * the architect rejected the time-window join as unsafe. Old conversations
373
+ * stay low; new conversations populate honestly.
374
+ */
375
+ contentBytes: number;
361
376
  totalSavedTokens: number;
362
377
  }
378
+ /**
379
+ * v1.0.133 Slice 3: Sum the bytes attributed to one session in the FTS5
380
+ * content DB.
381
+ *
382
+ * Returns `LENGTH(title) + LENGTH(content)` summed across every chunk
383
+ * whose `session_id` column matches `sessionId`. Best-effort — returns 0
384
+ * when the DB file is missing, the schema lacks the `session_id` column
385
+ * (pre-Slice-1 content DBs), or the query fails. Never throws.
386
+ *
387
+ * Render-time only. Does NOT mutate the content DB. Architect-approved
388
+ * because the read-only join carries no risk of cross-session attribution
389
+ * (the FK was set at chunk insert time by Slice 1).
390
+ */
391
+ export declare function getContentBytesForSession(sessionId: string, contentDbPath: string, opts?: {
392
+ loadDatabase?: () => unknown;
393
+ }): number;
363
394
  /**
364
395
  * Compute real-bytes stats across one session, one project (worktree
365
396
  * filter), or every session on disk (lifetime).
@@ -378,6 +409,13 @@ export declare function getRealBytesStats(opts: {
378
409
  sessionId?: string;
379
410
  sessionsDir?: string;
380
411
  worktreeHash?: string;
412
+ /**
413
+ * v1.0.133 Slice 3: when set alongside `sessionId`, the function joins
414
+ * the FTS5 content DB at this path and folds chunk bytes into
415
+ * `bytesAvoided` + `totalSavedTokens` + `contentBytes`. Render-time
416
+ * only — no DB writes.
417
+ */
418
+ contentDbPath?: string;
381
419
  loadDatabase?: () => unknown;
382
420
  }): RealBytesStats;
383
421
  /**
@@ -706,6 +706,50 @@ export function getConversationStats(opts) {
706
706
  byDay,
707
707
  };
708
708
  }
709
+ /**
710
+ * v1.0.133 Slice 3: Sum the bytes attributed to one session in the FTS5
711
+ * content DB.
712
+ *
713
+ * Returns `LENGTH(title) + LENGTH(content)` summed across every chunk
714
+ * whose `session_id` column matches `sessionId`. Best-effort — returns 0
715
+ * when the DB file is missing, the schema lacks the `session_id` column
716
+ * (pre-Slice-1 content DBs), or the query fails. Never throws.
717
+ *
718
+ * Render-time only. Does NOT mutate the content DB. Architect-approved
719
+ * because the read-only join carries no risk of cross-session attribution
720
+ * (the FK was set at chunk insert time by Slice 1).
721
+ */
722
+ export function getContentBytesForSession(sessionId, contentDbPath, opts) {
723
+ if (!sessionId || !contentDbPath)
724
+ return 0;
725
+ if (!existsSync(contentDbPath))
726
+ return 0;
727
+ let DatabaseCtor = null;
728
+ try {
729
+ DatabaseCtor = opts?.loadDatabase
730
+ ? opts.loadDatabase()
731
+ : loadDatabaseImpl();
732
+ }
733
+ catch {
734
+ return 0;
735
+ }
736
+ if (!DatabaseCtor)
737
+ return 0;
738
+ try {
739
+ const db = new DatabaseCtor(contentDbPath, { readonly: true });
740
+ try {
741
+ const row = db.prepare(`SELECT COALESCE(SUM(LENGTH(content) + LENGTH(title)), 0) AS bytes
742
+ FROM chunks WHERE session_id = ?`).get(sessionId);
743
+ return Number(row?.bytes ?? 0);
744
+ }
745
+ finally {
746
+ db.close();
747
+ }
748
+ }
749
+ catch {
750
+ return 0;
751
+ }
752
+ }
709
753
  /**
710
754
  * Compute real-bytes stats across one session, one project (worktree
711
755
  * filter), or every session on disk (lifetime).
@@ -726,6 +770,7 @@ export function getRealBytesStats(opts) {
726
770
  bytesAvoided: 0,
727
771
  bytesReturned: 0,
728
772
  snapshotBytes: 0,
773
+ contentBytes: 0,
729
774
  totalSavedTokens: 0,
730
775
  };
731
776
  const sessionsDir = opts.sessionsDir
@@ -812,8 +857,19 @@ export function getRealBytesStats(opts) {
812
857
  }
813
858
  catch { /* missing tables / corrupt — skip */ }
814
859
  }
860
+ // v1.0.133 Slice 3: fold content DB chunk bytes for this session into
861
+ // bytesAvoided. Skipped silently when caller didn't pass contentDbPath
862
+ // (lifetime / project tiers, or pre-Slice-3 callers). Treated as
863
+ // "avoided" because indexed chunks are bytes that would have been
864
+ // re-inflated into context on every search if the model had to
865
+ // re-read raw files.
866
+ let contentBytes = 0;
867
+ if (opts.sessionId && opts.contentDbPath) {
868
+ contentBytes = getContentBytesForSession(opts.sessionId, opts.contentDbPath, { loadDatabase: opts.loadDatabase });
869
+ bytesAvoided += contentBytes;
870
+ }
815
871
  const totalSavedTokens = Math.floor((eventDataBytes + bytesAvoided + snapshotBytes) / 4);
816
- return { eventDataBytes, bytesAvoided, bytesReturned, snapshotBytes, totalSavedTokens };
872
+ return { eventDataBytes, bytesAvoided, bytesReturned, snapshotBytes, contentBytes, totalSavedTokens };
817
873
  }
818
874
  const DEFAULT_REAL_USAGE_FILTER = {
819
875
  minEvents: 100,
@@ -975,6 +1031,7 @@ export function getMultiAdapterRealBytesStats(opts) {
975
1031
  bytesAvoided: 0,
976
1032
  bytesReturned: 0,
977
1033
  snapshotBytes: 0,
1034
+ contentBytes: 0,
978
1035
  totalSavedTokens: 0,
979
1036
  };
980
1037
  const perAdapter = [];