golem-cc 2.1.2 → 3.0.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.
Files changed (84) hide show
  1. package/.claude/commands/golem/build.md +18 -0
  2. package/.claude/commands/golem/config.md +39 -0
  3. package/.claude/commands/golem/continue.md +73 -0
  4. package/.claude/commands/golem/doctor.md +46 -0
  5. package/.claude/commands/golem/document.md +138 -0
  6. package/.claude/commands/golem/help.md +58 -0
  7. package/.claude/commands/golem/pause.md +130 -0
  8. package/.claude/commands/golem/plan.md +111 -0
  9. package/.claude/commands/golem/review.md +166 -0
  10. package/.claude/commands/golem/security.md +186 -0
  11. package/.claude/commands/golem/simplify.md +76 -0
  12. package/.claude/commands/golem/spec.md +105 -0
  13. package/.claude/commands/golem/status.md +33 -0
  14. package/.golem/agents/code-simplifier.md +54 -0
  15. package/.golem/agents/review-architecture.md +59 -0
  16. package/.golem/agents/review-logic.md +50 -0
  17. package/.golem/agents/review-security.md +50 -0
  18. package/.golem/agents/review-style.md +48 -0
  19. package/.golem/agents/review-tests.md +48 -0
  20. package/.golem/agents/spec-builder.md +60 -0
  21. package/.golem/bin/golem.mjs +270 -0
  22. package/.golem/lib/build.mjs +557 -0
  23. package/.golem/lib/claude.mjs +95 -0
  24. package/.golem/lib/config.mjs +421 -0
  25. package/.golem/lib/display.mjs +191 -0
  26. package/.golem/lib/doctor.mjs +197 -0
  27. package/.golem/lib/document.mjs +792 -0
  28. package/.golem/lib/gates.mjs +78 -0
  29. package/.golem/lib/init.mjs +166 -0
  30. package/.golem/lib/output.mjs +40 -0
  31. package/.golem/lib/ratelimit.mjs +86 -0
  32. package/.golem/lib/security.mjs +603 -0
  33. package/.golem/lib/simplify.mjs +101 -0
  34. package/.golem/lib/tui.mjs +368 -0
  35. package/.golem/lib/usage.mjs +119 -0
  36. package/.golem/lib/worktree.mjs +509 -0
  37. package/.golem/prompts/build.md +23 -0
  38. package/.golem/prompts/document-inline.md +66 -0
  39. package/.golem/prompts/document-markdown.md +80 -0
  40. package/.golem/prompts/simplify.md +35 -0
  41. package/README.md +141 -142
  42. package/bin/golem-shim.mjs +36 -0
  43. package/bin/install.mjs +193 -0
  44. package/package.json +27 -32
  45. package/.env.example +0 -17
  46. package/bin/golem +0 -1040
  47. package/commands/golem/build.md +0 -235
  48. package/commands/golem/config.md +0 -55
  49. package/commands/golem/doctor.md +0 -137
  50. package/commands/golem/help.md +0 -212
  51. package/commands/golem/plan.md +0 -214
  52. package/commands/golem/review.md +0 -376
  53. package/commands/golem/security.md +0 -204
  54. package/commands/golem/simplify.md +0 -94
  55. package/commands/golem/spec.md +0 -226
  56. package/commands/golem/status.md +0 -60
  57. package/dist/api/freshworks.d.ts +0 -61
  58. package/dist/api/freshworks.d.ts.map +0 -1
  59. package/dist/api/freshworks.js +0 -119
  60. package/dist/api/freshworks.js.map +0 -1
  61. package/dist/api/gitea.d.ts +0 -96
  62. package/dist/api/gitea.d.ts.map +0 -1
  63. package/dist/api/gitea.js +0 -154
  64. package/dist/api/gitea.js.map +0 -1
  65. package/dist/cli/index.d.ts +0 -9
  66. package/dist/cli/index.d.ts.map +0 -1
  67. package/dist/cli/index.js +0 -352
  68. package/dist/cli/index.js.map +0 -1
  69. package/dist/sync/ticket-sync.d.ts +0 -53
  70. package/dist/sync/ticket-sync.d.ts.map +0 -1
  71. package/dist/sync/ticket-sync.js +0 -226
  72. package/dist/sync/ticket-sync.js.map +0 -1
  73. package/dist/types.d.ts +0 -125
  74. package/dist/types.d.ts.map +0 -1
  75. package/dist/types.js +0 -5
  76. package/dist/types.js.map +0 -1
  77. package/dist/worktree/manager.d.ts +0 -54
  78. package/dist/worktree/manager.d.ts.map +0 -1
  79. package/dist/worktree/manager.js +0 -190
  80. package/dist/worktree/manager.js.map +0 -1
  81. package/golem/agents/code-simplifier.md +0 -81
  82. package/golem/agents/spec-builder.md +0 -90
  83. package/golem/prompts/PROMPT_build.md +0 -71
  84. package/golem/prompts/PROMPT_plan.md +0 -102
@@ -0,0 +1,792 @@
1
+ import { readFile, mkdir, writeFile as fsWriteFile, readdir, unlink } from 'node:fs/promises';
2
+ import { execSync } from 'node:child_process';
3
+ import { join, resolve, basename, relative } from 'node:path';
4
+ import { detectProject, loadConfig } from './config.mjs';
5
+ import { runClaude, checkClaudeCli } from './claude.mjs';
6
+ import { attachDisplay } from './display.mjs';
7
+ import { header, info, warn, fail, success, spinner } from './output.mjs';
8
+ import { runGates } from './gates.mjs';
9
+
10
+ const GIT_OPTS = { cwd: process.cwd(), stdio: 'pipe' };
11
+
12
+ function waitForClaude(emitter) {
13
+ let error = false;
14
+ return new Promise((res) => {
15
+ emitter.on('close', () => res(error));
16
+ emitter.on('error', (err) => {
17
+ fail(`Claude error: ${err.message}`);
18
+ error = true;
19
+ res(error);
20
+ });
21
+ });
22
+ }
23
+
24
+ function gitSnapshot() {
25
+ return execSync('git rev-parse HEAD', GIT_OPTS).toString().trim();
26
+ }
27
+
28
+ function gitRestore(sha) {
29
+ execSync('git checkout .', GIT_OPTS);
30
+ execSync('git clean -fd', GIT_OPTS);
31
+ execSync(`git reset --hard ${sha}`, GIT_OPTS);
32
+ }
33
+
34
+ const EXTENSION_MAP = {
35
+ javascript: ['.js', '.mjs', '.cjs', '.jsx'],
36
+ typescript: ['.ts', '.tsx', '.mts', '.cts'],
37
+ python: ['.py'],
38
+ };
39
+
40
+ const ALWAYS_INCLUDED = ['.vue', '.sql'];
41
+
42
+ const EXCLUDE_PATTERNS = [
43
+ /\.test\.[^.]+$/,
44
+ /\.spec\.[^.]+$/,
45
+ /__tests__\//,
46
+ /__mocks__\//,
47
+ /\.stories\.[^.]+$/,
48
+ /\.min\.[^.]+$/,
49
+ /node_modules\//,
50
+ /dist\//,
51
+ /build\//,
52
+ /\.next\//,
53
+ /\.nuxt\//,
54
+ /\.output\//,
55
+ /coverage\//,
56
+ /package-lock\.json$/,
57
+ /pnpm-lock\.yaml$/,
58
+ /yarn\.lock$/,
59
+ /\.lock$/,
60
+ /\.generated\.[^.]+$/,
61
+ /\.d\.ts$/,
62
+ ];
63
+
64
+ function gitListFiles(basePath) {
65
+ try {
66
+ const output = execSync('git ls-files', {
67
+ cwd: resolve(basePath),
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ encoding: 'utf-8',
70
+ });
71
+ return output.trim().split('\n').filter(Boolean);
72
+ } catch {
73
+ return [];
74
+ }
75
+ }
76
+
77
+ export function discoverSourceFiles(basePath, detection) {
78
+ const extensions = [...(EXTENSION_MAP[detection?.language] || EXTENSION_MAP.javascript), ...ALWAYS_INCLUDED];
79
+ const files = gitListFiles(basePath);
80
+
81
+ return files
82
+ .filter(f => extensions.some(ext => f.endsWith(ext)) && !EXCLUDE_PATTERNS.some(p => p.test(f)))
83
+ .sort();
84
+ }
85
+
86
+ const MAX_FILES_PER_BATCH = 30;
87
+
88
+ function formatProjectType(detection) {
89
+ const fields = [
90
+ ['Language', detection.language],
91
+ ['Framework', detection.framework, 'none'],
92
+ ['Module system', detection.moduleSystem, 'n/a'],
93
+ ['Database tooling', detection.databaseTooling, 'none'],
94
+ ['Inline doc standard', detection.inlineDocStandard],
95
+ ];
96
+ return fields
97
+ .filter(([, val, skip]) => val && val !== skip)
98
+ .map(([label, val]) => `${label}: ${val}`)
99
+ .join('\n');
100
+ }
101
+
102
+ export function buildInlinePrompt(template, detection, files) {
103
+ const projectType = formatProjectType(detection);
104
+ const fileList = files.map(f => `- ${f}`).join('\n');
105
+ return template
106
+ .replace('{{PROJECT_TYPE}}', projectType)
107
+ .replace('{{FILES}}', fileList);
108
+ }
109
+
110
+ function batchFiles(files, batchSize) {
111
+ return Array.from({ length: Math.ceil(files.length / batchSize) }, (_, i) =>
112
+ files.slice(i * batchSize, (i + 1) * batchSize)
113
+ );
114
+ }
115
+
116
+ async function loadInlineTemplate() {
117
+ try {
118
+ return await readFile(join(process.cwd(), '.golem/prompts/document-inline.md'), 'utf-8');
119
+ } catch {
120
+ return 'Add inline documentation to the following files. Follow the project type conventions.\n\n## Project Type\n\n{{PROJECT_TYPE}}\n\n## Target Files\n\n{{FILES}}';
121
+ }
122
+ }
123
+
124
+ export async function runDocumentInline(options = {}) {
125
+ if (!checkClaudeCli()) {
126
+ fail('Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code');
127
+ return;
128
+ }
129
+
130
+ const basePath = options.path || process.cwd();
131
+ const detection = await detectProject(basePath);
132
+ const files = discoverSourceFiles(basePath, detection);
133
+
134
+ if (files.length === 0) {
135
+ warn('No source files found to document.');
136
+ return;
137
+ }
138
+
139
+ header('Golem Document Inline');
140
+ info(`Project: ${detection.language} / ${detection.framework} (${detection.inlineDocStandard})`);
141
+ info(`Found ${files.length} source file(s)`);
142
+
143
+ const config = await loadConfig();
144
+ const template = await loadInlineTemplate();
145
+ const batches = batchFiles(files, MAX_FILES_PER_BATCH);
146
+ const skipValidation = options.skipGates === true;
147
+
148
+ for (let i = 0; i < batches.length; i++) {
149
+ const batch = batches[i];
150
+ if (batches.length > 1) {
151
+ info(`Batch ${i + 1}/${batches.length} (${batch.length} files)`);
152
+ }
153
+
154
+ const snapshot = gitSnapshot();
155
+
156
+ const prompt = buildInlinePrompt(template, detection, batch);
157
+ const model = options.model || 'opus';
158
+ const emitter = runClaude(prompt, { model });
159
+ attachDisplay(emitter);
160
+
161
+ const claudeError = await waitForClaude(emitter);
162
+
163
+ if (claudeError || skipValidation) continue;
164
+
165
+ info('Running validation gates...');
166
+ const gateResult = await runGates(config);
167
+
168
+ for (const r of gateResult.results) {
169
+ if (r.skipped) continue;
170
+ const dur = `(${(r.duration / 1000).toFixed(1)}s)`;
171
+ const log = r.passed ? success : fail;
172
+ log(`[Gate] ${r.name} ${r.passed ? 'PASS' : 'FAIL'} ${dur}`);
173
+ }
174
+
175
+ if (!gateResult.passed) {
176
+ warn(`Validation failed — reverting inline doc changes for batch ${i + 1}`);
177
+ gitRestore(snapshot);
178
+ continue;
179
+ }
180
+ success('Validation passed — inline docs preserved');
181
+ }
182
+ }
183
+
184
+ const API_PATTERNS = [
185
+ /routes\//,
186
+ /api\//,
187
+ /controllers\//,
188
+ /endpoints\//,
189
+ /server\//,
190
+ ];
191
+
192
+ const DB_PATTERNS = [
193
+ /prisma\//,
194
+ /drizzle/,
195
+ /migrations?\//,
196
+ /schema\.prisma$/,
197
+ /\.sql$/,
198
+ /knexfile/,
199
+ /models?\//,
200
+ ];
201
+
202
+ const GUIDE_WORKFLOWS = [
203
+ { pattern: /api\/|routes\/|controllers\//, name: 'adding-api-endpoint' },
204
+ { pattern: /components?\//, name: 'creating-component' },
205
+ { pattern: /migrations?\//, name: 'database-migration' },
206
+ { pattern: /\.test\.|\.spec\./, name: 'writing-tests' },
207
+ ];
208
+
209
+ const EXCLUDED_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage', 'test', 'tests', '__tests__']);
210
+
211
+ function detectModules(files) {
212
+ const dirs = new Set(
213
+ files
214
+ .filter(f => f.includes('/'))
215
+ .map(f => f.split('/')[0])
216
+ .filter(d => !d.startsWith('.') && !EXCLUDED_DIRS.has(d))
217
+ );
218
+ return [...dirs].sort();
219
+ }
220
+
221
+ function detectGuideWorkflows(files) {
222
+ return GUIDE_WORKFLOWS
223
+ .filter(({ pattern }) => files.some(f => pattern.test(f)))
224
+ .map(({ name }) => name);
225
+ }
226
+
227
+ function makeFrontmatter(title, description, order) {
228
+ return `---\ntitle: "${title}"\ndescription: "${description}"\nnavigation:\n order: ${order}\n---\n`;
229
+ }
230
+
231
+ async function loadMarkdownTemplate() {
232
+ try {
233
+ return await readFile(join(process.cwd(), '.golem/prompts/document-markdown.md'), 'utf-8');
234
+ } catch {
235
+ return 'Generate markdown documentation for the project.\n\n## Output Directory\n\n{{DOCS_PATH}}\n\n## Project Structure\n\n{{PROJECT_STRUCTURE}}';
236
+ }
237
+ }
238
+
239
+ export function buildMarkdownPrompt(template, docsPath, projectStructure) {
240
+ return template
241
+ .replace('{{DOCS_PATH}}', docsPath)
242
+ .replace('{{PROJECT_STRUCTURE}}', projectStructure);
243
+ }
244
+
245
+ function buildProjectStructure(files, detection, modules, hasApi, hasDb, workflows) {
246
+ const lines = [];
247
+ lines.push(`Language: ${detection.language}`);
248
+ if (detection.framework !== 'none') lines.push(`Framework: ${detection.framework}`);
249
+ if (detection.moduleSystem !== 'n/a') lines.push(`Module system: ${detection.moduleSystem}`);
250
+ if (detection.databaseTooling !== 'none') lines.push(`Database tooling: ${detection.databaseTooling}`);
251
+ lines.push('');
252
+ lines.push(`Total files: ${files.length}`);
253
+ lines.push('');
254
+ lines.push('Directories:');
255
+ for (const m of modules) {
256
+ const count = files.filter(f => f.startsWith(m + '/')).length;
257
+ lines.push(` ${m}/ (${count} files)`);
258
+ }
259
+ if (hasApi) lines.push('\nHas API/route layer: yes');
260
+ if (hasDb) lines.push(`Has database layer: yes (${detection.databaseTooling})`);
261
+ if (workflows.length > 0) {
262
+ lines.push('\nDetected workflows:');
263
+ for (const w of workflows) lines.push(` - ${w}`);
264
+ }
265
+ lines.push('\nFile listing (top 100):');
266
+ for (const f of files.slice(0, 100)) {
267
+ lines.push(` ${f}`);
268
+ }
269
+ if (files.length > 100) lines.push(` ... (${files.length - 100} more files)`);
270
+ return lines.join('\n');
271
+ }
272
+
273
+ async function writeDocFile(dir, filename, title, desc, order) {
274
+ await mkdir(dir, { recursive: true });
275
+ const filePath = join(dir, filename);
276
+ await fsWriteFile(filePath, makeFrontmatter(title, desc, order) + `\n# ${title}\n`, 'utf-8');
277
+ return filePath;
278
+ }
279
+
280
+ export async function createDocsStructure(docsPath, detection, files) {
281
+ const modules = detectModules(files);
282
+ const hasApi = files.some(f => API_PATTERNS.some(p => p.test(f)));
283
+ const hasDb = (detection?.databaseTooling && detection.databaseTooling !== 'none') || files.some(f => DB_PATTERNS.some(p => p.test(f)));
284
+ const workflows = detectGuideWorkflows(files);
285
+ const hasDeprecated = files.some(f => f.startsWith(docsPath + '/deprecated/') || f.startsWith(docsPath + 'deprecated/'));
286
+
287
+ await mkdir(docsPath, { recursive: true });
288
+
289
+ const created = [];
290
+
291
+ const baseFiles = [
292
+ { name: 'index.md', title: 'Project Overview', desc: 'Overview of the project, its purpose, and structure', order: 1 },
293
+ { name: 'architecture.md', title: 'Architecture', desc: 'System design and how components connect', order: 2 },
294
+ { name: 'getting-started.md', title: 'Getting Started', desc: 'Setup, installation, prerequisites, and first run', order: 3 },
295
+ { name: 'configuration.md', title: 'Configuration', desc: 'All configuration options with explanations', order: 4 },
296
+ ];
297
+
298
+ for (const { name, title, desc, order } of baseFiles) {
299
+ created.push(await writeDocFile(docsPath, name, title, desc, order));
300
+ }
301
+
302
+ if (modules.length > 0) {
303
+ const modulesDir = join(docsPath, 'modules');
304
+ for (let i = 0; i < modules.length; i++) {
305
+ created.push(await writeDocFile(modulesDir, `${modules[i]}.md`, modules[i], `Documentation for the ${modules[i]} module`, i + 1));
306
+ }
307
+ }
308
+
309
+ if (hasApi) {
310
+ created.push(await writeDocFile(join(docsPath, 'api'), 'index.md', 'API Reference', 'API endpoints and resources', 1));
311
+ }
312
+
313
+ if (hasDb) {
314
+ created.push(await writeDocFile(join(docsPath, 'database'), 'schema.md', 'Database Schema', 'Full schema overview with relationships', 1));
315
+ }
316
+
317
+ if (workflows.length > 0) {
318
+ const guidesDir = join(docsPath, 'guides');
319
+ created.push(await writeDocFile(guidesDir, 'setup.md', 'Development Setup', 'Dev environment setup guide', 1));
320
+
321
+ for (let i = 0; i < workflows.length; i++) {
322
+ const wf = workflows[i];
323
+ const title = wf.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
324
+ created.push(await writeDocFile(guidesDir, `${wf}.md`, title, `Guide for ${wf.replace(/-/g, ' ')}`, i + 2));
325
+ }
326
+ }
327
+
328
+ if (hasDeprecated) {
329
+ await mkdir(join(docsPath, 'deprecated'), { recursive: true });
330
+ }
331
+
332
+ return { created, modules, hasApi, hasDb, workflows, hasDeprecated };
333
+ }
334
+
335
+ export async function runDocumentMarkdown(options = {}) {
336
+ if (!checkClaudeCli()) {
337
+ fail('Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code');
338
+ return;
339
+ }
340
+
341
+ const basePath = options.path || process.cwd();
342
+ const config = await loadConfig();
343
+ const docsPath = resolve(basePath, options.docsPath || config.docsPath || 'docs');
344
+ const detection = await detectProject(basePath);
345
+ const files = gitListFiles(basePath);
346
+
347
+ if (files.length === 0) {
348
+ warn('No project files found.');
349
+ return;
350
+ }
351
+
352
+ header('Golem Document Markdown');
353
+ info(`Project: ${detection.language} / ${detection.framework}`);
354
+ info(`Docs path: ${docsPath}`);
355
+ info(`Found ${files.length} project file(s)`);
356
+
357
+ const { created, modules, hasApi, hasDb, workflows } = await createDocsStructure(docsPath, detection, files);
358
+
359
+ info(`Created ${created.length} documentation file(s)`);
360
+ if (modules.length > 0) info(`Modules: ${modules.join(', ')}`);
361
+ if (hasApi) info('API documentation: yes');
362
+ if (hasDb) info(`Database documentation: yes (${detection.databaseTooling})`);
363
+ if (workflows.length > 0) info(`Guides: ${workflows.join(', ')}`);
364
+
365
+ const template = await loadMarkdownTemplate();
366
+ const projectStructure = buildProjectStructure(files, detection, modules, hasApi, hasDb, workflows);
367
+ const prompt = buildMarkdownPrompt(template, docsPath, projectStructure);
368
+
369
+ const model = options.model || config.model || 'opus';
370
+ const emitter = runClaude(prompt, { model });
371
+ attachDisplay(emitter);
372
+
373
+ const claudeError = await waitForClaude(emitter);
374
+
375
+ if (claudeError) {
376
+ warn('Claude encountered an error — skeleton docs preserved');
377
+ } else {
378
+ success('Markdown documentation generated');
379
+ }
380
+ }
381
+
382
+ async function getLastChangelogDate(basePath) {
383
+ try {
384
+ const changelogPath = join(basePath, 'CHANGELOG.md');
385
+ const content = await readFile(changelogPath, 'utf-8');
386
+ const match = content.match(/^## (\d{4}-\d{2}-\d{2})/m);
387
+ return match ? match[1] : null;
388
+ } catch {
389
+ return null;
390
+ }
391
+ }
392
+
393
+ export function getGitLog(basePath, since) {
394
+ const cmd = `git log --pretty='format:%H|%ai|%s' --no-merges${since ? ` --since=${since}` : ''}`;
395
+ try {
396
+ const output = execSync(cmd, {
397
+ cwd: resolve(basePath),
398
+ stdio: ['pipe', 'pipe', 'pipe'],
399
+ encoding: 'utf-8',
400
+ });
401
+ const trimmed = output.trim();
402
+ if (!trimmed) return [];
403
+ return trimmed.split('\n').map(line => {
404
+ const [hash, date, ...msgParts] = line.split('|');
405
+ return { hash, date: date.split(' ')[0], message: msgParts.join('|') };
406
+ });
407
+ } catch {
408
+ return [];
409
+ }
410
+ }
411
+
412
+ export function groupCommitsByDate(commits) {
413
+ const groups = {};
414
+ for (const c of commits) {
415
+ (groups[c.date] ??= []).push(c);
416
+ }
417
+ return Object.entries(groups)
418
+ .sort(([a], [b]) => b.localeCompare(a))
419
+ .map(([date, items]) => ({ date, commits: items }));
420
+ }
421
+
422
+ export function buildChangelogPrompt(grouped) {
423
+ const lines = [
424
+ 'You are writing a CHANGELOG for a non-technical audience (managers, stakeholders).',
425
+ 'Translate the following git commits into plain-language sections.',
426
+ '',
427
+ 'Rules:',
428
+ '- Group changes under these sections: **New Features**, **Improvements**, **Bug Fixes**',
429
+ '- Only include sections that have entries — omit empty sections',
430
+ '- Use simple, jargon-free language focused on user-facing impact',
431
+ '- Do NOT reference file names, function names, or technical implementation details',
432
+ '- Each entry should be a single clear sentence',
433
+ '- Group by the date headings provided',
434
+ '- Output ONLY valid markdown — no code fences, no preamble, no explanation',
435
+ '- Start with a top-level heading: # Changelog',
436
+ '- Use ## YYYY-MM-DD for date headings',
437
+ '- Use ### for section headings (New Features, Improvements, Bug Fixes)',
438
+ '- Use bullet points for entries',
439
+ '',
440
+ 'Commits:',
441
+ '',
442
+ ];
443
+ for (const { date, commits } of grouped) {
444
+ lines.push(`Date: ${date}`);
445
+ for (const c of commits) {
446
+ lines.push(` - ${c.message}`);
447
+ }
448
+ lines.push('');
449
+ }
450
+ return lines.join('\n');
451
+ }
452
+
453
+ export async function generateChangelog(config = {}) {
454
+ const basePath = config.path || process.cwd();
455
+
456
+ if (!checkClaudeCli()) {
457
+ fail('Claude CLI not found. Install it from https://docs.anthropic.com/en/docs/claude-code');
458
+ return;
459
+ }
460
+
461
+ const since = await getLastChangelogDate(basePath);
462
+ const commits = getGitLog(basePath, since);
463
+
464
+ if (commits.length === 0) {
465
+ warn('No commits found for changelog generation.');
466
+ return;
467
+ }
468
+
469
+ header('Golem Changelog');
470
+ info(`Found ${commits.length} commit(s)${since ? ` since ${since}` : ''}`);
471
+
472
+ const grouped = groupCommitsByDate(commits);
473
+ const prompt = buildChangelogPrompt(grouped);
474
+
475
+ const model = config.model || 'sonnet';
476
+ const emitter = runClaude(prompt, { model, allowedTools: [] });
477
+ attachDisplay(emitter);
478
+
479
+ let result = '';
480
+ emitter.on('assistant', (event) => {
481
+ if (event.message?.content) {
482
+ for (const block of event.message.content) {
483
+ if (block.type === 'text') {
484
+ result += block.text;
485
+ }
486
+ }
487
+ }
488
+ });
489
+
490
+ const claudeError = await waitForClaude(emitter);
491
+
492
+ if (claudeError || !result.trim()) {
493
+ warn('Changelog generation failed — no output from Claude');
494
+ return;
495
+ }
496
+
497
+ const changelogPath = join(basePath, 'CHANGELOG.md');
498
+ await fsWriteFile(changelogPath, result.trim() + '\n', 'utf-8');
499
+ success(`CHANGELOG.md written (${commits.length} commits)`);
500
+ }
501
+
502
+ async function readProjectInfo(basePath) {
503
+ // Try package.json first
504
+ try {
505
+ const pkgPath = join(basePath, 'package.json');
506
+ const content = await readFile(pkgPath, 'utf-8');
507
+ const pkg = JSON.parse(content);
508
+ return {
509
+ name: pkg.name || 'Untitled Project',
510
+ description: pkg.description || '',
511
+ language: 'javascript',
512
+ };
513
+ } catch {
514
+ // Ignore package.json errors
515
+ }
516
+
517
+ // Try pyproject.toml
518
+ try {
519
+ const pyPath = join(basePath, 'pyproject.toml');
520
+ const content = await readFile(pyPath, 'utf-8');
521
+ const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
522
+ const descMatch = content.match(/^\s*description\s*=\s*"([^"]+)"/m);
523
+ return {
524
+ name: nameMatch ? nameMatch[1] : 'Untitled Project',
525
+ description: descMatch ? descMatch[1] : '',
526
+ language: 'python',
527
+ };
528
+ } catch {
529
+ // Ignore pyproject.toml errors
530
+ }
531
+
532
+ return {
533
+ name: 'Untitled Project',
534
+ description: '',
535
+ language: 'unknown',
536
+ };
537
+ }
538
+
539
+ const README_MARKERS = {
540
+ start: '<!-- GOLEM_GENERATED_START -->',
541
+ end: '<!-- GOLEM_GENERATED_END -->',
542
+ };
543
+
544
+ function buildReadmeContent(projectInfo, docsPath) {
545
+ const lines = [];
546
+ lines.push(`# ${projectInfo.name}`);
547
+ lines.push('');
548
+ if (projectInfo.description) {
549
+ lines.push(projectInfo.description);
550
+ lines.push('');
551
+ }
552
+ lines.push('## Documentation');
553
+ lines.push('');
554
+ lines.push(`This project includes comprehensive documentation in the [\`${docsPath}/\`](./${docsPath}/) directory:`);
555
+ lines.push('');
556
+ lines.push(`- [Overview](./${docsPath}/index.md) — Project overview and structure`);
557
+ lines.push(`- [Architecture](./${docsPath}/architecture.md) — System design and component connections`);
558
+ lines.push(`- [Getting Started](./${docsPath}/getting-started.md) — Setup and installation guide`);
559
+ lines.push(`- [Configuration](./${docsPath}/configuration.md) — All configuration options`);
560
+ lines.push('');
561
+ return lines.join('\n');
562
+ }
563
+
564
+ async function readExistingReadme(basePath) {
565
+ try {
566
+ return await readFile(join(basePath, 'README.md'), 'utf-8');
567
+ } catch {
568
+ return null;
569
+ }
570
+ }
571
+
572
+ function extractUserContent(existing) {
573
+ if (!existing) return null;
574
+
575
+ const startIdx = existing.indexOf(README_MARKERS.start);
576
+ const endIdx = existing.indexOf(README_MARKERS.end);
577
+
578
+ if (startIdx === -1 || endIdx === -1) {
579
+ // No markers found — treat entire README as user content
580
+ return existing;
581
+ }
582
+
583
+ // Extract content before and after markers
584
+ const before = existing.slice(0, startIdx).trim();
585
+ const after = existing.slice(endIdx + README_MARKERS.end.length).trim();
586
+
587
+ if (!before && !after) return null;
588
+ return { before, after };
589
+ }
590
+
591
+ function mergeReadmeContent(generated, userContent) {
592
+ if (!userContent) {
593
+ return `${README_MARKERS.start}\n${generated}\n${README_MARKERS.end}\n`;
594
+ }
595
+
596
+ if (typeof userContent === 'string') {
597
+ // Entire README was user content — prepend generated section
598
+ return `${README_MARKERS.start}\n${generated}\n${README_MARKERS.end}\n\n${userContent}`;
599
+ }
600
+
601
+ // User content was split around markers
602
+ const parts = [];
603
+ if (userContent.before) parts.push(userContent.before);
604
+ parts.push(`${README_MARKERS.start}\n${generated}\n${README_MARKERS.end}`);
605
+ if (userContent.after) parts.push(userContent.after);
606
+ return parts.join('\n\n') + '\n';
607
+ }
608
+
609
+ export async function generateReadme(detection, config = {}) {
610
+ const basePath = config.path || process.cwd();
611
+ const docsPath = config.docsPath || 'docs';
612
+
613
+ const projectInfo = await readProjectInfo(basePath);
614
+ const existing = await readExistingReadme(basePath);
615
+ const userContent = extractUserContent(existing);
616
+
617
+ const generated = buildReadmeContent(projectInfo, docsPath);
618
+ const final = mergeReadmeContent(generated, userContent);
619
+
620
+ const readmePath = join(basePath, 'README.md');
621
+ await fsWriteFile(readmePath, final, 'utf-8');
622
+
623
+ if (existing) {
624
+ success('README.md updated (preserved existing content)');
625
+ } else {
626
+ success('README.md created');
627
+ }
628
+ }
629
+
630
+ function addDeprecatedFrontmatter(content, date) {
631
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
632
+ if (!fmMatch) {
633
+ // No frontmatter — add one with deprecated fields
634
+ const fm = `---\ndeprecated_date: "${date}"\nreplaced_by: ""\n---\n`;
635
+ return fm + content;
636
+ }
637
+
638
+ let frontmatter = fmMatch[1];
639
+ // Add deprecated_date and replaced_by before closing ---
640
+ frontmatter += `\ndeprecated_date: "${date}"\nreplaced_by: ""`;
641
+ return `---\n${frontmatter}\n---` + content.slice(fmMatch[0].length);
642
+ }
643
+
644
+ async function listMdFiles(dir) {
645
+ try {
646
+ const entries = await readdir(dir);
647
+ return entries.filter(f => f.endsWith('.md'));
648
+ } catch {
649
+ return [];
650
+ }
651
+ }
652
+
653
+ export async function archiveDeprecated(docsPath, basePath) {
654
+ const projectRoot = basePath || process.cwd();
655
+ const files = gitListFiles(projectRoot);
656
+ const today = new Date().toISOString().split('T')[0];
657
+ const moved = [];
658
+
659
+ // Scan modules/ docs
660
+ const modulesDir = join(docsPath, 'modules');
661
+ const moduleDocs = await listMdFiles(modulesDir);
662
+
663
+ for (const docFile of moduleDocs) {
664
+ const moduleName = docFile.replace(/\.md$/, '');
665
+ const hasSource = files.some(f => f.startsWith(moduleName + '/'));
666
+
667
+ if (!hasSource) {
668
+ const srcPath = join(modulesDir, docFile);
669
+ const deprecatedDir = join(docsPath, 'deprecated');
670
+ await mkdir(deprecatedDir, { recursive: true });
671
+ const destPath = join(deprecatedDir, docFile);
672
+
673
+ const content = await readFile(srcPath, 'utf-8');
674
+ const updated = addDeprecatedFrontmatter(content, today);
675
+ await fsWriteFile(destPath, updated, 'utf-8');
676
+
677
+ // Remove original (move = write to dest + delete source)
678
+ await unlink(srcPath);
679
+
680
+ moved.push({ from: srcPath, to: destPath, module: moduleName });
681
+ }
682
+ }
683
+
684
+ return { moved, date: today };
685
+ }
686
+
687
+ export async function runDocument(opts = {}) {
688
+ const basePath = opts.path || process.cwd();
689
+ const config = await loadConfig();
690
+ const docsPath = opts.docsPath || config.docsPath || 'docs';
691
+ const dryRun = opts.dryRun === true;
692
+ const inlineOnly = opts.inlineOnly === true;
693
+ const markdownOnly = opts.markdownOnly === true;
694
+
695
+ header('Golem Document');
696
+ const detection = await detectProject(basePath);
697
+ info(`Project: ${detection.language} / ${detection.framework} (${detection.inlineDocStandard})`);
698
+ info(`Docs path: ${docsPath}`);
699
+
700
+ if (dryRun) {
701
+ info('Dry run — no files will be changed');
702
+ }
703
+
704
+ const summary = { inlineFiles: 0, markdownFiles: 0, changelogCommits: 0, readmeAction: 'none', deprecated: 0 };
705
+
706
+ // Step 1: Inline docs (unless --markdown-only)
707
+ if (!markdownOnly) {
708
+ const files = discoverSourceFiles(basePath, detection);
709
+ summary.inlineFiles = files.length;
710
+ if (dryRun) {
711
+ info(`[dry-run] Would document ${files.length} source file(s) with inline ${detection.inlineDocStandard}`);
712
+ } else if (files.length > 0) {
713
+ await runDocumentInline({ path: basePath, model: opts.model || config.model });
714
+ } else {
715
+ warn('No source files found for inline documentation.');
716
+ }
717
+ }
718
+
719
+ // Step 2: Markdown generation (unless --inline-only)
720
+ if (!inlineOnly) {
721
+ const allFiles = gitListFiles(basePath);
722
+ const modules = detectModules(allFiles);
723
+ const hasApi = allFiles.some(f => API_PATTERNS.some(p => p.test(f)));
724
+ const hasDb = (detection?.databaseTooling && detection.databaseTooling !== 'none') || allFiles.some(f => DB_PATTERNS.some(p => p.test(f)));
725
+ const workflows = detectGuideWorkflows(allFiles);
726
+
727
+ if (dryRun) {
728
+ const resolvedDocsPath = resolve(basePath, docsPath);
729
+ info(`[dry-run] Would generate markdown docs in ${resolvedDocsPath}`);
730
+ info(`[dry-run] Base files: index.md, architecture.md, getting-started.md, configuration.md`);
731
+ if (modules.length > 0) info(`[dry-run] Module docs: ${modules.join(', ')}`);
732
+ if (hasApi) info('[dry-run] API documentation: yes');
733
+ if (hasDb) info(`[dry-run] Database documentation: yes (${detection.databaseTooling})`);
734
+ if (workflows.length > 0) info(`[dry-run] Guides: ${workflows.join(', ')}`);
735
+ summary.markdownFiles = 4 + modules.length + (hasApi ? 1 : 0) + (hasDb ? 1 : 0) + (workflows.length > 0 ? 1 + workflows.length : 0);
736
+ } else {
737
+ await runDocumentMarkdown({ path: basePath, docsPath, model: opts.model || config.model });
738
+ }
739
+
740
+ // Step 3: Changelog
741
+ const commits = getGitLog(basePath);
742
+ summary.changelogCommits = commits.length;
743
+ if (dryRun) {
744
+ info(`[dry-run] Would generate CHANGELOG.md from ${commits.length} commit(s)`);
745
+ } else if (commits.length > 0) {
746
+ await generateChangelog({ path: basePath, model: opts.model || config.model });
747
+ } else {
748
+ warn('No commits found for changelog.');
749
+ }
750
+
751
+ // Step 4: README
752
+ try {
753
+ const existing = await readFile(join(basePath, 'README.md'), 'utf-8');
754
+ summary.readmeAction = 'update';
755
+ } catch {
756
+ summary.readmeAction = 'create';
757
+ }
758
+ if (dryRun) {
759
+ info(`[dry-run] Would ${summary.readmeAction} README.md`);
760
+ } else {
761
+ await generateReadme(detection, { path: basePath, docsPath });
762
+ }
763
+
764
+ // Step 5: Archive deprecated
765
+ const resolvedDocsPath = resolve(basePath, docsPath);
766
+ if (dryRun) {
767
+ info('[dry-run] Would check for deprecated documentation to archive');
768
+ } else {
769
+ const { moved } = await archiveDeprecated(resolvedDocsPath, basePath);
770
+ summary.deprecated = moved.length;
771
+ if (moved.length > 0) {
772
+ info(`Archived ${moved.length} deprecated doc(s)`);
773
+ }
774
+ }
775
+ }
776
+
777
+ // Summary
778
+ header('Document Summary');
779
+ if (!markdownOnly) info(`Inline docs: ${summary.inlineFiles} source file(s)`);
780
+ if (!inlineOnly) {
781
+ info(`Markdown files: ${summary.markdownFiles || 'generated'}`);
782
+ info(`Changelog: ${summary.changelogCommits} commit(s)`);
783
+ info(`README: ${summary.readmeAction}`);
784
+ if (summary.deprecated > 0) info(`Deprecated: ${summary.deprecated} archived`);
785
+ }
786
+
787
+ if (dryRun) {
788
+ success('Dry run complete — no changes made');
789
+ } else {
790
+ success('Documentation pass complete');
791
+ }
792
+ }