@webpresso/plugin-opencode 0.0.12 → 0.0.14
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/bin/wp +250 -0
- package/index.js +1216 -173
- package/package.json +7 -4
- package/plugin-skill-ownership.json +14 -17
- package/skills/autopilot/SKILL.md +2 -2
- package/skills/best-practice-research/SKILL.md +8 -0
- package/skills/claude/SKILL.md +6 -3
- package/skills/codex/SKILL.md +6 -3
- package/skills/deep-research/SKILL.md +1 -0
- package/skills/handoff/SKILL.md +2 -2
- package/skills/hooks-doctor/SKILL.md +42 -5
- package/skills/opencode-go/SKILL.md +7 -4
- package/skills/ralplan/SKILL.md +19 -5
- package/skills/tech-debt/SKILL.md +7 -2
- package/skills/tooling-friction/SKILL.md +17 -14
- package/skills/ultragoal/SKILL.md +13 -20
- package/skills/verify/SKILL.md +53 -57
- package/skills/grok/SKILL.md +0 -44
package/index.js
CHANGED
|
@@ -1,44 +1,807 @@
|
|
|
1
|
-
|
|
1
|
+
// src/host-adapters/opencode-plugin.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
accessSync,
|
|
5
|
+
chmodSync,
|
|
6
|
+
constants,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
rmdirSync,
|
|
11
|
+
statSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
2
15
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
16
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
4
18
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
19
|
+
// src/hooks/lane/hook-bundle-manifest.ts
|
|
20
|
+
var HOOK_BUNDLE_MANIFEST_VERSION = 1;
|
|
21
|
+
var MAX_HOOK_BUNDLE_FILES = 4096;
|
|
22
|
+
var MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
function statHookBundleFile(host, path) {
|
|
24
|
+
try {
|
|
25
|
+
const stats = host.statSync(path);
|
|
26
|
+
return stats.isFile() ? { path, mtimeMs: stats.mtimeMs, size: stats.size } : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function isFileStat(host, value) {
|
|
32
|
+
if (typeof value !== "object" || value === null) return false;
|
|
33
|
+
const record = value;
|
|
34
|
+
return (
|
|
35
|
+
typeof record.path === "string" &&
|
|
36
|
+
host.isAbsolute(record.path) &&
|
|
37
|
+
typeof record.mtimeMs === "number" &&
|
|
38
|
+
typeof record.size === "number"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
function isFileStatList(host, value) {
|
|
42
|
+
return (
|
|
43
|
+
Array.isArray(value) &&
|
|
44
|
+
value.length <= MAX_HOOK_BUNDLE_FILES &&
|
|
45
|
+
value.every((entry) => isFileStat(host, entry))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
function parseHookBundleManifest(host, text) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(text);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
56
|
+
const record = parsed;
|
|
57
|
+
if (
|
|
58
|
+
record.version !== HOOK_BUNDLE_MANIFEST_VERSION ||
|
|
59
|
+
typeof record.entry !== "string" ||
|
|
60
|
+
typeof record.bun !== "string" ||
|
|
61
|
+
!isFileStatList(host, record.inputs) ||
|
|
62
|
+
!isFileStatList(host, record.outputs) ||
|
|
63
|
+
record.outputs.length === 0
|
|
64
|
+
) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
version: HOOK_BUNDLE_MANIFEST_VERSION,
|
|
69
|
+
entry: record.entry,
|
|
70
|
+
bun: record.bun,
|
|
71
|
+
inputs: record.inputs,
|
|
72
|
+
outputs: record.outputs,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function readHookBundleManifest(host, manifestPath) {
|
|
76
|
+
const stat = statHookBundleFile(host, manifestPath);
|
|
77
|
+
if (stat === null || stat.size > MAX_MANIFEST_BYTES) return null;
|
|
78
|
+
try {
|
|
79
|
+
return parseHookBundleManifest(host, host.readFileSync(manifestPath, "utf8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function isUnchanged(host, recorded) {
|
|
85
|
+
const current = statHookBundleFile(host, recorded.path);
|
|
86
|
+
return current !== null && current.mtimeMs === recorded.mtimeMs && current.size === recorded.size;
|
|
87
|
+
}
|
|
88
|
+
function isHookBundleManifestFresh(host, manifest) {
|
|
89
|
+
const unchanged = (recorded) => isUnchanged(host, recorded);
|
|
90
|
+
return manifest.inputs.every(unchanged) && manifest.outputs.every(unchanged);
|
|
91
|
+
}
|
|
8
92
|
|
|
9
|
-
|
|
10
|
-
|
|
93
|
+
// src/host-adapters/opencode-output-filter.ts
|
|
94
|
+
var REDIRECT_STDERR = "2>&1";
|
|
95
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
96
|
+
var DURATION = /^\d+(?:\.\d+)?[smhd]?$/;
|
|
97
|
+
var REFUSED_BARE = new Set(["$", "`", "<", ">", "(", ")", "{", "}", "\\", "#", "!"]);
|
|
98
|
+
var REFUSED_DOUBLE_QUOTED = new Set(["$", "`", "\\"]);
|
|
99
|
+
var WORD_TERMINATORS = new Set([" ", "\t", "&", "|", ";"]);
|
|
100
|
+
var SINGLE_HEADS = new Map([
|
|
101
|
+
["grep", "grep"],
|
|
102
|
+
["rg", "rg"],
|
|
103
|
+
["find", "find"],
|
|
104
|
+
["fd", "fd"],
|
|
105
|
+
["vitest", "vitest"],
|
|
106
|
+
["tsc", "tsc"],
|
|
107
|
+
["pytest", "pytest"],
|
|
108
|
+
["mypy", "mypy"],
|
|
109
|
+
["prettier", "prettier"],
|
|
110
|
+
]);
|
|
111
|
+
var PAIR_HEADS = new Map([
|
|
112
|
+
["git log", "git-log"],
|
|
113
|
+
["git diff", "git-diff"],
|
|
114
|
+
["git status", "git-status"],
|
|
115
|
+
["cargo test", "cargo-test"],
|
|
116
|
+
["go test", "go-test"],
|
|
117
|
+
["go build", "go-build"],
|
|
118
|
+
["ruff check", "ruff-check"],
|
|
119
|
+
["ruff format", "ruff-format"],
|
|
120
|
+
]);
|
|
121
|
+
function hasControlCharacter(input) {
|
|
122
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
123
|
+
const code = input.charCodeAt(index);
|
|
124
|
+
if ((code < 32 && code !== 9) || code === 127) return true;
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function scanSingleQuoted(input, start) {
|
|
129
|
+
const close = input.indexOf("'", start + 1);
|
|
130
|
+
return close === -1 ? null : close + 1;
|
|
131
|
+
}
|
|
132
|
+
function scanDoubleQuoted(input, start) {
|
|
133
|
+
for (let index = start + 1; index < input.length; index += 1) {
|
|
134
|
+
const char = input.charAt(index);
|
|
135
|
+
if (char === '"') return index + 1;
|
|
136
|
+
if (REFUSED_DOUBLE_QUOTED.has(char)) return null;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
function scanWord(input, start) {
|
|
141
|
+
let index = start;
|
|
142
|
+
let bare = true;
|
|
143
|
+
while (index < input.length && !WORD_TERMINATORS.has(input.charAt(index))) {
|
|
144
|
+
const char = input.charAt(index);
|
|
145
|
+
const quoted = char === "'" || char === '"';
|
|
146
|
+
if (!quoted && REFUSED_BARE.has(char)) return null;
|
|
147
|
+
const next = quoted
|
|
148
|
+
? char === "'"
|
|
149
|
+
? scanSingleQuoted(input, index)
|
|
150
|
+
: scanDoubleQuoted(input, index)
|
|
151
|
+
: index + 1;
|
|
152
|
+
if (next === null) return null;
|
|
153
|
+
bare = bare && !quoted;
|
|
154
|
+
index = next;
|
|
155
|
+
}
|
|
156
|
+
if (index === start) return null;
|
|
157
|
+
return { token: { kind: "word", raw: input.slice(start, index), bare }, end: index };
|
|
158
|
+
}
|
|
159
|
+
function scanOperator(input, start) {
|
|
160
|
+
const char = input.charAt(start);
|
|
161
|
+
if (char === ";") return { token: { kind: "op", op: ";" }, end: start + 1 };
|
|
162
|
+
if (char === "&" && input.charAt(start + 1) === "&") {
|
|
163
|
+
return { token: { kind: "op", op: "&&" }, end: start + 2 };
|
|
164
|
+
}
|
|
165
|
+
if (char === "|" && input.charAt(start + 1) !== "|") {
|
|
166
|
+
return { token: { kind: "op", op: "|" }, end: start + 1 };
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
function scanRedirect(input, start) {
|
|
171
|
+
if (!input.startsWith(REDIRECT_STDERR, start)) return null;
|
|
172
|
+
const end = start + REDIRECT_STDERR.length;
|
|
173
|
+
const after = input.charAt(end);
|
|
174
|
+
return after === "" || after === " " || after === "\t"
|
|
175
|
+
? { token: { kind: "redirect" }, end }
|
|
176
|
+
: null;
|
|
177
|
+
}
|
|
178
|
+
function scanToken(input, start) {
|
|
179
|
+
return scanOperator(input, start) ?? scanRedirect(input, start) ?? scanWord(input, start);
|
|
180
|
+
}
|
|
181
|
+
function tokenize(input) {
|
|
182
|
+
if (hasControlCharacter(input)) return null;
|
|
183
|
+
const tokens = [];
|
|
184
|
+
let index = 0;
|
|
185
|
+
while (index < input.length) {
|
|
186
|
+
const char = input.charAt(index);
|
|
187
|
+
if (char === " " || char === "\t") {
|
|
188
|
+
index += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const scanned = scanToken(input, index);
|
|
192
|
+
if (scanned === null) return null;
|
|
193
|
+
tokens.push(scanned.token);
|
|
194
|
+
index = scanned.end;
|
|
195
|
+
}
|
|
196
|
+
return tokens;
|
|
197
|
+
}
|
|
198
|
+
function splitSegments(tokens) {
|
|
199
|
+
const segments = [];
|
|
200
|
+
let current = [];
|
|
201
|
+
for (const token of tokens) {
|
|
202
|
+
if (token.kind === "op" && token.op === "|") return null;
|
|
203
|
+
if (token.kind !== "op") {
|
|
204
|
+
current.push(token);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (current.length === 0) return null;
|
|
208
|
+
segments.push(current);
|
|
209
|
+
current = [];
|
|
210
|
+
}
|
|
211
|
+
if (current.length === 0) return null;
|
|
212
|
+
segments.push(current);
|
|
213
|
+
return segments;
|
|
214
|
+
}
|
|
215
|
+
function isWord(token) {
|
|
216
|
+
return token?.kind === "word";
|
|
217
|
+
}
|
|
218
|
+
function isAssignment(token) {
|
|
219
|
+
return isWord(token) && ASSIGNMENT.test(token.raw);
|
|
220
|
+
}
|
|
221
|
+
function isBareWord(token, text) {
|
|
222
|
+
return isWord(token) && token.bare && token.raw === text;
|
|
223
|
+
}
|
|
224
|
+
function isPrefixSegment(segment) {
|
|
225
|
+
const [first, second] = segment;
|
|
226
|
+
if (segment.length === 1) return isAssignment(first);
|
|
227
|
+
if (segment.length !== 2 || !isWord(second)) return false;
|
|
228
|
+
if (isBareWord(first, "cd")) return true;
|
|
229
|
+
return isBareWord(first, "export") && isAssignment(second);
|
|
230
|
+
}
|
|
231
|
+
function producerWords(segment) {
|
|
232
|
+
const body = segment.at(-1)?.kind === "redirect" ? segment.slice(0, -1) : segment;
|
|
233
|
+
const words = body.filter(isWord);
|
|
234
|
+
return words.length === body.length ? words : null;
|
|
235
|
+
}
|
|
236
|
+
function skipTimeout(words) {
|
|
237
|
+
const [first, duration] = words;
|
|
238
|
+
if (!isBareWord(first, "timeout")) return words;
|
|
239
|
+
return isWord(duration) && DURATION.test(duration.raw) ? words.slice(2) : null;
|
|
240
|
+
}
|
|
241
|
+
function headFilter(words) {
|
|
242
|
+
const [first, second] = words;
|
|
243
|
+
if (!first?.bare) return null;
|
|
244
|
+
const single = SINGLE_HEADS.get(first.raw);
|
|
245
|
+
if (single !== undefined) return single;
|
|
246
|
+
if (!second?.bare) return null;
|
|
247
|
+
return PAIR_HEADS.get(`${first.raw} ${second.raw}`) ?? null;
|
|
248
|
+
}
|
|
249
|
+
function producerFilter(segment) {
|
|
250
|
+
const words = producerWords(segment);
|
|
251
|
+
if (words === null) return null;
|
|
252
|
+
const firstCommandWord = words.findIndex((word) => !ASSIGNMENT.test(word.raw));
|
|
253
|
+
if (firstCommandWord === -1) return null;
|
|
254
|
+
const afterTimeout = skipTimeout(words.slice(firstCommandWord));
|
|
255
|
+
return afterTimeout === null ? null : headFilter(afterTimeout);
|
|
256
|
+
}
|
|
257
|
+
function selectOutputFilter(command) {
|
|
258
|
+
const tokens = tokenize(command);
|
|
259
|
+
if (tokens === null) return null;
|
|
260
|
+
const segments = splitSegments(tokens);
|
|
261
|
+
const producer = segments?.at(-1);
|
|
262
|
+
if (segments === null || producer === undefined) return null;
|
|
263
|
+
if (!segments.slice(0, -1).every(isPrefixSegment)) return null;
|
|
264
|
+
return producerFilter(producer);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/host-adapters/opencode-session-budget.ts
|
|
268
|
+
var OPENCODE_SESSION_BUDGET = {
|
|
269
|
+
inputCeilingTokens: 200000,
|
|
270
|
+
generalSteps: 150,
|
|
271
|
+
};
|
|
272
|
+
function ceilingFor(limit) {
|
|
273
|
+
const window = limit.input ?? limit.context;
|
|
274
|
+
if (limit.context <= 0 || window <= 0) return null;
|
|
275
|
+
return window > OPENCODE_SESSION_BUDGET.inputCeilingTokens
|
|
276
|
+
? OPENCODE_SESSION_BUDGET.inputCeilingTokens
|
|
277
|
+
: null;
|
|
278
|
+
}
|
|
279
|
+
function stepsFor(current) {
|
|
280
|
+
return current === undefined ? OPENCODE_SESSION_BUDGET.generalSteps : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/host-adapters/opencode-plugin.ts
|
|
284
|
+
var OPENCODE_MANAGED_HOOK_COMMANDS = {
|
|
285
|
+
sessionStart: "sessionstart-routing",
|
|
286
|
+
preTool: "pretool-guard",
|
|
287
|
+
postTool: "post-tool",
|
|
288
|
+
};
|
|
289
|
+
var OPENCODE_HOOK_EVENT_NAMES = {
|
|
290
|
+
sessionStart: "SessionStart",
|
|
291
|
+
preToolUse: "PreToolUse",
|
|
292
|
+
postToolUse: "PostToolUse",
|
|
293
|
+
};
|
|
294
|
+
var OPENCODE_PLUGIN_ID = "@webpresso/plugin-opencode";
|
|
295
|
+
var OPENCODE_V2_DOMAIN_HOOKS = {
|
|
296
|
+
toolBefore: "execute.before",
|
|
297
|
+
toolAfter: "execute.after",
|
|
298
|
+
shellBefore: "create.before",
|
|
299
|
+
sessionCompaction: "compaction",
|
|
300
|
+
};
|
|
301
|
+
var OPENCODE_V2_SESSION_EVENTS = {
|
|
302
|
+
created: "session.created",
|
|
303
|
+
deleted: "session.deleted",
|
|
304
|
+
};
|
|
305
|
+
var OPENCODE_HOOK_ENV = {
|
|
306
|
+
projectDir: "CLAUDE_PROJECT_DIR",
|
|
307
|
+
hookHost: "WP_HOOK_HOST",
|
|
308
|
+
};
|
|
309
|
+
var OPENCODE_HOOK_HOST_NAME = "opencode";
|
|
310
|
+
var SESSION_START_HOOKS = [OPENCODE_MANAGED_HOOK_COMMANDS.sessionStart];
|
|
311
|
+
var PRE_TOOL_HOOKS = [OPENCODE_MANAGED_HOOK_COMMANDS.preTool];
|
|
312
|
+
var POST_TOOL_HOOKS = [OPENCODE_MANAGED_HOOK_COMMANDS.postTool];
|
|
313
|
+
var HOOK_BUDGET_MS = 5000;
|
|
314
|
+
var SESSION_LOOKUP_TIMEOUT_MS = HOOK_BUDGET_MS;
|
|
315
|
+
var HOOK_OUTPUT_CAP_BYTES = 1024 * 1024;
|
|
316
|
+
var MAX_TRACKED_SESSIONS = 64;
|
|
317
|
+
var MAX_CONTEXT_CHARS = 64 * 1024;
|
|
318
|
+
var DEFAULT_DENY_REASON = "webpresso denied this tool call";
|
|
319
|
+
var HOOK_LANE_ARGS = ["hook", "lane", "--json"];
|
|
320
|
+
var SKIP_UPDATE_CHECK_ENV = "WP_SKIP_UPDATE_CHECK";
|
|
321
|
+
var MAX_CACHED_LANES = 64;
|
|
322
|
+
var HOOK_LANE_REFRESH_INTERVAL_MS = 30000;
|
|
323
|
+
var HOOK_LANE_RETRY_COOLDOWN_MS = 60000;
|
|
324
|
+
var OPENCODE_HOOK_OWNER_PID_ENV = "WP_HOOK_OWNER_PID";
|
|
325
|
+
var HOOK_OWNER_PID_ENV = OPENCODE_HOOK_OWNER_PID_ENV;
|
|
326
|
+
var PLUGIN_DEBUG_ENV = "WP_OPENCODE_PLUGIN_DEBUG";
|
|
327
|
+
var SHELL_TOOL_ID = "shell";
|
|
328
|
+
var SHELL_FILTER_MIN_BYTES = 4096;
|
|
329
|
+
var RTK_PIPE_BUDGET_MS = 1500;
|
|
330
|
+
var RTK_FOOTER_MARKER = "[wp: rtk pipe";
|
|
331
|
+
var RAW_OUTPUT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
332
|
+
var RAW_OUTPUT_PRUNE_LIMIT = 500;
|
|
333
|
+
var RAW_OUTPUT_DIR_MODE = 448;
|
|
334
|
+
var RAW_OUTPUT_FILE_MODE = 384;
|
|
335
|
+
var RAW_OUTPUT_SEGMENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
336
|
+
var PACKAGED_WP_SHIM = join(dirname(fileURLToPath(import.meta.url)), "bin", "wp");
|
|
337
|
+
function resolveWpLauncher(platform = process.platform, env = process.env) {
|
|
338
|
+
if (platform !== "win32") return PACKAGED_WP_SHIM;
|
|
339
|
+
if (env.WEBPRESSO_WP_BIN && env.WP_FORCE_JIT_PATH) {
|
|
340
|
+
throw new Error("WEBPRESSO_WP_BIN and WP_FORCE_JIT_PATH are both set; unset one.");
|
|
341
|
+
}
|
|
342
|
+
if (env.WEBPRESSO_WP_BIN) return env.WEBPRESSO_WP_BIN;
|
|
343
|
+
if (env.WP_FORCE_JIT_PATH) return join(env.WP_FORCE_JIT_PATH, "bin", "wp");
|
|
344
|
+
return join(env.USERPROFILE || homedir(), ".webpresso", "bin", "wp.exe");
|
|
345
|
+
}
|
|
346
|
+
function isExecutableFile(candidate) {
|
|
347
|
+
try {
|
|
348
|
+
const stat = statSync(candidate);
|
|
349
|
+
if (!stat.isFile()) return false;
|
|
350
|
+
if (process.platform !== "win32") accessSync(candidate, constants.X_OK);
|
|
351
|
+
return true;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function resolveWpHookCommand() {
|
|
357
|
+
const launcher = resolveWpLauncher();
|
|
358
|
+
if (!isExecutableFile(launcher)) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`the webpresso wp launcher ${launcher} is not available; reinstall @webpresso/plugin-opencode or the Webpresso app.`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return launcher;
|
|
364
|
+
}
|
|
365
|
+
function hookEnv(directory) {
|
|
366
|
+
return {
|
|
367
|
+
...process.env,
|
|
368
|
+
[OPENCODE_HOOK_ENV.projectDir]: directory,
|
|
369
|
+
[OPENCODE_HOOK_ENV.hookHost]: OPENCODE_HOOK_HOST_NAME,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function launcherSpawn(launcher, directory, args) {
|
|
373
|
+
return { command: launcher, args, env: hookEnv(directory), group: false };
|
|
374
|
+
}
|
|
375
|
+
function failedRun(message) {
|
|
376
|
+
return { stdout: Buffer.alloc(0), stderr: Buffer.from(message), exitCode: null };
|
|
377
|
+
}
|
|
378
|
+
function collectBounded(stream) {
|
|
379
|
+
const chunks = [];
|
|
380
|
+
let bytes = 0;
|
|
381
|
+
stream.on("data", (chunk) => {
|
|
382
|
+
if (bytes < HOOK_OUTPUT_CAP_BYTES) {
|
|
383
|
+
chunks.push(chunk);
|
|
384
|
+
bytes += chunk.length;
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
return () => Buffer.concat(chunks);
|
|
388
|
+
}
|
|
389
|
+
var killProcess = (pid, signal) => {
|
|
390
|
+
process.kill(pid, signal);
|
|
391
|
+
};
|
|
392
|
+
function killProcessGroup(pid, kill = killProcess) {
|
|
393
|
+
if (pid === undefined) return;
|
|
394
|
+
try {
|
|
395
|
+
kill(-pid, "SIGKILL");
|
|
396
|
+
} catch {}
|
|
397
|
+
}
|
|
398
|
+
function processGroupHasMembers(pid, kill) {
|
|
399
|
+
try {
|
|
400
|
+
kill(-pid, 0);
|
|
401
|
+
return true;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
return recordOf(error)?.code !== "ESRCH";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function sweepProcessGroup(pid, kill = killProcess) {
|
|
407
|
+
if (pid === undefined || !processGroupHasMembers(pid, kill)) return;
|
|
408
|
+
killProcessGroup(pid, kill);
|
|
409
|
+
}
|
|
410
|
+
function spawnHookProcess(spec, directory, stdinPayload) {
|
|
411
|
+
return new Promise((resolveRun) => {
|
|
412
|
+
if (spec.signal?.aborted === true) {
|
|
413
|
+
resolveRun(failedRun("aborted"));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const child = spawn(spec.command, spec.args, {
|
|
417
|
+
cwd: directory,
|
|
418
|
+
env: spec.env,
|
|
419
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
420
|
+
detached: spec.group,
|
|
421
|
+
});
|
|
422
|
+
const stdout = collectBounded(child.stdout);
|
|
423
|
+
const stderr = collectBounded(child.stderr);
|
|
424
|
+
const terminate = () => {
|
|
425
|
+
if (spec.group) killProcessGroup(child.pid);
|
|
426
|
+
else child.kill("SIGKILL");
|
|
427
|
+
};
|
|
428
|
+
let settled = false;
|
|
429
|
+
const settle = (result) => {
|
|
430
|
+
if (settled) return;
|
|
431
|
+
settled = true;
|
|
432
|
+
clearTimeout(deadline);
|
|
433
|
+
spec.signal?.removeEventListener("abort", onAbort);
|
|
434
|
+
if (spec.group) sweepProcessGroup(child.pid);
|
|
435
|
+
resolveRun(result);
|
|
436
|
+
};
|
|
437
|
+
const onAbort = () => {
|
|
438
|
+
terminate();
|
|
439
|
+
settle(failedRun("aborted"));
|
|
440
|
+
};
|
|
441
|
+
const budgetMs = spec.budgetMs ?? HOOK_BUDGET_MS;
|
|
442
|
+
const deadline = setTimeout(() => {
|
|
443
|
+
terminate();
|
|
444
|
+
settle(failedRun(`timed out after ${budgetMs} ms`));
|
|
445
|
+
}, budgetMs);
|
|
446
|
+
spec.signal?.addEventListener("abort", onAbort, { once: true });
|
|
447
|
+
let started = false;
|
|
448
|
+
child.on("spawn", () => {
|
|
449
|
+
started = true;
|
|
450
|
+
});
|
|
451
|
+
child.on("error", (error) => {
|
|
452
|
+
settle(
|
|
453
|
+
started ? failedRun(error.message) : { ...failedRun(error.message), notStarted: true },
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
child.on("close", (code) => {
|
|
457
|
+
settle({ stdout: stdout(), stderr: stderr(), exitCode: code });
|
|
458
|
+
});
|
|
459
|
+
child.stdin.on("error", () => {});
|
|
460
|
+
child.stdin.end(stdinPayload);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
var OPENCODE_HOOK_LANE_RUNTIME_ARGS = ["--no-install"];
|
|
464
|
+
function absolutePathField(value) {
|
|
465
|
+
return typeof value === "string" && isAbsolute(value) ? value : null;
|
|
466
|
+
}
|
|
467
|
+
function bundleField(value) {
|
|
468
|
+
if (typeof value !== "object" || value === null) return null;
|
|
469
|
+
const record = value;
|
|
470
|
+
const run = absolutePathField(record.run);
|
|
471
|
+
const manifest = absolutePathField(record.manifest);
|
|
472
|
+
return run === null || manifest === null ? null : { run, manifest };
|
|
473
|
+
}
|
|
474
|
+
function isRuntimeArg(value) {
|
|
475
|
+
return typeof value === "string" && OPENCODE_HOOK_LANE_RUNTIME_ARGS.includes(value);
|
|
476
|
+
}
|
|
477
|
+
function runtimeArgsField(value) {
|
|
478
|
+
if (!Array.isArray(value) || value.length > OPENCODE_HOOK_LANE_RUNTIME_ARGS.length) return null;
|
|
479
|
+
return value.every(isRuntimeArg) ? [...value] : null;
|
|
480
|
+
}
|
|
481
|
+
function parseJsonRecord(text) {
|
|
482
|
+
let parsed;
|
|
483
|
+
try {
|
|
484
|
+
parsed = JSON.parse(text);
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
489
|
+
return parsed;
|
|
490
|
+
}
|
|
491
|
+
function sourceLane(record, runtime, runtimeArgs) {
|
|
492
|
+
const entry = absolutePathField(record.entry);
|
|
493
|
+
if (record.kind !== "source" || entry === null) return null;
|
|
494
|
+
return {
|
|
495
|
+
kind: "source",
|
|
496
|
+
runtime,
|
|
497
|
+
runtimeArgs,
|
|
498
|
+
entry,
|
|
499
|
+
rtk: absolutePathField(record.rtk),
|
|
500
|
+
checkoutRoot: absolutePathField(record.checkoutRoot),
|
|
501
|
+
bundle: bundleField(record.bundle),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function parseHookLaneDescriptor(stdout) {
|
|
505
|
+
const record = parseJsonRecord(stdout);
|
|
506
|
+
if (record === null) return null;
|
|
507
|
+
const runtime = absolutePathField(record.runtime);
|
|
508
|
+
const runtimeArgs = runtimeArgsField(record.runtimeArgs);
|
|
509
|
+
if (runtime === null || runtimeArgs === null) return null;
|
|
510
|
+
if (record.kind !== "compiled") return sourceLane(record, runtime, runtimeArgs);
|
|
511
|
+
if (runtimeArgs.length > 0) return null;
|
|
512
|
+
return {
|
|
513
|
+
kind: "compiled",
|
|
514
|
+
runtime,
|
|
515
|
+
runtimeArgs: [],
|
|
516
|
+
entry: null,
|
|
517
|
+
rtk: absolutePathField(record.rtk),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function hookLaneSpawnArgs(lane, hookName, bundled = false) {
|
|
521
|
+
if (lane.kind === "compiled") return ["hook", hookName];
|
|
522
|
+
const script = bundled && lane.bundle !== null ? lane.bundle.run : lane.entry;
|
|
523
|
+
return [...lane.runtimeArgs, script, hookName];
|
|
524
|
+
}
|
|
525
|
+
function sourceNodePath(checkoutRoot, inherited) {
|
|
526
|
+
const required = [
|
|
527
|
+
join(checkoutRoot, "node_modules"),
|
|
528
|
+
join(checkoutRoot, "node_modules", ".pnpm", "node_modules"),
|
|
529
|
+
];
|
|
530
|
+
const existing = (inherited ?? "").split(delimiter).filter((entry) => entry.length > 0);
|
|
531
|
+
return [...new Set([...required, ...existing])].join(delimiter);
|
|
532
|
+
}
|
|
533
|
+
function laneSpawn({ lane, bundled }, directory, hookName, signal) {
|
|
534
|
+
const env = hookEnv(directory);
|
|
535
|
+
if (env[SKIP_UPDATE_CHECK_ENV] === undefined || env[SKIP_UPDATE_CHECK_ENV] === "") {
|
|
536
|
+
env[SKIP_UPDATE_CHECK_ENV] = "1";
|
|
537
|
+
}
|
|
538
|
+
env[HOOK_OWNER_PID_ENV] = String(process.pid);
|
|
539
|
+
if (lane.kind === "source" && lane.checkoutRoot !== null) {
|
|
540
|
+
env.NODE_PATH = sourceNodePath(lane.checkoutRoot, env.NODE_PATH);
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
command: lane.runtime,
|
|
544
|
+
args: hookLaneSpawnArgs(lane, hookName, bundled),
|
|
545
|
+
env,
|
|
546
|
+
group: true,
|
|
547
|
+
...(signal === undefined ? {} : { signal }),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function isRegularFile(candidate) {
|
|
551
|
+
try {
|
|
552
|
+
return statSync(candidate).isFile();
|
|
553
|
+
} catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function laneTargetsExist(lane) {
|
|
558
|
+
return isExecutableFile(lane.runtime) && (lane.entry === null || isRegularFile(lane.entry));
|
|
559
|
+
}
|
|
560
|
+
var NO_LANE_USAGE = /Usage: hook-entrypoint <hook-name>|Unknown hook "lane"|unknown hook: lane/u;
|
|
561
|
+
var MAX_REASON_CHARS = 200;
|
|
562
|
+
function laneAnswerOf(result) {
|
|
563
|
+
if (result.exitCode === 0) {
|
|
564
|
+
const lane = parseHookLaneDescriptor(result.stdout.toString().trim());
|
|
565
|
+
if (lane === null) return { kind: "none", reason: "unrecognized lane report" };
|
|
566
|
+
if (laneTargetsExist(lane)) return { kind: "lane", lane };
|
|
567
|
+
return { kind: "transient", reason: "lane runtime or entry is missing" };
|
|
568
|
+
}
|
|
569
|
+
const stderr = result.stderr.toString().replace(/\s+/gu, " ").trim().slice(0, MAX_REASON_CHARS);
|
|
570
|
+
if (result.exitCode !== null && NO_LANE_USAGE.test(stderr)) {
|
|
571
|
+
return { kind: "none", reason: "runtime predates `wp hook lane`" };
|
|
572
|
+
}
|
|
573
|
+
const status = result.exitCode === null ? "did not complete" : `exit ${result.exitCode}`;
|
|
574
|
+
return { kind: "transient", reason: stderr === "" ? status : `${status}: ${stderr}` };
|
|
575
|
+
}
|
|
576
|
+
async function resolveHookLane(directory, signal) {
|
|
577
|
+
if (process.platform === "win32") return { kind: "none", reason: "no process groups" };
|
|
578
|
+
let launcher;
|
|
579
|
+
try {
|
|
580
|
+
launcher = resolveWpHookCommand();
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return { kind: "transient", reason: messageOf(error) };
|
|
583
|
+
}
|
|
584
|
+
const spec = {
|
|
585
|
+
...launcherSpawn(launcher, directory, HOOK_LANE_ARGS),
|
|
586
|
+
group: true,
|
|
587
|
+
signal,
|
|
588
|
+
};
|
|
589
|
+
return laneAnswerOf(await spawnHookProcess(spec, directory, ""));
|
|
590
|
+
}
|
|
591
|
+
var HOOK_BUNDLE_HOST = { statSync, readFileSync, isAbsolute };
|
|
592
|
+
function evictOldest(map, max) {
|
|
593
|
+
while (map.size > max) {
|
|
594
|
+
const oldest = map.keys().next().value;
|
|
595
|
+
if (oldest === undefined) break;
|
|
596
|
+
map.delete(oldest);
|
|
597
|
+
}
|
|
11
598
|
}
|
|
12
599
|
|
|
600
|
+
class HookLaneCache {
|
|
601
|
+
entries = new Map();
|
|
602
|
+
manifests = new Map();
|
|
603
|
+
options;
|
|
604
|
+
constructor(options) {
|
|
605
|
+
this.options = options;
|
|
606
|
+
}
|
|
607
|
+
laneFor(directory) {
|
|
608
|
+
const entry = this.entryFor(directory);
|
|
609
|
+
const lane = this.liveLane(entry, directory);
|
|
610
|
+
if (lane === null) return null;
|
|
611
|
+
if (lane.kind !== "source" || lane.bundle === null) return { lane, bundled: false };
|
|
612
|
+
if (this.isBundleFresh(lane.bundle)) return { lane, bundled: true };
|
|
613
|
+
this.refresh(entry, directory);
|
|
614
|
+
return { lane, bundled: false };
|
|
615
|
+
}
|
|
616
|
+
rtkFor(directory) {
|
|
617
|
+
return this.entries.get(directory)?.lane?.rtk ?? null;
|
|
618
|
+
}
|
|
619
|
+
drop(directory, lane, reason) {
|
|
620
|
+
const entry = this.entries.get(directory);
|
|
621
|
+
if (entry === undefined || entry.lane !== lane) return;
|
|
622
|
+
entry.lane = null;
|
|
623
|
+
this.options.debug(
|
|
624
|
+
`hook lane ${directory}: could not start ${lane.runtime} (${reason}); using the launcher`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
liveLane(entry, directory) {
|
|
628
|
+
if (entry.lane !== null && !laneTargetsExist(entry.lane)) {
|
|
629
|
+
entry.lane = null;
|
|
630
|
+
this.options.debug(`hook lane ${directory}: runtime or entry vanished; using the launcher`);
|
|
631
|
+
}
|
|
632
|
+
if (entry.lane === null) this.refresh(entry, directory);
|
|
633
|
+
return entry.lane;
|
|
634
|
+
}
|
|
635
|
+
refresh(entry, directory) {
|
|
636
|
+
const now = this.options.now();
|
|
637
|
+
if (entry.final || entry.inFlight || now < entry.nextAttemptMs) return;
|
|
638
|
+
if (this.options.signal.aborted) return;
|
|
639
|
+
entry.inFlight = true;
|
|
640
|
+
entry.nextAttemptMs = now + HOOK_LANE_REFRESH_INTERVAL_MS;
|
|
641
|
+
resolveHookLane(directory, this.options.signal)
|
|
642
|
+
.catch((error) => ({ kind: "transient", reason: messageOf(error) }))
|
|
643
|
+
.then((answer) => this.settle(entry, directory, answer))
|
|
644
|
+
.catch(() => {});
|
|
645
|
+
}
|
|
646
|
+
settle(entry, directory, answer) {
|
|
647
|
+
entry.inFlight = false;
|
|
648
|
+
if (answer.kind === "lane") {
|
|
649
|
+
entry.lane = answer.lane;
|
|
650
|
+
this.options.debug(`hook lane ${directory}: settled ${answer.lane.kind} lane`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
if (answer.kind === "none") {
|
|
654
|
+
entry.lane = null;
|
|
655
|
+
entry.final = true;
|
|
656
|
+
this.options.debug(`hook lane ${directory}: settled no lane (${answer.reason})`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
entry.nextAttemptMs = this.options.now() + HOOK_LANE_RETRY_COOLDOWN_MS;
|
|
660
|
+
this.options.debug(
|
|
661
|
+
`hook lane ${directory}: settled transient failure, retry after ${HOOK_LANE_RETRY_COOLDOWN_MS} ms (${answer.reason})`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
isBundleFresh(bundle) {
|
|
665
|
+
const manifest = this.manifestOf(bundle.manifest);
|
|
666
|
+
return manifest !== null && isHookBundleManifestFresh(HOOK_BUNDLE_HOST, manifest);
|
|
667
|
+
}
|
|
668
|
+
manifestOf(path) {
|
|
669
|
+
const cached = this.manifests.get(path);
|
|
670
|
+
if (cached !== undefined) return cached;
|
|
671
|
+
const manifest = readHookBundleManifest(HOOK_BUNDLE_HOST, path);
|
|
672
|
+
if (manifest === null) return null;
|
|
673
|
+
this.manifests.set(path, manifest);
|
|
674
|
+
evictOldest(this.manifests, MAX_CACHED_LANES);
|
|
675
|
+
return manifest;
|
|
676
|
+
}
|
|
677
|
+
entryFor(directory) {
|
|
678
|
+
const existing = this.entries.get(directory);
|
|
679
|
+
if (existing !== undefined) return existing;
|
|
680
|
+
const entry = {
|
|
681
|
+
lane: null,
|
|
682
|
+
final: false,
|
|
683
|
+
inFlight: false,
|
|
684
|
+
nextAttemptMs: Number.NEGATIVE_INFINITY,
|
|
685
|
+
};
|
|
686
|
+
this.entries.set(directory, entry);
|
|
687
|
+
evictOldest(this.entries, MAX_CACHED_LANES);
|
|
688
|
+
return entry;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function hookFailure(hookName, detail, cause) {
|
|
692
|
+
return new Error(
|
|
693
|
+
`GUARD_PRETOOL_FAILURE: webpresso hook ${hookName} ${detail}`,
|
|
694
|
+
cause === undefined ? undefined : { cause },
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
function messageOf(error) {
|
|
698
|
+
return error instanceof Error ? error.message : String(error);
|
|
699
|
+
}
|
|
700
|
+
function extractAdditionalContext(envelope) {
|
|
701
|
+
const context = envelope?.hookSpecificOutput?.additionalContext;
|
|
702
|
+
return typeof context === "string" ? context : null;
|
|
703
|
+
}
|
|
13
704
|
function extractDenyReason(envelope) {
|
|
14
705
|
const output = envelope?.hookSpecificOutput;
|
|
15
706
|
if (output?.permissionDecision === "deny") {
|
|
16
|
-
|
|
707
|
+
const reason = output.permissionDecisionReason;
|
|
708
|
+
return typeof reason === "string" && reason.length > 0 ? reason : DEFAULT_DENY_REASON;
|
|
17
709
|
}
|
|
18
|
-
|
|
19
710
|
return null;
|
|
20
711
|
}
|
|
21
|
-
|
|
22
|
-
function extractUpdatedInput(envelope) {
|
|
712
|
+
function extractUpdatedCommand(envelope) {
|
|
23
713
|
const output = envelope?.hookSpecificOutput;
|
|
24
|
-
|
|
714
|
+
if (output?.permissionDecision !== "allow") return null;
|
|
715
|
+
const updatedInput = output.updatedInput;
|
|
716
|
+
if (typeof updatedInput !== "object" || updatedInput === null || Array.isArray(updatedInput)) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
const command = updatedInput.command;
|
|
720
|
+
return typeof command === "string" ? command : null;
|
|
25
721
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
722
|
+
function interpretHookResult(hookName, result, required) {
|
|
723
|
+
const stdout = result.stdout.toString().trim();
|
|
724
|
+
const stderr = result.stderr.toString().trim();
|
|
725
|
+
const detail = stderr === "" ? "" : `: ${stderr}`;
|
|
726
|
+
if (result.exitCode !== 0) {
|
|
727
|
+
if (!required) return null;
|
|
728
|
+
throw hookFailure(
|
|
729
|
+
hookName,
|
|
730
|
+
result.exitCode === null
|
|
731
|
+
? `did not complete${detail}`
|
|
732
|
+
: `failed with exit ${result.exitCode}${detail}`,
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (stdout === "") {
|
|
736
|
+
if (required) throw hookFailure(hookName, "produced no JSON response");
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
return parseHookEnvelope(hookName, stdout, detail, required);
|
|
740
|
+
}
|
|
741
|
+
function parseHookEnvelope(hookName, stdout, detail, required) {
|
|
742
|
+
let parsed;
|
|
743
|
+
try {
|
|
744
|
+
parsed = JSON.parse(stdout);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (required) throw hookFailure(hookName, `produced invalid JSON${detail}`, error);
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
750
|
+
if (required) throw hookFailure(hookName, "produced an invalid JSON envelope");
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
return parsed;
|
|
754
|
+
}
|
|
755
|
+
async function runOnLauncher(directory, hookName, stdinPayload, required) {
|
|
756
|
+
let spawnSpec;
|
|
757
|
+
try {
|
|
758
|
+
spawnSpec = launcherSpawn(resolveWpHookCommand(), directory, ["hook", hookName]);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (required) throw hookFailure(hookName, `cannot start: ${messageOf(error)}`, error);
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
const result = await spawnHookProcess(spawnSpec, directory, stdinPayload);
|
|
764
|
+
return interpretHookResult(hookName, result, required);
|
|
765
|
+
}
|
|
766
|
+
function createWpHookRunner(signal, lanes) {
|
|
767
|
+
return async (directory, hookName, payload, options = {}) => {
|
|
768
|
+
const required = options.required === true;
|
|
769
|
+
const stdinPayload = payload === null ? "" : JSON.stringify(payload);
|
|
770
|
+
const selected = lanes.laneFor(directory);
|
|
771
|
+
if (selected !== null) {
|
|
772
|
+
const spec = laneSpawn(selected, directory, hookName, signal);
|
|
773
|
+
const result = await spawnHookProcess(spec, directory, stdinPayload);
|
|
774
|
+
if (result.notStarted !== true) return interpretHookResult(hookName, result, required);
|
|
775
|
+
lanes.drop(directory, selected.lane, result.stderr.toString());
|
|
776
|
+
}
|
|
777
|
+
return runOnLauncher(directory, hookName, stdinPayload, required);
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
var OPENCODE_TOOL_NAME_ALIASES = {
|
|
781
|
+
bash: "shell",
|
|
782
|
+
ash: "shell",
|
|
783
|
+
ash_command: "shell",
|
|
784
|
+
};
|
|
785
|
+
function canonicalToolName(tool) {
|
|
786
|
+
if (typeof tool !== "string") return tool;
|
|
787
|
+
return Object.hasOwn(OPENCODE_TOOL_NAME_ALIASES, tool) ? OPENCODE_TOOL_NAME_ALIASES[tool] : tool;
|
|
788
|
+
}
|
|
789
|
+
function hookInputForTool(tool, input, directory, sessionId, hookEventName) {
|
|
790
|
+
const toolInput =
|
|
791
|
+
typeof input === "object" && input !== null && !Array.isArray(input) ? input : {};
|
|
29
792
|
const base = {
|
|
30
793
|
cwd: directory,
|
|
31
|
-
hook_event_name:
|
|
794
|
+
hook_event_name: hookEventName,
|
|
795
|
+
...(sessionId === null ? {} : { session_id: sessionId }),
|
|
32
796
|
};
|
|
33
|
-
|
|
34
797
|
switch (tool) {
|
|
35
|
-
case "
|
|
798
|
+
case "shell":
|
|
36
799
|
return {
|
|
37
800
|
...base,
|
|
38
801
|
tool_name: "Bash",
|
|
39
802
|
tool_input: {
|
|
40
|
-
...
|
|
41
|
-
command:
|
|
803
|
+
...toolInput,
|
|
804
|
+
command: typeof toolInput.command === "string" ? toolInput.command : "",
|
|
42
805
|
},
|
|
43
806
|
};
|
|
44
807
|
case "write":
|
|
@@ -46,8 +809,8 @@ function hookInputForTool(tool, args, directory) {
|
|
|
46
809
|
...base,
|
|
47
810
|
tool_name: "Write",
|
|
48
811
|
tool_input: {
|
|
49
|
-
file_path:
|
|
50
|
-
content:
|
|
812
|
+
file_path: stringField(toolInput.path),
|
|
813
|
+
content: stringField(toolInput.content),
|
|
51
814
|
},
|
|
52
815
|
};
|
|
53
816
|
case "edit":
|
|
@@ -55,196 +818,476 @@ function hookInputForTool(tool, args, directory) {
|
|
|
55
818
|
...base,
|
|
56
819
|
tool_name: "Edit",
|
|
57
820
|
tool_input: {
|
|
58
|
-
file_path:
|
|
59
|
-
old_string:
|
|
60
|
-
new_string:
|
|
821
|
+
file_path: stringField(toolInput.path),
|
|
822
|
+
old_string: stringField(toolInput.oldString),
|
|
823
|
+
new_string: stringField(toolInput.newString),
|
|
61
824
|
},
|
|
62
825
|
};
|
|
63
826
|
case "read":
|
|
64
827
|
return {
|
|
65
828
|
...base,
|
|
66
829
|
tool_name: "Read",
|
|
67
|
-
tool_input: {
|
|
68
|
-
file_path: args?.filePath ?? "",
|
|
69
|
-
},
|
|
830
|
+
tool_input: { file_path: stringField(toolInput.path) },
|
|
70
831
|
};
|
|
71
|
-
case "
|
|
832
|
+
case "patch":
|
|
72
833
|
return {
|
|
73
834
|
...base,
|
|
74
|
-
tool_name: "
|
|
75
|
-
tool_input: {
|
|
76
|
-
patch_text: args?.patchText ?? "",
|
|
77
|
-
},
|
|
835
|
+
tool_name: "apply_patch",
|
|
836
|
+
tool_input: { patch_text: stringField(toolInput.patchText) },
|
|
78
837
|
};
|
|
79
838
|
default:
|
|
80
839
|
return null;
|
|
81
840
|
}
|
|
82
841
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
842
|
+
function stringField(value) {
|
|
843
|
+
return typeof value === "string" ? value : "";
|
|
86
844
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
845
|
+
function sessionIdFrom(source) {
|
|
846
|
+
if (typeof source !== "object" || source === null) return null;
|
|
847
|
+
const record = source;
|
|
848
|
+
const candidate = record.sessionID ?? record.sessionId ?? record.session_id;
|
|
849
|
+
return typeof candidate === "string" && candidate.length > 0 ? candidate : null;
|
|
850
|
+
}
|
|
851
|
+
var pinnedContextMatches = (ctx) => ctx;
|
|
852
|
+
function applyModelCeilings(editor) {
|
|
853
|
+
for (const { providerID, id, limit } of editor.list()) {
|
|
854
|
+
const ceiling = ceilingFor(limit);
|
|
855
|
+
if (ceiling === null || typeof providerID !== "string" || typeof id !== "string") continue;
|
|
856
|
+
editor.update(providerID, id, (draft) => {
|
|
857
|
+
draft.limit.input = ceiling;
|
|
858
|
+
});
|
|
96
859
|
}
|
|
97
860
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
: join(home, ".webpresso", "bin", "wp");
|
|
861
|
+
function applyGeneralSteps(editor) {
|
|
862
|
+
const general = editor.get("general");
|
|
863
|
+
if (general === undefined) return;
|
|
864
|
+
const steps = stepsFor(general.steps);
|
|
865
|
+
if (steps === null) return;
|
|
866
|
+
editor.update("general", (draft) => {
|
|
867
|
+
draft.steps = steps;
|
|
868
|
+
});
|
|
107
869
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
870
|
+
async function registerSessionBudget(host, registrations, debug) {
|
|
871
|
+
if (host.options?.sessionBudget === false) return;
|
|
872
|
+
const model = host.model;
|
|
873
|
+
const agent = host.agent;
|
|
874
|
+
if (typeof model?.transform !== "function" || typeof agent?.transform !== "function") {
|
|
875
|
+
debug("session budget: the host has no model.transform/agent.transform; skipped");
|
|
876
|
+
return;
|
|
114
877
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
label = "WEBPRESSO_WP_BIN";
|
|
121
|
-
} else if (jitRoot) {
|
|
122
|
-
candidate = join(jitRoot, "bin", "wp");
|
|
123
|
-
label = "WP_FORCE_JIT_PATH";
|
|
124
|
-
} else {
|
|
125
|
-
candidate = fixedAppWpLauncher();
|
|
878
|
+
try {
|
|
879
|
+
registrations.push(await model.transform(applyModelCeilings));
|
|
880
|
+
registrations.push(await agent.transform(applyGeneralSteps));
|
|
881
|
+
} catch (error) {
|
|
882
|
+
debug(`session budget: transform registration failed (${messageOf(error)}); hooks unaffected`);
|
|
126
883
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
884
|
+
}
|
|
885
|
+
async function sessionDirectoryOf(session, sessionID) {
|
|
886
|
+
if (sessionID === null) throw new Error("no session id on tool event");
|
|
887
|
+
let info;
|
|
888
|
+
try {
|
|
889
|
+
info = await withDeadline(
|
|
890
|
+
session.get({ sessionID }),
|
|
891
|
+
SESSION_LOOKUP_TIMEOUT_MS,
|
|
892
|
+
`session lookup exceeded ${SESSION_LOOKUP_TIMEOUT_MS} ms`,
|
|
132
893
|
);
|
|
894
|
+
} catch (error) {
|
|
895
|
+
throw new Error(`session lookup failed: ${messageOf(error)}`);
|
|
896
|
+
}
|
|
897
|
+
const directory = info?.location?.directory;
|
|
898
|
+
if (typeof directory !== "string" || directory.length === 0) {
|
|
899
|
+
throw new Error(`session lookup failed: no location.directory for session ${sessionID}`);
|
|
133
900
|
}
|
|
134
|
-
return
|
|
901
|
+
return directory;
|
|
902
|
+
}
|
|
903
|
+
function withDeadline(pending, timeoutMs, message) {
|
|
904
|
+
let timer;
|
|
905
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
906
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
907
|
+
timer.unref?.();
|
|
908
|
+
});
|
|
909
|
+
return Promise.race([pending, deadline]).finally(() => {
|
|
910
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
function workdirAnchor(sessionDirectory, input) {
|
|
914
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return sessionDirectory;
|
|
915
|
+
const workdir = input.workdir;
|
|
916
|
+
if (typeof workdir !== "string" || workdir.length === 0) return sessionDirectory;
|
|
917
|
+
return isAbsolute(workdir) ? workdir : resolve(sessionDirectory, workdir);
|
|
135
918
|
}
|
|
136
919
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const required = options.required === true;
|
|
150
|
-
|
|
151
|
-
if (exitCode !== 0) {
|
|
152
|
-
if (required) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
`webpresso hook ${hookName} failed with exit ${exitCode}${stderr ? `: ${stderr}` : ""}`,
|
|
155
|
-
);
|
|
920
|
+
class SessionContextStore {
|
|
921
|
+
contexts = new Map();
|
|
922
|
+
set(sessionId, contexts) {
|
|
923
|
+
this.contexts.delete(sessionId);
|
|
924
|
+
this.contexts.set(
|
|
925
|
+
sessionId,
|
|
926
|
+
contexts.map((text) => text.slice(0, MAX_CONTEXT_CHARS)),
|
|
927
|
+
);
|
|
928
|
+
while (this.contexts.size > MAX_TRACKED_SESSIONS) {
|
|
929
|
+
const oldest = this.contexts.keys().next().value;
|
|
930
|
+
if (oldest === undefined) break;
|
|
931
|
+
this.contexts.delete(oldest);
|
|
156
932
|
}
|
|
157
|
-
return null;
|
|
158
933
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
934
|
+
get(sessionId) {
|
|
935
|
+
return this.contexts.get(sessionId) ?? [];
|
|
936
|
+
}
|
|
937
|
+
delete(sessionId) {
|
|
938
|
+
this.contexts.delete(sessionId);
|
|
939
|
+
}
|
|
940
|
+
clear() {
|
|
941
|
+
this.contexts.clear();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
function isTextPart(part) {
|
|
945
|
+
if (typeof part !== "object" || part === null) return false;
|
|
946
|
+
const record = part;
|
|
947
|
+
return record.type === "text" && typeof record.text === "string";
|
|
948
|
+
}
|
|
949
|
+
function joinedContentText(content) {
|
|
950
|
+
if (typeof content === "string") return content;
|
|
951
|
+
if (!Array.isArray(content)) return null;
|
|
952
|
+
const texts = content.filter(isTextPart);
|
|
953
|
+
return texts.length === 0 ? null : texts.map((part) => part.text).join("");
|
|
954
|
+
}
|
|
955
|
+
function replaceContentText(content, text) {
|
|
956
|
+
if (typeof content === "string") return text;
|
|
957
|
+
if (!Array.isArray(content)) return content;
|
|
958
|
+
const next = [];
|
|
959
|
+
let placed = false;
|
|
960
|
+
for (const part of content) {
|
|
961
|
+
if (!isTextPart(part)) next.push(part);
|
|
962
|
+
else if (!placed) {
|
|
963
|
+
placed = true;
|
|
964
|
+
next.push({ ...part, text });
|
|
163
965
|
}
|
|
164
|
-
return null;
|
|
165
966
|
}
|
|
166
|
-
|
|
967
|
+
return next;
|
|
968
|
+
}
|
|
969
|
+
function rawOutputRoot(env = process.env, home = homedir()) {
|
|
970
|
+
const state = env.XDG_STATE_HOME;
|
|
971
|
+
const base = state !== undefined && isAbsolute(state) ? state : join(home, ".local", "state");
|
|
972
|
+
return join(base, "webpresso", "opencode-raw-output");
|
|
973
|
+
}
|
|
974
|
+
function ownDirectories(root) {
|
|
167
975
|
try {
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
976
|
+
return readdirSync(root, { withFileTypes: true })
|
|
977
|
+
.filter((entry) => entry.isDirectory())
|
|
978
|
+
.map((entry) => join(root, entry.name));
|
|
979
|
+
} catch {
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function removeIfStale(file, cutoffMs) {
|
|
984
|
+
try {
|
|
985
|
+
if (statSync(file).mtimeMs >= cutoffMs) return false;
|
|
986
|
+
unlinkSync(file);
|
|
987
|
+
return true;
|
|
988
|
+
} catch {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function pruneSessionDirectory(directory, cutoffMs, budget) {
|
|
993
|
+
let removed = 0;
|
|
994
|
+
try {
|
|
995
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
996
|
+
if (removed >= budget) break;
|
|
997
|
+
if (entry.isFile() && removeIfStale(join(directory, entry.name), cutoffMs)) removed += 1;
|
|
175
998
|
}
|
|
999
|
+
rmdirSync(directory);
|
|
1000
|
+
} catch {}
|
|
1001
|
+
return removed;
|
|
1002
|
+
}
|
|
1003
|
+
function pruneRawOutput(root, nowMs = Date.now()) {
|
|
1004
|
+
const cutoffMs = nowMs - RAW_OUTPUT_MAX_AGE_MS;
|
|
1005
|
+
let removed = 0;
|
|
1006
|
+
for (const directory of ownDirectories(root)) {
|
|
1007
|
+
if (removed >= RAW_OUTPUT_PRUNE_LIMIT) break;
|
|
1008
|
+
removed += pruneSessionDirectory(directory, cutoffMs, RAW_OUTPUT_PRUNE_LIMIT - removed);
|
|
1009
|
+
}
|
|
1010
|
+
return removed;
|
|
1011
|
+
}
|
|
1012
|
+
function saveRawOutput(root, sessionID, callID, text) {
|
|
1013
|
+
if (!RAW_OUTPUT_SEGMENT.test(sessionID) || !RAW_OUTPUT_SEGMENT.test(callID)) return null;
|
|
1014
|
+
const directory = join(root, sessionID);
|
|
1015
|
+
const file = join(directory, `${callID}.txt`);
|
|
1016
|
+
try {
|
|
1017
|
+
mkdirSync(directory, { recursive: true, mode: RAW_OUTPUT_DIR_MODE });
|
|
1018
|
+
chmodSync(directory, RAW_OUTPUT_DIR_MODE);
|
|
1019
|
+
writeFileSync(file, text, { mode: RAW_OUTPUT_FILE_MODE, flag: "wx" });
|
|
1020
|
+
return file;
|
|
1021
|
+
} catch {
|
|
176
1022
|
return null;
|
|
177
1023
|
}
|
|
178
1024
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
1025
|
+
async function runRtkPipe(rtk, filter, text, directory, signal) {
|
|
1026
|
+
const result = await spawnHookProcess(
|
|
1027
|
+
{
|
|
1028
|
+
command: rtk,
|
|
1029
|
+
args: ["pipe", "--filter", filter],
|
|
1030
|
+
env: { ...process.env },
|
|
1031
|
+
group: true,
|
|
1032
|
+
signal,
|
|
1033
|
+
budgetMs: RTK_PIPE_BUDGET_MS,
|
|
1034
|
+
},
|
|
1035
|
+
directory,
|
|
1036
|
+
text,
|
|
1037
|
+
);
|
|
1038
|
+
if (result.exitCode !== 0 || result.stdout.length >= HOOK_OUTPUT_CAP_BYTES) return null;
|
|
1039
|
+
const filtered = result.stdout.toString();
|
|
1040
|
+
if (filtered.trim() === "") return null;
|
|
1041
|
+
return Buffer.byteLength(filtered) < Buffer.byteLength(text) ? filtered : null;
|
|
1042
|
+
}
|
|
1043
|
+
function recordOf(value) {
|
|
1044
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1045
|
+
}
|
|
1046
|
+
function shellFilterTarget(event, tool) {
|
|
1047
|
+
if (tool !== SHELL_TOOL_ID || event.status !== "completed") return null;
|
|
1048
|
+
const command = recordOf(event.input)?.command;
|
|
1049
|
+
const result = recordOf(event.result);
|
|
1050
|
+
if (typeof command !== "string" || result === null) return null;
|
|
1051
|
+
if (recordOf(result.metadata)?.truncated === true) return null;
|
|
1052
|
+
const text = joinedContentText(result.content);
|
|
1053
|
+
if (text === null || Buffer.byteLength(text) < SHELL_FILTER_MIN_BYTES) return null;
|
|
1054
|
+
if (text.includes(RTK_FOOTER_MARKER)) return null;
|
|
1055
|
+
const filter = selectOutputFilter(command);
|
|
1056
|
+
return filter === null ? null : { command, filter, result, text };
|
|
1057
|
+
}
|
|
1058
|
+
function filteredResultText(filtered, filter, inputBytes, raw) {
|
|
1059
|
+
const body = filtered.endsWith(`
|
|
1060
|
+
`)
|
|
1061
|
+
? filtered
|
|
1062
|
+
: `${filtered}
|
|
1063
|
+
`;
|
|
1064
|
+
const outputBytes = Buffer.byteLength(body);
|
|
1065
|
+
return `${body}${RTK_FOOTER_MARKER} --filter ${filter}, ${inputBytes}→${outputBytes} bytes; raw: ${raw}]`;
|
|
1066
|
+
}
|
|
1067
|
+
function replacedOutput(output, text, replacement) {
|
|
1068
|
+
if (typeof output !== "string") return {};
|
|
1069
|
+
const at = output.indexOf(text);
|
|
1070
|
+
if (at === -1) return {};
|
|
1071
|
+
return { output: `${output.slice(0, at)}${replacement}${output.slice(at + text.length)}` };
|
|
1072
|
+
}
|
|
1073
|
+
async function filterShellResult(event, tool, directory, deps) {
|
|
1074
|
+
try {
|
|
1075
|
+
const target = shellFilterTarget(event, tool);
|
|
1076
|
+
const sessionID = sessionIdFrom(event);
|
|
1077
|
+
if (target === null || sessionID === null || typeof event.id !== "string") return;
|
|
1078
|
+
const rtk = await deps.resolveRtk(directory);
|
|
1079
|
+
if (rtk === null || target.command.trimStart().startsWith(rtk)) return;
|
|
1080
|
+
const filtered = await runRtkPipe(rtk, target.filter, target.text, directory, deps.signal);
|
|
1081
|
+
if (filtered === null) return;
|
|
1082
|
+
const raw = saveRawOutput(deps.rawRoot, sessionID, event.id, target.text);
|
|
1083
|
+
if (raw === null) return;
|
|
1084
|
+
const inputBytes = Buffer.byteLength(target.text);
|
|
1085
|
+
const replacement = filteredResultText(filtered, target.filter, inputBytes, raw);
|
|
1086
|
+
if (Buffer.byteLength(replacement) >= inputBytes) return;
|
|
1087
|
+
event.result = {
|
|
1088
|
+
...target.result,
|
|
1089
|
+
content: replaceContentText(target.result.content, replacement),
|
|
1090
|
+
...replacedOutput(target.result.output, target.text, replacement),
|
|
1091
|
+
};
|
|
1092
|
+
} catch {}
|
|
1093
|
+
}
|
|
1094
|
+
function defaultDebug(message) {
|
|
1095
|
+
if (process.env[PLUGIN_DEBUG_ENV] === "1") {
|
|
1096
|
+
process.stderr.write(`[${OPENCODE_PLUGIN_ID}] ${message}
|
|
1097
|
+
`);
|
|
191
1098
|
}
|
|
192
|
-
|
|
193
|
-
return contexts;
|
|
194
1099
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
1100
|
+
function createWebpressoOpenCodePlugin(deps = {}) {
|
|
1101
|
+
const preToolHookName = PRE_TOOL_HOOKS[0];
|
|
1102
|
+
if (preToolHookName === undefined) {
|
|
1103
|
+
throw new Error("no managed PreToolUse hook in WP_HOOK_SPECS");
|
|
1104
|
+
}
|
|
199
1105
|
return {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
|
|
1106
|
+
id: OPENCODE_PLUGIN_ID,
|
|
1107
|
+
async setup(ctx) {
|
|
1108
|
+
const host = pinnedContextMatches(ctx);
|
|
1109
|
+
const store = new SessionContextStore();
|
|
1110
|
+
const controller = new AbortController();
|
|
1111
|
+
const debug = deps.debug ?? defaultDebug;
|
|
1112
|
+
const lanes = new HookLaneCache({
|
|
1113
|
+
signal: controller.signal,
|
|
1114
|
+
debug,
|
|
1115
|
+
now: deps.now ?? Date.now,
|
|
1116
|
+
});
|
|
1117
|
+
const runHook = deps.runWpHook ?? createWpHookRunner(controller.signal, lanes);
|
|
1118
|
+
const resolveRtk =
|
|
1119
|
+
deps.resolveRtk ??
|
|
1120
|
+
(deps.runWpHook === undefined
|
|
1121
|
+
? (directory) => Promise.resolve(lanes.rtkFor(directory))
|
|
1122
|
+
: () => Promise.resolve(null));
|
|
1123
|
+
const rawRoot = deps.rawOutputRoot ?? rawOutputRoot();
|
|
1124
|
+
pruneRawOutput(rawRoot);
|
|
1125
|
+
const registrations = [];
|
|
1126
|
+
let cleanedUp = false;
|
|
1127
|
+
const cleanup = async () => {
|
|
1128
|
+
if (cleanedUp) return;
|
|
1129
|
+
cleanedUp = true;
|
|
1130
|
+
controller.abort();
|
|
1131
|
+
store.clear();
|
|
1132
|
+
await Promise.all(
|
|
1133
|
+
registrations.map(async (registration) => {
|
|
1134
|
+
try {
|
|
1135
|
+
await registration.dispose();
|
|
1136
|
+
} catch {}
|
|
1137
|
+
}),
|
|
1138
|
+
);
|
|
1139
|
+
};
|
|
1140
|
+
const collectSessionStartContexts = async (directory, sessionId) => {
|
|
1141
|
+
const contexts = [];
|
|
1142
|
+
const payload = {
|
|
1143
|
+
cwd: directory,
|
|
1144
|
+
hook_event_name: OPENCODE_HOOK_EVENT_NAMES.sessionStart,
|
|
1145
|
+
session_id: sessionId,
|
|
1146
|
+
};
|
|
1147
|
+
for (const hookName of SESSION_START_HOOKS) {
|
|
1148
|
+
const envelope = await runHook(directory, hookName, payload);
|
|
1149
|
+
const context = extractAdditionalContext(envelope);
|
|
1150
|
+
if (context !== null) contexts.push(context);
|
|
224
1151
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
for (const hookName of POST_TOOL_HOOKS) {
|
|
234
|
-
await runHook($, directory, hookName, payload);
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
"experimental.session.compacting": async (_input, output) => {
|
|
238
|
-
cachedSessionContexts = await collectSessionStartContext($, directory);
|
|
239
|
-
if (output && Array.isArray(output.context)) {
|
|
240
|
-
for (const message of cachedSessionContexts) {
|
|
241
|
-
output.context.push(message);
|
|
1152
|
+
return contexts;
|
|
1153
|
+
};
|
|
1154
|
+
const resolveSessionDirectory = async (sessionID) => {
|
|
1155
|
+
try {
|
|
1156
|
+
return await sessionDirectoryOf(host.session, sessionID);
|
|
1157
|
+
} catch (error) {
|
|
1158
|
+
throw hookFailure(preToolHookName, messageOf(error), error);
|
|
242
1159
|
}
|
|
1160
|
+
};
|
|
1161
|
+
try {
|
|
1162
|
+
registrations.push(
|
|
1163
|
+
await host.tool.hook(OPENCODE_V2_DOMAIN_HOOKS.toolBefore, async (event) => {
|
|
1164
|
+
event.tool = canonicalToolName(event.tool);
|
|
1165
|
+
const sessionID = sessionIdFrom(event);
|
|
1166
|
+
const sessionDirectory = await resolveSessionDirectory(sessionID);
|
|
1167
|
+
const payload = hookInputForTool(
|
|
1168
|
+
event.tool,
|
|
1169
|
+
event.input,
|
|
1170
|
+
workdirAnchor(sessionDirectory, event.input),
|
|
1171
|
+
sessionID,
|
|
1172
|
+
OPENCODE_HOOK_EVENT_NAMES.preToolUse,
|
|
1173
|
+
);
|
|
1174
|
+
if (payload === null) return;
|
|
1175
|
+
for (const hookName of PRE_TOOL_HOOKS) {
|
|
1176
|
+
const envelope = await runHook(payload.cwd, hookName, payload, { required: true });
|
|
1177
|
+
const denyReason = extractDenyReason(envelope);
|
|
1178
|
+
if (denyReason !== null) throw new Error(denyReason);
|
|
1179
|
+
const updatedCommand = extractUpdatedCommand(envelope);
|
|
1180
|
+
if (event.tool === "shell" && updatedCommand !== null) {
|
|
1181
|
+
const input = event.input;
|
|
1182
|
+
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
1183
|
+
input.command = updatedCommand;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}),
|
|
1188
|
+
);
|
|
1189
|
+
registrations.push(
|
|
1190
|
+
await host.tool.hook(OPENCODE_V2_DOMAIN_HOOKS.toolAfter, async (event) => {
|
|
1191
|
+
if (event.status === "error") {
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const tool = canonicalToolName(event.tool);
|
|
1195
|
+
const sessionID = sessionIdFrom(event);
|
|
1196
|
+
let sessionDirectory;
|
|
1197
|
+
try {
|
|
1198
|
+
sessionDirectory = await sessionDirectoryOf(host.session, sessionID);
|
|
1199
|
+
} catch {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const payload = hookInputForTool(
|
|
1203
|
+
tool,
|
|
1204
|
+
event.input,
|
|
1205
|
+
workdirAnchor(sessionDirectory, event.input),
|
|
1206
|
+
sessionID,
|
|
1207
|
+
OPENCODE_HOOK_EVENT_NAMES.postToolUse,
|
|
1208
|
+
);
|
|
1209
|
+
if (payload === null) return;
|
|
1210
|
+
for (const hookName of POST_TOOL_HOOKS) {
|
|
1211
|
+
await runHook(payload.cwd, hookName, payload);
|
|
1212
|
+
}
|
|
1213
|
+
await filterShellResult(event, tool, payload.cwd, {
|
|
1214
|
+
resolveRtk,
|
|
1215
|
+
rawRoot,
|
|
1216
|
+
signal: controller.signal,
|
|
1217
|
+
});
|
|
1218
|
+
}),
|
|
1219
|
+
);
|
|
1220
|
+
registrations.push(
|
|
1221
|
+
await host.shell.hook(OPENCODE_V2_DOMAIN_HOOKS.shellBefore, (event) => {
|
|
1222
|
+
event.env[OPENCODE_HOOK_ENV.projectDir] = event.cwd;
|
|
1223
|
+
}),
|
|
1224
|
+
);
|
|
1225
|
+
registrations.push(
|
|
1226
|
+
await host.session.hook(OPENCODE_V2_DOMAIN_HOOKS.sessionCompaction, async (event) => {
|
|
1227
|
+
try {
|
|
1228
|
+
const directory = await sessionDirectoryOf(host.session, event.sessionID);
|
|
1229
|
+
const contexts = await collectSessionStartContexts(directory, event.sessionID);
|
|
1230
|
+
if (contexts.length > 0) store.set(event.sessionID, contexts);
|
|
1231
|
+
} catch {}
|
|
1232
|
+
for (const text of store.get(event.sessionID)) {
|
|
1233
|
+
event.system.push({ type: "text", text });
|
|
1234
|
+
}
|
|
1235
|
+
}),
|
|
1236
|
+
);
|
|
1237
|
+
(async () => {
|
|
1238
|
+
try {
|
|
1239
|
+
for await (const hostEvent of host.event.subscribe({ signal: controller.signal })) {
|
|
1240
|
+
if (typeof hostEvent !== "object" || hostEvent === null) continue;
|
|
1241
|
+
const record = hostEvent;
|
|
1242
|
+
if (typeof record.type !== "string") continue;
|
|
1243
|
+
if (record.type === OPENCODE_V2_SESSION_EVENTS.created) {
|
|
1244
|
+
const data = record.data;
|
|
1245
|
+
if (typeof data !== "object" || data === null) continue;
|
|
1246
|
+
const created = data;
|
|
1247
|
+
const sessionID = sessionIdFrom(created);
|
|
1248
|
+
const directory = created.location?.directory;
|
|
1249
|
+
if (sessionID === null || typeof directory !== "string" || directory === "") {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
store.set(sessionID, await collectSessionStartContexts(directory, sessionID));
|
|
1253
|
+
} else if (record.type === OPENCODE_V2_SESSION_EVENTS.deleted) {
|
|
1254
|
+
const data = record.data;
|
|
1255
|
+
if (typeof data !== "object" || data === null) continue;
|
|
1256
|
+
const sessionID = sessionIdFrom(data);
|
|
1257
|
+
if (sessionID !== null) store.delete(sessionID);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
} catch {}
|
|
1261
|
+
})().catch(() => {});
|
|
1262
|
+
await registerSessionBudget(host, registrations, debug);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
await cleanup();
|
|
1265
|
+
throw error;
|
|
243
1266
|
}
|
|
1267
|
+
return cleanup;
|
|
244
1268
|
},
|
|
245
1269
|
};
|
|
1270
|
+
}
|
|
1271
|
+
var webpressoOpenCodePlugin = createWebpressoOpenCodePlugin();
|
|
1272
|
+
var opencode_plugin_default = webpressoOpenCodePlugin;
|
|
1273
|
+
export {
|
|
1274
|
+
OPENCODE_HOOK_ENV,
|
|
1275
|
+
OPENCODE_HOOK_EVENT_NAMES,
|
|
1276
|
+
OPENCODE_HOOK_LANE_RUNTIME_ARGS,
|
|
1277
|
+
OPENCODE_HOOK_OWNER_PID_ENV,
|
|
1278
|
+
OPENCODE_MANAGED_HOOK_COMMANDS,
|
|
1279
|
+
OPENCODE_PLUGIN_ID,
|
|
1280
|
+
OPENCODE_TOOL_NAME_ALIASES,
|
|
1281
|
+
OPENCODE_V2_DOMAIN_HOOKS,
|
|
1282
|
+
OPENCODE_V2_SESSION_EVENTS,
|
|
1283
|
+
createWebpressoOpenCodePlugin,
|
|
1284
|
+
opencode_plugin_default as default,
|
|
1285
|
+
hookLaneSpawnArgs,
|
|
1286
|
+
joinedContentText,
|
|
1287
|
+
parseHookLaneDescriptor,
|
|
1288
|
+
pruneRawOutput,
|
|
1289
|
+
rawOutputRoot,
|
|
1290
|
+
replaceContentText,
|
|
1291
|
+
resolveWpLauncher,
|
|
1292
|
+
sweepProcessGroup,
|
|
246
1293
|
};
|
|
247
|
-
|
|
248
|
-
const webpressoOpenCodePlugins = [WebpressoHooksPlugin];
|
|
249
|
-
|
|
250
|
-
export default webpressoOpenCodePlugins;
|