context-mode 1.0.28 → 1.0.29

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.28"
9
+ "version": "1.0.29"
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.28",
16
+ "version": "1.0.29",
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.28",
3
+ "version": "1.0.29",
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.28",
6
+ "version": "1.0.29",
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.28",
3
+ "version": "1.0.29",
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
@@ -978,8 +978,35 @@ server.registerTool("ctx_fetch_and_index", {
978
978
  .string()
979
979
  .optional()
980
980
  .describe("Label for the indexed content (e.g., 'React useEffect docs', 'Supabase Auth API')"),
981
+ force: z
982
+ .boolean()
983
+ .optional()
984
+ .describe("Skip cache and re-fetch even if content was recently indexed"),
981
985
  }),
982
- }, async ({ url, source }) => {
986
+ }, async ({ url, source, force }) => {
987
+ // TTL cache: if source was indexed within 24h, return cached hint
988
+ if (!force) {
989
+ const store = getStore();
990
+ const label = source ?? url;
991
+ const meta = store.getSourceMeta(label);
992
+ if (meta) {
993
+ const indexedAt = new Date(meta.indexedAt + "Z"); // SQLite datetime is UTC without Z
994
+ const ageMs = Date.now() - indexedAt.getTime();
995
+ const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
996
+ if (ageMs < TTL_MS) {
997
+ const ageHours = Math.floor(ageMs / (60 * 60 * 1000));
998
+ const ageMin = Math.floor(ageMs / (60 * 1000));
999
+ const ageStr = ageHours > 0 ? `${ageHours}h ago` : ageMin > 0 ? `${ageMin}m ago` : "just now";
1000
+ return trackResponse("ctx_fetch_and_index", {
1001
+ content: [{
1002
+ type: "text",
1003
+ text: `Cached: **${meta.label}** — ${meta.chunkCount} sections, indexed ${ageStr} (fresh, TTL: 24h).\nTo refresh: call ctx_fetch_and_index again with \`force: true\`.\n\nYou MUST call search() to answer questions about this content — this cached response contains no content.\nUse: search(queries: [...], source: "${meta.label}")`,
1004
+ }],
1005
+ });
1006
+ }
1007
+ // Stale (>24h) — fall through to re-fetch silently
1008
+ }
1009
+ }
983
1010
  // Generate a unique temp file path for the subprocess to write fetched content.
984
1011
  // This bypasses the executor's 100KB stdout truncation — content goes file→handler directly.
985
1012
  const outputPath = join(tmpdir(), `ctx-fetch-${Date.now()}-${Math.random().toString(36).slice(2)}.dat`);
package/build/store.d.ts CHANGED
@@ -41,6 +41,12 @@ export declare class ContentStore {
41
41
  searchTrigram(query: string, limit?: number, source?: string, mode?: "AND" | "OR", contentType?: "code" | "prose"): SearchResult[];
42
42
  fuzzyCorrect(query: string): string | null;
43
43
  searchWithFallback(query: string, limit?: number, source?: string, contentType?: "code" | "prose"): SearchResult[];
44
+ getSourceMeta(label: string): {
45
+ label: string;
46
+ chunkCount: number;
47
+ codeChunkCount: number;
48
+ indexedAt: string;
49
+ } | null;
44
50
  listSources(): Array<{
45
51
  label: string;
46
52
  chunkCount: number;
package/build/store.js CHANGED
@@ -194,6 +194,7 @@ export class ContentStore {
194
194
  #stmtSourceChunkCount;
195
195
  #stmtChunkContent;
196
196
  #stmtStats;
197
+ #stmtSourceMeta;
197
198
  constructor(dbPath) {
198
199
  const Database = loadDatabase();
199
200
  this.#dbPath =
@@ -385,6 +386,7 @@ export class ContentStore {
385
386
  ORDER BY c.rowid`);
386
387
  this.#stmtSourceChunkCount = this.#db.prepare("SELECT chunk_count FROM sources WHERE id = ?");
387
388
  this.#stmtChunkContent = this.#db.prepare("SELECT content FROM chunks WHERE source_id = ?");
389
+ this.#stmtSourceMeta = this.#db.prepare("SELECT label, chunk_count, code_chunk_count, indexed_at FROM sources WHERE label = ?");
388
390
  this.#stmtStats = this.#db.prepare(`
389
391
  SELECT
390
392
  (SELECT COUNT(*) FROM sources) AS sources,
@@ -648,6 +650,12 @@ export class ContentStore {
648
650
  return [];
649
651
  }
650
652
  // ── Sources ──
653
+ getSourceMeta(label) {
654
+ const row = this.#stmtSourceMeta.get(label);
655
+ if (!row)
656
+ return null;
657
+ return { label: row.label, chunkCount: row.chunk_count, codeChunkCount: row.code_chunk_count, indexedAt: row.indexed_at };
658
+ }
651
659
  listSources() {
652
660
  return this.#stmtListSources.all();
653
661
  }