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 +22 -0
- package/README.md +2 -2
- package/lib/commands/help.js +155 -81
- package/package.json +1 -1
- package/templates/HELP_NAVIGATE.md +41 -0
- package/templates/HELP_QUERY.md +7 -11
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 (
|
|
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
|
|
358
|
+
| `help.js` | **Help, AI librarian, report-issue** | `runShowHelp()`, `showStaticHelp()`, `runShowVersion()` |
|
|
359
359
|
|
|
360
360
|
### Utility Modules (`lib/utils/`)
|
|
361
361
|
|
package/lib/commands/help.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Features:
|
|
6
6
|
* - Dynamic help from command registry (no args, --help, -h)
|
|
7
|
-
* - AI help:
|
|
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
|
-
*
|
|
165
|
-
* Why:
|
|
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
|
-
* @
|
|
176
|
+
* @param {string} relativePath - Path relative to package root
|
|
177
|
+
* @returns {Promise<string|null>} File content or null
|
|
168
178
|
*/
|
|
169
|
-
const
|
|
170
|
-
const claudeMdPath = path.join(__dirname, '..', '..', 'CLAUDE.md');
|
|
179
|
+
const _readPackageFile = async (relativePath) => {
|
|
171
180
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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:
|
|
186
|
-
* Why: Provides intelligent answers
|
|
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
|
|
190
|
-
* 2.
|
|
191
|
-
* 3.
|
|
192
|
-
* 4. If
|
|
193
|
-
* 5.
|
|
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
|
|
201
|
-
if (!
|
|
269
|
+
const catalog = await readLibraryCatalog();
|
|
270
|
+
if (!catalog) {
|
|
202
271
|
logger.debug(
|
|
203
272
|
'help - runAiHelp',
|
|
204
|
-
'
|
|
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('
|
|
211
|
-
|
|
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', {
|
|
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
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
301
|
+
catalog
|
|
233
302
|
);
|
|
234
|
-
if (
|
|
235
|
-
printAiResponse(
|
|
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
|
-
|
|
251
|
-
|
|
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:
|
|
322
|
+
error: err.message
|
|
254
323
|
});
|
|
255
324
|
showStaticHelp();
|
|
256
325
|
}
|
|
257
326
|
}
|
|
258
327
|
|
|
259
328
|
/**
|
|
260
|
-
*
|
|
329
|
+
* Pass 2: Read requested books from local disk and answer with enriched context
|
|
261
330
|
*
|
|
262
|
-
* @param {string}
|
|
331
|
+
* @param {string} booksLine - The line starting with "BOOKS:" containing comma-separated paths
|
|
263
332
|
* @param {string} question - Original user question
|
|
264
|
-
* @param {string}
|
|
265
|
-
* @returns {Promise<string|null>} Enriched
|
|
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
|
|
336
|
+
async function readAndAnswerWithBooks(booksLine, question, catalog) {
|
|
268
337
|
try {
|
|
269
|
-
|
|
270
|
-
const
|
|
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 (
|
|
277
|
-
logger.debug('help -
|
|
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 -
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
294
|
-
const additionalContent = fetchResults
|
|
365
|
+
const bookContent = readResults
|
|
295
366
|
.filter((r) => r.content !== null)
|
|
296
|
-
.map((r) =>
|
|
297
|
-
.join('\n');
|
|
367
|
+
.map((r) => `--- ${r.bookPath} ---\n${r.content}`)
|
|
368
|
+
.join('\n\n');
|
|
298
369
|
|
|
299
|
-
if (!
|
|
300
|
-
logger.debug('help -
|
|
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
|
-
|
|
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 (
|
|
312
|
-
logger.debug('help -
|
|
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
|
|
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
|
@@ -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}}
|
package/templates/HELP_QUERY.md
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
You are the claude-hooks CLI assistant.
|
|
2
|
-
Answer the user's question based
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
8
|
+
---
|
|
9
|
+
CATALOG:
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
14
|
+
BOOK CONTENT:
|
|
19
15
|
|
|
20
|
-
{{
|
|
16
|
+
{{BOOKS}}
|
|
21
17
|
|
|
22
18
|
---
|
|
23
19
|
Question: {{QUESTION}}
|