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,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/opencode — OpenCode platform adapter.
|
|
3
|
+
*
|
|
4
|
+
* Implements HookAdapter for OpenCode's TypeScript plugin paradigm.
|
|
5
|
+
*
|
|
6
|
+
* OpenCode hook specifics:
|
|
7
|
+
* - I/O: TS plugin functions (not JSON stdin/stdout)
|
|
8
|
+
* - Hook names: tool.execute.before, tool.execute.after, experimental.session.compacting
|
|
9
|
+
* - Arg modification: output.args mutation
|
|
10
|
+
* - Blocking: throw Error in tool.execute.before
|
|
11
|
+
* - Output modification: output.output mutation (TUI bug for bash #13575)
|
|
12
|
+
* - SessionStart: broken (#14808, no hook #5409)
|
|
13
|
+
* - Session ID: input.sessionID (camelCase!)
|
|
14
|
+
* - Project dir: ctx.directory in plugin init (no env var)
|
|
15
|
+
* - Config: opencode.json plugin array, .opencode/plugins/*.ts
|
|
16
|
+
* - Session dir: ~/.config/opencode/context-mode/sessions/
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { readFileSync, writeFileSync, mkdirSync, copyFileSync, accessSync, constants, } from "node:fs";
|
|
20
|
+
import { resolve, join } from "node:path";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
// ─────────────────────────────────────────────────────────
|
|
23
|
+
// Hook constants (re-exported from hooks.ts)
|
|
24
|
+
// ─────────────────────────────────────────────────────────
|
|
25
|
+
import { HOOK_TYPES as OPENCODE_HOOK_NAMES } from "./hooks.js";
|
|
26
|
+
// ─────────────────────────────────────────────────────────
|
|
27
|
+
// Adapter implementation
|
|
28
|
+
// ─────────────────────────────────────────────────────────
|
|
29
|
+
export class OpenCodeAdapter {
|
|
30
|
+
name = "OpenCode";
|
|
31
|
+
paradigm = "ts-plugin";
|
|
32
|
+
capabilities = {
|
|
33
|
+
preToolUse: true,
|
|
34
|
+
postToolUse: true,
|
|
35
|
+
preCompact: true, // experimental
|
|
36
|
+
sessionStart: true,
|
|
37
|
+
canModifyArgs: true,
|
|
38
|
+
canModifyOutput: true, // with TUI bug caveat for bash (#13575)
|
|
39
|
+
canInjectSessionContext: false,
|
|
40
|
+
};
|
|
41
|
+
// ── Input parsing ──────────────────────────────────────
|
|
42
|
+
parsePreToolUseInput(raw) {
|
|
43
|
+
const input = raw;
|
|
44
|
+
return {
|
|
45
|
+
toolName: input.tool_name ?? "",
|
|
46
|
+
toolInput: input.tool_input ?? {},
|
|
47
|
+
sessionId: this.extractSessionId(input),
|
|
48
|
+
projectDir: process.env.OPENCODE_PROJECT_DIR || process.cwd(),
|
|
49
|
+
raw,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
parsePostToolUseInput(raw) {
|
|
53
|
+
const input = raw;
|
|
54
|
+
return {
|
|
55
|
+
toolName: input.tool_name ?? "",
|
|
56
|
+
toolInput: input.tool_input ?? {},
|
|
57
|
+
toolOutput: input.tool_output,
|
|
58
|
+
isError: input.is_error,
|
|
59
|
+
sessionId: this.extractSessionId(input),
|
|
60
|
+
projectDir: process.env.OPENCODE_PROJECT_DIR || process.cwd(),
|
|
61
|
+
raw,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
parsePreCompactInput(raw) {
|
|
65
|
+
const input = raw;
|
|
66
|
+
return {
|
|
67
|
+
sessionId: this.extractSessionId(input),
|
|
68
|
+
projectDir: process.env.OPENCODE_PROJECT_DIR || process.cwd(),
|
|
69
|
+
raw,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
parseSessionStartInput(raw) {
|
|
73
|
+
const input = raw;
|
|
74
|
+
const rawSource = input.source ?? "startup";
|
|
75
|
+
let source;
|
|
76
|
+
switch (rawSource) {
|
|
77
|
+
case "compact":
|
|
78
|
+
source = "compact";
|
|
79
|
+
break;
|
|
80
|
+
case "resume":
|
|
81
|
+
source = "resume";
|
|
82
|
+
break;
|
|
83
|
+
case "clear":
|
|
84
|
+
source = "clear";
|
|
85
|
+
break;
|
|
86
|
+
default:
|
|
87
|
+
source = "startup";
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
sessionId: this.extractSessionId(input),
|
|
91
|
+
source,
|
|
92
|
+
projectDir: process.env.OPENCODE_PROJECT_DIR || process.cwd(),
|
|
93
|
+
raw,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// ── Response formatting ────────────────────────────────
|
|
97
|
+
formatPreToolUseResponse(response) {
|
|
98
|
+
if (response.decision === "deny") {
|
|
99
|
+
// OpenCode TS plugin paradigm: throw Error to block
|
|
100
|
+
throw new Error(response.reason ?? "Blocked by context-mode hook");
|
|
101
|
+
}
|
|
102
|
+
if (response.decision === "modify" && response.updatedInput) {
|
|
103
|
+
// OpenCode: output.args mutation
|
|
104
|
+
return { args: response.updatedInput };
|
|
105
|
+
}
|
|
106
|
+
if (response.decision === "ask") {
|
|
107
|
+
// OpenCode: no native "ask" mechanism — throw to be safe
|
|
108
|
+
throw new Error(response.reason ?? "Action requires user confirmation (security policy)");
|
|
109
|
+
}
|
|
110
|
+
// "context" — OpenCode's tool.execute.before cannot inject additionalContext
|
|
111
|
+
// in PreToolUse (platform limitation). The guidance is delivered via
|
|
112
|
+
// CLAUDE.md/AGENTS.md routing instructions instead. Passthrough.
|
|
113
|
+
// "allow" — passthrough
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
formatPostToolUseResponse(response) {
|
|
117
|
+
const result = {};
|
|
118
|
+
if (response.updatedOutput) {
|
|
119
|
+
// OpenCode: output.output mutation (TUI bug for bash #13575)
|
|
120
|
+
result.output = response.updatedOutput;
|
|
121
|
+
}
|
|
122
|
+
if (response.additionalContext) {
|
|
123
|
+
result.additionalContext = response.additionalContext;
|
|
124
|
+
}
|
|
125
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
126
|
+
}
|
|
127
|
+
formatPreCompactResponse(response) {
|
|
128
|
+
// experimental.session.compacting — return context string
|
|
129
|
+
return response.context ?? "";
|
|
130
|
+
}
|
|
131
|
+
formatSessionStartResponse(response) {
|
|
132
|
+
return response.context ?? "";
|
|
133
|
+
}
|
|
134
|
+
// ── Configuration ──────────────────────────────────────
|
|
135
|
+
getSettingsPath() {
|
|
136
|
+
// OpenCode uses opencode.json in the project root or .opencode/opencode.json
|
|
137
|
+
return resolve("opencode.json");
|
|
138
|
+
}
|
|
139
|
+
getSessionDir() {
|
|
140
|
+
const dir = join(homedir(), ".config", "opencode", "context-mode", "sessions");
|
|
141
|
+
mkdirSync(dir, { recursive: true });
|
|
142
|
+
return dir;
|
|
143
|
+
}
|
|
144
|
+
getSessionDBPath(projectDir) {
|
|
145
|
+
const hash = createHash("sha256")
|
|
146
|
+
.update(projectDir)
|
|
147
|
+
.digest("hex")
|
|
148
|
+
.slice(0, 16);
|
|
149
|
+
return join(this.getSessionDir(), `${hash}.db`);
|
|
150
|
+
}
|
|
151
|
+
getSessionEventsPath(projectDir) {
|
|
152
|
+
const hash = createHash("sha256")
|
|
153
|
+
.update(projectDir)
|
|
154
|
+
.digest("hex")
|
|
155
|
+
.slice(0, 16);
|
|
156
|
+
return join(this.getSessionDir(), `${hash}-events.md`);
|
|
157
|
+
}
|
|
158
|
+
generateHookConfig(_pluginRoot) {
|
|
159
|
+
// OpenCode uses TS plugin paradigm — hooks are registered via plugin array
|
|
160
|
+
// in opencode.json, not via command-based hook entries.
|
|
161
|
+
// Return the hook name mapping for documentation purposes.
|
|
162
|
+
return {
|
|
163
|
+
[OPENCODE_HOOK_NAMES.BEFORE]: [
|
|
164
|
+
{
|
|
165
|
+
matcher: "",
|
|
166
|
+
hooks: [
|
|
167
|
+
{
|
|
168
|
+
type: "plugin",
|
|
169
|
+
command: "context-mode",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
[OPENCODE_HOOK_NAMES.AFTER]: [
|
|
175
|
+
{
|
|
176
|
+
matcher: "",
|
|
177
|
+
hooks: [
|
|
178
|
+
{
|
|
179
|
+
type: "plugin",
|
|
180
|
+
command: "context-mode",
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
[OPENCODE_HOOK_NAMES.COMPACTING]: [
|
|
186
|
+
{
|
|
187
|
+
matcher: "",
|
|
188
|
+
hooks: [
|
|
189
|
+
{
|
|
190
|
+
type: "plugin",
|
|
191
|
+
command: "context-mode",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
readSettings() {
|
|
199
|
+
// Try opencode.json, then .opencode/opencode.json
|
|
200
|
+
const paths = [
|
|
201
|
+
resolve("opencode.json"),
|
|
202
|
+
resolve(".opencode", "opencode.json"),
|
|
203
|
+
];
|
|
204
|
+
for (const configPath of paths) {
|
|
205
|
+
try {
|
|
206
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
207
|
+
return JSON.parse(raw);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
writeSettings(settings) {
|
|
216
|
+
// Write to opencode.json in current directory
|
|
217
|
+
const configPath = resolve("opencode.json");
|
|
218
|
+
writeFileSync(configPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
219
|
+
}
|
|
220
|
+
// ── Diagnostics (doctor) ─────────────────────────────────
|
|
221
|
+
validateHooks(_pluginRoot) {
|
|
222
|
+
const results = [];
|
|
223
|
+
const settings = this.readSettings();
|
|
224
|
+
if (!settings) {
|
|
225
|
+
results.push({
|
|
226
|
+
check: "Plugin configuration",
|
|
227
|
+
status: "fail",
|
|
228
|
+
message: "Could not read opencode.json",
|
|
229
|
+
fix: "context-mode upgrade",
|
|
230
|
+
});
|
|
231
|
+
return results;
|
|
232
|
+
}
|
|
233
|
+
// Check for "context-mode" in plugin array
|
|
234
|
+
const plugins = settings.plugin;
|
|
235
|
+
if (plugins && Array.isArray(plugins)) {
|
|
236
|
+
const hasPlugin = plugins.some((p) => p.includes("context-mode"));
|
|
237
|
+
results.push({
|
|
238
|
+
check: "Plugin registration",
|
|
239
|
+
status: hasPlugin ? "pass" : "fail",
|
|
240
|
+
message: hasPlugin
|
|
241
|
+
? "context-mode found in plugin array"
|
|
242
|
+
: "context-mode not found in plugin array",
|
|
243
|
+
fix: hasPlugin
|
|
244
|
+
? undefined
|
|
245
|
+
: "context-mode upgrade",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
results.push({
|
|
250
|
+
check: "Plugin registration",
|
|
251
|
+
status: "fail",
|
|
252
|
+
message: "No plugin array found in opencode.json",
|
|
253
|
+
fix: "context-mode upgrade",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// Warn about SessionStart limitation
|
|
257
|
+
results.push({
|
|
258
|
+
check: "SessionStart hook",
|
|
259
|
+
status: "warn",
|
|
260
|
+
message: "SessionStart not supported in OpenCode (see issues #14808, #5409)",
|
|
261
|
+
});
|
|
262
|
+
return results;
|
|
263
|
+
}
|
|
264
|
+
checkPluginRegistration() {
|
|
265
|
+
const settings = this.readSettings();
|
|
266
|
+
if (!settings) {
|
|
267
|
+
return {
|
|
268
|
+
check: "Plugin registration",
|
|
269
|
+
status: "warn",
|
|
270
|
+
message: "Could not read opencode.json",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const plugins = settings.plugin;
|
|
274
|
+
if (plugins && Array.isArray(plugins)) {
|
|
275
|
+
const hasPlugin = plugins.some((p) => p.includes("context-mode"));
|
|
276
|
+
if (hasPlugin) {
|
|
277
|
+
return {
|
|
278
|
+
check: "Plugin registration",
|
|
279
|
+
status: "pass",
|
|
280
|
+
message: "context-mode found in plugin array",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
check: "Plugin registration",
|
|
286
|
+
status: "fail",
|
|
287
|
+
message: "context-mode not found in opencode.json plugin array",
|
|
288
|
+
fix: "context-mode upgrade",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
getInstalledVersion() {
|
|
292
|
+
// Check ~/.cache/opencode/node_modules/ for context-mode
|
|
293
|
+
try {
|
|
294
|
+
const pkgPath = resolve(homedir(), ".cache", "opencode", "node_modules", "context-mode", "package.json");
|
|
295
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
296
|
+
if (typeof pkg.version === "string")
|
|
297
|
+
return pkg.version;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* not found */
|
|
301
|
+
}
|
|
302
|
+
return "not installed";
|
|
303
|
+
}
|
|
304
|
+
// ── Upgrade ────────────────────────────────────────────
|
|
305
|
+
configureAllHooks(_pluginRoot) {
|
|
306
|
+
const settings = this.readSettings() ?? {};
|
|
307
|
+
const changes = [];
|
|
308
|
+
// Add "context-mode" to the plugin array
|
|
309
|
+
const plugins = (settings.plugin ?? []);
|
|
310
|
+
if (!plugins.some((p) => p.includes("context-mode"))) {
|
|
311
|
+
plugins.push("context-mode");
|
|
312
|
+
changes.push("Added context-mode to plugin array");
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
changes.push("context-mode already in plugin array");
|
|
316
|
+
}
|
|
317
|
+
settings.plugin = plugins;
|
|
318
|
+
this.writeSettings(settings);
|
|
319
|
+
return changes;
|
|
320
|
+
}
|
|
321
|
+
backupSettings() {
|
|
322
|
+
const paths = [
|
|
323
|
+
resolve("opencode.json"),
|
|
324
|
+
resolve(".opencode", "opencode.json"),
|
|
325
|
+
];
|
|
326
|
+
for (const configPath of paths) {
|
|
327
|
+
try {
|
|
328
|
+
accessSync(configPath, constants.R_OK);
|
|
329
|
+
const backupPath = configPath + ".bak";
|
|
330
|
+
copyFileSync(configPath, backupPath);
|
|
331
|
+
return backupPath;
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
setHookPermissions(_pluginRoot) {
|
|
340
|
+
// OpenCode uses TS plugin paradigm — no shell scripts to chmod
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
updatePluginRegistry(_pluginRoot, _version) {
|
|
344
|
+
// OpenCode manages plugins through npm/opencode.json — no separate registry
|
|
345
|
+
}
|
|
346
|
+
// ── Routing Instructions (soft enforcement) ────────────
|
|
347
|
+
getRoutingInstructionsConfig() {
|
|
348
|
+
return {
|
|
349
|
+
fileName: "AGENTS.md",
|
|
350
|
+
globalPath: resolve(homedir(), ".config", "opencode", "AGENTS.md"),
|
|
351
|
+
projectRelativePath: "AGENTS.md",
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
writeRoutingInstructions(projectDir, pluginRoot) {
|
|
355
|
+
const config = this.getRoutingInstructionsConfig();
|
|
356
|
+
const targetPath = resolve(projectDir, config.projectRelativePath);
|
|
357
|
+
const sourcePath = resolve(pluginRoot, "configs", "opencode", config.fileName);
|
|
358
|
+
try {
|
|
359
|
+
const content = readFileSync(sourcePath, "utf-8");
|
|
360
|
+
try {
|
|
361
|
+
const existing = readFileSync(targetPath, "utf-8");
|
|
362
|
+
if (existing.includes("context-mode"))
|
|
363
|
+
return null;
|
|
364
|
+
writeFileSync(targetPath, existing.trimEnd() + "\n\n" + content, "utf-8");
|
|
365
|
+
return targetPath;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
369
|
+
return targetPath;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ── Internal helpers ───────────────────────────────────
|
|
377
|
+
/**
|
|
378
|
+
* Extract session ID from OpenCode hook input.
|
|
379
|
+
* OpenCode uses camelCase sessionID.
|
|
380
|
+
*/
|
|
381
|
+
extractSessionId(input) {
|
|
382
|
+
if (input.sessionID)
|
|
383
|
+
return input.sessionID;
|
|
384
|
+
return `pid-${process.ppid}`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/types — Platform adapter interface for multi-platform hook support.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract that each platform adapter must implement.
|
|
5
|
+
* Three paradigms exist across supported platforms:
|
|
6
|
+
* A) JSON stdin/stdout — Claude Code, Gemini CLI, VS Code Copilot, Copilot CLI, Cursor
|
|
7
|
+
* B) TS Plugin Functions — OpenCode
|
|
8
|
+
* C) MCP-only (no hooks) — Codex CLI
|
|
9
|
+
*
|
|
10
|
+
* The MCP server layer is 100% portable and needs no adapter.
|
|
11
|
+
* Only the hook layer requires platform-specific adapters.
|
|
12
|
+
*/
|
|
13
|
+
export type HookParadigm = "json-stdio" | "ts-plugin" | "mcp-only";
|
|
14
|
+
export interface PlatformCapabilities {
|
|
15
|
+
/** Platform supports PreToolUse / BeforeTool / tool.execute.before hooks. */
|
|
16
|
+
preToolUse: boolean;
|
|
17
|
+
/** Platform supports PostToolUse / AfterTool / tool.execute.after hooks. */
|
|
18
|
+
postToolUse: boolean;
|
|
19
|
+
/** Platform supports PreCompact / PreCompress / session.compacting hooks. */
|
|
20
|
+
preCompact: boolean;
|
|
21
|
+
/** Platform supports SessionStart / session.created hooks. */
|
|
22
|
+
sessionStart: boolean;
|
|
23
|
+
/** Platform allows modifying tool input arguments via hooks. */
|
|
24
|
+
canModifyArgs: boolean;
|
|
25
|
+
/** Platform allows modifying tool output via PostToolUse hooks. */
|
|
26
|
+
canModifyOutput: boolean;
|
|
27
|
+
/** Platform allows injecting context during session start or compaction. */
|
|
28
|
+
canInjectSessionContext: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Normalized PreToolUse event — platform-agnostic representation. */
|
|
31
|
+
export interface PreToolUseEvent {
|
|
32
|
+
/** Tool name being invoked (e.g., "Bash", "Read", "WebFetch"). */
|
|
33
|
+
toolName: string;
|
|
34
|
+
/** Tool input arguments as key-value pairs. */
|
|
35
|
+
toolInput: Record<string, unknown>;
|
|
36
|
+
/** Session ID extracted by the adapter. */
|
|
37
|
+
sessionId: string;
|
|
38
|
+
/** Project directory (if available). */
|
|
39
|
+
projectDir?: string;
|
|
40
|
+
/** Raw platform-specific input (for passthrough if needed). */
|
|
41
|
+
raw: unknown;
|
|
42
|
+
}
|
|
43
|
+
/** Normalized PostToolUse event — platform-agnostic representation. */
|
|
44
|
+
export interface PostToolUseEvent {
|
|
45
|
+
/** Tool name that was invoked. */
|
|
46
|
+
toolName: string;
|
|
47
|
+
/** Tool input arguments. */
|
|
48
|
+
toolInput: Record<string, unknown>;
|
|
49
|
+
/** Tool output/response (if available). */
|
|
50
|
+
toolOutput?: string;
|
|
51
|
+
/** Whether the tool call resulted in an error. */
|
|
52
|
+
isError?: boolean;
|
|
53
|
+
/** Session ID extracted by the adapter. */
|
|
54
|
+
sessionId: string;
|
|
55
|
+
/** Project directory (if available). */
|
|
56
|
+
projectDir?: string;
|
|
57
|
+
/** Raw platform-specific input. */
|
|
58
|
+
raw: unknown;
|
|
59
|
+
}
|
|
60
|
+
/** Normalized PreCompact event. */
|
|
61
|
+
export interface PreCompactEvent {
|
|
62
|
+
/** Session ID. */
|
|
63
|
+
sessionId: string;
|
|
64
|
+
/** Project directory (if available). */
|
|
65
|
+
projectDir?: string;
|
|
66
|
+
/** Raw platform-specific input. */
|
|
67
|
+
raw: unknown;
|
|
68
|
+
}
|
|
69
|
+
/** Normalized SessionStart event. */
|
|
70
|
+
export interface SessionStartEvent {
|
|
71
|
+
/** Session ID. */
|
|
72
|
+
sessionId: string;
|
|
73
|
+
/** Lifecycle source: fresh start, compaction, resume, or clear. */
|
|
74
|
+
source: "startup" | "compact" | "resume" | "clear";
|
|
75
|
+
/** Project directory (if available). */
|
|
76
|
+
projectDir?: string;
|
|
77
|
+
/** Raw platform-specific input. */
|
|
78
|
+
raw: unknown;
|
|
79
|
+
}
|
|
80
|
+
/** Response from PreToolUse hook — can block, modify, inject context, or pass through. */
|
|
81
|
+
export interface PreToolUseResponse {
|
|
82
|
+
/**
|
|
83
|
+
* "allow" = pass through (no action)
|
|
84
|
+
* "deny" = block tool execution
|
|
85
|
+
* "modify" = change input args
|
|
86
|
+
* "context" = inject additional context (soft guidance)
|
|
87
|
+
* "ask" = prompt user for confirmation (security policy match)
|
|
88
|
+
*/
|
|
89
|
+
decision: "allow" | "deny" | "modify" | "context" | "ask";
|
|
90
|
+
/** Reason for denial (shown to the model). */
|
|
91
|
+
reason?: string;
|
|
92
|
+
/** Modified tool input (only when decision = "modify"). */
|
|
93
|
+
updatedInput?: Record<string, unknown>;
|
|
94
|
+
/** Additional context to inject (only when decision = "context"). */
|
|
95
|
+
additionalContext?: string;
|
|
96
|
+
}
|
|
97
|
+
/** Response from PostToolUse hook — can inject context or modify output. */
|
|
98
|
+
export interface PostToolUseResponse {
|
|
99
|
+
/** Additional context to inject after tool output. */
|
|
100
|
+
additionalContext?: string;
|
|
101
|
+
/** Modified tool output (if platform supports it). */
|
|
102
|
+
updatedOutput?: string;
|
|
103
|
+
}
|
|
104
|
+
/** Response from PreCompact hook — injects context before compaction. */
|
|
105
|
+
export interface PreCompactResponse {
|
|
106
|
+
/** Context to preserve across compaction. */
|
|
107
|
+
context?: string;
|
|
108
|
+
}
|
|
109
|
+
/** Response from SessionStart hook — injects context at session start. */
|
|
110
|
+
export interface SessionStartResponse {
|
|
111
|
+
/** Context to inject at session start. */
|
|
112
|
+
context?: string;
|
|
113
|
+
}
|
|
114
|
+
/** A single hook entry in platform configuration. */
|
|
115
|
+
export interface HookEntry {
|
|
116
|
+
/** Tool matcher pattern (empty = match all). */
|
|
117
|
+
matcher: string;
|
|
118
|
+
/** Hook commands/handlers to execute. */
|
|
119
|
+
hooks: Array<{
|
|
120
|
+
type: string;
|
|
121
|
+
command: string;
|
|
122
|
+
}>;
|
|
123
|
+
}
|
|
124
|
+
/** Hook registration map — maps hook types to their entries. */
|
|
125
|
+
export type HookRegistration = Record<string, HookEntry[]>;
|
|
126
|
+
/**
|
|
127
|
+
* HookAdapter — contract for platform-specific hook implementations.
|
|
128
|
+
*
|
|
129
|
+
* Each supported platform (Claude Code, Gemini CLI, OpenCode, etc.)
|
|
130
|
+
* provides an adapter that normalizes its hook I/O into a common format.
|
|
131
|
+
*/
|
|
132
|
+
export interface HookAdapter {
|
|
133
|
+
/** Human-readable platform name (e.g., "Claude Code", "Gemini CLI"). */
|
|
134
|
+
readonly name: string;
|
|
135
|
+
/** Hook I/O paradigm used by this platform. */
|
|
136
|
+
readonly paradigm: HookParadigm;
|
|
137
|
+
/** What this platform supports. */
|
|
138
|
+
readonly capabilities: PlatformCapabilities;
|
|
139
|
+
/** Parse raw PreToolUse input into normalized form. */
|
|
140
|
+
parsePreToolUseInput(raw: unknown): PreToolUseEvent;
|
|
141
|
+
/** Parse raw PostToolUse input into normalized form. */
|
|
142
|
+
parsePostToolUseInput(raw: unknown): PostToolUseEvent;
|
|
143
|
+
/** Parse raw PreCompact input (optional — not all platforms support it). */
|
|
144
|
+
parsePreCompactInput?(raw: unknown): PreCompactEvent;
|
|
145
|
+
/** Parse raw SessionStart input (optional — not all platforms support it). */
|
|
146
|
+
parseSessionStartInput?(raw: unknown): SessionStartEvent;
|
|
147
|
+
/** Format a PreToolUse response into platform-specific output. */
|
|
148
|
+
formatPreToolUseResponse(response: PreToolUseResponse): unknown;
|
|
149
|
+
/** Format a PostToolUse response into platform-specific output. */
|
|
150
|
+
formatPostToolUseResponse(response: PostToolUseResponse): unknown;
|
|
151
|
+
/** Format a PreCompact response into platform-specific output. */
|
|
152
|
+
formatPreCompactResponse?(response: PreCompactResponse): unknown;
|
|
153
|
+
/** Format a SessionStart response into platform-specific output. */
|
|
154
|
+
formatSessionStartResponse?(response: SessionStartResponse): unknown;
|
|
155
|
+
/** Path to the platform's settings file (e.g., ~/.claude/settings.json). */
|
|
156
|
+
getSettingsPath(): string;
|
|
157
|
+
/** Directory where session data is stored. */
|
|
158
|
+
getSessionDir(): string;
|
|
159
|
+
/** Compute per-project session DB path. */
|
|
160
|
+
getSessionDBPath(projectDir: string): string;
|
|
161
|
+
/** Compute per-project session events file path. */
|
|
162
|
+
getSessionEventsPath(projectDir: string): string;
|
|
163
|
+
/** Generate hook registration config for this platform. */
|
|
164
|
+
generateHookConfig(pluginRoot: string): HookRegistration;
|
|
165
|
+
/** Read current platform settings. */
|
|
166
|
+
readSettings(): Record<string, unknown> | null;
|
|
167
|
+
/** Write platform settings. */
|
|
168
|
+
writeSettings(settings: Record<string, unknown>): void;
|
|
169
|
+
/** Validate that hooks are properly configured for this platform. */
|
|
170
|
+
validateHooks(pluginRoot: string): DiagnosticResult[];
|
|
171
|
+
/** Check if the plugin is registered/enabled on this platform. */
|
|
172
|
+
checkPluginRegistration(): DiagnosticResult;
|
|
173
|
+
/** Get the installed version from this platform's registry/marketplace. */
|
|
174
|
+
getInstalledVersion(): string;
|
|
175
|
+
/** Configure all hooks for this platform. Returns change descriptions. */
|
|
176
|
+
configureAllHooks(pluginRoot: string): string[];
|
|
177
|
+
/** Backup platform settings before modification. Returns backup path or null. */
|
|
178
|
+
backupSettings(): string | null;
|
|
179
|
+
/** Set executable permissions on hook scripts. Returns paths that were set. */
|
|
180
|
+
setHookPermissions(pluginRoot: string): string[];
|
|
181
|
+
/** Update platform's plugin registry to point to given path and version. */
|
|
182
|
+
updatePluginRegistry(pluginRoot: string, version: string): void;
|
|
183
|
+
/** Get the routing instructions file config for this platform. */
|
|
184
|
+
getRoutingInstructionsConfig(): RoutingInstructionsConfig;
|
|
185
|
+
/** Write routing instructions file to project directory if not present. Returns path written or null if already exists. */
|
|
186
|
+
writeRoutingInstructions(projectDir: string, pluginRoot: string): string | null;
|
|
187
|
+
}
|
|
188
|
+
/** Configuration for platform-specific routing instruction files. */
|
|
189
|
+
export interface RoutingInstructionsConfig {
|
|
190
|
+
/** File name the platform reads (e.g., "CLAUDE.md", "GEMINI.md", "AGENTS.md"). */
|
|
191
|
+
fileName: string;
|
|
192
|
+
/** Global path for this platform (e.g., "~/.claude/CLAUDE.md"). */
|
|
193
|
+
globalPath: string;
|
|
194
|
+
/** Project-level path relative to project root (e.g., "GEMINI.md", ".github/copilot-instructions.md"). */
|
|
195
|
+
projectRelativePath: string;
|
|
196
|
+
}
|
|
197
|
+
/** Result from a platform-specific diagnostic check. */
|
|
198
|
+
export interface DiagnosticResult {
|
|
199
|
+
/** What was checked. */
|
|
200
|
+
check: string;
|
|
201
|
+
/** Pass, fail, or warning. */
|
|
202
|
+
status: "pass" | "fail" | "warn";
|
|
203
|
+
/** Human-readable message. */
|
|
204
|
+
message: string;
|
|
205
|
+
/** Suggested fix command (if applicable). */
|
|
206
|
+
fix?: string;
|
|
207
|
+
}
|
|
208
|
+
/** Supported platform identifiers. */
|
|
209
|
+
export type PlatformId = "claude-code" | "gemini-cli" | "opencode" | "codex" | "copilot-cli" | "vscode-copilot" | "cursor" | "unknown";
|
|
210
|
+
/** Detection signal used to identify which platform is running. */
|
|
211
|
+
export interface DetectionSignal {
|
|
212
|
+
/** Platform identifier. */
|
|
213
|
+
platform: PlatformId;
|
|
214
|
+
/** Confidence: env var match > config dir match > fallback. */
|
|
215
|
+
confidence: "high" | "medium" | "low";
|
|
216
|
+
/** How it was detected. */
|
|
217
|
+
reason: string;
|
|
218
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/types — Platform adapter interface for multi-platform hook support.
|
|
3
|
+
*
|
|
4
|
+
* Defines the contract that each platform adapter must implement.
|
|
5
|
+
* Three paradigms exist across supported platforms:
|
|
6
|
+
* A) JSON stdin/stdout — Claude Code, Gemini CLI, VS Code Copilot, Copilot CLI, Cursor
|
|
7
|
+
* B) TS Plugin Functions — OpenCode
|
|
8
|
+
* C) MCP-only (no hooks) — Codex CLI
|
|
9
|
+
*
|
|
10
|
+
* The MCP server layer is 100% portable and needs no adapter.
|
|
11
|
+
* Only the hook layer requires platform-specific adapters.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/vscode-copilot/config — Thin re-exports from VSCodeCopilotAdapter.
|
|
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 { VSCodeCopilotAdapter } from "./index.js";
|
|
8
|
+
export { HOOK_TYPES, HOOK_SCRIPTS, REQUIRED_HOOKS, OPTIONAL_HOOKS } from "./hooks.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/vscode-copilot/config — Thin re-exports from VSCodeCopilotAdapter.
|
|
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 { VSCodeCopilotAdapter } from "./index.js";
|
|
8
|
+
export { HOOK_TYPES, HOOK_SCRIPTS, REQUIRED_HOOKS, OPTIONAL_HOOKS } from "./hooks.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adapters/vscode-copilot/hooks — VS Code Copilot hook definitions and matchers.
|
|
3
|
+
*
|
|
4
|
+
* Defines the hook types, matchers, and registration format specific to
|
|
5
|
+
* VS Code Copilot's hook system. This module is used by:
|
|
6
|
+
* - CLI setup/upgrade commands (to configure hooks)
|
|
7
|
+
* - Doctor command (to validate hook configuration)
|
|
8
|
+
* - Hook config generation
|
|
9
|
+
*
|
|
10
|
+
* VS Code Copilot hook system reference:
|
|
11
|
+
* - Hooks are registered in .github/hooks/*.json
|
|
12
|
+
* - Hook names: PreToolUse, PostToolUse, PreCompact, SessionStart (PascalCase)
|
|
13
|
+
* - Additional hooks: Stop, SubagentStart, SubagentStop (unique to VS Code)
|
|
14
|
+
* - CRITICAL: matchers are parsed but IGNORED (all hooks fire on all tools)
|
|
15
|
+
* - Input: JSON on stdin
|
|
16
|
+
* - Output: JSON on stdout (or empty for passthrough)
|
|
17
|
+
* - Preview status — API may change
|
|
18
|
+
*/
|
|
19
|
+
/** VS Code Copilot hook types. */
|
|
20
|
+
export declare const HOOK_TYPES: {
|
|
21
|
+
readonly PRE_TOOL_USE: "PreToolUse";
|
|
22
|
+
readonly POST_TOOL_USE: "PostToolUse";
|
|
23
|
+
readonly PRE_COMPACT: "PreCompact";
|
|
24
|
+
readonly SESSION_START: "SessionStart";
|
|
25
|
+
readonly STOP: "Stop";
|
|
26
|
+
readonly SUBAGENT_START: "SubagentStart";
|
|
27
|
+
readonly SUBAGENT_STOP: "SubagentStop";
|
|
28
|
+
};
|
|
29
|
+
export type HookType = (typeof HOOK_TYPES)[keyof typeof HOOK_TYPES];
|
|
30
|
+
/** Map of hook types to their script file names. */
|
|
31
|
+
export declare const HOOK_SCRIPTS: Record<string, string>;
|
|
32
|
+
/** Required hooks that must be configured for context-mode to function. */
|
|
33
|
+
export declare const REQUIRED_HOOKS: HookType[];
|
|
34
|
+
/** Optional hooks that enhance functionality but aren't critical. */
|
|
35
|
+
export declare const OPTIONAL_HOOKS: HookType[];
|
|
36
|
+
/**
|
|
37
|
+
* Check if a hook entry points to a context-mode hook script.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isContextModeHook(entry: {
|
|
40
|
+
hooks?: Array<{
|
|
41
|
+
command?: string;
|
|
42
|
+
}>;
|
|
43
|
+
}, hookType: HookType): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Build the hook command string for a given hook type.
|
|
46
|
+
* Uses the CLI dispatcher: `context-mode hook vscode-copilot <event>`
|
|
47
|
+
* Requires global install: `npm install -g context-mode`
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildHookCommand(hookType: HookType): string;
|