@mxalbert/context-mode 2.0.0 → 2.0.2

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": "2.0.0"
9
+ "version": "2.0.2"
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": "2.0.0",
16
+ "version": "2.0.2",
17
17
  "author": {
18
18
  "name": "Mert Koseoğlu"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-mode",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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": "2.0.0",
6
+ "version": "2.0.2",
7
7
  "sandbox": {
8
8
  "mode": "permissive",
9
9
  "filesystem_access": "full",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mxalbert/context-mode",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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/README.md CHANGED
@@ -483,13 +483,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
483
483
 
484
484
  The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — OpenCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
485
485
 
486
- 2. *(Optional)* Copy the routing rules file:
487
-
488
- ```bash
489
- cp node_modules/context-mode/configs/opencode/AGENTS.md AGENTS.md
490
- ```
491
-
492
- The plugin already injects its routing guidance (which tools to use, which commands are blocked and why) into the system prompt on every model dispatch — on both opencode v1 and v2. The copy is only useful if you want the same guidance also visible as native project rules (`AGENTS.md`) for sessions without the plugin. Your own `AGENTS.md`/`CLAUDE.md`/`CONTEXT.md` rules are captured automatically for session continuity — no copy needed.
486
+ 2. **No AGENTS.md copy needed.** The plugin injects the FULL routing mandate — Think in Code, BLOCKED/REDIRECTED commands, tool selection, memory tables (the complete [`configs/opencode/AGENTS.md`](configs/opencode/AGENTS.md) content, verbatim) — into the system prompt on every model dispatch. An existing copy stays harmless: OpenCode loads a project `AGENTS.md` natively as active project rules, and the plugin auto-skips its own injection when the project file's content matches the shipped template (normalized full-content comparison — an edited copy keeps getting re-injected, deliberately, so your edits don't get shadowed by the stale shipped version).
493
487
 
494
488
  3. Restart OpenCode.
495
489
 
@@ -497,7 +491,7 @@ Full configs: [`configs/cursor/hooks.json`](configs/cursor/hooks.json) | [`confi
497
491
 
498
492
  **Upgrade note:** If an existing config has BOTH `plugin: ["context-mode"]` AND `mcp.context-mode`, OpenCode will register zero `ctx_*` tools — the plugin path correctly suppresses MCP duplicates, but the legacy MCP entry confuses the loader. Run `context-mode upgrade` to remove the legacy `mcp.context-mode` entry; your other MCP servers are preserved. v1.0.140+ emits a stderr diagnostic with the same guidance when this happens.
499
493
 
500
- **Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
494
+ **Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`, and the plugin injects the full routing guidance (condensed block + complete AGENTS.md mandate) into the system prompt on every model dispatch — no copied AGENTS.md is required. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
501
495
 
502
496
  > **Note:** OpenCode lacks a real SessionStart hook ([#14808](https://github.com/sst/opencode/issues/14808), [#5409](https://github.com/sst/opencode/issues/5409)). The plugin uses `experimental.chat.system.transform` as a surrogate — it injects both the routing block and resume snapshots into the system prompt. User-prompt capture uses `chat.message` instead of the missing UserPromptSubmit hook. AGENTS.md/CLAUDE.md/CONTEXT.md rules are captured automatically on first hook fire per project.
503
497
 
@@ -523,11 +517,7 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
523
517
 
524
518
  The `plugin` entry registers all 11 `ctx_*` tools natively and enables hooks — KiloCode calls context-mode's TypeScript plugin in-process, so there is no redundant stdio MCP child per session.
525
519
 
526
- 2. *(Optional)* Copy the routing rules file. KiloCode shares the OpenCode plugin architecture; the plugin already injects its routing guidance into the system prompt on every model dispatch, so the copy is only useful if you want the guidance also visible as native project rules:
527
-
528
- ```bash
529
- cp node_modules/context-mode/configs/opencode/AGENTS.md AGENTS.md
530
- ```
520
+ 2. **No AGENTS.md copy needed.** KiloCode shares the OpenCode plugin architecture the plugin injects the FULL routing mandate ([`configs/opencode/AGENTS.md`](configs/opencode/AGENTS.md), verbatim) into the system prompt on every model dispatch. An existing copy is harmless (the host loads it natively as project rules) and identical copies are auto-skipped by the plugin's dedupe guard, so the mandate is never injected twice.
531
521
 
532
522
  3. Restart KiloCode.
533
523
 
@@ -535,7 +525,7 @@ Full configs: [`configs/opencode/opencode.json`](configs/opencode/opencode.json)
535
525
 
536
526
  **Upgrade note:** If an existing config has BOTH `plugin: ["context-mode"]` AND `mcp.context-mode`, KiloCode will register zero `ctx_*` tools — the plugin path correctly suppresses MCP duplicates, but the legacy MCP entry confuses the loader. Run `context-mode upgrade` to remove the legacy `mcp.context-mode` entry; your other MCP servers are preserved. v1.0.140+ emits a stderr diagnostic with the same guidance when this happens.
537
527
 
538
- **Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`. The optional [`AGENTS.md`](configs/opencode/AGENTS.md) file provides routing instructions for model awareness. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
528
+ **Routing:** Hooks enforce routing programmatically via `tool.execute.before` and `tool.execute.after`, and the plugin injects the full routing guidance (condensed block + complete AGENTS.md mandate) into the system prompt on every model dispatch — no copied AGENTS.md is required. The `experimental.session.compacting` hook builds resume snapshots when the conversation compacts. The `experimental.chat.system.transform` hook injects the routing block and prior-session snapshots at session start, enabling session continuity across restarts. The `chat.message` hook captures user prompts and decisions (UserPromptSubmit equivalent).
539
529
 
540
530
  > **Note:** KiloCode shares the same plugin architecture as OpenCode, using the OpenCodeAdapter with platform-specific configuration paths (`kilo.json` instead of `opencode.json`, `~/.config/kilo/` instead of `~/.config/opencode/`). Like OpenCode, it lacks a real SessionStart hook — the plugin uses `experimental.chat.system.transform` as a surrogate. User-prompt capture uses `chat.message` instead of the missing UserPromptSubmit hook. AGENTS.md/CLAUDE.md/CONTEXT.md rules are captured automatically on first hook fire per project.
541
531
 
@@ -182,6 +182,8 @@ interface ChatMessageHookOutput {
182
182
  message: unknown;
183
183
  parts: ChatMessagePart[];
184
184
  }
185
+ /** Test-only: point the template loader at a specific path (tests pass a nonexistent path to simulate a missing template). */
186
+ export declare function __setAgentsTemplatePathForTests(path: string | undefined): void;
185
187
  declare const ROUTING_MARKERS: string[];
186
188
  declare function systemHasRoutingInstructions(system: string[]): boolean;
187
189
  /** Test-only: clear process-global activation + log-dedupe state. */
@@ -75,6 +75,93 @@ function isSyntheticMessage(text) {
75
75
  const trimmed = text.trim();
76
76
  return SYNTHETIC_MESSAGE_PREFIXES.some((p) => trimmed.startsWith(p));
77
77
  }
78
+ // ── Full AGENTS.md mandate injection (user decision: opencode must not
79
+ // require the copied AGENTS.md — the injected guidance carries the FULL
80
+ // mandate, not just the condensed block) ─────────────────
81
+ /** Test-only override for the template path (null/undefined → default). */
82
+ let agentsTemplatePathOverride;
83
+ /** Test-only: point the template loader at a specific path (tests pass a nonexistent path to simulate a missing template). */
84
+ export function __setAgentsTemplatePathForTests(path) {
85
+ agentsTemplatePathOverride = path;
86
+ }
87
+ function agentsTemplatePathFor(buildDir) {
88
+ if (agentsTemplatePathOverride !== undefined)
89
+ return agentsTemplatePathOverride;
90
+ // Same buildDir-relative pattern as the routing .mjs islands: the plugin
91
+ // runs from build/adapters/opencode (or src/adapters/opencode under tsx),
92
+ // so ../../.. is the package root where configs/ lives.
93
+ return resolve(buildDir, "..", "..", "..", "configs", "opencode", "AGENTS.md");
94
+ }
95
+ /**
96
+ * Normalize a markdown document for content comparison: strip \r, trim each
97
+ * line, drop empty lines, re-join with "\n". Absorbs CRLF line endings,
98
+ * trailing whitespace, and blank-line differences — content-equal documents
99
+ * normalize identically; ANY edited line does not.
100
+ */
101
+ function normalizeMarkdownForCompare(content) {
102
+ return content
103
+ .replace(/\r/g, "")
104
+ .split("\n")
105
+ .map((line) => line.trim())
106
+ .filter((line) => line.length > 0)
107
+ .join("\n");
108
+ }
109
+ /**
110
+ * Compose the injected guidance: the condensed routing block FIRST
111
+ * (<context_window_protection> + quorum markers), then the FULL AGENTS.md
112
+ * mandate (Think in Code, BLOCKED/REDIRECTED, tool selection, memory
113
+ * tables, ctx commands) appended VERBATIM — the template uses the SAME
114
+ * tool naming (`context-mode_ctx_*`) as the runtime redirect guidance, so
115
+ * no rewrite is needed, and it does NOT contain the condensed block's XML
116
+ * tag, so there is no tag collision.
117
+ *
118
+ * Best-effort: a missing/unreadable template degrades to the condensed
119
+ * block alone (nothing logged — same degrade-honestly pattern; the
120
+ * condensed block alone must keep working).
121
+ *
122
+ * Dedupe guard (evaluated ONCE at setup) — NORMALIZED FULL-CONTENT
123
+ * EQUALITY: the template is skipped ONLY when the project-root AGENTS.md is
124
+ * content-equal to the template after normalization (CRLF/trailing-
125
+ * whitespace/blank-line differences absorbed). Rationale: the v1 host then
126
+ * loads the identical mandate natively, so appending it again would
127
+ * double-inject and waste context. ANY edited copy re-appends the full
128
+ * template DELIBERATELY — even when the copy still carries the old
129
+ * signature line — because an edited copy is no longer guaranteed to match
130
+ * what the plugin injects, and the injected guidance is the authoritative
131
+ * mandate. Unreadable project files fall through and append (harmless
132
+ * duplicate worst-case; never break setup over a diagnostics read).
133
+ */
134
+ function composeRoutingGuidance(condensedBlock, templatePath, projectDir) {
135
+ let agentsMdContent = "";
136
+ try {
137
+ if (existsSync(templatePath)) {
138
+ const content = readFileSync(templatePath, "utf-8");
139
+ if (content.trim())
140
+ agentsMdContent = content;
141
+ }
142
+ }
143
+ catch {
144
+ agentsMdContent = ""; // unreadable template — condensed block alone
145
+ }
146
+ if (!agentsMdContent)
147
+ return condensedBlock;
148
+ try {
149
+ const projectAgentsPath = join(projectDir, "AGENTS.md");
150
+ if (existsSync(projectAgentsPath)) {
151
+ const projectAgents = readFileSync(projectAgentsPath, "utf-8");
152
+ if (normalizeMarkdownForCompare(projectAgents) === normalizeMarkdownForCompare(agentsMdContent)) {
153
+ // Content-equal copy → the v1 host loads the identical mandate
154
+ // natively — skip the template. Edited copies re-append above.
155
+ return condensedBlock;
156
+ }
157
+ }
158
+ }
159
+ catch {
160
+ // Unreadable project file — fall through and append (harmless duplicate
161
+ // worst-case; never break setup over a diagnostics read).
162
+ }
163
+ return `${condensedBlock}\n\n${agentsMdContent}`;
164
+ }
78
165
  // ── Helpers ───────────────────────────────────────────────
79
166
  // Quorum markers — must NOT be substrings of each other (#487).
80
167
  // Each token uniquely identifies the routing block / context-mode rules
@@ -368,12 +455,16 @@ async function createPluginRuntime(ctx, projectDirOverride, adapterOptions) {
368
455
  // sessionID, so we cache it. createToolNamer accepts both "opencode" and
369
456
  // "kilo" per hooks/core/tool-naming.mjs:25-26.
370
457
  const toolNamer = toolNamingMod.createToolNamer(platform);
371
- const routingBlock = routingBlockMod.createRoutingBlock(toolNamer);
458
+ const condensedRoutingBlock = routingBlockMod.createRoutingBlock(toolNamer);
372
459
  // Initialize per-process state. We do NOT fabricate a sessionId here —
373
460
  // OpenCode/Kilo provide the real `input.sessionID` on every hook, and a
374
461
  // process-global UUID would (a) never match prior-session resume rows and
375
462
  // (b) collide across multi-session reuse (Mickey / PR #376 root cause).
376
463
  const projectDir = projectDirOverride ?? ctx?.directory ?? process.cwd();
464
+ // Compose the FULL injected guidance (condensed block first + the complete
465
+ // AGENTS.md mandate verbatim, subject to the project-root dedupe guard) —
466
+ // evaluated ONCE per process; needs the resolved project dir.
467
+ const routingBlock = composeRoutingGuidance(condensedRoutingBlock, agentsTemplatePathFor(buildDir), projectDir);
377
468
  // C2 narrowing: resolve DB path through the canonical helper directly.
378
469
  // BaseAdapter no longer exposes getSessionDBPath; the adapter only owns
379
470
  // the sessions DIR (per-platform), the helper owns the per-project FILE
@@ -185,6 +185,22 @@ export declare function renameCorruptDB(dbPath: string): void;
185
185
  * error) go to stderr via console.error under this prefix.
186
186
  */
187
187
  export declare const DB_LOG_PREFIX = "[context-mode:db]";
188
+ /**
189
+ * Extract the SQLite error code (e.g. "SQLITE_IOERR") from an arbitrary
190
+ * thrown value, or "" when absent. better-sqlite3 and node:sqlite set
191
+ * `code` on SqliteError; bun:sqlite encodes it in the message instead.
192
+ *
193
+ * Exported so store.ts can append the same `[<code>]` context to the
194
+ * tool-visible `[context-mode:store] …` error messages without
195
+ * duplicating the throw-shape handling.
196
+ */
197
+ export declare function extractErrorCode(err: unknown): string;
198
+ /**
199
+ * Normalize the human-readable message of an arbitrary thrown value.
200
+ * Exported alongside extractErrorCode for store.ts's tool-error
201
+ * enrichment (non-Error shapes included).
202
+ */
203
+ export declare function errorMessage(err: unknown): string;
188
204
  /**
189
205
  * Log a swallowed DB failure to stderr with the stable
190
206
  * `[context-mode:db]` prefix: operation name, error code, error message,
package/build/db-base.js CHANGED
@@ -490,6 +490,15 @@ export function withRetry(fn, delays = [100, 500, 2000]) {
490
490
  throw err;
491
491
  }
492
492
  lastError = err instanceof Error ? err : new Error(errorSignature(err));
493
+ if (!(err instanceof Error)) {
494
+ // Preserve the SQLite code (e.g. SQLITE_IOERR) on the normalized
495
+ // Error — non-Error throw shapes ({ code } objects, strings) would
496
+ // otherwise lose it, leaving callers unable to classify the failure
497
+ // after retries are exhausted.
498
+ const code = extractErrorCode(err);
499
+ if (code)
500
+ lastError.code = code;
501
+ }
493
502
  if (attempt < delays.length) {
494
503
  const delay = delays[attempt];
495
504
  const start = Date.now();
@@ -497,8 +506,16 @@ export function withRetry(fn, delays = [100, 500, 2000]) {
497
506
  }
498
507
  }
499
508
  }
500
- throw new Error(`SQLITE_BUSY/SQLITE_IOERR: transient SQLite error after ${delays.length} retries. ` +
509
+ const exhausted = new Error(`SQLITE_BUSY/SQLITE_IOERR: transient SQLite error after ${delays.length} retries. ` +
501
510
  `Original error: ${lastError?.message}`);
511
+ // Carry the last error's SQLite code on the wrapper (message alone is
512
+ // ambiguous — it names both SQLITE_BUSY and SQLITE_IOERR). store.ts's
513
+ // `[context-mode:store]` enrichment appends this as ` (<code>)` so the
514
+ // tool-visible error is never a bare "disk I/O error".
515
+ const exhaustedCode = lastError ? extractErrorCode(lastError) : "";
516
+ if (exhaustedCode)
517
+ exhausted.code = exhaustedCode;
518
+ throw exhausted;
502
519
  }
503
520
  // ─────────────────────────────────────────────────────────
504
521
  // Corrupt DB recovery (#244)
@@ -545,8 +562,12 @@ const _recentDbErrors = new Map();
545
562
  * Extract the SQLite error code (e.g. "SQLITE_IOERR") from an arbitrary
546
563
  * thrown value, or "" when absent. better-sqlite3 and node:sqlite set
547
564
  * `code` on SqliteError; bun:sqlite encodes it in the message instead.
565
+ *
566
+ * Exported so store.ts can append the same `[<code>]` context to the
567
+ * tool-visible `[context-mode:store] …` error messages without
568
+ * duplicating the throw-shape handling.
548
569
  */
549
- function extractErrorCode(err) {
570
+ export function extractErrorCode(err) {
550
571
  if (err instanceof Error) {
551
572
  const code = err.code;
552
573
  return typeof code === "string" ? code : "";
@@ -557,7 +578,12 @@ function extractErrorCode(err) {
557
578
  }
558
579
  return "";
559
580
  }
560
- function errorMessage(err) {
581
+ /**
582
+ * Normalize the human-readable message of an arbitrary thrown value.
583
+ * Exported alongside extractErrorCode for store.ts's tool-error
584
+ * enrichment (non-Error shapes included).
585
+ */
586
+ export function errorMessage(err) {
561
587
  if (err instanceof Error)
562
588
  return err.message;
563
589
  if (typeof err === "string")