@webpresso/plugin-opencode 0.0.13 → 0.0.15
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/index.js +1200 -219
- package/package.json +3 -3
- package/plugin-skill-ownership.json +11 -14
- package/skills/autopilot/SKILL.md +1 -1
- package/skills/best-practice-research/SKILL.md +8 -0
- 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 +1 -1
- package/skills/tech-debt/SKILL.md +7 -2
- package/skills/tooling-friction/SKILL.md +17 -14
- package/skills/ultragoal/SKILL.md +5 -11
- package/skills/verify/SKILL.md +6 -7
- package/skills/grok/SKILL.md +0 -47
package/index.js
CHANGED
|
@@ -1,116 +1,341 @@
|
|
|
1
|
+
// src/host-adapters/opencode-plugin.ts
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
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";
|
|
3
15
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
16
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
17
|
import { fileURLToPath } from "node:url";
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|
|
14
30
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
|
20
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
|
+
}
|
|
21
92
|
|
|
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
|
+
}
|
|
22
138
|
return null;
|
|
23
139
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 };
|
|
28
158
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
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;
|
|
32
169
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
content: args?.content ?? "",
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
case "edit":
|
|
62
|
-
return {
|
|
63
|
-
...base,
|
|
64
|
-
tool_name: "Edit",
|
|
65
|
-
tool_input: {
|
|
66
|
-
file_path: args?.filePath ?? "",
|
|
67
|
-
old_string: args?.oldString ?? "",
|
|
68
|
-
new_string: args?.newString ?? "",
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
case "read":
|
|
72
|
-
return {
|
|
73
|
-
...base,
|
|
74
|
-
tool_name: "Read",
|
|
75
|
-
tool_input: {
|
|
76
|
-
file_path: args?.filePath ?? "",
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
case "apply_patch":
|
|
80
|
-
return {
|
|
81
|
-
...base,
|
|
82
|
-
tool_name: "Write",
|
|
83
|
-
tool_input: {
|
|
84
|
-
patch_text: args?.patchText ?? "",
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
default:
|
|
88
|
-
return null;
|
|
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;
|
|
89
195
|
}
|
|
196
|
+
return tokens;
|
|
90
197
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 = [];
|
|
100
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);
|
|
101
265
|
}
|
|
102
266
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
282
|
|
|
110
|
-
|
|
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) {
|
|
111
338
|
if (platform !== "win32") return PACKAGED_WP_SHIM;
|
|
112
|
-
// The shim is POSIX sh. Until a native Windows shim exists, Windows keeps the
|
|
113
|
-
// explicit overrides (and their conflict refusal) plus the app launcher.
|
|
114
339
|
if (env.WEBPRESSO_WP_BIN && env.WP_FORCE_JIT_PATH) {
|
|
115
340
|
throw new Error("WEBPRESSO_WP_BIN and WP_FORCE_JIT_PATH are both set; unset one.");
|
|
116
341
|
}
|
|
@@ -118,7 +343,16 @@ export function resolveWpLauncher(platform = process.platform, env = process.env
|
|
|
118
343
|
if (env.WP_FORCE_JIT_PATH) return join(env.WP_FORCE_JIT_PATH, "bin", "wp");
|
|
119
344
|
return join(env.USERPROFILE || homedir(), ".webpresso", "bin", "wp.exe");
|
|
120
345
|
}
|
|
121
|
-
|
|
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
|
+
}
|
|
122
356
|
function resolveWpHookCommand() {
|
|
123
357
|
const launcher = resolveWpLauncher();
|
|
124
358
|
if (!isExecutableFile(launcher)) {
|
|
@@ -128,80 +362,367 @@ function resolveWpHookCommand() {
|
|
|
128
362
|
}
|
|
129
363
|
return launcher;
|
|
130
364
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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, {
|
|
135
417
|
cwd: directory,
|
|
136
|
-
env:
|
|
137
|
-
...process.env,
|
|
138
|
-
CLAUDE_PROJECT_DIR: directory,
|
|
139
|
-
WP_HOOK_HOST: "opencode",
|
|
140
|
-
},
|
|
418
|
+
env: spec.env,
|
|
141
419
|
stdio: ["pipe", "pipe", "pipe"],
|
|
420
|
+
detached: spec.group,
|
|
142
421
|
});
|
|
143
|
-
const
|
|
144
|
-
const
|
|
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
|
+
};
|
|
145
428
|
let settled = false;
|
|
146
429
|
const settle = (result) => {
|
|
147
430
|
if (settled) return;
|
|
148
431
|
settled = true;
|
|
149
432
|
clearTimeout(deadline);
|
|
150
|
-
|
|
433
|
+
spec.signal?.removeEventListener("abort", onAbort);
|
|
434
|
+
if (spec.group) sweepProcessGroup(child.pid);
|
|
435
|
+
resolveRun(result);
|
|
151
436
|
};
|
|
152
|
-
|
|
437
|
+
const onAbort = () => {
|
|
438
|
+
terminate();
|
|
439
|
+
settle(failedRun("aborted"));
|
|
440
|
+
};
|
|
441
|
+
const budgetMs = spec.budgetMs ?? HOOK_BUDGET_MS;
|
|
153
442
|
const deadline = setTimeout(() => {
|
|
154
|
-
|
|
155
|
-
settle({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
child.stderr.on("data", (chunk) => stderrChunks.push(Buffer.from(chunk)));
|
|
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
|
+
});
|
|
163
451
|
child.on("error", (error) => {
|
|
164
|
-
settle(
|
|
452
|
+
settle(
|
|
453
|
+
started ? failedRun(error.message) : { ...failedRun(error.message), notStarted: true },
|
|
454
|
+
);
|
|
165
455
|
});
|
|
166
456
|
child.on("close", (code) => {
|
|
167
|
-
settle({
|
|
168
|
-
stdout: Buffer.concat(stdoutChunks),
|
|
169
|
-
stderr: Buffer.concat(stderrChunks),
|
|
170
|
-
exitCode: code,
|
|
171
|
-
});
|
|
457
|
+
settle({ stdout: stdout(), stderr: stderr(), exitCode: code });
|
|
172
458
|
});
|
|
173
459
|
child.stdin.on("error", () => {});
|
|
174
460
|
child.stdin.end(stdinPayload);
|
|
175
461
|
});
|
|
176
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
|
+
}
|
|
598
|
+
}
|
|
177
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
|
+
}
|
|
178
691
|
function hookFailure(hookName, detail, cause) {
|
|
179
692
|
return new Error(
|
|
180
693
|
`GUARD_PRETOOL_FAILURE: webpresso hook ${hookName} ${detail}`,
|
|
181
694
|
cause === undefined ? undefined : { cause },
|
|
182
695
|
);
|
|
183
696
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
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
|
+
}
|
|
704
|
+
function extractDenyReason(envelope) {
|
|
705
|
+
const output = envelope?.hookSpecificOutput;
|
|
706
|
+
if (output?.permissionDecision === "deny") {
|
|
707
|
+
const reason = output.permissionDecisionReason;
|
|
708
|
+
return typeof reason === "string" && reason.length > 0 ? reason : DEFAULT_DENY_REASON;
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
function extractUpdatedCommand(envelope) {
|
|
713
|
+
const output = envelope?.hookSpecificOutput;
|
|
714
|
+
if (output?.permissionDecision !== "allow") return null;
|
|
715
|
+
const updatedInput = output.updatedInput;
|
|
716
|
+
if (typeof updatedInput !== "object" || updatedInput === null || Array.isArray(updatedInput)) {
|
|
198
717
|
return null;
|
|
199
718
|
}
|
|
200
|
-
const
|
|
719
|
+
const command = updatedInput.command;
|
|
720
|
+
return typeof command === "string" ? command : null;
|
|
721
|
+
}
|
|
722
|
+
function interpretHookResult(hookName, result, required) {
|
|
201
723
|
const stdout = result.stdout.toString().trim();
|
|
202
724
|
const stderr = result.stderr.toString().trim();
|
|
203
|
-
const detail = stderr ? `: ${stderr}
|
|
204
|
-
|
|
725
|
+
const detail = stderr === "" ? "" : `: ${stderr}`;
|
|
205
726
|
if (result.exitCode !== 0) {
|
|
206
727
|
if (!required) return null;
|
|
207
728
|
throw hookFailure(
|
|
@@ -215,98 +736,558 @@ async function runHook(directory, hookName, payload, options = {}) {
|
|
|
215
736
|
if (required) throw hookFailure(hookName, "produced no JSON response");
|
|
216
737
|
return null;
|
|
217
738
|
}
|
|
739
|
+
return parseHookEnvelope(hookName, stdout, detail, required);
|
|
740
|
+
}
|
|
741
|
+
function parseHookEnvelope(hookName, stdout, detail, required) {
|
|
742
|
+
let parsed;
|
|
218
743
|
try {
|
|
219
|
-
|
|
744
|
+
parsed = JSON.parse(stdout);
|
|
220
745
|
} catch (error) {
|
|
221
746
|
if (required) throw hookFailure(hookName, `produced invalid JSON${detail}`, error);
|
|
222
747
|
return null;
|
|
223
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;
|
|
224
754
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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());
|
|
237
776
|
}
|
|
238
|
-
|
|
239
|
-
|
|
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 : {};
|
|
792
|
+
const base = {
|
|
793
|
+
cwd: directory,
|
|
794
|
+
hook_event_name: hookEventName,
|
|
795
|
+
...(sessionId === null ? {} : { session_id: sessionId }),
|
|
796
|
+
};
|
|
797
|
+
switch (tool) {
|
|
798
|
+
case "shell":
|
|
799
|
+
return {
|
|
800
|
+
...base,
|
|
801
|
+
tool_name: "Bash",
|
|
802
|
+
tool_input: {
|
|
803
|
+
...toolInput,
|
|
804
|
+
command: typeof toolInput.command === "string" ? toolInput.command : "",
|
|
805
|
+
},
|
|
806
|
+
};
|
|
807
|
+
case "write":
|
|
808
|
+
return {
|
|
809
|
+
...base,
|
|
810
|
+
tool_name: "Write",
|
|
811
|
+
tool_input: {
|
|
812
|
+
file_path: stringField(toolInput.path),
|
|
813
|
+
content: stringField(toolInput.content),
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
case "edit":
|
|
817
|
+
return {
|
|
818
|
+
...base,
|
|
819
|
+
tool_name: "Edit",
|
|
820
|
+
tool_input: {
|
|
821
|
+
file_path: stringField(toolInput.path),
|
|
822
|
+
old_string: stringField(toolInput.oldString),
|
|
823
|
+
new_string: stringField(toolInput.newString),
|
|
824
|
+
},
|
|
825
|
+
};
|
|
826
|
+
case "read":
|
|
827
|
+
return {
|
|
828
|
+
...base,
|
|
829
|
+
tool_name: "Read",
|
|
830
|
+
tool_input: { file_path: stringField(toolInput.path) },
|
|
831
|
+
};
|
|
832
|
+
case "patch":
|
|
833
|
+
return {
|
|
834
|
+
...base,
|
|
835
|
+
tool_name: "apply_patch",
|
|
836
|
+
tool_input: { patch_text: stringField(toolInput.patchText) },
|
|
837
|
+
};
|
|
838
|
+
default:
|
|
839
|
+
return null;
|
|
240
840
|
}
|
|
241
|
-
|
|
242
|
-
|
|
841
|
+
}
|
|
842
|
+
function stringField(value) {
|
|
843
|
+
return typeof value === "string" ? value : "";
|
|
844
|
+
}
|
|
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
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
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
|
+
});
|
|
869
|
+
}
|
|
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;
|
|
877
|
+
}
|
|
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`);
|
|
883
|
+
}
|
|
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`,
|
|
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}`);
|
|
900
|
+
}
|
|
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);
|
|
243
918
|
}
|
|
244
919
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
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 });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
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) {
|
|
975
|
+
try {
|
|
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;
|
|
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 {
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
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,
|
|
256
1034
|
},
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
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
|
+
}
|
|
1105
|
+
return {
|
|
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);
|
|
278
1151
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
sessionIdFrom(input),
|
|
287
|
-
);
|
|
288
|
-
if (payload === null) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
for (const hookName of POST_TOOL_HOOKS) {
|
|
293
|
-
await runHook(directory, hookName, payload);
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
"experimental.session.compacting": async (_input, output) => {
|
|
297
|
-
cachedSessionContexts = await collectSessionStartContext(directory);
|
|
298
|
-
if (output && Array.isArray(output.context)) {
|
|
299
|
-
for (const message of cachedSessionContexts) {
|
|
300
|
-
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);
|
|
301
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;
|
|
302
1266
|
}
|
|
1267
|
+
return cleanup;
|
|
303
1268
|
},
|
|
304
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,
|
|
305
1293
|
};
|
|
306
|
-
|
|
307
|
-
const webpressoOpenCodePlugin = {
|
|
308
|
-
id: "@webpresso/plugin-opencode",
|
|
309
|
-
server: WebpressoHooksPlugin,
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
export default webpressoOpenCodePlugin;
|