context-mode 1.0.158 → 1.0.159

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.158"
9
+ "version": "1.0.159"
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.158",
16
+ "version": "1.0.159",
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.158",
3
+ "version": "1.0.159",
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": "1.0.158",
3
+ "version": "1.0.159",
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.158",
6
+ "version": "1.0.159",
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.158",
3
+ "version": "1.0.159",
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",
@@ -42,7 +42,7 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
42
42
  generateHookConfig(_pluginRoot: string): HookRegistration;
43
43
  readSettings(): Record<string, unknown> | null;
44
44
  writeSettings(_settings: Record<string, unknown>): void;
45
- validateHooks(_pluginRoot: string): DiagnosticResult[];
45
+ validateHooks(pluginRoot: string): DiagnosticResult[];
46
46
  checkPluginRegistration(): DiagnosticResult;
47
47
  getInstalledVersion(): string;
48
48
  configureAllHooks(pluginRoot: string): string[];
@@ -63,6 +63,9 @@ export declare class CodexAdapter extends BaseAdapter implements HookAdapter {
63
63
  private readHooksConfig;
64
64
  private writeHooksConfig;
65
65
  private upsertManagedHookEntry;
66
+ private removeManagedHookEntries;
67
+ private hasCodexPluginHookManifest;
68
+ private pruneStaleUserHookTrustState;
66
69
  private isExpectedHookEntry;
67
70
  private isManagedContextModeEntry;
68
71
  private entryContainsManagedCommand;
@@ -14,7 +14,7 @@
14
14
  * Track: https://github.com/openai/codex/issues/18491
15
15
  */
16
16
  import { execFileSync } from "node:child_process";
17
- import { readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
17
+ import { existsSync, readFileSync, writeFileSync, accessSync, copyFileSync, constants, mkdirSync, } from "node:fs";
18
18
  import { resolve, dirname, join } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { BaseAdapter, resolveContextModeDataRoot } from "../base.js";
@@ -100,6 +100,13 @@ function hasDeprecatedCodexHooksFeature(raw) {
100
100
  const features = getTomlSection(raw, "features");
101
101
  return features !== null && /^\s*codex_hooks\s*=\s*true\s*(?:#.*)?$/mi.test(features);
102
102
  }
103
+ function hasCodexPluginEnabled(raw) {
104
+ const plugin = getTomlSection(raw, 'plugins."context-mode@context-mode"');
105
+ return plugin !== null && /^\s*enabled\s*=\s*true\s*(?:#.*)?$/mi.test(plugin);
106
+ }
107
+ function hasStandaloneContextModeMcp(raw) {
108
+ return getTomlSection(raw, "mcp_servers.context-mode") !== null;
109
+ }
103
110
  function ensureCodexHooksFeature(raw) {
104
111
  if (hasCodexHooksFeature(raw))
105
112
  return { text: raw, changed: false };
@@ -129,6 +136,56 @@ function ensureCodexHooksFeature(raw) {
129
136
  lines.splice(featuresIndex + 1, 0, "hooks = true");
130
137
  return { text: lines.join(newline), changed: true };
131
138
  }
139
+ function removeTomlSections(raw, shouldRemove) {
140
+ const newline = raw.includes("\r\n") ? "\r\n" : "\n";
141
+ const lines = raw.split(/\r?\n/);
142
+ const out = [];
143
+ const removed = [];
144
+ let skipping = false;
145
+ for (const line of lines) {
146
+ const section = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
147
+ if (section) {
148
+ const sectionName = section[1]?.trim() ?? "";
149
+ skipping = shouldRemove(sectionName);
150
+ if (skipping)
151
+ removed.push(sectionName);
152
+ }
153
+ if (!skipping)
154
+ out.push(line);
155
+ }
156
+ return { text: out.join(newline), removed };
157
+ }
158
+ function parseTomlQuotedString(raw) {
159
+ const trimmed = raw.trim();
160
+ if (!trimmed.startsWith('"') || !trimmed.endsWith('"'))
161
+ return null;
162
+ try {
163
+ const parsed = JSON.parse(trimmed);
164
+ return typeof parsed === "string" ? parsed : null;
165
+ }
166
+ catch {
167
+ // Codex hook-state keys are TOML quoted keys, not guaranteed JSON strings.
168
+ // Preserve Windows backslashes such as C:\Users\... even when they are not
169
+ // valid JSON escapes, while still handling the common escaped quote/slash.
170
+ let out = "";
171
+ let escaping = false;
172
+ for (const ch of trimmed.slice(1, -1)) {
173
+ if (escaping) {
174
+ out += ch === '"' || ch === "\\" ? ch : `\\${ch}`;
175
+ escaping = false;
176
+ }
177
+ else if (ch === "\\") {
178
+ escaping = true;
179
+ }
180
+ else {
181
+ out += ch;
182
+ }
183
+ }
184
+ if (escaping)
185
+ out += "\\";
186
+ return out;
187
+ }
188
+ }
132
189
  // ─────────────────────────────────────────────────────────
133
190
  // Adapter implementation
134
191
  // ─────────────────────────────────────────────────────────
@@ -384,9 +441,11 @@ export class CodexAdapter extends BaseAdapter {
384
441
  // manually or via the `codex` CLI tool.
385
442
  }
386
443
  // ── Diagnostics (doctor) ─────────────────────────────────
387
- validateHooks(_pluginRoot) {
444
+ validateHooks(pluginRoot) {
388
445
  const results = [];
389
446
  const codexCliVersion = probeCodexCliVersion();
447
+ let settingsRaw = "";
448
+ let settingsReadable = false;
390
449
  results.push({
391
450
  check: "Codex CLI binary",
392
451
  status: codexCliVersion ? "pass" : "warn",
@@ -396,9 +455,10 @@ export class CodexAdapter extends BaseAdapter {
396
455
  ...(codexCliVersion ? {} : { fix: "Install Codex CLI or make codex available on PATH" }),
397
456
  });
398
457
  try {
399
- const raw = readFileSync(this.getSettingsPath(), "utf-8");
400
- const enabled = hasCodexHooksFeature(raw);
401
- const deprecatedOnly = !enabled && hasDeprecatedCodexHooksFeature(raw);
458
+ settingsRaw = readFileSync(this.getSettingsPath(), "utf-8");
459
+ settingsReadable = true;
460
+ const enabled = hasCodexHooksFeature(settingsRaw);
461
+ const deprecatedOnly = !enabled && hasDeprecatedCodexHooksFeature(settingsRaw);
402
462
  results.push({
403
463
  check: "Codex hooks feature flag",
404
464
  status: enabled ? "pass" : "fail",
@@ -418,8 +478,35 @@ export class CodexAdapter extends BaseAdapter {
418
478
  fix: "context-mode upgrade",
419
479
  });
420
480
  }
481
+ const expected = this.generateHookConfig("");
482
+ const codexPluginEnabled = settingsReadable && hasCodexPluginEnabled(settingsRaw);
483
+ const codexPluginHooksAvailable = codexPluginEnabled && this.hasCodexPluginHookManifest(pluginRoot);
484
+ if (codexPluginEnabled && !codexPluginHooksAvailable) {
485
+ results.push({
486
+ check: "Codex plugin hooks",
487
+ status: "fail",
488
+ message: `context-mode Codex plugin is enabled, but ${join(pluginRoot, ".codex-plugin", "hooks.json")} is missing`,
489
+ fix: "Reinstall or upgrade the context-mode Codex plugin",
490
+ });
491
+ }
492
+ if (codexPluginEnabled && hasStandaloneContextModeMcp(settingsRaw)) {
493
+ results.push({
494
+ check: "Standalone MCP duplicate",
495
+ status: "warn",
496
+ message: "[mcp_servers.context-mode] is still registered while context-mode@context-mode is enabled; Codex may start both plugin and standalone MCP surfaces",
497
+ fix: "context-mode upgrade (removes the standalone Codex MCP registration when the plugin owns context-mode)",
498
+ });
499
+ }
421
500
  const hookConfig = this.readHooksConfig();
422
501
  if (!hookConfig.ok) {
502
+ if (hookConfig.reason === "missing" && codexPluginHooksAvailable) {
503
+ const pluginHookChecks = Object.keys(expected).map((hookName) => ({
504
+ check: `${hookName} hook`,
505
+ status: "pass",
506
+ message: `${hookName} hook provided by context-mode@context-mode plugin`,
507
+ }));
508
+ return results.concat(pluginHookChecks);
509
+ }
423
510
  if (hookConfig.reason === "missing") {
424
511
  return results.concat([{
425
512
  check: "Hooks config",
@@ -443,7 +530,7 @@ export class CodexAdapter extends BaseAdapter {
443
530
  fix: "Check permissions and file accessibility for hooks.json, then rerun context-mode upgrade if needed",
444
531
  }]);
445
532
  }
446
- if (!hookConfig.config.hooks) {
533
+ if (!hookConfig.config.hooks && !codexPluginHooksAvailable) {
447
534
  return results.concat([{
448
535
  check: "Hooks config",
449
536
  status: "fail",
@@ -451,24 +538,29 @@ export class CodexAdapter extends BaseAdapter {
451
538
  fix: `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
452
539
  }]);
453
540
  }
454
- const expected = this.generateHookConfig("");
455
- const hookChecks = Object.entries(expected).map(([hookName, entries]) => {
456
- const actualEntries = hookConfig.config.hooks?.[hookName];
457
- const expectedEntry = entries[0];
458
- const ok = Array.isArray(actualEntries)
459
- && actualEntries.some((entry) => this.isExpectedHookEntry(hookName, entry, expectedEntry));
460
- const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
461
- return {
541
+ const hookChecks = codexPluginHooksAvailable
542
+ ? Object.keys(expected).map((hookName) => ({
462
543
  check: `${hookName} hook`,
463
- status: (ok ? "pass" : missingStatus),
464
- message: ok
465
- ? `${hookName} hook configured in ${this.getHooksPath()}`
466
- : hookName === "PreCompact"
467
- ? `${hookName} hook missing or not pointing to context-mode; compaction snapshots require a Codex build that emits PreCompact`
468
- : `${hookName} hook missing or not pointing to context-mode`,
469
- fix: ok ? undefined : `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
470
- };
471
- });
544
+ status: "pass",
545
+ message: `${hookName} hook provided by context-mode@context-mode plugin`,
546
+ }))
547
+ : Object.entries(expected).map(([hookName, entries]) => {
548
+ const actualEntries = hookConfig.config.hooks?.[hookName];
549
+ const expectedEntry = entries[0];
550
+ const ok = Array.isArray(actualEntries)
551
+ && actualEntries.some((entry) => this.isExpectedHookEntry(hookName, entry, expectedEntry));
552
+ const missingStatus = hookName === "PreCompact" ? "warn" : "fail";
553
+ return {
554
+ check: `${hookName} hook`,
555
+ status: (ok ? "pass" : missingStatus),
556
+ message: ok
557
+ ? `${hookName} hook configured in ${this.getHooksPath()}`
558
+ : hookName === "PreCompact"
559
+ ? `${hookName} hook missing or not pointing to context-mode; compaction snapshots require a Codex build that emits PreCompact`
560
+ : `${hookName} hook missing or not pointing to context-mode`,
561
+ fix: ok ? undefined : `Update ${this.getHooksPath()} to match configs/codex/hooks.json`,
562
+ };
563
+ });
472
564
  // #603: surface duplicate context-mode entries per hook event. Codex fires
473
565
  // every matching entry, so duplicates double the work, can saturate the
474
566
  // MCP transport (`Transport closed`), and have been observed to inflate
@@ -488,16 +580,39 @@ export class CodexAdapter extends BaseAdapter {
488
580
  fix: "context-mode upgrade (collapses duplicate context-mode entries; preserves unrelated hooks)",
489
581
  });
490
582
  }
583
+ else if (codexPluginHooksAvailable && managedCount === 1) {
584
+ duplicateChecks.push({
585
+ check: `${hookName} plugin duplicate`,
586
+ status: "warn",
587
+ message: `${hookName} is configured in both ${this.getHooksPath()} and the context-mode Codex plugin; Codex will fire both hooks`,
588
+ fix: "context-mode upgrade (removes user config context-mode hooks; preserves unrelated hooks)",
589
+ });
590
+ }
491
591
  }
492
592
  return results.concat(hookChecks, duplicateChecks);
493
593
  }
494
594
  checkPluginRegistration() {
495
- // Check for context-mode in [mcp_servers] section of config.toml
496
595
  try {
497
596
  const raw = readFileSync(this.getSettingsPath(), "utf-8");
498
- const hasContextMode = raw.includes("context-mode");
597
+ const pluginEnabled = hasCodexPluginEnabled(raw);
598
+ const standaloneMcp = hasStandaloneContextModeMcp(raw);
499
599
  const hasMcpSection = raw.includes("[mcp_servers]") || raw.includes("[mcp_servers.");
500
- if (hasContextMode && hasMcpSection) {
600
+ if (pluginEnabled && standaloneMcp) {
601
+ return {
602
+ check: "MCP registration",
603
+ status: "warn",
604
+ message: "context-mode@context-mode plugin is enabled, but standalone [mcp_servers.context-mode] is also configured",
605
+ fix: "context-mode upgrade",
606
+ };
607
+ }
608
+ if (pluginEnabled) {
609
+ return {
610
+ check: "MCP registration",
611
+ status: "pass",
612
+ message: "context-mode@context-mode plugin enabled",
613
+ };
614
+ }
615
+ if (standaloneMcp) {
501
616
  return {
502
617
  check: "MCP registration",
503
618
  status: "pass",
@@ -536,6 +651,15 @@ export class CodexAdapter extends BaseAdapter {
536
651
  configureAllHooks(pluginRoot) {
537
652
  const hookConfig = this.readHooksConfig();
538
653
  const changes = [];
654
+ const settingsPath = this.getSettingsPath();
655
+ let settingsRaw = "";
656
+ try {
657
+ settingsRaw = readFileSync(settingsPath, "utf-8");
658
+ }
659
+ catch {
660
+ settingsRaw = "";
661
+ }
662
+ const codexPluginOwnsHooks = hasCodexPluginEnabled(settingsRaw) && this.hasCodexPluginHookManifest(pluginRoot);
539
663
  let hookFile;
540
664
  if (hookConfig.ok) {
541
665
  hookFile = hookConfig.config;
@@ -555,31 +679,48 @@ export class CodexAdapter extends BaseAdapter {
555
679
  ? hookFile.hooks
556
680
  : {};
557
681
  const desiredHooks = this.generateHookConfig(pluginRoot);
558
- for (const [hookName, entries] of Object.entries(desiredHooks)) {
559
- this.upsertManagedHookEntry(hooks, hookName, entries[0], changes);
682
+ const hookChangeStart = changes.length;
683
+ if (codexPluginOwnsHooks) {
684
+ for (const hookName of Object.keys(desiredHooks)) {
685
+ this.removeManagedHookEntries(hooks, hookName, changes);
686
+ }
560
687
  }
561
- if (changes.length > 0) {
688
+ else {
689
+ for (const [hookName, entries] of Object.entries(desiredHooks)) {
690
+ this.upsertManagedHookEntry(hooks, hookName, entries[0], changes);
691
+ }
692
+ }
693
+ if (changes.length > hookChangeStart) {
562
694
  hookFile.hooks = hooks;
563
695
  this.writeHooksConfig(hookFile);
564
- changes.push(`Wrote native Codex hooks to ${this.getHooksPath()}`);
565
- }
566
- const settingsPath = this.getSettingsPath();
567
- let settingsRaw = "";
568
- try {
569
- settingsRaw = readFileSync(settingsPath, "utf-8");
570
- }
571
- catch {
572
- settingsRaw = "";
696
+ changes.push(codexPluginOwnsHooks
697
+ ? `Removed duplicate context-mode user hooks from ${this.getHooksPath()}`
698
+ : `Wrote native Codex hooks to ${this.getHooksPath()}`);
699
+ }
700
+ let settingsText = ensureCodexHooksFeature(settingsRaw).text;
701
+ const enabledSettingsChanged = settingsText !== settingsRaw;
702
+ if (codexPluginOwnsHooks) {
703
+ const removedMcp = removeTomlSections(settingsText, (sectionName) => sectionName === "mcp_servers.context-mode"
704
+ || sectionName.startsWith("mcp_servers.context-mode.tools."));
705
+ if (removedMcp.removed.length > 0) {
706
+ settingsText = removedMcp.text;
707
+ changes.push("Removed standalone Codex context-mode MCP registration");
708
+ }
709
+ const prunedTrust = this.pruneStaleUserHookTrustState(settingsText, hooks);
710
+ if (prunedTrust.removed.length > 0) {
711
+ settingsText = prunedTrust.text;
712
+ changes.push(`Removed ${prunedTrust.removed.length} stale Codex hook trust entr${prunedTrust.removed.length === 1 ? "y" : "ies"}`);
713
+ }
573
714
  }
574
- const enabledSettings = ensureCodexHooksFeature(settingsRaw);
575
- if (enabledSettings.changed) {
576
- const newline = enabledSettings.text.includes("\r\n") ? "\r\n" : "\n";
577
- const text = enabledSettings.text.endsWith("\n")
578
- ? enabledSettings.text
579
- : `${enabledSettings.text}${newline}`;
715
+ if (settingsText !== settingsRaw) {
716
+ const newline = settingsText.includes("\r\n") ? "\r\n" : "\n";
717
+ const text = settingsText.endsWith("\n")
718
+ ? settingsText
719
+ : `${settingsText}${newline}`;
580
720
  mkdirSync(dirname(settingsPath), { recursive: true });
581
721
  writeFileSync(settingsPath, text, "utf-8");
582
- changes.push("Enabled Codex hooks feature flag");
722
+ if (enabledSettingsChanged)
723
+ changes.push("Enabled Codex hooks feature flag");
583
724
  }
584
725
  return changes;
585
726
  }
@@ -681,6 +822,57 @@ export class CodexAdapter extends BaseAdapter {
681
822
  }
682
823
  hooks[hookName] = currentEntries;
683
824
  }
825
+ removeManagedHookEntries(hooks, hookName, changes) {
826
+ const currentEntries = Array.isArray(hooks[hookName]) ? [...hooks[hookName]] : [];
827
+ const filtered = currentEntries.filter((entry) => !this.isManagedContextModeEntry(hookName, entry));
828
+ const removed = currentEntries.length - filtered.length;
829
+ if (removed === 0)
830
+ return;
831
+ if (filtered.length > 0) {
832
+ hooks[hookName] = filtered;
833
+ }
834
+ else {
835
+ delete hooks[hookName];
836
+ }
837
+ changes.push(`Removed ${removed} ${hookName} context-mode user hook${removed === 1 ? "" : "s"}`);
838
+ }
839
+ hasCodexPluginHookManifest(pluginRoot) {
840
+ return existsSync(join(pluginRoot, ".codex-plugin", "hooks.json"));
841
+ }
842
+ pruneStaleUserHookTrustState(settingsRaw, hooks) {
843
+ const hooksPath = this.normalizeCommand(this.getHooksPath());
844
+ const eventNames = {
845
+ post_compact: "PostCompact",
846
+ post_tool_use: "PostToolUse",
847
+ pre_compact: "PreCompact",
848
+ pre_tool_use: "PreToolUse",
849
+ session_start: "SessionStart",
850
+ stop: "Stop",
851
+ user_prompt_submit: "UserPromptSubmit",
852
+ };
853
+ return removeTomlSections(settingsRaw, (sectionName) => {
854
+ const prefix = "hooks.state.";
855
+ if (!sectionName.startsWith(prefix))
856
+ return false;
857
+ const key = parseTomlQuotedString(sectionName.slice(prefix.length));
858
+ if (key === null)
859
+ return false;
860
+ const normalized = this.normalizeCommand(key);
861
+ const parts = normalized.split(":");
862
+ const hookIndex = Number(parts.pop());
863
+ const entryIndex = Number(parts.pop());
864
+ const eventName = eventNames[parts.pop() ?? ""];
865
+ const stateHooksPath = parts.join(":");
866
+ if (stateHooksPath !== hooksPath
867
+ || !eventName
868
+ || !Number.isInteger(entryIndex)
869
+ || !Number.isInteger(hookIndex)) {
870
+ return false;
871
+ }
872
+ const entry = hooks[eventName]?.[entryIndex];
873
+ return !entry || !Array.isArray(entry.hooks) || !entry.hooks[hookIndex];
874
+ });
875
+ }
684
876
  isExpectedHookEntry(hookName, entry, expectedEntry) {
685
877
  if (!entry || typeof entry !== "object")
686
878
  return false;