@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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +1059 -0
  3. 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
+ }