@adonis0123/skill-development 1.0.8 → 1.0.9

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/install-skill.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -24,8 +25,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
25
 
25
26
  // shared/src/install-skill.ts
26
27
  var import_fs3 = __toESM(require("fs"));
27
- var import_path3 = __toESM(require("path"));
28
- var import_os3 = __toESM(require("os"));
28
+ var import_path4 = __toESM(require("path"));
29
+ var import_os4 = __toESM(require("os"));
29
30
  var import_child_process = require("child_process");
30
31
 
31
32
  // shared/src/utils.ts
@@ -161,7 +162,12 @@ ${body}`;
161
162
  var import_fs2 = __toESM(require("fs"));
162
163
  var import_path2 = __toESM(require("path"));
163
164
  var import_os2 = __toESM(require("os"));
165
+ var CLAUDE_SETTINGS_PATH_ENV = "CLAUDE_CODE_SETTINGS_PATH";
164
166
  function getClaudeSettingsPath() {
167
+ const overridePath = process.env[CLAUDE_SETTINGS_PATH_ENV];
168
+ if (overridePath && overridePath.trim()) {
169
+ return overridePath.trim();
170
+ }
165
171
  return import_path2.default.join(import_os2.default.homedir(), ".claude", "settings.json");
166
172
  }
167
173
  function readClaudeSettings() {
@@ -185,8 +191,49 @@ function writeClaudeSettings(settings) {
185
191
  }
186
192
  import_fs2.default.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
187
193
  }
188
- function hookMatcherExists(existingHooks, newMatcher) {
189
- return existingHooks.some((hook) => hook.matcher === newMatcher.matcher);
194
+ function normalizeHook(hook) {
195
+ if (!hook || typeof hook !== "object") return null;
196
+ const command = hook.command;
197
+ if (typeof command !== "string" || !command.trim()) return null;
198
+ return { type: "command", command };
199
+ }
200
+ function normalizeHooks(hooks) {
201
+ if (!Array.isArray(hooks)) return [];
202
+ const normalized = [];
203
+ for (const h of hooks) {
204
+ const nh = normalizeHook(h);
205
+ if (nh) normalized.push(nh);
206
+ }
207
+ return normalized;
208
+ }
209
+ function getHookKey(hook) {
210
+ return hook.command;
211
+ }
212
+ function mergeHooks(existing, incoming) {
213
+ const merged = [];
214
+ const seen = /* @__PURE__ */ new Set();
215
+ let didChange = false;
216
+ for (const hook of existing) {
217
+ const key = getHookKey(hook);
218
+ if (!seen.has(key)) {
219
+ merged.push(hook);
220
+ seen.add(key);
221
+ } else {
222
+ didChange = true;
223
+ }
224
+ }
225
+ for (const hook of incoming) {
226
+ const key = getHookKey(hook);
227
+ if (!seen.has(key)) {
228
+ merged.push(hook);
229
+ seen.add(key);
230
+ didChange = true;
231
+ }
232
+ }
233
+ return { merged, didChange };
234
+ }
235
+ function findMatcherIndex(existingHooks, matcher) {
236
+ return existingHooks.findIndex((hook) => hook && hook.matcher === matcher);
190
237
  }
191
238
  function addClaudeHooks(hooksConfig, skillName) {
192
239
  const settings = readClaudeSettings();
@@ -204,12 +251,30 @@ function addClaudeHooks(hooksConfig, skillName) {
204
251
  }
205
252
  const existingHooks = hooks[hookType];
206
253
  for (const matcher of hookMatchers) {
207
- if (!hookMatcherExists(existingHooks, matcher)) {
208
- existingHooks.push(matcher);
209
- modified = true;
210
- console.log(` \u2713 Added ${hookType} hook for ${skillName}`);
254
+ const idx = findMatcherIndex(existingHooks, matcher.matcher);
255
+ if (idx === -1) {
256
+ const normalized = {
257
+ matcher: matcher.matcher,
258
+ hooks: normalizeHooks(matcher.hooks)
259
+ };
260
+ if (normalized.hooks.length > 0) {
261
+ existingHooks.push(normalized);
262
+ modified = true;
263
+ console.log(` \u2713 Added ${hookType} hook for ${skillName}`);
264
+ }
211
265
  } else {
212
- console.log(` \u2139 ${hookType} hook already exists, skipping`);
266
+ const existingMatcher = existingHooks[idx];
267
+ const existingMatcherHooks = normalizeHooks(existingMatcher.hooks);
268
+ const incomingHooks = normalizeHooks(matcher.hooks);
269
+ const { merged, didChange } = mergeHooks(existingMatcherHooks, incomingHooks);
270
+ const normalizedChanged = Array.isArray(existingMatcher.hooks) && JSON.stringify(existingMatcher.hooks) !== JSON.stringify(merged);
271
+ if (didChange || normalizedChanged) {
272
+ existingHooks[idx] = { ...existingMatcher, hooks: merged };
273
+ modified = true;
274
+ console.log(` \u2713 Merged ${hookType} hooks for ${skillName} (matcher: ${matcher.matcher})`);
275
+ } else {
276
+ console.log(` \u2139 ${hookType} hook already exists, skipping`);
277
+ }
213
278
  }
214
279
  }
215
280
  hooks[hookType] = existingHooks;
@@ -220,7 +285,263 @@ function addClaudeHooks(hooksConfig, skillName) {
220
285
  return modified;
221
286
  }
222
287
 
288
+ // shared/src/remote-update-checker-script.ts
289
+ var import_path3 = __toESM(require("path"));
290
+ var import_os3 = __toESM(require("os"));
291
+ var REMOTE_UPDATE_CHECKER_VERSION = "1";
292
+ function getRemoteUpdateCheckerScriptContents() {
293
+ const version = REMOTE_UPDATE_CHECKER_VERSION;
294
+ return `#!/usr/bin/env node
295
+ /* remote-skill-update-checker v${version} */
296
+
297
+ const fs = require('fs');
298
+ const path = require('path');
299
+ const os = require('os');
300
+ const https = require('https');
301
+
302
+ const DEFAULT_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 1 day
303
+
304
+ function parseArgs(argv) {
305
+ const args = new Set(argv.slice(2));
306
+ return {
307
+ force: args.has('--force') || args.has('-f'),
308
+ verbose: args.has('--verbose') || args.has('-v'),
309
+ };
310
+ }
311
+
312
+ function readJson(filePath) {
313
+ try {
314
+ if (!fs.existsSync(filePath)) return null;
315
+ const txt = fs.readFileSync(filePath, 'utf-8');
316
+ return JSON.parse(txt);
317
+ } catch {
318
+ return null;
319
+ }
320
+ }
321
+
322
+ function writeJson(filePath, obj) {
323
+ try {
324
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
325
+ fs.writeFileSync(filePath, JSON.stringify(obj, null, 2), 'utf-8');
326
+ } catch {
327
+ // ignore
328
+ }
329
+ }
330
+
331
+ function getManifestPaths() {
332
+ const cwd = process.cwd();
333
+ return Array.from(
334
+ new Set([
335
+ path.join(os.homedir(), '.claude', 'skills', '.skills-manifest.json'),
336
+ path.join(cwd, '.claude', 'skills', '.skills-manifest.json'),
337
+ ])
338
+ );
339
+ }
340
+
341
+ function getRemoteSourcesFromManifest(manifest) {
342
+ const skills = manifest && typeof manifest.skills === 'object' ? manifest.skills : {};
343
+ const sources = [];
344
+ for (const v of Object.values(skills || {})) {
345
+ if (!v || typeof v !== 'object') continue;
346
+ const s = v.source;
347
+ if (typeof s === 'string' && s.trim()) sources.push(s.trim());
348
+ }
349
+ return Array.from(new Set(sources));
350
+ }
351
+
352
+ function parseRemoteSource(remoteSource) {
353
+ const trimmed = String(remoteSource || '')
354
+ .trim()
355
+ .replace(/^https?:\\/\\/github\\.com\\//, '');
356
+ const parts = trimmed.split('/').filter(Boolean);
357
+ if (parts.length < 2) return null;
358
+ const owner = parts[0];
359
+ const repo = parts[1];
360
+ const repoPath = parts.slice(2).join('/') || null;
361
+ return { owner, repo, repoPath };
362
+ }
363
+
364
+ function buildCommitsUrl(remoteSource) {
365
+ const parsed = parseRemoteSource(remoteSource);
366
+ if (!parsed) return null;
367
+ const base =
368
+ 'https://api.github.com/repos/' +
369
+ encodeURIComponent(parsed.owner) +
370
+ '/' +
371
+ encodeURIComponent(parsed.repo) +
372
+ '/commits?per_page=1';
373
+ if (!parsed.repoPath) return base;
374
+ return base + '&path=' + encodeURIComponent(parsed.repoPath);
375
+ }
376
+
377
+ function shouldSkipByCooldown(lastCheckedAtIso, now, cooldownMs) {
378
+ if (!lastCheckedAtIso) return false;
379
+ const last = new Date(lastCheckedAtIso);
380
+ if (Number.isNaN(last.getTime())) return false;
381
+ return now.getTime() - last.getTime() < cooldownMs;
382
+ }
383
+
384
+ function fetchText(url) {
385
+ return new Promise((resolve, reject) => {
386
+ const req = https.request(
387
+ url,
388
+ {
389
+ method: 'GET',
390
+ headers: {
391
+ 'user-agent': 'claude-skills-remote-update-checker',
392
+ accept: 'application/vnd.github+json',
393
+ },
394
+ },
395
+ (res) => {
396
+ let data = '';
397
+ res.setEncoding('utf8');
398
+ res.on('data', (chunk) => (data += chunk));
399
+ res.on('end', () => {
400
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
401
+ resolve(data);
402
+ } else {
403
+ reject(new Error('HTTP ' + (res.statusCode || 0)));
404
+ }
405
+ });
406
+ }
407
+ );
408
+ req.on('error', reject);
409
+ req.end();
410
+ });
411
+ }
412
+
413
+ function extractLatestSha(jsonText) {
414
+ try {
415
+ const data = JSON.parse(jsonText);
416
+ if (Array.isArray(data) && data[0] && typeof data[0] === 'object' && typeof data[0].sha === 'string') {
417
+ return data[0].sha || null;
418
+ }
419
+ return null;
420
+ } catch {
421
+ return null;
422
+ }
423
+ }
424
+
425
+ async function main() {
426
+ const { force, verbose } = parseArgs(process.argv);
427
+ const now = new Date();
428
+ const cooldownMs = DEFAULT_COOLDOWN_MS;
429
+
430
+ const manifestPaths = getManifestPaths();
431
+ const updates = [];
432
+
433
+ for (const manifestPath of manifestPaths) {
434
+ const manifest = readJson(manifestPath) || {};
435
+ const sources = getRemoteSourcesFromManifest(manifest);
436
+ if (!sources.length) continue;
437
+
438
+ manifest.remoteCache = manifest.remoteCache && typeof manifest.remoteCache === 'object' ? manifest.remoteCache : {};
439
+
440
+ for (const remoteSource of sources) {
441
+ const prevEntry = manifest.remoteCache[remoteSource] || {};
442
+ const prevSha = prevEntry.lastSeenSha || null;
443
+
444
+ const url = buildCommitsUrl(remoteSource);
445
+ if (!url) continue;
446
+
447
+ if (!force && shouldSkipByCooldown(prevEntry.lastCheckedAt, now, cooldownMs)) {
448
+ continue;
449
+ }
450
+
451
+ try {
452
+ const text = await fetchText(url);
453
+ const latestSha = extractLatestSha(text);
454
+
455
+ manifest.remoteCache[remoteSource] = {
456
+ ...prevEntry,
457
+ lastCheckedAt: now.toISOString(),
458
+ ...(latestSha ? { lastSeenSha: latestSha } : {}),
459
+ };
460
+ writeJson(manifestPath, manifest);
461
+
462
+ // Baseline: first time seeing sha => do not notify
463
+ if (!prevSha && latestSha) continue;
464
+
465
+ if (prevSha && latestSha && prevSha !== latestSha) {
466
+ updates.push({ remoteSource, prevSha, latestSha });
467
+ }
468
+ } catch (e) {
469
+ manifest.remoteCache[remoteSource] = {
470
+ ...prevEntry,
471
+ lastCheckedAt: now.toISOString(),
472
+ };
473
+ writeJson(manifestPath, manifest);
474
+ if (verbose) {
475
+ console.log('[remote-skill-check] error', remoteSource, String(e && e.message ? e.message : e));
476
+ }
477
+ }
478
+ }
479
+ }
480
+
481
+ if (updates.length > 0) {
482
+ console.log('\\n\u{1F4E1} \u68C0\u6D4B\u5230\u8FDC\u7A0B skill \u6709\u66F4\u65B0\uFF1A');
483
+ for (const u of updates) {
484
+ console.log(' -', u.remoteSource);
485
+ console.log(' ', (u.prevSha || '').slice(0, 7), '->', (u.latestSha || '').slice(0, 7));
486
+ }
487
+ console.log('\\n\u{1F4A1} \u5EFA\u8BAE\u91CD\u65B0\u5B89\u88C5\u5BF9\u5E94 skill \u4EE5\u62C9\u53D6\u6700\u65B0\u7248\u672C\uFF08remoteSource \u6A21\u5F0F\u4E0D\u4F1A\u81EA\u52A8\u66F4\u65B0\uFF09\u3002\\n');
488
+ process.exitCode = 0;
489
+ }
490
+ }
491
+
492
+ main().catch(() => {
493
+ // ignore
494
+ });
495
+ `;
496
+ }
497
+ function getRemoteUpdateCheckerScriptInstallPath() {
498
+ const scriptsDir = import_path3.default.join(import_os3.default.homedir(), ".claude", "scripts");
499
+ return import_path3.default.join(scriptsDir, "remote-skill-update-check.js");
500
+ }
501
+
223
502
  // shared/src/install-skill.ts
503
+ function ensureRemoteUpdateCheckerInstalled() {
504
+ const scriptPath = getRemoteUpdateCheckerScriptInstallPath();
505
+ const scriptsDir = import_path4.default.dirname(scriptPath);
506
+ ensureDir(scriptsDir);
507
+ const marker = `/* remote-skill-update-checker v${REMOTE_UPDATE_CHECKER_VERSION} */`;
508
+ if (import_fs3.default.existsSync(scriptPath)) {
509
+ try {
510
+ const existing = import_fs3.default.readFileSync(scriptPath, "utf-8");
511
+ if (existing.includes(marker)) {
512
+ return scriptPath;
513
+ }
514
+ } catch {
515
+ }
516
+ }
517
+ const contents = getRemoteUpdateCheckerScriptContents();
518
+ import_fs3.default.writeFileSync(scriptPath, contents, { encoding: "utf-8" });
519
+ try {
520
+ import_fs3.default.chmodSync(scriptPath, 493);
521
+ } catch {
522
+ }
523
+ return scriptPath;
524
+ }
525
+ function ensureRemoteUpdateSessionEndHook(scriptPath) {
526
+ const command = `node "${scriptPath}"`;
527
+ addClaudeHooks(
528
+ {
529
+ SessionEnd: [
530
+ {
531
+ matcher: "*",
532
+ hooks: [
533
+ {
534
+ command,
535
+ // Claude Code settings schema expects "command"
536
+ type: "command"
537
+ }
538
+ ]
539
+ }
540
+ ]
541
+ },
542
+ "remote-update-checker"
543
+ );
544
+ }
224
545
  function fetchFromRemote(tempDir, remoteSource) {
225
546
  try {
226
547
  console.log(` \u{1F310} Fetching latest from ${remoteSource}...`);
@@ -228,7 +549,7 @@ function fetchFromRemote(tempDir, remoteSource) {
228
549
  stdio: "pipe",
229
550
  timeout: 6e4
230
551
  });
231
- if (import_fs3.default.existsSync(import_path3.default.join(tempDir, "SKILL.md"))) {
552
+ if (import_fs3.default.existsSync(import_path4.default.join(tempDir, "SKILL.md"))) {
232
553
  console.log(" \u2713 Fetched latest version from remote");
233
554
  return true;
234
555
  }
@@ -250,7 +571,7 @@ function getSourceDir(config, packageDir) {
250
571
  isRemote: false
251
572
  };
252
573
  }
253
- const tempDir = import_path3.default.join(import_os3.default.tmpdir(), `skill-fetch-${Date.now()}`);
574
+ const tempDir = import_path4.default.join(import_os4.default.tmpdir(), `skill-fetch-${Date.now()}`);
254
575
  const remoteSuccess = fetchFromRemote(tempDir, config.remoteSource);
255
576
  if (remoteSuccess) {
256
577
  return {
@@ -276,7 +597,7 @@ function getSourceDir(config, packageDir) {
276
597
  };
277
598
  }
278
599
  function updateManifest(skillsDir, config, targetName, isRemote) {
279
- const manifestPath = import_path3.default.join(skillsDir, ".skills-manifest.json");
600
+ const manifestPath = import_path4.default.join(skillsDir, ".skills-manifest.json");
280
601
  let manifest = { skills: {} };
281
602
  if (import_fs3.default.existsSync(manifestPath)) {
282
603
  try {
@@ -291,7 +612,7 @@ function updateManifest(skillsDir, config, targetName, isRemote) {
291
612
  version: config.version,
292
613
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
293
614
  package: config.package || config.name,
294
- path: import_path3.default.join(skillsDir, skillName),
615
+ path: import_path4.default.join(skillsDir, skillName),
295
616
  target: targetName,
296
617
  ...config.remoteSource && { source: config.remoteSource }
297
618
  };
@@ -304,8 +625,8 @@ function installToTarget(target, config, sourceDir, isRemote) {
304
625
  const isGlobal = isGlobalInstall();
305
626
  const location = detectInstallLocation(target.paths, isGlobal);
306
627
  const skillName = extractSkillName(config.name);
307
- const targetDir = import_path3.default.join(location.base, skillName);
308
- const altTargetDir = import_path3.default.join(location.base, config.name);
628
+ const targetDir = import_path4.default.join(location.base, skillName);
629
+ const altTargetDir = import_path4.default.join(location.base, config.name);
309
630
  console.log(` Type: ${location.type}${isGlobal ? " (global)" : " (project)"}`);
310
631
  console.log(` Directory: ${targetDir}`);
311
632
  if (import_fs3.default.existsSync(altTargetDir) && altTargetDir !== targetDir) {
@@ -314,37 +635,46 @@ function installToTarget(target, config, sourceDir, isRemote) {
314
635
  console.log(` \u2713 Removed directory: ${config.name}`);
315
636
  }
316
637
  ensureDir(targetDir);
317
- const skillMdSource = import_path3.default.join(sourceDir, "SKILL.md");
638
+ const skillMdSource = import_path4.default.join(sourceDir, "SKILL.md");
318
639
  if (!import_fs3.default.existsSync(skillMdSource)) {
319
640
  throw new Error("SKILL.md is required but not found");
320
641
  }
321
- import_fs3.default.copyFileSync(skillMdSource, import_path3.default.join(targetDir, "SKILL.md"));
642
+ import_fs3.default.copyFileSync(skillMdSource, import_path4.default.join(targetDir, "SKILL.md"));
322
643
  console.log(" \u2713 Copied SKILL.md");
323
644
  if (isRemote && config.remoteSource) {
324
- patchSkillMdName(import_path3.default.join(targetDir, "SKILL.md"), config.name);
645
+ patchSkillMdName(import_path4.default.join(targetDir, "SKILL.md"), config.name);
325
646
  }
326
647
  const filesToCopy = config.files || {};
327
648
  for (const [source, dest] of Object.entries(filesToCopy)) {
328
649
  if (source === "SKILL.md") {
329
650
  continue;
330
651
  }
331
- const sourcePath = import_path3.default.join(sourceDir, source);
652
+ const sourcePath = import_path4.default.join(sourceDir, source);
332
653
  if (!import_fs3.default.existsSync(sourcePath)) {
333
654
  console.warn(` \u26A0 Warning: ${source} not found, skipping`);
334
655
  continue;
335
656
  }
336
- const destPath = import_path3.default.join(targetDir, dest);
657
+ const destPath = import_path4.default.join(targetDir, dest);
337
658
  if (import_fs3.default.statSync(sourcePath).isDirectory()) {
338
659
  copyDir(sourcePath, destPath);
339
660
  console.log(` \u2713 Copied directory: ${source}`);
340
661
  } else {
341
- const destDir = import_path3.default.dirname(destPath);
662
+ const destDir = import_path4.default.dirname(destPath);
342
663
  ensureDir(destDir);
343
664
  import_fs3.default.copyFileSync(sourcePath, destPath);
344
665
  console.log(` \u2713 Copied file: ${source}`);
345
666
  }
346
667
  }
347
668
  updateManifest(location.base, config, target.name, isRemote);
669
+ if (target.name === "claude-code" && config.remoteSource) {
670
+ try {
671
+ const checkerPath = ensureRemoteUpdateCheckerInstalled();
672
+ ensureRemoteUpdateSessionEndHook(checkerPath);
673
+ } catch (error) {
674
+ const message = error instanceof Error ? error.message : String(error);
675
+ console.warn(` \u26A0 \u8B66\u544A: \u65E0\u6CD5\u914D\u7F6E\u8FDC\u7A0B\u66F4\u65B0\u68C0\u67E5\uFF08\u53EF\u5B89\u5168\u5FFD\u7565\uFF09: ${message}`);
676
+ }
677
+ }
348
678
  if (target.name === "claude-code" && ((_a = config.claudeSettings) == null ? void 0 : _a.hooks)) {
349
679
  console.log(" \u{1F527} \u914D\u7F6E Claude Code \u94A9\u5B50...");
350
680
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adonis0123/skill-development",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Claude Code Skill - 技能开发指南,提供创建有效技能的完整流程和最佳实践。安装时自动从 Anthropic 官方仓库拉取最新版本。",
5
5
  "scripts": {
6
6
  "postinstall": "node install-skill.js",
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ "use strict";
2
3
  var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -100,7 +101,12 @@ function readSkillConfig(dir) {
100
101
  var import_fs2 = __toESM(require("fs"));
101
102
  var import_path2 = __toESM(require("path"));
102
103
  var import_os2 = __toESM(require("os"));
104
+ var CLAUDE_SETTINGS_PATH_ENV = "CLAUDE_CODE_SETTINGS_PATH";
103
105
  function getClaudeSettingsPath() {
106
+ const overridePath = process.env[CLAUDE_SETTINGS_PATH_ENV];
107
+ if (overridePath && overridePath.trim()) {
108
+ return overridePath.trim();
109
+ }
104
110
  return import_path2.default.join(import_os2.default.homedir(), ".claude", "settings.json");
105
111
  }
106
112
  function readClaudeSettings() {
@@ -124,6 +130,32 @@ function writeClaudeSettings(settings) {
124
130
  }
125
131
  import_fs2.default.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
126
132
  }
133
+ function normalizeHook(hook) {
134
+ if (!hook || typeof hook !== "object") return null;
135
+ const command = hook.command;
136
+ if (typeof command !== "string" || !command.trim()) return null;
137
+ return { type: "command", command };
138
+ }
139
+ function normalizeHooks(hooks) {
140
+ if (!Array.isArray(hooks)) return [];
141
+ const normalized = [];
142
+ for (const h of hooks) {
143
+ const nh = normalizeHook(h);
144
+ if (nh) normalized.push(nh);
145
+ }
146
+ return normalized;
147
+ }
148
+ function getHookKey(hook) {
149
+ return hook.command;
150
+ }
151
+ function removeHooks(existing, toRemove) {
152
+ const removeKeys = new Set(toRemove.map(getHookKey));
153
+ const remaining = existing.filter((h) => !removeKeys.has(getHookKey(h)));
154
+ return { remaining, didChange: remaining.length !== existing.length };
155
+ }
156
+ function findMatcherIndex(existingHooks, matcher) {
157
+ return existingHooks.findIndex((hook) => hook && hook.matcher === matcher);
158
+ }
127
159
  function removeClaudeHooks(hooksConfig, skillName) {
128
160
  const settings = readClaudeSettings();
129
161
  let modified = false;
@@ -139,16 +171,35 @@ function removeClaudeHooks(hooksConfig, skillName) {
139
171
  continue;
140
172
  }
141
173
  const existingHooks = hooks[hookType];
142
- const initialLength = existingHooks.length;
143
- const matchersToRemove = hookMatchers.map((m) => m.matcher);
144
- const filteredHooks = existingHooks.filter(
145
- (hook) => !matchersToRemove.includes(hook.matcher)
146
- );
147
- if (filteredHooks.length < initialLength) {
148
- hooks[hookType] = filteredHooks;
149
- modified = true;
150
- console.log(` \u2713 Removed ${hookType} hook for ${skillName}`);
174
+ for (const matcher of hookMatchers) {
175
+ const idx = findMatcherIndex(existingHooks, matcher.matcher);
176
+ if (idx === -1) {
177
+ continue;
178
+ }
179
+ const existingMatcher = existingHooks[idx];
180
+ const existingMatcherHooks = normalizeHooks(existingMatcher.hooks);
181
+ const toRemoveHooks = normalizeHooks(matcher.hooks);
182
+ const { remaining, didChange } = removeHooks(existingMatcherHooks, toRemoveHooks);
183
+ if (didChange) {
184
+ modified = true;
185
+ if (remaining.length === 0) {
186
+ existingHooks.splice(idx, 1);
187
+ console.log(
188
+ ` \u2713 Removed ${hookType} matcher for ${skillName} (matcher: ${matcher.matcher})`
189
+ );
190
+ } else {
191
+ existingHooks[idx] = { ...existingMatcher, hooks: remaining };
192
+ console.log(` \u2713 Removed ${hookType} hooks for ${skillName} (matcher: ${matcher.matcher})`);
193
+ }
194
+ } else {
195
+ const normalizedChanged = Array.isArray(existingMatcher.hooks) && JSON.stringify(existingMatcher.hooks) !== JSON.stringify(existingMatcherHooks);
196
+ if (normalizedChanged) {
197
+ existingHooks[idx] = { ...existingMatcher, hooks: existingMatcherHooks };
198
+ modified = true;
199
+ }
200
+ }
151
201
  }
202
+ hooks[hookType] = existingHooks;
152
203
  }
153
204
  if (modified) {
154
205
  writeClaudeSettings(settings);