skillrepo 1.5.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.5.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)) {
package/src/lib/paths.mjs CHANGED
@@ -24,7 +24,10 @@ export const claudeSkillrepoConfig = () => join(cwd(), ".claude", "skillrepo-con
24
24
  export const cursorDir = () => join(cwd(), ".cursor");
25
25
  export const cursorMcpJson = () => join(cwd(), ".cursor", "mcp.json");
26
26
  export const cursorRulesDir = () => join(cwd(), ".cursor", "rules");
27
+ export const cursorHooksDir = () => join(cwd(), ".cursor", "hooks");
27
28
  export const cursorSkillrepoMdc = () => join(cwd(), ".cursor", "rules", "skillrepo.mdc");
29
+ export const cursorSkillrepoIndex = () => join(cwd(), ".cursor", "skillrepo-index.json");
30
+ export const cursorHooksJson = () => join(cwd(), ".cursor", "hooks.json");
28
31
 
29
32
  // Windsurf (always global — no project-level config)
30
33
  export const windsurfDir = () => join(homedir(), ".codeium", "windsurf");
@@ -3,8 +3,21 @@
3
3
  * Calls individual mergers and collects results.
4
4
  */
5
5
 
6
- import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
7
- import { claudeSkillrepoMd, claudeSkillrepoIndex, claudeSkillrepoConfig, cursorSkillrepoMdc } from "./paths.mjs";
6
+ import { resolve } from "node:path";
7
+ import { readFileSafe, writeFileSafe, writeExecutable } from "./fs-utils.mjs";
8
+ import { claudeSkillrepoMd, claudeSkillrepoIndex, claudeSkillrepoConfig } from "./paths.mjs";
9
+
10
+ /**
11
+ * Validate that a server-supplied path stays within an allowed directory.
12
+ * Prevents path traversal attacks from compromised server responses.
13
+ */
14
+ function assertPathContained(filePath, allowedDir) {
15
+ const abs = resolve(process.cwd(), filePath);
16
+ const boundary = resolve(process.cwd(), allowedDir);
17
+ if (!abs.startsWith(boundary + "/") && abs !== boundary) {
18
+ throw new Error(`Server returned path outside ${allowedDir}: ${filePath}`);
19
+ }
20
+ }
8
21
  import { mergeClaudeMcpConfig } from "./mergers/claude-mcp.mjs";
9
22
  import { mergeCursorMcpConfig } from "./mergers/cursor-mcp.mjs";
10
23
  import { mergeWindsurfMcpConfig } from "./mergers/windsurf-mcp.mjs";
@@ -59,10 +72,37 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, payload }) {
59
72
  if (ides.includes("cursor")) {
60
73
  results.push(mergeCursorMcpConfig(mcpUrl));
61
74
 
62
- // Skill mapping file (always refresh with latest)
63
- const cursorMdcExisted = readFileSafe(cursorSkillrepoMdc()) !== null;
64
- writeFileSafe(cursorSkillrepoMdc(), payload.cursor.skillrepoMdc.content);
65
- results.push({ path: ".cursor/rules/skillrepo.mdc", action: cursorMdcExisted ? "updated" : "created" });
75
+ // Write all rule files (per-skill .mdc + aggregate .mdc)
76
+ if (Array.isArray(payload.cursor?.rules)) {
77
+ for (const rule of payload.cursor.rules) {
78
+ assertPathContained(rule.path, ".cursor");
79
+ const existed = readFileSafe(rule.path) !== null;
80
+ writeFileSafe(rule.path, rule.content);
81
+ results.push({ path: rule.path, action: existed ? "updated" : "created" });
82
+ }
83
+ } else if (payload.cursor?.skillrepoMdc) {
84
+ // Legacy fallback: single .mdc file (pre-v2 server)
85
+ const existed = readFileSafe(payload.cursor.skillrepoMdc.path) !== null;
86
+ writeFileSafe(payload.cursor.skillrepoMdc.path, payload.cursor.skillrepoMdc.content);
87
+ results.push({ path: payload.cursor.skillrepoMdc.path, action: existed ? "updated" : "created" });
88
+ }
89
+
90
+ // Cursor hooks and skill index
91
+ if (payload.cursor?.hooksJson) {
92
+ const existed = readFileSafe(payload.cursor.hooksJson.path) !== null;
93
+ writeFileSafe(payload.cursor.hooksJson.path, payload.cursor.hooksJson.content);
94
+ results.push({ path: payload.cursor.hooksJson.path, action: existed ? "updated" : "created" });
95
+ }
96
+ if (payload.cursor?.sessionHook) {
97
+ const existed = readFileSafe(payload.cursor.sessionHook.path) !== null;
98
+ writeExecutable(payload.cursor.sessionHook.path, payload.cursor.sessionHook.content);
99
+ results.push({ path: payload.cursor.sessionHook.path, action: existed ? "updated" : "created" });
100
+ }
101
+ if (payload.cursor?.skillIndex) {
102
+ const existed = readFileSafe(payload.cursor.skillIndex.path) !== null;
103
+ writeFileSafe(payload.cursor.skillIndex.path, payload.cursor.skillIndex.content);
104
+ results.push({ path: payload.cursor.skillIndex.path, action: existed ? "updated" : "created" });
105
+ }
66
106
  }
67
107
 
68
108
  // Windsurf
@@ -0,0 +1,244 @@
1
+ # CLI E2E Test Harness — Handoff
2
+
3
+ ## What This Is
4
+
5
+ A new agent needs to build a CLI E2E test platform for SkillRepo's `npx skillrepo init` command. The feature code is complete and merged (PR #479). The CLI E2E tests are the missing piece — no test code has been written yet, only the branch and directory exist.
6
+
7
+ ## Current State
8
+
9
+ **Branch:** `feat/457-cli-e2e-tests` (off `feat/457-integration`)
10
+ **Directory created:** `packages/cli/src/test/e2e/` (empty)
11
+ **Approved plan:** `/Users/jpace/.claude/plans/valiant-scribbling-whisper.md`
12
+
13
+ ## What Was Built (the feature being tested)
14
+
15
+ Epic #457: "Deterministic Skill Injection — Cross-Integration Architecture"
16
+
17
+ The CLI (`npx skillrepo init`) now generates:
18
+
19
+ **Claude Code files:**
20
+ - `.claude/skillrepo.md` — skill reference
21
+ - `.claude/skillrepo-index.json` — skill index with `contextSignals` (files, project, tasks)
22
+ - `.claude/skillrepo-config.json` — version 2 config with `signals` array and `companions` map
23
+ - `.claude/hooks/skillrepo-sync.mjs` — SessionStart hook (refreshes configs + repo profiling)
24
+ - `.claude/hooks/skillrepo-prompt-match.mjs` — UserPromptSubmit hook (weighted scoring + profile multiplier)
25
+ - `.claude/hooks/skillrepo-pretool-match.mjs` — PreToolUse hook (dynamic signal map + glob matching)
26
+ - `.claude/settings.local.json` — hook registration (merged with existing)
27
+ - `.mcp.json` — MCP server config
28
+
29
+ **Cursor files:**
30
+ - `.cursor/rules/skillrepo-{name}.mdc` — per-skill rules with `globs` for file-based activation
31
+ - `.cursor/rules/skillrepo.mdc` — aggregate `alwaysApply` rule for skills without file signals
32
+ - `.cursor/hooks.json` — registers `sessionStart` hook
33
+ - `.cursor/hooks/skillrepo-session.mjs` — session hook with repo profiling + project-aware context
34
+ - `.cursor/skillrepo-index.json` — skill index (same as Claude Code)
35
+ - `.cursor/mcp.json` — MCP server config
36
+
37
+ **Both:**
38
+ - `.env.local` — `SKILLREPO_ACCESS_KEY=...`
39
+
40
+ ## Key Code Paths
41
+
42
+ | File | What it does |
43
+ |------|-------------|
44
+ | `packages/cli/bin/skillrepo.mjs` | CLI entry point |
45
+ | `packages/cli/src/commands/init.mjs` | Init command: detect IDEs → prompt for key → fetch payload → write configs |
46
+ | `packages/cli/src/lib/http.mjs` | `fetchSetupPayload(apiKey, baseUrl)` — GET `/api/v1/setup` with Bearer auth |
47
+ | `packages/cli/src/lib/write-configs.mjs` | Orchestrates all file writes per IDE, includes path traversal protection |
48
+ | `packages/cli/src/lib/paths.mjs` | All output file path constants |
49
+ | `packages/cli/src/lib/detect-ides.mjs` | Scans for `.claude/`, `.cursor/`, `.vscode/` directories |
50
+ | `packages/cli/src/lib/fs-utils.mjs` | `readFileSafe()`, `writeFileSafe()` |
51
+ | `packages/cli/src/lib/mergers/` | Individual mergers for settings.local.json, mcp.json, etc. |
52
+ | `src/lib/setup/generate.ts` | Server-side: `generateSetupPayload()` produces the full payload |
53
+
54
+ ## The HTTP Contract
55
+
56
+ The CLI makes ONE HTTP call:
57
+
58
+ ```
59
+ GET {baseUrl}/api/v1/setup
60
+ Authorization: Bearer sk_live_...
61
+ Accept: application/json
62
+ User-Agent: skillrepo-cli/1.0.0
63
+ ```
64
+
65
+ Response is a `SetupPayload` JSON object (see `src/lib/setup/generate.ts` for the type).
66
+
67
+ ## What To Build
68
+
69
+ ### Approach: Subprocess + Mock HTTP Server
70
+
71
+ Tests run the actual CLI binary (`node bin/skillrepo.mjs init`) as a child process against a lightweight mock HTTP server. This tests what users actually experience.
72
+
73
+ ### Test Runner
74
+
75
+ Use `node:test` (Node.js built-in) — consistent with existing CLI tests in `packages/cli/src/test/`. Coverage via `c8` wrapping the subprocess.
76
+
77
+ ### Files To Create
78
+
79
+ | File | Purpose |
80
+ |------|---------|
81
+ | `packages/cli/src/test/e2e/mock-server.mjs` | Minimal `node:http` server returning configurable `SetupPayload` |
82
+ | `packages/cli/src/test/e2e/payload-factory.mjs` | Builds realistic payloads with various skill configurations |
83
+ | `packages/cli/src/test/e2e/cli-init.test.mjs` | Full init flow E2E tests |
84
+
85
+ ### Mock Server Requirements
86
+
87
+ - Listens on random available port
88
+ - Serves `GET /api/v1/setup` with configurable payload
89
+ - Validates `Authorization: Bearer sk_live_...` header
90
+ - Returns 401 for invalid/missing keys
91
+ - Returns 200 with payload for valid keys
92
+ - Shuts down cleanly after tests
93
+
94
+ ### Payload Factory Requirements
95
+
96
+ Must generate realistic `SetupPayload` objects matching what `generateSetupPayload()` produces. Include:
97
+
98
+ 1. Skills WITH contextSignals (files, project, tasks populated)
99
+ 2. Skills WITHOUT contextSignals (null)
100
+ 3. Skills with special characters in descriptions (colons, quotes — YAML safety)
101
+ 4. Configurable skill count
102
+ 5. Both Claude Code and Cursor output structures
103
+
104
+ Reference `src/lib/setup/generate.ts` for the exact `SetupPayload` type:
105
+
106
+ ```typescript
107
+ interface SetupPayload {
108
+ skillCount: number;
109
+ mcpUrl: string;
110
+ skillEntries: string[];
111
+ claudeCode: {
112
+ skillrepoMd: { path: string; content: string };
113
+ syncHook: { path: string; content: string };
114
+ settingsHooks: { hooks: Record<string, unknown> };
115
+ skillIndex: { path: string; content: string };
116
+ skillrepoConfig: { path: string; content: string };
117
+ promptHook: { path: string; content: string };
118
+ preToolHook: { path: string; content: string };
119
+ };
120
+ cursor: {
121
+ rules: Array<{ path: string; content: string }>;
122
+ hooksJson: { path: string; content: string };
123
+ sessionHook: { path: string; content: string };
124
+ skillIndex: { path: string; content: string };
125
+ };
126
+ }
127
+ ```
128
+
129
+ ### Test Cases (goal-driven, not implementation-driven)
130
+
131
+ Tests verify OUTCOMES ("does this produce a working setup?") not IMPLEMENTATION ("does the string contain this function name?").
132
+
133
+ **1. Full Init Flow**
134
+ - `init --yes --url http://localhost:{port} --key sk_live_test` in a temp dir produces all expected files
135
+ - Running init twice produces correct merge behavior (updated, not duplicated)
136
+ - Invalid API key returns clean auth error message
137
+ - Unreachable server returns clean network error message
138
+
139
+ **2. Generated Files Are Structurally Valid**
140
+ - All JSON files (`skillrepo-config.json`, `skillrepo-index.json`, `hooks.json`, `settings.local.json`, `mcp.json`) parse without error
141
+ - YAML frontmatter in `.mdc` files parses correctly (including descriptions with colons)
142
+ - Hook scripts compile as valid JavaScript (`vm.compileFunction` or `new Function()`)
143
+
144
+ **3. Signal Map Works End-to-End**
145
+ - Extract `matchGlob` from the generated PreToolUse hook script, eval it, and test against real file paths:
146
+ - `**/*.tsx` matches `src/components/Button.tsx` ✓
147
+ - `**/*.tsx` does NOT match `src/utils/helper.ts` ✗
148
+ - `**/*.test.*` matches `tests/foo.test.ts` ✓
149
+ - `**/api/**/route.ts` matches `src/app/api/users/route.ts` ✓
150
+ - `**/*.{ts,tsx}` does not crash (regex metachar escaping) ✓
151
+ - Extract `extractFilePath` and verify it parses tool input correctly
152
+ - Extract `scoreSkills` and verify: task signals = 3x weight, keywords = 1x
153
+ - Extract `applyProfileMultiplier` and verify: matching project = 1.5x, no match = 0.3x, no tags = 1.0x
154
+
155
+ **4. Cursor Output Goals**
156
+ - Skills with `contextSignals.files` produce per-skill `.mdc` with `globs` frontmatter
157
+ - Skills without file signals go to aggregate `skillrepo.mdc` with `alwaysApply: true`
158
+ - No skill appears in both per-skill AND aggregate files
159
+ - `hooks.json` is valid JSON with `sessionStart` event
160
+ - Session hook script produces valid `additional_context` JSON when executed
161
+
162
+ **5. Claude Code Output Goals**
163
+ - Config has `version: 2` and parseable `signals` array
164
+ - Signal entries have toolName, files, project, tasks fields
165
+ - Companions are correctly resolved
166
+ - All three hooks registered in `settings.local.json`
167
+ - Sync hook contains repo profiling logic
168
+
169
+ **6. Repo Profiling**
170
+ - In a simulated Next.js project (temp dir with `package.json` containing `next`, `react`), the sync hook's profiling code detects the correct frameworks
171
+ - Execute the profiling section of the generated sync hook and verify the output
172
+
173
+ **7. Security**
174
+ - Server-supplied `.mdc` path outside `.cursor/` is rejected with error (path traversal protection)
175
+
176
+ ### How To Extract and Test Generated Functions
177
+
178
+ The generated hook scripts are JavaScript strings. To test functions like `matchGlob`:
179
+
180
+ ```javascript
181
+ import { readFileSync } from 'fs';
182
+
183
+ // Read the generated hook script
184
+ const script = readFileSync('.claude/hooks/skillrepo-pretool-match.mjs', 'utf-8');
185
+
186
+ // Extract matchGlob function
187
+ const matchGlobMatch = script.match(/function matchGlob\(filePath, pattern\) \{[\s\S]*?\n\}/);
188
+ const matchGlobBody = matchGlobMatch[0]
189
+ .replace(/^function matchGlob\(filePath, pattern\) \{/, '')
190
+ .replace(/\}$/, '');
191
+ const matchGlob = new Function('filePath', 'pattern', matchGlobBody);
192
+
193
+ // Now test it
194
+ assert.strictEqual(matchGlob('src/components/Button.tsx', '**/*.tsx'), true);
195
+ assert.strictEqual(matchGlob('src/utils/helper.ts', '**/*.tsx'), false);
196
+ ```
197
+
198
+ ### Existing Test Patterns to Follow
199
+
200
+ Look at `packages/cli/src/test/` for the established patterns:
201
+ - `detect-ides.test.mjs` — temp directory creation, `process.cwd()` override
202
+ - `mergers/hooks-json.test.mjs` — file merge testing (19 test cases)
203
+ - `env-local.test.mjs` — `.env.local` merge behavior
204
+
205
+ All use `node:test`, `node:assert`, `mkdtempSync`, `rmSync` for cleanup.
206
+
207
+ ### Coverage
208
+
209
+ Use `c8` to instrument the CLI subprocess. Add to root `package.json`:
210
+
211
+ ```json
212
+ "test:cli-e2e": "cd packages/cli && c8 node --test src/test/e2e/*.test.mjs"
213
+ ```
214
+
215
+ Coverage output should be merged into the overall codecov upload in `.github/workflows/e2e.yml`.
216
+
217
+ ### Running the Tests
218
+
219
+ ```bash
220
+ # From repo root
221
+ cd packages/cli && node --test src/test/e2e/cli-init.test.mjs
222
+
223
+ # With coverage
224
+ cd packages/cli && c8 node --test src/test/e2e/cli-init.test.mjs
225
+ ```
226
+
227
+ ## Important Rules (from CLAUDE.md and memory)
228
+
229
+ - NEVER edit `.claude/` generated files directly — modify generators in `src/lib/setup/generate.ts`
230
+ - NEVER run tests against production — require explicit `DATABASE_URL_E2E` (this harness doesn't need a DB since it mocks the HTTP server)
231
+ - Always use GitHub Issues, never Asana
232
+ - Run `npm run check` before considering work complete
233
+ - CLI is at `packages/cli/` — it's a standalone package with its own `package.json`
234
+ - CLI version is currently 1.6.0 — bump if CLI code changes
235
+
236
+ ## References
237
+
238
+ - Epic: https://github.com/atxpace/skill-repo/issues/457
239
+ - Integration PR (merged): https://github.com/atxpace/skill-repo/pull/479
240
+ - Review fixes PR: https://github.com/atxpace/skill-repo/pull/480
241
+ - Approved plan: `/Users/jpace/.claude/plans/valiant-scribbling-whisper.md`
242
+ - Existing CLI tests: `packages/cli/src/test/`
243
+ - Server-side generator: `src/lib/setup/generate.ts`
244
+ - CLI write orchestrator: `packages/cli/src/lib/write-configs.mjs`