@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.
Files changed (2) hide show
  1. package/dist/index.js +703 -269
  2. package/package.json +21 -6
package/dist/index.js CHANGED
@@ -1,6 +1,11 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { dirname, extname, isAbsolute, resolve, sep } from "node:path";
3
- import { execFile } from "node:child_process";
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
- surround("[", dottedKey, "]");
477
- surround("[[", dottedKey, "]]");
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 ../omp-utils/dist/index.js
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-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
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 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;
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
- function resolveCommand(command, cwd) {
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", unquoted]
1025
+ args: ["-c", trimmed.startsWith("\"") && trimmed.endsWith("\"") ? trimmed.slice(1, -1) : trimmed]
694
1026
  };
695
1027
  }
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,
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
- child.stdin?.on("error", () => {}).write(stdin);
722
- child.stdin?.end();
723
- return promise;
724
- }
725
- async function runHooksForEvent(entries, matchValue, input, ctx, event) {
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.split(/[\\/]/).pop() ?? 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, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
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
- }, (err) => {
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
- let result;
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: null,
762
- error: err instanceof Error ? err.message : "unknown error"
1120
+ exit_code: r.code
763
1121
  });
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
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
- if (result.code === 2) return {
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: 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
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
- } catch {}
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: input } : {},
1157
+ ...inputModified ? { updatedInput: currentInput } : {},
805
1158
  ...warning !== void 0 ? { warning } : {}
806
1159
  };
807
- }
808
- async function runPreToolUseHooks(settings, event, ctx) {
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
- ...getSessionIds(ctx),
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
- ...getSessionIds(ctx),
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 = await runHooksForEvent(settings.hooks.PreToolUse, "Bash", bashInput, ctx, "PreToolUse");
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 = await runHooksForEvent(settings.hooks.PreToolUse, claudeToolName, input, ctx, "PreToolUse");
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
- async function runPostToolUseHooks(settings, event, ctx) {
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
- ...getSessionIds(ctx),
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
- async function runUserPromptSubmitHooks(settings, event, ctx) {
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
- ...getSessionIds(ctx),
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 = await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3);
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
- async function runSessionStartHooks(settings, reason, ctx) {
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
- ...getSessionIds(ctx),
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.split(/[\\/]/).pop() ?? 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, (hook.timeout ?? 10) * 1e3).then((result) => {
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
- }, (err) => {
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
- await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
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
- }, (err) => {
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
- async function runLifecycleHooks(entries, ctx) {
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 = { ...getSessionIds(ctx) };
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.split(/[\\/]/).pop() ?? hook.command;
939
- if (hook.async) runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
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
- }, (err) => {
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 await runHookScript(hook.command, input, cwd, (hook.timeout ?? 10) * 1e3).then((result) => {
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
- }, (err) => {
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/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);
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 ((baseResolved.startsWith(projectDir + sep) || baseResolved === projectDir) && existsSync(baseResolved)) {
995
- refs.push({
996
- sourcePath: filePath,
997
- resolvedPath: baseResolved
998
- });
999
- continue;
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 + sep) || rootResolved === projectDir) && existsSync(rootResolved)) refs.push({
1003
- sourcePath: filePath,
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
- * 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")];
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) if (existsSync(filePath)) allRefs.push(...extractRefs(filePath, projectDir));
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
- if (uniqueRefs.length === 0) return "";
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 uniqueRefs) {
1453
+ for (const ref of validRefs) {
1027
1454
  const relativePath = ref.resolvedPath.slice(projectDir.length + 1);
1028
1455
  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
- }
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
- 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;
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
- cached
1476
+ injected
1049
1477
  ] };
1050
1478
  });
1051
- }
1479
+ }));
1052
1480
  //#endregion
1053
1481
  //#region src/index.ts
1054
1482
  function claudeCompatExtension(pi) {
1055
- hookDispatcherExtension(pi);
1056
- injectInstructionsExtension(pi);
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": "0.0.0",
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/tsconfig": "^1.1.0",
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 node:child_process node:fs node:path",
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}"