oh-my-opencode 0.1.32 → 0.3.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.ko.md +64 -8
- package/README.md +64 -20
- package/dist/features/hook-message-injector/constants.d.ts +3 -0
- package/dist/features/hook-message-injector/index.d.ts +2 -0
- package/dist/features/hook-message-injector/injector.d.ts +2 -0
- package/dist/features/hook-message-injector/types.d.ts +43 -0
- package/dist/hooks/claude-code-hooks/config-loader.d.ts +12 -0
- package/dist/hooks/claude-code-hooks/config.d.ts +3 -0
- package/dist/hooks/claude-code-hooks/index.d.ts +42 -0
- package/dist/hooks/claude-code-hooks/plugin-config.d.ts +8 -0
- package/dist/hooks/claude-code-hooks/post-tool-use.d.ts +40 -0
- package/dist/hooks/claude-code-hooks/pre-tool-use.d.ts +25 -0
- package/dist/hooks/claude-code-hooks/stop.d.ts +20 -0
- package/dist/hooks/claude-code-hooks/todo.d.ts +12 -0
- package/dist/hooks/claude-code-hooks/tool-input-cache.d.ts +5 -0
- package/dist/hooks/claude-code-hooks/transcript.d.ts +38 -0
- package/dist/hooks/claude-code-hooks/types.d.ts +166 -0
- package/dist/hooks/claude-code-hooks/user-prompt-submit.d.ts +22 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/index.js +1475 -307
- package/dist/shared/hook-disabled.d.ts +2 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/pattern-matcher.d.ts +3 -0
- package/dist/shared/snake-case.d.ts +4 -0
- package/dist/shared/tool-name.d.ts +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1989,10 +1989,166 @@ function createThinkModeHook() {
|
|
|
1989
1989
|
}
|
|
1990
1990
|
};
|
|
1991
1991
|
}
|
|
1992
|
-
// src/
|
|
1993
|
-
import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
|
|
1992
|
+
// src/hooks/claude-code-hooks/config.ts
|
|
1994
1993
|
import { homedir as homedir2 } from "os";
|
|
1995
|
-
import { join as join8
|
|
1994
|
+
import { join as join8 } from "path";
|
|
1995
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1996
|
+
function normalizeHookMatcher(raw) {
|
|
1997
|
+
return {
|
|
1998
|
+
matcher: raw.matcher ?? raw.pattern ?? "*",
|
|
1999
|
+
hooks: raw.hooks
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function normalizeHooksConfig(raw) {
|
|
2003
|
+
const result = {};
|
|
2004
|
+
const eventTypes = [
|
|
2005
|
+
"PreToolUse",
|
|
2006
|
+
"PostToolUse",
|
|
2007
|
+
"UserPromptSubmit",
|
|
2008
|
+
"Stop"
|
|
2009
|
+
];
|
|
2010
|
+
for (const eventType of eventTypes) {
|
|
2011
|
+
if (raw[eventType]) {
|
|
2012
|
+
result[eventType] = raw[eventType].map(normalizeHookMatcher);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return result;
|
|
2016
|
+
}
|
|
2017
|
+
function getClaudeSettingsPaths(customPath) {
|
|
2018
|
+
const home = homedir2();
|
|
2019
|
+
const paths = [
|
|
2020
|
+
join8(home, ".claude", "settings.json"),
|
|
2021
|
+
join8(process.cwd(), ".claude", "settings.json"),
|
|
2022
|
+
join8(process.cwd(), ".claude", "settings.local.json")
|
|
2023
|
+
];
|
|
2024
|
+
if (customPath && existsSync7(customPath)) {
|
|
2025
|
+
paths.unshift(customPath);
|
|
2026
|
+
}
|
|
2027
|
+
return paths;
|
|
2028
|
+
}
|
|
2029
|
+
function mergeHooksConfig(base, override) {
|
|
2030
|
+
const result = { ...base };
|
|
2031
|
+
const eventTypes = [
|
|
2032
|
+
"PreToolUse",
|
|
2033
|
+
"PostToolUse",
|
|
2034
|
+
"UserPromptSubmit",
|
|
2035
|
+
"Stop"
|
|
2036
|
+
];
|
|
2037
|
+
for (const eventType of eventTypes) {
|
|
2038
|
+
if (override[eventType]) {
|
|
2039
|
+
result[eventType] = [...base[eventType] || [], ...override[eventType]];
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
async function loadClaudeHooksConfig(customSettingsPath) {
|
|
2045
|
+
const paths = getClaudeSettingsPaths(customSettingsPath);
|
|
2046
|
+
let mergedConfig = {};
|
|
2047
|
+
for (const settingsPath of paths) {
|
|
2048
|
+
if (existsSync7(settingsPath)) {
|
|
2049
|
+
try {
|
|
2050
|
+
const content = await Bun.file(settingsPath).text();
|
|
2051
|
+
const settings = JSON.parse(content);
|
|
2052
|
+
if (settings.hooks) {
|
|
2053
|
+
const normalizedHooks = normalizeHooksConfig(settings.hooks);
|
|
2054
|
+
mergedConfig = mergeHooksConfig(mergedConfig, normalizedHooks);
|
|
2055
|
+
}
|
|
2056
|
+
} catch {
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
return Object.keys(mergedConfig).length > 0 ? mergedConfig : null;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// src/hooks/claude-code-hooks/config-loader.ts
|
|
2065
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2066
|
+
import { homedir as homedir3 } from "os";
|
|
2067
|
+
import { join as join10 } from "path";
|
|
2068
|
+
|
|
2069
|
+
// src/shared/logger.ts
|
|
2070
|
+
import * as fs3 from "fs";
|
|
2071
|
+
import * as os2 from "os";
|
|
2072
|
+
import * as path2 from "path";
|
|
2073
|
+
var logFile = path2.join(os2.tmpdir(), "oh-my-opencode.log");
|
|
2074
|
+
function log(message, data) {
|
|
2075
|
+
try {
|
|
2076
|
+
const timestamp = new Date().toISOString();
|
|
2077
|
+
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
2078
|
+
`;
|
|
2079
|
+
fs3.appendFileSync(logFile, logEntry);
|
|
2080
|
+
} catch {}
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
// src/hooks/claude-code-hooks/config-loader.ts
|
|
2084
|
+
var USER_CONFIG_PATH = join10(homedir3(), ".config", "opencode", "opencode-cc-plugin.json");
|
|
2085
|
+
function getProjectConfigPath() {
|
|
2086
|
+
return join10(process.cwd(), ".opencode", "opencode-cc-plugin.json");
|
|
2087
|
+
}
|
|
2088
|
+
async function loadConfigFromPath(path3) {
|
|
2089
|
+
if (!existsSync8(path3)) {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
try {
|
|
2093
|
+
const content = await Bun.file(path3).text();
|
|
2094
|
+
return JSON.parse(content);
|
|
2095
|
+
} catch (error) {
|
|
2096
|
+
log("Failed to load config", { path: path3, error });
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function mergeDisabledHooks(base, override) {
|
|
2101
|
+
if (!override)
|
|
2102
|
+
return base ?? {};
|
|
2103
|
+
if (!base)
|
|
2104
|
+
return override;
|
|
2105
|
+
return {
|
|
2106
|
+
Stop: override.Stop ?? base.Stop,
|
|
2107
|
+
PreToolUse: override.PreToolUse ?? base.PreToolUse,
|
|
2108
|
+
PostToolUse: override.PostToolUse ?? base.PostToolUse,
|
|
2109
|
+
UserPromptSubmit: override.UserPromptSubmit ?? base.UserPromptSubmit
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
async function loadPluginExtendedConfig() {
|
|
2113
|
+
const userConfig = await loadConfigFromPath(USER_CONFIG_PATH);
|
|
2114
|
+
const projectConfig = await loadConfigFromPath(getProjectConfigPath());
|
|
2115
|
+
const merged = {
|
|
2116
|
+
disabledHooks: mergeDisabledHooks(userConfig?.disabledHooks, projectConfig?.disabledHooks)
|
|
2117
|
+
};
|
|
2118
|
+
if (userConfig || projectConfig) {
|
|
2119
|
+
log("Plugin extended config loaded", {
|
|
2120
|
+
userConfigExists: userConfig !== null,
|
|
2121
|
+
projectConfigExists: projectConfig !== null,
|
|
2122
|
+
mergedDisabledHooks: merged.disabledHooks
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
return merged;
|
|
2126
|
+
}
|
|
2127
|
+
var regexCache = new Map;
|
|
2128
|
+
function getRegex(pattern) {
|
|
2129
|
+
let regex = regexCache.get(pattern);
|
|
2130
|
+
if (!regex) {
|
|
2131
|
+
try {
|
|
2132
|
+
regex = new RegExp(pattern);
|
|
2133
|
+
regexCache.set(pattern, regex);
|
|
2134
|
+
} catch {
|
|
2135
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2136
|
+
regexCache.set(pattern, regex);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return regex;
|
|
2140
|
+
}
|
|
2141
|
+
function isHookCommandDisabled(eventType, command, config) {
|
|
2142
|
+
if (!config?.disabledHooks)
|
|
2143
|
+
return false;
|
|
2144
|
+
const patterns = config.disabledHooks[eventType];
|
|
2145
|
+
if (!patterns || patterns.length === 0)
|
|
2146
|
+
return false;
|
|
2147
|
+
return patterns.some((pattern) => {
|
|
2148
|
+
const regex = getRegex(pattern);
|
|
2149
|
+
return regex.test(command);
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
1996
2152
|
|
|
1997
2153
|
// src/shared/frontmatter.ts
|
|
1998
2154
|
function parseFrontmatter(content) {
|
|
@@ -2017,31 +2173,1181 @@ function parseFrontmatter(content) {
|
|
|
2017
2173
|
data[key] = value;
|
|
2018
2174
|
}
|
|
2019
2175
|
}
|
|
2020
|
-
return { data, body };
|
|
2176
|
+
return { data, body };
|
|
2177
|
+
}
|
|
2178
|
+
// src/shared/command-executor.ts
|
|
2179
|
+
import { spawn as spawn3 } from "child_process";
|
|
2180
|
+
import { exec } from "child_process";
|
|
2181
|
+
import { promisify } from "util";
|
|
2182
|
+
import { existsSync as existsSync9 } from "fs";
|
|
2183
|
+
var DEFAULT_ZSH_PATHS = ["/bin/zsh", "/usr/bin/zsh", "/usr/local/bin/zsh"];
|
|
2184
|
+
function findZshPath(customZshPath) {
|
|
2185
|
+
if (customZshPath && existsSync9(customZshPath)) {
|
|
2186
|
+
return customZshPath;
|
|
2187
|
+
}
|
|
2188
|
+
for (const path3 of DEFAULT_ZSH_PATHS) {
|
|
2189
|
+
if (existsSync9(path3)) {
|
|
2190
|
+
return path3;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return null;
|
|
2194
|
+
}
|
|
2195
|
+
var execAsync = promisify(exec);
|
|
2196
|
+
async function executeHookCommand(command, stdin, cwd, options) {
|
|
2197
|
+
const home = process.env.HOME ?? "";
|
|
2198
|
+
let expandedCommand = command.replace(/^~(?=\/|$)/g, home).replace(/\s~(?=\/)/g, ` ${home}`).replace(/\$CLAUDE_PROJECT_DIR/g, cwd).replace(/\$\{CLAUDE_PROJECT_DIR\}/g, cwd);
|
|
2199
|
+
let finalCommand = expandedCommand;
|
|
2200
|
+
if (options?.forceZsh) {
|
|
2201
|
+
const zshPath = options.zshPath || findZshPath();
|
|
2202
|
+
if (zshPath) {
|
|
2203
|
+
const escapedCommand = expandedCommand.replace(/'/g, "'\\''");
|
|
2204
|
+
finalCommand = `${zshPath} -lc '${escapedCommand}'`;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return new Promise((resolve2) => {
|
|
2208
|
+
const proc = spawn3(finalCommand, {
|
|
2209
|
+
cwd,
|
|
2210
|
+
shell: true,
|
|
2211
|
+
env: { ...process.env, HOME: home, CLAUDE_PROJECT_DIR: cwd }
|
|
2212
|
+
});
|
|
2213
|
+
let stdout = "";
|
|
2214
|
+
let stderr = "";
|
|
2215
|
+
proc.stdout?.on("data", (data) => {
|
|
2216
|
+
stdout += data.toString();
|
|
2217
|
+
});
|
|
2218
|
+
proc.stderr?.on("data", (data) => {
|
|
2219
|
+
stderr += data.toString();
|
|
2220
|
+
});
|
|
2221
|
+
proc.stdin?.write(stdin);
|
|
2222
|
+
proc.stdin?.end();
|
|
2223
|
+
proc.on("close", (code) => {
|
|
2224
|
+
resolve2({
|
|
2225
|
+
exitCode: code ?? 0,
|
|
2226
|
+
stdout: stdout.trim(),
|
|
2227
|
+
stderr: stderr.trim()
|
|
2228
|
+
});
|
|
2229
|
+
});
|
|
2230
|
+
proc.on("error", (err) => {
|
|
2231
|
+
resolve2({
|
|
2232
|
+
exitCode: 1,
|
|
2233
|
+
stderr: err.message
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
async function executeCommand(command) {
|
|
2239
|
+
try {
|
|
2240
|
+
const { stdout, stderr } = await execAsync(command);
|
|
2241
|
+
const out = stdout?.toString().trim() ?? "";
|
|
2242
|
+
const err = stderr?.toString().trim() ?? "";
|
|
2243
|
+
if (err) {
|
|
2244
|
+
if (out) {
|
|
2245
|
+
return `${out}
|
|
2246
|
+
[stderr: ${err}]`;
|
|
2247
|
+
}
|
|
2248
|
+
return `[stderr: ${err}]`;
|
|
2249
|
+
}
|
|
2250
|
+
return out;
|
|
2251
|
+
} catch (error) {
|
|
2252
|
+
const e = error;
|
|
2253
|
+
const stdout = e?.stdout?.toString().trim() ?? "";
|
|
2254
|
+
const stderr = e?.stderr?.toString().trim() ?? "";
|
|
2255
|
+
const errMsg = stderr || e?.message || String(error);
|
|
2256
|
+
if (stdout) {
|
|
2257
|
+
return `${stdout}
|
|
2258
|
+
[stderr: ${errMsg}]`;
|
|
2259
|
+
}
|
|
2260
|
+
return `[stderr: ${errMsg}]`;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
var COMMAND_PATTERN = /!`([^`]+)`/g;
|
|
2264
|
+
function findCommands(text) {
|
|
2265
|
+
const matches = [];
|
|
2266
|
+
let match;
|
|
2267
|
+
COMMAND_PATTERN.lastIndex = 0;
|
|
2268
|
+
while ((match = COMMAND_PATTERN.exec(text)) !== null) {
|
|
2269
|
+
matches.push({
|
|
2270
|
+
fullMatch: match[0],
|
|
2271
|
+
command: match[1],
|
|
2272
|
+
start: match.index,
|
|
2273
|
+
end: match.index + match[0].length
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
return matches;
|
|
2277
|
+
}
|
|
2278
|
+
async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
|
|
2279
|
+
if (depth >= maxDepth) {
|
|
2280
|
+
return text;
|
|
2281
|
+
}
|
|
2282
|
+
const matches = findCommands(text);
|
|
2283
|
+
if (matches.length === 0) {
|
|
2284
|
+
return text;
|
|
2285
|
+
}
|
|
2286
|
+
const tasks = matches.map((m) => executeCommand(m.command));
|
|
2287
|
+
const results = await Promise.allSettled(tasks);
|
|
2288
|
+
const replacements = new Map;
|
|
2289
|
+
matches.forEach((match, idx) => {
|
|
2290
|
+
const result = results[idx];
|
|
2291
|
+
if (result.status === "rejected") {
|
|
2292
|
+
replacements.set(match.fullMatch, `[error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`);
|
|
2293
|
+
} else {
|
|
2294
|
+
replacements.set(match.fullMatch, result.value);
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
let resolved = text;
|
|
2298
|
+
for (const [pattern, replacement] of replacements.entries()) {
|
|
2299
|
+
resolved = resolved.split(pattern).join(replacement);
|
|
2300
|
+
}
|
|
2301
|
+
if (findCommands(resolved).length > 0) {
|
|
2302
|
+
return resolveCommandsInText(resolved, depth + 1, maxDepth);
|
|
2303
|
+
}
|
|
2304
|
+
return resolved;
|
|
2305
|
+
}
|
|
2306
|
+
// src/shared/file-reference-resolver.ts
|
|
2307
|
+
import { existsSync as existsSync10, readFileSync as readFileSync4, statSync } from "fs";
|
|
2308
|
+
import { join as join11, isAbsolute } from "path";
|
|
2309
|
+
var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
|
|
2310
|
+
function findFileReferences(text) {
|
|
2311
|
+
const matches = [];
|
|
2312
|
+
let match;
|
|
2313
|
+
FILE_REFERENCE_PATTERN.lastIndex = 0;
|
|
2314
|
+
while ((match = FILE_REFERENCE_PATTERN.exec(text)) !== null) {
|
|
2315
|
+
matches.push({
|
|
2316
|
+
fullMatch: match[0],
|
|
2317
|
+
filePath: match[1],
|
|
2318
|
+
start: match.index,
|
|
2319
|
+
end: match.index + match[0].length
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
return matches;
|
|
2323
|
+
}
|
|
2324
|
+
function resolveFilePath(filePath, cwd) {
|
|
2325
|
+
if (isAbsolute(filePath)) {
|
|
2326
|
+
return filePath;
|
|
2327
|
+
}
|
|
2328
|
+
return join11(cwd, filePath);
|
|
2329
|
+
}
|
|
2330
|
+
function readFileContent(resolvedPath) {
|
|
2331
|
+
if (!existsSync10(resolvedPath)) {
|
|
2332
|
+
return `[file not found: ${resolvedPath}]`;
|
|
2333
|
+
}
|
|
2334
|
+
const stat = statSync(resolvedPath);
|
|
2335
|
+
if (stat.isDirectory()) {
|
|
2336
|
+
return `[cannot read directory: ${resolvedPath}]`;
|
|
2337
|
+
}
|
|
2338
|
+
const content = readFileSync4(resolvedPath, "utf-8");
|
|
2339
|
+
return content;
|
|
2340
|
+
}
|
|
2341
|
+
async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
|
|
2342
|
+
if (depth >= maxDepth) {
|
|
2343
|
+
return text;
|
|
2344
|
+
}
|
|
2345
|
+
const matches = findFileReferences(text);
|
|
2346
|
+
if (matches.length === 0) {
|
|
2347
|
+
return text;
|
|
2348
|
+
}
|
|
2349
|
+
const replacements = new Map;
|
|
2350
|
+
for (const match of matches) {
|
|
2351
|
+
const resolvedPath = resolveFilePath(match.filePath, cwd);
|
|
2352
|
+
const content = readFileContent(resolvedPath);
|
|
2353
|
+
replacements.set(match.fullMatch, content);
|
|
2354
|
+
}
|
|
2355
|
+
let resolved = text;
|
|
2356
|
+
for (const [pattern, replacement] of replacements.entries()) {
|
|
2357
|
+
resolved = resolved.split(pattern).join(replacement);
|
|
2358
|
+
}
|
|
2359
|
+
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
|
2360
|
+
return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
|
|
2361
|
+
}
|
|
2362
|
+
return resolved;
|
|
2363
|
+
}
|
|
2364
|
+
// src/shared/model-sanitizer.ts
|
|
2365
|
+
function sanitizeModelField(_model) {
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
// src/shared/snake-case.ts
|
|
2369
|
+
function camelToSnake(str) {
|
|
2370
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
2371
|
+
}
|
|
2372
|
+
function isPlainObject(value) {
|
|
2373
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2374
|
+
}
|
|
2375
|
+
function objectToSnakeCase(obj, deep = true) {
|
|
2376
|
+
const result = {};
|
|
2377
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2378
|
+
const snakeKey = camelToSnake(key);
|
|
2379
|
+
if (deep && isPlainObject(value)) {
|
|
2380
|
+
result[snakeKey] = objectToSnakeCase(value, true);
|
|
2381
|
+
} else if (deep && Array.isArray(value)) {
|
|
2382
|
+
result[snakeKey] = value.map((item) => isPlainObject(item) ? objectToSnakeCase(item, true) : item);
|
|
2383
|
+
} else {
|
|
2384
|
+
result[snakeKey] = value;
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return result;
|
|
2388
|
+
}
|
|
2389
|
+
// src/shared/tool-name.ts
|
|
2390
|
+
var SPECIAL_TOOL_MAPPINGS = {
|
|
2391
|
+
webfetch: "WebFetch",
|
|
2392
|
+
websearch: "WebSearch",
|
|
2393
|
+
todoread: "TodoRead",
|
|
2394
|
+
todowrite: "TodoWrite"
|
|
2395
|
+
};
|
|
2396
|
+
function toPascalCase(str) {
|
|
2397
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
2398
|
+
}
|
|
2399
|
+
function transformToolName(toolName) {
|
|
2400
|
+
const lower = toolName.toLowerCase();
|
|
2401
|
+
if (lower in SPECIAL_TOOL_MAPPINGS) {
|
|
2402
|
+
return SPECIAL_TOOL_MAPPINGS[lower];
|
|
2403
|
+
}
|
|
2404
|
+
if (toolName.includes("-") || toolName.includes("_")) {
|
|
2405
|
+
return toPascalCase(toolName);
|
|
2406
|
+
}
|
|
2407
|
+
return toolName.charAt(0).toUpperCase() + toolName.slice(1);
|
|
2408
|
+
}
|
|
2409
|
+
// src/shared/pattern-matcher.ts
|
|
2410
|
+
function matchesToolMatcher(toolName, matcher) {
|
|
2411
|
+
if (!matcher) {
|
|
2412
|
+
return true;
|
|
2413
|
+
}
|
|
2414
|
+
const patterns = matcher.split("|").map((p) => p.trim());
|
|
2415
|
+
return patterns.some((p) => {
|
|
2416
|
+
if (p.includes("*")) {
|
|
2417
|
+
const regex = new RegExp(`^${p.replace(/\*/g, ".*")}$`, "i");
|
|
2418
|
+
return regex.test(toolName);
|
|
2419
|
+
}
|
|
2420
|
+
return p.toLowerCase() === toolName.toLowerCase();
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
function findMatchingHooks(config, eventName, toolName) {
|
|
2424
|
+
const hookMatchers = config[eventName];
|
|
2425
|
+
if (!hookMatchers)
|
|
2426
|
+
return [];
|
|
2427
|
+
return hookMatchers.filter((hookMatcher) => {
|
|
2428
|
+
if (!toolName)
|
|
2429
|
+
return true;
|
|
2430
|
+
return matchesToolMatcher(toolName, hookMatcher.matcher);
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
// src/shared/hook-disabled.ts
|
|
2434
|
+
function isHookDisabled(config, hookType) {
|
|
2435
|
+
const { disabledHooks } = config;
|
|
2436
|
+
if (disabledHooks === undefined) {
|
|
2437
|
+
return false;
|
|
2438
|
+
}
|
|
2439
|
+
if (disabledHooks === true) {
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
if (Array.isArray(disabledHooks)) {
|
|
2443
|
+
return disabledHooks.includes(hookType);
|
|
2444
|
+
}
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
// src/hooks/claude-code-hooks/plugin-config.ts
|
|
2448
|
+
var DEFAULT_CONFIG = {
|
|
2449
|
+
forceZsh: true,
|
|
2450
|
+
zshPath: "/bin/zsh"
|
|
2451
|
+
};
|
|
2452
|
+
|
|
2453
|
+
// src/hooks/claude-code-hooks/pre-tool-use.ts
|
|
2454
|
+
function buildInputLines(toolInput) {
|
|
2455
|
+
return Object.entries(toolInput).slice(0, 3).map(([key, val]) => {
|
|
2456
|
+
const valStr = String(val).slice(0, 40);
|
|
2457
|
+
return ` ${key}: ${valStr}${String(val).length > 40 ? "..." : ""}`;
|
|
2458
|
+
}).join(`
|
|
2459
|
+
`);
|
|
2460
|
+
}
|
|
2461
|
+
async function executePreToolUseHooks(ctx, config, extendedConfig) {
|
|
2462
|
+
if (!config) {
|
|
2463
|
+
return { decision: "allow" };
|
|
2464
|
+
}
|
|
2465
|
+
const transformedToolName = transformToolName(ctx.toolName);
|
|
2466
|
+
const matchers = findMatchingHooks(config, "PreToolUse", transformedToolName);
|
|
2467
|
+
if (matchers.length === 0) {
|
|
2468
|
+
return { decision: "allow" };
|
|
2469
|
+
}
|
|
2470
|
+
const stdinData = {
|
|
2471
|
+
session_id: ctx.sessionId,
|
|
2472
|
+
transcript_path: ctx.transcriptPath,
|
|
2473
|
+
cwd: ctx.cwd,
|
|
2474
|
+
permission_mode: ctx.permissionMode ?? "bypassPermissions",
|
|
2475
|
+
hook_event_name: "PreToolUse",
|
|
2476
|
+
tool_name: transformedToolName,
|
|
2477
|
+
tool_input: objectToSnakeCase(ctx.toolInput),
|
|
2478
|
+
tool_use_id: ctx.toolUseId,
|
|
2479
|
+
hook_source: "opencode-plugin"
|
|
2480
|
+
};
|
|
2481
|
+
const startTime = Date.now();
|
|
2482
|
+
let firstHookName;
|
|
2483
|
+
const inputLines = buildInputLines(ctx.toolInput);
|
|
2484
|
+
for (const matcher of matchers) {
|
|
2485
|
+
for (const hook of matcher.hooks) {
|
|
2486
|
+
if (hook.type !== "command")
|
|
2487
|
+
continue;
|
|
2488
|
+
if (isHookCommandDisabled("PreToolUse", hook.command, extendedConfig ?? null)) {
|
|
2489
|
+
log("PreToolUse hook command skipped (disabled by config)", { command: hook.command, toolName: ctx.toolName });
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
const hookName = hook.command.split("/").pop() || hook.command;
|
|
2493
|
+
if (!firstHookName)
|
|
2494
|
+
firstHookName = hookName;
|
|
2495
|
+
const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
|
|
2496
|
+
if (result.exitCode === 2) {
|
|
2497
|
+
return {
|
|
2498
|
+
decision: "deny",
|
|
2499
|
+
reason: result.stderr || result.stdout || "Hook blocked the operation",
|
|
2500
|
+
elapsedMs: Date.now() - startTime,
|
|
2501
|
+
hookName: firstHookName,
|
|
2502
|
+
toolName: transformedToolName,
|
|
2503
|
+
inputLines
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
if (result.exitCode === 1) {
|
|
2507
|
+
return {
|
|
2508
|
+
decision: "ask",
|
|
2509
|
+
reason: result.stderr || result.stdout,
|
|
2510
|
+
elapsedMs: Date.now() - startTime,
|
|
2511
|
+
hookName: firstHookName,
|
|
2512
|
+
toolName: transformedToolName,
|
|
2513
|
+
inputLines
|
|
2514
|
+
};
|
|
2515
|
+
}
|
|
2516
|
+
if (result.stdout) {
|
|
2517
|
+
try {
|
|
2518
|
+
const output = JSON.parse(result.stdout);
|
|
2519
|
+
let decision;
|
|
2520
|
+
let reason;
|
|
2521
|
+
let modifiedInput;
|
|
2522
|
+
if (output.hookSpecificOutput?.permissionDecision) {
|
|
2523
|
+
decision = output.hookSpecificOutput.permissionDecision;
|
|
2524
|
+
reason = output.hookSpecificOutput.permissionDecisionReason;
|
|
2525
|
+
modifiedInput = output.hookSpecificOutput.updatedInput;
|
|
2526
|
+
} else if (output.decision) {
|
|
2527
|
+
const legacyDecision = output.decision;
|
|
2528
|
+
if (legacyDecision === "approve" || legacyDecision === "allow") {
|
|
2529
|
+
decision = "allow";
|
|
2530
|
+
} else if (legacyDecision === "block" || legacyDecision === "deny") {
|
|
2531
|
+
decision = "deny";
|
|
2532
|
+
} else if (legacyDecision === "ask") {
|
|
2533
|
+
decision = "ask";
|
|
2534
|
+
}
|
|
2535
|
+
reason = output.reason;
|
|
2536
|
+
}
|
|
2537
|
+
const hasCommonFields = output.continue !== undefined || output.stopReason !== undefined || output.suppressOutput !== undefined || output.systemMessage !== undefined;
|
|
2538
|
+
if (decision || hasCommonFields) {
|
|
2539
|
+
return {
|
|
2540
|
+
decision: decision ?? "allow",
|
|
2541
|
+
reason,
|
|
2542
|
+
modifiedInput,
|
|
2543
|
+
elapsedMs: Date.now() - startTime,
|
|
2544
|
+
hookName: firstHookName,
|
|
2545
|
+
toolName: transformedToolName,
|
|
2546
|
+
inputLines,
|
|
2547
|
+
continue: output.continue,
|
|
2548
|
+
stopReason: output.stopReason,
|
|
2549
|
+
suppressOutput: output.suppressOutput,
|
|
2550
|
+
systemMessage: output.systemMessage
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
} catch {}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
return { decision: "allow" };
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
// src/hooks/claude-code-hooks/transcript.ts
|
|
2561
|
+
import { join as join12 } from "path";
|
|
2562
|
+
import { mkdirSync as mkdirSync4, appendFileSync as appendFileSync5, existsSync as existsSync11, writeFileSync as writeFileSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
2563
|
+
import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
|
|
2564
|
+
import { randomUUID } from "crypto";
|
|
2565
|
+
var TRANSCRIPT_DIR = join12(homedir4(), ".claude", "transcripts");
|
|
2566
|
+
function getTranscriptPath(sessionId) {
|
|
2567
|
+
return join12(TRANSCRIPT_DIR, `${sessionId}.jsonl`);
|
|
2568
|
+
}
|
|
2569
|
+
function ensureTranscriptDir() {
|
|
2570
|
+
if (!existsSync11(TRANSCRIPT_DIR)) {
|
|
2571
|
+
mkdirSync4(TRANSCRIPT_DIR, { recursive: true });
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
function appendTranscriptEntry(sessionId, entry) {
|
|
2575
|
+
ensureTranscriptDir();
|
|
2576
|
+
const path3 = getTranscriptPath(sessionId);
|
|
2577
|
+
const line = JSON.stringify(entry) + `
|
|
2578
|
+
`;
|
|
2579
|
+
appendFileSync5(path3, line);
|
|
2580
|
+
}
|
|
2581
|
+
function recordToolUse(sessionId, toolName, toolInput) {
|
|
2582
|
+
appendTranscriptEntry(sessionId, {
|
|
2583
|
+
type: "tool_use",
|
|
2584
|
+
timestamp: new Date().toISOString(),
|
|
2585
|
+
tool_name: toolName,
|
|
2586
|
+
tool_input: toolInput
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
function recordToolResult(sessionId, toolName, toolInput, toolOutput) {
|
|
2590
|
+
appendTranscriptEntry(sessionId, {
|
|
2591
|
+
type: "tool_result",
|
|
2592
|
+
timestamp: new Date().toISOString(),
|
|
2593
|
+
tool_name: toolName,
|
|
2594
|
+
tool_input: toolInput,
|
|
2595
|
+
tool_output: toolOutput
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
function recordUserMessage(sessionId, content) {
|
|
2599
|
+
appendTranscriptEntry(sessionId, {
|
|
2600
|
+
type: "user",
|
|
2601
|
+
timestamp: new Date().toISOString(),
|
|
2602
|
+
content
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
async function buildTranscriptFromSession(client, sessionId, directory, currentToolName, currentToolInput) {
|
|
2606
|
+
try {
|
|
2607
|
+
const response = await client.session.messages({
|
|
2608
|
+
path: { id: sessionId },
|
|
2609
|
+
query: { directory }
|
|
2610
|
+
});
|
|
2611
|
+
const messages = response["200"] ?? response.data ?? (Array.isArray(response) ? response : []);
|
|
2612
|
+
const entries = [];
|
|
2613
|
+
if (Array.isArray(messages)) {
|
|
2614
|
+
for (const msg of messages) {
|
|
2615
|
+
if (msg.info?.role !== "assistant")
|
|
2616
|
+
continue;
|
|
2617
|
+
for (const part of msg.parts || []) {
|
|
2618
|
+
if (part.type !== "tool")
|
|
2619
|
+
continue;
|
|
2620
|
+
if (part.state?.status !== "completed")
|
|
2621
|
+
continue;
|
|
2622
|
+
if (!part.state?.input)
|
|
2623
|
+
continue;
|
|
2624
|
+
const rawToolName = part.tool;
|
|
2625
|
+
const toolName = transformToolName(rawToolName);
|
|
2626
|
+
const entry = {
|
|
2627
|
+
type: "assistant",
|
|
2628
|
+
message: {
|
|
2629
|
+
role: "assistant",
|
|
2630
|
+
content: [
|
|
2631
|
+
{
|
|
2632
|
+
type: "tool_use",
|
|
2633
|
+
name: toolName,
|
|
2634
|
+
input: part.state.input
|
|
2635
|
+
}
|
|
2636
|
+
]
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
entries.push(JSON.stringify(entry));
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
const currentEntry = {
|
|
2644
|
+
type: "assistant",
|
|
2645
|
+
message: {
|
|
2646
|
+
role: "assistant",
|
|
2647
|
+
content: [
|
|
2648
|
+
{
|
|
2649
|
+
type: "tool_use",
|
|
2650
|
+
name: transformToolName(currentToolName),
|
|
2651
|
+
input: currentToolInput
|
|
2652
|
+
}
|
|
2653
|
+
]
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
entries.push(JSON.stringify(currentEntry));
|
|
2657
|
+
const tempPath = join12(tmpdir2(), `opencode-transcript-${sessionId}-${randomUUID()}.jsonl`);
|
|
2658
|
+
writeFileSync3(tempPath, entries.join(`
|
|
2659
|
+
`) + `
|
|
2660
|
+
`);
|
|
2661
|
+
return tempPath;
|
|
2662
|
+
} catch {
|
|
2663
|
+
try {
|
|
2664
|
+
const currentEntry = {
|
|
2665
|
+
type: "assistant",
|
|
2666
|
+
message: {
|
|
2667
|
+
role: "assistant",
|
|
2668
|
+
content: [
|
|
2669
|
+
{
|
|
2670
|
+
type: "tool_use",
|
|
2671
|
+
name: transformToolName(currentToolName),
|
|
2672
|
+
input: currentToolInput
|
|
2673
|
+
}
|
|
2674
|
+
]
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
const tempPath = join12(tmpdir2(), `opencode-transcript-${sessionId}-${randomUUID()}.jsonl`);
|
|
2678
|
+
writeFileSync3(tempPath, JSON.stringify(currentEntry) + `
|
|
2679
|
+
`);
|
|
2680
|
+
return tempPath;
|
|
2681
|
+
} catch {
|
|
2682
|
+
return null;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
function deleteTempTranscript(path3) {
|
|
2687
|
+
if (!path3)
|
|
2688
|
+
return;
|
|
2689
|
+
try {
|
|
2690
|
+
unlinkSync4(path3);
|
|
2691
|
+
} catch {}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/hooks/claude-code-hooks/post-tool-use.ts
|
|
2695
|
+
async function executePostToolUseHooks(ctx, config, extendedConfig) {
|
|
2696
|
+
if (!config) {
|
|
2697
|
+
return { block: false };
|
|
2698
|
+
}
|
|
2699
|
+
const transformedToolName = transformToolName(ctx.toolName);
|
|
2700
|
+
const matchers = findMatchingHooks(config, "PostToolUse", transformedToolName);
|
|
2701
|
+
if (matchers.length === 0) {
|
|
2702
|
+
return { block: false };
|
|
2703
|
+
}
|
|
2704
|
+
let tempTranscriptPath = null;
|
|
2705
|
+
try {
|
|
2706
|
+
if (ctx.client) {
|
|
2707
|
+
tempTranscriptPath = await buildTranscriptFromSession(ctx.client, ctx.sessionId, ctx.cwd, ctx.toolName, ctx.toolInput);
|
|
2708
|
+
}
|
|
2709
|
+
const stdinData = {
|
|
2710
|
+
session_id: ctx.sessionId,
|
|
2711
|
+
transcript_path: tempTranscriptPath ?? ctx.transcriptPath,
|
|
2712
|
+
cwd: ctx.cwd,
|
|
2713
|
+
permission_mode: ctx.permissionMode ?? "bypassPermissions",
|
|
2714
|
+
hook_event_name: "PostToolUse",
|
|
2715
|
+
tool_name: transformedToolName,
|
|
2716
|
+
tool_input: objectToSnakeCase(ctx.toolInput),
|
|
2717
|
+
tool_response: objectToSnakeCase(ctx.toolOutput),
|
|
2718
|
+
tool_use_id: ctx.toolUseId,
|
|
2719
|
+
hook_source: "opencode-plugin"
|
|
2720
|
+
};
|
|
2721
|
+
const messages = [];
|
|
2722
|
+
const warnings = [];
|
|
2723
|
+
let firstHookName;
|
|
2724
|
+
const startTime = Date.now();
|
|
2725
|
+
for (const matcher of matchers) {
|
|
2726
|
+
for (const hook of matcher.hooks) {
|
|
2727
|
+
if (hook.type !== "command")
|
|
2728
|
+
continue;
|
|
2729
|
+
if (isHookCommandDisabled("PostToolUse", hook.command, extendedConfig ?? null)) {
|
|
2730
|
+
log("PostToolUse hook command skipped (disabled by config)", { command: hook.command, toolName: ctx.toolName });
|
|
2731
|
+
continue;
|
|
2732
|
+
}
|
|
2733
|
+
const hookName = hook.command.split("/").pop() || hook.command;
|
|
2734
|
+
if (!firstHookName)
|
|
2735
|
+
firstHookName = hookName;
|
|
2736
|
+
const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
|
|
2737
|
+
if (result.stdout) {
|
|
2738
|
+
messages.push(result.stdout);
|
|
2739
|
+
}
|
|
2740
|
+
if (result.exitCode === 2) {
|
|
2741
|
+
if (result.stderr) {
|
|
2742
|
+
warnings.push(`[${hookName}]
|
|
2743
|
+
${result.stderr.trim()}`);
|
|
2744
|
+
}
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
if (result.exitCode === 0 && result.stdout) {
|
|
2748
|
+
try {
|
|
2749
|
+
const output = JSON.parse(result.stdout);
|
|
2750
|
+
if (output.decision === "block") {
|
|
2751
|
+
return {
|
|
2752
|
+
block: true,
|
|
2753
|
+
reason: output.reason || result.stderr,
|
|
2754
|
+
message: messages.join(`
|
|
2755
|
+
`),
|
|
2756
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
2757
|
+
elapsedMs: Date.now() - startTime,
|
|
2758
|
+
hookName: firstHookName,
|
|
2759
|
+
toolName: transformedToolName,
|
|
2760
|
+
additionalContext: output.hookSpecificOutput?.additionalContext,
|
|
2761
|
+
continue: output.continue,
|
|
2762
|
+
stopReason: output.stopReason,
|
|
2763
|
+
suppressOutput: output.suppressOutput,
|
|
2764
|
+
systemMessage: output.systemMessage
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
if (output.hookSpecificOutput?.additionalContext || output.continue !== undefined || output.systemMessage || output.suppressOutput === true || output.stopReason !== undefined) {
|
|
2768
|
+
return {
|
|
2769
|
+
block: false,
|
|
2770
|
+
message: messages.join(`
|
|
2771
|
+
`),
|
|
2772
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
2773
|
+
elapsedMs: Date.now() - startTime,
|
|
2774
|
+
hookName: firstHookName,
|
|
2775
|
+
toolName: transformedToolName,
|
|
2776
|
+
additionalContext: output.hookSpecificOutput?.additionalContext,
|
|
2777
|
+
continue: output.continue,
|
|
2778
|
+
stopReason: output.stopReason,
|
|
2779
|
+
suppressOutput: output.suppressOutput,
|
|
2780
|
+
systemMessage: output.systemMessage
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
} catch {}
|
|
2784
|
+
} else if (result.exitCode !== 0 && result.exitCode !== 2) {
|
|
2785
|
+
try {
|
|
2786
|
+
const output = JSON.parse(result.stdout || "{}");
|
|
2787
|
+
if (output.decision === "block") {
|
|
2788
|
+
return {
|
|
2789
|
+
block: true,
|
|
2790
|
+
reason: output.reason || result.stderr,
|
|
2791
|
+
message: messages.join(`
|
|
2792
|
+
`),
|
|
2793
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
2794
|
+
elapsedMs: Date.now() - startTime,
|
|
2795
|
+
hookName: firstHookName,
|
|
2796
|
+
toolName: transformedToolName,
|
|
2797
|
+
additionalContext: output.hookSpecificOutput?.additionalContext,
|
|
2798
|
+
continue: output.continue,
|
|
2799
|
+
stopReason: output.stopReason,
|
|
2800
|
+
suppressOutput: output.suppressOutput,
|
|
2801
|
+
systemMessage: output.systemMessage
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
} catch {}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
const elapsedMs = Date.now() - startTime;
|
|
2809
|
+
return {
|
|
2810
|
+
block: false,
|
|
2811
|
+
message: messages.length > 0 ? messages.join(`
|
|
2812
|
+
`) : undefined,
|
|
2813
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
2814
|
+
elapsedMs,
|
|
2815
|
+
hookName: firstHookName,
|
|
2816
|
+
toolName: transformedToolName
|
|
2817
|
+
};
|
|
2818
|
+
} finally {
|
|
2819
|
+
deleteTempTranscript(tempTranscriptPath);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/hooks/claude-code-hooks/user-prompt-submit.ts
|
|
2824
|
+
var USER_PROMPT_SUBMIT_TAG_OPEN = "<user-prompt-submit-hook>";
|
|
2825
|
+
var USER_PROMPT_SUBMIT_TAG_CLOSE = "</user-prompt-submit-hook>";
|
|
2826
|
+
async function executeUserPromptSubmitHooks(ctx, config, extendedConfig) {
|
|
2827
|
+
const modifiedParts = ctx.parts;
|
|
2828
|
+
const messages = [];
|
|
2829
|
+
if (ctx.parentSessionId) {
|
|
2830
|
+
return { block: false, modifiedParts, messages };
|
|
2831
|
+
}
|
|
2832
|
+
if (ctx.prompt.includes(USER_PROMPT_SUBMIT_TAG_OPEN) && ctx.prompt.includes(USER_PROMPT_SUBMIT_TAG_CLOSE)) {
|
|
2833
|
+
return { block: false, modifiedParts, messages };
|
|
2834
|
+
}
|
|
2835
|
+
if (!config) {
|
|
2836
|
+
return { block: false, modifiedParts, messages };
|
|
2837
|
+
}
|
|
2838
|
+
const matchers = findMatchingHooks(config, "UserPromptSubmit");
|
|
2839
|
+
if (matchers.length === 0) {
|
|
2840
|
+
return { block: false, modifiedParts, messages };
|
|
2841
|
+
}
|
|
2842
|
+
const stdinData = {
|
|
2843
|
+
session_id: ctx.sessionId,
|
|
2844
|
+
cwd: ctx.cwd,
|
|
2845
|
+
permission_mode: ctx.permissionMode ?? "bypassPermissions",
|
|
2846
|
+
hook_event_name: "UserPromptSubmit",
|
|
2847
|
+
prompt: ctx.prompt,
|
|
2848
|
+
session: { id: ctx.sessionId },
|
|
2849
|
+
hook_source: "opencode-plugin"
|
|
2850
|
+
};
|
|
2851
|
+
for (const matcher of matchers) {
|
|
2852
|
+
for (const hook of matcher.hooks) {
|
|
2853
|
+
if (hook.type !== "command")
|
|
2854
|
+
continue;
|
|
2855
|
+
if (isHookCommandDisabled("UserPromptSubmit", hook.command, extendedConfig ?? null)) {
|
|
2856
|
+
log("UserPromptSubmit hook command skipped (disabled by config)", { command: hook.command });
|
|
2857
|
+
continue;
|
|
2858
|
+
}
|
|
2859
|
+
const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
|
|
2860
|
+
if (result.stdout) {
|
|
2861
|
+
const output = result.stdout.trim();
|
|
2862
|
+
if (output.startsWith(USER_PROMPT_SUBMIT_TAG_OPEN)) {
|
|
2863
|
+
messages.push(output);
|
|
2864
|
+
} else {
|
|
2865
|
+
messages.push(`${USER_PROMPT_SUBMIT_TAG_OPEN}
|
|
2866
|
+
${output}
|
|
2867
|
+
${USER_PROMPT_SUBMIT_TAG_CLOSE}`);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if (result.exitCode !== 0) {
|
|
2871
|
+
try {
|
|
2872
|
+
const output = JSON.parse(result.stdout || "{}");
|
|
2873
|
+
if (output.decision === "block") {
|
|
2874
|
+
return {
|
|
2875
|
+
block: true,
|
|
2876
|
+
reason: output.reason || result.stderr,
|
|
2877
|
+
modifiedParts,
|
|
2878
|
+
messages
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
} catch {}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
return { block: false, modifiedParts, messages };
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/hooks/claude-code-hooks/todo.ts
|
|
2889
|
+
import { join as join13 } from "path";
|
|
2890
|
+
import { homedir as homedir5 } from "os";
|
|
2891
|
+
var TODO_DIR = join13(homedir5(), ".claude", "todos");
|
|
2892
|
+
function getTodoPath(sessionId) {
|
|
2893
|
+
return join13(TODO_DIR, `${sessionId}-agent-${sessionId}.json`);
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
// src/hooks/claude-code-hooks/stop.ts
|
|
2897
|
+
var stopHookActiveState = new Map;
|
|
2898
|
+
async function executeStopHooks(ctx, config, extendedConfig) {
|
|
2899
|
+
if (ctx.parentSessionId) {
|
|
2900
|
+
return { block: false };
|
|
2901
|
+
}
|
|
2902
|
+
if (!config) {
|
|
2903
|
+
return { block: false };
|
|
2904
|
+
}
|
|
2905
|
+
const matchers = findMatchingHooks(config, "Stop");
|
|
2906
|
+
if (matchers.length === 0) {
|
|
2907
|
+
return { block: false };
|
|
2908
|
+
}
|
|
2909
|
+
const stdinData = {
|
|
2910
|
+
session_id: ctx.sessionId,
|
|
2911
|
+
transcript_path: ctx.transcriptPath,
|
|
2912
|
+
cwd: ctx.cwd,
|
|
2913
|
+
permission_mode: ctx.permissionMode ?? "bypassPermissions",
|
|
2914
|
+
hook_event_name: "Stop",
|
|
2915
|
+
stop_hook_active: stopHookActiveState.get(ctx.sessionId) ?? false,
|
|
2916
|
+
todo_path: getTodoPath(ctx.sessionId),
|
|
2917
|
+
hook_source: "opencode-plugin"
|
|
2918
|
+
};
|
|
2919
|
+
for (const matcher of matchers) {
|
|
2920
|
+
for (const hook of matcher.hooks) {
|
|
2921
|
+
if (hook.type !== "command")
|
|
2922
|
+
continue;
|
|
2923
|
+
if (isHookCommandDisabled("Stop", hook.command, extendedConfig ?? null)) {
|
|
2924
|
+
log("Stop hook command skipped (disabled by config)", { command: hook.command });
|
|
2925
|
+
continue;
|
|
2926
|
+
}
|
|
2927
|
+
const result = await executeHookCommand(hook.command, JSON.stringify(stdinData), ctx.cwd, { forceZsh: DEFAULT_CONFIG.forceZsh, zshPath: DEFAULT_CONFIG.zshPath });
|
|
2928
|
+
if (result.exitCode === 2) {
|
|
2929
|
+
const reason = result.stderr || result.stdout || "Blocked by stop hook";
|
|
2930
|
+
return {
|
|
2931
|
+
block: true,
|
|
2932
|
+
reason,
|
|
2933
|
+
injectPrompt: reason
|
|
2934
|
+
};
|
|
2935
|
+
}
|
|
2936
|
+
if (result.stdout) {
|
|
2937
|
+
try {
|
|
2938
|
+
const output = JSON.parse(result.stdout);
|
|
2939
|
+
if (output.stop_hook_active !== undefined) {
|
|
2940
|
+
stopHookActiveState.set(ctx.sessionId, output.stop_hook_active);
|
|
2941
|
+
}
|
|
2942
|
+
const isBlock = output.decision === "block";
|
|
2943
|
+
const injectPrompt = output.inject_prompt ?? (isBlock && output.reason ? output.reason : undefined);
|
|
2944
|
+
return {
|
|
2945
|
+
block: isBlock,
|
|
2946
|
+
reason: output.reason,
|
|
2947
|
+
stopHookActive: output.stop_hook_active,
|
|
2948
|
+
permissionMode: output.permission_mode,
|
|
2949
|
+
injectPrompt
|
|
2950
|
+
};
|
|
2951
|
+
} catch {}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return { block: false };
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// src/hooks/claude-code-hooks/tool-input-cache.ts
|
|
2959
|
+
var cache = new Map;
|
|
2960
|
+
var CACHE_TTL = 60000;
|
|
2961
|
+
function cacheToolInput(sessionId, toolName, invocationId, toolInput) {
|
|
2962
|
+
const key = `${sessionId}:${toolName}:${invocationId}`;
|
|
2963
|
+
cache.set(key, { toolInput, timestamp: Date.now() });
|
|
2964
|
+
}
|
|
2965
|
+
function getToolInput(sessionId, toolName, invocationId) {
|
|
2966
|
+
const key = `${sessionId}:${toolName}:${invocationId}`;
|
|
2967
|
+
const entry = cache.get(key);
|
|
2968
|
+
if (!entry)
|
|
2969
|
+
return null;
|
|
2970
|
+
cache.delete(key);
|
|
2971
|
+
if (Date.now() - entry.timestamp > CACHE_TTL)
|
|
2972
|
+
return null;
|
|
2973
|
+
return entry.toolInput;
|
|
2974
|
+
}
|
|
2975
|
+
setInterval(() => {
|
|
2976
|
+
const now = Date.now();
|
|
2977
|
+
for (const [key, entry] of cache.entries()) {
|
|
2978
|
+
if (now - entry.timestamp > CACHE_TTL) {
|
|
2979
|
+
cache.delete(key);
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
}, CACHE_TTL);
|
|
2983
|
+
|
|
2984
|
+
// src/features/hook-message-injector/injector.ts
|
|
2985
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2986
|
+
import { join as join15 } from "path";
|
|
2987
|
+
|
|
2988
|
+
// src/features/hook-message-injector/constants.ts
|
|
2989
|
+
import { join as join14 } from "path";
|
|
2990
|
+
import { homedir as homedir6 } from "os";
|
|
2991
|
+
var xdgData2 = process.env.XDG_DATA_HOME || join14(homedir6(), ".local", "share");
|
|
2992
|
+
var OPENCODE_STORAGE3 = join14(xdgData2, "opencode", "storage");
|
|
2993
|
+
var MESSAGE_STORAGE2 = join14(OPENCODE_STORAGE3, "message");
|
|
2994
|
+
var PART_STORAGE2 = join14(OPENCODE_STORAGE3, "part");
|
|
2995
|
+
|
|
2996
|
+
// src/features/hook-message-injector/injector.ts
|
|
2997
|
+
function findNearestMessageWithFields(messageDir) {
|
|
2998
|
+
try {
|
|
2999
|
+
const files = readdirSync2(messageDir).filter((f) => f.endsWith(".json")).sort().reverse();
|
|
3000
|
+
for (const file of files) {
|
|
3001
|
+
try {
|
|
3002
|
+
const content = readFileSync5(join15(messageDir, file), "utf-8");
|
|
3003
|
+
const msg = JSON.parse(content);
|
|
3004
|
+
if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
|
|
3005
|
+
return msg;
|
|
3006
|
+
}
|
|
3007
|
+
} catch {
|
|
3008
|
+
continue;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
} catch {
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
function generateMessageId() {
|
|
3017
|
+
const timestamp = Date.now().toString(16);
|
|
3018
|
+
const random = Math.random().toString(36).substring(2, 14);
|
|
3019
|
+
return `msg_${timestamp}${random}`;
|
|
3020
|
+
}
|
|
3021
|
+
function generatePartId2() {
|
|
3022
|
+
const timestamp = Date.now().toString(16);
|
|
3023
|
+
const random = Math.random().toString(36).substring(2, 10);
|
|
3024
|
+
return `prt_${timestamp}${random}`;
|
|
3025
|
+
}
|
|
3026
|
+
function getOrCreateMessageDir(sessionID) {
|
|
3027
|
+
if (!existsSync12(MESSAGE_STORAGE2)) {
|
|
3028
|
+
mkdirSync5(MESSAGE_STORAGE2, { recursive: true });
|
|
3029
|
+
}
|
|
3030
|
+
const directPath = join15(MESSAGE_STORAGE2, sessionID);
|
|
3031
|
+
if (existsSync12(directPath)) {
|
|
3032
|
+
return directPath;
|
|
3033
|
+
}
|
|
3034
|
+
for (const dir of readdirSync2(MESSAGE_STORAGE2)) {
|
|
3035
|
+
const sessionPath = join15(MESSAGE_STORAGE2, dir, sessionID);
|
|
3036
|
+
if (existsSync12(sessionPath)) {
|
|
3037
|
+
return sessionPath;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
mkdirSync5(directPath, { recursive: true });
|
|
3041
|
+
return directPath;
|
|
3042
|
+
}
|
|
3043
|
+
function injectHookMessage(sessionID, hookContent, originalMessage) {
|
|
3044
|
+
const messageDir = getOrCreateMessageDir(sessionID);
|
|
3045
|
+
const needsFallback = !originalMessage.agent || !originalMessage.model?.providerID || !originalMessage.model?.modelID;
|
|
3046
|
+
const fallback = needsFallback ? findNearestMessageWithFields(messageDir) : null;
|
|
3047
|
+
const now = Date.now();
|
|
3048
|
+
const messageID = generateMessageId();
|
|
3049
|
+
const partID = generatePartId2();
|
|
3050
|
+
const resolvedAgent = originalMessage.agent ?? fallback?.agent ?? "general";
|
|
3051
|
+
const resolvedModel = originalMessage.model?.providerID && originalMessage.model?.modelID ? { providerID: originalMessage.model.providerID, modelID: originalMessage.model.modelID } : fallback?.model?.providerID && fallback?.model?.modelID ? { providerID: fallback.model.providerID, modelID: fallback.model.modelID } : undefined;
|
|
3052
|
+
const resolvedTools = originalMessage.tools ?? fallback?.tools;
|
|
3053
|
+
const messageMeta = {
|
|
3054
|
+
id: messageID,
|
|
3055
|
+
sessionID,
|
|
3056
|
+
role: "user",
|
|
3057
|
+
time: {
|
|
3058
|
+
created: now
|
|
3059
|
+
},
|
|
3060
|
+
agent: resolvedAgent,
|
|
3061
|
+
model: resolvedModel,
|
|
3062
|
+
path: originalMessage.path?.cwd ? {
|
|
3063
|
+
cwd: originalMessage.path.cwd,
|
|
3064
|
+
root: originalMessage.path.root ?? "/"
|
|
3065
|
+
} : undefined,
|
|
3066
|
+
tools: resolvedTools
|
|
3067
|
+
};
|
|
3068
|
+
const textPart = {
|
|
3069
|
+
id: partID,
|
|
3070
|
+
type: "text",
|
|
3071
|
+
text: hookContent,
|
|
3072
|
+
synthetic: true,
|
|
3073
|
+
time: {
|
|
3074
|
+
start: now,
|
|
3075
|
+
end: now
|
|
3076
|
+
},
|
|
3077
|
+
messageID,
|
|
3078
|
+
sessionID
|
|
3079
|
+
};
|
|
3080
|
+
try {
|
|
3081
|
+
writeFileSync4(join15(messageDir, `${messageID}.json`), JSON.stringify(messageMeta, null, 2));
|
|
3082
|
+
const partDir = join15(PART_STORAGE2, messageID);
|
|
3083
|
+
if (!existsSync12(partDir)) {
|
|
3084
|
+
mkdirSync5(partDir, { recursive: true });
|
|
3085
|
+
}
|
|
3086
|
+
writeFileSync4(join15(partDir, `${partID}.json`), JSON.stringify(textPart, null, 2));
|
|
3087
|
+
return true;
|
|
3088
|
+
} catch {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
2021
3091
|
}
|
|
3092
|
+
// src/hooks/claude-code-hooks/index.ts
|
|
3093
|
+
var sessionFirstMessageProcessed = new Set;
|
|
3094
|
+
var sessionErrorState = new Map;
|
|
3095
|
+
var sessionInterruptState = new Map;
|
|
3096
|
+
function createClaudeCodeHooksHook(ctx, config = {}) {
|
|
3097
|
+
return {
|
|
3098
|
+
"chat.message": async (input, output) => {
|
|
3099
|
+
const interruptState = sessionInterruptState.get(input.sessionID);
|
|
3100
|
+
if (interruptState?.interrupted) {
|
|
3101
|
+
log("chat.message hook skipped - session interrupted", { sessionID: input.sessionID });
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
const claudeConfig = await loadClaudeHooksConfig();
|
|
3105
|
+
const extendedConfig = await loadPluginExtendedConfig();
|
|
3106
|
+
const textParts = output.parts.filter((p) => p.type === "text" && p.text);
|
|
3107
|
+
const prompt = textParts.map((p) => p.text ?? "").join(`
|
|
3108
|
+
`);
|
|
3109
|
+
recordUserMessage(input.sessionID, prompt);
|
|
3110
|
+
const messageParts = textParts.map((p) => ({
|
|
3111
|
+
type: p.type,
|
|
3112
|
+
text: p.text
|
|
3113
|
+
}));
|
|
3114
|
+
const interruptStateBeforeHooks = sessionInterruptState.get(input.sessionID);
|
|
3115
|
+
if (interruptStateBeforeHooks?.interrupted) {
|
|
3116
|
+
log("chat.message hooks skipped - interrupted during preparation", { sessionID: input.sessionID });
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
let parentSessionId;
|
|
3120
|
+
try {
|
|
3121
|
+
const sessionInfo = await ctx.client.session.get({
|
|
3122
|
+
path: { id: input.sessionID }
|
|
3123
|
+
});
|
|
3124
|
+
parentSessionId = sessionInfo.data?.parentID;
|
|
3125
|
+
} catch {}
|
|
3126
|
+
const isFirstMessage = !sessionFirstMessageProcessed.has(input.sessionID);
|
|
3127
|
+
sessionFirstMessageProcessed.add(input.sessionID);
|
|
3128
|
+
if (isFirstMessage) {
|
|
3129
|
+
log("Skipping UserPromptSubmit hooks on first message for title generation", { sessionID: input.sessionID });
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
if (!isHookDisabled(config, "UserPromptSubmit")) {
|
|
3133
|
+
const userPromptCtx = {
|
|
3134
|
+
sessionId: input.sessionID,
|
|
3135
|
+
parentSessionId,
|
|
3136
|
+
prompt,
|
|
3137
|
+
parts: messageParts,
|
|
3138
|
+
cwd: ctx.directory
|
|
3139
|
+
};
|
|
3140
|
+
const result = await executeUserPromptSubmitHooks(userPromptCtx, claudeConfig, extendedConfig);
|
|
3141
|
+
if (result.block) {
|
|
3142
|
+
throw new Error(result.reason ?? "Hook blocked the prompt");
|
|
3143
|
+
}
|
|
3144
|
+
const interruptStateAfterHooks = sessionInterruptState.get(input.sessionID);
|
|
3145
|
+
if (interruptStateAfterHooks?.interrupted) {
|
|
3146
|
+
log("chat.message injection skipped - interrupted during hooks", { sessionID: input.sessionID });
|
|
3147
|
+
return;
|
|
3148
|
+
}
|
|
3149
|
+
if (result.messages.length > 0) {
|
|
3150
|
+
const hookContent = result.messages.join(`
|
|
2022
3151
|
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
3152
|
+
`);
|
|
3153
|
+
const message = output.message;
|
|
3154
|
+
const success = injectHookMessage(input.sessionID, hookContent, {
|
|
3155
|
+
agent: message.agent,
|
|
3156
|
+
model: message.model,
|
|
3157
|
+
path: message.path ?? { cwd: ctx.directory, root: "/" },
|
|
3158
|
+
tools: message.tools
|
|
3159
|
+
});
|
|
3160
|
+
log(success ? "Hook message injected via file system" : "File injection failed", {
|
|
3161
|
+
sessionID: input.sessionID
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
},
|
|
3166
|
+
"tool.execute.before": async (input, output) => {
|
|
3167
|
+
const claudeConfig = await loadClaudeHooksConfig();
|
|
3168
|
+
const extendedConfig = await loadPluginExtendedConfig();
|
|
3169
|
+
recordToolUse(input.sessionID, input.tool, output.args);
|
|
3170
|
+
cacheToolInput(input.sessionID, input.tool, input.callID, output.args);
|
|
3171
|
+
if (!isHookDisabled(config, "PreToolUse")) {
|
|
3172
|
+
const preCtx = {
|
|
3173
|
+
sessionId: input.sessionID,
|
|
3174
|
+
toolName: input.tool,
|
|
3175
|
+
toolInput: output.args,
|
|
3176
|
+
cwd: ctx.directory,
|
|
3177
|
+
toolUseId: input.callID
|
|
3178
|
+
};
|
|
3179
|
+
const result = await executePreToolUseHooks(preCtx, claudeConfig, extendedConfig);
|
|
3180
|
+
if (result.decision === "deny") {
|
|
3181
|
+
ctx.client.tui.showToast({
|
|
3182
|
+
body: {
|
|
3183
|
+
title: "PreToolUse Hook Executed",
|
|
3184
|
+
message: `\u2717 ${result.toolName ?? input.tool} ${result.hookName ?? "hook"}: BLOCKED ${result.elapsedMs ?? 0}ms
|
|
3185
|
+
${result.inputLines ?? ""}`,
|
|
3186
|
+
variant: "error",
|
|
3187
|
+
duration: 4000
|
|
3188
|
+
}
|
|
3189
|
+
}).catch(() => {});
|
|
3190
|
+
throw new Error(result.reason ?? "Hook blocked the operation");
|
|
3191
|
+
}
|
|
3192
|
+
if (result.modifiedInput) {
|
|
3193
|
+
Object.assign(output.args, result.modifiedInput);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
},
|
|
3197
|
+
"tool.execute.after": async (input, output) => {
|
|
3198
|
+
const claudeConfig = await loadClaudeHooksConfig();
|
|
3199
|
+
const extendedConfig = await loadPluginExtendedConfig();
|
|
3200
|
+
const cachedInput = getToolInput(input.sessionID, input.tool, input.callID) || {};
|
|
3201
|
+
recordToolResult(input.sessionID, input.tool, cachedInput, output.metadata || {});
|
|
3202
|
+
if (!isHookDisabled(config, "PostToolUse")) {
|
|
3203
|
+
const postClient = {
|
|
3204
|
+
session: {
|
|
3205
|
+
messages: (opts) => ctx.client.session.messages(opts)
|
|
3206
|
+
}
|
|
3207
|
+
};
|
|
3208
|
+
const postCtx = {
|
|
3209
|
+
sessionId: input.sessionID,
|
|
3210
|
+
toolName: input.tool,
|
|
3211
|
+
toolInput: cachedInput,
|
|
3212
|
+
toolOutput: {
|
|
3213
|
+
title: input.tool,
|
|
3214
|
+
output: output.output,
|
|
3215
|
+
metadata: output.metadata
|
|
3216
|
+
},
|
|
3217
|
+
cwd: ctx.directory,
|
|
3218
|
+
transcriptPath: getTranscriptPath(input.sessionID),
|
|
3219
|
+
toolUseId: input.callID,
|
|
3220
|
+
client: postClient,
|
|
3221
|
+
permissionMode: "bypassPermissions"
|
|
3222
|
+
};
|
|
3223
|
+
const result = await executePostToolUseHooks(postCtx, claudeConfig, extendedConfig);
|
|
3224
|
+
if (result.block) {
|
|
3225
|
+
ctx.client.tui.showToast({
|
|
3226
|
+
body: {
|
|
3227
|
+
title: "PostToolUse Hook Warning",
|
|
3228
|
+
message: result.reason ?? "Hook returned warning",
|
|
3229
|
+
variant: "warning",
|
|
3230
|
+
duration: 4000
|
|
3231
|
+
}
|
|
3232
|
+
}).catch(() => {});
|
|
3233
|
+
}
|
|
3234
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
3235
|
+
output.output = `${output.output}
|
|
3236
|
+
|
|
3237
|
+
${result.warnings.join(`
|
|
3238
|
+
`)}`;
|
|
3239
|
+
}
|
|
3240
|
+
if (result.message) {
|
|
3241
|
+
output.output = `${output.output}
|
|
2027
3242
|
|
|
3243
|
+
${result.message}`;
|
|
3244
|
+
}
|
|
3245
|
+
if (result.hookName) {
|
|
3246
|
+
ctx.client.tui.showToast({
|
|
3247
|
+
body: {
|
|
3248
|
+
title: "PostToolUse Hook Executed",
|
|
3249
|
+
message: `\u25B6 ${result.toolName ?? input.tool} ${result.hookName}: ${result.elapsedMs ?? 0}ms`,
|
|
3250
|
+
variant: "success",
|
|
3251
|
+
duration: 2000
|
|
3252
|
+
}
|
|
3253
|
+
}).catch(() => {});
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
},
|
|
3257
|
+
event: async (input) => {
|
|
3258
|
+
const { event } = input;
|
|
3259
|
+
if (event.type === "session.error") {
|
|
3260
|
+
const props = event.properties;
|
|
3261
|
+
const sessionID = props?.sessionID;
|
|
3262
|
+
if (sessionID) {
|
|
3263
|
+
sessionErrorState.set(sessionID, {
|
|
3264
|
+
hasError: true,
|
|
3265
|
+
errorMessage: String(props?.error ?? "Unknown error")
|
|
3266
|
+
});
|
|
3267
|
+
}
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
if (event.type === "session.deleted") {
|
|
3271
|
+
const props = event.properties;
|
|
3272
|
+
const sessionInfo = props?.info;
|
|
3273
|
+
if (sessionInfo?.id) {
|
|
3274
|
+
sessionErrorState.delete(sessionInfo.id);
|
|
3275
|
+
sessionInterruptState.delete(sessionInfo.id);
|
|
3276
|
+
sessionFirstMessageProcessed.delete(sessionInfo.id);
|
|
3277
|
+
}
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
if (event.type === "session.idle") {
|
|
3281
|
+
const props = event.properties;
|
|
3282
|
+
const sessionID = props?.sessionID;
|
|
3283
|
+
if (!sessionID)
|
|
3284
|
+
return;
|
|
3285
|
+
const claudeConfig = await loadClaudeHooksConfig();
|
|
3286
|
+
const extendedConfig = await loadPluginExtendedConfig();
|
|
3287
|
+
const errorStateBefore = sessionErrorState.get(sessionID);
|
|
3288
|
+
const endedWithErrorBefore = errorStateBefore?.hasError === true;
|
|
3289
|
+
const interruptStateBefore = sessionInterruptState.get(sessionID);
|
|
3290
|
+
const interruptedBefore = interruptStateBefore?.interrupted === true;
|
|
3291
|
+
let parentSessionId;
|
|
3292
|
+
try {
|
|
3293
|
+
const sessionInfo = await ctx.client.session.get({
|
|
3294
|
+
path: { id: sessionID }
|
|
3295
|
+
});
|
|
3296
|
+
parentSessionId = sessionInfo.data?.parentID;
|
|
3297
|
+
} catch {}
|
|
3298
|
+
if (!isHookDisabled(config, "Stop")) {
|
|
3299
|
+
const stopCtx = {
|
|
3300
|
+
sessionId: sessionID,
|
|
3301
|
+
parentSessionId,
|
|
3302
|
+
cwd: ctx.directory
|
|
3303
|
+
};
|
|
3304
|
+
const stopResult = await executeStopHooks(stopCtx, claudeConfig, extendedConfig);
|
|
3305
|
+
const errorStateAfter = sessionErrorState.get(sessionID);
|
|
3306
|
+
const endedWithErrorAfter = errorStateAfter?.hasError === true;
|
|
3307
|
+
const interruptStateAfter = sessionInterruptState.get(sessionID);
|
|
3308
|
+
const interruptedAfter = interruptStateAfter?.interrupted === true;
|
|
3309
|
+
const shouldBypass = endedWithErrorBefore || endedWithErrorAfter || interruptedBefore || interruptedAfter;
|
|
3310
|
+
if (shouldBypass && stopResult.block) {
|
|
3311
|
+
const interrupted = interruptedBefore || interruptedAfter;
|
|
3312
|
+
const endedWithError = endedWithErrorBefore || endedWithErrorAfter;
|
|
3313
|
+
log("Stop hook block ignored", { sessionID, block: stopResult.block, interrupted, endedWithError });
|
|
3314
|
+
} else if (stopResult.block && stopResult.injectPrompt) {
|
|
3315
|
+
log("Stop hook returned block with inject_prompt", { sessionID });
|
|
3316
|
+
ctx.client.session.prompt({
|
|
3317
|
+
path: { id: sessionID },
|
|
3318
|
+
body: { parts: [{ type: "text", text: stopResult.injectPrompt }] },
|
|
3319
|
+
query: { directory: ctx.directory }
|
|
3320
|
+
}).catch((err) => log("Failed to inject prompt from Stop hook", err));
|
|
3321
|
+
} else if (stopResult.block) {
|
|
3322
|
+
log("Stop hook returned block", { sessionID, reason: stopResult.reason });
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
sessionErrorState.delete(sessionID);
|
|
3326
|
+
sessionInterruptState.delete(sessionID);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
}
|
|
2028
3331
|
// src/features/claude-code-command-loader/loader.ts
|
|
3332
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
|
|
3333
|
+
import { homedir as homedir7 } from "os";
|
|
3334
|
+
import { join as join16, basename } from "path";
|
|
2029
3335
|
function isMarkdownFile(entry) {
|
|
2030
3336
|
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
|
|
2031
3337
|
}
|
|
2032
3338
|
function loadCommandsFromDir(commandsDir, scope) {
|
|
2033
|
-
if (!
|
|
3339
|
+
if (!existsSync13(commandsDir)) {
|
|
2034
3340
|
return [];
|
|
2035
3341
|
}
|
|
2036
|
-
const entries =
|
|
3342
|
+
const entries = readdirSync3(commandsDir, { withFileTypes: true });
|
|
2037
3343
|
const commands = [];
|
|
2038
3344
|
for (const entry of entries) {
|
|
2039
3345
|
if (!isMarkdownFile(entry))
|
|
2040
3346
|
continue;
|
|
2041
|
-
const commandPath =
|
|
3347
|
+
const commandPath = join16(commandsDir, entry.name);
|
|
2042
3348
|
const commandName = basename(entry.name, ".md");
|
|
2043
3349
|
try {
|
|
2044
|
-
const content =
|
|
3350
|
+
const content = readFileSync6(commandPath, "utf-8");
|
|
2045
3351
|
const { data, body } = parseFrontmatter(content);
|
|
2046
3352
|
const wrappedTemplate = `<command-instruction>
|
|
2047
3353
|
${body.trim()}
|
|
@@ -2080,50 +3386,50 @@ function commandsToRecord(commands) {
|
|
|
2080
3386
|
return result;
|
|
2081
3387
|
}
|
|
2082
3388
|
function loadUserCommands() {
|
|
2083
|
-
const userCommandsDir =
|
|
3389
|
+
const userCommandsDir = join16(homedir7(), ".claude", "commands");
|
|
2084
3390
|
const commands = loadCommandsFromDir(userCommandsDir, "user");
|
|
2085
3391
|
return commandsToRecord(commands);
|
|
2086
3392
|
}
|
|
2087
3393
|
function loadProjectCommands() {
|
|
2088
|
-
const projectCommandsDir =
|
|
3394
|
+
const projectCommandsDir = join16(process.cwd(), ".claude", "commands");
|
|
2089
3395
|
const commands = loadCommandsFromDir(projectCommandsDir, "project");
|
|
2090
3396
|
return commandsToRecord(commands);
|
|
2091
3397
|
}
|
|
2092
3398
|
function loadOpencodeGlobalCommands() {
|
|
2093
|
-
const opencodeCommandsDir =
|
|
3399
|
+
const opencodeCommandsDir = join16(homedir7(), ".config", "opencode", "command");
|
|
2094
3400
|
const commands = loadCommandsFromDir(opencodeCommandsDir, "opencode");
|
|
2095
3401
|
return commandsToRecord(commands);
|
|
2096
3402
|
}
|
|
2097
3403
|
function loadOpencodeProjectCommands() {
|
|
2098
|
-
const opencodeProjectDir =
|
|
3404
|
+
const opencodeProjectDir = join16(process.cwd(), ".opencode", "command");
|
|
2099
3405
|
const commands = loadCommandsFromDir(opencodeProjectDir, "opencode-project");
|
|
2100
3406
|
return commandsToRecord(commands);
|
|
2101
3407
|
}
|
|
2102
3408
|
// src/features/claude-code-skill-loader/loader.ts
|
|
2103
|
-
import { existsSync as
|
|
2104
|
-
import { homedir as
|
|
2105
|
-
import { join as
|
|
3409
|
+
import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync2, readlinkSync } from "fs";
|
|
3410
|
+
import { homedir as homedir8 } from "os";
|
|
3411
|
+
import { join as join17, resolve as resolve2 } from "path";
|
|
2106
3412
|
function loadSkillsFromDir(skillsDir, scope) {
|
|
2107
|
-
if (!
|
|
3413
|
+
if (!existsSync14(skillsDir)) {
|
|
2108
3414
|
return [];
|
|
2109
3415
|
}
|
|
2110
|
-
const entries =
|
|
3416
|
+
const entries = readdirSync4(skillsDir, { withFileTypes: true });
|
|
2111
3417
|
const skills = [];
|
|
2112
3418
|
for (const entry of entries) {
|
|
2113
3419
|
if (entry.name.startsWith("."))
|
|
2114
3420
|
continue;
|
|
2115
|
-
const skillPath =
|
|
3421
|
+
const skillPath = join17(skillsDir, entry.name);
|
|
2116
3422
|
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
2117
3423
|
continue;
|
|
2118
3424
|
let resolvedPath = skillPath;
|
|
2119
|
-
if (
|
|
3425
|
+
if (statSync2(skillPath, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
2120
3426
|
resolvedPath = resolve2(skillPath, "..", readlinkSync(skillPath));
|
|
2121
3427
|
}
|
|
2122
|
-
const skillMdPath =
|
|
2123
|
-
if (!
|
|
3428
|
+
const skillMdPath = join17(resolvedPath, "SKILL.md");
|
|
3429
|
+
if (!existsSync14(skillMdPath))
|
|
2124
3430
|
continue;
|
|
2125
3431
|
try {
|
|
2126
|
-
const content =
|
|
3432
|
+
const content = readFileSync7(skillMdPath, "utf-8");
|
|
2127
3433
|
const { data, body } = parseFrontmatter(content);
|
|
2128
3434
|
const skillName = data.name || entry.name;
|
|
2129
3435
|
const originalDescription = data.description || "";
|
|
@@ -2154,7 +3460,7 @@ $ARGUMENTS
|
|
|
2154
3460
|
return skills;
|
|
2155
3461
|
}
|
|
2156
3462
|
function loadUserSkillsAsCommands() {
|
|
2157
|
-
const userSkillsDir =
|
|
3463
|
+
const userSkillsDir = join17(homedir8(), ".claude", "skills");
|
|
2158
3464
|
const skills = loadSkillsFromDir(userSkillsDir, "user");
|
|
2159
3465
|
return skills.reduce((acc, skill) => {
|
|
2160
3466
|
acc[skill.name] = skill.definition;
|
|
@@ -2162,7 +3468,7 @@ function loadUserSkillsAsCommands() {
|
|
|
2162
3468
|
}, {});
|
|
2163
3469
|
}
|
|
2164
3470
|
function loadProjectSkillsAsCommands() {
|
|
2165
|
-
const projectSkillsDir =
|
|
3471
|
+
const projectSkillsDir = join17(process.cwd(), ".claude", "skills");
|
|
2166
3472
|
const skills = loadSkillsFromDir(projectSkillsDir, "project");
|
|
2167
3473
|
return skills.reduce((acc, skill) => {
|
|
2168
3474
|
acc[skill.name] = skill.definition;
|
|
@@ -2170,9 +3476,9 @@ function loadProjectSkillsAsCommands() {
|
|
|
2170
3476
|
}, {});
|
|
2171
3477
|
}
|
|
2172
3478
|
// src/features/claude-code-agent-loader/loader.ts
|
|
2173
|
-
import { existsSync as
|
|
2174
|
-
import { homedir as
|
|
2175
|
-
import { join as
|
|
3479
|
+
import { existsSync as existsSync15, readdirSync as readdirSync5, readFileSync as readFileSync8 } from "fs";
|
|
3480
|
+
import { homedir as homedir9 } from "os";
|
|
3481
|
+
import { join as join18, basename as basename2 } from "path";
|
|
2176
3482
|
function parseToolsConfig(toolsStr) {
|
|
2177
3483
|
if (!toolsStr)
|
|
2178
3484
|
return;
|
|
@@ -2189,18 +3495,18 @@ function isMarkdownFile2(entry) {
|
|
|
2189
3495
|
return !entry.name.startsWith(".") && entry.name.endsWith(".md") && entry.isFile();
|
|
2190
3496
|
}
|
|
2191
3497
|
function loadAgentsFromDir(agentsDir, scope) {
|
|
2192
|
-
if (!
|
|
3498
|
+
if (!existsSync15(agentsDir)) {
|
|
2193
3499
|
return [];
|
|
2194
3500
|
}
|
|
2195
|
-
const entries =
|
|
3501
|
+
const entries = readdirSync5(agentsDir, { withFileTypes: true });
|
|
2196
3502
|
const agents = [];
|
|
2197
3503
|
for (const entry of entries) {
|
|
2198
3504
|
if (!isMarkdownFile2(entry))
|
|
2199
3505
|
continue;
|
|
2200
|
-
const agentPath =
|
|
3506
|
+
const agentPath = join18(agentsDir, entry.name);
|
|
2201
3507
|
const agentName = basename2(entry.name, ".md");
|
|
2202
3508
|
try {
|
|
2203
|
-
const content =
|
|
3509
|
+
const content = readFileSync8(agentPath, "utf-8");
|
|
2204
3510
|
const { data, body } = parseFrontmatter(content);
|
|
2205
3511
|
const name = data.name || agentName;
|
|
2206
3512
|
const originalDescription = data.description || "";
|
|
@@ -2227,7 +3533,7 @@ function loadAgentsFromDir(agentsDir, scope) {
|
|
|
2227
3533
|
return agents;
|
|
2228
3534
|
}
|
|
2229
3535
|
function loadUserAgents() {
|
|
2230
|
-
const userAgentsDir =
|
|
3536
|
+
const userAgentsDir = join18(homedir9(), ".claude", "agents");
|
|
2231
3537
|
const agents = loadAgentsFromDir(userAgentsDir, "user");
|
|
2232
3538
|
const result = {};
|
|
2233
3539
|
for (const agent of agents) {
|
|
@@ -2236,7 +3542,7 @@ function loadUserAgents() {
|
|
|
2236
3542
|
return result;
|
|
2237
3543
|
}
|
|
2238
3544
|
function loadProjectAgents() {
|
|
2239
|
-
const projectAgentsDir =
|
|
3545
|
+
const projectAgentsDir = join18(process.cwd(), ".claude", "agents");
|
|
2240
3546
|
const agents = loadAgentsFromDir(projectAgentsDir, "project");
|
|
2241
3547
|
const result = {};
|
|
2242
3548
|
for (const agent of agents) {
|
|
@@ -2245,9 +3551,9 @@ function loadProjectAgents() {
|
|
|
2245
3551
|
return result;
|
|
2246
3552
|
}
|
|
2247
3553
|
// src/features/claude-code-mcp-loader/loader.ts
|
|
2248
|
-
import { existsSync as
|
|
2249
|
-
import { homedir as
|
|
2250
|
-
import { join as
|
|
3554
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3555
|
+
import { homedir as homedir10 } from "os";
|
|
3556
|
+
import { join as join19 } from "path";
|
|
2251
3557
|
|
|
2252
3558
|
// src/features/claude-code-mcp-loader/env-expander.ts
|
|
2253
3559
|
function expandEnvVars(value) {
|
|
@@ -2311,32 +3617,18 @@ function transformMcpServer(name, server) {
|
|
|
2311
3617
|
return config;
|
|
2312
3618
|
}
|
|
2313
3619
|
|
|
2314
|
-
// src/shared/logger.ts
|
|
2315
|
-
import * as fs3 from "fs";
|
|
2316
|
-
import * as os2 from "os";
|
|
2317
|
-
import * as path2 from "path";
|
|
2318
|
-
var logFile = path2.join(os2.tmpdir(), "oh-my-opencode.log");
|
|
2319
|
-
function log(message, data) {
|
|
2320
|
-
try {
|
|
2321
|
-
const timestamp = new Date().toISOString();
|
|
2322
|
-
const logEntry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}
|
|
2323
|
-
`;
|
|
2324
|
-
fs3.appendFileSync(logFile, logEntry);
|
|
2325
|
-
} catch {}
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
3620
|
// src/features/claude-code-mcp-loader/loader.ts
|
|
2329
3621
|
function getMcpConfigPaths() {
|
|
2330
|
-
const home =
|
|
3622
|
+
const home = homedir10();
|
|
2331
3623
|
const cwd = process.cwd();
|
|
2332
3624
|
return [
|
|
2333
|
-
{ path:
|
|
2334
|
-
{ path:
|
|
2335
|
-
{ path:
|
|
3625
|
+
{ path: join19(home, ".claude", ".mcp.json"), scope: "user" },
|
|
3626
|
+
{ path: join19(cwd, ".mcp.json"), scope: "project" },
|
|
3627
|
+
{ path: join19(cwd, ".claude", ".mcp.json"), scope: "local" }
|
|
2336
3628
|
];
|
|
2337
3629
|
}
|
|
2338
3630
|
async function loadMcpConfigFile(filePath) {
|
|
2339
|
-
if (!
|
|
3631
|
+
if (!existsSync16(filePath)) {
|
|
2340
3632
|
return null;
|
|
2341
3633
|
}
|
|
2342
3634
|
try {
|
|
@@ -2377,10 +3669,10 @@ async function loadMcpConfigs() {
|
|
|
2377
3669
|
return { servers, loadedServers };
|
|
2378
3670
|
}
|
|
2379
3671
|
// src/features/claude-code-session-state/state.ts
|
|
2380
|
-
var
|
|
2381
|
-
var
|
|
3672
|
+
var sessionErrorState2 = new Map;
|
|
3673
|
+
var sessionInterruptState2 = new Map;
|
|
2382
3674
|
var subagentSessions = new Set;
|
|
2383
|
-
var
|
|
3675
|
+
var sessionFirstMessageProcessed2 = new Set;
|
|
2384
3676
|
var currentSessionID;
|
|
2385
3677
|
var currentSessionTitle;
|
|
2386
3678
|
var mainSessionID;
|
|
@@ -2622,14 +3914,14 @@ var EXT_TO_LANG = {
|
|
|
2622
3914
|
".tfvars": "terraform"
|
|
2623
3915
|
};
|
|
2624
3916
|
// src/tools/lsp/config.ts
|
|
2625
|
-
import { existsSync as
|
|
2626
|
-
import { join as
|
|
2627
|
-
import { homedir as
|
|
3917
|
+
import { existsSync as existsSync17, readFileSync as readFileSync9 } from "fs";
|
|
3918
|
+
import { join as join20 } from "path";
|
|
3919
|
+
import { homedir as homedir11 } from "os";
|
|
2628
3920
|
function loadJsonFile(path3) {
|
|
2629
|
-
if (!
|
|
3921
|
+
if (!existsSync17(path3))
|
|
2630
3922
|
return null;
|
|
2631
3923
|
try {
|
|
2632
|
-
return JSON.parse(
|
|
3924
|
+
return JSON.parse(readFileSync9(path3, "utf-8"));
|
|
2633
3925
|
} catch {
|
|
2634
3926
|
return null;
|
|
2635
3927
|
}
|
|
@@ -2637,9 +3929,9 @@ function loadJsonFile(path3) {
|
|
|
2637
3929
|
function getConfigPaths() {
|
|
2638
3930
|
const cwd = process.cwd();
|
|
2639
3931
|
return {
|
|
2640
|
-
project:
|
|
2641
|
-
user:
|
|
2642
|
-
opencode:
|
|
3932
|
+
project: join20(cwd, ".opencode", "oh-my-opencode.json"),
|
|
3933
|
+
user: join20(homedir11(), ".config", "opencode", "oh-my-opencode.json"),
|
|
3934
|
+
opencode: join20(homedir11(), ".config", "opencode", "opencode.json")
|
|
2643
3935
|
};
|
|
2644
3936
|
}
|
|
2645
3937
|
function loadAllConfigs() {
|
|
@@ -2732,7 +4024,7 @@ function isServerInstalled(command) {
|
|
|
2732
4024
|
const pathEnv = process.env.PATH || "";
|
|
2733
4025
|
const paths = pathEnv.split(":");
|
|
2734
4026
|
for (const p of paths) {
|
|
2735
|
-
if (
|
|
4027
|
+
if (existsSync17(join20(p, cmd))) {
|
|
2736
4028
|
return true;
|
|
2737
4029
|
}
|
|
2738
4030
|
}
|
|
@@ -2781,8 +4073,8 @@ function getAllServers() {
|
|
|
2781
4073
|
return result;
|
|
2782
4074
|
}
|
|
2783
4075
|
// src/tools/lsp/client.ts
|
|
2784
|
-
var {spawn:
|
|
2785
|
-
import { readFileSync as
|
|
4076
|
+
var {spawn: spawn4 } = globalThis.Bun;
|
|
4077
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2786
4078
|
import { extname, resolve as resolve3 } from "path";
|
|
2787
4079
|
class LSPServerManager {
|
|
2788
4080
|
static instance;
|
|
@@ -2918,7 +4210,7 @@ class LSPClient {
|
|
|
2918
4210
|
this.server = server;
|
|
2919
4211
|
}
|
|
2920
4212
|
async start() {
|
|
2921
|
-
this.proc =
|
|
4213
|
+
this.proc = spawn4(this.server.command, {
|
|
2922
4214
|
stdin: "pipe",
|
|
2923
4215
|
stdout: "pipe",
|
|
2924
4216
|
stderr: "pipe",
|
|
@@ -3182,7 +4474,7 @@ ${msg}`);
|
|
|
3182
4474
|
const absPath = resolve3(filePath);
|
|
3183
4475
|
if (this.openedFiles.has(absPath))
|
|
3184
4476
|
return;
|
|
3185
|
-
const text =
|
|
4477
|
+
const text = readFileSync10(absPath, "utf-8");
|
|
3186
4478
|
const ext = extname(absPath);
|
|
3187
4479
|
const languageId = getLanguageId(ext);
|
|
3188
4480
|
this.notify("textDocument/didOpen", {
|
|
@@ -3297,16 +4589,16 @@ ${msg}`);
|
|
|
3297
4589
|
}
|
|
3298
4590
|
// src/tools/lsp/utils.ts
|
|
3299
4591
|
import { extname as extname2, resolve as resolve4 } from "path";
|
|
3300
|
-
import { existsSync as
|
|
4592
|
+
import { existsSync as existsSync18, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
|
|
3301
4593
|
function findWorkspaceRoot(filePath) {
|
|
3302
4594
|
let dir = resolve4(filePath);
|
|
3303
|
-
if (!
|
|
4595
|
+
if (!existsSync18(dir) || !__require("fs").statSync(dir).isDirectory()) {
|
|
3304
4596
|
dir = __require("path").dirname(dir);
|
|
3305
4597
|
}
|
|
3306
4598
|
const markers = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
3307
4599
|
while (dir !== "/") {
|
|
3308
4600
|
for (const marker of markers) {
|
|
3309
|
-
if (
|
|
4601
|
+
if (existsSync18(__require("path").join(dir, marker))) {
|
|
3310
4602
|
return dir;
|
|
3311
4603
|
}
|
|
3312
4604
|
}
|
|
@@ -3464,7 +4756,7 @@ function formatCodeActions(actions) {
|
|
|
3464
4756
|
}
|
|
3465
4757
|
function applyTextEditsToFile(filePath, edits) {
|
|
3466
4758
|
try {
|
|
3467
|
-
let content =
|
|
4759
|
+
let content = readFileSync11(filePath, "utf-8");
|
|
3468
4760
|
const lines = content.split(`
|
|
3469
4761
|
`);
|
|
3470
4762
|
const sortedEdits = [...edits].sort((a, b) => {
|
|
@@ -3489,7 +4781,7 @@ function applyTextEditsToFile(filePath, edits) {
|
|
|
3489
4781
|
`));
|
|
3490
4782
|
}
|
|
3491
4783
|
}
|
|
3492
|
-
|
|
4784
|
+
writeFileSync5(filePath, lines.join(`
|
|
3493
4785
|
`), "utf-8");
|
|
3494
4786
|
return { success: true, editCount: edits.length };
|
|
3495
4787
|
} catch (err) {
|
|
@@ -3520,7 +4812,7 @@ function applyWorkspaceEdit(edit) {
|
|
|
3520
4812
|
if (change.kind === "create") {
|
|
3521
4813
|
try {
|
|
3522
4814
|
const filePath = change.uri.replace("file://", "");
|
|
3523
|
-
|
|
4815
|
+
writeFileSync5(filePath, "", "utf-8");
|
|
3524
4816
|
result.filesModified.push(filePath);
|
|
3525
4817
|
} catch (err) {
|
|
3526
4818
|
result.success = false;
|
|
@@ -3530,8 +4822,8 @@ function applyWorkspaceEdit(edit) {
|
|
|
3530
4822
|
try {
|
|
3531
4823
|
const oldPath = change.oldUri.replace("file://", "");
|
|
3532
4824
|
const newPath = change.newUri.replace("file://", "");
|
|
3533
|
-
const content =
|
|
3534
|
-
|
|
4825
|
+
const content = readFileSync11(oldPath, "utf-8");
|
|
4826
|
+
writeFileSync5(newPath, content, "utf-8");
|
|
3535
4827
|
__require("fs").unlinkSync(oldPath);
|
|
3536
4828
|
result.filesModified.push(newPath);
|
|
3537
4829
|
} catch (err) {
|
|
@@ -4172,7 +5464,7 @@ __export(exports_util, {
|
|
|
4172
5464
|
jsonStringifyReplacer: () => jsonStringifyReplacer,
|
|
4173
5465
|
joinValues: () => joinValues,
|
|
4174
5466
|
issue: () => issue,
|
|
4175
|
-
isPlainObject: () =>
|
|
5467
|
+
isPlainObject: () => isPlainObject2,
|
|
4176
5468
|
isObject: () => isObject,
|
|
4177
5469
|
hexToUint8Array: () => hexToUint8Array,
|
|
4178
5470
|
getSizableOrigin: () => getSizableOrigin,
|
|
@@ -4354,7 +5646,7 @@ var allowsEval = cached(() => {
|
|
|
4354
5646
|
return false;
|
|
4355
5647
|
}
|
|
4356
5648
|
});
|
|
4357
|
-
function
|
|
5649
|
+
function isPlainObject2(o) {
|
|
4358
5650
|
if (isObject(o) === false)
|
|
4359
5651
|
return false;
|
|
4360
5652
|
const ctor = o.constructor;
|
|
@@ -4369,7 +5661,7 @@ function isPlainObject(o) {
|
|
|
4369
5661
|
return true;
|
|
4370
5662
|
}
|
|
4371
5663
|
function shallowClone(o) {
|
|
4372
|
-
if (
|
|
5664
|
+
if (isPlainObject2(o))
|
|
4373
5665
|
return { ...o };
|
|
4374
5666
|
if (Array.isArray(o))
|
|
4375
5667
|
return [...o];
|
|
@@ -4552,7 +5844,7 @@ function omit(schema, mask) {
|
|
|
4552
5844
|
return clone(schema, def);
|
|
4553
5845
|
}
|
|
4554
5846
|
function extend(schema, shape) {
|
|
4555
|
-
if (!
|
|
5847
|
+
if (!isPlainObject2(shape)) {
|
|
4556
5848
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
4557
5849
|
}
|
|
4558
5850
|
const checks = schema._zod.def.checks;
|
|
@@ -4571,7 +5863,7 @@ function extend(schema, shape) {
|
|
|
4571
5863
|
return clone(schema, def);
|
|
4572
5864
|
}
|
|
4573
5865
|
function safeExtend(schema, shape) {
|
|
4574
|
-
if (!
|
|
5866
|
+
if (!isPlainObject2(shape)) {
|
|
4575
5867
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
4576
5868
|
}
|
|
4577
5869
|
const def = {
|
|
@@ -6721,7 +8013,7 @@ function mergeValues(a, b) {
|
|
|
6721
8013
|
if (a instanceof Date && b instanceof Date && +a === +b) {
|
|
6722
8014
|
return { valid: true, data: a };
|
|
6723
8015
|
}
|
|
6724
|
-
if (
|
|
8016
|
+
if (isPlainObject2(a) && isPlainObject2(b)) {
|
|
6725
8017
|
const bKeys = Object.keys(b);
|
|
6726
8018
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
6727
8019
|
const newObj = { ...a, ...b };
|
|
@@ -6851,7 +8143,7 @@ var $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
|
|
|
6851
8143
|
$ZodType.init(inst, def);
|
|
6852
8144
|
inst._zod.parse = (payload, ctx) => {
|
|
6853
8145
|
const input = payload.value;
|
|
6854
|
-
if (!
|
|
8146
|
+
if (!isPlainObject2(input)) {
|
|
6855
8147
|
payload.issues.push({
|
|
6856
8148
|
expected: "record",
|
|
6857
8149
|
code: "invalid_type",
|
|
@@ -16231,14 +17523,14 @@ var lsp_code_action_resolve = tool({
|
|
|
16231
17523
|
});
|
|
16232
17524
|
// src/tools/ast-grep/constants.ts
|
|
16233
17525
|
import { createRequire as createRequire4 } from "module";
|
|
16234
|
-
import { dirname as dirname3, join as
|
|
16235
|
-
import { existsSync as
|
|
17526
|
+
import { dirname as dirname3, join as join22 } from "path";
|
|
17527
|
+
import { existsSync as existsSync20, statSync as statSync3 } from "fs";
|
|
16236
17528
|
|
|
16237
17529
|
// src/tools/ast-grep/downloader.ts
|
|
16238
|
-
var {spawn:
|
|
16239
|
-
import { existsSync as
|
|
16240
|
-
import { join as
|
|
16241
|
-
import { homedir as
|
|
17530
|
+
var {spawn: spawn5 } = globalThis.Bun;
|
|
17531
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync6, chmodSync as chmodSync2, unlinkSync as unlinkSync5 } from "fs";
|
|
17532
|
+
import { join as join21 } from "path";
|
|
17533
|
+
import { homedir as homedir12 } from "os";
|
|
16242
17534
|
import { createRequire as createRequire3 } from "module";
|
|
16243
17535
|
var REPO2 = "ast-grep/ast-grep";
|
|
16244
17536
|
var DEFAULT_VERSION = "0.40.0";
|
|
@@ -16263,26 +17555,26 @@ var PLATFORM_MAP2 = {
|
|
|
16263
17555
|
function getCacheDir2() {
|
|
16264
17556
|
if (process.platform === "win32") {
|
|
16265
17557
|
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
|
|
16266
|
-
const base2 = localAppData ||
|
|
16267
|
-
return
|
|
17558
|
+
const base2 = localAppData || join21(homedir12(), "AppData", "Local");
|
|
17559
|
+
return join21(base2, "oh-my-opencode", "bin");
|
|
16268
17560
|
}
|
|
16269
17561
|
const xdgCache2 = process.env.XDG_CACHE_HOME;
|
|
16270
|
-
const base = xdgCache2 ||
|
|
16271
|
-
return
|
|
17562
|
+
const base = xdgCache2 || join21(homedir12(), ".cache");
|
|
17563
|
+
return join21(base, "oh-my-opencode", "bin");
|
|
16272
17564
|
}
|
|
16273
17565
|
function getBinaryName3() {
|
|
16274
17566
|
return process.platform === "win32" ? "sg.exe" : "sg";
|
|
16275
17567
|
}
|
|
16276
17568
|
function getCachedBinaryPath2() {
|
|
16277
|
-
const binaryPath =
|
|
16278
|
-
return
|
|
17569
|
+
const binaryPath = join21(getCacheDir2(), getBinaryName3());
|
|
17570
|
+
return existsSync19(binaryPath) ? binaryPath : null;
|
|
16279
17571
|
}
|
|
16280
17572
|
async function extractZip2(archivePath, destDir) {
|
|
16281
|
-
const proc = process.platform === "win32" ?
|
|
17573
|
+
const proc = process.platform === "win32" ? spawn5([
|
|
16282
17574
|
"powershell",
|
|
16283
17575
|
"-command",
|
|
16284
17576
|
`Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force`
|
|
16285
|
-
], { stdout: "pipe", stderr: "pipe" }) :
|
|
17577
|
+
], { stdout: "pipe", stderr: "pipe" }) : spawn5(["unzip", "-o", archivePath, "-d", destDir], { stdout: "pipe", stderr: "pipe" });
|
|
16286
17578
|
const exitCode = await proc.exited;
|
|
16287
17579
|
if (exitCode !== 0) {
|
|
16288
17580
|
const stderr = await new Response(proc.stderr).text();
|
|
@@ -16301,8 +17593,8 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
|
|
|
16301
17593
|
}
|
|
16302
17594
|
const cacheDir = getCacheDir2();
|
|
16303
17595
|
const binaryName = getBinaryName3();
|
|
16304
|
-
const binaryPath =
|
|
16305
|
-
if (
|
|
17596
|
+
const binaryPath = join21(cacheDir, binaryName);
|
|
17597
|
+
if (existsSync19(binaryPath)) {
|
|
16306
17598
|
return binaryPath;
|
|
16307
17599
|
}
|
|
16308
17600
|
const { arch, os: os3 } = platformInfo;
|
|
@@ -16310,21 +17602,21 @@ async function downloadAstGrep(version2 = DEFAULT_VERSION) {
|
|
|
16310
17602
|
const downloadUrl = `https://github.com/${REPO2}/releases/download/${version2}/${assetName}`;
|
|
16311
17603
|
console.log(`[oh-my-opencode] Downloading ast-grep binary...`);
|
|
16312
17604
|
try {
|
|
16313
|
-
if (!
|
|
16314
|
-
|
|
17605
|
+
if (!existsSync19(cacheDir)) {
|
|
17606
|
+
mkdirSync6(cacheDir, { recursive: true });
|
|
16315
17607
|
}
|
|
16316
17608
|
const response = await fetch(downloadUrl, { redirect: "follow" });
|
|
16317
17609
|
if (!response.ok) {
|
|
16318
17610
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
16319
17611
|
}
|
|
16320
|
-
const archivePath =
|
|
17612
|
+
const archivePath = join21(cacheDir, assetName);
|
|
16321
17613
|
const arrayBuffer = await response.arrayBuffer();
|
|
16322
17614
|
await Bun.write(archivePath, arrayBuffer);
|
|
16323
17615
|
await extractZip2(archivePath, cacheDir);
|
|
16324
|
-
if (
|
|
16325
|
-
|
|
17616
|
+
if (existsSync19(archivePath)) {
|
|
17617
|
+
unlinkSync5(archivePath);
|
|
16326
17618
|
}
|
|
16327
|
-
if (process.platform !== "win32" &&
|
|
17619
|
+
if (process.platform !== "win32" && existsSync19(binaryPath)) {
|
|
16328
17620
|
chmodSync2(binaryPath, 493);
|
|
16329
17621
|
}
|
|
16330
17622
|
console.log(`[oh-my-opencode] ast-grep binary ready.`);
|
|
@@ -16346,7 +17638,7 @@ async function ensureAstGrepBinary() {
|
|
|
16346
17638
|
// src/tools/ast-grep/constants.ts
|
|
16347
17639
|
function isValidBinary(filePath) {
|
|
16348
17640
|
try {
|
|
16349
|
-
return
|
|
17641
|
+
return statSync3(filePath).size > 1e4;
|
|
16350
17642
|
} catch {
|
|
16351
17643
|
return false;
|
|
16352
17644
|
}
|
|
@@ -16375,8 +17667,8 @@ function findSgCliPathSync() {
|
|
|
16375
17667
|
const require2 = createRequire4(import.meta.url);
|
|
16376
17668
|
const cliPkgPath = require2.resolve("@ast-grep/cli/package.json");
|
|
16377
17669
|
const cliDir = dirname3(cliPkgPath);
|
|
16378
|
-
const sgPath =
|
|
16379
|
-
if (
|
|
17670
|
+
const sgPath = join22(cliDir, binaryName);
|
|
17671
|
+
if (existsSync20(sgPath) && isValidBinary(sgPath)) {
|
|
16380
17672
|
return sgPath;
|
|
16381
17673
|
}
|
|
16382
17674
|
} catch {}
|
|
@@ -16387,8 +17679,8 @@ function findSgCliPathSync() {
|
|
|
16387
17679
|
const pkgPath = require2.resolve(`${platformPkg}/package.json`);
|
|
16388
17680
|
const pkgDir = dirname3(pkgPath);
|
|
16389
17681
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
|
|
16390
|
-
const binaryPath =
|
|
16391
|
-
if (
|
|
17682
|
+
const binaryPath = join22(pkgDir, astGrepName);
|
|
17683
|
+
if (existsSync20(binaryPath) && isValidBinary(binaryPath)) {
|
|
16392
17684
|
return binaryPath;
|
|
16393
17685
|
}
|
|
16394
17686
|
} catch {}
|
|
@@ -16396,7 +17688,7 @@ function findSgCliPathSync() {
|
|
|
16396
17688
|
if (process.platform === "darwin") {
|
|
16397
17689
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
16398
17690
|
for (const path3 of homebrewPaths) {
|
|
16399
|
-
if (
|
|
17691
|
+
if (existsSync20(path3) && isValidBinary(path3)) {
|
|
16400
17692
|
return path3;
|
|
16401
17693
|
}
|
|
16402
17694
|
}
|
|
@@ -16451,12 +17743,12 @@ var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
|
16451
17743
|
var DEFAULT_MAX_MATCHES = 500;
|
|
16452
17744
|
|
|
16453
17745
|
// src/tools/ast-grep/cli.ts
|
|
16454
|
-
var {spawn:
|
|
16455
|
-
import { existsSync as
|
|
17746
|
+
var {spawn: spawn6 } = globalThis.Bun;
|
|
17747
|
+
import { existsSync as existsSync21 } from "fs";
|
|
16456
17748
|
var resolvedCliPath3 = null;
|
|
16457
17749
|
var initPromise2 = null;
|
|
16458
17750
|
async function getAstGrepPath() {
|
|
16459
|
-
if (resolvedCliPath3 !== null &&
|
|
17751
|
+
if (resolvedCliPath3 !== null && existsSync21(resolvedCliPath3)) {
|
|
16460
17752
|
return resolvedCliPath3;
|
|
16461
17753
|
}
|
|
16462
17754
|
if (initPromise2) {
|
|
@@ -16464,7 +17756,7 @@ async function getAstGrepPath() {
|
|
|
16464
17756
|
}
|
|
16465
17757
|
initPromise2 = (async () => {
|
|
16466
17758
|
const syncPath = findSgCliPathSync();
|
|
16467
|
-
if (syncPath &&
|
|
17759
|
+
if (syncPath && existsSync21(syncPath)) {
|
|
16468
17760
|
resolvedCliPath3 = syncPath;
|
|
16469
17761
|
setSgCliPath(syncPath);
|
|
16470
17762
|
return syncPath;
|
|
@@ -16498,14 +17790,14 @@ async function runSg(options) {
|
|
|
16498
17790
|
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."];
|
|
16499
17791
|
args.push(...paths);
|
|
16500
17792
|
let cliPath = getSgCliPath();
|
|
16501
|
-
if (!
|
|
17793
|
+
if (!existsSync21(cliPath) && cliPath !== "sg") {
|
|
16502
17794
|
const downloadedPath = await getAstGrepPath();
|
|
16503
17795
|
if (downloadedPath) {
|
|
16504
17796
|
cliPath = downloadedPath;
|
|
16505
17797
|
}
|
|
16506
17798
|
}
|
|
16507
17799
|
const timeout = DEFAULT_TIMEOUT_MS;
|
|
16508
|
-
const proc =
|
|
17800
|
+
const proc = spawn6([cliPath, ...args], {
|
|
16509
17801
|
stdout: "pipe",
|
|
16510
17802
|
stderr: "pipe"
|
|
16511
17803
|
});
|
|
@@ -16759,11 +18051,11 @@ var ast_grep_replace = tool({
|
|
|
16759
18051
|
}
|
|
16760
18052
|
});
|
|
16761
18053
|
// src/tools/grep/cli.ts
|
|
16762
|
-
var {spawn:
|
|
18054
|
+
var {spawn: spawn7 } = globalThis.Bun;
|
|
16763
18055
|
|
|
16764
18056
|
// src/tools/grep/constants.ts
|
|
16765
|
-
import { existsSync as
|
|
16766
|
-
import { join as
|
|
18057
|
+
import { existsSync as existsSync22 } from "fs";
|
|
18058
|
+
import { join as join23, dirname as dirname4 } from "path";
|
|
16767
18059
|
import { spawnSync } from "child_process";
|
|
16768
18060
|
var cachedCli = null;
|
|
16769
18061
|
function findExecutable(name) {
|
|
@@ -16784,13 +18076,13 @@ function getOpenCodeBundledRg() {
|
|
|
16784
18076
|
const isWindows = process.platform === "win32";
|
|
16785
18077
|
const rgName = isWindows ? "rg.exe" : "rg";
|
|
16786
18078
|
const candidates = [
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
18079
|
+
join23(execDir, rgName),
|
|
18080
|
+
join23(execDir, "bin", rgName),
|
|
18081
|
+
join23(execDir, "..", "bin", rgName),
|
|
18082
|
+
join23(execDir, "..", "libexec", rgName)
|
|
16791
18083
|
];
|
|
16792
18084
|
for (const candidate of candidates) {
|
|
16793
|
-
if (
|
|
18085
|
+
if (existsSync22(candidate)) {
|
|
16794
18086
|
return candidate;
|
|
16795
18087
|
}
|
|
16796
18088
|
}
|
|
@@ -16931,7 +18223,7 @@ async function runRg(options) {
|
|
|
16931
18223
|
}
|
|
16932
18224
|
const paths = options.paths?.length ? options.paths : ["."];
|
|
16933
18225
|
args.push(...paths);
|
|
16934
|
-
const proc =
|
|
18226
|
+
const proc = spawn7([cli.path, ...args], {
|
|
16935
18227
|
stdout: "pipe",
|
|
16936
18228
|
stderr: "pipe"
|
|
16937
18229
|
});
|
|
@@ -17033,7 +18325,7 @@ var grep = tool({
|
|
|
17033
18325
|
});
|
|
17034
18326
|
|
|
17035
18327
|
// src/tools/glob/cli.ts
|
|
17036
|
-
var {spawn:
|
|
18328
|
+
var {spawn: spawn8 } = globalThis.Bun;
|
|
17037
18329
|
|
|
17038
18330
|
// src/tools/glob/constants.ts
|
|
17039
18331
|
var DEFAULT_TIMEOUT_MS3 = 60000;
|
|
@@ -17090,7 +18382,7 @@ async function runRgFiles(options) {
|
|
|
17090
18382
|
args.push(...paths);
|
|
17091
18383
|
}
|
|
17092
18384
|
const cwd = paths[0] || ".";
|
|
17093
|
-
const proc =
|
|
18385
|
+
const proc = spawn8([cli.path, ...args], {
|
|
17094
18386
|
stdout: "pipe",
|
|
17095
18387
|
stderr: "pipe",
|
|
17096
18388
|
cwd: isRg ? undefined : cwd
|
|
@@ -17188,145 +18480,14 @@ var glob = tool({
|
|
|
17188
18480
|
}
|
|
17189
18481
|
});
|
|
17190
18482
|
// src/tools/slashcommand/tools.ts
|
|
17191
|
-
import { existsSync as
|
|
17192
|
-
import { homedir as
|
|
17193
|
-
import { join as
|
|
17194
|
-
// src/shared/command-executor.ts
|
|
17195
|
-
import { exec } from "child_process";
|
|
17196
|
-
import { promisify } from "util";
|
|
17197
|
-
var execAsync = promisify(exec);
|
|
17198
|
-
async function executeCommand(command) {
|
|
17199
|
-
try {
|
|
17200
|
-
const { stdout, stderr } = await execAsync(command);
|
|
17201
|
-
const out = stdout?.toString().trim() ?? "";
|
|
17202
|
-
const err = stderr?.toString().trim() ?? "";
|
|
17203
|
-
if (err) {
|
|
17204
|
-
if (out) {
|
|
17205
|
-
return `${out}
|
|
17206
|
-
[stderr: ${err}]`;
|
|
17207
|
-
}
|
|
17208
|
-
return `[stderr: ${err}]`;
|
|
17209
|
-
}
|
|
17210
|
-
return out;
|
|
17211
|
-
} catch (error45) {
|
|
17212
|
-
const e = error45;
|
|
17213
|
-
const stdout = e?.stdout?.toString().trim() ?? "";
|
|
17214
|
-
const stderr = e?.stderr?.toString().trim() ?? "";
|
|
17215
|
-
const errMsg = stderr || e?.message || String(error45);
|
|
17216
|
-
if (stdout) {
|
|
17217
|
-
return `${stdout}
|
|
17218
|
-
[stderr: ${errMsg}]`;
|
|
17219
|
-
}
|
|
17220
|
-
return `[stderr: ${errMsg}]`;
|
|
17221
|
-
}
|
|
17222
|
-
}
|
|
17223
|
-
var COMMAND_PATTERN = /!`([^`]+)`/g;
|
|
17224
|
-
function findCommands(text) {
|
|
17225
|
-
const matches = [];
|
|
17226
|
-
let match;
|
|
17227
|
-
COMMAND_PATTERN.lastIndex = 0;
|
|
17228
|
-
while ((match = COMMAND_PATTERN.exec(text)) !== null) {
|
|
17229
|
-
matches.push({
|
|
17230
|
-
fullMatch: match[0],
|
|
17231
|
-
command: match[1],
|
|
17232
|
-
start: match.index,
|
|
17233
|
-
end: match.index + match[0].length
|
|
17234
|
-
});
|
|
17235
|
-
}
|
|
17236
|
-
return matches;
|
|
17237
|
-
}
|
|
17238
|
-
async function resolveCommandsInText(text, depth = 0, maxDepth = 3) {
|
|
17239
|
-
if (depth >= maxDepth) {
|
|
17240
|
-
return text;
|
|
17241
|
-
}
|
|
17242
|
-
const matches = findCommands(text);
|
|
17243
|
-
if (matches.length === 0) {
|
|
17244
|
-
return text;
|
|
17245
|
-
}
|
|
17246
|
-
const tasks = matches.map((m) => executeCommand(m.command));
|
|
17247
|
-
const results = await Promise.allSettled(tasks);
|
|
17248
|
-
const replacements = new Map;
|
|
17249
|
-
matches.forEach((match, idx) => {
|
|
17250
|
-
const result = results[idx];
|
|
17251
|
-
if (result.status === "rejected") {
|
|
17252
|
-
replacements.set(match.fullMatch, `[error: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}]`);
|
|
17253
|
-
} else {
|
|
17254
|
-
replacements.set(match.fullMatch, result.value);
|
|
17255
|
-
}
|
|
17256
|
-
});
|
|
17257
|
-
let resolved = text;
|
|
17258
|
-
for (const [pattern, replacement] of replacements.entries()) {
|
|
17259
|
-
resolved = resolved.split(pattern).join(replacement);
|
|
17260
|
-
}
|
|
17261
|
-
if (findCommands(resolved).length > 0) {
|
|
17262
|
-
return resolveCommandsInText(resolved, depth + 1, maxDepth);
|
|
17263
|
-
}
|
|
17264
|
-
return resolved;
|
|
17265
|
-
}
|
|
17266
|
-
// src/shared/file-reference-resolver.ts
|
|
17267
|
-
import { existsSync as existsSync17, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
|
|
17268
|
-
import { join as join17, isAbsolute } from "path";
|
|
17269
|
-
var FILE_REFERENCE_PATTERN = /@([^\s@]+)/g;
|
|
17270
|
-
function findFileReferences(text) {
|
|
17271
|
-
const matches = [];
|
|
17272
|
-
let match;
|
|
17273
|
-
FILE_REFERENCE_PATTERN.lastIndex = 0;
|
|
17274
|
-
while ((match = FILE_REFERENCE_PATTERN.exec(text)) !== null) {
|
|
17275
|
-
matches.push({
|
|
17276
|
-
fullMatch: match[0],
|
|
17277
|
-
filePath: match[1],
|
|
17278
|
-
start: match.index,
|
|
17279
|
-
end: match.index + match[0].length
|
|
17280
|
-
});
|
|
17281
|
-
}
|
|
17282
|
-
return matches;
|
|
17283
|
-
}
|
|
17284
|
-
function resolveFilePath(filePath, cwd) {
|
|
17285
|
-
if (isAbsolute(filePath)) {
|
|
17286
|
-
return filePath;
|
|
17287
|
-
}
|
|
17288
|
-
return join17(cwd, filePath);
|
|
17289
|
-
}
|
|
17290
|
-
function readFileContent(resolvedPath) {
|
|
17291
|
-
if (!existsSync17(resolvedPath)) {
|
|
17292
|
-
return `[file not found: ${resolvedPath}]`;
|
|
17293
|
-
}
|
|
17294
|
-
const stat2 = statSync3(resolvedPath);
|
|
17295
|
-
if (stat2.isDirectory()) {
|
|
17296
|
-
return `[cannot read directory: ${resolvedPath}]`;
|
|
17297
|
-
}
|
|
17298
|
-
const content = readFileSync10(resolvedPath, "utf-8");
|
|
17299
|
-
return content;
|
|
17300
|
-
}
|
|
17301
|
-
async function resolveFileReferencesInText(text, cwd = process.cwd(), depth = 0, maxDepth = 3) {
|
|
17302
|
-
if (depth >= maxDepth) {
|
|
17303
|
-
return text;
|
|
17304
|
-
}
|
|
17305
|
-
const matches = findFileReferences(text);
|
|
17306
|
-
if (matches.length === 0) {
|
|
17307
|
-
return text;
|
|
17308
|
-
}
|
|
17309
|
-
const replacements = new Map;
|
|
17310
|
-
for (const match of matches) {
|
|
17311
|
-
const resolvedPath = resolveFilePath(match.filePath, cwd);
|
|
17312
|
-
const content = readFileContent(resolvedPath);
|
|
17313
|
-
replacements.set(match.fullMatch, content);
|
|
17314
|
-
}
|
|
17315
|
-
let resolved = text;
|
|
17316
|
-
for (const [pattern, replacement] of replacements.entries()) {
|
|
17317
|
-
resolved = resolved.split(pattern).join(replacement);
|
|
17318
|
-
}
|
|
17319
|
-
if (findFileReferences(resolved).length > 0 && depth + 1 < maxDepth) {
|
|
17320
|
-
return resolveFileReferencesInText(resolved, cwd, depth + 1, maxDepth);
|
|
17321
|
-
}
|
|
17322
|
-
return resolved;
|
|
17323
|
-
}
|
|
17324
|
-
// src/tools/slashcommand/tools.ts
|
|
18483
|
+
import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync12 } from "fs";
|
|
18484
|
+
import { homedir as homedir13 } from "os";
|
|
18485
|
+
import { join as join24, basename as basename3, dirname as dirname5 } from "path";
|
|
17325
18486
|
function discoverCommandsFromDir(commandsDir, scope) {
|
|
17326
|
-
if (!
|
|
18487
|
+
if (!existsSync23(commandsDir)) {
|
|
17327
18488
|
return [];
|
|
17328
18489
|
}
|
|
17329
|
-
const entries =
|
|
18490
|
+
const entries = readdirSync6(commandsDir, { withFileTypes: true });
|
|
17330
18491
|
const commands = [];
|
|
17331
18492
|
for (const entry of entries) {
|
|
17332
18493
|
if (entry.name.startsWith("."))
|
|
@@ -17335,10 +18496,10 @@ function discoverCommandsFromDir(commandsDir, scope) {
|
|
|
17335
18496
|
continue;
|
|
17336
18497
|
if (!entry.isFile())
|
|
17337
18498
|
continue;
|
|
17338
|
-
const commandPath =
|
|
18499
|
+
const commandPath = join24(commandsDir, entry.name);
|
|
17339
18500
|
const commandName = basename3(entry.name, ".md");
|
|
17340
18501
|
try {
|
|
17341
|
-
const content =
|
|
18502
|
+
const content = readFileSync12(commandPath, "utf-8");
|
|
17342
18503
|
const { data, body } = parseFrontmatter(content);
|
|
17343
18504
|
const metadata = {
|
|
17344
18505
|
name: commandName,
|
|
@@ -17362,10 +18523,10 @@ function discoverCommandsFromDir(commandsDir, scope) {
|
|
|
17362
18523
|
return commands;
|
|
17363
18524
|
}
|
|
17364
18525
|
function discoverCommandsSync() {
|
|
17365
|
-
const userCommandsDir =
|
|
17366
|
-
const projectCommandsDir =
|
|
17367
|
-
const opencodeGlobalDir =
|
|
17368
|
-
const opencodeProjectDir =
|
|
18526
|
+
const userCommandsDir = join24(homedir13(), ".claude", "commands");
|
|
18527
|
+
const projectCommandsDir = join24(process.cwd(), ".claude", "commands");
|
|
18528
|
+
const opencodeGlobalDir = join24(homedir13(), ".config", "opencode", "command");
|
|
18529
|
+
const opencodeProjectDir = join24(process.cwd(), ".opencode", "command");
|
|
17369
18530
|
const userCommands = discoverCommandsFromDir(userCommandsDir, "user");
|
|
17370
18531
|
const opencodeGlobalCommands = discoverCommandsFromDir(opencodeGlobalDir, "opencode");
|
|
17371
18532
|
const projectCommands = discoverCommandsFromDir(projectCommandsDir, "project");
|
|
@@ -17489,19 +18650,19 @@ Try a different command name.`;
|
|
|
17489
18650
|
}
|
|
17490
18651
|
});
|
|
17491
18652
|
// src/tools/skill/tools.ts
|
|
17492
|
-
import { existsSync as
|
|
17493
|
-
import { homedir as
|
|
17494
|
-
import { join as
|
|
18653
|
+
import { existsSync as existsSync24, readdirSync as readdirSync7, statSync as statSync4, readlinkSync as readlinkSync2, readFileSync as readFileSync13 } from "fs";
|
|
18654
|
+
import { homedir as homedir14 } from "os";
|
|
18655
|
+
import { join as join25, resolve as resolve5, basename as basename4 } from "path";
|
|
17495
18656
|
function discoverSkillsFromDir(skillsDir, scope) {
|
|
17496
|
-
if (!
|
|
18657
|
+
if (!existsSync24(skillsDir)) {
|
|
17497
18658
|
return [];
|
|
17498
18659
|
}
|
|
17499
|
-
const entries =
|
|
18660
|
+
const entries = readdirSync7(skillsDir, { withFileTypes: true });
|
|
17500
18661
|
const skills = [];
|
|
17501
18662
|
for (const entry of entries) {
|
|
17502
18663
|
if (entry.name.startsWith("."))
|
|
17503
18664
|
continue;
|
|
17504
|
-
const skillPath =
|
|
18665
|
+
const skillPath = join25(skillsDir, entry.name);
|
|
17505
18666
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
17506
18667
|
let resolvedPath = skillPath;
|
|
17507
18668
|
try {
|
|
@@ -17512,11 +18673,11 @@ function discoverSkillsFromDir(skillsDir, scope) {
|
|
|
17512
18673
|
} catch {
|
|
17513
18674
|
continue;
|
|
17514
18675
|
}
|
|
17515
|
-
const skillMdPath =
|
|
17516
|
-
if (!
|
|
18676
|
+
const skillMdPath = join25(resolvedPath, "SKILL.md");
|
|
18677
|
+
if (!existsSync24(skillMdPath))
|
|
17517
18678
|
continue;
|
|
17518
18679
|
try {
|
|
17519
|
-
const content =
|
|
18680
|
+
const content = readFileSync13(skillMdPath, "utf-8");
|
|
17520
18681
|
const { data } = parseFrontmatter(content);
|
|
17521
18682
|
skills.push({
|
|
17522
18683
|
name: data.name || entry.name,
|
|
@@ -17531,8 +18692,8 @@ function discoverSkillsFromDir(skillsDir, scope) {
|
|
|
17531
18692
|
return skills;
|
|
17532
18693
|
}
|
|
17533
18694
|
function discoverSkillsSync() {
|
|
17534
|
-
const userSkillsDir =
|
|
17535
|
-
const projectSkillsDir =
|
|
18695
|
+
const userSkillsDir = join25(homedir14(), ".claude", "skills");
|
|
18696
|
+
const projectSkillsDir = join25(process.cwd(), ".claude", "skills");
|
|
17536
18697
|
const userSkills = discoverSkillsFromDir(userSkillsDir, "user");
|
|
17537
18698
|
const projectSkills = discoverSkillsFromDir(projectSkillsDir, "project");
|
|
17538
18699
|
return [...projectSkills, ...userSkills];
|
|
@@ -17553,12 +18714,12 @@ function resolveSymlink(skillPath) {
|
|
|
17553
18714
|
}
|
|
17554
18715
|
async function parseSkillMd(skillPath) {
|
|
17555
18716
|
const resolvedPath = resolveSymlink(skillPath);
|
|
17556
|
-
const skillMdPath =
|
|
17557
|
-
if (!
|
|
18717
|
+
const skillMdPath = join25(resolvedPath, "SKILL.md");
|
|
18718
|
+
if (!existsSync24(skillMdPath)) {
|
|
17558
18719
|
return null;
|
|
17559
18720
|
}
|
|
17560
18721
|
try {
|
|
17561
|
-
let content =
|
|
18722
|
+
let content = readFileSync13(skillMdPath, "utf-8");
|
|
17562
18723
|
content = await resolveCommandsInText(content);
|
|
17563
18724
|
const { data, body } = parseFrontmatter(content);
|
|
17564
18725
|
const metadata = {
|
|
@@ -17566,12 +18727,12 @@ async function parseSkillMd(skillPath) {
|
|
|
17566
18727
|
description: data.description || "",
|
|
17567
18728
|
license: data.license
|
|
17568
18729
|
};
|
|
17569
|
-
const referencesDir =
|
|
17570
|
-
const scriptsDir =
|
|
17571
|
-
const assetsDir =
|
|
17572
|
-
const references =
|
|
17573
|
-
const scripts =
|
|
17574
|
-
const assets =
|
|
18730
|
+
const referencesDir = join25(resolvedPath, "references");
|
|
18731
|
+
const scriptsDir = join25(resolvedPath, "scripts");
|
|
18732
|
+
const assetsDir = join25(resolvedPath, "assets");
|
|
18733
|
+
const references = existsSync24(referencesDir) ? readdirSync7(referencesDir).filter((f) => !f.startsWith(".")) : [];
|
|
18734
|
+
const scripts = existsSync24(scriptsDir) ? readdirSync7(scriptsDir).filter((f) => !f.startsWith(".") && !f.startsWith("__")) : [];
|
|
18735
|
+
const assets = existsSync24(assetsDir) ? readdirSync7(assetsDir).filter((f) => !f.startsWith(".")) : [];
|
|
17575
18736
|
return {
|
|
17576
18737
|
name: metadata.name,
|
|
17577
18738
|
path: resolvedPath,
|
|
@@ -17586,15 +18747,15 @@ async function parseSkillMd(skillPath) {
|
|
|
17586
18747
|
}
|
|
17587
18748
|
}
|
|
17588
18749
|
async function discoverSkillsFromDirAsync(skillsDir) {
|
|
17589
|
-
if (!
|
|
18750
|
+
if (!existsSync24(skillsDir)) {
|
|
17590
18751
|
return [];
|
|
17591
18752
|
}
|
|
17592
|
-
const entries =
|
|
18753
|
+
const entries = readdirSync7(skillsDir, { withFileTypes: true });
|
|
17593
18754
|
const skills = [];
|
|
17594
18755
|
for (const entry of entries) {
|
|
17595
18756
|
if (entry.name.startsWith("."))
|
|
17596
18757
|
continue;
|
|
17597
|
-
const skillPath =
|
|
18758
|
+
const skillPath = join25(skillsDir, entry.name);
|
|
17598
18759
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
17599
18760
|
const skillInfo = await parseSkillMd(skillPath);
|
|
17600
18761
|
if (skillInfo) {
|
|
@@ -17605,8 +18766,8 @@ async function discoverSkillsFromDirAsync(skillsDir) {
|
|
|
17605
18766
|
return skills;
|
|
17606
18767
|
}
|
|
17607
18768
|
async function discoverSkills() {
|
|
17608
|
-
const userSkillsDir =
|
|
17609
|
-
const projectSkillsDir =
|
|
18769
|
+
const userSkillsDir = join25(homedir14(), ".claude", "skills");
|
|
18770
|
+
const projectSkillsDir = join25(process.cwd(), ".claude", "skills");
|
|
17610
18771
|
const userSkills = await discoverSkillsFromDirAsync(userSkillsDir);
|
|
17611
18772
|
const projectSkills = await discoverSkillsFromDirAsync(projectSkillsDir);
|
|
17612
18773
|
return [...projectSkills, ...userSkills];
|
|
@@ -17635,9 +18796,9 @@ async function loadSkillWithReferences(skill, includeRefs) {
|
|
|
17635
18796
|
const referencesLoaded = [];
|
|
17636
18797
|
if (includeRefs && skill.references.length > 0) {
|
|
17637
18798
|
for (const ref of skill.references) {
|
|
17638
|
-
const refPath =
|
|
18799
|
+
const refPath = join25(skill.path, "references", ref);
|
|
17639
18800
|
try {
|
|
17640
|
-
let content =
|
|
18801
|
+
let content = readFileSync13(refPath, "utf-8");
|
|
17641
18802
|
content = await resolveCommandsInText(content);
|
|
17642
18803
|
referencesLoaded.push({ path: ref, content });
|
|
17643
18804
|
} catch {}
|
|
@@ -17870,6 +19031,7 @@ function loadPluginConfig(directory) {
|
|
|
17870
19031
|
return {};
|
|
17871
19032
|
}
|
|
17872
19033
|
var OhMyOpenCodePlugin = async (ctx) => {
|
|
19034
|
+
const pluginConfig = loadPluginConfig(ctx.directory);
|
|
17873
19035
|
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx);
|
|
17874
19036
|
const contextWindowMonitor = createContextWindowMonitorHook(ctx);
|
|
17875
19037
|
const sessionRecovery = createSessionRecoveryHook(ctx);
|
|
@@ -17878,19 +19040,22 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
17878
19040
|
const directoryAgentsInjector = createDirectoryAgentsInjectorHook(ctx);
|
|
17879
19041
|
const emptyTaskResponseDetector = createEmptyTaskResponseDetectorHook(ctx);
|
|
17880
19042
|
const thinkMode = createThinkModeHook();
|
|
19043
|
+
const claudeCodeHooks = createClaudeCodeHooksHook(ctx, {});
|
|
17881
19044
|
updateTerminalTitle({ sessionId: "main" });
|
|
17882
|
-
const pluginConfig = loadPluginConfig(ctx.directory);
|
|
17883
19045
|
return {
|
|
17884
19046
|
tool: builtinTools,
|
|
19047
|
+
"chat.message": async (input, output) => {
|
|
19048
|
+
await claudeCodeHooks["chat.message"]?.(input, output);
|
|
19049
|
+
},
|
|
17885
19050
|
config: async (config3) => {
|
|
17886
19051
|
const builtinAgents = createBuiltinAgents(pluginConfig.disabled_agents, pluginConfig.agents);
|
|
17887
19052
|
const userAgents = loadUserAgents();
|
|
17888
19053
|
const projectAgents = loadProjectAgents();
|
|
17889
19054
|
config3.agent = {
|
|
17890
|
-
...config3.agent,
|
|
17891
19055
|
...builtinAgents,
|
|
17892
19056
|
...userAgents,
|
|
17893
|
-
...projectAgents
|
|
19057
|
+
...projectAgents,
|
|
19058
|
+
...config3.agent
|
|
17894
19059
|
};
|
|
17895
19060
|
config3.tools = {
|
|
17896
19061
|
...config3.tools
|
|
@@ -17919,6 +19084,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
17919
19084
|
};
|
|
17920
19085
|
},
|
|
17921
19086
|
event: async (input) => {
|
|
19087
|
+
await claudeCodeHooks.event(input);
|
|
17922
19088
|
await todoContinuationEnforcer(input);
|
|
17923
19089
|
await contextWindowMonitor.event(input);
|
|
17924
19090
|
await directoryAgentsInjector.event(input);
|
|
@@ -18002,6 +19168,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
18002
19168
|
}
|
|
18003
19169
|
},
|
|
18004
19170
|
"tool.execute.before": async (input, output) => {
|
|
19171
|
+
await claudeCodeHooks["tool.execute.before"](input, output);
|
|
18005
19172
|
await commentChecker["tool.execute.before"](input, output);
|
|
18006
19173
|
if (input.sessionID === getMainSessionID()) {
|
|
18007
19174
|
updateTerminalTitle({
|
|
@@ -18014,6 +19181,7 @@ var OhMyOpenCodePlugin = async (ctx) => {
|
|
|
18014
19181
|
}
|
|
18015
19182
|
},
|
|
18016
19183
|
"tool.execute.after": async (input, output) => {
|
|
19184
|
+
await claudeCodeHooks["tool.execute.after"](input, output);
|
|
18017
19185
|
await grepOutputTruncator["tool.execute.after"](input, output);
|
|
18018
19186
|
await contextWindowMonitor["tool.execute.after"](input, output);
|
|
18019
19187
|
await commentChecker["tool.execute.after"](input, output);
|