claude-code-autoconfig 1.0.134 → 1.0.136

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.
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * sync-docs.js — Scans .claude/ and updates the interactive docs HTML.
5
+ *
6
+ * Updates three sections in autoconfig.docs.html:
7
+ * 1. File tree (HTML divs)
8
+ * 2. treeInfo (JS object — title, desc, trigger per key)
9
+ * 3. fileContents (JS object — filename + content preview per key)
10
+ *
11
+ * Run: node .claude/scripts/sync-docs.js
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const cwd = process.cwd();
18
+ const claudeDir = path.join(cwd, '.claude');
19
+ const docsPath = path.join(claudeDir, 'docs', 'autoconfig.docs.html');
20
+
21
+ if (!fs.existsSync(docsPath)) {
22
+ // No docs file — nothing to sync
23
+ process.exit(0);
24
+ }
25
+
26
+ // Directories/files to skip entirely
27
+ const SKIP = new Set([
28
+ 'docs', 'plans', 'migration', 'retro', 'scripts',
29
+ 'settings.local.json'
30
+ ]);
31
+
32
+ // Folders we scan for files
33
+ const SCAN_FOLDERS = ['commands', 'agents', 'hooks', 'feedback'];
34
+
35
+ // Structural keys that are not file-backed (always preserved, never generated)
36
+ const STRUCTURAL_KEYS = new Set([
37
+ 'memory-md', 'root', 'claude-md', 'claude-dir'
38
+ ]);
39
+
40
+ // Hardcoded entries that have special handling
41
+ const STATIC_ENTRIES = {
42
+ 'settings': {
43
+ file: 'settings.json',
44
+ parent: 'claude-dir',
45
+ icon: '⚙️',
46
+ indent: 2
47
+ },
48
+ 'mcp': {
49
+ file: '.mcp.json',
50
+ parent: 'claude-dir',
51
+ icon: '🔌',
52
+ indent: 2
53
+ },
54
+ 'docs': {
55
+ file: 'autoconfig.docs.html',
56
+ parent: 'docs-folder',
57
+ icon: '🌐',
58
+ indent: 3,
59
+ folder: {
60
+ key: 'docs',
61
+ name: 'docs',
62
+ dataFolder: 'docs-folder',
63
+ parent: 'claude-dir'
64
+ }
65
+ }
66
+ };
67
+
68
+ /**
69
+ * Extract @description from a file's content.
70
+ */
71
+ function extractDescription(content, ext) {
72
+ if (ext === '.md') {
73
+ const match = content.match(/<!--\s*@description\s+(.+?)\s*-->/);
74
+ return match ? match[1] : null;
75
+ }
76
+ if (ext === '.js') {
77
+ // Look for @description in JSDoc or comment block
78
+ const match = content.match(/@description\s+(.+?)(?:\n|\*\/)/);
79
+ return match ? match[1].replace(/\s*\*\s*$/, '').trim() : null;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Extract @trigger from a JS file's JSDoc.
86
+ */
87
+ function extractTrigger(content) {
88
+ const match = content.match(/@trigger\s+(.+?)(?:\n|\*\/)/);
89
+ return match ? match[1].replace(/\s*\*\s*$/, '').trim() : null;
90
+ }
91
+
92
+ /**
93
+ * Derive a unique key for a file.
94
+ */
95
+ function deriveKey(folder, filename) {
96
+ const stem = filename.replace(/\.[^.]+$/, '');
97
+ if (folder === 'commands') return stem;
98
+ if (folder === 'hooks') return stem + '-hook';
99
+ if (folder === 'agents') return stem + '-agent';
100
+ if (folder === 'feedback') return 'feedback-' + stem.toLowerCase();
101
+ if (folder === 'updates') return 'update-' + stem;
102
+ return stem;
103
+ }
104
+
105
+ /**
106
+ * Derive trigger text for a file.
107
+ */
108
+ function deriveTrigger(folder, filename, content) {
109
+ if (folder === 'commands') {
110
+ const stem = filename.replace(/\.md$/, '');
111
+ return '/' + stem;
112
+ }
113
+ if (folder === 'hooks') {
114
+ const trigger = extractTrigger(content);
115
+ return trigger || 'PostToolUse hook';
116
+ }
117
+ if (folder === 'agents') return 'Background agent';
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Generate a content preview (first ~30 meaningful lines).
123
+ */
124
+ function generatePreview(content, ext) {
125
+ const lines = content.split(/\r?\n/);
126
+ // Skip metadata comments at the top
127
+ let start = 0;
128
+ while (start < lines.length && /^<!--/.test(lines[start].trim())) {
129
+ // Find the closing -->
130
+ while (start < lines.length && !lines[start].includes('-->')) start++;
131
+ start++;
132
+ }
133
+ // Skip leading blank lines
134
+ while (start < lines.length && lines[start].trim() === '') start++;
135
+
136
+ const preview = lines.slice(start, start + 30).join('\n').trim();
137
+ return preview || content.slice(0, 500).trim();
138
+ }
139
+
140
+ /**
141
+ * Escape a string for use inside a JS template literal.
142
+ */
143
+ function escapeTemplateLiteral(str) {
144
+ return str
145
+ .replace(/\\/g, '\\\\')
146
+ .replace(/`/g, '\\`')
147
+ .replace(/\$\{/g, '\\${');
148
+ }
149
+
150
+ /**
151
+ * Scan .claude/ and collect file entries.
152
+ */
153
+ function scanFiles() {
154
+ const entries = [];
155
+
156
+ for (const folder of SCAN_FOLDERS) {
157
+ const folderPath = path.join(claudeDir, folder);
158
+ if (!fs.existsSync(folderPath)) continue;
159
+
160
+ const files = fs.readdirSync(folderPath).filter(f => {
161
+ const stat = fs.statSync(path.join(folderPath, f));
162
+ return stat.isFile();
163
+ }).sort();
164
+
165
+ for (const file of files) {
166
+ const ext = path.extname(file);
167
+ const filePath = path.join(folderPath, file);
168
+ const content = fs.readFileSync(filePath, 'utf8');
169
+ const key = deriveKey(folder, file);
170
+ const desc = extractDescription(content, ext) || `${file} in ${folder}/`;
171
+ const trigger = deriveTrigger(folder, file, content);
172
+ const preview = generatePreview(content, ext);
173
+
174
+ entries.push({ key, folder, file, desc, trigger, preview });
175
+ }
176
+ }
177
+
178
+ // Check for rules/ directory
179
+ const rulesPath = path.join(claudeDir, 'rules');
180
+ if (fs.existsSync(rulesPath)) {
181
+ entries.push({
182
+ key: 'rules',
183
+ folder: null,
184
+ file: null,
185
+ desc: 'Path-scoped context that loads when Claude works on matching files.',
186
+ trigger: null,
187
+ preview: null,
188
+ isEmptyFolder: true,
189
+ emptyMessage: 'Add .md files here to define rules for specific paths in your codebase.'
190
+ });
191
+ }
192
+
193
+ return entries;
194
+ }
195
+
196
+ /**
197
+ * Generate the tree HTML for file-backed entries.
198
+ */
199
+ function generateTreeHtml(entries) {
200
+ const lines = [];
201
+ const folders = new Map(); // folder name -> entries
202
+
203
+ // Group entries by folder
204
+ for (const entry of entries) {
205
+ if (entry.isEmptyFolder) continue;
206
+ if (!folders.has(entry.folder)) folders.set(entry.folder, []);
207
+ folders.get(entry.folder).push(entry);
208
+ }
209
+
210
+ // Emit folder groups in a consistent order
211
+ const folderOrder = ['commands', 'agents', 'feedback', 'hooks'];
212
+ for (const folderName of folderOrder) {
213
+ const folderEntries = folders.get(folderName);
214
+ if (!folderEntries || folderEntries.length === 0) continue;
215
+
216
+ // Folder row
217
+ lines.push(` <div class="tree-item indent-2 folder-row hidden collapsed" data-info="${folderName}" data-folder="${folderName}" data-parent="claude-dir">`);
218
+ lines.push(` <span class="tree-chevron">›</span>`);
219
+ lines.push(` <span class="tree-folder-icon">📁</span>`);
220
+ lines.push(` <span class="folder">${folderName}</span>`);
221
+ lines.push(` </div>`);
222
+
223
+ // File rows
224
+ for (const entry of folderEntries) {
225
+ lines.push(` <div class="tree-item indent-3 hidden" data-info="${entry.key}" data-parent="${folderName}">`);
226
+ lines.push(` <span class="tree-spacer"></span>`);
227
+ lines.push(` <span class="tree-file-icon">📄</span>`);
228
+ lines.push(` <span class="file">${entry.file}</span>`);
229
+ lines.push(` </div>`);
230
+ }
231
+ }
232
+
233
+ // Docs folder (static)
234
+ lines.push(` <div class="tree-item indent-2 folder-row hidden collapsed" data-info="docs" data-folder="docs-folder" data-parent="claude-dir">`);
235
+ lines.push(` <span class="tree-chevron">›</span>`);
236
+ lines.push(` <span class="tree-folder-icon">📁</span>`);
237
+ lines.push(` <span class="folder">docs</span>`);
238
+ lines.push(` </div>`);
239
+ lines.push(` <div class="tree-item indent-3 hidden" data-info="docs" data-parent="docs-folder">`);
240
+ lines.push(` <span class="tree-spacer"></span>`);
241
+ lines.push(` <span class="tree-file-icon">🌐</span>`);
242
+ lines.push(` <span class="file">autoconfig.docs.html</span>`);
243
+ lines.push(` </div>`);
244
+
245
+ // Rules folder
246
+ const hasRules = entries.some(e => e.key === 'rules');
247
+ if (hasRules) {
248
+ lines.push(` <div class="tree-item indent-2 hidden" data-info="rules" data-parent="claude-dir">`);
249
+ lines.push(` <span class="tree-spacer"></span>`);
250
+ lines.push(` <span class="tree-folder-icon">📁</span>`);
251
+ lines.push(` <span class="folder">rules</span>`);
252
+ lines.push(` </div>`);
253
+ }
254
+
255
+ // .mcp.json
256
+ if (fs.existsSync(path.join(claudeDir, '.mcp.json'))) {
257
+ lines.push(` <div class="tree-item indent-2 hidden" data-info="mcp" data-parent="claude-dir">`);
258
+ lines.push(` <span class="tree-spacer"></span>`);
259
+ lines.push(` <span class="tree-file-icon">🔌</span>`);
260
+ lines.push(` <span class="file">.mcp.json</span>`);
261
+ lines.push(` </div>`);
262
+ }
263
+
264
+ // settings.json
265
+ if (fs.existsSync(path.join(claudeDir, 'settings.json'))) {
266
+ lines.push(` <div class="tree-item indent-2 hidden" data-info="settings" data-parent="claude-dir">`);
267
+ lines.push(` <span class="tree-spacer"></span>`);
268
+ lines.push(` <span class="tree-file-icon">⚙️</span>`);
269
+ lines.push(` <span class="file">settings.json</span>`);
270
+ lines.push(` </div>`);
271
+ }
272
+
273
+ return lines.join('\n');
274
+ }
275
+
276
+ /**
277
+ * Generate treeInfo JS entries for file-backed items.
278
+ */
279
+ function generateTreeInfo(entries) {
280
+ const lines = [];
281
+
282
+ for (const entry of entries) {
283
+ if (entry.isEmptyFolder) {
284
+ lines.push(` '${entry.key}': {`);
285
+ lines.push(` title: '${entry.key}/',`);
286
+ lines.push(` desc: '${entry.desc.replace(/'/g, "\\'")}'`);
287
+ lines.push(` },`);
288
+ continue;
289
+ }
290
+
291
+ // Folder-level info card
292
+ // (we emit these once per folder)
293
+ }
294
+
295
+ // Emit folder info cards
296
+ const folderDescs = {
297
+ 'commands': 'On-demand workflows you trigger with <code>/name</code>. Each .md file becomes a <a href="https://docs.anthropic.com/en/docs/claude-code/slash-commands" target="_blank" style="color: var(--accent-cyan);">slash command</a>.',
298
+ 'agents': 'Reusable agent definitions that Claude can invoke for specialized tasks.',
299
+ 'feedback': 'Team-maintained corrections and guidance for Claude. Add notes here when Claude does something wrong — it learns for next time. This directory persists across <code>/autoconfig</code> runs.',
300
+ 'hooks': 'Executable hook scripts that trigger on Claude Code events like PostToolUse.'
301
+ };
302
+
303
+ const seenFolders = new Set();
304
+ for (const entry of entries) {
305
+ if (entry.isEmptyFolder) continue;
306
+ if (!seenFolders.has(entry.folder)) {
307
+ seenFolders.add(entry.folder);
308
+ const desc = folderDescs[entry.folder] || `Files in ${entry.folder}/`;
309
+ lines.push(` '${entry.folder}': {`);
310
+ lines.push(` title: '${entry.folder}/',`);
311
+ lines.push(` desc: '${desc.replace(/'/g, "\\'")}'`);
312
+ lines.push(` },`);
313
+ }
314
+ }
315
+
316
+ // Emit file info cards
317
+ for (const entry of entries) {
318
+ if (entry.isEmptyFolder) continue;
319
+
320
+ const escapedDesc = entry.desc.replace(/'/g, "\\'");
321
+ lines.push(` '${entry.key}': {`);
322
+ lines.push(` title: '${entry.file}',`);
323
+ lines.push(` desc: '${escapedDesc}'${entry.trigger ? ',' : ''}`);
324
+ if (entry.trigger) {
325
+ lines.push(` trigger: '${entry.trigger.replace(/'/g, "\\'")}'`);
326
+ }
327
+ lines.push(` },`);
328
+ }
329
+
330
+ // Static entries: docs, rules, mcp, settings
331
+ lines.push(` 'docs': {`);
332
+ lines.push(` title: 'docs/autoconfig.docs.html',`);
333
+ lines.push(` desc: 'This interactive docs. Open it anytime to review what each file does.',`);
334
+ lines.push(` trigger: '/show-docs'`);
335
+ lines.push(` },`);
336
+
337
+ if (entries.some(e => e.key === 'rules')) {
338
+ lines.push(` 'rules': {`);
339
+ lines.push(` title: 'rules/',`);
340
+ lines.push(` desc: 'Path-scoped context that loads when Claude works on matching files. Optimized rules are based on your project\\'s needs, patterns and practices.<br><br><div style="background: var(--bg-elevated); border: 1px solid var(--accent-cyan); border-radius: 8px; padding: 16px; margin-top: 8px;"><strong style="color: var(--accent-orange);">Want optimized rules for your project?</strong><br>Reach out: <a href="mailto:info@adac1001.com" style="color: var(--accent-cyan);">info@adac1001.com</a></div>'`);
341
+ lines.push(` },`);
342
+ }
343
+
344
+ if (fs.existsSync(path.join(claudeDir, '.mcp.json'))) {
345
+ lines.push(` 'mcp': {`);
346
+ lines.push(` title: '.mcp.json',`);
347
+ lines.push(` desc: 'MCP (Model Context Protocol) server configuration. Add your MCP servers here.'`);
348
+ lines.push(` },`);
349
+ }
350
+
351
+ if (fs.existsSync(path.join(claudeDir, 'settings.json'))) {
352
+ lines.push(` 'settings': {`);
353
+ lines.push(` title: 'settings.json',`);
354
+ lines.push(` desc: 'Permissions and security settings. Controls what Claude can auto-approve (allow) and what is always blocked (deny).'`);
355
+ lines.push(` }`);
356
+ }
357
+
358
+ return lines.join('\n');
359
+ }
360
+
361
+ /**
362
+ * Generate fileContents JS entries.
363
+ */
364
+ function generateFileContents(entries) {
365
+ const lines = [];
366
+
367
+ for (const entry of entries) {
368
+ if (entry.isEmptyFolder) {
369
+ lines.push(` '${entry.key}': {`);
370
+ lines.push(` filename: '${entry.key}/',`);
371
+ lines.push(` content: null,`);
372
+ lines.push(` empty: true,`);
373
+ lines.push(` emptyMessage: '${entry.emptyMessage.replace(/'/g, "\\'")}'`);
374
+ lines.push(` },`);
375
+ continue;
376
+ }
377
+
378
+ const escaped = escapeTemplateLiteral(entry.preview);
379
+ lines.push(` '${entry.key}': {`);
380
+ lines.push(` filename: '${entry.file}',`);
381
+ lines.push(' content: `' + escaped + '`');
382
+ lines.push(` },`);
383
+ }
384
+
385
+ // Folder-level preview entries for hooks
386
+ lines.push(` 'hooks': {`);
387
+ lines.push(` filename: 'hooks/',`);
388
+ lines.push(` content: null,`);
389
+ lines.push(` empty: true,`);
390
+ lines.push(` emptyMessage: 'Contains executable hook scripts that trigger on Claude Code events.'`);
391
+ lines.push(` },`);
392
+
393
+ // Static entries
394
+ lines.push(` 'docs': {`);
395
+ lines.push(` filename: 'autoconfig.docs.html',`);
396
+ lines.push(` content: null,`);
397
+ lines.push(` empty: true,`);
398
+ lines.push(` emptyMessage: "You\\\'re looking at it! 👀"`);
399
+ lines.push(` },`);
400
+
401
+ if (entries.some(e => e.key === 'rules')) {
402
+ lines.push(` 'rules': {`);
403
+ lines.push(` filename: 'rules/',`);
404
+ lines.push(` content: null,`);
405
+ lines.push(` empty: true,`);
406
+ lines.push(` emptyMessage: 'This directory is empty.\\nAdd .md files here to define rules for specific paths in your codebase.'`);
407
+ lines.push(` },`);
408
+ }
409
+
410
+ // settings.json — read actual content
411
+ const settingsPath = path.join(claudeDir, 'settings.json');
412
+ if (fs.existsSync(settingsPath)) {
413
+ const content = fs.readFileSync(settingsPath, 'utf8').trim();
414
+ lines.push(` 'settings': {`);
415
+ lines.push(` filename: 'settings.json',`);
416
+ lines.push(' content: `' + escapeTemplateLiteral(content) + '`');
417
+ lines.push(` },`);
418
+ }
419
+
420
+ // .mcp.json — read actual content
421
+ const mcpPath = path.join(claudeDir, '.mcp.json');
422
+ if (fs.existsSync(mcpPath)) {
423
+ const content = fs.readFileSync(mcpPath, 'utf8').trim();
424
+ lines.push(` 'mcp': {`);
425
+ lines.push(` filename: '.mcp.json',`);
426
+ lines.push(' content: `' + escapeTemplateLiteral(content) + '`');
427
+ lines.push(` }`);
428
+ }
429
+
430
+ return lines.join('\n');
431
+ }
432
+
433
+ // =============================================================================
434
+ // Main
435
+ // =============================================================================
436
+
437
+ const entries = scanFiles();
438
+ let html = fs.readFileSync(docsPath, 'utf8');
439
+
440
+ // 1. Replace the file tree (between claude-dir folder row and settings.json closing div)
441
+ // We find the marker after the claude-dir folder and replace up to the settings div
442
+ const treeStartMarker = '<span class="folder">.claude</span>';
443
+ const treeStartIdx = html.indexOf(treeStartMarker);
444
+ if (treeStartIdx === -1) {
445
+ console.error('Could not find .claude folder marker in docs HTML');
446
+ process.exit(1);
447
+ }
448
+ // Find the closing </div> of the claude-dir folder row
449
+ const claudeDirClose = html.indexOf('</div>', treeStartIdx);
450
+ const treeContentStart = claudeDirClose + '</div>'.length;
451
+
452
+ // Find the end of the tree: look for the closing of tree-side/tree-content after settings.json
453
+ const treeEndMarker = '</div>\n </div>\n <div class="info-side">';
454
+ const treeEndIdx = html.indexOf(treeEndMarker, treeContentStart);
455
+ if (treeEndIdx === -1) {
456
+ console.error('Could not find tree end marker in docs HTML');
457
+ process.exit(1);
458
+ }
459
+
460
+ const newTreeHtml = generateTreeHtml(entries);
461
+ html = html.slice(0, treeContentStart) + '\n' + newTreeHtml + '\n ' + html.slice(treeEndIdx);
462
+
463
+ // 2. Replace treeInfo (between structural entries and closing })
464
+ // We keep memory-md, root, claude-md, claude-dir and replace everything after
465
+ const treeInfoStartMarker = "// Tree panel info data";
466
+ let treeInfoIdx = html.indexOf(treeInfoStartMarker);
467
+ if (treeInfoIdx === -1) {
468
+ // Try alternate marker
469
+ treeInfoIdx = html.indexOf("const treeInfo = {");
470
+ }
471
+ if (treeInfoIdx === -1) {
472
+ console.error('Could not find treeInfo in docs HTML');
473
+ process.exit(1);
474
+ }
475
+
476
+ // Find the claude-dir entry end (last structural entry)
477
+ const claudeDirInfoMarker = "'claude-dir': {";
478
+ const claudeDirInfoIdx = html.indexOf(claudeDirInfoMarker, treeInfoIdx);
479
+ if (claudeDirInfoIdx === -1) {
480
+ console.error('Could not find claude-dir info entry');
481
+ process.exit(1);
482
+ }
483
+ // Find the closing }, of the claude-dir entry
484
+ let braceDepth = 0;
485
+ let i = claudeDirInfoIdx;
486
+ while (i < html.length) {
487
+ if (html[i] === '{') braceDepth++;
488
+ if (html[i] === '}') {
489
+ braceDepth--;
490
+ if (braceDepth === 0) break;
491
+ }
492
+ i++;
493
+ }
494
+ // Skip past the closing },
495
+ const afterClaudeDir = i + 1;
496
+ // Skip whitespace/comma
497
+ let treeInfoInsertPoint = afterClaudeDir;
498
+ while (treeInfoInsertPoint < html.length && /[\s,]/.test(html[treeInfoInsertPoint])) {
499
+ treeInfoInsertPoint++;
500
+ }
501
+
502
+ // Find the closing }; of treeInfo
503
+ const treeInfoEnd = html.indexOf('};', treeInfoInsertPoint);
504
+ if (treeInfoEnd === -1) {
505
+ console.error('Could not find treeInfo closing');
506
+ process.exit(1);
507
+ }
508
+
509
+ const newTreeInfo = generateTreeInfo(entries);
510
+ html = html.slice(0, treeInfoInsertPoint) + newTreeInfo + '\n ' + html.slice(treeInfoEnd);
511
+
512
+ // 3. Replace fileContents
513
+ // Keep memory-md and claude-md (structural), replace the rest
514
+ const fileContentsMarker = "const fileContents = {";
515
+ const fcIdx = html.indexOf(fileContentsMarker);
516
+ if (fcIdx === -1) {
517
+ console.error('Could not find fileContents in docs HTML');
518
+ process.exit(1);
519
+ }
520
+
521
+ // Find claude-md entry end (last structural fileContents entry)
522
+ const claudeMdFcMarker = "'claude-md': {";
523
+ const claudeMdFcIdx = html.indexOf(claudeMdFcMarker, fcIdx);
524
+ if (claudeMdFcIdx === -1) {
525
+ console.error('Could not find claude-md fileContents entry');
526
+ process.exit(1);
527
+ }
528
+ braceDepth = 0;
529
+ i = claudeMdFcIdx;
530
+ while (i < html.length) {
531
+ if (html[i] === '{') braceDepth++;
532
+ if (html[i] === '}') {
533
+ braceDepth--;
534
+ if (braceDepth === 0) break;
535
+ }
536
+ i++;
537
+ }
538
+ const afterClaudeMdFc = i + 1;
539
+ let fcInsertPoint = afterClaudeMdFc;
540
+ while (fcInsertPoint < html.length && /[\s,]/.test(html[fcInsertPoint])) {
541
+ fcInsertPoint++;
542
+ }
543
+
544
+ const fcEnd = html.indexOf('};', fcInsertPoint);
545
+ if (fcEnd === -1) {
546
+ console.error('Could not find fileContents closing');
547
+ process.exit(1);
548
+ }
549
+
550
+ const newFileContents = generateFileContents(entries);
551
+ html = html.slice(0, fcInsertPoint) + newFileContents + '\n ' + html.slice(fcEnd);
552
+
553
+ fs.writeFileSync(docsPath, html);
554
+
555
+ const fileCount = entries.filter(e => !e.isEmptyFolder).length;
556
+ console.log(`Synced ${fileCount} files to docs.`);