context-mode 0.9.21 → 1.0.0
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/hooks/hooks.json +46 -4
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +4 -4
- package/README.md +377 -191
- package/build/adapters/claude-code/config.d.ts +8 -0
- package/build/adapters/claude-code/config.js +8 -0
- package/build/adapters/claude-code/hooks.d.ts +53 -0
- package/build/adapters/claude-code/hooks.js +88 -0
- package/build/adapters/claude-code/index.d.ts +50 -0
- package/build/adapters/claude-code/index.js +523 -0
- package/build/adapters/codex/config.d.ts +8 -0
- package/build/adapters/codex/config.js +8 -0
- package/build/adapters/codex/hooks.d.ts +21 -0
- package/build/adapters/codex/hooks.js +27 -0
- package/build/adapters/codex/index.d.ts +44 -0
- package/build/adapters/codex/index.js +223 -0
- package/build/adapters/detect.d.ts +26 -0
- package/build/adapters/detect.js +131 -0
- package/build/adapters/gemini-cli/config.d.ts +8 -0
- package/build/adapters/gemini-cli/config.js +8 -0
- package/build/adapters/gemini-cli/hooks.d.ts +44 -0
- package/build/adapters/gemini-cli/hooks.js +64 -0
- package/build/adapters/gemini-cli/index.d.ts +57 -0
- package/build/adapters/gemini-cli/index.js +468 -0
- package/build/adapters/opencode/config.d.ts +8 -0
- package/build/adapters/opencode/config.js +8 -0
- package/build/adapters/opencode/hooks.d.ts +38 -0
- package/build/adapters/opencode/hooks.js +50 -0
- package/build/adapters/opencode/index.d.ts +52 -0
- package/build/adapters/opencode/index.js +386 -0
- package/build/adapters/types.d.ts +218 -0
- package/build/adapters/types.js +13 -0
- package/build/adapters/vscode-copilot/config.d.ts +8 -0
- package/build/adapters/vscode-copilot/config.js +8 -0
- package/build/adapters/vscode-copilot/hooks.d.ts +49 -0
- package/build/adapters/vscode-copilot/hooks.js +76 -0
- package/build/adapters/vscode-copilot/index.d.ts +58 -0
- package/build/adapters/vscode-copilot/index.js +512 -0
- package/build/cli.d.ts +9 -6
- package/build/cli.js +133 -423
- package/build/db-base.d.ts +84 -0
- package/build/db-base.js +128 -0
- package/build/executor.d.ts +6 -7
- package/build/executor.js +111 -51
- package/build/opencode-plugin.d.ts +37 -0
- package/build/opencode-plugin.js +118 -0
- package/build/runtime.js +1 -1
- package/build/server.js +436 -117
- package/build/session/db.d.ts +110 -0
- package/build/session/db.js +285 -0
- package/build/session/extract.d.ts +51 -0
- package/build/session/extract.js +407 -0
- package/build/session/snapshot.d.ts +70 -0
- package/build/session/snapshot.js +309 -0
- package/build/store.d.ts +4 -22
- package/build/store.js +67 -55
- package/build/truncate.d.ts +59 -0
- package/build/truncate.js +157 -0
- package/build/types.d.ts +101 -0
- package/build/types.js +20 -0
- package/configs/claude-code/CLAUDE.md +62 -0
- package/configs/codex/AGENTS.md +58 -0
- package/configs/codex/config.toml +5 -0
- package/configs/gemini-cli/GEMINI.md +58 -0
- package/configs/gemini-cli/mcp.json +7 -0
- package/configs/gemini-cli/settings.json +49 -0
- package/configs/opencode/AGENTS.md +58 -0
- package/configs/opencode/opencode.json +10 -0
- package/configs/vscode-copilot/copilot-instructions.md +58 -0
- package/configs/vscode-copilot/hooks.json +16 -0
- package/configs/vscode-copilot/mcp.json +8 -0
- package/hooks/core/formatters.mjs +86 -0
- package/hooks/core/routing.mjs +262 -0
- package/hooks/core/stdin.mjs +19 -0
- package/hooks/formatters/claude-code.mjs +57 -0
- package/hooks/formatters/gemini-cli.mjs +55 -0
- package/hooks/formatters/vscode-copilot.mjs +55 -0
- package/hooks/gemini-cli/aftertool.mjs +58 -0
- package/hooks/gemini-cli/beforetool.mjs +25 -0
- package/hooks/gemini-cli/precompress.mjs +51 -0
- package/hooks/gemini-cli/sessionstart.mjs +117 -0
- package/hooks/hooks.json +46 -4
- package/hooks/posttooluse.mjs +53 -0
- package/hooks/precompact.mjs +55 -0
- package/hooks/pretooluse.mjs +23 -266
- package/hooks/routing-block.mjs +19 -6
- package/hooks/session-directive.mjs +353 -0
- package/hooks/session-helpers.mjs +112 -0
- package/hooks/sessionstart.mjs +123 -16
- package/hooks/userpromptsubmit.mjs +58 -0
- package/hooks/vscode-copilot/posttooluse.mjs +58 -0
- package/hooks/vscode-copilot/precompact.mjs +51 -0
- package/hooks/vscode-copilot/pretooluse.mjs +25 -0
- package/hooks/vscode-copilot/sessionstart.mjs +115 -0
- package/package.json +20 -17
- package/skills/context-mode/SKILL.md +49 -49
- package/skills/{doctor → ctx-doctor}/SKILL.md +3 -3
- package/skills/{stats → ctx-stats}/SKILL.md +3 -3
- package/skills/{upgrade → ctx-upgrade}/SKILL.md +3 -3
- package/start.mjs +47 -0
- package/hooks/pretooluse.sh +0 -147
- package/server.bundle.mjs +0 -341
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/claude-code — Claude Code platform adapter.
|
|
3
|
+
*
|
|
4
|
+
* Implements HookAdapter for Claude Code's JSON stdin/stdout hook paradigm.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code hook specifics:
|
|
7
|
+
* - I/O: JSON on stdin, JSON on stdout
|
|
8
|
+
* - Arg modification: `updatedInput` field in response
|
|
9
|
+
* - Blocking: `permissionDecision: "deny"` in response
|
|
10
|
+
* - PostToolUse output: `updatedMCPToolOutput` field
|
|
11
|
+
* - PreCompact: stdout on exit 0
|
|
12
|
+
* - Session ID: transcript_path UUID > session_id > CLAUDE_SESSION_ID > ppid
|
|
13
|
+
* - Config: ~/.claude/settings.json
|
|
14
|
+
* - Session dir: ~/.claude/context-mode/sessions/
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, readdirSync, chmodSync, constants, } from "node:fs";
|
|
18
|
+
import { resolve, join } from "node:path";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { HOOK_TYPES, HOOK_SCRIPTS, PRE_TOOL_USE_MATCHER_PATTERN, isContextModeHook, buildHookCommand, } from "./hooks.js";
|
|
21
|
+
// ─────────────────────────────────────────────────────────
|
|
22
|
+
// Adapter implementation
|
|
23
|
+
// ─────────────────────────────────────────────────────────
|
|
24
|
+
export class ClaudeCodeAdapter {
|
|
25
|
+
name = "Claude Code";
|
|
26
|
+
paradigm = "json-stdio";
|
|
27
|
+
capabilities = {
|
|
28
|
+
preToolUse: true,
|
|
29
|
+
postToolUse: true,
|
|
30
|
+
preCompact: true,
|
|
31
|
+
sessionStart: true,
|
|
32
|
+
canModifyArgs: true,
|
|
33
|
+
canModifyOutput: true,
|
|
34
|
+
canInjectSessionContext: true,
|
|
35
|
+
};
|
|
36
|
+
// ── Input parsing ──────────────────────────────────────
|
|
37
|
+
parsePreToolUseInput(raw) {
|
|
38
|
+
const input = raw;
|
|
39
|
+
return {
|
|
40
|
+
toolName: input.tool_name ?? "",
|
|
41
|
+
toolInput: input.tool_input ?? {},
|
|
42
|
+
sessionId: this.extractSessionId(input),
|
|
43
|
+
projectDir: process.env.CLAUDE_PROJECT_DIR,
|
|
44
|
+
raw,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
parsePostToolUseInput(raw) {
|
|
48
|
+
const input = raw;
|
|
49
|
+
return {
|
|
50
|
+
toolName: input.tool_name ?? "",
|
|
51
|
+
toolInput: input.tool_input ?? {},
|
|
52
|
+
toolOutput: input.tool_output,
|
|
53
|
+
isError: input.is_error,
|
|
54
|
+
sessionId: this.extractSessionId(input),
|
|
55
|
+
projectDir: process.env.CLAUDE_PROJECT_DIR,
|
|
56
|
+
raw,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
parsePreCompactInput(raw) {
|
|
60
|
+
const input = raw;
|
|
61
|
+
return {
|
|
62
|
+
sessionId: this.extractSessionId(input),
|
|
63
|
+
projectDir: process.env.CLAUDE_PROJECT_DIR,
|
|
64
|
+
raw,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
parseSessionStartInput(raw) {
|
|
68
|
+
const input = raw;
|
|
69
|
+
const rawSource = input.source ?? "startup";
|
|
70
|
+
let source;
|
|
71
|
+
switch (rawSource) {
|
|
72
|
+
case "compact":
|
|
73
|
+
source = "compact";
|
|
74
|
+
break;
|
|
75
|
+
case "resume":
|
|
76
|
+
source = "resume";
|
|
77
|
+
break;
|
|
78
|
+
case "clear":
|
|
79
|
+
source = "clear";
|
|
80
|
+
break;
|
|
81
|
+
default:
|
|
82
|
+
source = "startup";
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
sessionId: this.extractSessionId(input),
|
|
86
|
+
source,
|
|
87
|
+
projectDir: process.env.CLAUDE_PROJECT_DIR,
|
|
88
|
+
raw,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// ── Response formatting ────────────────────────────────
|
|
92
|
+
formatPreToolUseResponse(response) {
|
|
93
|
+
if (response.decision === "deny") {
|
|
94
|
+
return {
|
|
95
|
+
permissionDecision: "deny",
|
|
96
|
+
reason: response.reason ?? "Blocked by context-mode hook",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (response.decision === "modify" && response.updatedInput) {
|
|
100
|
+
return { updatedInput: response.updatedInput };
|
|
101
|
+
}
|
|
102
|
+
if (response.decision === "context" && response.additionalContext) {
|
|
103
|
+
// Claude Code: inject additionalContext into model context
|
|
104
|
+
return { additionalContext: response.additionalContext };
|
|
105
|
+
}
|
|
106
|
+
if (response.decision === "ask") {
|
|
107
|
+
// Claude Code: native "ask" — prompt user for permission
|
|
108
|
+
return { permissionDecision: "ask" };
|
|
109
|
+
}
|
|
110
|
+
// "allow" — return null/undefined for passthrough
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
formatPostToolUseResponse(response) {
|
|
114
|
+
const result = {};
|
|
115
|
+
if (response.additionalContext) {
|
|
116
|
+
result.additionalContext = response.additionalContext;
|
|
117
|
+
}
|
|
118
|
+
if (response.updatedOutput) {
|
|
119
|
+
result.updatedMCPToolOutput = response.updatedOutput;
|
|
120
|
+
}
|
|
121
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
122
|
+
}
|
|
123
|
+
formatPreCompactResponse(response) {
|
|
124
|
+
// Claude Code: stdout content on exit 0 is injected as context
|
|
125
|
+
return response.context ?? "";
|
|
126
|
+
}
|
|
127
|
+
formatSessionStartResponse(response) {
|
|
128
|
+
// Claude Code: stdout content is injected as additional context
|
|
129
|
+
return response.context ?? "";
|
|
130
|
+
}
|
|
131
|
+
// ── Configuration ──────────────────────────────────────
|
|
132
|
+
getSettingsPath() {
|
|
133
|
+
return resolve(homedir(), ".claude", "settings.json");
|
|
134
|
+
}
|
|
135
|
+
getSessionDir() {
|
|
136
|
+
const dir = join(homedir(), ".claude", "context-mode", "sessions");
|
|
137
|
+
mkdirSync(dir, { recursive: true });
|
|
138
|
+
return dir;
|
|
139
|
+
}
|
|
140
|
+
getSessionDBPath(projectDir) {
|
|
141
|
+
const hash = createHash("sha256")
|
|
142
|
+
.update(projectDir)
|
|
143
|
+
.digest("hex")
|
|
144
|
+
.slice(0, 16);
|
|
145
|
+
return join(this.getSessionDir(), `${hash}.db`);
|
|
146
|
+
}
|
|
147
|
+
getSessionEventsPath(projectDir) {
|
|
148
|
+
const hash = createHash("sha256")
|
|
149
|
+
.update(projectDir)
|
|
150
|
+
.digest("hex")
|
|
151
|
+
.slice(0, 16);
|
|
152
|
+
return join(this.getSessionDir(), `${hash}-events.md`);
|
|
153
|
+
}
|
|
154
|
+
generateHookConfig(pluginRoot) {
|
|
155
|
+
const preToolUseCommand = `node ${pluginRoot}/hooks/pretooluse.mjs`;
|
|
156
|
+
const preToolUseMatchers = [
|
|
157
|
+
"Bash",
|
|
158
|
+
"WebFetch",
|
|
159
|
+
"Read",
|
|
160
|
+
"Grep",
|
|
161
|
+
"Task",
|
|
162
|
+
"mcp__plugin_context-mode_context-mode__ctx_execute",
|
|
163
|
+
"mcp__plugin_context-mode_context-mode__ctx_execute_file",
|
|
164
|
+
"mcp__plugin_context-mode_context-mode__ctx_batch_execute",
|
|
165
|
+
];
|
|
166
|
+
return {
|
|
167
|
+
PreToolUse: preToolUseMatchers.map((matcher) => ({
|
|
168
|
+
matcher,
|
|
169
|
+
hooks: [{ type: "command", command: preToolUseCommand }],
|
|
170
|
+
})),
|
|
171
|
+
PostToolUse: [
|
|
172
|
+
{
|
|
173
|
+
matcher: "",
|
|
174
|
+
hooks: [
|
|
175
|
+
{
|
|
176
|
+
type: "command",
|
|
177
|
+
command: `node ${pluginRoot}/hooks/posttooluse.mjs`,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
PreCompact: [
|
|
183
|
+
{
|
|
184
|
+
matcher: "",
|
|
185
|
+
hooks: [
|
|
186
|
+
{
|
|
187
|
+
type: "command",
|
|
188
|
+
command: `node ${pluginRoot}/hooks/precompact.mjs`,
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
UserPromptSubmit: [
|
|
194
|
+
{
|
|
195
|
+
matcher: "",
|
|
196
|
+
hooks: [
|
|
197
|
+
{
|
|
198
|
+
type: "command",
|
|
199
|
+
command: `node ${pluginRoot}/hooks/userpromptsubmit.mjs`,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
SessionStart: [
|
|
205
|
+
{
|
|
206
|
+
matcher: "",
|
|
207
|
+
hooks: [
|
|
208
|
+
{
|
|
209
|
+
type: "command",
|
|
210
|
+
command: `node ${pluginRoot}/hooks/sessionstart.mjs`,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
readSettings() {
|
|
218
|
+
try {
|
|
219
|
+
const raw = readFileSync(this.getSettingsPath(), "utf-8");
|
|
220
|
+
return JSON.parse(raw);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
writeSettings(settings) {
|
|
227
|
+
writeFileSync(this.getSettingsPath(), JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
228
|
+
}
|
|
229
|
+
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
230
|
+
validateHooks(pluginRoot) {
|
|
231
|
+
const results = [];
|
|
232
|
+
const settings = this.readSettings();
|
|
233
|
+
if (!settings) {
|
|
234
|
+
results.push({
|
|
235
|
+
check: "PreToolUse hook",
|
|
236
|
+
status: "fail",
|
|
237
|
+
message: "Could not read ~/.claude/settings.json",
|
|
238
|
+
fix: "context-mode upgrade",
|
|
239
|
+
});
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
const hooks = settings.hooks;
|
|
243
|
+
// Check PreToolUse
|
|
244
|
+
const preToolUse = hooks?.PreToolUse;
|
|
245
|
+
if (preToolUse && preToolUse.length > 0) {
|
|
246
|
+
const hasHook = preToolUse.some((entry) => isContextModeHook(entry, HOOK_TYPES.PRE_TOOL_USE));
|
|
247
|
+
results.push({
|
|
248
|
+
check: "PreToolUse hook",
|
|
249
|
+
status: hasHook ? "pass" : "fail",
|
|
250
|
+
message: hasHook
|
|
251
|
+
? "PreToolUse hook configured"
|
|
252
|
+
: "PreToolUse exists but does not point to pretooluse.mjs",
|
|
253
|
+
fix: hasHook ? undefined : "context-mode upgrade",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
results.push({
|
|
258
|
+
check: "PreToolUse hook",
|
|
259
|
+
status: "fail",
|
|
260
|
+
message: "No PreToolUse hooks found",
|
|
261
|
+
fix: "context-mode upgrade",
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// Check SessionStart
|
|
265
|
+
const sessionStart = hooks?.SessionStart;
|
|
266
|
+
if (sessionStart && sessionStart.length > 0) {
|
|
267
|
+
const hasHook = sessionStart.some((entry) => isContextModeHook(entry, HOOK_TYPES.SESSION_START));
|
|
268
|
+
results.push({
|
|
269
|
+
check: "SessionStart hook",
|
|
270
|
+
status: hasHook ? "pass" : "fail",
|
|
271
|
+
message: hasHook
|
|
272
|
+
? "SessionStart hook configured"
|
|
273
|
+
: "SessionStart exists but does not point to sessionstart.mjs",
|
|
274
|
+
fix: hasHook ? undefined : "context-mode upgrade",
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
results.push({
|
|
279
|
+
check: "SessionStart hook",
|
|
280
|
+
status: "fail",
|
|
281
|
+
message: "No SessionStart hooks found",
|
|
282
|
+
fix: "context-mode upgrade",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return results;
|
|
286
|
+
}
|
|
287
|
+
checkPluginRegistration() {
|
|
288
|
+
const settings = this.readSettings();
|
|
289
|
+
if (!settings) {
|
|
290
|
+
return {
|
|
291
|
+
check: "Plugin registration",
|
|
292
|
+
status: "warn",
|
|
293
|
+
message: "Could not read settings.json",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const enabledPlugins = settings.enabledPlugins;
|
|
297
|
+
if (!enabledPlugins) {
|
|
298
|
+
return {
|
|
299
|
+
check: "Plugin registration",
|
|
300
|
+
status: "warn",
|
|
301
|
+
message: "No enabledPlugins section found (might be using standalone MCP mode)",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const pluginKey = Object.keys(enabledPlugins).find((k) => k.startsWith("context-mode"));
|
|
305
|
+
if (pluginKey && enabledPlugins[pluginKey]) {
|
|
306
|
+
return {
|
|
307
|
+
check: "Plugin registration",
|
|
308
|
+
status: "pass",
|
|
309
|
+
message: `Plugin enabled: ${pluginKey}`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
check: "Plugin registration",
|
|
314
|
+
status: "warn",
|
|
315
|
+
message: "context-mode not in enabledPlugins (might be using standalone MCP mode)",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
getInstalledVersion() {
|
|
319
|
+
// Primary: read from installed_plugins.json
|
|
320
|
+
try {
|
|
321
|
+
const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
322
|
+
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
323
|
+
const plugins = ipRaw.plugins ?? {};
|
|
324
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
325
|
+
if (!key.toLowerCase().includes("context-mode"))
|
|
326
|
+
continue;
|
|
327
|
+
const arr = entries;
|
|
328
|
+
if (arr.length > 0 && typeof arr[0].version === "string") {
|
|
329
|
+
return arr[0].version;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
/* fallback below */
|
|
335
|
+
}
|
|
336
|
+
// Fallback: scan common plugin cache locations
|
|
337
|
+
const bases = [
|
|
338
|
+
resolve(homedir(), ".claude"),
|
|
339
|
+
resolve(homedir(), ".config", "claude"),
|
|
340
|
+
];
|
|
341
|
+
for (const base of bases) {
|
|
342
|
+
const cacheDir = resolve(base, "plugins", "cache", "context-mode", "context-mode");
|
|
343
|
+
try {
|
|
344
|
+
const entries = readdirSync(cacheDir);
|
|
345
|
+
const versions = entries
|
|
346
|
+
.filter((e) => /^\d+\.\d+\.\d+/.test(e))
|
|
347
|
+
.sort((a, b) => {
|
|
348
|
+
const pa = a.split(".").map(Number);
|
|
349
|
+
const pb = b.split(".").map(Number);
|
|
350
|
+
for (let i = 0; i < 3; i++) {
|
|
351
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
352
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
353
|
+
}
|
|
354
|
+
return 0;
|
|
355
|
+
});
|
|
356
|
+
if (versions.length > 0)
|
|
357
|
+
return versions[versions.length - 1];
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
/* continue */
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return "not installed";
|
|
364
|
+
}
|
|
365
|
+
// ── Upgrade ────────────────────────────────────────────
|
|
366
|
+
configureAllHooks(pluginRoot) {
|
|
367
|
+
const settings = this.readSettings() ?? {};
|
|
368
|
+
const hooks = (settings.hooks ?? {});
|
|
369
|
+
const changes = [];
|
|
370
|
+
const hookTypes = [
|
|
371
|
+
HOOK_TYPES.PRE_TOOL_USE,
|
|
372
|
+
HOOK_TYPES.SESSION_START,
|
|
373
|
+
];
|
|
374
|
+
for (const hookType of hookTypes) {
|
|
375
|
+
const command = buildHookCommand(hookType);
|
|
376
|
+
if (hookType === HOOK_TYPES.PRE_TOOL_USE) {
|
|
377
|
+
const entry = {
|
|
378
|
+
matcher: PRE_TOOL_USE_MATCHER_PATTERN,
|
|
379
|
+
hooks: [{ type: "command", command }],
|
|
380
|
+
};
|
|
381
|
+
const existing = hooks.PreToolUse;
|
|
382
|
+
if (existing && Array.isArray(existing)) {
|
|
383
|
+
const idx = existing.findIndex((e) => isContextModeHook(e, hookType));
|
|
384
|
+
if (idx >= 0) {
|
|
385
|
+
existing[idx] = entry;
|
|
386
|
+
changes.push(`Updated existing ${hookType} hook entry`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
existing.push(entry);
|
|
390
|
+
changes.push(`Added ${hookType} hook entry`);
|
|
391
|
+
}
|
|
392
|
+
hooks.PreToolUse = existing;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
hooks.PreToolUse = [entry];
|
|
396
|
+
changes.push(`Created ${hookType} hooks section`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const entry = {
|
|
401
|
+
matcher: "",
|
|
402
|
+
hooks: [{ type: "command", command }],
|
|
403
|
+
};
|
|
404
|
+
const existing = hooks[hookType];
|
|
405
|
+
if (existing && Array.isArray(existing)) {
|
|
406
|
+
const idx = existing.findIndex((e) => isContextModeHook(e, hookType));
|
|
407
|
+
if (idx >= 0) {
|
|
408
|
+
existing[idx] = entry;
|
|
409
|
+
changes.push(`Updated existing ${hookType} hook entry`);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
existing.push(entry);
|
|
413
|
+
changes.push(`Added ${hookType} hook entry`);
|
|
414
|
+
}
|
|
415
|
+
hooks[hookType] = existing;
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
hooks[hookType] = [entry];
|
|
419
|
+
changes.push(`Created ${hookType} hooks section`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
settings.hooks = hooks;
|
|
424
|
+
this.writeSettings(settings);
|
|
425
|
+
return changes;
|
|
426
|
+
}
|
|
427
|
+
backupSettings() {
|
|
428
|
+
const settingsPath = this.getSettingsPath();
|
|
429
|
+
try {
|
|
430
|
+
accessSync(settingsPath, constants.R_OK);
|
|
431
|
+
const backupPath = settingsPath + ".bak";
|
|
432
|
+
copyFileSync(settingsPath, backupPath);
|
|
433
|
+
return backupPath;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
setHookPermissions(pluginRoot) {
|
|
440
|
+
const set = [];
|
|
441
|
+
for (const [, scriptName] of Object.entries(HOOK_SCRIPTS)) {
|
|
442
|
+
const scriptPath = resolve(pluginRoot, "hooks", scriptName);
|
|
443
|
+
try {
|
|
444
|
+
accessSync(scriptPath, constants.R_OK);
|
|
445
|
+
chmodSync(scriptPath, 0o755);
|
|
446
|
+
set.push(scriptPath);
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
/* skip missing scripts */
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return set;
|
|
453
|
+
}
|
|
454
|
+
updatePluginRegistry(pluginRoot, version) {
|
|
455
|
+
try {
|
|
456
|
+
const ipPath = resolve(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
457
|
+
const ipRaw = JSON.parse(readFileSync(ipPath, "utf-8"));
|
|
458
|
+
for (const [key, entries] of Object.entries(ipRaw.plugins || {})) {
|
|
459
|
+
if (!key.toLowerCase().includes("context-mode"))
|
|
460
|
+
continue;
|
|
461
|
+
for (const entry of entries) {
|
|
462
|
+
entry.installPath = pluginRoot;
|
|
463
|
+
entry.version = version;
|
|
464
|
+
entry.lastUpdated = new Date().toISOString();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
writeFileSync(ipPath, JSON.stringify(ipRaw, null, 2) + "\n", "utf-8");
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
/* best effort */
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
// ── Routing Instructions (soft enforcement) ────────────
|
|
474
|
+
getRoutingInstructionsConfig() {
|
|
475
|
+
return {
|
|
476
|
+
fileName: "CLAUDE.md",
|
|
477
|
+
globalPath: resolve(homedir(), ".claude", "CLAUDE.md"),
|
|
478
|
+
projectRelativePath: "CLAUDE.md",
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
writeRoutingInstructions(projectDir, pluginRoot) {
|
|
482
|
+
const config = this.getRoutingInstructionsConfig();
|
|
483
|
+
const targetPath = resolve(projectDir, config.projectRelativePath);
|
|
484
|
+
const sourcePath = resolve(pluginRoot, "configs", "claude-code", config.fileName);
|
|
485
|
+
try {
|
|
486
|
+
const content = readFileSync(sourcePath, "utf-8");
|
|
487
|
+
// Check if file exists and already has context-mode instructions
|
|
488
|
+
try {
|
|
489
|
+
const existing = readFileSync(targetPath, "utf-8");
|
|
490
|
+
if (existing.includes("context-mode"))
|
|
491
|
+
return null;
|
|
492
|
+
// Append to existing file
|
|
493
|
+
writeFileSync(targetPath, existing.trimEnd() + "\n\n" + content, "utf-8");
|
|
494
|
+
return targetPath;
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
// File doesn't exist — create it
|
|
498
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
499
|
+
return targetPath;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// ── Internal helpers ───────────────────────────────────
|
|
507
|
+
/**
|
|
508
|
+
* Extract session ID from Claude Code hook input.
|
|
509
|
+
* Priority: transcript_path UUID > session_id field > CLAUDE_SESSION_ID env > ppid fallback.
|
|
510
|
+
*/
|
|
511
|
+
extractSessionId(input) {
|
|
512
|
+
if (input.transcript_path) {
|
|
513
|
+
const match = input.transcript_path.match(/([a-f0-9-]{36})\.jsonl$/);
|
|
514
|
+
if (match)
|
|
515
|
+
return match[1];
|
|
516
|
+
}
|
|
517
|
+
if (input.session_id)
|
|
518
|
+
return input.session_id;
|
|
519
|
+
if (process.env.CLAUDE_SESSION_ID)
|
|
520
|
+
return process.env.CLAUDE_SESSION_ID;
|
|
521
|
+
return `pid-${process.ppid}`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex/config — Thin re-exports from CodexAdapter.
|
|
3
|
+
*
|
|
4
|
+
* This module exists for backward compatibility. All logic lives in the
|
|
5
|
+
* adapter class (index.ts). New code should use getAdapter() from detect.ts.
|
|
6
|
+
*/
|
|
7
|
+
export { CodexAdapter } from "./index.js";
|
|
8
|
+
export { HOOK_TYPES, ROUTING_INSTRUCTIONS_PATH } from "./hooks.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex/config — Thin re-exports from CodexAdapter.
|
|
3
|
+
*
|
|
4
|
+
* This module exists for backward compatibility. All logic lives in the
|
|
5
|
+
* adapter class (index.ts). New code should use getAdapter() from detect.ts.
|
|
6
|
+
*/
|
|
7
|
+
export { CodexAdapter } from "./index.js";
|
|
8
|
+
export { HOOK_TYPES, ROUTING_INSTRUCTIONS_PATH } from "./hooks.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex/hooks — Codex CLI hook definitions (stub).
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI does NOT support hooks (PRs #2904, #9796 were closed without merge).
|
|
5
|
+
* Only MCP integration is available. This module exports empty/stub constants
|
|
6
|
+
* for interface consistency with other adapters.
|
|
7
|
+
*
|
|
8
|
+
* Config: ~/.codex/config.toml (TOML format, not JSON)
|
|
9
|
+
* MCP: full support via [mcp_servers] in config.toml
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Codex CLI hook types — empty object.
|
|
13
|
+
* Codex CLI has no hook support; only MCP integration is available.
|
|
14
|
+
*/
|
|
15
|
+
export declare const HOOK_TYPES: {};
|
|
16
|
+
/**
|
|
17
|
+
* Path to the routing instructions file appended to the system prompt
|
|
18
|
+
* when Codex CLI initializes the MCP server. This is the only integration
|
|
19
|
+
* point since hooks are not supported.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ROUTING_INSTRUCTIONS_PATH = "configs/codex/AGENTS.md";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex/hooks — Codex CLI hook definitions (stub).
|
|
3
|
+
*
|
|
4
|
+
* Codex CLI does NOT support hooks (PRs #2904, #9796 were closed without merge).
|
|
5
|
+
* Only MCP integration is available. This module exports empty/stub constants
|
|
6
|
+
* for interface consistency with other adapters.
|
|
7
|
+
*
|
|
8
|
+
* Config: ~/.codex/config.toml (TOML format, not JSON)
|
|
9
|
+
* MCP: full support via [mcp_servers] in config.toml
|
|
10
|
+
*/
|
|
11
|
+
// ─────────────────────────────────────────────────────────
|
|
12
|
+
// Hook type constants (empty — no hook support)
|
|
13
|
+
// ─────────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Codex CLI hook types — empty object.
|
|
16
|
+
* Codex CLI has no hook support; only MCP integration is available.
|
|
17
|
+
*/
|
|
18
|
+
export const HOOK_TYPES = {};
|
|
19
|
+
// ─────────────────────────────────────────────────────────
|
|
20
|
+
// Routing instructions
|
|
21
|
+
// ─────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Path to the routing instructions file appended to the system prompt
|
|
24
|
+
* when Codex CLI initializes the MCP server. This is the only integration
|
|
25
|
+
* point since hooks are not supported.
|
|
26
|
+
*/
|
|
27
|
+
export const ROUTING_INSTRUCTIONS_PATH = "configs/codex/AGENTS.md";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/codex — Codex CLI platform adapter.
|
|
3
|
+
*
|
|
4
|
+
* Implements HookAdapter for Codex CLI's MCP-only paradigm.
|
|
5
|
+
*
|
|
6
|
+
* Codex CLI hook specifics:
|
|
7
|
+
* - NO hook support (PRs #2904, #9796 were closed without merge)
|
|
8
|
+
* - Only "hook": notify config for agent-turn-complete (very limited)
|
|
9
|
+
* - Config: ~/.codex/config.toml (TOML format, not JSON)
|
|
10
|
+
* - MCP: full support via [mcp_servers] in config.toml
|
|
11
|
+
* - All capabilities are false — MCP is the only integration path
|
|
12
|
+
* - Session dir: ~/.codex/context-mode/sessions/
|
|
13
|
+
*/
|
|
14
|
+
import type { HookAdapter, HookParadigm, PlatformCapabilities, DiagnosticResult, PreToolUseEvent, PostToolUseEvent, PreCompactEvent, SessionStartEvent, PreToolUseResponse, PostToolUseResponse, PreCompactResponse, SessionStartResponse, HookRegistration, RoutingInstructionsConfig } from "../types.js";
|
|
15
|
+
export declare class CodexAdapter implements HookAdapter {
|
|
16
|
+
readonly name = "Codex CLI";
|
|
17
|
+
readonly paradigm: HookParadigm;
|
|
18
|
+
readonly capabilities: PlatformCapabilities;
|
|
19
|
+
parsePreToolUseInput(_raw: unknown): PreToolUseEvent;
|
|
20
|
+
parsePostToolUseInput(_raw: unknown): PostToolUseEvent;
|
|
21
|
+
parsePreCompactInput(_raw: unknown): PreCompactEvent;
|
|
22
|
+
parseSessionStartInput(_raw: unknown): SessionStartEvent;
|
|
23
|
+
formatPreToolUseResponse(_response: PreToolUseResponse): unknown;
|
|
24
|
+
formatPostToolUseResponse(_response: PostToolUseResponse): unknown;
|
|
25
|
+
formatPreCompactResponse(_response: PreCompactResponse): unknown;
|
|
26
|
+
formatSessionStartResponse(_response: SessionStartResponse): unknown;
|
|
27
|
+
getSettingsPath(): string;
|
|
28
|
+
getSessionDir(): string;
|
|
29
|
+
getSessionDBPath(projectDir: string): string;
|
|
30
|
+
getSessionEventsPath(projectDir: string): string;
|
|
31
|
+
generateHookConfig(_pluginRoot: string): HookRegistration;
|
|
32
|
+
readSettings(): Record<string, unknown> | null;
|
|
33
|
+
writeSettings(_settings: Record<string, unknown>): void;
|
|
34
|
+
validateHooks(_pluginRoot: string): DiagnosticResult[];
|
|
35
|
+
checkPluginRegistration(): DiagnosticResult;
|
|
36
|
+
getInstalledVersion(): string;
|
|
37
|
+
configureAllHooks(_pluginRoot: string): string[];
|
|
38
|
+
backupSettings(): string | null;
|
|
39
|
+
setHookPermissions(_pluginRoot: string): string[];
|
|
40
|
+
updatePluginRegistry(_pluginRoot: string, _version: string): void;
|
|
41
|
+
getRoutingInstructionsConfig(): RoutingInstructionsConfig;
|
|
42
|
+
writeRoutingInstructions(projectDir: string, pluginRoot: string): string | null;
|
|
43
|
+
getRoutingInstructions(): string;
|
|
44
|
+
}
|