ted-mosby 1.0.0 → 1.1.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/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import inquirer from 'inquirer';
10
10
  import inquirerAutocomplete from 'inquirer-autocomplete-prompt';
11
11
  import { DevelopmentAgentAgent } from './agent.js';
12
12
  import { ArchitecturalWikiAgent } from './wiki-agent.js';
13
+ import { SiteGenerator } from './site-generator.js';
13
14
  import { ConfigManager } from './config.js';
14
15
  import { PermissionManager } from './permissions.js';
15
16
  import { PlanManager, formatAge } from './planner.js';
@@ -62,6 +63,16 @@ program
62
63
  .option('-f, --force', 'Force regeneration (ignore cache)')
63
64
  .option('-v, --verbose', 'Verbose output')
64
65
  .option('-e, --estimate', 'Estimate time and cost without running (dry run)')
66
+ .option('-s, --site', 'Generate interactive static site from wiki')
67
+ .option('--site-only', 'Only generate static site (skip wiki generation, use existing markdown)')
68
+ .option('--site-title <title>', 'Site title for static site')
69
+ .option('--theme <theme>', 'Site theme: light, dark, or auto', 'auto')
70
+ .option('--max-chunks <number>', 'Maximum chunks to index (for large codebases, e.g., 5000)', parseInt)
71
+ .option('--max-results <number>', 'Maximum search results per query (default 10, reduce for large codebases)', parseInt)
72
+ .option('--batch-size <number>', 'Enable batched mode: process codebase in batches of N chunks (for very large repos)', parseInt)
73
+ .option('--skip-index', 'Skip indexing and use existing cached index (for debugging agent behavior)')
74
+ .option('--max-turns <number>', 'Maximum agent turns (default 200, lower to reduce cost estimate)', parseInt)
75
+ .option('--direct-api', 'Use Anthropic API directly (bypasses Claude Code billing, uses your API credits)')
65
76
  .action(async (options) => {
66
77
  try {
67
78
  const configManager = new ConfigManager();
@@ -135,23 +146,52 @@ program
135
146
  }
136
147
  return;
137
148
  }
149
+ // Handle --site-only mode (generate site from existing markdown)
150
+ if (options.siteOnly) {
151
+ await generateStaticSite(options);
152
+ return;
153
+ }
138
154
  const spinner = ora('Starting wiki generation...').start();
139
155
  let currentPhase = '';
156
+ // Choose generation method based on options
157
+ const generationOptions = {
158
+ repoUrl: options.repo,
159
+ outputDir: options.output,
160
+ configPath: options.config,
161
+ accessToken: options.token || process.env.GITHUB_TOKEN,
162
+ model: options.model,
163
+ targetPath: options.path,
164
+ forceRegenerate: options.force,
165
+ verbose: options.verbose,
166
+ maxChunks: options.maxChunks,
167
+ maxSearchResults: options.maxResults,
168
+ batchSize: options.batchSize,
169
+ skipIndex: options.skipIndex,
170
+ maxTurns: options.maxTurns,
171
+ directApi: options.directApi
172
+ };
173
+ // Choose generator based on options
174
+ let generator;
175
+ if (options.directApi) {
176
+ console.log(chalk.yellow('\n⚡ Direct API mode: Using Anthropic API directly (bypasses Claude Code)\n'));
177
+ generator = agent.generateWikiDirectApi(generationOptions);
178
+ }
179
+ else if (options.skipIndex) {
180
+ console.log(chalk.yellow('\n⚡ Skip-index mode: Using existing cached index (debug mode)\n'));
181
+ generator = agent.generateWikiAgentOnly(generationOptions);
182
+ }
183
+ else if (options.batchSize) {
184
+ generator = agent.generateWikiBatched(generationOptions);
185
+ }
186
+ else {
187
+ generator = agent.generateWiki(generationOptions);
188
+ }
140
189
  try {
141
- for await (const event of agent.generateWiki({
142
- repoUrl: options.repo,
143
- outputDir: options.output,
144
- configPath: options.config,
145
- accessToken: options.token || process.env.GITHUB_TOKEN,
146
- model: options.model,
147
- targetPath: options.path,
148
- forceRegenerate: options.force,
149
- verbose: options.verbose
150
- })) {
190
+ for await (const event of generator) {
151
191
  // Handle progress events
152
192
  if (event.type === 'phase') {
153
- // Stop spinner during indexing phase so RAG output is visible
154
- if (event.message.includes('Indexing')) {
193
+ // Stop spinner during indexing/batch phases so RAG output is visible
194
+ if (event.message.includes('Indexing') || event.message.includes('batch') || event.message.includes('Analyzing') || event.message.includes('Finalizing')) {
155
195
  spinner.stop();
156
196
  console.log(chalk.cyan(`\n📊 ${event.message}`));
157
197
  }
@@ -165,10 +205,13 @@ program
165
205
  }
166
206
  }
167
207
  else if (event.type === 'step') {
168
- // Resume spinner after indexing completes
169
- if (event.message.includes('Indexed')) {
208
+ // Show step messages for batch progress
209
+ if (event.message.includes('Indexed') || event.message.includes('Batch') || event.message.includes('Found') || event.message.includes('Final index') || event.message.includes('chunks loaded')) {
170
210
  console.log(chalk.green(` ✓ ${event.message}`));
171
- spinner.start('Generating architectural documentation');
211
+ // Resume spinner after last step before agent runs
212
+ if (event.message.includes('chunks loaded') || event.message.includes('Final index')) {
213
+ spinner.start('Generating architectural documentation...');
214
+ }
172
215
  }
173
216
  else if (options.verbose) {
174
217
  spinner.info(event.message);
@@ -212,7 +255,14 @@ program
212
255
  }
213
256
  console.log();
214
257
  console.log(chalk.cyan('📁 Wiki generated at:'), chalk.white(path.resolve(options.output)));
215
- console.log(chalk.gray('Open wiki/README.md to start exploring the documentation.'));
258
+ // Generate static site if requested
259
+ if (options.site || options.siteOnly) {
260
+ await generateStaticSite(options);
261
+ }
262
+ else {
263
+ console.log(chalk.gray('Open wiki/README.md to start exploring the documentation.'));
264
+ console.log(chalk.gray('Tip: Add --site flag to generate an interactive website.'));
265
+ }
216
266
  console.log();
217
267
  }
218
268
  catch (error) {
@@ -228,6 +278,373 @@ program
228
278
  process.exit(1);
229
279
  }
230
280
  });
281
+ // Update-docs command - incremental documentation updates
282
+ program
283
+ .command('update-docs')
284
+ .description('Update documentation based on changes since last index')
285
+ .requiredOption('-r, --repo <path>', 'Repository path (local)')
286
+ .option('-o, --output <dir>', 'Output directory for wiki', './wiki')
287
+ .option('-m, --model <model>', 'Claude model to use', 'claude-sonnet-4-20250514')
288
+ .option('-v, --verbose', 'Verbose output')
289
+ .action(async (options) => {
290
+ try {
291
+ const configManager = new ConfigManager();
292
+ const config = await configManager.load();
293
+ if (!configManager.hasApiKey()) {
294
+ console.log(chalk.red('❌ No API key found.'));
295
+ console.log(chalk.yellow('\nSet your Anthropic API key:'));
296
+ console.log(chalk.gray(' export ANTHROPIC_API_KEY=your-key-here'));
297
+ process.exit(1);
298
+ }
299
+ const wikiDir = path.resolve(options.output);
300
+ const cacheDir = path.join(wikiDir, '.ted-mosby-cache');
301
+ const indexStatePath = path.join(cacheDir, 'index-state.json');
302
+ // Check if we have an existing index
303
+ if (!fs.existsSync(indexStatePath)) {
304
+ console.log(chalk.yellow('⚠️ No existing index found.'));
305
+ console.log(chalk.gray('Run `ted-mosby generate` first to create the initial documentation.'));
306
+ process.exit(1);
307
+ }
308
+ const indexState = JSON.parse(fs.readFileSync(indexStatePath, 'utf-8'));
309
+ console.log(chalk.cyan.bold('\n📝 Documentation Update\n'));
310
+ console.log(chalk.white('Last indexed:'), chalk.green(indexState.commitHash.slice(0, 7)));
311
+ console.log(chalk.white('Indexed at:'), chalk.green(new Date(indexState.indexedAt).toLocaleString()));
312
+ console.log();
313
+ // Get changed files since last index
314
+ const git = (await import('simple-git')).simpleGit(options.repo);
315
+ const currentLog = await git.log({ maxCount: 1 });
316
+ const currentCommit = currentLog.latest?.hash || 'unknown';
317
+ if (currentCommit === indexState.commitHash) {
318
+ console.log(chalk.green('✓ Documentation is up to date. No changes since last index.'));
319
+ return;
320
+ }
321
+ const diffResult = await git.diff(['--name-only', indexState.commitHash, 'HEAD']);
322
+ const changedFiles = diffResult.split('\n').filter(f => f.trim().length > 0);
323
+ if (changedFiles.length === 0) {
324
+ console.log(chalk.green('✓ No relevant file changes detected.'));
325
+ return;
326
+ }
327
+ console.log(chalk.white('Current commit:'), chalk.green(currentCommit.slice(0, 7)));
328
+ console.log(chalk.white('Changed files:'), chalk.yellow(changedFiles.length.toString()));
329
+ console.log();
330
+ // Show changed files
331
+ console.log(chalk.white.bold('Files changed since last index:'));
332
+ const relevantExts = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java'];
333
+ const relevantFiles = changedFiles.filter(f => relevantExts.some(ext => f.endsWith(ext)));
334
+ for (const file of relevantFiles.slice(0, 15)) {
335
+ console.log(chalk.gray(` • ${file}`));
336
+ }
337
+ if (relevantFiles.length > 15) {
338
+ console.log(chalk.gray(` ... and ${relevantFiles.length - 15} more`));
339
+ }
340
+ console.log();
341
+ // Prompt for confirmation
342
+ const { confirm } = await inquirer.prompt([{
343
+ type: 'confirm',
344
+ name: 'confirm',
345
+ message: `Update documentation for ${relevantFiles.length} changed files?`,
346
+ default: true
347
+ }]);
348
+ if (!confirm) {
349
+ console.log(chalk.yellow('\nUpdate cancelled.'));
350
+ return;
351
+ }
352
+ // Run incremental update
353
+ const spinner = ora('Updating documentation...').start();
354
+ const permissionManager = new PermissionManager({ policy: 'permissive' });
355
+ const agent = new ArchitecturalWikiAgent({
356
+ verbose: options.verbose,
357
+ apiKey: config.apiKey,
358
+ permissionManager
359
+ });
360
+ // TODO: Implement incremental update mode in agent
361
+ // For now, re-run full generation but the RAG index is cached
362
+ // Future: Pass changedFiles to agent for targeted updates
363
+ try {
364
+ for await (const event of agent.generateWiki({
365
+ repoUrl: options.repo,
366
+ outputDir: options.output,
367
+ model: options.model,
368
+ verbose: options.verbose,
369
+ forceRegenerate: true // Force re-index to update commit hash
370
+ })) {
371
+ if (event.type === 'phase') {
372
+ spinner.text = event.message;
373
+ }
374
+ else if (event.type === 'complete') {
375
+ spinner.succeed(chalk.green('Documentation updated!'));
376
+ }
377
+ else if (event.type === 'error') {
378
+ spinner.fail(chalk.red(event.message));
379
+ }
380
+ }
381
+ console.log();
382
+ console.log(chalk.cyan('📁 Wiki updated at:'), chalk.white(wikiDir));
383
+ console.log(chalk.gray('Tip: Run with --site flag to regenerate the static site.'));
384
+ console.log();
385
+ }
386
+ catch (error) {
387
+ spinner.fail('Update failed');
388
+ throw error;
389
+ }
390
+ }
391
+ catch (error) {
392
+ console.error(chalk.red('\nError:'), error instanceof Error ? error.message : String(error));
393
+ process.exit(1);
394
+ }
395
+ });
396
+ // Continue-generation command - complete missing wiki pages
397
+ program
398
+ .command('continue')
399
+ .description('Continue generating missing wiki pages (verifies completeness and creates missing pages)')
400
+ .requiredOption('-r, --repo <path>', 'Repository path (local)')
401
+ .option('-o, --output <dir>', 'Output directory for wiki', './wiki')
402
+ .option('-m, --model <model>', 'Claude model to use', 'claude-sonnet-4-20250514')
403
+ .option('-v, --verbose', 'Verbose output')
404
+ .option('--verify-only', 'Only verify completeness, do not generate missing pages')
405
+ .option('--skip-index', 'Skip indexing and use existing cached index')
406
+ .option('--direct-api', 'Use Anthropic API directly (bypasses Claude Code billing)')
407
+ .option('--max-turns <number>', 'Maximum agent turns (default 200)', parseInt)
408
+ .action(async (options) => {
409
+ try {
410
+ const configManager = new ConfigManager();
411
+ const config = await configManager.load();
412
+ if (!configManager.hasApiKey()) {
413
+ console.log(chalk.red('❌ No API key found.'));
414
+ console.log(chalk.yellow('\nSet your Anthropic API key:'));
415
+ console.log(chalk.gray(' export ANTHROPIC_API_KEY=your-key-here'));
416
+ process.exit(1);
417
+ }
418
+ const wikiDir = path.resolve(options.output);
419
+ // Check if wiki directory exists
420
+ if (!fs.existsSync(wikiDir)) {
421
+ console.log(chalk.red('❌ Wiki directory not found: ' + wikiDir));
422
+ console.log(chalk.gray('Run `ted-mosby generate` first to create the wiki.'));
423
+ process.exit(1);
424
+ }
425
+ console.log(chalk.cyan.bold('\n📋 Wiki Completeness Check\n'));
426
+ // Use the agent's verification method
427
+ const permissionManager = new PermissionManager({ policy: 'permissive' });
428
+ const agent = new ArchitecturalWikiAgent({
429
+ verbose: options.verbose,
430
+ apiKey: config.apiKey,
431
+ permissionManager
432
+ });
433
+ const verification = await agent.verifyWikiCompleteness(wikiDir);
434
+ console.log(chalk.white('Total pages:'), chalk.green(verification.totalPages.toString()));
435
+ console.log(chalk.white('Broken links:'), verification.brokenLinks.length > 0
436
+ ? chalk.red(verification.brokenLinks.length.toString())
437
+ : chalk.green('0'));
438
+ console.log(chalk.white('Missing pages:'), verification.missingPages.length > 0
439
+ ? chalk.red(verification.missingPages.length.toString())
440
+ : chalk.green('0'));
441
+ console.log();
442
+ if (verification.isComplete) {
443
+ console.log(chalk.green('✅ Wiki is complete! All internal links are valid.'));
444
+ return;
445
+ }
446
+ // Show missing pages
447
+ console.log(chalk.yellow.bold('Missing pages:'));
448
+ const uniqueMissing = [...new Set(verification.brokenLinks.map(l => l.target))];
449
+ for (const page of uniqueMissing.slice(0, 20)) {
450
+ console.log(chalk.gray(` • ${page}`));
451
+ }
452
+ if (uniqueMissing.length > 20) {
453
+ console.log(chalk.gray(` ... and ${uniqueMissing.length - 20} more`));
454
+ }
455
+ console.log();
456
+ if (options.verifyOnly) {
457
+ console.log(chalk.yellow('Verification complete. Run without --verify-only to generate missing pages.'));
458
+ process.exit(verification.isComplete ? 0 : 1);
459
+ }
460
+ // Prompt for confirmation
461
+ const { confirm } = await inquirer.prompt([{
462
+ type: 'confirm',
463
+ name: 'confirm',
464
+ message: `Generate ${uniqueMissing.length} missing pages?`,
465
+ default: true
466
+ }]);
467
+ if (!confirm) {
468
+ console.log(chalk.yellow('\nGeneration cancelled.'));
469
+ return;
470
+ }
471
+ // Run continuation
472
+ const spinner = ora('Generating missing pages...').start();
473
+ // Choose generator based on options
474
+ const generationOptions = {
475
+ repoUrl: options.repo,
476
+ outputDir: options.output,
477
+ model: options.model,
478
+ verbose: options.verbose,
479
+ missingPages: uniqueMissing, // Pass missing pages to agent for targeted generation
480
+ skipIndex: options.skipIndex,
481
+ directApi: options.directApi,
482
+ maxTurns: options.maxTurns
483
+ };
484
+ let generator;
485
+ if (options.directApi) {
486
+ console.log(chalk.yellow('\n⚡ Direct API mode: Using Anthropic API directly\n'));
487
+ generator = agent.generateWikiDirectApi(generationOptions);
488
+ }
489
+ else if (options.skipIndex) {
490
+ console.log(chalk.yellow('\n⚡ Skip-index mode: Using existing cached index\n'));
491
+ generator = agent.generateWikiAgentOnly(generationOptions);
492
+ }
493
+ else {
494
+ generator = agent.generateWiki(generationOptions);
495
+ }
496
+ try {
497
+ for await (const event of generator) {
498
+ if (event.type === 'phase') {
499
+ spinner.text = event.message;
500
+ }
501
+ else if (event.type === 'complete') {
502
+ spinner.succeed(chalk.green('Missing pages generated!'));
503
+ }
504
+ else if (event.type === 'error') {
505
+ spinner.fail(chalk.red(event.message));
506
+ }
507
+ }
508
+ // Verify again
509
+ console.log();
510
+ console.log(chalk.cyan('Verifying completeness...'));
511
+ const postVerification = await agent.verifyWikiCompleteness(wikiDir);
512
+ if (postVerification.isComplete) {
513
+ console.log(chalk.green('✅ Wiki is now complete! All internal links are valid.'));
514
+ }
515
+ else {
516
+ console.log(chalk.yellow(`⚠️ ${postVerification.brokenLinks.length} broken links remain.`));
517
+ console.log(chalk.gray('Run `ted-mosby continue` again to generate remaining pages.'));
518
+ }
519
+ console.log();
520
+ console.log(chalk.cyan('📁 Wiki at:'), chalk.white(wikiDir));
521
+ console.log();
522
+ }
523
+ catch (error) {
524
+ spinner.fail('Generation failed');
525
+ throw error;
526
+ }
527
+ }
528
+ catch (error) {
529
+ console.error(chalk.red('\nError:'), error instanceof Error ? error.message : String(error));
530
+ process.exit(1);
531
+ }
532
+ });
533
+ // Verify command - check wiki completeness without generating
534
+ program
535
+ .command('verify')
536
+ .description('Verify wiki completeness and report broken links')
537
+ .option('-o, --output <dir>', 'Wiki directory to verify', './wiki')
538
+ .option('--json', 'Output as JSON')
539
+ .action(async (options) => {
540
+ try {
541
+ const wikiDir = path.resolve(options.output);
542
+ if (!fs.existsSync(wikiDir)) {
543
+ console.log(chalk.red('❌ Wiki directory not found: ' + wikiDir));
544
+ process.exit(1);
545
+ }
546
+ const configManager = new ConfigManager();
547
+ const config = await configManager.load();
548
+ const permissionManager = new PermissionManager({ policy: 'permissive' });
549
+ const agent = new ArchitecturalWikiAgent({
550
+ apiKey: config.apiKey,
551
+ permissionManager
552
+ });
553
+ const verification = await agent.verifyWikiCompleteness(wikiDir);
554
+ if (options.json) {
555
+ console.log(JSON.stringify(verification, null, 2));
556
+ process.exit(verification.isComplete ? 0 : 1);
557
+ }
558
+ console.log(chalk.cyan.bold('\n📋 Wiki Completeness Report\n'));
559
+ console.log(chalk.white('Total pages:'), chalk.green(verification.totalPages.toString()));
560
+ console.log(chalk.white('Broken links:'), verification.brokenLinks.length > 0
561
+ ? chalk.red(verification.brokenLinks.length.toString())
562
+ : chalk.green('0'));
563
+ console.log();
564
+ if (verification.isComplete) {
565
+ console.log(chalk.green('✅ Wiki is complete! All internal links are valid.'));
566
+ }
567
+ else {
568
+ console.log(chalk.yellow.bold('Broken links found:'));
569
+ // Group by target
570
+ const byTarget = new Map();
571
+ for (const link of verification.brokenLinks) {
572
+ if (!byTarget.has(link.target)) {
573
+ byTarget.set(link.target, []);
574
+ }
575
+ byTarget.get(link.target).push(link.source);
576
+ }
577
+ for (const [target, sources] of byTarget) {
578
+ console.log(chalk.red(`\n Missing: ${target}`));
579
+ console.log(chalk.gray(` Referenced by:`));
580
+ for (const source of sources.slice(0, 3)) {
581
+ console.log(chalk.gray(` - ${source}`));
582
+ }
583
+ if (sources.length > 3) {
584
+ console.log(chalk.gray(` ... and ${sources.length - 3} more`));
585
+ }
586
+ }
587
+ console.log();
588
+ console.log(chalk.yellow('Run `ted-mosby continue` to generate missing pages.'));
589
+ }
590
+ process.exit(verification.isComplete ? 0 : 1);
591
+ }
592
+ catch (error) {
593
+ console.error(chalk.red('\nError:'), error instanceof Error ? error.message : String(error));
594
+ process.exit(1);
595
+ }
596
+ });
597
+ // Static site generation helper
598
+ async function generateStaticSite(options) {
599
+ console.log(chalk.cyan.bold('\n🌐 Generating Interactive Static Site\n'));
600
+ const wikiDir = path.resolve(options.output);
601
+ const siteDir = path.join(path.dirname(wikiDir), 'site');
602
+ // Check if wiki directory exists
603
+ if (!fs.existsSync(wikiDir)) {
604
+ console.log(chalk.red('❌ Wiki directory not found: ' + wikiDir));
605
+ console.log(chalk.gray('Run without --site-only to generate wiki first.'));
606
+ process.exit(1);
607
+ }
608
+ const spinner = ora('Building static site...').start();
609
+ try {
610
+ const siteGenerator = new SiteGenerator({
611
+ wikiDir,
612
+ outputDir: siteDir,
613
+ title: options.siteTitle || 'Architecture Wiki',
614
+ description: 'Interactive architectural documentation',
615
+ theme: options.theme || 'auto',
616
+ features: {
617
+ guidedTour: false, // Disabled by default - can be enabled via flag
618
+ codeExplorer: true,
619
+ search: true,
620
+ progressTracking: true,
621
+ keyboardNav: true
622
+ },
623
+ repoUrl: options.repo || ''
624
+ });
625
+ await siteGenerator.generate();
626
+ spinner.succeed(chalk.green('Static site generated!'));
627
+ console.log();
628
+ console.log(chalk.cyan('🌐 Site generated at:'), chalk.white(siteDir));
629
+ console.log();
630
+ console.log(chalk.white.bold('Features included:'));
631
+ console.log(chalk.gray(' ✓ Interactive search (press "/" to open)'));
632
+ console.log(chalk.gray(' ✓ Guided tours for onboarding'));
633
+ console.log(chalk.gray(' ✓ Code explorer with syntax highlighting'));
634
+ console.log(chalk.gray(' ✓ Live Mermaid diagrams (click to zoom)'));
635
+ console.log(chalk.gray(' ✓ Dark/light theme toggle'));
636
+ console.log(chalk.gray(' ✓ Keyboard navigation (press "?" for help)'));
637
+ console.log(chalk.gray(' ✓ Progress tracking'));
638
+ console.log();
639
+ console.log(chalk.white('To preview locally:'));
640
+ console.log(chalk.gray(' npx serve ' + siteDir));
641
+ console.log();
642
+ }
643
+ catch (error) {
644
+ spinner.fail('Static site generation failed');
645
+ throw error;
646
+ }
647
+ }
231
648
  program
232
649
  .argument('[query]', 'Direct query to the agent')
233
650
  .option('-i, --interactive', 'Start interactive session')