opencode-command-hooks 0.1.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/README.md +796 -0
- package/dist/config/agent.d.ts +82 -0
- package/dist/config/agent.d.ts.map +1 -0
- package/dist/config/agent.js +145 -0
- package/dist/config/agent.js.map +1 -0
- package/dist/config/global.d.ts +36 -0
- package/dist/config/global.d.ts.map +1 -0
- package/dist/config/global.js +219 -0
- package/dist/config/global.js.map +1 -0
- package/dist/config/markdown.d.ts +119 -0
- package/dist/config/markdown.d.ts.map +1 -0
- package/dist/config/markdown.js +373 -0
- package/dist/config/markdown.js.map +1 -0
- package/dist/config/merge.d.ts +67 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +192 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/execution/shell.d.ts +55 -0
- package/dist/execution/shell.d.ts.map +1 -0
- package/dist/execution/shell.js +187 -0
- package/dist/execution/shell.js.map +1 -0
- package/dist/execution/template.d.ts +55 -0
- package/dist/execution/template.d.ts.map +1 -0
- package/dist/execution/template.js +106 -0
- package/dist/execution/template.js.map +1 -0
- package/dist/executor.d.ts +54 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +314 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +359 -0
- package/dist/index.js.map +1 -0
- package/dist/logging.d.ts +24 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +57 -0
- package/dist/logging.js.map +1 -0
- package/dist/schemas.d.ts +425 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +150 -0
- package/dist/schemas.js.map +1 -0
- package/dist/types/hooks.d.ts +635 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/hooks.js +12 -0
- package/dist/types/hooks.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown configuration parser for loading hooks from agent and slash-command markdown files
|
|
3
|
+
*
|
|
4
|
+
* Parses YAML frontmatter from markdown files and extracts command_hooks configuration.
|
|
5
|
+
* Supports both agent markdown files (typically in .opencode/agents/) and slash-command
|
|
6
|
+
* markdown files (typically in .opencode/commands/).
|
|
7
|
+
*/
|
|
8
|
+
import { load as parseYaml } from "js-yaml";
|
|
9
|
+
import { logger } from "../logging.js";
|
|
10
|
+
/**
|
|
11
|
+
* In-memory cache for markdown configurations
|
|
12
|
+
* Maps file paths to their parsed CommandHooksConfig to avoid repeated file reads.
|
|
13
|
+
* Entries are cached indefinitely; clear manually if config files change.
|
|
14
|
+
*/
|
|
15
|
+
const markdownConfigCache = new Map();
|
|
16
|
+
/**
|
|
17
|
+
* Extract YAML frontmatter from markdown content
|
|
18
|
+
*
|
|
19
|
+
* Frontmatter is defined as content between the first `---` and second `---`
|
|
20
|
+
* at the start of the file. Returns the raw YAML string (without delimiters).
|
|
21
|
+
*
|
|
22
|
+
* @param content - Full markdown file content
|
|
23
|
+
* @returns Raw YAML string, or null if no frontmatter found
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```
|
|
27
|
+
* ---
|
|
28
|
+
* name: my-agent
|
|
29
|
+
* command_hooks:
|
|
30
|
+
* tool: [...]
|
|
31
|
+
* ---
|
|
32
|
+
*
|
|
33
|
+
* # Agent content
|
|
34
|
+
* ```
|
|
35
|
+
* Returns the YAML between the delimiters
|
|
36
|
+
*/
|
|
37
|
+
export function extractYamlFrontmatter(content) {
|
|
38
|
+
// Check if content starts with ---
|
|
39
|
+
if (!content.startsWith("---")) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
// Find the second --- delimiter
|
|
43
|
+
// Start searching from position 3 (after the first ---)
|
|
44
|
+
const secondDelimiterIndex = content.indexOf("---", 3);
|
|
45
|
+
if (secondDelimiterIndex === -1) {
|
|
46
|
+
// No closing delimiter found
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// Extract YAML between the delimiters
|
|
50
|
+
const yamlContent = content.substring(3, secondDelimiterIndex).trim();
|
|
51
|
+
return yamlContent;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse YAML content and return the parsed object
|
|
55
|
+
*
|
|
56
|
+
* Handles YAML parsing errors gracefully by returning null.
|
|
57
|
+
* Does not throw errors - callers should check for null return value.
|
|
58
|
+
*
|
|
59
|
+
* @param yamlContent - Raw YAML string to parse
|
|
60
|
+
* @returns Parsed YAML object, or null if parsing failed
|
|
61
|
+
*/
|
|
62
|
+
export function parseYamlFrontmatter(content) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = parseYaml(content);
|
|
65
|
+
return parsed === undefined ? null : parsed;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
logger.debug(`Failed to parse YAML: ${message}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse simplified agent hooks from YAML frontmatter content
|
|
75
|
+
*
|
|
76
|
+
* Extracts and validates the new simplified `hooks` format from agent markdown
|
|
77
|
+
* frontmatter. Returns null if no hooks are present or if parsing fails.
|
|
78
|
+
*
|
|
79
|
+
* @param yamlContent - Raw YAML string from frontmatter
|
|
80
|
+
* @param agentName - Name of the agent (used for auto-generating hook IDs)
|
|
81
|
+
* @returns Parsed AgentHooks object, or null if no valid hooks found
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```yaml
|
|
85
|
+
* hooks:
|
|
86
|
+
* before:
|
|
87
|
+
* - run: "echo starting"
|
|
88
|
+
* after:
|
|
89
|
+
* - run: ["npm run test"]
|
|
90
|
+
* inject: "Results:\n{stdout}"
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function parseAgentHooks(yamlContent) {
|
|
94
|
+
const parsed = parseYamlFrontmatter(yamlContent);
|
|
95
|
+
if (parsed === null || typeof parsed !== "object" || parsed === null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const config = parsed;
|
|
99
|
+
const hooks = config.hooks;
|
|
100
|
+
if (hooks === undefined) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (typeof hooks !== "object" || hooks === null) {
|
|
104
|
+
logger.debug("hooks field is not an object");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const agentHooks = hooks;
|
|
108
|
+
const result = {};
|
|
109
|
+
// Parse before array
|
|
110
|
+
if (agentHooks.before !== undefined) {
|
|
111
|
+
if (!Array.isArray(agentHooks.before)) {
|
|
112
|
+
logger.debug("hooks.before is not an array");
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
result.before = agentHooks.before;
|
|
116
|
+
}
|
|
117
|
+
// Parse after array
|
|
118
|
+
if (agentHooks.after !== undefined) {
|
|
119
|
+
if (!Array.isArray(agentHooks.after)) {
|
|
120
|
+
logger.debug("hooks.after is not an array");
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
result.after = agentHooks.after;
|
|
124
|
+
}
|
|
125
|
+
// Return null if both are empty/undefined
|
|
126
|
+
if (!result.before?.length && !result.after?.length) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Convert simplified agent hooks to internal CommandHooksConfig format
|
|
133
|
+
*
|
|
134
|
+
* Takes the simplified AgentHooks format and converts it to the internal
|
|
135
|
+
* ToolHook[] format with auto-generated IDs and proper when clauses.
|
|
136
|
+
*
|
|
137
|
+
* @param agentHooks - Simplified agent hooks configuration
|
|
138
|
+
* @param agentName - Name of the agent (used for hook ID generation)
|
|
139
|
+
* @returns CommandHooksConfig with tool hooks converted to internal format
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* const simpleHooks: AgentHooks = {
|
|
144
|
+
* after: [{ run: "npm run test", inject: "Results: {stdout}" }]
|
|
145
|
+
* };
|
|
146
|
+
* const config = convertToCommandHooksConfig(simpleHooks, "engineer");
|
|
147
|
+
* // Results in: { tool: [{ id: "engineer-after-0", when: {...}, run: ..., inject: ... }] }
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function convertToCommandHooksConfig(agentHooks, agentName) {
|
|
151
|
+
const toolHooks = [];
|
|
152
|
+
// Convert before hooks
|
|
153
|
+
if (agentHooks.before) {
|
|
154
|
+
agentHooks.before.forEach((hook, index) => {
|
|
155
|
+
const toolHook = convertAgentHookEntryToToolHook(hook, agentName, "before", index);
|
|
156
|
+
if (toolHook) {
|
|
157
|
+
toolHooks.push(toolHook);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// Convert after hooks
|
|
162
|
+
if (agentHooks.after) {
|
|
163
|
+
agentHooks.after.forEach((hook, index) => {
|
|
164
|
+
const toolHook = convertAgentHookEntryToToolHook(hook, agentName, "after", index);
|
|
165
|
+
if (toolHook) {
|
|
166
|
+
toolHooks.push(toolHook);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
tool: toolHooks,
|
|
172
|
+
session: [],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Convert a single AgentHookEntry to ToolHook format
|
|
177
|
+
*/
|
|
178
|
+
function convertAgentHookEntryToToolHook(entry, agentName, phase, index) {
|
|
179
|
+
if (!entry.run) {
|
|
180
|
+
logger.debug("Hook entry missing required 'run' field");
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const hookId = `${agentName}-${phase}-${index}`;
|
|
184
|
+
return {
|
|
185
|
+
id: hookId,
|
|
186
|
+
when: {
|
|
187
|
+
phase: phase,
|
|
188
|
+
tool: ["task"],
|
|
189
|
+
callingAgent: [agentName], // Implicit scoping to this agent
|
|
190
|
+
},
|
|
191
|
+
run: entry.run,
|
|
192
|
+
inject: entry.inject,
|
|
193
|
+
toast: entry.toast,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if a value is a valid CommandHooksConfig object
|
|
198
|
+
*/
|
|
199
|
+
function isValidCommandHooksConfig(value) {
|
|
200
|
+
if (typeof value !== "object" || value === null) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const obj = value;
|
|
204
|
+
// Both tool and session are optional
|
|
205
|
+
if (obj.tool !== undefined && !Array.isArray(obj.tool)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (obj.session !== undefined && !Array.isArray(obj.session)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Extract agent name from file path
|
|
215
|
+
*
|
|
216
|
+
* Takes an absolute path to an agent markdown file and extracts the agent name
|
|
217
|
+
* (filename without .md extension).
|
|
218
|
+
*
|
|
219
|
+
* @param filePath - Absolute path to agent markdown file
|
|
220
|
+
* @returns Agent name (filename without extension)
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* const name = extractAgentNameFromPath("/project/.opencode/agent/engineer.md");
|
|
225
|
+
* // Returns: "engineer"
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
function extractAgentNameFromPath(filePath) {
|
|
229
|
+
const fileName = filePath.split("/").pop() || "";
|
|
230
|
+
return fileName.replace(/\.md$/, "");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Load and parse command hooks configuration from a markdown file
|
|
234
|
+
*
|
|
235
|
+
* Reads a markdown file, extracts YAML frontmatter, parses it, and extracts
|
|
236
|
+
* either the new simplified `hooks` format or the legacy `command_hooks` format.
|
|
237
|
+
*
|
|
238
|
+
* **Caching:** This function implements in-memory caching per file path to avoid
|
|
239
|
+
* repeated file reads. The cache is checked first; if not found, the file is read
|
|
240
|
+
* from disk and cached for subsequent calls.
|
|
241
|
+
*
|
|
242
|
+
* Error handling:
|
|
243
|
+
* - If file doesn't exist: returns empty config (not an error)
|
|
244
|
+
* - If no frontmatter found: returns empty config
|
|
245
|
+
* - If YAML is malformed: logs warning, returns empty config
|
|
246
|
+
* - If hooks format is invalid: logs warning, returns empty config
|
|
247
|
+
* - Never throws errors - always returns a valid config
|
|
248
|
+
*
|
|
249
|
+
* @param filePath - Absolute path to the markdown file
|
|
250
|
+
* @returns Promise resolving to CommandHooksConfig (may be empty)
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```typescript
|
|
254
|
+
* const config = await loadMarkdownConfig("/path/to/agent.md");
|
|
255
|
+
* // Returns { tool: [...], session: [...] } or { tool: [], session: [] }
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
export async function loadMarkdownConfig(filePath) {
|
|
259
|
+
// Check cache first
|
|
260
|
+
if (markdownConfigCache.has(filePath)) {
|
|
261
|
+
const cached = markdownConfigCache.get(filePath);
|
|
262
|
+
logger.debug(`Returning cached markdown config from ${filePath}: ${cached.tool?.length ?? 0} tool hooks, ${cached.session?.length ?? 0} session hooks`);
|
|
263
|
+
return cached;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
// Try to read the file
|
|
267
|
+
let content;
|
|
268
|
+
try {
|
|
269
|
+
const file = Bun.file(filePath);
|
|
270
|
+
if (!(await file.exists())) {
|
|
271
|
+
logger.debug(`Markdown file not found: ${filePath}`);
|
|
272
|
+
const emptyConfig = { tool: [], session: [] };
|
|
273
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
274
|
+
return emptyConfig;
|
|
275
|
+
}
|
|
276
|
+
content = await file.text();
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
280
|
+
logger.debug(`Failed to read markdown file ${filePath}: ${message}`);
|
|
281
|
+
const emptyConfig = { tool: [], session: [] };
|
|
282
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
283
|
+
return emptyConfig;
|
|
284
|
+
}
|
|
285
|
+
// Extract YAML frontmatter
|
|
286
|
+
const yamlContent = extractYamlFrontmatter(content);
|
|
287
|
+
if (!yamlContent) {
|
|
288
|
+
logger.debug(`No YAML frontmatter found in ${filePath}`);
|
|
289
|
+
const emptyConfig = { tool: [], session: [] };
|
|
290
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
291
|
+
return emptyConfig;
|
|
292
|
+
}
|
|
293
|
+
// Parse YAML
|
|
294
|
+
const parsed = parseYamlFrontmatter(yamlContent);
|
|
295
|
+
if (parsed === null) {
|
|
296
|
+
logger.info(`Failed to parse YAML frontmatter in ${filePath}`);
|
|
297
|
+
const emptyConfig = { tool: [], session: [] };
|
|
298
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
299
|
+
return emptyConfig;
|
|
300
|
+
}
|
|
301
|
+
// Extract command_hooks field
|
|
302
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
303
|
+
logger.debug(`Parsed YAML is not an object in ${filePath}`);
|
|
304
|
+
const emptyConfig = { tool: [], session: [] };
|
|
305
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
306
|
+
return emptyConfig;
|
|
307
|
+
}
|
|
308
|
+
const config = parsed;
|
|
309
|
+
// First, try to parse the new simplified hooks format
|
|
310
|
+
const agentName = extractAgentNameFromPath(filePath);
|
|
311
|
+
const agentHooks = parseAgentHooks(yamlContent);
|
|
312
|
+
if (agentHooks) {
|
|
313
|
+
// Convert simplified format to internal format
|
|
314
|
+
const result = convertToCommandHooksConfig(agentHooks, agentName);
|
|
315
|
+
logger.debug(`Loaded simplified hooks from ${filePath}: ${result.tool?.length ?? 0} tool hooks`);
|
|
316
|
+
// Cache the result
|
|
317
|
+
markdownConfigCache.set(filePath, result);
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
// Fall back to old command_hooks format
|
|
321
|
+
const commandHooks = config.command_hooks;
|
|
322
|
+
if (commandHooks === undefined) {
|
|
323
|
+
logger.debug(`No hooks or command_hooks field in ${filePath}`);
|
|
324
|
+
const emptyConfig = { tool: [], session: [] };
|
|
325
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
326
|
+
return emptyConfig;
|
|
327
|
+
}
|
|
328
|
+
// Validate command_hooks structure
|
|
329
|
+
if (!isValidCommandHooksConfig(commandHooks)) {
|
|
330
|
+
logger.info(`command_hooks field is not a valid object in ${filePath} (expected { tool?: [], session?: [] })`);
|
|
331
|
+
const emptyConfig = { tool: [], session: [] };
|
|
332
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
333
|
+
return emptyConfig;
|
|
334
|
+
}
|
|
335
|
+
// Return with defaults for missing arrays
|
|
336
|
+
const result = {
|
|
337
|
+
tool: commandHooks.tool ?? [],
|
|
338
|
+
session: commandHooks.session ?? [],
|
|
339
|
+
};
|
|
340
|
+
logger.debug(`Loaded command_hooks from ${filePath}: ${result.tool?.length ?? 0} tool hooks, ${result.session?.length ?? 0} session hooks`);
|
|
341
|
+
// Cache the result
|
|
342
|
+
markdownConfigCache.set(filePath, result);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
// Catch-all for unexpected errors
|
|
347
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
+
logger.info(`Unexpected error loading markdown config from ${filePath}: ${message}`);
|
|
349
|
+
const emptyConfig = { tool: [], session: [] };
|
|
350
|
+
markdownConfigCache.set(filePath, emptyConfig);
|
|
351
|
+
return emptyConfig;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Clear the markdown config cache for a specific file
|
|
356
|
+
*
|
|
357
|
+
* Forces the next call to loadMarkdownConfig() for this file to reload from disk.
|
|
358
|
+
* Useful for testing or when config files may have changed.
|
|
359
|
+
*
|
|
360
|
+
* @param filePath - Path to clear from cache, or undefined to clear all
|
|
361
|
+
* @internal For testing purposes
|
|
362
|
+
*/
|
|
363
|
+
export function clearMarkdownConfigCache(filePath) {
|
|
364
|
+
if (filePath) {
|
|
365
|
+
logger.debug(`Clearing markdown config cache for ${filePath}`);
|
|
366
|
+
markdownConfigCache.delete(filePath);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
logger.debug("Clearing all markdown config cache");
|
|
370
|
+
markdownConfigCache.clear();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
//# sourceMappingURL=markdown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.js","sourceRoot":"","sources":["../../src/config/markdown.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration merging and precedence logic for combining global and markdown configs
|
|
3
|
+
*
|
|
4
|
+
* Implements the precedence rules from PRD section 5.3:
|
|
5
|
+
* 1. Start with global hooks
|
|
6
|
+
* 2. Markdown hooks with same `id` replace global hooks (no error)
|
|
7
|
+
* 3. Markdown hooks with unique `id` are added
|
|
8
|
+
* 4. Duplicate IDs within same source are errors
|
|
9
|
+
*/
|
|
10
|
+
import type { CommandHooksConfig, ToolHook, SessionHook, HookValidationError } from "../types/hooks.js";
|
|
11
|
+
/**
|
|
12
|
+
* Find duplicate IDs within a hook array
|
|
13
|
+
*
|
|
14
|
+
* Scans through an array of hooks and returns a list of IDs that appear
|
|
15
|
+
* more than once. Useful for validation of both tool and session hooks.
|
|
16
|
+
*
|
|
17
|
+
* @param hooks - Array of hooks to check (ToolHook[] or SessionHook[])
|
|
18
|
+
* @returns Array of duplicate IDs (empty if no duplicates found)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const hooks = [
|
|
23
|
+
* { id: "hook-1", ... },
|
|
24
|
+
* { id: "hook-2", ... },
|
|
25
|
+
* { id: "hook-1", ... } // duplicate
|
|
26
|
+
* ]
|
|
27
|
+
* findDuplicateIds(hooks) // Returns ["hook-1"]
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function findDuplicateIds(hooks: (ToolHook | SessionHook)[]): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Merge global and markdown configs with proper precedence
|
|
33
|
+
*
|
|
34
|
+
* Implements the precedence rules from PRD section 5.3:
|
|
35
|
+
* 1. Start with global hooks
|
|
36
|
+
* 2. Markdown hooks with same `id` replace global hooks (no error)
|
|
37
|
+
* 3. Markdown hooks with unique `id` are added
|
|
38
|
+
* 4. Duplicate IDs within same source are errors
|
|
39
|
+
*
|
|
40
|
+
* Returns both the merged config and any validation errors found.
|
|
41
|
+
* Errors are returned but don't prevent merging - the caller can decide
|
|
42
|
+
* how to handle them.
|
|
43
|
+
*
|
|
44
|
+
* @param global - Global configuration from opencode.json
|
|
45
|
+
* @param markdown - Markdown configuration from agent/command .md file
|
|
46
|
+
* @returns Object with merged config and validation errors
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typescript
|
|
50
|
+
* const global = {
|
|
51
|
+
* tool: [{ id: "hook-1", when: { phase: "after" }, run: "echo global" }],
|
|
52
|
+
* session: []
|
|
53
|
+
* }
|
|
54
|
+
* const markdown = {
|
|
55
|
+
* tool: [{ id: "hook-1", when: { phase: "after" }, run: "echo markdown" }],
|
|
56
|
+
* session: []
|
|
57
|
+
* }
|
|
58
|
+
* const result = mergeConfigs(global, markdown)
|
|
59
|
+
* // result.config.tool[0].run === "echo markdown" (markdown replaced global)
|
|
60
|
+
* // result.errors === [] (no duplicates)
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function mergeConfigs(global: CommandHooksConfig, markdown: CommandHooksConfig): {
|
|
64
|
+
config: CommandHooksConfig;
|
|
65
|
+
errors: HookValidationError[];
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=merge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/config/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACR,kBAAkB,EAClB,QAAQ,EACR,WAAW,EACX,mBAAmB,EACtB,MAAM,mBAAmB,CAAA;AAG1B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,CAAC,QAAQ,GAAG,WAAW,CAAC,EAAE,GAAG,MAAM,EAAE,CAa5E;AA+GD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,YAAY,CACzB,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,kBAAkB,GAC5B;IAAE,MAAM,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAmC/D"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration merging and precedence logic for combining global and markdown configs
|
|
3
|
+
*
|
|
4
|
+
* Implements the precedence rules from PRD section 5.3:
|
|
5
|
+
* 1. Start with global hooks
|
|
6
|
+
* 2. Markdown hooks with same `id` replace global hooks (no error)
|
|
7
|
+
* 3. Markdown hooks with unique `id` are added
|
|
8
|
+
* 4. Duplicate IDs within same source are errors
|
|
9
|
+
*/
|
|
10
|
+
import { logger } from "../logging.js";
|
|
11
|
+
/**
|
|
12
|
+
* Find duplicate IDs within a hook array
|
|
13
|
+
*
|
|
14
|
+
* Scans through an array of hooks and returns a list of IDs that appear
|
|
15
|
+
* more than once. Useful for validation of both tool and session hooks.
|
|
16
|
+
*
|
|
17
|
+
* @param hooks - Array of hooks to check (ToolHook[] or SessionHook[])
|
|
18
|
+
* @returns Array of duplicate IDs (empty if no duplicates found)
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const hooks = [
|
|
23
|
+
* { id: "hook-1", ... },
|
|
24
|
+
* { id: "hook-2", ... },
|
|
25
|
+
* { id: "hook-1", ... } // duplicate
|
|
26
|
+
* ]
|
|
27
|
+
* findDuplicateIds(hooks) // Returns ["hook-1"]
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function findDuplicateIds(hooks) {
|
|
31
|
+
const idCounts = new Map();
|
|
32
|
+
// Count occurrences of each ID
|
|
33
|
+
for (const hook of hooks) {
|
|
34
|
+
const count = idCounts.get(hook.id) ?? 0;
|
|
35
|
+
idCounts.set(hook.id, count + 1);
|
|
36
|
+
}
|
|
37
|
+
// Return IDs that appear more than once
|
|
38
|
+
return Array.from(idCounts.entries())
|
|
39
|
+
.filter(([, count]) => count > 1)
|
|
40
|
+
.map(([id]) => id);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate a single config source for duplicate IDs
|
|
44
|
+
*
|
|
45
|
+
* Checks both tool and session hooks for duplicates and returns
|
|
46
|
+
* validation errors for any found.
|
|
47
|
+
*
|
|
48
|
+
* @param config - Configuration to validate
|
|
49
|
+
* @param source - Source identifier for error reporting (e.g., "global", "markdown")
|
|
50
|
+
* @returns Array of validation errors (empty if no duplicates)
|
|
51
|
+
*/
|
|
52
|
+
function validateConfigForDuplicates(config, source) {
|
|
53
|
+
const errors = [];
|
|
54
|
+
// Check tool hooks for duplicates
|
|
55
|
+
if (config.tool && config.tool.length > 0) {
|
|
56
|
+
const toolDuplicates = findDuplicateIds(config.tool);
|
|
57
|
+
for (const id of toolDuplicates) {
|
|
58
|
+
errors.push({
|
|
59
|
+
hookId: id,
|
|
60
|
+
type: "duplicate_id",
|
|
61
|
+
message: `Duplicate hook ID "${id}" found in ${source} tool hooks`,
|
|
62
|
+
severity: "error",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Check session hooks for duplicates
|
|
67
|
+
if (config.session && config.session.length > 0) {
|
|
68
|
+
const sessionDuplicates = findDuplicateIds(config.session);
|
|
69
|
+
for (const id of sessionDuplicates) {
|
|
70
|
+
errors.push({
|
|
71
|
+
hookId: id,
|
|
72
|
+
type: "duplicate_id",
|
|
73
|
+
message: `Duplicate hook ID "${id}" found in ${source} session hooks`,
|
|
74
|
+
severity: "error",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Merge two hook arrays with markdown taking precedence
|
|
82
|
+
*
|
|
83
|
+
* Combines global and markdown hooks, where markdown hooks with the same ID
|
|
84
|
+
* replace global hooks. Markdown hooks with unique IDs are appended.
|
|
85
|
+
*
|
|
86
|
+
* Order is preserved: global hooks first (except those replaced), then new markdown hooks.
|
|
87
|
+
*
|
|
88
|
+
* @param globalHooks - Hooks from global config
|
|
89
|
+
* @param markdownHooks - Hooks from markdown config
|
|
90
|
+
* @returns Merged hook array
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const global = [
|
|
95
|
+
* { id: "hook-1", ... },
|
|
96
|
+
* { id: "hook-2", ... }
|
|
97
|
+
* ]
|
|
98
|
+
* const markdown = [
|
|
99
|
+
* { id: "hook-1", ... }, // replaces global hook-1
|
|
100
|
+
* { id: "hook-3", ... } // new hook
|
|
101
|
+
* ]
|
|
102
|
+
* mergeHookArrays(global, markdown)
|
|
103
|
+
* // Returns: [{ id: "hook-1", ... (markdown version) }, { id: "hook-2", ... }, { id: "hook-3", ... }]
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
function mergeHookArrays(globalHooks, markdownHooks) {
|
|
107
|
+
// Create a map of markdown hooks by ID for quick lookup
|
|
108
|
+
const markdownMap = new Map();
|
|
109
|
+
const markdownIds = new Set();
|
|
110
|
+
for (const hook of markdownHooks) {
|
|
111
|
+
markdownMap.set(hook.id, hook);
|
|
112
|
+
markdownIds.add(hook.id);
|
|
113
|
+
}
|
|
114
|
+
// Start with global hooks, replacing those that appear in markdown
|
|
115
|
+
const result = [];
|
|
116
|
+
const processedIds = new Set();
|
|
117
|
+
for (const hook of globalHooks) {
|
|
118
|
+
if (markdownMap.has(hook.id)) {
|
|
119
|
+
// Replace with markdown version
|
|
120
|
+
result.push(markdownMap.get(hook.id));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Keep global hook
|
|
124
|
+
result.push(hook);
|
|
125
|
+
}
|
|
126
|
+
processedIds.add(hook.id);
|
|
127
|
+
}
|
|
128
|
+
// Add markdown hooks that weren't replacements
|
|
129
|
+
for (const hook of markdownHooks) {
|
|
130
|
+
if (!processedIds.has(hook.id)) {
|
|
131
|
+
result.push(hook);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Merge global and markdown configs with proper precedence
|
|
138
|
+
*
|
|
139
|
+
* Implements the precedence rules from PRD section 5.3:
|
|
140
|
+
* 1. Start with global hooks
|
|
141
|
+
* 2. Markdown hooks with same `id` replace global hooks (no error)
|
|
142
|
+
* 3. Markdown hooks with unique `id` are added
|
|
143
|
+
* 4. Duplicate IDs within same source are errors
|
|
144
|
+
*
|
|
145
|
+
* Returns both the merged config and any validation errors found.
|
|
146
|
+
* Errors are returned but don't prevent merging - the caller can decide
|
|
147
|
+
* how to handle them.
|
|
148
|
+
*
|
|
149
|
+
* @param global - Global configuration from opencode.json
|
|
150
|
+
* @param markdown - Markdown configuration from agent/command .md file
|
|
151
|
+
* @returns Object with merged config and validation errors
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const global = {
|
|
156
|
+
* tool: [{ id: "hook-1", when: { phase: "after" }, run: "echo global" }],
|
|
157
|
+
* session: []
|
|
158
|
+
* }
|
|
159
|
+
* const markdown = {
|
|
160
|
+
* tool: [{ id: "hook-1", when: { phase: "after" }, run: "echo markdown" }],
|
|
161
|
+
* session: []
|
|
162
|
+
* }
|
|
163
|
+
* const result = mergeConfigs(global, markdown)
|
|
164
|
+
* // result.config.tool[0].run === "echo markdown" (markdown replaced global)
|
|
165
|
+
* // result.errors === [] (no duplicates)
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function mergeConfigs(global, markdown) {
|
|
169
|
+
const errors = [];
|
|
170
|
+
// Validate global config for duplicates
|
|
171
|
+
const globalErrors = validateConfigForDuplicates(global, "global");
|
|
172
|
+
errors.push(...globalErrors);
|
|
173
|
+
// Validate markdown config for duplicates
|
|
174
|
+
const markdownErrors = validateConfigForDuplicates(markdown, "markdown");
|
|
175
|
+
errors.push(...markdownErrors);
|
|
176
|
+
// Merge tool hooks
|
|
177
|
+
const globalToolHooks = global.tool ?? [];
|
|
178
|
+
const markdownToolHooks = markdown.tool ?? [];
|
|
179
|
+
const mergedToolHooks = mergeHookArrays(globalToolHooks, markdownToolHooks);
|
|
180
|
+
// Merge session hooks
|
|
181
|
+
const globalSessionHooks = global.session ?? [];
|
|
182
|
+
const markdownSessionHooks = markdown.session ?? [];
|
|
183
|
+
const mergedSessionHooks = mergeHookArrays(globalSessionHooks, markdownSessionHooks);
|
|
184
|
+
// Build merged config
|
|
185
|
+
const mergedConfig = {
|
|
186
|
+
tool: mergedToolHooks.length > 0 ? mergedToolHooks : [],
|
|
187
|
+
session: mergedSessionHooks.length > 0 ? mergedSessionHooks : [],
|
|
188
|
+
};
|
|
189
|
+
logger.debug(`Merged configs: ${mergedToolHooks.length} tool hooks, ${mergedSessionHooks.length} session hooks, ${errors.length} errors`);
|
|
190
|
+
return { config: mergedConfig, errors };
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=merge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge.js","sourceRoot":"","sources":["../../src/config/merge.ts"],"names":[],"mappings":"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"}
|