skillrepo 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Set up SkillRepo in any IDE — one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,11 +9,12 @@
9
9
 
10
10
  import { unlinkSync } from "node:fs";
11
11
  import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
12
- import { claudeSettingsLocal, claudeSyncHook, claudePromptHook, claudeHooksDir } from "../paths.mjs";
12
+ import { claudeSettingsLocal, claudeSyncHook, claudePromptHook, claudePreToolHook, claudeHooksDir } from "../paths.mjs";
13
13
  import { join } from "node:path";
14
14
 
15
15
  const DEFAULT_SYNC_COMMAND = "node .claude/hooks/skillrepo-sync.mjs";
16
16
  const DEFAULT_PROMPT_COMMAND = "node .claude/hooks/skillrepo-prompt-match.mjs";
17
+ const DEFAULT_PRETOOL_COMMAND = "node .claude/hooks/skillrepo-pretool-match.mjs";
17
18
 
18
19
  /**
19
20
  * Extract a hook command from a hooks config group, falling back to a default.
@@ -50,11 +51,13 @@ function hasCommand(groups, command) {
50
51
  * @param {object} hooksConfig - The hooks config object from the server payload (settingsHooks.hooks)
51
52
  * @param {string} syncHookContent - The sync hook script content
52
53
  * @param {string} promptHookContent - The prompt-match hook script content
54
+ * @param {string} [preToolHookContent] - The pretool-match hook script content (optional for backward compat)
53
55
  * @returns {{ results: { path: string; action: string }[] }}
54
56
  */
55
- export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent) {
57
+ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent, preToolHookContent) {
56
58
  const syncCommand = extractCommand(hooksConfig, "SessionStart", DEFAULT_SYNC_COMMAND);
57
59
  const promptCommand = extractCommand(hooksConfig, "UserPromptSubmit", DEFAULT_PROMPT_COMMAND);
60
+ const preToolCommand = extractCommand(hooksConfig, "PreToolUse", DEFAULT_PRETOOL_COMMAND);
58
61
  const results = [];
59
62
 
60
63
  // Always write hook scripts (latest version from server)
@@ -72,6 +75,15 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
72
75
  action: promptExisted ? "updated" : "created",
73
76
  });
74
77
 
78
+ if (preToolHookContent) {
79
+ const preToolExisted = readFileSafe(claudePreToolHook()) !== null;
80
+ writeFileSafe(claudePreToolHook(), preToolHookContent);
81
+ results.push({
82
+ path: ".claude/hooks/skillrepo-pretool-match.mjs",
83
+ action: preToolExisted ? "updated" : "created",
84
+ });
85
+ }
86
+
75
87
  // Clean up stale .claude/hooks/hooks.json if it exists
76
88
  const staleHooksJson = join(claudeHooksDir(), "hooks.json");
77
89
  try {
@@ -118,6 +130,17 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
118
130
  changed = true;
119
131
  }
120
132
 
133
+ // Merge PreToolUse
134
+ if (preToolHookContent) {
135
+ if (!Array.isArray(config.hooks.PreToolUse)) config.hooks.PreToolUse = [];
136
+ if (!hasCommand(config.hooks.PreToolUse, preToolCommand)) {
137
+ config.hooks.PreToolUse.push({
138
+ hooks: [{ type: "command", command: preToolCommand }],
139
+ });
140
+ changed = true;
141
+ }
142
+ }
143
+
121
144
  if (existing === null) {
122
145
  writeFileSafe(filePath, JSON.stringify(config, null, 2) + "\n");
123
146
  results.push({ path: ".claude/settings.local.json", action: "created" });
package/src/lib/paths.mjs CHANGED
@@ -15,8 +15,10 @@ export const claudeHooksDir = () => join(cwd(), ".claude", "hooks");
15
15
  export const claudeSettingsLocal = () => join(cwd(), ".claude", "settings.local.json");
16
16
  export const claudeSyncHook = () => join(cwd(), ".claude", "hooks", "skillrepo-sync.mjs");
17
17
  export const claudePromptHook = () => join(cwd(), ".claude", "hooks", "skillrepo-prompt-match.mjs");
18
+ export const claudePreToolHook = () => join(cwd(), ".claude", "hooks", "skillrepo-pretool-match.mjs");
18
19
  export const claudeSkillrepoMd = () => join(cwd(), ".claude", "skillrepo.md");
19
20
  export const claudeSkillrepoIndex = () => join(cwd(), ".claude", "skillrepo-index.json");
21
+ export const claudeSkillrepoConfig = () => join(cwd(), ".claude", "skillrepo-config.json");
20
22
 
21
23
  // Cursor
22
24
  export const cursorDir = () => join(cwd(), ".cursor");
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
7
- import { claudeSkillrepoMd, claudeSkillrepoIndex, cursorSkillrepoMdc } from "./paths.mjs";
7
+ import { claudeSkillrepoMd, claudeSkillrepoIndex, claudeSkillrepoConfig, cursorSkillrepoMdc } from "./paths.mjs";
8
8
  import { mergeClaudeMcpConfig } from "./mergers/claude-mcp.mjs";
9
9
  import { mergeCursorMcpConfig } from "./mergers/cursor-mcp.mjs";
10
10
  import { mergeWindsurfMcpConfig } from "./mergers/windsurf-mcp.mjs";
@@ -38,11 +38,19 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, payload }) {
38
38
  writeFileSafe(claudeSkillrepoIndex(), payload.claudeCode.skillIndex.content);
39
39
  results.push({ path: ".claude/skillrepo-index.json", action: indexExisted ? "updated" : "created" });
40
40
 
41
- // Hooks (SessionStart sync + UserPromptSubmit prompt-match)
41
+ // Hook config (injection limits + companion map)
42
+ if (payload.claudeCode.skillrepoConfig?.content) {
43
+ const configExisted = readFileSafe(claudeSkillrepoConfig()) !== null;
44
+ writeFileSafe(claudeSkillrepoConfig(), payload.claudeCode.skillrepoConfig.content);
45
+ results.push({ path: ".claude/skillrepo-config.json", action: configExisted ? "updated" : "created" });
46
+ }
47
+
48
+ // Hooks (SessionStart sync + UserPromptSubmit prompt-match + PreToolUse pretool-match)
42
49
  const hookResults = mergeHooksConfig(
43
50
  payload.claudeCode.settingsHooks.hooks,
44
51
  payload.claudeCode.syncHook.content,
45
- payload.claudeCode.promptHook.content
52
+ payload.claudeCode.promptHook.content,
53
+ payload.claudeCode.preToolHook?.content
46
54
  );
47
55
  results.push(...hookResults.results);
48
56
  }
@@ -20,10 +20,12 @@ afterEach(() => {
20
20
 
21
21
  const SYNC_CONTENT = "// sync hook";
22
22
  const PROMPT_CONTENT = "// prompt hook";
23
+ const PRETOOL_CONTENT = "// pretool hook";
23
24
 
24
25
  const HOOKS_CONFIG = {
25
26
  SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
26
27
  UserPromptSubmit: [{ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }] }],
28
+ PreToolUse: [{ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-pretool-match.mjs" }] }],
27
29
  };
28
30
 
29
31
  describe("Hooks settings.local.json merger", () => {
@@ -225,4 +227,47 @@ describe("Hooks settings.local.json merger", () => {
225
227
  const syncOnDisk = readFileSync(join(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "utf-8");
226
228
  assert.equal(syncOnDisk, "// new sync content", "sync script should have new content (partial write)");
227
229
  });
230
+
231
+ it("writes pretool hook script when preToolHookContent is provided", async () => {
232
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
233
+ const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT, PRETOOL_CONTENT);
234
+
235
+ const preToolResult = results.find((r) => r.path.includes("pretool"));
236
+ assert.equal(preToolResult.action, "created");
237
+ assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-pretool-match.mjs"), "utf-8"), PRETOOL_CONTENT);
238
+ });
239
+
240
+ it("merges PreToolUse hook into settings.local.json", async () => {
241
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
242
+ mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT, PRETOOL_CONTENT);
243
+
244
+ const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
245
+ assert.ok(Array.isArray(config.hooks.PreToolUse), "PreToolUse should be an array");
246
+ assert.equal(config.hooks.PreToolUse.length, 1);
247
+ assert.equal(config.hooks.PreToolUse[0].hooks[0].command, "node .claude/hooks/skillrepo-pretool-match.mjs");
248
+ });
249
+
250
+ it("skips pretool hook when preToolHookContent is not provided", async () => {
251
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
252
+ const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT);
253
+
254
+ const preToolResult = results.find((r) => r.path.includes("pretool"));
255
+ assert.equal(preToolResult, undefined, "no pretool result when content not provided");
256
+ assert.equal(existsSync(join(tempDir, ".claude/hooks/skillrepo-pretool-match.mjs")), false);
257
+ });
258
+
259
+ it("does not duplicate PreToolUse when already installed", async () => {
260
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
261
+
262
+ // First install
263
+ mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT, PRETOOL_CONTENT);
264
+ // Second install
265
+ const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT, PRETOOL_CONTENT);
266
+
267
+ const settingsResult = results.find((r) => r.path.includes("settings.local.json"));
268
+ assert.equal(settingsResult.action, "skipped");
269
+
270
+ const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
271
+ assert.equal(config.hooks.PreToolUse.length, 1, "should not duplicate PreToolUse entry");
272
+ });
228
273
  });