claude-git-hooks 2.43.0 → 2.44.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/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ Todos los cambios notables en este proyecto se documentarán en este archivo.
5
5
  El formato está basado en [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  y este proyecto adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.44.0] - 2026-05-04
9
+
10
+ ### ✨ Added
11
+ - New `HELP_NAVIGATE.md` prompt template for the first-pass catalog navigation (SUE-152)
12
+ - Auto-discovery of `.library/` catalog files (by-code, by-domain, by-task-type shelves) for AI help context
13
+
14
+ ### 🔧 Changed
15
+ - Rewrote AI help command to use a two-pass librarian approach navigating local `.library/` instead of single-pass CLAUDE.md lookup (SUE-152)
16
+ - Pass 1 sends project catalog to Claude; if deeper detail is needed, Pass 2 reads specific book files from local disk — no GitHub API calls
17
+ - Updated `HELP_QUERY.md` template to accept catalog + book content instead of flat documentation
18
+ - Replaced `NEED_MORE_CONTEXT: file1, file2` protocol with structured `ANSWER:` / `BOOKS:` response format
19
+ - Updated `report-issue` flow to use internal `_readPackageFile()` helper instead of the removed `readClaudeMd()`
20
+ - Updated unit tests to cover two-pass librarian flow, catalog mocking, and book-read scenarios
21
+
22
+ ### 🔒 Security
23
+ - Added path traversal protection when reading LLM-requested book paths — resolved paths are verified against the package root before disk access
24
+
25
+ ### 🗑️ Removed
26
+ - Removed GitHub API dependency (`fetchFileContent`, `parseGitHubRepo`) from AI help flow — all context is now read from local disk
27
+ - Removed `readClaudeMd()` single-file reader in favor of `readLibraryCatalog()` multi-file catalog builder
28
+
29
+
8
30
  ## [2.43.0] - 2026-04-30
9
31
 
10
32
  ### ✨ Added
package/README.md CHANGED
@@ -295,7 +295,7 @@ claude-hooks telemetry clear # Clear data
295
295
  ### Help & Issue Reporting
296
296
 
297
297
  ```bash
298
- claude-hooks help "how do presets work?" # AI-powered help (uses CLAUDE.md)
298
+ claude-hooks help "how do presets work?" # AI-powered help (navigates .library/)
299
299
  claude-hooks help --report-issue # Interactive GitHub issue creation
300
300
  claude-hooks --help # Static command reference
301
301
  ```
@@ -355,7 +355,7 @@ claude-hooks update # Update to latest version
355
355
  | `debug.js` | **Debug toggle** - enable/disable verbose logging | `runSetDebug()` |
356
356
  | `telemetry-cmd.js` | **Telemetry commands** - show/clear statistics | `runShowTelemetry()`, `runClearTelemetry()` |
357
357
  | `diff-batch-info.js` | **Batch info** - orchestration config + per-model speed telemetry | `runDiffBatchInfo()` |
358
- | `help.js` | **Help, AI help, report-issue** | `runShowHelp()`, `showStaticHelp()`, `runShowVersion()` |
358
+ | `help.js` | **Help, AI librarian, report-issue** | `runShowHelp()`, `showStaticHelp()`, `runShowVersion()` |
359
359
 
360
360
  ### Utility Modules (`lib/utils/`)
361
361
 
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Features:
6
6
  * - Dynamic help from command registry (no args, --help, -h)
7
- * - AI help: routes questions to Claude with CLAUDE.md as knowledge base
7
+ * - AI help: two-pass librarian navigating .library/ for answers
8
8
  * - Report issue: interactive issue creation guided by Claude
9
9
  */
10
10
 
@@ -160,59 +160,130 @@ function printAiResponse(response, source) {
160
160
  console.log('');
161
161
  }
162
162
 
163
+ /** Package root directory — resolved once from __dirname */
164
+ const _packageRoot = path.join(__dirname, '..', '..');
165
+
163
166
  /**
164
- * Read CLAUDE.md from package root
165
- * Why: CLAUDE.md is the primary knowledge source shipped with the npm package
167
+ * Catalog directories within .library/ to auto-discover
168
+ * Why: These contain index and shelf files that form the navigational catalog.
169
+ * books/ is excluded — those are fetched on-demand in Pass 2.
170
+ */
171
+ const _CATALOG_DIRS = ['by-code', 'by-domain', 'by-task-type'];
172
+
173
+ /**
174
+ * Read a single file from package root, returning null on failure
166
175
  *
167
- * @returns {Promise<string|null>} CLAUDE.md content or null
176
+ * @param {string} relativePath - Path relative to package root
177
+ * @returns {Promise<string|null>} File content or null
168
178
  */
169
- const readClaudeMd = async () => {
170
- const claudeMdPath = path.join(__dirname, '..', '..', 'CLAUDE.md');
179
+ const _readPackageFile = async (relativePath) => {
171
180
  try {
172
- const content = await fs.readFile(claudeMdPath, 'utf8');
173
- logger.debug('help - readClaudeMd', 'CLAUDE.md loaded', { length: content.length });
174
- return content;
175
- } catch (error) {
176
- logger.debug('help - readClaudeMd', 'CLAUDE.md not found', {
177
- path: claudeMdPath,
178
- error: error.message
179
- });
181
+ return await fs.readFile(path.join(_packageRoot, relativePath), 'utf8');
182
+ } catch {
183
+ return null;
184
+ }
185
+ };
186
+
187
+ /**
188
+ * Read the project catalog from .library/ and CLAUDE.md
189
+ * Why: The catalog provides navigational context for the AI librarian (Pass 1).
190
+ * Auto-discovers index/shelf files so the catalog stays current as .library/ grows.
191
+ *
192
+ * Reads:
193
+ * - CLAUDE.md (global rules)
194
+ * - .library/index.md (repo identity, capabilities)
195
+ * - .library/conventions.md (coding standards)
196
+ * - .library/by-code/*.md (source-path shelves)
197
+ * - .library/by-domain/*.md (workflow reading lists)
198
+ * - .library/by-task-type/*.md (task guidance)
199
+ *
200
+ * @returns {Promise<string|null>} Concatenated catalog or null if nothing could be read
201
+ */
202
+ const readLibraryCatalog = async () => {
203
+ const sections = [];
204
+
205
+ // Fixed files: CLAUDE.md + core library files
206
+ const fixedFiles = [
207
+ { label: 'CLAUDE.md', path: 'CLAUDE.md' },
208
+ { label: '.library/index.md', path: '.library/index.md' },
209
+ { label: '.library/conventions.md', path: '.library/conventions.md' }
210
+ ];
211
+
212
+ for (const file of fixedFiles) {
213
+ const content = await _readPackageFile(file.path);
214
+ if (content) {
215
+ sections.push(`--- ${file.label} ---\n${content}`);
216
+ }
217
+ }
218
+
219
+ // Auto-discover .md files in each catalog directory
220
+ for (const dir of _CATALOG_DIRS) {
221
+ const dirPath = path.join(_packageRoot, '.library', dir);
222
+ let entries;
223
+ try {
224
+ entries = await fs.readdir(dirPath);
225
+ } catch {
226
+ logger.debug('help - readLibraryCatalog', `Could not read .library/${dir}/`);
227
+ continue;
228
+ }
229
+
230
+ const mdFiles = entries.filter((f) => f.endsWith('.md')).sort();
231
+ for (const file of mdFiles) {
232
+ const relativePath = `.library/${dir}/${file}`;
233
+ const content = await _readPackageFile(relativePath);
234
+ if (content) {
235
+ sections.push(`--- ${relativePath} ---\n${content}`);
236
+ }
237
+ }
238
+ }
239
+
240
+ if (sections.length === 0) {
241
+ logger.debug('help - readLibraryCatalog', 'No catalog files found');
180
242
  return null;
181
243
  }
244
+
245
+ const catalog = sections.join('\n\n');
246
+ logger.debug('help - readLibraryCatalog', 'Catalog loaded', {
247
+ sections: sections.length,
248
+ length: catalog.length
249
+ });
250
+ return catalog;
182
251
  };
183
252
 
184
253
  /**
185
- * AI-powered help: routes question to Claude with CLAUDE.md context
186
- * Why: Provides intelligent answers about claude-hooks based on documentation
254
+ * AI-powered help: two-pass librarian navigating .library/
255
+ * Why: Provides intelligent answers by reading the project catalog, then
256
+ * fetching specific books from local disk only when deeper detail is needed.
187
257
  *
188
258
  * Flow:
189
- * 1. Read CLAUDE.md from package root
190
- * 2. Load HELP_QUERY.md template
191
- * 3. Call Claude with 30s timeout
192
- * 4. If NEED_MORE_CONTEXT: fetch referenced files from GitHub, re-query
193
- * 5. Display response
194
- * 6. On any error: fall back to static help
259
+ * 1. Read catalog from .library/ (auto-discovered indexes + shelves)
260
+ * 2. Pass 1: Send catalog to Claude via HELP_NAVIGATE.md
261
+ * 3. If ANSWER: display directly (1 Claude call)
262
+ * 4. If BOOKS: read requested books from local disk, send to HELP_QUERY.md (2 Claude calls)
263
+ * 5. On any error: fall back to static help
195
264
  *
196
265
  * @param {string} question - User's natural language question
197
266
  */
198
267
  async function runAiHelp(question) {
199
268
  try {
200
- const claudeMdContent = await readClaudeMd();
201
- if (!claudeMdContent) {
269
+ const catalog = await readLibraryCatalog();
270
+ if (!catalog) {
202
271
  logger.debug(
203
272
  'help - runAiHelp',
204
- 'CLAUDE.md not available, falling back to static help'
273
+ 'Catalog not available, falling back to static help'
205
274
  );
206
275
  showStaticHelp();
207
276
  return;
208
277
  }
209
278
 
210
- const prompt = await loadPrompt('HELP_QUERY.md', {
211
- DOCUMENTATION: claudeMdContent,
279
+ const prompt = await loadPrompt('HELP_NAVIGATE.md', {
280
+ CATALOG: catalog,
212
281
  QUESTION: question
213
282
  });
214
283
 
215
- logger.debug('help - runAiHelp', 'Querying Claude', { questionLength: question.length });
284
+ logger.debug('help - runAiHelp', 'Querying Claude (Pass 1)', {
285
+ questionLength: question.length
286
+ });
216
287
  console.log('\nSearching documentation...\n');
217
288
 
218
289
  const pkg = getPackageJson();
@@ -221,95 +292,98 @@ async function runAiHelp(question) {
221
292
  const response = await executeClaudeWithRetry(prompt, { timeout: 60000 });
222
293
  const trimmedResponse = response.trim();
223
294
 
224
- // Check for NEED_MORE_CONTEXT second pass
225
- const needMoreLine = trimmedResponse
226
- .split('\n')
227
- .find((l) => l.includes('NEED_MORE_CONTEXT'));
228
- if (needMoreLine) {
229
- const enrichedResponse = await handleNeedMoreContext(
230
- needMoreLine,
295
+ // Check for BOOKS: second pass
296
+ const booksLine = trimmedResponse.split('\n').find((l) => l.startsWith('BOOKS:'));
297
+ if (booksLine) {
298
+ const enrichedAnswer = await readAndAnswerWithBooks(
299
+ booksLine,
231
300
  question,
232
- claudeMdContent
301
+ catalog
233
302
  );
234
- if (enrichedResponse) {
235
- printAiResponse(enrichedResponse, `${localVersion} + source`);
236
- return;
237
- }
238
- // Enrichment failed: show first-pass answer with marker line stripped
239
- const cleanResponse = trimmedResponse
240
- .split('\n')
241
- .filter((l) => !l.includes('NEED_MORE_CONTEXT'))
242
- .join('\n')
243
- .trim();
244
- if (cleanResponse) {
245
- printAiResponse(cleanResponse, localVersion);
303
+ if (enrichedAnswer) {
304
+ printAiResponse(enrichedAnswer, `${localVersion} + source`);
246
305
  return;
247
306
  }
307
+ // Books not readable — avoid surfacing raw BOOKS: line to the user
308
+ showStaticHelp();
309
+ return;
248
310
  }
249
311
 
250
- printAiResponse(trimmedResponse, localVersion);
251
- } catch (error) {
312
+ // ANSWER: response or fallback — strip the "ANSWER:" prefix if present
313
+ const answerLine = trimmedResponse.indexOf('ANSWER:');
314
+ const answer =
315
+ answerLine !== -1
316
+ ? trimmedResponse.substring(answerLine + 'ANSWER:'.length).trim()
317
+ : trimmedResponse;
318
+
319
+ printAiResponse(answer, localVersion);
320
+ } catch (err) {
252
321
  logger.debug('help - runAiHelp', 'AI help failed, falling back to static help', {
253
- error: error.message
322
+ error: err.message
254
323
  });
255
324
  showStaticHelp();
256
325
  }
257
326
  }
258
327
 
259
328
  /**
260
- * Handle NEED_MORE_CONTEXT response by fetching referenced files from GitHub
329
+ * Pass 2: Read requested books from local disk and answer with enriched context
261
330
  *
262
- * @param {string} needMoreLine - The line containing NEED_MORE_CONTEXT marker
331
+ * @param {string} booksLine - The line starting with "BOOKS:" containing comma-separated paths
263
332
  * @param {string} question - Original user question
264
- * @param {string} claudeMdContent - Original CLAUDE.md content
265
- * @returns {Promise<string|null>} Enriched response or null on failure
333
+ * @param {string} catalog - The catalog content from Pass 1
334
+ * @returns {Promise<string|null>} Enriched answer or null on failure
266
335
  */
267
- async function handleNeedMoreContext(needMoreLine, question, claudeMdContent) {
336
+ async function readAndAnswerWithBooks(booksLine, question, catalog) {
268
337
  try {
269
- // Parse file paths from line: "NEED_MORE_CONTEXT: file1.js, file2.js"
270
- const pathsPart = needMoreLine.replace('NEED_MORE_CONTEXT', '').replace(':', '').trim();
271
- const filePaths = pathsPart
338
+ const pathsPart = booksLine.replace('BOOKS:', '').trim();
339
+ const bookPaths = pathsPart
272
340
  .split(',')
273
341
  .map((p) => p.trim())
274
342
  .filter(Boolean);
275
343
 
276
- if (filePaths.length === 0) {
277
- logger.debug('help - handleNeedMoreContext', 'No file paths in NEED_MORE_CONTEXT');
344
+ if (bookPaths.length === 0) {
345
+ logger.debug('help - readAndAnswerWithBooks', 'No book paths in BOOKS line');
278
346
  return null;
279
347
  }
280
348
 
281
- logger.debug('help - handleNeedMoreContext', 'Fetching additional files', { filePaths });
282
-
283
- const { owner, repo } = getSourceRepo();
284
-
285
- // Fetch files in parallel (no ref = default branch, avoids 404 on unreleased tags)
286
- const fetchResults = await Promise.all(
287
- filePaths.map(async (filePath) => {
288
- const content = await fetchFileContent(owner, repo, filePath);
289
- return { filePath, content };
349
+ logger.debug('help - readAndAnswerWithBooks', 'Reading books from disk', { bookPaths });
350
+
351
+ // Read books from local filesystem in parallel
352
+ // Contain LLM-sourced paths to the package root to prevent traversal
353
+ const readResults = await Promise.all(
354
+ bookPaths.map(async (bookPath) => {
355
+ const resolvedPath = path.resolve(_packageRoot, bookPath);
356
+ if (!resolvedPath.startsWith(_packageRoot + path.sep)) {
357
+ logger.debug('help - readAndAnswerWithBooks', 'Path traversal blocked', { bookPath });
358
+ return { bookPath, content: null };
359
+ }
360
+ const content = await _readPackageFile(bookPath);
361
+ return { bookPath, content };
290
362
  })
291
363
  );
292
364
 
293
- // Build enriched documentation
294
- const additionalContent = fetchResults
365
+ const bookContent = readResults
295
366
  .filter((r) => r.content !== null)
296
- .map((r) => `\n--- Source: ${r.filePath} ---\n${r.content}`)
297
- .join('\n');
367
+ .map((r) => `--- ${r.bookPath} ---\n${r.content}`)
368
+ .join('\n\n');
298
369
 
299
- if (!additionalContent) {
300
- logger.debug('help - handleNeedMoreContext', 'No additional content fetched');
370
+ if (!bookContent) {
371
+ logger.debug('help - readAndAnswerWithBooks', 'No book content read from disk');
301
372
  return null;
302
373
  }
303
374
 
304
375
  const enrichedPrompt = await loadPrompt('HELP_QUERY.md', {
305
- DOCUMENTATION: `${claudeMdContent}\n\n=== ADDITIONAL SOURCE FILES ===\n${additionalContent}`,
376
+ CATALOG: catalog,
377
+ BOOKS: bookContent,
306
378
  QUESTION: question
307
379
  });
308
380
 
309
381
  const enrichedResponse = await executeClaudeWithRetry(enrichedPrompt, { timeout: 60000 });
310
382
  return enrichedResponse.trim();
311
- } catch (error) {
312
- logger.debug('help - handleNeedMoreContext', 'Enrichment failed', { error: error.message });
383
+ } catch (err) {
384
+ logger.debug('help - readAndAnswerWithBooks', 'Book enrichment failed', {
385
+ error: err.message
386
+ });
313
387
  return null;
314
388
  }
315
389
  }
@@ -371,8 +445,8 @@ async function runReportIssue() {
371
445
  return;
372
446
  }
373
447
 
374
- // Step 5: Read CLAUDE.md for project context
375
- const claudeMdContent = await readClaudeMd();
448
+ // Step 5: Read CLAUDE.md for project context (report-issue only needs basic context)
449
+ const claudeMdContent = await _readPackageFile('CLAUDE.md');
376
450
 
377
451
  // Step 6: Generate questions from template using Claude
378
452
  const questionsPrompt = await loadPrompt('HELP_REPORT_ISSUE.md', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-git-hooks",
3
- "version": "2.43.0",
3
+ "version": "2.44.0",
4
4
  "description": "Git hooks with Claude CLI for code analysis and automatic commit messages",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,41 @@
1
+ You are the claude-hooks CLI assistant.
2
+ Answer the user's question using the project catalog provided below.
3
+
4
+ The catalog contains:
5
+ - CLAUDE.md: Global rules and restrictions
6
+ - .library/index.md: Repo identity, capabilities, directory structure
7
+ - .library/conventions.md: Coding standards and testing patterns
8
+ - .library/by-code/ shelves: Source file indexes (maps file paths to book references)
9
+ - .library/by-domain/ shelves: Business workflow reading lists
10
+ - .library/by-task-type/ shelves: Task-specific guidance
11
+
12
+ You must respond in EXACTLY ONE of two formats:
13
+
14
+ FORMAT 1 — You CAN answer fully from the catalog:
15
+ Start your response with "ANSWER:" on its own line, then provide the answer.
16
+ Be concise, practical, and include command examples when relevant.
17
+ Format for terminal output (no markdown headers, use indentation).
18
+
19
+ ANSWER:
20
+ [your complete answer here]
21
+
22
+ FORMAT 2 — You need to read specific book files for a deep-dive answer:
23
+ Start your response with "BOOKS:" followed by comma-separated .library/books/*.md paths.
24
+ Pick ONLY the books that are directly relevant — do not request more than 5.
25
+ The book paths are referenced in the by-code shelf files (look for @books/... references).
26
+
27
+ BOOKS: .library/books/analysis-engine.md, .library/books/claude-client.md
28
+
29
+ RULES:
30
+ - Most questions about commands, workflows, configuration, and usage are answerable from the catalog (use FORMAT 1).
31
+ - Only use FORMAT 2 for questions about internal implementation details (e.g., "how does analyzeCode retry on failure").
32
+ - Do NOT include any text before the ANSWER: or BOOKS: line.
33
+ - Do NOT combine both formats.
34
+
35
+ ---
36
+ CATALOG:
37
+
38
+ {{CATALOG}}
39
+
40
+ ---
41
+ Question: {{QUESTION}}
@@ -1,23 +1,19 @@
1
1
  You are the claude-hooks CLI assistant.
2
- Answer the user's question based ONLY on the documentation provided below.
2
+ Answer the user's question based on the project catalog and source book content provided below.
3
3
  Be concise, practical, and include command examples when relevant.
4
4
  Format for terminal output (no markdown headers, use indentation).
5
5
 
6
- IMPORTANT: You must choose exactly ONE of two response modes:
6
+ The catalog gives you the project overview. The book content gives you the deep implementation details you need to answer this specific question.
7
7
 
8
- MODE 1 - You CAN answer from the documentation:
9
- Respond directly with the answer. Do NOT include "NEED_MORE_CONTEXT" anywhere.
8
+ ---
9
+ CATALOG:
10
10
 
11
- MODE 2 - You CANNOT answer accurately from the documentation alone:
12
- Respond with ONLY a single line in this exact format (nothing else before or after):
13
- NEED_MORE_CONTEXT: file1.js, file2.js
14
- List the source file paths from the documentation that would contain the answer.
15
- Do NOT include any explanation, apology, or partial answer. Just the single line.
11
+ {{CATALOG}}
16
12
 
17
13
  ---
18
- DOCUMENTATION:
14
+ BOOK CONTENT:
19
15
 
20
- {{DOCUMENTATION}}
16
+ {{BOOKS}}
21
17
 
22
18
  ---
23
19
  Question: {{QUESTION}}