@systemfsoftware/omp-claude-compat 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.js +1059 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, isAbsolute, resolve, sep } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
//#region ../../../node_modules/.pnpm/@jsr+std__collections@1.3.0/node_modules/@jsr/std__collections/deep_merge.js
|
|
5
|
+
/** Default merging options - cached to avoid object allocation on each call */ const DEFAULT_OPTIONS = {
|
|
6
|
+
arrays: "merge",
|
|
7
|
+
sets: "merge",
|
|
8
|
+
maps: "merge"
|
|
9
|
+
};
|
|
10
|
+
function deepMerge(record, other, options) {
|
|
11
|
+
return deepMergeInternal(record, other, /* @__PURE__ */ new Set(), options ?? DEFAULT_OPTIONS);
|
|
12
|
+
}
|
|
13
|
+
function deepMergeInternal(record, other, seen, options) {
|
|
14
|
+
const result = {};
|
|
15
|
+
const keys = /* @__PURE__ */ new Set([...getKeys(record), ...getKeys(other)]);
|
|
16
|
+
for (const key of keys) {
|
|
17
|
+
if (key === "__proto__") continue;
|
|
18
|
+
const a = record[key];
|
|
19
|
+
if (!Object.hasOwn(other, key)) {
|
|
20
|
+
result[key] = a;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const b = other[key];
|
|
24
|
+
if (isNonNullObject(a) && isNonNullObject(b) && !seen.has(a) && !seen.has(b)) {
|
|
25
|
+
seen.add(a);
|
|
26
|
+
seen.add(b);
|
|
27
|
+
result[key] = mergeObjects(a, b, seen, options);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
result[key] = b;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function mergeObjects(left, right, seen, options) {
|
|
35
|
+
if (isMergeable(left) && isMergeable(right)) return deepMergeInternal(left, right, seen, options);
|
|
36
|
+
if (isIterable(left) && isIterable(right)) {
|
|
37
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
38
|
+
if (options.arrays === "merge") return left.concat(right);
|
|
39
|
+
return right;
|
|
40
|
+
}
|
|
41
|
+
if (left instanceof Map && right instanceof Map) {
|
|
42
|
+
if (options.maps === "merge") {
|
|
43
|
+
const result = new Map(left);
|
|
44
|
+
for (const [k, v] of right) result.set(k, v);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
return right;
|
|
48
|
+
}
|
|
49
|
+
if (left instanceof Set && right instanceof Set) {
|
|
50
|
+
if (options.sets === "merge") {
|
|
51
|
+
const result = new Set(left);
|
|
52
|
+
for (const v of right) result.add(v);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
return right;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return right;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Test whether a value is mergeable or not
|
|
62
|
+
* Builtins that look like objects, null and user defined classes
|
|
63
|
+
* are not considered mergeable (it means that reference will be copied)
|
|
64
|
+
*/ function isMergeable(value) {
|
|
65
|
+
return Object.getPrototypeOf(value) === Object.prototype;
|
|
66
|
+
}
|
|
67
|
+
function isIterable(value) {
|
|
68
|
+
return typeof value[Symbol.iterator] === "function";
|
|
69
|
+
}
|
|
70
|
+
function isNonNullObject(value) {
|
|
71
|
+
return value !== null && typeof value === "object";
|
|
72
|
+
}
|
|
73
|
+
function getKeys(record) {
|
|
74
|
+
const keys = Object.keys(record);
|
|
75
|
+
const symbols = Object.getOwnPropertySymbols(record);
|
|
76
|
+
if (symbols.length === 0) return keys;
|
|
77
|
+
for (const sym of symbols) if (Object.prototype.propertyIsEnumerable.call(record, sym)) keys.push(sym);
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region ../../../node_modules/.pnpm/@jsr+std__toml@1.0.11/node_modules/@jsr/std__toml/_parser.js
|
|
82
|
+
/**
|
|
83
|
+
* Copy of `import { isLeap } from "@std/datetime";` because it cannot be impoted as long as it is unstable.
|
|
84
|
+
*/ function isLeap(yearNumber) {
|
|
85
|
+
return yearNumber % 4 === 0 && yearNumber % 100 !== 0 || yearNumber % 400 === 0;
|
|
86
|
+
}
|
|
87
|
+
function success(body) {
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
body
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function failure() {
|
|
94
|
+
return { ok: false };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Creates a nested object from the keys and values.
|
|
98
|
+
*
|
|
99
|
+
* e.g. `unflat(["a", "b", "c"], 1)` returns `{ a: { b: { c: 1 } } }`
|
|
100
|
+
*/ function unflat(keys, values = { __proto__: null }) {
|
|
101
|
+
return keys.reduceRight((acc, key) => ({ [key]: acc }), values);
|
|
102
|
+
}
|
|
103
|
+
function or(parsers) {
|
|
104
|
+
return (scanner) => {
|
|
105
|
+
for (const parse of parsers) {
|
|
106
|
+
const result = parse(scanner);
|
|
107
|
+
if (result.ok) return result;
|
|
108
|
+
}
|
|
109
|
+
return failure();
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Join the parse results of the given parser into an array.
|
|
113
|
+
*
|
|
114
|
+
* If the parser fails at the first attempt, it will return an empty array.
|
|
115
|
+
*/ function join(parser, separator) {
|
|
116
|
+
const Separator = character(separator);
|
|
117
|
+
return (scanner) => {
|
|
118
|
+
const out = [];
|
|
119
|
+
const first = parser(scanner);
|
|
120
|
+
if (!first.ok) return success(out);
|
|
121
|
+
out.push(first.body);
|
|
122
|
+
while (!scanner.eof()) {
|
|
123
|
+
if (!Separator(scanner).ok) break;
|
|
124
|
+
const result = parser(scanner);
|
|
125
|
+
if (!result.ok) throw new SyntaxError(`Invalid token after "${separator}"`);
|
|
126
|
+
out.push(result.body);
|
|
127
|
+
}
|
|
128
|
+
return success(out);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Join the parse results of the given parser into an array.
|
|
132
|
+
*
|
|
133
|
+
* This requires the parser to succeed at least once.
|
|
134
|
+
*/ function join1(parser, separator) {
|
|
135
|
+
const Separator = character(separator);
|
|
136
|
+
return (scanner) => {
|
|
137
|
+
const first = parser(scanner);
|
|
138
|
+
if (!first.ok) return failure();
|
|
139
|
+
const out = [first.body];
|
|
140
|
+
while (!scanner.eof()) {
|
|
141
|
+
if (!Separator(scanner).ok) break;
|
|
142
|
+
const result = parser(scanner);
|
|
143
|
+
if (!result.ok) throw new SyntaxError(`Invalid token after "${separator}"`);
|
|
144
|
+
out.push(result.body);
|
|
145
|
+
}
|
|
146
|
+
return success(out);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function kv(keyParser, separator, valueParser) {
|
|
150
|
+
const Separator = character(separator);
|
|
151
|
+
return (scanner) => {
|
|
152
|
+
const position = scanner.position;
|
|
153
|
+
const key = keyParser(scanner);
|
|
154
|
+
if (!key.ok) return failure();
|
|
155
|
+
if (!Separator(scanner).ok) throw new SyntaxError(`key/value pair doesn't have "${separator}"`);
|
|
156
|
+
const value = valueParser(scanner);
|
|
157
|
+
if (!value.ok) {
|
|
158
|
+
const lineEndIndex = scanner.source.indexOf("\n", scanner.position);
|
|
159
|
+
const endPosition = lineEndIndex > 0 ? lineEndIndex : scanner.source.length;
|
|
160
|
+
const line = scanner.source.slice(position, endPosition);
|
|
161
|
+
throw new SyntaxError(`Cannot parse value on line '${line}'`);
|
|
162
|
+
}
|
|
163
|
+
return success(unflat(key.body, value.body));
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function surround(left, parser, right) {
|
|
167
|
+
const Left = character(left);
|
|
168
|
+
const Right = character(right);
|
|
169
|
+
return (scanner) => {
|
|
170
|
+
if (!Left(scanner).ok) return failure();
|
|
171
|
+
const result = parser(scanner);
|
|
172
|
+
if (!result.ok) throw new SyntaxError(`Invalid token after "${left}"`);
|
|
173
|
+
if (!Right(scanner).ok) throw new SyntaxError(`Not closed by "${right}" after started with "${left}"`);
|
|
174
|
+
return success(result.body);
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function character(str) {
|
|
178
|
+
return (scanner) => {
|
|
179
|
+
scanner.skipWhitespaces();
|
|
180
|
+
if (!scanner.startsWith(str)) return failure();
|
|
181
|
+
scanner.next(str.length);
|
|
182
|
+
scanner.skipWhitespaces();
|
|
183
|
+
return success(void 0);
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const BARE_KEY_REGEXP = /[A-Za-z0-9_-]+/y;
|
|
187
|
+
function bareKey(scanner) {
|
|
188
|
+
scanner.skipWhitespaces();
|
|
189
|
+
const key = scanner.match(BARE_KEY_REGEXP)?.[0];
|
|
190
|
+
if (!key) return failure();
|
|
191
|
+
scanner.next(key.length);
|
|
192
|
+
return success(key);
|
|
193
|
+
}
|
|
194
|
+
function escapeSequence(scanner) {
|
|
195
|
+
if (scanner.char() !== "\\") return failure();
|
|
196
|
+
scanner.next();
|
|
197
|
+
switch (scanner.char()) {
|
|
198
|
+
case "b":
|
|
199
|
+
scanner.next();
|
|
200
|
+
return success("\b");
|
|
201
|
+
case "t":
|
|
202
|
+
scanner.next();
|
|
203
|
+
return success(" ");
|
|
204
|
+
case "n":
|
|
205
|
+
scanner.next();
|
|
206
|
+
return success("\n");
|
|
207
|
+
case "f":
|
|
208
|
+
scanner.next();
|
|
209
|
+
return success("\f");
|
|
210
|
+
case "r":
|
|
211
|
+
scanner.next();
|
|
212
|
+
return success("\r");
|
|
213
|
+
case "u":
|
|
214
|
+
case "U": {
|
|
215
|
+
const codePointLen = scanner.char() === "u" ? 4 : 6;
|
|
216
|
+
const codePoint = parseInt("0x" + scanner.slice(1, 1 + codePointLen), 16);
|
|
217
|
+
const str = String.fromCodePoint(codePoint);
|
|
218
|
+
scanner.next(codePointLen + 1);
|
|
219
|
+
return success(str);
|
|
220
|
+
}
|
|
221
|
+
case "\"":
|
|
222
|
+
scanner.next();
|
|
223
|
+
return success("\"");
|
|
224
|
+
case "\\":
|
|
225
|
+
scanner.next();
|
|
226
|
+
return success("\\");
|
|
227
|
+
default: throw new SyntaxError(`Invalid escape sequence: \\${scanner.char()}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function basicString(scanner) {
|
|
231
|
+
scanner.skipWhitespaces();
|
|
232
|
+
if (scanner.char() !== "\"") return failure();
|
|
233
|
+
scanner.next();
|
|
234
|
+
const acc = [];
|
|
235
|
+
while (scanner.char() !== "\"" && !scanner.eof()) {
|
|
236
|
+
if (scanner.char() === "\n") throw new SyntaxError("Single-line string cannot contain EOL");
|
|
237
|
+
const escapedChar = escapeSequence(scanner);
|
|
238
|
+
if (escapedChar.ok) acc.push(escapedChar.body);
|
|
239
|
+
else {
|
|
240
|
+
acc.push(scanner.char());
|
|
241
|
+
scanner.next();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (scanner.eof()) throw new SyntaxError(`Single-line string is not closed:\n${acc.join("")}`);
|
|
245
|
+
scanner.next();
|
|
246
|
+
return success(acc.join(""));
|
|
247
|
+
}
|
|
248
|
+
function literalString(scanner) {
|
|
249
|
+
scanner.skipWhitespaces();
|
|
250
|
+
if (scanner.char() !== "'") return failure();
|
|
251
|
+
scanner.next();
|
|
252
|
+
const acc = [];
|
|
253
|
+
while (scanner.char() !== "'" && !scanner.eof()) {
|
|
254
|
+
if (scanner.char() === "\n") throw new SyntaxError("Single-line string cannot contain EOL");
|
|
255
|
+
acc.push(scanner.char());
|
|
256
|
+
scanner.next();
|
|
257
|
+
}
|
|
258
|
+
if (scanner.eof()) throw new SyntaxError(`Single-line string is not closed:\n${acc.join("")}`);
|
|
259
|
+
scanner.next();
|
|
260
|
+
return success(acc.join(""));
|
|
261
|
+
}
|
|
262
|
+
function multilineBasicString(scanner) {
|
|
263
|
+
scanner.skipWhitespaces();
|
|
264
|
+
if (!scanner.startsWith("\"\"\"")) return failure();
|
|
265
|
+
scanner.next(3);
|
|
266
|
+
if (scanner.char() === "\n") scanner.next();
|
|
267
|
+
else if (scanner.startsWith("\r\n")) scanner.next(2);
|
|
268
|
+
const acc = [];
|
|
269
|
+
while (!scanner.startsWith("\"\"\"") && !scanner.eof()) {
|
|
270
|
+
if (scanner.startsWith("\\\n")) {
|
|
271
|
+
scanner.next();
|
|
272
|
+
scanner.nextUntilChar({ skipComments: false });
|
|
273
|
+
continue;
|
|
274
|
+
} else if (scanner.startsWith("\\\r\n")) {
|
|
275
|
+
scanner.next();
|
|
276
|
+
scanner.nextUntilChar({ skipComments: false });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const escapedChar = escapeSequence(scanner);
|
|
280
|
+
if (escapedChar.ok) acc.push(escapedChar.body);
|
|
281
|
+
else {
|
|
282
|
+
acc.push(scanner.char());
|
|
283
|
+
scanner.next();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (scanner.eof()) throw new SyntaxError(`Multi-line string is not closed:\n${acc.join("")}`);
|
|
287
|
+
if (scanner.char(3) === "\"") {
|
|
288
|
+
acc.push("\"");
|
|
289
|
+
scanner.next();
|
|
290
|
+
}
|
|
291
|
+
scanner.next(3);
|
|
292
|
+
return success(acc.join(""));
|
|
293
|
+
}
|
|
294
|
+
function multilineLiteralString(scanner) {
|
|
295
|
+
scanner.skipWhitespaces();
|
|
296
|
+
if (!scanner.startsWith("'''")) return failure();
|
|
297
|
+
scanner.next(3);
|
|
298
|
+
if (scanner.char() === "\n") scanner.next();
|
|
299
|
+
else if (scanner.startsWith("\r\n")) scanner.next(2);
|
|
300
|
+
const acc = [];
|
|
301
|
+
while (!scanner.startsWith("'''") && !scanner.eof()) {
|
|
302
|
+
acc.push(scanner.char());
|
|
303
|
+
scanner.next();
|
|
304
|
+
}
|
|
305
|
+
if (scanner.eof()) throw new SyntaxError(`Multi-line string is not closed:\n${acc.join("")}`);
|
|
306
|
+
if (scanner.char(3) === "'") {
|
|
307
|
+
acc.push("'");
|
|
308
|
+
scanner.next();
|
|
309
|
+
}
|
|
310
|
+
scanner.next(3);
|
|
311
|
+
return success(acc.join(""));
|
|
312
|
+
}
|
|
313
|
+
const BOOLEAN_REGEXP = /(?:true|false)\b/y;
|
|
314
|
+
function boolean(scanner) {
|
|
315
|
+
scanner.skipWhitespaces();
|
|
316
|
+
const match = scanner.match(BOOLEAN_REGEXP);
|
|
317
|
+
if (!match) return failure();
|
|
318
|
+
const string = match[0];
|
|
319
|
+
scanner.next(string.length);
|
|
320
|
+
return success(string === "true");
|
|
321
|
+
}
|
|
322
|
+
const INFINITY_MAP = /* @__PURE__ */ new Map([
|
|
323
|
+
["inf", Infinity],
|
|
324
|
+
["+inf", Infinity],
|
|
325
|
+
["-inf", -Infinity]
|
|
326
|
+
]);
|
|
327
|
+
const INFINITY_REGEXP = /[+-]?inf\b/y;
|
|
328
|
+
function infinity(scanner) {
|
|
329
|
+
scanner.skipWhitespaces();
|
|
330
|
+
const match = scanner.match(INFINITY_REGEXP);
|
|
331
|
+
if (!match) return failure();
|
|
332
|
+
const string = match[0];
|
|
333
|
+
scanner.next(string.length);
|
|
334
|
+
return success(INFINITY_MAP.get(string));
|
|
335
|
+
}
|
|
336
|
+
const NAN_REGEXP = /[+-]?nan\b/y;
|
|
337
|
+
function nan(scanner) {
|
|
338
|
+
scanner.skipWhitespaces();
|
|
339
|
+
const match = scanner.match(NAN_REGEXP);
|
|
340
|
+
if (!match) return failure();
|
|
341
|
+
const string = match[0];
|
|
342
|
+
scanner.next(string.length);
|
|
343
|
+
return success(NaN);
|
|
344
|
+
}
|
|
345
|
+
const dottedKey = join1(or([
|
|
346
|
+
bareKey,
|
|
347
|
+
basicString,
|
|
348
|
+
literalString
|
|
349
|
+
]), ".");
|
|
350
|
+
const BINARY_REGEXP = /0b[01]+(?:_[01]+)*\b/y;
|
|
351
|
+
function binary(scanner) {
|
|
352
|
+
scanner.skipWhitespaces();
|
|
353
|
+
const match = scanner.match(BINARY_REGEXP)?.[0];
|
|
354
|
+
if (!match) return failure();
|
|
355
|
+
scanner.next(match.length);
|
|
356
|
+
const value = match.slice(2).replaceAll("_", "");
|
|
357
|
+
const number = parseInt(value, 2);
|
|
358
|
+
return isNaN(number) ? failure() : success(number);
|
|
359
|
+
}
|
|
360
|
+
const OCTAL_REGEXP = /0o[0-7]+(?:_[0-7]+)*\b/y;
|
|
361
|
+
function octal(scanner) {
|
|
362
|
+
scanner.skipWhitespaces();
|
|
363
|
+
const match = scanner.match(OCTAL_REGEXP)?.[0];
|
|
364
|
+
if (!match) return failure();
|
|
365
|
+
scanner.next(match.length);
|
|
366
|
+
const value = match.slice(2).replaceAll("_", "");
|
|
367
|
+
const number = parseInt(value, 8);
|
|
368
|
+
return isNaN(number) ? failure() : success(number);
|
|
369
|
+
}
|
|
370
|
+
const HEX_REGEXP = /0x[0-9a-f]+(?:_[0-9a-f]+)*\b/iy;
|
|
371
|
+
function hex(scanner) {
|
|
372
|
+
scanner.skipWhitespaces();
|
|
373
|
+
const match = scanner.match(HEX_REGEXP)?.[0];
|
|
374
|
+
if (!match) return failure();
|
|
375
|
+
scanner.next(match.length);
|
|
376
|
+
const value = match.slice(2).replaceAll("_", "");
|
|
377
|
+
const number = parseInt(value, 16);
|
|
378
|
+
return isNaN(number) ? failure() : success(number);
|
|
379
|
+
}
|
|
380
|
+
const INTEGER_REGEXP = /[+-]?(?:0|[1-9][0-9]*(?:_[0-9]+)*)\b/y;
|
|
381
|
+
function integer(scanner) {
|
|
382
|
+
scanner.skipWhitespaces();
|
|
383
|
+
const match = scanner.match(INTEGER_REGEXP)?.[0];
|
|
384
|
+
if (!match) return failure();
|
|
385
|
+
scanner.next(match.length);
|
|
386
|
+
const value = match.replaceAll("_", "");
|
|
387
|
+
return success(parseInt(value, 10));
|
|
388
|
+
}
|
|
389
|
+
const FLOAT_REGEXP = /[+-]?(?:0|[1-9][0-9]*(?:_[0-9]+)*)(?:\.[0-9]+(?:_[0-9]+)*)?(?:e[+-]?[0-9]+(?:_[0-9]+)*)?\b/iy;
|
|
390
|
+
function float(scanner) {
|
|
391
|
+
scanner.skipWhitespaces();
|
|
392
|
+
const match = scanner.match(FLOAT_REGEXP)?.[0];
|
|
393
|
+
if (!match) return failure();
|
|
394
|
+
scanner.next(match.length);
|
|
395
|
+
const value = match.replaceAll("_", "");
|
|
396
|
+
const float = parseFloat(value);
|
|
397
|
+
if (isNaN(float)) return failure();
|
|
398
|
+
return success(float);
|
|
399
|
+
}
|
|
400
|
+
const DATE_TIME_REGEXP = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})(?:[ 0-9TZ.:+-]+)?\b/y;
|
|
401
|
+
function dateTime(scanner) {
|
|
402
|
+
scanner.skipWhitespaces();
|
|
403
|
+
const match = scanner.match(DATE_TIME_REGEXP);
|
|
404
|
+
if (!match) return failure();
|
|
405
|
+
const string = match[0];
|
|
406
|
+
scanner.next(string.length);
|
|
407
|
+
const groups = match.groups;
|
|
408
|
+
if (groups.month == "02") {
|
|
409
|
+
const days = parseInt(groups.day);
|
|
410
|
+
if (days > 29) throw new SyntaxError(`Invalid date string "${match}"`);
|
|
411
|
+
const year = parseInt(groups.year);
|
|
412
|
+
if (days > 28 && !isLeap(year)) throw new SyntaxError(`Invalid date string "${match}"`);
|
|
413
|
+
}
|
|
414
|
+
const date = new Date(string.trim());
|
|
415
|
+
if (isNaN(date.getTime())) throw new SyntaxError(`Invalid date string "${match}"`);
|
|
416
|
+
return success(date);
|
|
417
|
+
}
|
|
418
|
+
const LOCAL_TIME_REGEXP = /(\d{2}):(\d{2}):(\d{2})(?:\.[0-9]+)?\b/y;
|
|
419
|
+
function localTime(scanner) {
|
|
420
|
+
scanner.skipWhitespaces();
|
|
421
|
+
const match = scanner.match(LOCAL_TIME_REGEXP)?.[0];
|
|
422
|
+
if (!match) return failure();
|
|
423
|
+
scanner.next(match.length);
|
|
424
|
+
return success(match);
|
|
425
|
+
}
|
|
426
|
+
function arrayValue(scanner) {
|
|
427
|
+
scanner.skipWhitespaces();
|
|
428
|
+
if (scanner.char() !== "[") return failure();
|
|
429
|
+
scanner.next();
|
|
430
|
+
const array = [];
|
|
431
|
+
while (!scanner.eof()) {
|
|
432
|
+
scanner.nextUntilChar();
|
|
433
|
+
const result = value(scanner);
|
|
434
|
+
if (!result.ok) break;
|
|
435
|
+
array.push(result.body);
|
|
436
|
+
scanner.skipWhitespaces();
|
|
437
|
+
if (scanner.char() !== ",") break;
|
|
438
|
+
scanner.next();
|
|
439
|
+
}
|
|
440
|
+
scanner.nextUntilChar();
|
|
441
|
+
if (scanner.char() !== "]") throw new SyntaxError("Array is not closed");
|
|
442
|
+
scanner.next();
|
|
443
|
+
return success(array);
|
|
444
|
+
}
|
|
445
|
+
function inlineTable(scanner) {
|
|
446
|
+
scanner.nextUntilChar();
|
|
447
|
+
if (scanner.char(1) === "}") {
|
|
448
|
+
scanner.next(2);
|
|
449
|
+
return success({ __proto__: null });
|
|
450
|
+
}
|
|
451
|
+
const pairs = surround("{", join(pair, ","), "}")(scanner);
|
|
452
|
+
if (!pairs.ok) return failure();
|
|
453
|
+
let table = { __proto__: null };
|
|
454
|
+
for (const pair of pairs.body) table = deepMerge(table, pair);
|
|
455
|
+
return success(table);
|
|
456
|
+
}
|
|
457
|
+
const value = or([
|
|
458
|
+
multilineBasicString,
|
|
459
|
+
multilineLiteralString,
|
|
460
|
+
basicString,
|
|
461
|
+
literalString,
|
|
462
|
+
boolean,
|
|
463
|
+
infinity,
|
|
464
|
+
nan,
|
|
465
|
+
dateTime,
|
|
466
|
+
localTime,
|
|
467
|
+
binary,
|
|
468
|
+
octal,
|
|
469
|
+
hex,
|
|
470
|
+
float,
|
|
471
|
+
integer,
|
|
472
|
+
arrayValue,
|
|
473
|
+
inlineTable
|
|
474
|
+
]);
|
|
475
|
+
const pair = kv(dottedKey, "=", value);
|
|
476
|
+
surround("[", dottedKey, "]");
|
|
477
|
+
surround("[[", dottedKey, "]]");
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region ../omp-utils/dist/index.js
|
|
480
|
+
/**
|
|
481
|
+
* ACL: detect and extract shell commands from context-mode tool invocations.
|
|
482
|
+
*
|
|
483
|
+
* Context-mode tools (ctx_execute, ctx_batch_execute) can execute shell
|
|
484
|
+
* commands that should be inspected by shell-guard hooks.
|
|
485
|
+
*/
|
|
486
|
+
const CONTEXT_MODE_SHELL_TOOLS = {
|
|
487
|
+
ctx_execute: true,
|
|
488
|
+
ctx_batch_execute: true
|
|
489
|
+
};
|
|
490
|
+
function isContextModeShellTool(toolName, input) {
|
|
491
|
+
if (!CONTEXT_MODE_SHELL_TOOLS[toolName]) return false;
|
|
492
|
+
if (toolName === "ctx_execute") return input["language"] === "shell";
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
function extractShellCommand(toolName, input) {
|
|
496
|
+
if (!isContextModeShellTool(toolName, input)) return void 0;
|
|
497
|
+
if (toolName === "ctx_execute") return typeof input["code"] === "string" ? input["code"] : void 0;
|
|
498
|
+
if (Array.isArray(input["commands"])) return input["commands"].map((entry) => typeof entry === "object" && entry !== null ? entry["command"] : "").filter((cmd) => typeof cmd === "string").join("\n");
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* ACL: match OMP tool names against Claude Code hook matcher patterns.
|
|
502
|
+
*
|
|
503
|
+
* Hook matchers are pipe-separated regex patterns (e.g. "Write|Edit").
|
|
504
|
+
* An empty/undefined matcher matches everything.
|
|
505
|
+
*/
|
|
506
|
+
const regexCache = /* @__PURE__ */ new Map();
|
|
507
|
+
function matchesMatcher(toolName, matcher) {
|
|
508
|
+
if (!matcher || matcher.length === 0) return true;
|
|
509
|
+
const pattern = matcher.split("|").map((part) => part.trim()).filter(Boolean).join("|");
|
|
510
|
+
if (pattern.length === 0) return true;
|
|
511
|
+
const cached = regexCache.get(pattern);
|
|
512
|
+
if (cached !== void 0) return cached.test(toolName);
|
|
513
|
+
const regex = new RegExp(`^(?:${pattern})$`);
|
|
514
|
+
regexCache.set(pattern, regex);
|
|
515
|
+
return regex.test(toolName);
|
|
516
|
+
}
|
|
517
|
+
function sessionIds(getSessionId) {
|
|
518
|
+
return {
|
|
519
|
+
session_id: getSessionId(),
|
|
520
|
+
agent_id: null
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Create a telemetry emitter for a plugin.
|
|
525
|
+
*
|
|
526
|
+
* @param plugin - The event prefix (e.g. `claude_compat`, `agent_discipline`).
|
|
527
|
+
* @param logger - The `pi.logger` instance injected by the host at factory time.
|
|
528
|
+
*/
|
|
529
|
+
function createTelemetry(plugin, logger) {
|
|
530
|
+
return (eventName, fields) => {
|
|
531
|
+
try {
|
|
532
|
+
logger.info(eventName, {
|
|
533
|
+
plugin,
|
|
534
|
+
event: eventName,
|
|
535
|
+
...fields
|
|
536
|
+
});
|
|
537
|
+
} catch {}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* ACL: translate OMP tool input shapes to Claude Code hook input shapes.
|
|
542
|
+
*
|
|
543
|
+
* OMP sends `edits: [{ old_text, new_text }]` and `path`;
|
|
544
|
+
* Claude Code hooks expect `old_string`/`new_string` and `file_path`.
|
|
545
|
+
*/
|
|
546
|
+
const FILE_TOOLS = {
|
|
547
|
+
Write: true,
|
|
548
|
+
Edit: true,
|
|
549
|
+
Read: true,
|
|
550
|
+
MultiEdit: true,
|
|
551
|
+
Update: true,
|
|
552
|
+
Create: true
|
|
553
|
+
};
|
|
554
|
+
const EDIT_TOOLS = {
|
|
555
|
+
Edit: true,
|
|
556
|
+
MultiEdit: true,
|
|
557
|
+
Update: true
|
|
558
|
+
};
|
|
559
|
+
function isOmpEditArray(value) {
|
|
560
|
+
return Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "object" && entry !== null && ("new_text" in entry || "old_text" in entry));
|
|
561
|
+
}
|
|
562
|
+
function normalizeToolInput(toolName, input) {
|
|
563
|
+
let out = input;
|
|
564
|
+
if (FILE_TOOLS[toolName] === true && "path" in out && !("file_path" in out)) {
|
|
565
|
+
const { path, ...rest } = out;
|
|
566
|
+
out = {
|
|
567
|
+
file_path: path,
|
|
568
|
+
...rest
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
if (EDIT_TOOLS[toolName] === true && "edits" in out && isOmpEditArray(out["edits"]) && !("new_string" in out)) {
|
|
572
|
+
const claudeEdits = out["edits"].map((entry) => ({
|
|
573
|
+
old_string: typeof entry["old_text"] === "string" ? entry["old_text"] : "",
|
|
574
|
+
new_string: typeof entry["new_text"] === "string" ? entry["new_text"] : ""
|
|
575
|
+
}));
|
|
576
|
+
out = {
|
|
577
|
+
...out,
|
|
578
|
+
edits: claudeEdits,
|
|
579
|
+
old_string: claudeEdits.map((entry) => entry["old_string"]).join("\n"),
|
|
580
|
+
new_string: claudeEdits.map((entry) => entry["new_string"]).join("\n")
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return out;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* ACL: normalize OMP tool names to Claude Code convention.
|
|
587
|
+
*
|
|
588
|
+
* OMP emits lowercase tool names (write, bash, read);
|
|
589
|
+
* Claude Code hook system expects capitalized (Write, Bash, Read).
|
|
590
|
+
*/
|
|
591
|
+
function normalizeToolName(name) {
|
|
592
|
+
if (name.length === 0) return name;
|
|
593
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
594
|
+
}
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/hook-dispatcher.ts
|
|
597
|
+
function getSessionIds(ctx) {
|
|
598
|
+
return sessionIds(() => ctx.sessionManager.getSessionId());
|
|
599
|
+
}
|
|
600
|
+
/** Module-scoped telemetry emitter, initialized in the default export. */
|
|
601
|
+
let tel = () => {};
|
|
602
|
+
function hookDispatcherExtension(pi) {
|
|
603
|
+
tel = createTelemetry("claude_compat", pi.logger);
|
|
604
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
605
|
+
const settings = loadSettings(ctx.cwd);
|
|
606
|
+
if (!settings) return void 0;
|
|
607
|
+
return runPreToolUseHooks(settings, event, ctx);
|
|
608
|
+
});
|
|
609
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
610
|
+
const settings = loadSettings(ctx.cwd);
|
|
611
|
+
if (!settings) return void 0;
|
|
612
|
+
const result = await runPostToolUseHooks(settings, event, ctx);
|
|
613
|
+
if (result?.block) return {
|
|
614
|
+
isError: true,
|
|
615
|
+
content: [{
|
|
616
|
+
type: "text",
|
|
617
|
+
text: result.reason ?? `Blocked by PostToolUse hook`
|
|
618
|
+
}]
|
|
619
|
+
};
|
|
620
|
+
if (result?.warning) return {
|
|
621
|
+
content: [...event.content ?? [], {
|
|
622
|
+
type: "text",
|
|
623
|
+
text: result.warning
|
|
624
|
+
}],
|
|
625
|
+
isError: event.isError
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
pi.on("input", async (event, ctx) => {
|
|
629
|
+
const settings = loadSettings(ctx.cwd);
|
|
630
|
+
if (!settings) return void 0;
|
|
631
|
+
return runUserPromptSubmitHooks(settings, event, ctx);
|
|
632
|
+
});
|
|
633
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
634
|
+
const settings = loadSettings(ctx.cwd);
|
|
635
|
+
if (!settings) return void 0;
|
|
636
|
+
await runSessionStartHooks(settings, "start", ctx);
|
|
637
|
+
});
|
|
638
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
639
|
+
const settings = loadSettings(ctx.cwd);
|
|
640
|
+
if (!settings) return void 0;
|
|
641
|
+
await runSessionStartHooks(settings, "compact", ctx);
|
|
642
|
+
});
|
|
643
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
644
|
+
const settings = loadSettings(ctx.cwd);
|
|
645
|
+
if (!settings) return void 0;
|
|
646
|
+
await runSessionStartHooks(settings, "resume", ctx);
|
|
647
|
+
});
|
|
648
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
649
|
+
const settings = loadSettings(ctx.cwd);
|
|
650
|
+
if (!settings) return void 0;
|
|
651
|
+
await runLifecycleHooks(settings.hooks.SessionEnd, ctx);
|
|
652
|
+
});
|
|
653
|
+
pi.on("session_stop", async (_event, ctx) => {
|
|
654
|
+
const settings = loadSettings(ctx.cwd);
|
|
655
|
+
if (!settings) return void 0;
|
|
656
|
+
await runLifecycleHooks(settings.hooks.Stop, ctx);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
const settingsCache = /* @__PURE__ */ new Map();
|
|
660
|
+
function loadSettings(cwd) {
|
|
661
|
+
const cached = settingsCache.get(cwd);
|
|
662
|
+
if (cached !== void 0) return cached;
|
|
663
|
+
const settingsPath = resolve(cwd, ".claude", "settings.json");
|
|
664
|
+
if (!existsSync(settingsPath)) return null;
|
|
665
|
+
try {
|
|
666
|
+
const data = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
667
|
+
const source = data.hooks ?? data;
|
|
668
|
+
const group = (key) => Array.isArray(source[key]) ? source[key] : [];
|
|
669
|
+
const result = { hooks: {
|
|
670
|
+
PreToolUse: group("PreToolUse"),
|
|
671
|
+
PostToolUse: group("PostToolUse"),
|
|
672
|
+
UserPromptSubmit: group("UserPromptSubmit"),
|
|
673
|
+
Stop: group("Stop"),
|
|
674
|
+
SessionStart: group("SessionStart"),
|
|
675
|
+
SessionEnd: group("SessionEnd")
|
|
676
|
+
} };
|
|
677
|
+
settingsCache.set(cwd, result);
|
|
678
|
+
return result;
|
|
679
|
+
} catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function resolveCommand(command, cwd) {
|
|
684
|
+
const trimmed = command.replace(/"\$OMP_PROJECT_DIR"|'\$OMP_PROJECT_DIR'/g, JSON.stringify(cwd)).replace(/"\$\{OMP_PROJECT_DIR\}"|'\$\{OMP_PROJECT_DIR\}'/g, JSON.stringify(cwd)).replace(/"\$CLAUDE_PROJECT_DIR"|'\$CLAUDE_PROJECT_DIR'/g, JSON.stringify(cwd)).replace(/"\$\{CLAUDE_PROJECT_DIR\}"|'\$\{CLAUDE_PROJECT_DIR\}'/g, JSON.stringify(cwd)).replace(/\$OMP_PROJECT_DIR|\$\{OMP_PROJECT_DIR\}/g, JSON.stringify(cwd)).replace(/\$CLAUDE_PROJECT_DIR|\$\{CLAUDE_PROJECT_DIR\}/g, JSON.stringify(cwd)).trim();
|
|
685
|
+
const unquoted = trimmed.startsWith("\"") && trimmed.endsWith("\"") ? trimmed.slice(1, -1) : trimmed;
|
|
686
|
+
const pathPart = unquoted.split(/\s+/)[0] ?? "";
|
|
687
|
+
if (extname(pathPart) === ".ts") return {
|
|
688
|
+
cmd: "bun",
|
|
689
|
+
args: [resolve(cwd, pathPart)]
|
|
690
|
+
};
|
|
691
|
+
return {
|
|
692
|
+
cmd: "sh",
|
|
693
|
+
args: ["-c", unquoted]
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async function runHookScript(command, input, cwd, timeoutMs = 1e4) {
|
|
697
|
+
const { cmd, args } = resolveCommand(command, cwd);
|
|
698
|
+
const stdin = JSON.stringify(input);
|
|
699
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
700
|
+
const child = execFile(cmd, [...args], {
|
|
701
|
+
cwd,
|
|
702
|
+
timeout: timeoutMs,
|
|
703
|
+
killSignal: "SIGKILL",
|
|
704
|
+
env: {
|
|
705
|
+
...process.env,
|
|
706
|
+
OMP_PROJECT_DIR: cwd,
|
|
707
|
+
CLAUDE_PROJECT_DIR: cwd
|
|
708
|
+
},
|
|
709
|
+
maxBuffer: 1024 * 1024
|
|
710
|
+
}, (error, stdout, stderr) => {
|
|
711
|
+
if (error && typeof error.code !== "number") {
|
|
712
|
+
reject(error);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
resolve({
|
|
716
|
+
code: error?.code ?? 0,
|
|
717
|
+
stdout,
|
|
718
|
+
stderr
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
child.stdin?.on("error", () => {}).write(stdin);
|
|
722
|
+
child.stdin?.end();
|
|
723
|
+
return promise;
|
|
724
|
+
}
|
|
725
|
+
async function runHooksForEvent(entries, matchValue, input, ctx, event) {
|
|
726
|
+
const cwd = ctx.cwd;
|
|
727
|
+
let warning;
|
|
728
|
+
let inputModified = false;
|
|
729
|
+
for (const entry of entries) {
|
|
730
|
+
if (!matchesMatcher(matchValue, entry.matcher)) continue;
|
|
731
|
+
for (const hook of entry.hooks) {
|
|
732
|
+
const hookName = hook.command.split(/[\\/]/).pop() ?? hook.command;
|
|
733
|
+
const hookStart = performance.now();
|
|
734
|
+
if (hook.async) {
|
|
735
|
+
runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
|
|
736
|
+
const durationMs = Math.round(performance.now() - hookStart);
|
|
737
|
+
tel("hook.executed", {
|
|
738
|
+
hook: hookName,
|
|
739
|
+
duration_ms: durationMs,
|
|
740
|
+
exit_code: result.code
|
|
741
|
+
});
|
|
742
|
+
}, (err) => {
|
|
743
|
+
const durationMs = Math.round(performance.now() - hookStart);
|
|
744
|
+
tel("hook.executed", {
|
|
745
|
+
hook: hookName,
|
|
746
|
+
duration_ms: durationMs,
|
|
747
|
+
exit_code: null,
|
|
748
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
let result;
|
|
754
|
+
try {
|
|
755
|
+
result = await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3);
|
|
756
|
+
} catch (err) {
|
|
757
|
+
const durationMs = Math.round(performance.now() - hookStart);
|
|
758
|
+
tel("hook.executed", {
|
|
759
|
+
hook: hookName,
|
|
760
|
+
duration_ms: durationMs,
|
|
761
|
+
exit_code: null,
|
|
762
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
763
|
+
});
|
|
764
|
+
throw err;
|
|
765
|
+
}
|
|
766
|
+
const durationMs = Math.round(performance.now() - hookStart);
|
|
767
|
+
tel("hook.executed", {
|
|
768
|
+
hook: hookName,
|
|
769
|
+
duration_ms: durationMs,
|
|
770
|
+
exit_code: result.code
|
|
771
|
+
});
|
|
772
|
+
if (result.code === 2) return {
|
|
773
|
+
block: true,
|
|
774
|
+
reason: result.stderr.trim() || `Blocked by ${event} hook`
|
|
775
|
+
};
|
|
776
|
+
if (result.code !== 0) {
|
|
777
|
+
const msg = result.stderr.trim();
|
|
778
|
+
if (msg && warning === void 0) warning = msg;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const stdout = result.stdout.trim();
|
|
782
|
+
if (stdout.length > 0) try {
|
|
783
|
+
const parsed = JSON.parse(stdout);
|
|
784
|
+
const hookOutput = parsed.hookSpecificOutput;
|
|
785
|
+
if (hookOutput?.permissionDecision === "deny") return {
|
|
786
|
+
block: true,
|
|
787
|
+
reason: hookOutput.permissionDecisionReason ?? `Blocked by ${event} hook`
|
|
788
|
+
};
|
|
789
|
+
if (parsed.decision === "block") return {
|
|
790
|
+
block: true,
|
|
791
|
+
reason: parsed.reason ?? `Blocked by ${event} hook`
|
|
792
|
+
};
|
|
793
|
+
if (hookOutput?.updatedInput) {
|
|
794
|
+
input = {
|
|
795
|
+
...input,
|
|
796
|
+
...hookOutput.updatedInput
|
|
797
|
+
};
|
|
798
|
+
inputModified = true;
|
|
799
|
+
}
|
|
800
|
+
} catch {}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return {
|
|
804
|
+
...inputModified ? { updatedInput: input } : {},
|
|
805
|
+
...warning !== void 0 ? { warning } : {}
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
async function runPreToolUseHooks(settings, event, ctx) {
|
|
809
|
+
const claudeToolName = normalizeToolName(event.toolName);
|
|
810
|
+
const input = {
|
|
811
|
+
...getSessionIds(ctx),
|
|
812
|
+
tool_name: claudeToolName,
|
|
813
|
+
tool_input: normalizeToolInput(claudeToolName, event.input),
|
|
814
|
+
tool_call_id: event.toolCallId
|
|
815
|
+
};
|
|
816
|
+
const shellCommand = extractShellCommand(event.toolName, event.input);
|
|
817
|
+
if (shellCommand !== void 0 && shellCommand.length > 0) {
|
|
818
|
+
const bashInput = {
|
|
819
|
+
...getSessionIds(ctx),
|
|
820
|
+
tool_name: "Bash",
|
|
821
|
+
tool_input: { command: shellCommand },
|
|
822
|
+
tool_call_id: event.toolCallId
|
|
823
|
+
};
|
|
824
|
+
const bashResult = await runHooksForEvent(settings.hooks.PreToolUse, "Bash", bashInput, ctx, "PreToolUse");
|
|
825
|
+
if (bashResult.block) {
|
|
826
|
+
tel("tool_call.decision", {
|
|
827
|
+
tool_name: claudeToolName,
|
|
828
|
+
decision: "block",
|
|
829
|
+
reason: bashResult.reason ?? `Bash blocked for ${shellCommand}`
|
|
830
|
+
});
|
|
831
|
+
return bashResult.reason === void 0 ? { block: true } : {
|
|
832
|
+
block: true,
|
|
833
|
+
reason: bashResult.reason
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const result = await runHooksForEvent(settings.hooks.PreToolUse, claudeToolName, input, ctx, "PreToolUse");
|
|
838
|
+
if (result.block) {
|
|
839
|
+
tel("tool_call.decision", {
|
|
840
|
+
tool_name: claudeToolName,
|
|
841
|
+
decision: "block",
|
|
842
|
+
reason: result.reason ?? void 0
|
|
843
|
+
});
|
|
844
|
+
return result.reason === void 0 ? { block: true } : {
|
|
845
|
+
block: true,
|
|
846
|
+
reason: result.reason
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (result.updatedInput && typeof result.updatedInput === "object" && "tool_input" in result.updatedInput && result.updatedInput["tool_input"] && typeof result.updatedInput["tool_input"] === "object") {
|
|
850
|
+
const updated = result.updatedInput["tool_input"];
|
|
851
|
+
for (const [key, value] of Object.entries(updated)) event.input[key] = value;
|
|
852
|
+
}
|
|
853
|
+
tel("tool_call.decision", {
|
|
854
|
+
tool_name: claudeToolName,
|
|
855
|
+
decision: "allow"
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async function runPostToolUseHooks(settings, event, ctx) {
|
|
859
|
+
const claudeToolName = normalizeToolName(event.toolName);
|
|
860
|
+
const input = {
|
|
861
|
+
...getSessionIds(ctx),
|
|
862
|
+
tool_name: claudeToolName,
|
|
863
|
+
tool_input: normalizeToolInput(claudeToolName, event.input),
|
|
864
|
+
tool_call_id: event.toolCallId,
|
|
865
|
+
output: event.content,
|
|
866
|
+
is_error: event.isError ?? false
|
|
867
|
+
};
|
|
868
|
+
return runHooksForEvent(settings.hooks.PostToolUse, claudeToolName, input, ctx, "PostToolUse");
|
|
869
|
+
}
|
|
870
|
+
async function runUserPromptSubmitHooks(settings, event, ctx) {
|
|
871
|
+
const entries = settings.hooks.UserPromptSubmit;
|
|
872
|
+
if (entries.length === 0) return void 0;
|
|
873
|
+
const cwd = ctx.cwd;
|
|
874
|
+
let injected = "";
|
|
875
|
+
const input = {
|
|
876
|
+
...getSessionIds(ctx),
|
|
877
|
+
prompt: event.text,
|
|
878
|
+
source: event.source
|
|
879
|
+
};
|
|
880
|
+
for (const entry of entries) for (const hook of entry.hooks) {
|
|
881
|
+
const result = await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3);
|
|
882
|
+
if (result.code !== 0) continue;
|
|
883
|
+
const stdout = result.stdout.trim();
|
|
884
|
+
if (stdout.length > 0) injected += (injected.length > 0 ? "\n\n" : "") + stdout;
|
|
885
|
+
}
|
|
886
|
+
if (injected.length === 0) return void 0;
|
|
887
|
+
const result = { text: `${injected}\n\n${event.text}` };
|
|
888
|
+
if (event.images !== void 0) result.images = event.images;
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
async function runSessionStartHooks(settings, reason, ctx) {
|
|
892
|
+
const entries = settings.hooks.SessionStart;
|
|
893
|
+
if (entries.length === 0) return;
|
|
894
|
+
const cwd = ctx.cwd;
|
|
895
|
+
const input = {
|
|
896
|
+
...getSessionIds(ctx),
|
|
897
|
+
reason
|
|
898
|
+
};
|
|
899
|
+
for (const entry of entries) {
|
|
900
|
+
if (entry.matcher && !matchesMatcher(reason, entry.matcher)) continue;
|
|
901
|
+
for (const hook of entry.hooks) {
|
|
902
|
+
const hookName = hook.command.split(/[\\/]/).pop() ?? hook.command;
|
|
903
|
+
if (hook.async) {
|
|
904
|
+
runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
|
|
905
|
+
tel("hook.executed", {
|
|
906
|
+
hook: hookName,
|
|
907
|
+
exit_code: result.code
|
|
908
|
+
});
|
|
909
|
+
}, (err) => {
|
|
910
|
+
tel("hook.executed", {
|
|
911
|
+
hook: hookName,
|
|
912
|
+
exit_code: null,
|
|
913
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
|
|
919
|
+
tel("hook.executed", {
|
|
920
|
+
hook: hookName,
|
|
921
|
+
exit_code: result.code
|
|
922
|
+
});
|
|
923
|
+
}, (err) => {
|
|
924
|
+
tel("hook.executed", {
|
|
925
|
+
hook: hookName,
|
|
926
|
+
exit_code: null,
|
|
927
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function runLifecycleHooks(entries, ctx) {
|
|
934
|
+
if (entries.length === 0) return;
|
|
935
|
+
const cwd = ctx.cwd;
|
|
936
|
+
const input = { ...getSessionIds(ctx) };
|
|
937
|
+
for (const entry of entries) for (const hook of entry.hooks) {
|
|
938
|
+
const hookName = hook.command.split(/[\\/]/).pop() ?? hook.command;
|
|
939
|
+
if (hook.async) runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
|
|
940
|
+
tel("hook.executed", {
|
|
941
|
+
hook: hookName,
|
|
942
|
+
exit_code: result.code
|
|
943
|
+
});
|
|
944
|
+
}, (err) => {
|
|
945
|
+
tel("hook.executed", {
|
|
946
|
+
hook: hookName,
|
|
947
|
+
exit_code: null,
|
|
948
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
949
|
+
});
|
|
950
|
+
});
|
|
951
|
+
else await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
|
|
952
|
+
tel("hook.executed", {
|
|
953
|
+
hook: hookName,
|
|
954
|
+
exit_code: result.code
|
|
955
|
+
});
|
|
956
|
+
}, (err) => {
|
|
957
|
+
tel("hook.executed", {
|
|
958
|
+
hook: hookName,
|
|
959
|
+
exit_code: null,
|
|
960
|
+
error: err instanceof Error ? err.message : "unknown error"
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/inject-instructions.ts
|
|
967
|
+
/**
|
|
968
|
+
* Find all @-references in a CLAUDE.md file.
|
|
969
|
+
*
|
|
970
|
+
* Supports:
|
|
971
|
+
* - Plain @-ref: `@path/to/file.md`
|
|
972
|
+
* - Bullet-list @-ref: `- @path/to/file.md`
|
|
973
|
+
*
|
|
974
|
+
* Only extracts refs where the @-token is the first thing on the line after
|
|
975
|
+
* stripping leading list markers (`- `, `* `, `+ `) — conservative contract
|
|
976
|
+
* that avoids matching inline prose references.
|
|
977
|
+
*/
|
|
978
|
+
function extractRefs(filePath, projectDir) {
|
|
979
|
+
let content;
|
|
980
|
+
try {
|
|
981
|
+
content = readFileSync(filePath, "utf-8");
|
|
982
|
+
} catch {
|
|
983
|
+
return [];
|
|
984
|
+
}
|
|
985
|
+
const baseDir = dirname(filePath);
|
|
986
|
+
const refs = [];
|
|
987
|
+
for (const rawLine of content.split("\n")) {
|
|
988
|
+
const noMarker = rawLine.trim().replace(/^[-*+]\s+/, "");
|
|
989
|
+
if (!noMarker.startsWith("@")) continue;
|
|
990
|
+
const ref = noMarker.slice(1).trim();
|
|
991
|
+
if (!ref || ref.includes(" ")) continue;
|
|
992
|
+
if (isAbsolute(ref)) continue;
|
|
993
|
+
const baseResolved = resolve(baseDir, ref);
|
|
994
|
+
if ((baseResolved.startsWith(projectDir + sep) || baseResolved === projectDir) && existsSync(baseResolved)) {
|
|
995
|
+
refs.push({
|
|
996
|
+
sourcePath: filePath,
|
|
997
|
+
resolvedPath: baseResolved
|
|
998
|
+
});
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const rootResolved = resolve(projectDir, ref);
|
|
1002
|
+
if ((rootResolved.startsWith(projectDir + sep) || rootResolved === projectDir) && existsSync(rootResolved)) refs.push({
|
|
1003
|
+
sourcePath: filePath,
|
|
1004
|
+
resolvedPath: rootResolved
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
return refs;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Read all @-referenced files and format them for injection.
|
|
1011
|
+
*/
|
|
1012
|
+
function loadReferencedContent(projectDir) {
|
|
1013
|
+
const claudeMdPaths = [resolve(projectDir, "CLAUDE.md"), resolve(projectDir, ".claude", "CLAUDE.md")];
|
|
1014
|
+
const allRefs = [];
|
|
1015
|
+
for (const filePath of claudeMdPaths) if (existsSync(filePath)) allRefs.push(...extractRefs(filePath, projectDir));
|
|
1016
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1017
|
+
const uniqueRefs = [];
|
|
1018
|
+
for (const ref of allRefs) if (!seen.has(ref.resolvedPath)) {
|
|
1019
|
+
seen.add(ref.resolvedPath);
|
|
1020
|
+
uniqueRefs.push(ref);
|
|
1021
|
+
}
|
|
1022
|
+
if (uniqueRefs.length === 0) return "";
|
|
1023
|
+
const parts = ["# Injected @-references from CLAUDE.md"];
|
|
1024
|
+
parts.push("The following files were @-imported by CLAUDE.md and contain project rules.");
|
|
1025
|
+
parts.push("");
|
|
1026
|
+
for (const ref of uniqueRefs) {
|
|
1027
|
+
const relativePath = ref.resolvedPath.slice(projectDir.length + 1);
|
|
1028
|
+
parts.push(`## ${relativePath}`);
|
|
1029
|
+
try {
|
|
1030
|
+
const content = readFileSync(ref.resolvedPath, "utf-8");
|
|
1031
|
+
parts.push(content);
|
|
1032
|
+
} catch {
|
|
1033
|
+
parts.push(`[error reading ${relativePath}]`);
|
|
1034
|
+
}
|
|
1035
|
+
parts.push("");
|
|
1036
|
+
}
|
|
1037
|
+
return parts.join("\n");
|
|
1038
|
+
}
|
|
1039
|
+
function injectInstructionsExtension(pi) {
|
|
1040
|
+
const projectDir = process.env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
|
|
1041
|
+
let cached;
|
|
1042
|
+
pi.on("before_agent_start", async (event) => {
|
|
1043
|
+
if (cached === void 0) cached = loadReferencedContent(projectDir);
|
|
1044
|
+
if (!cached) return void 0;
|
|
1045
|
+
return { systemPrompt: [
|
|
1046
|
+
...event.systemPrompt,
|
|
1047
|
+
"",
|
|
1048
|
+
cached
|
|
1049
|
+
] };
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/index.ts
|
|
1054
|
+
function claudeCompatExtension(pi) {
|
|
1055
|
+
hookDispatcherExtension(pi);
|
|
1056
|
+
injectInstructionsExtension(pi);
|
|
1057
|
+
}
|
|
1058
|
+
//#endregion
|
|
1059
|
+
export { claudeCompatExtension as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@systemfsoftware/omp-claude-compat",
|
|
3
|
+
"license": "MIT",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
|
+
"description": "OMP Claude Code compatibility: dispatch .claude/settings.json hooks + inject CLAUDE.md @-references",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@oh-my-pi/pi-coding-agent": "^17.0.5"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@oh-my-pi/pi-coding-agent": "^17.0.5",
|
|
21
|
+
"@types/node": "^24",
|
|
22
|
+
"rimraf": "^6.1.3",
|
|
23
|
+
"tsdown": "^0.22.9",
|
|
24
|
+
"typescript": "^7",
|
|
25
|
+
"vitest": "^4",
|
|
26
|
+
"@systemfsoftware/tsconfig": "^1.1.0",
|
|
27
|
+
"@systemfsoftware/omp-utils": "^0.0.0",
|
|
28
|
+
"@systemfsoftware/oxlint-config": "^0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"omp": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./dist/index.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"inlinedDependencies": {
|
|
36
|
+
"@jsr/std__collections": "1.3.0",
|
|
37
|
+
"@jsr/std__toml": "1.0.11"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"clean": "rimraf dist",
|
|
41
|
+
"build": "rimraf dist && tsdown",
|
|
42
|
+
"verify-dist": "node ../../scripts/check-dist-builtins.mjs --dist dist/index.js --expect node:child_process node:fs node:path",
|
|
43
|
+
"postbuild": "pnpm verify-dist",
|
|
44
|
+
"typecheck": "tsc --noEmit --incremental",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"lint": "oxlint . ${AGENT:+--format=unix --quiet}"
|
|
47
|
+
}
|
|
48
|
+
}
|