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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/server.js +26 -2
- package/build/session/analytics.d.ts +38 -0
- package/build/session/analytics.js +58 -1
- package/cli.bundle.mjs +133 -131
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +93 -91
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Claude Code plugins by Mert Koseoğlu",
|
|
9
|
-
"version": "1.0.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 = [];
|