oh-my-customcode 1.1.52 → 1.1.53

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/cli/index.js CHANGED
@@ -243,7 +243,7 @@ var init_package = __esm(() => {
243
243
  workspaces: [
244
244
  "packages/*"
245
245
  ],
246
- version: "1.1.52",
246
+ version: "1.1.53",
247
247
  description: "Batteries-included agent harness for Claude Code",
248
248
  type: "module",
249
249
  bin: {
@@ -7634,7 +7634,7 @@ async function _scanDirForLockfiles(dir2, depth, seen, results, home, currentVer
7634
7634
  }
7635
7635
  }
7636
7636
  async function _findProjectsFromLockfiles(options, currentVersion) {
7637
- const { dirname: dirname3 } = await import("node:path");
7637
+ const { dirname: dirname4 } = await import("node:path");
7638
7638
  const fs3 = await import("node:fs/promises");
7639
7639
  const seen = new Set;
7640
7640
  const results = [];
@@ -7644,7 +7644,7 @@ async function _findProjectsFromLockfiles(options, currentVersion) {
7644
7644
  const cwd = process.cwd();
7645
7645
  if (!searchPaths.includes(cwd))
7646
7646
  searchPaths.push(cwd);
7647
- const parent = dirname3(cwd);
7647
+ const parent = dirname4(cwd);
7648
7648
  if (parent !== cwd && !searchPaths.includes(parent))
7649
7649
  searchPaths.push(parent);
7650
7650
  }
@@ -27949,6 +27949,489 @@ function getDefaultWorkflow() {
27949
27949
  };
27950
27950
  }
27951
27951
 
27952
+ // src/core/hooks-settings.ts
27953
+ init_fs();
27954
+ import { dirname as dirname3 } from "node:path";
27955
+ var IDENT_CHAR = /[A-Za-z0-9_.]/;
27956
+ var IDENT_START = /[A-Za-z_]/;
27957
+ function consumeSingleCharOp(input, i) {
27958
+ const ch = input[i];
27959
+ if (ch === "(" || ch === ")" || ch === "!") {
27960
+ return { token: { type: "OP", value: ch }, next: i + 1 };
27961
+ }
27962
+ return null;
27963
+ }
27964
+ function consumeTwoCharOp(input, i) {
27965
+ const two = input.slice(i, i + 2);
27966
+ if (two === "&&" || two === "||" || two === "==") {
27967
+ return { token: { type: "OP", value: two }, next: i + 2 };
27968
+ }
27969
+ return null;
27970
+ }
27971
+ function consumeStringLiteral(input, i) {
27972
+ const n = input.length;
27973
+ let j = i + 1;
27974
+ let value = "";
27975
+ while (j < n && input[j] !== '"') {
27976
+ if (input[j] === "\\" && j + 1 < n) {
27977
+ value += input[j + 1];
27978
+ j += 2;
27979
+ } else {
27980
+ value += input[j];
27981
+ j++;
27982
+ }
27983
+ }
27984
+ if (j >= n) {
27985
+ throw new Error(`unterminated string literal in matcher expression: ${input}`);
27986
+ }
27987
+ return { token: { type: "STRING", value }, next: j + 1 };
27988
+ }
27989
+ function consumeIdentifierOrKeyword(input, i) {
27990
+ const n = input.length;
27991
+ let j = i;
27992
+ while (j < n && IDENT_CHAR.test(input[j]))
27993
+ j++;
27994
+ const word = input.slice(i, j);
27995
+ const token = word === "matches" ? { type: "OP", value: "matches" } : { type: "IDENT", value: word };
27996
+ return { token, next: j };
27997
+ }
27998
+ function tokenizeMatcher(input) {
27999
+ const tokens = [];
28000
+ const n = input.length;
28001
+ let i = 0;
28002
+ while (i < n) {
28003
+ const ch = input[i];
28004
+ if (ch === " " || ch === "\t" || ch === `
28005
+ ` || ch === "\r") {
28006
+ i++;
28007
+ continue;
28008
+ }
28009
+ const twoCharOp = consumeTwoCharOp(input, i);
28010
+ if (twoCharOp) {
28011
+ tokens.push(twoCharOp.token);
28012
+ i = twoCharOp.next;
28013
+ continue;
28014
+ }
28015
+ const singleCharOp = consumeSingleCharOp(input, i);
28016
+ if (singleCharOp) {
28017
+ tokens.push(singleCharOp.token);
28018
+ i = singleCharOp.next;
28019
+ continue;
28020
+ }
28021
+ if (ch === '"') {
28022
+ const literal = consumeStringLiteral(input, i);
28023
+ tokens.push(literal.token);
28024
+ i = literal.next;
28025
+ continue;
28026
+ }
28027
+ if (IDENT_START.test(ch)) {
28028
+ const ident = consumeIdentifierOrKeyword(input, i);
28029
+ tokens.push(ident.token);
28030
+ i = ident.next;
28031
+ continue;
28032
+ }
28033
+ throw new Error(`unexpected character '${ch}' at offset ${i} in matcher expression: ${input}`);
28034
+ }
28035
+ return tokens;
28036
+ }
28037
+
28038
+ class MatcherParser {
28039
+ tokens;
28040
+ pos = 0;
28041
+ constructor(tokens) {
28042
+ this.tokens = tokens;
28043
+ }
28044
+ parse() {
28045
+ const node = this.parseOr();
28046
+ if (this.pos !== this.tokens.length) {
28047
+ throw new Error(`trailing tokens after position ${this.pos} in matcher expression`);
28048
+ }
28049
+ return node;
28050
+ }
28051
+ peek() {
28052
+ return this.tokens[this.pos];
28053
+ }
28054
+ next() {
28055
+ const t2 = this.tokens[this.pos];
28056
+ if (!t2)
28057
+ throw new Error("unexpected end of matcher expression");
28058
+ this.pos++;
28059
+ return t2;
28060
+ }
28061
+ expectOp(value) {
28062
+ const t2 = this.next();
28063
+ if (t2.type !== "OP" || t2.value !== value) {
28064
+ throw new Error(`expected '${value}', got ${JSON.stringify(t2)}`);
28065
+ }
28066
+ }
28067
+ parseOr() {
28068
+ let left = this.parseAnd();
28069
+ while (this.peek()?.type === "OP" && this.peek()?.value === "||") {
28070
+ this.next();
28071
+ const right = this.parseAnd();
28072
+ left = { kind: "or", left, right };
28073
+ }
28074
+ return left;
28075
+ }
28076
+ parseAnd() {
28077
+ let left = this.parseUnary();
28078
+ while (this.peek()?.type === "OP" && this.peek()?.value === "&&") {
28079
+ this.next();
28080
+ const right = this.parseUnary();
28081
+ left = { kind: "and", left, right };
28082
+ }
28083
+ return left;
28084
+ }
28085
+ parseUnary() {
28086
+ if (this.peek()?.type === "OP" && this.peek()?.value === "!") {
28087
+ this.next();
28088
+ this.expectOp("(");
28089
+ const inner = this.parseOr();
28090
+ this.expectOp(")");
28091
+ return { kind: "not", child: inner };
28092
+ }
28093
+ return this.parseAtom();
28094
+ }
28095
+ parseAtom() {
28096
+ if (this.peek()?.type === "OP" && this.peek()?.value === "(") {
28097
+ this.next();
28098
+ const inner = this.parseOr();
28099
+ this.expectOp(")");
28100
+ return inner;
28101
+ }
28102
+ const identTok = this.next();
28103
+ if (identTok.type !== "IDENT") {
28104
+ throw new Error(`expected identifier, got ${JSON.stringify(identTok)}`);
28105
+ }
28106
+ const opTok = this.next();
28107
+ if (opTok.type !== "OP" || opTok.value !== "==" && opTok.value !== "matches") {
28108
+ throw new Error(`expected '==' or 'matches' after identifier '${identTok.value}'`);
28109
+ }
28110
+ const strTok = this.next();
28111
+ if (strTok.type !== "STRING") {
28112
+ throw new Error(`expected string literal after '${opTok.value}'`);
28113
+ }
28114
+ if (opTok.value === "==") {
28115
+ if (identTok.value !== "tool") {
28116
+ throw new Error(`unsupported '==' comparison on field '${identTok.value}' (only 'tool' is supported)`);
28117
+ }
28118
+ return { kind: "toolEq", tool: strTok.value };
28119
+ }
28120
+ return { kind: "fieldMatch", field: identTok.value, pattern: strTok.value };
28121
+ }
28122
+ }
28123
+ function parseMatcherExpr(raw) {
28124
+ const tokens = tokenizeMatcher(raw);
28125
+ if (tokens.length === 0) {
28126
+ throw new Error("empty matcher expression");
28127
+ }
28128
+ return new MatcherParser(tokens).parse();
28129
+ }
28130
+ function isToolOnly(node) {
28131
+ switch (node.kind) {
28132
+ case "toolEq":
28133
+ return true;
28134
+ case "and":
28135
+ case "or":
28136
+ return isToolOnly(node.left) && isToolOnly(node.right);
28137
+ case "not":
28138
+ return isToolOnly(node.child);
28139
+ case "fieldMatch":
28140
+ return false;
28141
+ }
28142
+ }
28143
+ function collectToolNames(node) {
28144
+ switch (node.kind) {
28145
+ case "toolEq":
28146
+ return [node.tool];
28147
+ case "and":
28148
+ case "or":
28149
+ return [...collectToolNames(node.left), ...collectToolNames(node.right)];
28150
+ default:
28151
+ return [];
28152
+ }
28153
+ }
28154
+ function flattenAnd(node) {
28155
+ if (node.kind === "and") {
28156
+ return [...flattenAnd(node.left), ...flattenAnd(node.right)];
28157
+ }
28158
+ return [node];
28159
+ }
28160
+ function tryPureOrFieldRegex(node, field) {
28161
+ if (node.kind === "fieldMatch") {
28162
+ return node.field === field ? node.pattern : null;
28163
+ }
28164
+ if (node.kind === "or") {
28165
+ const left = tryPureOrFieldRegex(node.left, field);
28166
+ const right = tryPureOrFieldRegex(node.right, field);
28167
+ if (left === null || right === null)
28168
+ return null;
28169
+ return `${left}|${right}`;
28170
+ }
28171
+ return null;
28172
+ }
28173
+ function fallbackExtractToolNames(raw) {
28174
+ const names = new Set;
28175
+ const re = /tool\s*==\s*"([^"]+)"/g;
28176
+ let m;
28177
+ while ((m = re.exec(raw)) !== null) {
28178
+ names.add(m[1]);
28179
+ }
28180
+ return Array.from(names);
28181
+ }
28182
+ function analyzeMixedAndExpr(ast, raw) {
28183
+ const parts = flattenAnd(ast);
28184
+ const toolParts = parts.filter(isToolOnly);
28185
+ const fieldParts = parts.filter((p) => !isToolOnly(p));
28186
+ if (toolParts.length === 0) {
28187
+ const names = fallbackExtractToolNames(raw);
28188
+ if (names.length === 0) {
28189
+ throw new Error(`cannot extract a tool matcher from expression (no top-level tool==X clause): ${raw}`);
28190
+ }
28191
+ return { toolRegex: names.join("|"), fieldNode: null };
28192
+ }
28193
+ const tools = Array.from(new Set(toolParts.flatMap(collectToolNames)));
28194
+ const toolRegex = tools.join("|") || "*";
28195
+ if (fieldParts.length === 0) {
28196
+ return { toolRegex, fieldNode: null };
28197
+ }
28198
+ const fieldNode = fieldParts.reduce((acc, part) => acc === null ? part : { kind: "and", left: acc, right: part }, null);
28199
+ return { toolRegex, fieldNode };
28200
+ }
28201
+ function analyzeMatcher(raw) {
28202
+ if (raw === undefined || raw === "*" || raw.trim() === "") {
28203
+ return { toolRegex: "*", fieldNode: null };
28204
+ }
28205
+ let ast;
28206
+ try {
28207
+ ast = parseMatcherExpr(raw);
28208
+ } catch (err) {
28209
+ const names = fallbackExtractToolNames(raw);
28210
+ if (names.length === 0) {
28211
+ const message = err instanceof Error ? err.message : String(err);
28212
+ throw new Error(`unparseable matcher expression and no fallback tool name found: ${raw} (${message})`);
28213
+ }
28214
+ return { toolRegex: names.join("|"), fieldNode: null };
28215
+ }
28216
+ if (isToolOnly(ast)) {
28217
+ const tools = Array.from(new Set(collectToolNames(ast)));
28218
+ return { toolRegex: tools.join("|") || "*", fieldNode: null };
28219
+ }
28220
+ const mcpRegex = tryPureOrFieldRegex(ast, "mcp_tool_name");
28221
+ if (mcpRegex !== null) {
28222
+ return { toolRegex: mcpRegex, fieldNode: null };
28223
+ }
28224
+ return analyzeMixedAndExpr(ast, raw);
28225
+ }
28226
+ function fieldNodeToJq(node) {
28227
+ switch (node.kind) {
28228
+ case "and":
28229
+ return `(${fieldNodeToJq(node.left)} and ${fieldNodeToJq(node.right)})`;
28230
+ case "or":
28231
+ return `(${fieldNodeToJq(node.left)} or ${fieldNodeToJq(node.right)})`;
28232
+ case "not":
28233
+ return `(${fieldNodeToJq(node.child)} | not)`;
28234
+ case "fieldMatch": {
28235
+ const jqPath = node.field === "mcp_tool_name" ? ".tool_name" : `.${node.field}`;
28236
+ return `((${jqPath} // "") | test(${JSON.stringify(node.pattern)}))`;
28237
+ }
28238
+ case "toolEq":
28239
+ return "true";
28240
+ }
28241
+ }
28242
+ function shellSingleQuote(value) {
28243
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
28244
+ }
28245
+ function buildGuardedCommand(jqCond, originalCommand) {
28246
+ const quotedCond = shellSingleQuote(jqCond);
28247
+ return [
28248
+ "input=$(cat)",
28249
+ `if printf '%s' "$input" | jq -e ${quotedCond} >/dev/null 2>&1; then`,
28250
+ ` printf '%s' "$input" | (`,
28251
+ originalCommand,
28252
+ " )",
28253
+ "else",
28254
+ ` printf '%s' "$input"`,
28255
+ "fi"
28256
+ ].join(`
28257
+ `);
28258
+ }
28259
+ var CLASSIFICATION_TABLE = [
28260
+ {
28261
+ event: "PreToolUse",
28262
+ description: "Block dev servers outside tmux - ensures you can access logs",
28263
+ decision: "wrap-guard",
28264
+ reason: 'inline script unconditionally `exit 1`-blocks on ANY match once invoked; it never re-parses tool_input.command, so widening the matcher to bare "Bash" would block every Bash call'
28265
+ },
28266
+ {
28267
+ event: "PreToolUse",
28268
+ description: "Reminder to use tmux for long-running commands",
28269
+ decision: "wrap-guard",
28270
+ reason: 'inline script only checks `[ -z "$TMUX" ]`; it never re-checks tool_input.command against the npm/pnpm/yarn/bun/cargo/docker/pytest/vitest/playwright pattern, so widening the matcher would nag on every Bash call'
28271
+ },
28272
+ {
28273
+ event: "PreToolUse",
28274
+ description: "Pause before git push to review changes",
28275
+ decision: "wrap-guard",
28276
+ reason: 'inline script unconditionally prints the pause prompt once invoked; it never re-checks tool_input.command for "git push", so widening the matcher would pause on every Bash call'
28277
+ },
28278
+ {
28279
+ event: "PreToolUse",
28280
+ description: "Block creation of random .md files - keeps docs consolidated",
28281
+ decision: "drop-condition",
28282
+ reason: "inline script re-parses tool_input.file_path via jq and re-tests both the `\\.(md|txt)$` and the README/CLAUDE/AGENT/SKILL exclusion with its own [[ =~ ]] checks before blocking — the matcher condition is fully redundant with the script body"
28283
+ },
28284
+ {
28285
+ event: "PreToolUse",
28286
+ description: "Warn before destructive git commands — advisory recovery guidance (R001/R021)",
28287
+ decision: "drop-condition",
28288
+ reason: "destructive-git-guard.sh re-parses tool_input.command via jq/python3 and re-greps for each destructive git subcommand (reset --hard, clean -f*, restore, checkout -- ., branch -D) itself before warning — matcher condition is redundant"
28289
+ },
28290
+ {
28291
+ event: "PreToolUse",
28292
+ description: "Block rule file deletion — requires individual user confirmation per rule (R001 safety)",
28293
+ decision: "drop-condition",
28294
+ reason: 'rule-deletion-guard.sh explicitly re-checks `tool != "Bash"` (passthrough) and re-greps tool_input.command for rm/git rm/mv/unlink AND a .claude/rules path before blocking — matcher condition is redundant'
28295
+ },
28296
+ {
28297
+ event: "PostToolUse",
28298
+ description: "Auto-format JS/TS files with Prettier after edits",
28299
+ decision: "wrap-guard",
28300
+ reason: 'inline script only checks file existence (`[ -f "$file_path" ]`); it never re-checks the .ts/.tsx/.js/.jsx extension, so widening the matcher to bare "Edit" would run prettier on every edited file regardless of type'
28301
+ },
28302
+ {
28303
+ event: "PostToolUse",
28304
+ description: "TypeScript check after editing .ts/.tsx files",
28305
+ decision: "wrap-guard",
28306
+ reason: "inline script never re-checks the .ts/.tsx extension before walking up for tsconfig.json and invoking tsc — widening the matcher would run a TS project check after editing non-TS files"
28307
+ },
28308
+ {
28309
+ event: "PostToolUse",
28310
+ description: "Warn about console.log statements after edits",
28311
+ decision: "wrap-guard",
28312
+ reason: "inline script greps the edited file for `console\\.log` unconditionally on file existence; it never re-checks the .ts/.tsx/.js/.jsx extension, so widening the matcher would run this grep on every edited file type"
28313
+ },
28314
+ {
28315
+ event: "PostToolUse",
28316
+ description: "Auto-format Go files with gofmt after edits",
28317
+ decision: "wrap-guard",
28318
+ reason: "inline script never re-checks the .go extension before invoking gofmt -w; running gofmt on a non-Go file would error/corrupt output"
28319
+ },
28320
+ {
28321
+ event: "PostToolUse",
28322
+ description: "Auto-format and lint Python files with ruff after edits",
28323
+ decision: "wrap-guard",
28324
+ reason: "inline script never re-checks the .py extension before invoking ruff format/check --fix; running ruff on a non-Python file would error"
28325
+ },
28326
+ {
28327
+ event: "PostToolUse",
28328
+ description: "Type check Python files with ty after edits",
28329
+ decision: "wrap-guard",
28330
+ reason: "inline script never re-checks the .py extension before invoking `ty check`; running a Python type checker on a non-Python file would error"
28331
+ }
28332
+ ];
28333
+ function findClassification(event, description) {
28334
+ if (description === undefined)
28335
+ return;
28336
+ return CLASSIFICATION_TABLE.find((c) => c.event === event && c.description === description);
28337
+ }
28338
+ function looksHardBlock(command) {
28339
+ return /\bexit\s+[12]\b/.test(command);
28340
+ }
28341
+ function convertHookCommand(cmd, fieldNode, decision, warnCollector, context) {
28342
+ if (fieldNode === null || decision === "drop-condition") {
28343
+ return { ...cmd };
28344
+ }
28345
+ if (cmd.type !== "command" || typeof cmd.command !== "string") {
28346
+ warnCollector.push(`hooks: ${context} — cannot wrap a non-command hook (type="${cmd.type}") with a stdin guard; condition dropped, hook now fires unconditionally on matcher match`);
28347
+ return { ...cmd };
28348
+ }
28349
+ let jqCond;
28350
+ try {
28351
+ jqCond = fieldNodeToJq(fieldNode);
28352
+ } catch (err) {
28353
+ const message = err instanceof Error ? err.message : String(err);
28354
+ if (looksHardBlock(cmd.command)) {
28355
+ warnCollector.push(`hooks: ${context} — could not translate condition to jq (${message}); hook looks hard-block (exit 1/2) so it was conservatively EXCLUDED from the converted settings — manual review required`);
28356
+ return null;
28357
+ }
28358
+ warnCollector.push(`hooks: ${context} — could not translate condition to jq (${message}); relaxed to unconditional firing (advisory hook, over-firing preferred over silent drop)`);
28359
+ return { ...cmd };
28360
+ }
28361
+ return {
28362
+ ...cmd,
28363
+ command: buildGuardedCommand(jqCond, cmd.command)
28364
+ };
28365
+ }
28366
+ function convertHooksEntry(event, entry, warnCollector) {
28367
+ const context = `${event} / "${entry.description ?? "(no description)"}"`;
28368
+ let analysis;
28369
+ try {
28370
+ analysis = analyzeMatcher(entry.matcher);
28371
+ } catch (err) {
28372
+ const message = err instanceof Error ? err.message : String(err);
28373
+ warnCollector.push(`hooks: ${context} — matcher could not be auto-converted, entry EXCLUDED: ${message}`);
28374
+ return null;
28375
+ }
28376
+ let decision = "auto";
28377
+ if (analysis.fieldNode !== null) {
28378
+ const classification = findClassification(event, entry.description);
28379
+ if (classification) {
28380
+ decision = classification.decision;
28381
+ } else {
28382
+ decision = "wrap-guard";
28383
+ warnCollector.push(`hooks: ${context} — has a tool_input condition not present in CLASSIFICATION_TABLE; defaulted to wrap-guard (safer) — add an explicit classification entry after reading the script`);
28384
+ }
28385
+ }
28386
+ const convertedHooks = entry.hooks.map((cmd) => convertHookCommand(cmd, analysis.fieldNode, decision, warnCollector, context)).filter((cmd) => cmd !== null);
28387
+ if (convertedHooks.length === 0) {
28388
+ return null;
28389
+ }
28390
+ const out = { hooks: convertedHooks };
28391
+ if (analysis.toolRegex !== "*") {
28392
+ out.matcher = analysis.toolRegex;
28393
+ } else if (entry.matcher !== undefined) {
28394
+ out.matcher = "*";
28395
+ }
28396
+ if (entry.description !== undefined) {
28397
+ out.description = entry.description;
28398
+ }
28399
+ return out;
28400
+ }
28401
+ function convertHooksJson(hooksJson) {
28402
+ const warnings = [];
28403
+ const warnCollector = { push: (msg) => warnings.push(msg) };
28404
+ const result = {};
28405
+ for (const [event, entries] of Object.entries(hooksJson.hooks ?? {})) {
28406
+ const convertedEntries = [];
28407
+ for (const entry of entries) {
28408
+ const converted = convertHooksEntry(event, entry, warnCollector);
28409
+ if (converted)
28410
+ convertedEntries.push(converted);
28411
+ }
28412
+ if (convertedEntries.length > 0) {
28413
+ result[event] = convertedEntries;
28414
+ }
28415
+ }
28416
+ return { hooks: result, warnings };
28417
+ }
28418
+ async function mergeHooksIntoSettings(settingsPath, hooksJsonPath) {
28419
+ const hooksJson = await readJsonFile(hooksJsonPath);
28420
+ const { hooks, warnings } = convertHooksJson(hooksJson);
28421
+ let settings = {};
28422
+ try {
28423
+ const existingRaw = await readTextFile(settingsPath);
28424
+ settings = JSON.parse(existingRaw);
28425
+ } catch {
28426
+ settings = {};
28427
+ }
28428
+ settings.hooks = hooks;
28429
+ await ensureDirectory(dirname3(settingsPath));
28430
+ await writeTextFile(settingsPath, `${JSON.stringify(settings, null, 2)}
28431
+ `);
28432
+ return { warnings };
28433
+ }
28434
+
27952
28435
  // src/core/scope-filter.ts
27953
28436
  function getSkillScope(content) {
27954
28437
  const cleaned = content.replace(/^\uFEFF/, "");
@@ -28129,6 +28612,26 @@ async function installSettingsLocal(targetDir, result) {
28129
28612
  await writeJsonFile(settingsPath, statusLineConfig);
28130
28613
  debug("install.settings_local_created", {});
28131
28614
  }
28615
+ async function installHooksSettings(targetDir, _options, result) {
28616
+ const layout = getProviderLayout();
28617
+ const hooksJsonPath = join8(targetDir, getComponentPath("hooks"), "hooks.json");
28618
+ const settingsPath = join8(targetDir, layout.rootDir, "settings.local.json");
28619
+ if (!await fileExists(hooksJsonPath)) {
28620
+ debug("install.hooks_settings_skipped", { reason: "hooks.json not installed" });
28621
+ return;
28622
+ }
28623
+ try {
28624
+ const mergeResult = await mergeHooksIntoSettings(settingsPath, hooksJsonPath);
28625
+ if (mergeResult && Array.isArray(mergeResult.warnings) && mergeResult.warnings.length > 0) {
28626
+ result.warnings.push(...mergeResult.warnings);
28627
+ }
28628
+ debug("install.hooks_settings_merged", {});
28629
+ } catch (err) {
28630
+ const message = err instanceof Error ? err.message : String(err);
28631
+ result.warnings.push(`Failed to merge hooks into settings.local.json: ${message}`);
28632
+ warn("install.hooks_settings_failed", { error: message });
28633
+ }
28634
+ }
28132
28635
  async function installEntryDocWithTracking(targetDir, options, result) {
28133
28636
  const language = options.language ?? DEFAULT_LANGUAGE2;
28134
28637
  const overwrite = !!(options.force || options.backup);
@@ -28187,6 +28690,7 @@ async function install(options) {
28187
28690
  await installStatusline(options.targetDir, options, result);
28188
28691
  await installTestsConfig(options.targetDir, options, result);
28189
28692
  await installSettingsLocal(options.targetDir, result);
28693
+ await installHooksSettings(options.targetDir, options, result);
28190
28694
  await installEntryDocWithTracking(options.targetDir, options, result);
28191
28695
  if (preservation) {
28192
28696
  const layout = getProviderLayout();
@@ -29670,7 +30174,7 @@ async function initCommand(options) {
29670
30174
  }
29671
30175
 
29672
30176
  // src/cli/list.ts
29673
- import { basename as basename5, dirname as dirname3, join as join14, relative as relative3 } from "node:path";
30177
+ import { basename as basename5, dirname as dirname4, join as join14, relative as relative3 } from "node:path";
29674
30178
  init_fs();
29675
30179
  var ALLOWED_TOP_LEVEL_KEYS = new Set(["name", "type", "description", "version", "category"]);
29676
30180
  function parseKeyValue(line) {
@@ -29871,7 +30375,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
29871
30375
  const customSkillPaths = new Set(customComponents.filter((c) => c.type === "skill").map((c) => c.path));
29872
30376
  const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
29873
30377
  const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
29874
- const skillDir = dirname3(skillMdPath);
30378
+ const skillDir = dirname4(skillMdPath);
29875
30379
  const indexYamlPath = join14(skillDir, "index.yaml");
29876
30380
  const { description, version } = await tryReadIndexYamlMetadata(indexYamlPath);
29877
30381
  const relativePath = relative3(targetDir, skillDir);