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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/adapters/codex/index.d.ts +4 -1
- package/build/adapters/codex/index.js +237 -45
- package/cli.bundle.mjs +181 -178
- package/hooks/session-loaders.mjs +218 -3
- package/hooks/sessionstart.mjs +52 -2
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server.bundle.mjs +132 -129
|
@@ -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.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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
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
|
|
455
|
-
|
|
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:
|
|
464
|
-
message:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
597
|
+
const pluginEnabled = hasCodexPluginEnabled(raw);
|
|
598
|
+
const standaloneMcp = hasStandaloneContextModeMcp(raw);
|
|
499
599
|
const hasMcpSection = raw.includes("[mcp_servers]") || raw.includes("[mcp_servers.");
|
|
500
|
-
if (
|
|
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
|
-
|
|
559
|
-
|
|
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
|
-
|
|
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(
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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;
|