@systemfsoftware/omp-claude-compat 0.0.0 → 1.1.3
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/dist/index.js +703 -269
- package/package.json +21 -6
package/dist/index.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { Cause, Config, Context, Effect, Either, Exit, Layer, ManagedRuntime, Match, MutableHashMap, Option, ParseResult, Ref, Schema, Stream } from "effect";
|
|
2
|
+
import { CommandExecutor } from "@effect/platform/CommandExecutor";
|
|
3
|
+
import { FileSystem } from "@effect/platform/FileSystem";
|
|
4
|
+
import * as PathModule from "@effect/platform/Path";
|
|
5
|
+
import { Path } from "@effect/platform/Path";
|
|
6
|
+
import { Command } from "@effect/platform";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { NodeCommandExecutor, NodeFileSystem } from "@effect/platform-node";
|
|
4
9
|
//#region ../../../node_modules/.pnpm/@jsr+std__collections@1.3.0/node_modules/@jsr/std__collections/deep_merge.js
|
|
5
10
|
/** Default merging options - cached to avoid object allocation on each call */ const DEFAULT_OPTIONS = {
|
|
6
11
|
arrays: "merge",
|
|
@@ -84,6 +89,70 @@ function getKeys(record) {
|
|
|
84
89
|
*/ function isLeap(yearNumber) {
|
|
85
90
|
return yearNumber % 4 === 0 && yearNumber % 100 !== 0 || yearNumber % 400 === 0;
|
|
86
91
|
}
|
|
92
|
+
var Scanner = class {
|
|
93
|
+
#whitespace = /[ \t]/;
|
|
94
|
+
#position = 0;
|
|
95
|
+
#source;
|
|
96
|
+
constructor(source) {
|
|
97
|
+
this.#source = source;
|
|
98
|
+
}
|
|
99
|
+
get position() {
|
|
100
|
+
return this.#position;
|
|
101
|
+
}
|
|
102
|
+
get source() {
|
|
103
|
+
return this.#source;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get current character
|
|
107
|
+
* @param index - relative index from current position
|
|
108
|
+
*/ char(index = 0) {
|
|
109
|
+
return this.#source[this.#position + index] ?? "";
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get sliced string
|
|
113
|
+
* @param start - start position relative from current position
|
|
114
|
+
* @param end - end position relative from current position
|
|
115
|
+
*/ slice(start, end) {
|
|
116
|
+
return this.#source.slice(this.#position + start, this.#position + end);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Move position to next
|
|
120
|
+
*/ next(count = 1) {
|
|
121
|
+
this.#position += count;
|
|
122
|
+
}
|
|
123
|
+
skipWhitespaces() {
|
|
124
|
+
while (this.#whitespace.test(this.char()) && !this.eof()) this.next();
|
|
125
|
+
if (!this.isCurrentCharEOL() && /\s/.test(this.char())) {
|
|
126
|
+
const escaped = "\\u" + this.char().charCodeAt(0).toString(16);
|
|
127
|
+
const position = this.#position;
|
|
128
|
+
throw new SyntaxError(`Cannot parse the TOML: It contains invalid whitespace at position '${position}': \`${escaped}\``);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
nextUntilChar(options = { skipComments: true }) {
|
|
132
|
+
while (!this.eof()) {
|
|
133
|
+
const char = this.char();
|
|
134
|
+
if (this.#whitespace.test(char) || this.isCurrentCharEOL()) this.next();
|
|
135
|
+
else if (options.skipComments && this.char() === "#") while (!this.isCurrentCharEOL() && !this.eof()) this.next();
|
|
136
|
+
else break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Position reached EOF or not
|
|
141
|
+
*/ eof() {
|
|
142
|
+
return this.#position >= this.#source.length;
|
|
143
|
+
}
|
|
144
|
+
isCurrentCharEOL() {
|
|
145
|
+
return this.char() === "\n" || this.startsWith("\r\n");
|
|
146
|
+
}
|
|
147
|
+
startsWith(searchString) {
|
|
148
|
+
return this.#source.startsWith(searchString, this.#position);
|
|
149
|
+
}
|
|
150
|
+
match(regExp) {
|
|
151
|
+
if (!regExp.sticky) throw new Error(`RegExp ${regExp} does not have a sticky 'y' flag`);
|
|
152
|
+
regExp.lastIndex = this.#position;
|
|
153
|
+
return this.#source.match(regExp);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
87
156
|
function success(body) {
|
|
88
157
|
return {
|
|
89
158
|
ok: true,
|
|
@@ -100,6 +169,66 @@ function failure() {
|
|
|
100
169
|
*/ function unflat(keys, values = { __proto__: null }) {
|
|
101
170
|
return keys.reduceRight((acc, key) => ({ [key]: acc }), values);
|
|
102
171
|
}
|
|
172
|
+
function isObject(value) {
|
|
173
|
+
return typeof value === "object" && value !== null;
|
|
174
|
+
}
|
|
175
|
+
function getTargetValue(target, keys) {
|
|
176
|
+
const key = keys[0];
|
|
177
|
+
if (!key) throw new Error("Cannot parse the TOML: key length is not a positive number");
|
|
178
|
+
return target[key];
|
|
179
|
+
}
|
|
180
|
+
function deepAssignTable(target, table) {
|
|
181
|
+
const { keys, type, value } = table;
|
|
182
|
+
const currentValue = getTargetValue(target, keys);
|
|
183
|
+
if (currentValue === void 0) return Object.assign(target, unflat(keys, value));
|
|
184
|
+
if (Array.isArray(currentValue)) {
|
|
185
|
+
deepAssign(currentValue.at(-1), {
|
|
186
|
+
type,
|
|
187
|
+
keys: keys.slice(1),
|
|
188
|
+
value
|
|
189
|
+
});
|
|
190
|
+
return target;
|
|
191
|
+
}
|
|
192
|
+
if (isObject(currentValue)) {
|
|
193
|
+
deepAssign(currentValue, {
|
|
194
|
+
type,
|
|
195
|
+
keys: keys.slice(1),
|
|
196
|
+
value
|
|
197
|
+
});
|
|
198
|
+
return target;
|
|
199
|
+
}
|
|
200
|
+
throw new Error("Unexpected assign");
|
|
201
|
+
}
|
|
202
|
+
function deepAssignTableArray(target, table) {
|
|
203
|
+
const { type, keys, value } = table;
|
|
204
|
+
const currentValue = getTargetValue(target, keys);
|
|
205
|
+
if (currentValue === void 0) return Object.assign(target, unflat(keys, [value]));
|
|
206
|
+
if (Array.isArray(currentValue)) {
|
|
207
|
+
if (table.keys.length === 1) currentValue.push(value);
|
|
208
|
+
else deepAssign(currentValue.at(-1), {
|
|
209
|
+
type: table.type,
|
|
210
|
+
keys: table.keys.slice(1),
|
|
211
|
+
value: table.value
|
|
212
|
+
});
|
|
213
|
+
return target;
|
|
214
|
+
}
|
|
215
|
+
if (isObject(currentValue)) {
|
|
216
|
+
deepAssign(currentValue, {
|
|
217
|
+
type,
|
|
218
|
+
keys: keys.slice(1),
|
|
219
|
+
value
|
|
220
|
+
});
|
|
221
|
+
return target;
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Unexpected assign");
|
|
224
|
+
}
|
|
225
|
+
function deepAssign(target, body) {
|
|
226
|
+
switch (body.type) {
|
|
227
|
+
case "Block": return deepMerge(target, body.value);
|
|
228
|
+
case "Table": return deepAssignTable(target, body);
|
|
229
|
+
case "TableArray": return deepAssignTableArray(target, body);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
103
232
|
function or(parsers) {
|
|
104
233
|
return (scanner) => {
|
|
105
234
|
for (const parse of parsers) {
|
|
@@ -163,6 +292,28 @@ function kv(keyParser, separator, valueParser) {
|
|
|
163
292
|
return success(unflat(key.body, value.body));
|
|
164
293
|
};
|
|
165
294
|
}
|
|
295
|
+
function merge(parser) {
|
|
296
|
+
return (scanner) => {
|
|
297
|
+
const result = parser(scanner);
|
|
298
|
+
if (!result.ok) return failure();
|
|
299
|
+
let body = { __proto__: null };
|
|
300
|
+
for (const record of result.body) if (typeof record === "object" && record !== null) body = deepMerge(body, record);
|
|
301
|
+
return success(body);
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function repeat(parser) {
|
|
305
|
+
return (scanner) => {
|
|
306
|
+
const body = [];
|
|
307
|
+
while (!scanner.eof()) {
|
|
308
|
+
const result = parser(scanner);
|
|
309
|
+
if (!result.ok) break;
|
|
310
|
+
body.push(result.body);
|
|
311
|
+
scanner.nextUntilChar();
|
|
312
|
+
}
|
|
313
|
+
if (body.length === 0) return failure();
|
|
314
|
+
return success(body);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
166
317
|
function surround(left, parser, right) {
|
|
167
318
|
const Left = character(left);
|
|
168
319
|
const Right = character(right);
|
|
@@ -473,10 +624,93 @@ const value = or([
|
|
|
473
624
|
inlineTable
|
|
474
625
|
]);
|
|
475
626
|
const pair = kv(dottedKey, "=", value);
|
|
476
|
-
|
|
477
|
-
|
|
627
|
+
function block(scanner) {
|
|
628
|
+
scanner.nextUntilChar();
|
|
629
|
+
const result = merge(repeat(pair))(scanner);
|
|
630
|
+
if (result.ok) return success({
|
|
631
|
+
type: "Block",
|
|
632
|
+
value: result.body
|
|
633
|
+
});
|
|
634
|
+
return failure();
|
|
635
|
+
}
|
|
636
|
+
const tableHeader = surround("[", dottedKey, "]");
|
|
637
|
+
function table(scanner) {
|
|
638
|
+
scanner.nextUntilChar();
|
|
639
|
+
const header = tableHeader(scanner);
|
|
640
|
+
if (!header.ok) return failure();
|
|
641
|
+
scanner.nextUntilChar();
|
|
642
|
+
const b = block(scanner);
|
|
643
|
+
return success({
|
|
644
|
+
type: "Table",
|
|
645
|
+
keys: header.body,
|
|
646
|
+
value: b.ok ? b.body.value : { __proto__: null }
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
const tableArrayHeader = surround("[[", dottedKey, "]]");
|
|
650
|
+
function tableArray(scanner) {
|
|
651
|
+
scanner.nextUntilChar();
|
|
652
|
+
const header = tableArrayHeader(scanner);
|
|
653
|
+
if (!header.ok) return failure();
|
|
654
|
+
scanner.nextUntilChar();
|
|
655
|
+
const b = block(scanner);
|
|
656
|
+
return success({
|
|
657
|
+
type: "TableArray",
|
|
658
|
+
keys: header.body,
|
|
659
|
+
value: b.ok ? b.body.value : { __proto__: null }
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
function toml(scanner) {
|
|
663
|
+
const blocks = repeat(or([
|
|
664
|
+
block,
|
|
665
|
+
tableArray,
|
|
666
|
+
table
|
|
667
|
+
]))(scanner);
|
|
668
|
+
if (!blocks.ok) return success({ __proto__: null });
|
|
669
|
+
return success(blocks.body.reduce(deepAssign, { __proto__: null }));
|
|
670
|
+
}
|
|
671
|
+
function createParseErrorMessage(scanner, message) {
|
|
672
|
+
const lines = scanner.source.slice(0, scanner.position).split("\n");
|
|
673
|
+
return `Parse error on line ${lines.length}, column ${lines.at(-1)?.length ?? 0}: ${message}`;
|
|
674
|
+
}
|
|
675
|
+
function parserFactory(parser) {
|
|
676
|
+
return (tomlString) => {
|
|
677
|
+
const scanner = new Scanner(tomlString);
|
|
678
|
+
try {
|
|
679
|
+
const result = parser(scanner);
|
|
680
|
+
if (result.ok && scanner.eof()) return result.body;
|
|
681
|
+
const message = `Unexpected character: "${scanner.char()}"`;
|
|
682
|
+
throw new SyntaxError(createParseErrorMessage(scanner, message));
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (error instanceof Error) throw new SyntaxError(createParseErrorMessage(scanner, error.message));
|
|
685
|
+
throw new SyntaxError(createParseErrorMessage(scanner, "Invalid error type caught"));
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
478
689
|
//#endregion
|
|
479
|
-
//#region
|
|
690
|
+
//#region ../../../node_modules/.pnpm/@jsr+std__toml@1.0.11/node_modules/@jsr/std__toml/parse.js
|
|
691
|
+
/**
|
|
692
|
+
* Parses a {@link https://toml.io | TOML} string into an object.
|
|
693
|
+
*
|
|
694
|
+
* @example Usage
|
|
695
|
+
* ```ts
|
|
696
|
+
* import { parse } from "@std/toml/parse";
|
|
697
|
+
* import { assertEquals } from "@std/assert";
|
|
698
|
+
*
|
|
699
|
+
* const tomlString = `title = "TOML Example"
|
|
700
|
+
* [owner]
|
|
701
|
+
* name = "Alice"
|
|
702
|
+
* bio = "Alice is a programmer."`;
|
|
703
|
+
*
|
|
704
|
+
* const obj = parse(tomlString);
|
|
705
|
+
* assertEquals(obj, { title: "TOML Example", owner: { name: "Alice", bio: "Alice is a programmer." } });
|
|
706
|
+
* ```
|
|
707
|
+
* @param tomlString TOML string to be parsed.
|
|
708
|
+
* @returns The parsed JS object.
|
|
709
|
+
*/ function parse(tomlString) {
|
|
710
|
+
return parserFactory(toml)(tomlString);
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region ../../packages/omp-utils/dist/index.js
|
|
480
714
|
/**
|
|
481
715
|
* ACL: detect and extract shell commands from context-mode tool invocations.
|
|
482
716
|
*
|
|
@@ -538,6 +772,63 @@ function createTelemetry(plugin, logger) {
|
|
|
538
772
|
};
|
|
539
773
|
}
|
|
540
774
|
/**
|
|
775
|
+
* Schema: TOML config — the inner vocabulary of `systemfsoftware.toml`.
|
|
776
|
+
*
|
|
777
|
+
* Shape: a record of string key to string array. The only consumer of the inner
|
|
778
|
+
* keys is the agent-discipline plugin (it reads `no_delegate_skills`); other
|
|
779
|
+
* plugins may add their own keys without this schema changing. Branding makes
|
|
780
|
+
* a parsed config distinguishable from a raw record and surfaces shape errors
|
|
781
|
+
* through the loader's parse path.
|
|
782
|
+
*
|
|
783
|
+
* Pure declaration. No behavior, no `@std/toml`, no I/O. The ACL owns the
|
|
784
|
+
* boundary crossing; the executor owns the I/O.
|
|
785
|
+
*/
|
|
786
|
+
const TomlConfig = Schema.Record({
|
|
787
|
+
key: Schema.String,
|
|
788
|
+
value: Schema.Array(Schema.String)
|
|
789
|
+
}).pipe(Schema.brand("TomlConfig"));
|
|
790
|
+
/**
|
|
791
|
+
* ACL: TOML text → `TomlConfig`.
|
|
792
|
+
*
|
|
793
|
+
* The only file in this package that imports `@std/toml`. Anything that
|
|
794
|
+
* decodes from outside the domain types routes through here so the foreign
|
|
795
|
+
* parser is contained at one boundary.
|
|
796
|
+
*
|
|
797
|
+
* `Schema.transformOrFail` with `strict: true` makes the decode go through
|
|
798
|
+
* Schema's identity contract — branding is earned by `ParseResult.decode`,
|
|
799
|
+
* never by a cast. Encode is `Forbidden` because the TOML format is the
|
|
800
|
+
* source, not the destination. Constitutes ACL1 (see `omp/AGENTS.md`).
|
|
801
|
+
*/
|
|
802
|
+
const TomlConfigFromText = Schema.transformOrFail(Schema.String, Schema.typeSchema(TomlConfig), {
|
|
803
|
+
strict: true,
|
|
804
|
+
decode: (raw) => ParseResult.try({
|
|
805
|
+
try: () => parse(raw),
|
|
806
|
+
catch: (e) => new ParseResult.Unexpected(e, "TOML parse error")
|
|
807
|
+
}).pipe(ParseResult.flatMap((parsed) => ParseResult.decodeUnknown(TomlConfig)(parsed))),
|
|
808
|
+
encode: (_, _d, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, _, "TomlConfigFromText is decode-only"))
|
|
809
|
+
});
|
|
810
|
+
const CONFIG_FILE = "systemfsoftware.toml";
|
|
811
|
+
const EMPTY_CONFIG = Schema.decodeSync(TomlConfig)({});
|
|
812
|
+
var TomlLoader = class extends Context.Tag("TomlLoader")() {};
|
|
813
|
+
Layer.effect(TomlLoader, Effect.gen(function* () {
|
|
814
|
+
const cache = yield* Ref.make(MutableHashMap.empty());
|
|
815
|
+
const fs = yield* FileSystem;
|
|
816
|
+
const path = yield* Path;
|
|
817
|
+
return TomlLoader.of({ load: Effect.fn("TomlLoader.load")(function* (cwd) {
|
|
818
|
+
const cached = yield* Ref.get(cache);
|
|
819
|
+
const existing = MutableHashMap.get(cached, cwd);
|
|
820
|
+
if (Option.isSome(existing)) return existing.value;
|
|
821
|
+
const configPath = path.join(cwd, CONFIG_FILE);
|
|
822
|
+
if (!(yield* fs.exists(configPath))) {
|
|
823
|
+
yield* Ref.update(cache, (m) => (MutableHashMap.set(m, cwd, EMPTY_CONFIG), m));
|
|
824
|
+
return EMPTY_CONFIG;
|
|
825
|
+
}
|
|
826
|
+
const result = yield* fs.readFileString(configPath).pipe(Effect.flatMap(Schema.decodeUnknown(TomlConfigFromText)), Effect.tapError((error) => Effect.logWarning(`[toml-loader] malformed ${CONFIG_FILE} at ${configPath} — failing open (no config)`, error)), Effect.catchAll(() => Effect.succeed(EMPTY_CONFIG)));
|
|
827
|
+
yield* Ref.update(cache, (m) => (MutableHashMap.set(m, cwd, result), m));
|
|
828
|
+
return result;
|
|
829
|
+
}) });
|
|
830
|
+
}));
|
|
831
|
+
/**
|
|
541
832
|
* ACL: translate OMP tool input shapes to Claude Code hook input shapes.
|
|
542
833
|
*
|
|
543
834
|
* OMP sends `edits: [{ old_text, new_text }]` and `path`;
|
|
@@ -592,223 +883,288 @@ function normalizeToolName(name) {
|
|
|
592
883
|
if (name.length === 0) return name;
|
|
593
884
|
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
594
885
|
}
|
|
886
|
+
Schema.Struct({
|
|
887
|
+
code: Schema.Number,
|
|
888
|
+
stdout: Schema.String,
|
|
889
|
+
stderr: Schema.String
|
|
890
|
+
});
|
|
891
|
+
var Block = class extends Schema.TaggedClass()("Block", { reason: Schema.String }) {};
|
|
892
|
+
var Allow = class extends Schema.TaggedClass()("Allow", {}) {};
|
|
893
|
+
var Warning = class extends Schema.TaggedClass()("Warning", { message: Schema.String }) {};
|
|
894
|
+
Schema.Union(Block, Allow, Warning);
|
|
895
|
+
var Blocked = class extends Schema.TaggedClass()("Blocked", { reason: Schema.String }) {};
|
|
896
|
+
var Continue = class extends Schema.TaggedClass()("Continue", {
|
|
897
|
+
warning: Schema.optional(Schema.String),
|
|
898
|
+
updatedInput: Schema.optional(Schema.Record({
|
|
899
|
+
key: Schema.String,
|
|
900
|
+
value: Schema.Unknown
|
|
901
|
+
}))
|
|
902
|
+
}) {};
|
|
903
|
+
Schema.Union(Blocked, Continue);
|
|
595
904
|
//#endregion
|
|
596
|
-
//#region src/hook-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
905
|
+
//#region src/hook-output.acl.ts
|
|
906
|
+
const ParsedHookOutputSchema = Schema.Struct({
|
|
907
|
+
decision: Schema.optional(Schema.String),
|
|
908
|
+
reason: Schema.optional(Schema.String),
|
|
909
|
+
hookSpecificOutput: Schema.optional(Schema.Struct({
|
|
910
|
+
permissionDecision: Schema.optional(Schema.String),
|
|
911
|
+
permissionDecisionReason: Schema.optional(Schema.String),
|
|
912
|
+
updatedInput: Schema.optional(Schema.Record({
|
|
913
|
+
key: Schema.String,
|
|
914
|
+
value: Schema.Unknown
|
|
915
|
+
}))
|
|
916
|
+
}))
|
|
917
|
+
});
|
|
918
|
+
const parseHookOutput = Schema.decodeUnknownEither(Schema.parseJson(ParsedHookOutputSchema));
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/hook-settings.acl.ts
|
|
921
|
+
const HookCommand = Schema.Struct({
|
|
922
|
+
type: Schema.Literal("command"),
|
|
923
|
+
command: Schema.String,
|
|
924
|
+
async: Schema.optional(Schema.Boolean),
|
|
925
|
+
timeout: Schema.optional(Schema.Number)
|
|
926
|
+
});
|
|
927
|
+
const HookEntry = Schema.Struct({
|
|
928
|
+
matcher: Schema.optional(Schema.String),
|
|
929
|
+
hooks: Schema.Array(HookCommand)
|
|
930
|
+
});
|
|
931
|
+
const HookGroups = Schema.Struct({
|
|
932
|
+
PreToolUse: Schema.optionalWith(Schema.Array(HookEntry), {
|
|
933
|
+
exact: true,
|
|
934
|
+
default: () => []
|
|
935
|
+
}),
|
|
936
|
+
PostToolUse: Schema.optionalWith(Schema.Array(HookEntry), {
|
|
937
|
+
exact: true,
|
|
938
|
+
default: () => []
|
|
939
|
+
}),
|
|
940
|
+
UserPromptSubmit: Schema.optionalWith(Schema.Array(HookEntry), {
|
|
941
|
+
exact: true,
|
|
942
|
+
default: () => []
|
|
943
|
+
}),
|
|
944
|
+
Stop: Schema.optionalWith(Schema.Array(HookEntry), {
|
|
945
|
+
exact: true,
|
|
946
|
+
default: () => []
|
|
947
|
+
}),
|
|
948
|
+
SessionStart: Schema.optionalWith(Schema.Array(HookEntry), {
|
|
949
|
+
exact: true,
|
|
950
|
+
default: () => []
|
|
951
|
+
}),
|
|
952
|
+
SessionEnd: Schema.optionalWith(Schema.Array(HookEntry), {
|
|
953
|
+
exact: true,
|
|
954
|
+
default: () => []
|
|
955
|
+
})
|
|
956
|
+
});
|
|
957
|
+
const SettingsWrapped = Schema.Struct({
|
|
958
|
+
hooks: HookGroups,
|
|
959
|
+
disableAllHooks: Schema.optional(Schema.Boolean)
|
|
960
|
+
});
|
|
961
|
+
const SettingsFlat = Schema.Struct({
|
|
962
|
+
...HookGroups.fields,
|
|
963
|
+
disableAllHooks: Schema.optional(Schema.Boolean)
|
|
964
|
+
});
|
|
965
|
+
const SettingsJSON = Schema.Union(SettingsWrapped, SettingsFlat);
|
|
966
|
+
const decodeSettings = Schema.decodeUnknownEither(SettingsJSON);
|
|
967
|
+
function parseSettings(json) {
|
|
968
|
+
return Either.map(decodeSettings(json), (s) => {
|
|
969
|
+
if ("hooks" in s) return s;
|
|
970
|
+
const { disableAllHooks: d, ...hookGroups } = s;
|
|
971
|
+
return {
|
|
972
|
+
hooks: hookGroups,
|
|
973
|
+
disableAllHooks: d
|
|
626
974
|
};
|
|
627
975
|
});
|
|
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
976
|
}
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
} catch {
|
|
680
|
-
return null;
|
|
977
|
+
const ALL_HOOK_EVENTS = [
|
|
978
|
+
"PreToolUse",
|
|
979
|
+
"PostToolUse",
|
|
980
|
+
"UserPromptSubmit",
|
|
981
|
+
"SessionStart",
|
|
982
|
+
"SessionEnd",
|
|
983
|
+
"Stop"
|
|
984
|
+
];
|
|
985
|
+
function mergeSettings(settings) {
|
|
986
|
+
const merged = { hooks: {
|
|
987
|
+
PreToolUse: [],
|
|
988
|
+
PostToolUse: [],
|
|
989
|
+
UserPromptSubmit: [],
|
|
990
|
+
SessionStart: [],
|
|
991
|
+
SessionEnd: [],
|
|
992
|
+
Stop: []
|
|
993
|
+
} };
|
|
994
|
+
for (const s of settings) {
|
|
995
|
+
for (const event of ALL_HOOK_EVENTS) merged.hooks[event] = (merged.hooks[event] ?? []).concat(Array.from(s.hooks[event]));
|
|
996
|
+
if (s.disableAllHooks !== void 0) merged.disableAllHooks = s.disableAllHooks;
|
|
681
997
|
}
|
|
998
|
+
return merged;
|
|
999
|
+
}
|
|
1000
|
+
function isHooksDisabled(settings) {
|
|
1001
|
+
return settings.disableAllHooks === true;
|
|
682
1002
|
}
|
|
683
|
-
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/hook-verdict.workflow.ts
|
|
1005
|
+
var HookVerdictError = class extends Schema.TaggedError()("HookVerdictError", { raw: Schema.String }) {};
|
|
1006
|
+
var ExitBlock = class extends Schema.TaggedClass()("ExitBlock", {}) {};
|
|
1007
|
+
var ExitParse = class extends Schema.TaggedClass()("ExitParse", {}) {};
|
|
1008
|
+
var ExitOther = class extends Schema.TaggedClass()("ExitOther", {}) {};
|
|
1009
|
+
Schema.Union(ExitBlock, ExitParse, ExitOther);
|
|
1010
|
+
const classifyExit = (code) => Match.value(code).pipe(Match.when(2, () => new ExitBlock({})), Match.when(0, () => new ExitParse({})), Match.orElse(() => new ExitOther({})));
|
|
1011
|
+
const blockReason = (stderr, event) => Match.value(stderr.trim()).pipe(Match.when("", () => `Blocked by ${event} hook`), Match.orElse((trimmed) => trimmed));
|
|
1012
|
+
const decideFromParsed = (parsed, event) => Match.value(parsed.hookSpecificOutput?.permissionDecision ?? parsed.decision).pipe(Match.when("deny", () => new Block({ reason: parsed.hookSpecificOutput?.permissionDecisionReason ?? `Blocked by ${event} hook` })), Match.when("block", () => new Block({ reason: parsed.reason ?? `Blocked by ${event} hook` })), Match.orElse(() => new Allow({})));
|
|
1013
|
+
const decideFromNonStandardExit = (stderr) => Match.value(stderr.trim()).pipe(Match.when("", () => new Allow({})), Match.orElse((message) => new Warning({ message })));
|
|
1014
|
+
const interpretHookResult = (result, event) => Match.value(classifyExit(result.code)).pipe(Match.tag("ExitBlock", () => Either.right(new Block({ reason: blockReason(result.stderr, event) }))), Match.tag("ExitParse", () => Either.match(parseHookOutput(result.stdout), {
|
|
1015
|
+
onLeft: () => Either.left(new HookVerdictError({ raw: result.stdout })),
|
|
1016
|
+
onRight: (parsed) => Either.right(decideFromParsed(parsed, event))
|
|
1017
|
+
})), Match.tag("ExitOther", () => Either.right(decideFromNonStandardExit(result.stderr))), Match.exhaustive);
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/hook-dispatcher.executor.ts
|
|
1020
|
+
var HookDispatcherExecutorDeps = class extends Context.Tag("HookDispatcherExecutorDeps")() {};
|
|
1021
|
+
function resolveCommandPath(command, cwd) {
|
|
684
1022
|
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
1023
|
return {
|
|
692
1024
|
cmd: "sh",
|
|
693
|
-
args: ["-c",
|
|
1025
|
+
args: ["-c", trimmed.startsWith("\"") && trimmed.endsWith("\"") ? trimmed.slice(1, -1) : trimmed]
|
|
694
1026
|
};
|
|
695
1027
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
|
|
1028
|
+
function hookNameFromCommand(command) {
|
|
1029
|
+
return command.split(/[\\/]/).pop() ?? command;
|
|
1030
|
+
}
|
|
1031
|
+
const loadSettingsFile = Effect.fn("loadSettingsFile")(function* (path) {
|
|
1032
|
+
const content = yield* (yield* FileSystem).readFileString(path).pipe(Effect.catchAll(() => Effect.succeed("")));
|
|
1033
|
+
if (content === "") return null;
|
|
1034
|
+
const jsonOrError = Schema.decodeUnknownEither(Schema.parseJson(Schema.Record({
|
|
1035
|
+
key: Schema.String,
|
|
1036
|
+
value: Schema.Unknown
|
|
1037
|
+
})))(content);
|
|
1038
|
+
if (Either.isLeft(jsonOrError)) return null;
|
|
1039
|
+
const json = jsonOrError.right;
|
|
1040
|
+
const either = parseSettings(json);
|
|
1041
|
+
return Either.isLeft(either) ? null : either.right;
|
|
1042
|
+
});
|
|
1043
|
+
const loadSettings = Effect.fn("loadSettings")(function* (cwd) {
|
|
1044
|
+
const paths = [
|
|
1045
|
+
`${homedir()}/.claude/settings.json`,
|
|
1046
|
+
`${cwd}/.claude/settings.json`,
|
|
1047
|
+
`${cwd}/.claude/settings.local.json`,
|
|
1048
|
+
"/etc/claude-code/managed-settings.json"
|
|
1049
|
+
];
|
|
1050
|
+
return yield* loadSettingsWithPaths(paths);
|
|
1051
|
+
});
|
|
1052
|
+
const loadSettingsWithPaths = Effect.fn("loadSettingsWithPaths")(function* (paths) {
|
|
1053
|
+
const results = [];
|
|
1054
|
+
for (const p of paths) {
|
|
1055
|
+
const s = yield* loadSettingsFile(p);
|
|
1056
|
+
if (s !== null) results.push(s);
|
|
1057
|
+
}
|
|
1058
|
+
if (results.length === 0) return null;
|
|
1059
|
+
return mergeSettings(results);
|
|
1060
|
+
});
|
|
1061
|
+
const runHookScript = Effect.fn("runHookScript")(function* (command, input, cwd, timeoutMs) {
|
|
1062
|
+
const executor = yield* CommandExecutor;
|
|
1063
|
+
const { cmd, args } = resolveCommandPath(command, cwd);
|
|
1064
|
+
const stdinText = JSON.stringify(input);
|
|
1065
|
+
const hookCommand = Command.make(cmd, ...args).pipe(Command.workingDirectory(cwd), Command.env({
|
|
1066
|
+
OMP_PROJECT_DIR: cwd,
|
|
1067
|
+
CLAUDE_PROJECT_DIR: cwd
|
|
1068
|
+
}), Command.feed(stdinText), Command.stdout("pipe"), Command.stderr("pipe"));
|
|
1069
|
+
return yield* Effect.scoped(Effect.gen(function* () {
|
|
1070
|
+
const process = yield* executor.start(hookCommand);
|
|
1071
|
+
const stdout = yield* process.stdout.pipe(Stream.decodeText(), Stream.mkString);
|
|
1072
|
+
const stderr = yield* process.stderr.pipe(Stream.decodeText(), Stream.mkString);
|
|
1073
|
+
const exitCode = yield* process.exitCode;
|
|
1074
|
+
return {
|
|
1075
|
+
code: typeof exitCode === "number" ? exitCode : Number(exitCode),
|
|
717
1076
|
stdout,
|
|
718
1077
|
stderr
|
|
719
|
-
}
|
|
720
|
-
})
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
1078
|
+
};
|
|
1079
|
+
})).pipe(Effect.timeout(timeoutMs), Effect.catchTag("TimeoutException", () => Effect.succeed({
|
|
1080
|
+
code: -1,
|
|
1081
|
+
stdout: "",
|
|
1082
|
+
stderr: `timeout after ${timeoutMs}ms`
|
|
1083
|
+
})));
|
|
1084
|
+
});
|
|
1085
|
+
const runHooksForEvent = Effect.fn("runHooksForEvent")(function* (entries, matchValue, input, ctx, event) {
|
|
726
1086
|
const cwd = ctx.cwd;
|
|
1087
|
+
const { tel } = yield* HookDispatcherExecutorDeps;
|
|
727
1088
|
let warning;
|
|
728
1089
|
let inputModified = false;
|
|
1090
|
+
let currentInput = input;
|
|
729
1091
|
for (const entry of entries) {
|
|
730
1092
|
if (!matchesMatcher(matchValue, entry.matcher)) continue;
|
|
731
1093
|
for (const hook of entry.hooks) {
|
|
732
|
-
const hookName = hook.command
|
|
1094
|
+
const hookName = hookNameFromCommand(hook.command);
|
|
733
1095
|
const hookStart = performance.now();
|
|
1096
|
+
const timeoutMs = (hook.timeout ?? 10) * 1e3;
|
|
734
1097
|
if (hook.async) {
|
|
735
|
-
runHookScript(hook.command,
|
|
1098
|
+
yield* Effect.forkDaemon(runHookScript(hook.command, currentInput, cwd, timeoutMs).pipe(Effect.tap((result) => Effect.sync(() => {
|
|
736
1099
|
const durationMs = Math.round(performance.now() - hookStart);
|
|
737
1100
|
tel("hook.executed", {
|
|
738
1101
|
hook: hookName,
|
|
739
1102
|
duration_ms: durationMs,
|
|
740
1103
|
exit_code: result.code
|
|
741
1104
|
});
|
|
742
|
-
}, (
|
|
1105
|
+
})), Effect.catchAll(() => Effect.sync(() => {
|
|
743
1106
|
const durationMs = Math.round(performance.now() - hookStart);
|
|
744
1107
|
tel("hook.executed", {
|
|
745
1108
|
hook: hookName,
|
|
746
1109
|
duration_ms: durationMs,
|
|
747
|
-
exit_code: null
|
|
748
|
-
error: err instanceof Error ? err.message : "unknown error"
|
|
1110
|
+
exit_code: null
|
|
749
1111
|
});
|
|
750
|
-
});
|
|
1112
|
+
}))));
|
|
751
1113
|
continue;
|
|
752
1114
|
}
|
|
753
|
-
|
|
754
|
-
try {
|
|
755
|
-
result = await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3);
|
|
756
|
-
} catch (err) {
|
|
1115
|
+
const result = yield* runHookScript(hook.command, currentInput, cwd, timeoutMs).pipe(Effect.tap((r) => Effect.sync(() => {
|
|
757
1116
|
const durationMs = Math.round(performance.now() - hookStart);
|
|
758
1117
|
tel("hook.executed", {
|
|
759
1118
|
hook: hookName,
|
|
760
1119
|
duration_ms: durationMs,
|
|
761
|
-
exit_code:
|
|
762
|
-
error: err instanceof Error ? err.message : "unknown error"
|
|
1120
|
+
exit_code: r.code
|
|
763
1121
|
});
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1122
|
+
})), Effect.tapError(() => Effect.sync(() => {
|
|
1123
|
+
const durationMs = Math.round(performance.now() - hookStart);
|
|
1124
|
+
tel("hook.executed", {
|
|
1125
|
+
hook: hookName,
|
|
1126
|
+
duration_ms: durationMs,
|
|
1127
|
+
exit_code: null
|
|
1128
|
+
});
|
|
1129
|
+
})));
|
|
1130
|
+
const verdict = interpretHookResult(result, event);
|
|
1131
|
+
const decision = Either.match(verdict, {
|
|
1132
|
+
onLeft: (err) => Match.value(err).pipe(Match.tag("HookVerdictError", (e) => new Warning({ message: `Hook exited 0 but produced invalid JSON: ${e.raw.slice(0, 200)}` })), Match.exhaustive),
|
|
1133
|
+
onRight: (d) => d
|
|
771
1134
|
});
|
|
772
|
-
|
|
1135
|
+
const outcome = Match.value(decision).pipe(Match.tag("Block", (d) => new Blocked({ reason: d.reason })), Match.tag("Warning", (d) => new Continue({ warning: d.message })), Match.tag("Allow", () => {
|
|
1136
|
+
const either = parseHookOutput(result.stdout);
|
|
1137
|
+
return new Continue({ updatedInput: Either.isRight(either) ? either.right.hookSpecificOutput?.updatedInput : void 0 });
|
|
1138
|
+
}), Match.exhaustive);
|
|
1139
|
+
const hookExit = Match.value(outcome).pipe(Match.tag("Blocked", (b) => Option.some({
|
|
773
1140
|
block: true,
|
|
774
|
-
reason:
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
|
1141
|
+
reason: b.reason
|
|
1142
|
+
})), Match.tag("Continue", (c) => {
|
|
1143
|
+
if (c.warning !== void 0 && warning === void 0) warning = c.warning;
|
|
1144
|
+
if (c.updatedInput !== void 0) {
|
|
1145
|
+
currentInput = {
|
|
1146
|
+
...currentInput,
|
|
1147
|
+
...c.updatedInput
|
|
797
1148
|
};
|
|
798
1149
|
inputModified = true;
|
|
799
1150
|
}
|
|
800
|
-
|
|
1151
|
+
return Option.none();
|
|
1152
|
+
}), Match.exhaustive);
|
|
1153
|
+
if (Option.isSome(hookExit)) return hookExit.value;
|
|
801
1154
|
}
|
|
802
1155
|
}
|
|
803
1156
|
return {
|
|
804
|
-
...inputModified ? { updatedInput:
|
|
1157
|
+
...inputModified ? { updatedInput: currentInput } : {},
|
|
805
1158
|
...warning !== void 0 ? { warning } : {}
|
|
806
1159
|
};
|
|
807
|
-
}
|
|
808
|
-
|
|
1160
|
+
});
|
|
1161
|
+
const runPreToolUseHooks = Effect.fn("runPreToolUseHooks")(function* (settings, event, ctx) {
|
|
1162
|
+
const { tel } = yield* HookDispatcherExecutorDeps;
|
|
1163
|
+
if (isHooksDisabled(settings)) return void 0;
|
|
809
1164
|
const claudeToolName = normalizeToolName(event.toolName);
|
|
1165
|
+
const sessionData = sessionIds(() => ctx.sessionManager.getSessionId());
|
|
810
1166
|
const input = {
|
|
811
|
-
...
|
|
1167
|
+
...sessionData,
|
|
812
1168
|
tool_name: claudeToolName,
|
|
813
1169
|
tool_input: normalizeToolInput(claudeToolName, event.input),
|
|
814
1170
|
tool_call_id: event.toolCallId
|
|
@@ -816,12 +1172,12 @@ async function runPreToolUseHooks(settings, event, ctx) {
|
|
|
816
1172
|
const shellCommand = extractShellCommand(event.toolName, event.input);
|
|
817
1173
|
if (shellCommand !== void 0 && shellCommand.length > 0) {
|
|
818
1174
|
const bashInput = {
|
|
819
|
-
...
|
|
1175
|
+
...sessionData,
|
|
820
1176
|
tool_name: "Bash",
|
|
821
1177
|
tool_input: { command: shellCommand },
|
|
822
1178
|
tool_call_id: event.toolCallId
|
|
823
1179
|
};
|
|
824
|
-
const bashResult =
|
|
1180
|
+
const bashResult = yield* runHooksForEvent(settings.hooks.PreToolUse, "Bash", bashInput, ctx, "PreToolUse");
|
|
825
1181
|
if (bashResult.block) {
|
|
826
1182
|
tel("tool_call.decision", {
|
|
827
1183
|
tool_name: claudeToolName,
|
|
@@ -834,7 +1190,7 @@ async function runPreToolUseHooks(settings, event, ctx) {
|
|
|
834
1190
|
};
|
|
835
1191
|
}
|
|
836
1192
|
}
|
|
837
|
-
const result =
|
|
1193
|
+
const result = yield* runHooksForEvent(settings.hooks.PreToolUse, claudeToolName, input, ctx, "PreToolUse");
|
|
838
1194
|
if (result.block) {
|
|
839
1195
|
tel("tool_call.decision", {
|
|
840
1196
|
tool_name: claudeToolName,
|
|
@@ -854,31 +1210,33 @@ async function runPreToolUseHooks(settings, event, ctx) {
|
|
|
854
1210
|
tool_name: claudeToolName,
|
|
855
1211
|
decision: "allow"
|
|
856
1212
|
});
|
|
857
|
-
}
|
|
858
|
-
|
|
1213
|
+
});
|
|
1214
|
+
const runPostToolUseHooks = Effect.fn("runPostToolUseHooks")(function* (settings, event, ctx) {
|
|
859
1215
|
const claudeToolName = normalizeToolName(event.toolName);
|
|
1216
|
+
if (isHooksDisabled(settings)) return void 0;
|
|
860
1217
|
const input = {
|
|
861
|
-
...
|
|
1218
|
+
...sessionIds(() => ctx.sessionManager.getSessionId()),
|
|
862
1219
|
tool_name: claudeToolName,
|
|
863
1220
|
tool_input: normalizeToolInput(claudeToolName, event.input),
|
|
864
1221
|
tool_call_id: event.toolCallId,
|
|
865
1222
|
output: event.content,
|
|
866
1223
|
is_error: event.isError ?? false
|
|
867
1224
|
};
|
|
868
|
-
return runHooksForEvent(settings.hooks.PostToolUse, claudeToolName, input, ctx, "PostToolUse");
|
|
869
|
-
}
|
|
870
|
-
|
|
1225
|
+
return yield* runHooksForEvent(settings.hooks.PostToolUse, claudeToolName, input, ctx, "PostToolUse");
|
|
1226
|
+
});
|
|
1227
|
+
const runUserPromptSubmitHooks = Effect.fn("runUserPromptSubmitHooks")(function* (settings, event, ctx) {
|
|
871
1228
|
const entries = settings.hooks.UserPromptSubmit;
|
|
1229
|
+
if (isHooksDisabled(settings)) return void 0;
|
|
872
1230
|
if (entries.length === 0) return void 0;
|
|
873
1231
|
const cwd = ctx.cwd;
|
|
874
1232
|
let injected = "";
|
|
875
1233
|
const input = {
|
|
876
|
-
...
|
|
1234
|
+
...sessionIds(() => ctx.sessionManager.getSessionId()),
|
|
877
1235
|
prompt: event.text,
|
|
878
1236
|
source: event.source
|
|
879
1237
|
};
|
|
880
1238
|
for (const entry of entries) for (const hook of entry.hooks) {
|
|
881
|
-
const result =
|
|
1239
|
+
const result = yield* runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3);
|
|
882
1240
|
if (result.code !== 0) continue;
|
|
883
1241
|
const stdout = result.stdout.trim();
|
|
884
1242
|
if (stdout.length > 0) injected += (injected.length > 0 ? "\n\n" : "") + stdout;
|
|
@@ -887,173 +1245,249 @@ async function runUserPromptSubmitHooks(settings, event, ctx) {
|
|
|
887
1245
|
const result = { text: `${injected}\n\n${event.text}` };
|
|
888
1246
|
if (event.images !== void 0) result.images = event.images;
|
|
889
1247
|
return result;
|
|
890
|
-
}
|
|
891
|
-
|
|
1248
|
+
});
|
|
1249
|
+
const runSessionStartHooks = Effect.fn("runSessionStartHooks")(function* (settings, reason, ctx) {
|
|
1250
|
+
const { tel } = yield* HookDispatcherExecutorDeps;
|
|
1251
|
+
if (isHooksDisabled(settings)) return;
|
|
892
1252
|
const entries = settings.hooks.SessionStart;
|
|
893
1253
|
if (entries.length === 0) return;
|
|
894
1254
|
const cwd = ctx.cwd;
|
|
895
1255
|
const input = {
|
|
896
|
-
...
|
|
1256
|
+
...sessionIds(() => ctx.sessionManager.getSessionId()),
|
|
897
1257
|
reason
|
|
898
1258
|
};
|
|
899
1259
|
for (const entry of entries) {
|
|
900
1260
|
if (entry.matcher && !matchesMatcher(reason, entry.matcher)) continue;
|
|
901
1261
|
for (const hook of entry.hooks) {
|
|
902
|
-
const hookName = hook.command
|
|
1262
|
+
const hookName = hookNameFromCommand(hook.command);
|
|
1263
|
+
const timeoutMs = (hook.timeout ?? 10) * 1e3;
|
|
903
1264
|
if (hook.async) {
|
|
904
|
-
runHookScript(hook.command, input, cwd, (
|
|
1265
|
+
yield* Effect.forkDaemon(runHookScript(hook.command, input, cwd, timeoutMs).pipe(Effect.tap((result) => Effect.sync(() => {
|
|
905
1266
|
tel("hook.executed", {
|
|
906
1267
|
hook: hookName,
|
|
907
1268
|
exit_code: result.code
|
|
908
1269
|
});
|
|
909
|
-
}, (
|
|
1270
|
+
})), Effect.catchAll(() => Effect.sync(() => {
|
|
910
1271
|
tel("hook.executed", {
|
|
911
1272
|
hook: hookName,
|
|
912
|
-
exit_code: null
|
|
913
|
-
error: err instanceof Error ? err.message : "unknown error"
|
|
1273
|
+
exit_code: null
|
|
914
1274
|
});
|
|
915
|
-
});
|
|
1275
|
+
}))));
|
|
916
1276
|
continue;
|
|
917
1277
|
}
|
|
918
|
-
|
|
1278
|
+
yield* runHookScript(hook.command, input, cwd, timeoutMs).pipe(Effect.tap((result) => Effect.sync(() => {
|
|
919
1279
|
tel("hook.executed", {
|
|
920
1280
|
hook: hookName,
|
|
921
1281
|
exit_code: result.code
|
|
922
1282
|
});
|
|
923
|
-
}, (
|
|
1283
|
+
})), Effect.catchAll(() => Effect.sync(() => {
|
|
924
1284
|
tel("hook.executed", {
|
|
925
1285
|
hook: hookName,
|
|
926
|
-
exit_code: null
|
|
927
|
-
error: err instanceof Error ? err.message : "unknown error"
|
|
1286
|
+
exit_code: null
|
|
928
1287
|
});
|
|
929
|
-
});
|
|
1288
|
+
})));
|
|
930
1289
|
}
|
|
931
1290
|
}
|
|
932
|
-
}
|
|
933
|
-
|
|
1291
|
+
});
|
|
1292
|
+
const runLifecycleHooks = Effect.fn("runLifecycleHooks")(function* (entries, ctx) {
|
|
934
1293
|
if (entries.length === 0) return;
|
|
1294
|
+
const { tel } = yield* HookDispatcherExecutorDeps;
|
|
935
1295
|
const cwd = ctx.cwd;
|
|
936
|
-
const input = { ...
|
|
1296
|
+
const input = { ...sessionIds(() => ctx.sessionManager.getSessionId()) };
|
|
937
1297
|
for (const entry of entries) for (const hook of entry.hooks) {
|
|
938
|
-
const hookName = hook.command
|
|
939
|
-
|
|
1298
|
+
const hookName = hookNameFromCommand(hook.command);
|
|
1299
|
+
const timeoutMs = (hook.timeout ?? 10) * 1e3;
|
|
1300
|
+
if (hook.async) yield* Effect.forkDaemon(runHookScript(hook.command, input, cwd, timeoutMs).pipe(Effect.tap((result) => Effect.sync(() => {
|
|
940
1301
|
tel("hook.executed", {
|
|
941
1302
|
hook: hookName,
|
|
942
1303
|
exit_code: result.code
|
|
943
1304
|
});
|
|
944
|
-
}, (
|
|
1305
|
+
})), Effect.catchAll(() => Effect.sync(() => {
|
|
945
1306
|
tel("hook.executed", {
|
|
946
1307
|
hook: hookName,
|
|
947
|
-
exit_code: null
|
|
948
|
-
error: err instanceof Error ? err.message : "unknown error"
|
|
1308
|
+
exit_code: null
|
|
949
1309
|
});
|
|
950
|
-
});
|
|
951
|
-
else
|
|
1310
|
+
}))));
|
|
1311
|
+
else yield* runHookScript(hook.command, input, cwd, timeoutMs).pipe(Effect.tap((result) => Effect.sync(() => {
|
|
952
1312
|
tel("hook.executed", {
|
|
953
1313
|
hook: hookName,
|
|
954
1314
|
exit_code: result.code
|
|
955
1315
|
});
|
|
956
|
-
}, (
|
|
1316
|
+
})), Effect.catchAll(() => Effect.sync(() => {
|
|
957
1317
|
tel("hook.executed", {
|
|
958
1318
|
hook: hookName,
|
|
959
|
-
exit_code: null
|
|
960
|
-
error: err instanceof Error ? err.message : "unknown error"
|
|
1319
|
+
exit_code: null
|
|
961
1320
|
});
|
|
962
|
-
});
|
|
1321
|
+
})));
|
|
963
1322
|
}
|
|
964
|
-
}
|
|
1323
|
+
});
|
|
965
1324
|
//#endregion
|
|
966
|
-
//#region src/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1325
|
+
//#region src/runtime.ts
|
|
1326
|
+
const nodeLayer = NodeCommandExecutor.layer.pipe(Layer.provideMerge(NodeFileSystem.layer), Layer.provideMerge(PathModule.layer));
|
|
1327
|
+
const runtime = ManagedRuntime.make(nodeLayer);
|
|
1328
|
+
//#endregion
|
|
1329
|
+
//#region src/hook-dispatcher.handler.ts
|
|
1330
|
+
const HookDispatcherTask = (pi) => Layer.effectDiscard(Effect.sync(() => {
|
|
1331
|
+
const tel = createTelemetry("claude_compat", pi.logger);
|
|
1332
|
+
const telLayer = Layer.succeed(HookDispatcherExecutorDeps, { tel });
|
|
1333
|
+
const runSafe = async (effect) => {
|
|
1334
|
+
const exit = await runtime.runPromise(effect.pipe(Effect.provide(telLayer), Effect.exit));
|
|
1335
|
+
if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
|
|
1336
|
+
return exit.value;
|
|
1337
|
+
};
|
|
1338
|
+
pi.on("tool_call", (event, ctx) => runSafe(Effect.gen(function* () {
|
|
1339
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1340
|
+
if (!settings) return void 0;
|
|
1341
|
+
return yield* runPreToolUseHooks(settings, event, ctx);
|
|
1342
|
+
})));
|
|
1343
|
+
pi.on("tool_result", (event, ctx) => runSafe(Effect.gen(function* () {
|
|
1344
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1345
|
+
if (!settings) return void 0;
|
|
1346
|
+
const result = yield* runPostToolUseHooks(settings, event, ctx);
|
|
1347
|
+
if (result?.block) return {
|
|
1348
|
+
isError: true,
|
|
1349
|
+
content: [{
|
|
1350
|
+
type: "text",
|
|
1351
|
+
text: result.reason ?? "Blocked by PostToolUse hook"
|
|
1352
|
+
}]
|
|
1353
|
+
};
|
|
1354
|
+
if (result?.warning) return {
|
|
1355
|
+
content: [...event.content ?? [], {
|
|
1356
|
+
type: "text",
|
|
1357
|
+
text: result.warning
|
|
1358
|
+
}],
|
|
1359
|
+
isError: event.isError
|
|
1360
|
+
};
|
|
1361
|
+
})));
|
|
1362
|
+
pi.on("input", (event, ctx) => runSafe(Effect.gen(function* () {
|
|
1363
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1364
|
+
if (!settings) return void 0;
|
|
1365
|
+
return yield* runUserPromptSubmitHooks(settings, event, ctx);
|
|
1366
|
+
})));
|
|
1367
|
+
pi.on("session_start", (_event, ctx) => runSafe(Effect.gen(function* () {
|
|
1368
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1369
|
+
if (!settings) return void 0;
|
|
1370
|
+
yield* runSessionStartHooks(settings, "start", ctx);
|
|
1371
|
+
})));
|
|
1372
|
+
pi.on("session_compact", (_event, ctx) => runSafe(Effect.gen(function* () {
|
|
1373
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1374
|
+
if (!settings) return void 0;
|
|
1375
|
+
yield* runSessionStartHooks(settings, "compact", ctx);
|
|
1376
|
+
})));
|
|
1377
|
+
pi.on("agent_start", (_event, ctx) => runSafe(Effect.gen(function* () {
|
|
1378
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1379
|
+
if (!settings) return void 0;
|
|
1380
|
+
yield* runSessionStartHooks(settings, "resume", ctx);
|
|
1381
|
+
})));
|
|
1382
|
+
pi.on("session_shutdown", (_event, ctx) => runSafe(Effect.gen(function* () {
|
|
1383
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1384
|
+
if (!settings) return void 0;
|
|
1385
|
+
if (settings.disableAllHooks) return void 0;
|
|
1386
|
+
yield* runLifecycleHooks(settings.hooks.SessionEnd, ctx);
|
|
1387
|
+
})));
|
|
1388
|
+
pi.on("session_stop", (_event, ctx) => runSafe(Effect.gen(function* () {
|
|
1389
|
+
const settings = yield* loadSettings(ctx.cwd);
|
|
1390
|
+
if (!settings) return void 0;
|
|
1391
|
+
if (settings.disableAllHooks) return void 0;
|
|
1392
|
+
yield* runLifecycleHooks(settings.hooks.Stop, ctx);
|
|
1393
|
+
})));
|
|
1394
|
+
}));
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region src/inject-instructions.executor.ts
|
|
1397
|
+
const extractRefs = Effect.fn("extractRefs")(function* (content, baseDir, projectDir) {
|
|
1398
|
+
const fs = yield* FileSystem;
|
|
1399
|
+
const path = yield* PathModule.Path;
|
|
986
1400
|
const refs = [];
|
|
987
1401
|
for (const rawLine of content.split("\n")) {
|
|
988
1402
|
const noMarker = rawLine.trim().replace(/^[-*+]\s+/, "");
|
|
989
1403
|
if (!noMarker.startsWith("@")) continue;
|
|
990
1404
|
const ref = noMarker.slice(1).trim();
|
|
991
1405
|
if (!ref || ref.includes(" ")) continue;
|
|
992
|
-
if (isAbsolute(ref)) continue;
|
|
993
|
-
const baseResolved = resolve(baseDir, ref);
|
|
994
|
-
if (
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1406
|
+
if (path.isAbsolute(ref)) continue;
|
|
1407
|
+
const baseResolved = path.resolve(baseDir, ref);
|
|
1408
|
+
if (baseResolved.startsWith(projectDir + "/") || baseResolved === projectDir) {
|
|
1409
|
+
const baseExists = yield* Effect.either(fs.exists(baseResolved));
|
|
1410
|
+
if (Either.isRight(baseExists) && baseExists.right) {
|
|
1411
|
+
refs.push({
|
|
1412
|
+
sourcePath: baseDir,
|
|
1413
|
+
resolvedPath: baseResolved
|
|
1414
|
+
});
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1000
1417
|
}
|
|
1001
|
-
const rootResolved = resolve(projectDir, ref);
|
|
1002
|
-
if ((rootResolved.startsWith(projectDir +
|
|
1003
|
-
sourcePath:
|
|
1418
|
+
const rootResolved = path.resolve(projectDir, ref);
|
|
1419
|
+
if ((rootResolved.startsWith(projectDir + "/") || rootResolved === projectDir) && rootResolved !== baseResolved) refs.push({
|
|
1420
|
+
sourcePath: projectDir,
|
|
1004
1421
|
resolvedPath: rootResolved
|
|
1005
1422
|
});
|
|
1006
1423
|
}
|
|
1007
1424
|
return refs;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const claudeMdPaths = [resolve(projectDir, "CLAUDE.md"), resolve(projectDir, ".claude", "CLAUDE.md")];
|
|
1425
|
+
});
|
|
1426
|
+
const loadReferencedContent = Effect.fn("loadReferencedContent")(function* (projectDir) {
|
|
1427
|
+
const fs = yield* FileSystem;
|
|
1428
|
+
const path = yield* PathModule.Path;
|
|
1429
|
+
const claudeMdPaths = [path.resolve(projectDir, "CLAUDE.md"), path.resolve(projectDir, ".claude", "CLAUDE.md")];
|
|
1014
1430
|
const allRefs = [];
|
|
1015
|
-
for (const filePath of claudeMdPaths)
|
|
1431
|
+
for (const filePath of claudeMdPaths) {
|
|
1432
|
+
const content = yield* Effect.either(fs.readFileString(filePath, "utf-8"));
|
|
1433
|
+
if (Either.isRight(content)) {
|
|
1434
|
+
const refs = yield* extractRefs(content.right, path.dirname(filePath), projectDir);
|
|
1435
|
+
allRefs.push(...refs);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1016
1438
|
const seen = /* @__PURE__ */ new Set();
|
|
1017
1439
|
const uniqueRefs = [];
|
|
1018
1440
|
for (const ref of allRefs) if (!seen.has(ref.resolvedPath)) {
|
|
1019
1441
|
seen.add(ref.resolvedPath);
|
|
1020
1442
|
uniqueRefs.push(ref);
|
|
1021
1443
|
}
|
|
1022
|
-
|
|
1444
|
+
const validRefs = [];
|
|
1445
|
+
for (const ref of uniqueRefs) {
|
|
1446
|
+
const exists = yield* Effect.either(fs.exists(ref.resolvedPath));
|
|
1447
|
+
if (Either.isRight(exists) && exists.right) validRefs.push(ref);
|
|
1448
|
+
}
|
|
1449
|
+
if (validRefs.length === 0) return "";
|
|
1023
1450
|
const parts = ["# Injected @-references from CLAUDE.md"];
|
|
1024
1451
|
parts.push("The following files were @-imported by CLAUDE.md and contain project rules.");
|
|
1025
1452
|
parts.push("");
|
|
1026
|
-
for (const ref of
|
|
1453
|
+
for (const ref of validRefs) {
|
|
1027
1454
|
const relativePath = ref.resolvedPath.slice(projectDir.length + 1);
|
|
1028
1455
|
parts.push(`## ${relativePath}`);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
} catch {
|
|
1033
|
-
parts.push(`[error reading ${relativePath}]`);
|
|
1034
|
-
}
|
|
1456
|
+
const refContent = yield* Effect.either(fs.readFileString(ref.resolvedPath, "utf-8"));
|
|
1457
|
+
if (Either.isRight(refContent)) parts.push(refContent.right);
|
|
1458
|
+
else parts.push(`[error reading ${relativePath}]`);
|
|
1035
1459
|
parts.push("");
|
|
1036
1460
|
}
|
|
1037
1461
|
return parts.join("\n");
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
pi.on("before_agent_start", async (event) => {
|
|
1043
|
-
|
|
1044
|
-
|
|
1462
|
+
});
|
|
1463
|
+
//#endregion
|
|
1464
|
+
//#region src/inject-instructions.handler.ts
|
|
1465
|
+
const InjectInstructionsTask = (pi) => Layer.effectDiscard(Effect.sync(() => {
|
|
1466
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
1467
|
+
const exit = await runtime.runPromise(Effect.gen(function* () {
|
|
1468
|
+
return yield* loadReferencedContent(yield* Config.string("CLAUDE_PROJECT_DIR").pipe(Config.withDefault(process.cwd())));
|
|
1469
|
+
}).pipe(Effect.exit));
|
|
1470
|
+
if (Exit.isFailure(exit)) throw Cause.squash(exit.cause);
|
|
1471
|
+
const injected = exit.value;
|
|
1472
|
+
if (injected === "") return void 0;
|
|
1045
1473
|
return { systemPrompt: [
|
|
1046
1474
|
...event.systemPrompt,
|
|
1047
1475
|
"",
|
|
1048
|
-
|
|
1476
|
+
injected
|
|
1049
1477
|
] };
|
|
1050
1478
|
});
|
|
1051
|
-
}
|
|
1479
|
+
}));
|
|
1052
1480
|
//#endregion
|
|
1053
1481
|
//#region src/index.ts
|
|
1054
1482
|
function claudeCompatExtension(pi) {
|
|
1055
|
-
|
|
1056
|
-
|
|
1483
|
+
Effect.runSync(Effect.scoped(Layer.build(Layer.mergeAll(InjectInstructionsTask(pi), HookDispatcherTask(pi)))));
|
|
1484
|
+
ManagedRuntime.make(Layer.mergeAll(InjectInstructionsTask(pi), HookDispatcherTask(pi)));
|
|
1485
|
+
process.on("SIGINT", () => {
|
|
1486
|
+
runtime.dispose();
|
|
1487
|
+
});
|
|
1488
|
+
process.on("SIGTERM", () => {
|
|
1489
|
+
runtime.dispose();
|
|
1490
|
+
});
|
|
1057
1491
|
}
|
|
1058
1492
|
//#endregion
|
|
1059
1493
|
export { claudeCompatExtension as default };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemfsoftware/omp-claude-compat",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.1.3",
|
|
5
5
|
"author": "Ryan Lee <drdgvhbh@gmail.com>",
|
|
6
6
|
"description": "OMP Claude Code compatibility: dispatch .claude/settings.json hooks + inject CLAUDE.md @-references",
|
|
7
7
|
"type": "module",
|
|
@@ -12,35 +12,50 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist"
|
|
14
14
|
],
|
|
15
|
-
"dependencies": {
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@effect/platform": "^0.97.0",
|
|
17
|
+
"@effect/platform-node": "^0.97.0",
|
|
18
|
+
"effect": "^3.22.0"
|
|
19
|
+
},
|
|
16
20
|
"peerDependencies": {
|
|
17
21
|
"@oh-my-pi/pi-coding-agent": "^17.0.5"
|
|
18
22
|
},
|
|
19
23
|
"devDependencies": {
|
|
24
|
+
"@effect/vitest": "0.30.0",
|
|
20
25
|
"@oh-my-pi/pi-coding-agent": "^17.0.5",
|
|
21
26
|
"@types/node": "^24",
|
|
27
|
+
"fast-check": "^3",
|
|
22
28
|
"rimraf": "^6.1.3",
|
|
23
29
|
"tsdown": "^0.22.9",
|
|
24
30
|
"typescript": "^7",
|
|
25
31
|
"vitest": "^4",
|
|
26
|
-
"@systemfsoftware/
|
|
32
|
+
"@systemfsoftware/effect-memfs": "^1.0.5",
|
|
33
|
+
"@systemfsoftware/effect-gherkin-spec": "^0.3.5",
|
|
27
34
|
"@systemfsoftware/omp-utils": "^0.0.0",
|
|
28
|
-
"@systemfsoftware/oxlint-config": "^0.1.0"
|
|
35
|
+
"@systemfsoftware/oxlint-config": "^0.1.0",
|
|
36
|
+
"@systemfsoftware/tsconfig": "^1.2.4"
|
|
29
37
|
},
|
|
30
38
|
"omp": {
|
|
31
39
|
"extensions": [
|
|
32
40
|
"./dist/index.js"
|
|
33
41
|
]
|
|
34
42
|
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"provenance": true
|
|
45
|
+
},
|
|
35
46
|
"inlinedDependencies": {
|
|
36
47
|
"@jsr/std__collections": "1.3.0",
|
|
37
48
|
"@jsr/std__toml": "1.0.11"
|
|
38
49
|
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/systemfsoftware/systemfsoftware.git",
|
|
53
|
+
"directory": "omp/plugins/omp-claude-compat"
|
|
54
|
+
},
|
|
39
55
|
"scripts": {
|
|
40
56
|
"clean": "rimraf dist",
|
|
41
57
|
"build": "rimraf dist && tsdown",
|
|
42
|
-
"verify-dist": "node ../../scripts/check-dist-builtins.mjs --dist dist/index.js --expect
|
|
43
|
-
"postbuild": "pnpm verify-dist",
|
|
58
|
+
"verify-dist": "node ../../scripts/check-dist-builtins.mjs --dist dist/index.js --expect os",
|
|
44
59
|
"typecheck": "tsc --noEmit --incremental",
|
|
45
60
|
"test": "vitest run",
|
|
46
61
|
"lint": "oxlint . ${AGENT:+--format=unix --quiet}"
|