skillrepo 1.6.0 → 1.6.1

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.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Set up SkillRepo in any IDE — one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,7 @@
3
3
  * Creates directories as needed, handles errors cleanly.
4
4
  */
5
5
 
6
- import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
6
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from "node:fs";
7
7
  import { dirname } from "node:path";
8
8
 
9
9
  /**
@@ -35,6 +35,15 @@ export function writeFileSafe(filePath, content) {
35
35
  writeFileSync(filePath, content, "utf-8");
36
36
  }
37
37
 
38
+ /**
39
+ * Write a file and mark it executable (0o755).
40
+ * Used for hook scripts that have shebangs and are invoked directly.
41
+ */
42
+ export function writeExecutable(filePath, content) {
43
+ writeFileSafe(filePath, content);
44
+ chmodSync(filePath, 0o755);
45
+ }
46
+
38
47
  /**
39
48
  * Check if a path exists (file or directory).
40
49
  */
@@ -8,13 +8,21 @@
8
8
  */
9
9
 
10
10
  import { unlinkSync } from "node:fs";
11
- import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
11
+ import { readFileSafe, writeFileSafe, writeExecutable } from "../fs-utils.mjs";
12
12
  import { claudeSettingsLocal, claudeSyncHook, claudePromptHook, claudePreToolHook, claudeHooksDir } from "../paths.mjs";
13
13
  import { join } from "node:path";
14
14
 
15
- const DEFAULT_SYNC_COMMAND = "node .claude/hooks/skillrepo-sync.mjs";
16
- const DEFAULT_PROMPT_COMMAND = "node .claude/hooks/skillrepo-prompt-match.mjs";
17
- const DEFAULT_PRETOOL_COMMAND = "node .claude/hooks/skillrepo-pretool-match.mjs";
15
+ const DEFAULT_SYNC_COMMAND = ".claude/hooks/skillrepo-sync.mjs";
16
+ const DEFAULT_PROMPT_COMMAND = ".claude/hooks/skillrepo-prompt-match.mjs";
17
+ const DEFAULT_PRETOOL_COMMAND = ".claude/hooks/skillrepo-pretool-match.mjs";
18
+
19
+ // Legacy commands used before v1.6.1 — recognized during migration so
20
+ // re-running init replaces them rather than duplicating.
21
+ const LEGACY_COMMANDS = new Set([
22
+ "node .claude/hooks/skillrepo-sync.mjs",
23
+ "node .claude/hooks/skillrepo-prompt-match.mjs",
24
+ "node .claude/hooks/skillrepo-pretool-match.mjs",
25
+ ]);
18
26
 
19
27
  /**
20
28
  * Extract a hook command from a hooks config group, falling back to a default.
@@ -46,6 +54,26 @@ function hasCommand(groups, command) {
46
54
  );
47
55
  }
48
56
 
57
+ /**
58
+ * Replace legacy `node .claude/hooks/...` commands with the new direct-execution
59
+ * format. Returns true if any replacement was made.
60
+ */
61
+ function replaceLegacyCommands(groups) {
62
+ if (!Array.isArray(groups)) return false;
63
+ let replaced = false;
64
+ for (const group of groups) {
65
+ if (!Array.isArray(group.hooks)) continue;
66
+ for (const hook of group.hooks) {
67
+ if (LEGACY_COMMANDS.has(hook.command)) {
68
+ // Strip the "node " prefix → ".claude/hooks/..."
69
+ hook.command = hook.command.replace(/^node /, "");
70
+ replaced = true;
71
+ }
72
+ }
73
+ }
74
+ return replaced;
75
+ }
76
+
49
77
  /**
50
78
  * Merge SkillRepo hooks into .claude/settings.local.json and write hook scripts.
51
79
  * @param {object} hooksConfig - The hooks config object from the server payload (settingsHooks.hooks)
@@ -60,16 +88,17 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
60
88
  const preToolCommand = extractCommand(hooksConfig, "PreToolUse", DEFAULT_PRETOOL_COMMAND);
61
89
  const results = [];
62
90
 
63
- // Always write hook scripts (latest version from server)
91
+ // Always write hook scripts (latest version from server) and mark executable.
92
+ // Scripts have #!/usr/bin/env node shebangs and are invoked directly (not via `node`).
64
93
  const syncExisted = readFileSafe(claudeSyncHook()) !== null;
65
- writeFileSafe(claudeSyncHook(), syncHookContent);
94
+ writeExecutable(claudeSyncHook(), syncHookContent);
66
95
  results.push({
67
96
  path: ".claude/hooks/skillrepo-sync.mjs",
68
97
  action: syncExisted ? "updated" : "created",
69
98
  });
70
99
 
71
100
  const promptExisted = readFileSafe(claudePromptHook()) !== null;
72
- writeFileSafe(claudePromptHook(), promptHookContent);
101
+ writeExecutable(claudePromptHook(), promptHookContent);
73
102
  results.push({
74
103
  path: ".claude/hooks/skillrepo-prompt-match.mjs",
75
104
  action: promptExisted ? "updated" : "created",
@@ -77,7 +106,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
77
106
 
78
107
  if (preToolHookContent) {
79
108
  const preToolExisted = readFileSafe(claudePreToolHook()) !== null;
80
- writeFileSafe(claudePreToolHook(), preToolHookContent);
109
+ writeExecutable(claudePreToolHook(), preToolHookContent);
81
110
  results.push({
82
111
  path: ".claude/hooks/skillrepo-pretool-match.mjs",
83
112
  action: preToolExisted ? "updated" : "created",
@@ -111,6 +140,13 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
111
140
  if (!config.hooks) config.hooks = {};
112
141
  let changed = false;
113
142
 
143
+ // Migrate legacy "node .claude/hooks/..." commands to direct execution
144
+ for (const event of ["SessionStart", "UserPromptSubmit", "PreToolUse"]) {
145
+ if (Array.isArray(config.hooks[event]) && replaceLegacyCommands(config.hooks[event])) {
146
+ changed = true;
147
+ }
148
+ }
149
+
114
150
  // Merge SessionStart
115
151
  if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
116
152
  if (!hasCommand(config.hooks.SessionStart, syncCommand)) {
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { resolve } from "node:path";
7
- import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
7
+ import { readFileSafe, writeFileSafe, writeExecutable } from "./fs-utils.mjs";
8
8
  import { claudeSkillrepoMd, claudeSkillrepoIndex, claudeSkillrepoConfig } from "./paths.mjs";
9
9
 
10
10
  /**
@@ -95,7 +95,7 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, payload }) {
95
95
  }
96
96
  if (payload.cursor?.sessionHook) {
97
97
  const existed = readFileSafe(payload.cursor.sessionHook.path) !== null;
98
- writeFileSafe(payload.cursor.sessionHook.path, payload.cursor.sessionHook.content);
98
+ writeExecutable(payload.cursor.sessionHook.path, payload.cursor.sessionHook.content);
99
99
  results.push({ path: payload.cursor.sessionHook.path, action: existed ? "updated" : "created" });
100
100
  }
101
101
  if (payload.cursor?.skillIndex) {
@@ -187,7 +187,7 @@ function buildSyncHook(baseUrl) {
187
187
  return `#!/usr/bin/env node
188
188
  // SkillRepo SessionStart hook — auto-refreshes skill config files.
189
189
  // Installed by npx skillrepo init. Commit this file to your repo.
190
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
190
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
191
191
  import { join, dirname } from "path";
192
192
 
193
193
  const SETUP_URL = "${setupUrl}";
@@ -223,6 +223,11 @@ function safeWrite(relPath, content) {
223
223
  writeFileSync(p, content, "utf-8");
224
224
  }
225
225
 
226
+ function writeExecutable(relPath, content) {
227
+ safeWrite(relPath, content);
228
+ chmodSync(join(process.cwd(), relPath), 0o755);
229
+ }
230
+
226
231
  try {
227
232
  const res = await fetch(SETUP_URL, {
228
233
  headers: { Authorization: \`Bearer \${API_KEY}\`, Accept: "application/json" },
@@ -234,8 +239,8 @@ try {
234
239
  if (cc.skillrepoMd?.content) safeWrite(cc.skillrepoMd.path, cc.skillrepoMd.content);
235
240
  if (cc.skillIndex?.content) safeWrite(cc.skillIndex.path, cc.skillIndex.content);
236
241
  if (cc.skillrepoConfig?.content) safeWrite(cc.skillrepoConfig.path, cc.skillrepoConfig.content);
237
- if (cc.promptHook?.content) safeWrite(cc.promptHook.path, cc.promptHook.content);
238
- if (cc.preToolHook?.content) safeWrite(cc.preToolHook.path, cc.preToolHook.content);
242
+ if (cc.promptHook?.content) writeExecutable(cc.promptHook.path, cc.promptHook.content);
243
+ if (cc.preToolHook?.content) writeExecutable(cc.preToolHook.path, cc.preToolHook.content);
239
244
  } catch { /* silently fail */ }
240
245
 
241
246
  // Repo profiling
@@ -473,12 +478,13 @@ import { tmpdir } from "os";
473
478
 
474
479
  const CONTENT_URL = "${contentUrl}";
475
480
 
476
- let hookConfig = { maxTotalBytes: 10000, reinjectionLineThreshold: 50 };
481
+ let hookConfig = { maxTotalBytes: 10000, reinjectionLineThreshold: 50, maxSkillsPerPrompt: 3 };
477
482
  try {
478
483
  const cfg = JSON.parse(readFileSync(join(process.cwd(), ".claude", "skillrepo-config.json"), "utf-8"));
479
484
  if (cfg.hookInjection) hookConfig = { ...hookConfig, ...cfg.hookInjection };
480
485
  } catch {}
481
486
  const MAX_BYTES = hookConfig.maxTotalBytes;
487
+ const MAX_SKILLS = hookConfig.maxSkillsPerPrompt;
482
488
  const REINJECT_THRESHOLD = hookConfig.reinjectionLineThreshold ?? 50;
483
489
 
484
490
  let dynamicSignals = [];
@@ -489,13 +495,8 @@ try {
489
495
  }
490
496
  } catch {}
491
497
 
492
- const FALLBACK_SIGNALS = [
493
- { toolName: "skill__affaan-m__frontend-patterns", files: ["**/*.tsx", "**/*.jsx", "**/*.css"], project: [], tasks: [] },
494
- { toolName: "skill__affaan-m__e2e-testing", files: ["**/*.test.*", "**/*.spec.*"], project: [], tasks: [] },
495
- { toolName: "skill__atxpace__gh-issue-workflow", files: [], project: [], tasks: ["github issue", "pull request"] },
496
- { toolName: "skill__affaan-m__api-routes", files: ["**/api/**/route.ts", "**/api/**/route.tsx"], project: [], tasks: [] },
497
- ];
498
- const SIGNALS = dynamicSignals.length > 0 ? dynamicSignals : FALLBACK_SIGNALS;
498
+ const SIGNALS = dynamicSignals;
499
+ if (SIGNALS.length === 0) { process.stdout.write("{}"); process.exit(0); }
499
500
 
500
501
  // Minimal glob matcher — self-contained, no npm deps.
501
502
  function matchGlob(filePath, pattern) {
@@ -621,7 +622,7 @@ if (matchedSkills.length === 0) { process.stdout.write("{}"); process.exit(0); }
621
622
 
622
623
  const { path: statePath, state } = readState(sessionId);
623
624
  const currentLines = countTranscriptLines(transcriptPath);
624
- const candidates = matchedSkills.filter((s) => shouldReinject(s.toolName, state, currentLines));
625
+ const candidates = matchedSkills.filter((s) => shouldReinject(s.toolName, state, currentLines)).slice(0, MAX_SKILLS);
625
626
  if (candidates.length === 0) { process.stdout.write("{}"); process.exit(0); }
626
627
 
627
628
  const contents = [];
@@ -675,21 +676,21 @@ function buildSettingsHooks() {
675
676
  {
676
677
  matcher: "startup|resume",
677
678
  hooks: [
678
- { type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" },
679
+ { type: "command", command: ".claude/hooks/skillrepo-sync.mjs" },
679
680
  ],
680
681
  },
681
682
  ],
682
683
  UserPromptSubmit: [
683
684
  {
684
685
  hooks: [
685
- { type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" },
686
+ { type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" },
686
687
  ],
687
688
  },
688
689
  ],
689
690
  PreToolUse: [
690
691
  {
691
692
  hooks: [
692
- { type: "command", command: "node .claude/hooks/skillrepo-pretool-match.mjs" },
693
+ { type: "command", command: ".claude/hooks/skillrepo-pretool-match.mjs" },
693
694
  ],
694
695
  },
695
696
  ],
@@ -766,7 +767,7 @@ function buildCursorHooksJson() {
766
767
  hooks: [
767
768
  {
768
769
  event: "sessionStart",
769
- command: "node .cursor/hooks/skillrepo-session.mjs",
770
+ command: ".cursor/hooks/skillrepo-session.mjs",
770
771
  },
771
772
  ],
772
773
  }, null, 2) + "\n";
@@ -23,9 +23,9 @@ const PROMPT_CONTENT = "// prompt hook";
23
23
  const PRETOOL_CONTENT = "// pretool hook";
24
24
 
25
25
  const HOOKS_CONFIG = {
26
- SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
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" }] }],
26
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
27
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
28
+ PreToolUse: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-pretool-match.mjs" }] }],
29
29
  };
30
30
 
31
31
  describe("Hooks settings.local.json merger", () => {
@@ -86,8 +86,8 @@ describe("Hooks settings.local.json merger", () => {
86
86
  // Pre-existing with both hooks already
87
87
  const existing = {
88
88
  hooks: {
89
- SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
90
- UserPromptSubmit: [{ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }] }],
89
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
90
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
91
91
  },
92
92
  };
93
93
  mkdirSync(join(tempDir, ".claude"), { recursive: true });
@@ -105,7 +105,7 @@ describe("Hooks settings.local.json merger", () => {
105
105
  // Only SessionStart installed
106
106
  const existing = {
107
107
  hooks: {
108
- SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
108
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
109
109
  },
110
110
  };
111
111
  mkdirSync(join(tempDir, ".claude"), { recursive: true });
@@ -119,7 +119,7 @@ describe("Hooks settings.local.json merger", () => {
119
119
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
120
120
  assert.equal(config.hooks.SessionStart.length, 1);
121
121
  assert.equal(config.hooks.UserPromptSubmit.length, 1);
122
- assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, "node .claude/hooks/skillrepo-prompt-match.mjs");
122
+ assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, ".claude/hooks/skillrepo-prompt-match.mjs");
123
123
  });
124
124
 
125
125
  it("always updates hook scripts even when settings merge is skipped", async () => {
@@ -132,8 +132,8 @@ describe("Hooks settings.local.json merger", () => {
132
132
  mkdirSync(join(tempDir, ".claude"), { recursive: true });
133
133
  const existing = {
134
134
  hooks: {
135
- SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }] }],
136
- UserPromptSubmit: [{ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }] }],
135
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
136
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
137
137
  },
138
138
  };
139
139
  writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(existing, null, 2));
@@ -203,7 +203,7 @@ describe("Hooks settings.local.json merger", () => {
203
203
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
204
204
  assert.ok(Array.isArray(config.hooks.SessionStart), "SessionStart should be an array");
205
205
  assert.equal(config.hooks.SessionStart.length, 1);
206
- assert.equal(config.hooks.SessionStart[0].hooks[0].command, "node .claude/hooks/skillrepo-sync.mjs");
206
+ assert.equal(config.hooks.SessionStart[0].hooks[0].command, ".claude/hooks/skillrepo-sync.mjs");
207
207
  });
208
208
 
209
209
  it("documents partial write when JSON parse fails (hook scripts written before throw)", async () => {
@@ -244,7 +244,7 @@ describe("Hooks settings.local.json merger", () => {
244
244
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
245
245
  assert.ok(Array.isArray(config.hooks.PreToolUse), "PreToolUse should be an array");
246
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");
247
+ assert.equal(config.hooks.PreToolUse[0].hooks[0].command, ".claude/hooks/skillrepo-pretool-match.mjs");
248
248
  });
249
249
 
250
250
  it("skips pretool hook when preToolHookContent is not provided", async () => {
@@ -270,4 +270,43 @@ describe("Hooks settings.local.json merger", () => {
270
270
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
271
271
  assert.equal(config.hooks.PreToolUse.length, 1, "should not duplicate PreToolUse entry");
272
272
  });
273
+
274
+ it("migrates legacy 'node .claude/hooks/...' commands to direct execution", async () => {
275
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
276
+
277
+ // Pre-existing settings with OLD command format (node .claude/hooks/...)
278
+ const oldSettings = {
279
+ hooks: {
280
+ SessionStart: [{
281
+ matcher: "startup|resume",
282
+ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-sync.mjs" }]
283
+ }],
284
+ UserPromptSubmit: [{
285
+ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-prompt-match.mjs" }]
286
+ }],
287
+ PreToolUse: [{
288
+ hooks: [{ type: "command", command: "node .claude/hooks/skillrepo-pretool-match.mjs" }]
289
+ }]
290
+ }
291
+ };
292
+ mkdirSync(join(tempDir, ".claude"), { recursive: true });
293
+ writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(oldSettings, null, 2));
294
+ mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
295
+
296
+ // Run merger with NEW hook config (HOOKS_CONFIG uses .claude/hooks/... without "node" prefix)
297
+ const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT, PRETOOL_CONTENT);
298
+
299
+ // Settings should show "merged" (not "created" or "skipped")
300
+ const settingsResult = results.find((r) => r.path.includes("settings.local.json"));
301
+ assert.equal(settingsResult.action, "merged");
302
+
303
+ // Read the result and verify: ONE entry per event, with NEW command format
304
+ const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
305
+ assert.equal(config.hooks.SessionStart.length, 1);
306
+ assert.equal(config.hooks.SessionStart[0].hooks[0].command, ".claude/hooks/skillrepo-sync.mjs");
307
+ assert.equal(config.hooks.UserPromptSubmit.length, 1);
308
+ assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, ".claude/hooks/skillrepo-prompt-match.mjs");
309
+ assert.equal(config.hooks.PreToolUse.length, 1);
310
+ assert.equal(config.hooks.PreToolUse[0].hooks[0].command, ".claude/hooks/skillrepo-pretool-match.mjs");
311
+ });
273
312
  });