openreport 0.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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/bin/openreport.ts +6 -0
  4. package/package.json +61 -0
  5. package/src/agents/api-documentation.ts +66 -0
  6. package/src/agents/architecture-analyst.ts +46 -0
  7. package/src/agents/code-quality-reviewer.ts +59 -0
  8. package/src/agents/dependency-analyzer.ts +51 -0
  9. package/src/agents/onboarding-guide.ts +59 -0
  10. package/src/agents/orchestrator.ts +41 -0
  11. package/src/agents/performance-analyzer.ts +57 -0
  12. package/src/agents/registry.ts +50 -0
  13. package/src/agents/security-auditor.ts +61 -0
  14. package/src/agents/test-coverage-analyst.ts +58 -0
  15. package/src/agents/todo-generator.ts +50 -0
  16. package/src/app/App.tsx +151 -0
  17. package/src/app/theme.ts +54 -0
  18. package/src/cli.ts +145 -0
  19. package/src/commands/init.ts +81 -0
  20. package/src/commands/interactive.tsx +29 -0
  21. package/src/commands/list.ts +53 -0
  22. package/src/commands/run.ts +168 -0
  23. package/src/commands/view.tsx +52 -0
  24. package/src/components/generation/AgentStatusItem.tsx +125 -0
  25. package/src/components/generation/AgentStatusList.tsx +70 -0
  26. package/src/components/generation/ProgressSummary.tsx +107 -0
  27. package/src/components/generation/StreamingOutput.tsx +154 -0
  28. package/src/components/layout/Container.tsx +24 -0
  29. package/src/components/layout/Footer.tsx +52 -0
  30. package/src/components/layout/Header.tsx +50 -0
  31. package/src/components/report/MarkdownRenderer.tsx +50 -0
  32. package/src/components/report/ReportCard.tsx +31 -0
  33. package/src/components/report/ScrollableView.tsx +164 -0
  34. package/src/config/cli-detection.ts +130 -0
  35. package/src/config/cli-model.ts +397 -0
  36. package/src/config/cli-prompt-formatter.ts +129 -0
  37. package/src/config/defaults.ts +79 -0
  38. package/src/config/loader.ts +168 -0
  39. package/src/config/ollama.ts +48 -0
  40. package/src/config/providers.ts +199 -0
  41. package/src/config/resolve-provider.ts +62 -0
  42. package/src/config/saver.ts +50 -0
  43. package/src/config/schema.ts +51 -0
  44. package/src/errors.ts +34 -0
  45. package/src/hooks/useReportGeneration.ts +199 -0
  46. package/src/hooks/useTerminalSize.ts +35 -0
  47. package/src/ingestion/context-selector.ts +247 -0
  48. package/src/ingestion/file-tree.ts +227 -0
  49. package/src/ingestion/token-budget.ts +52 -0
  50. package/src/pipeline/agent-runner.ts +360 -0
  51. package/src/pipeline/combiner.ts +199 -0
  52. package/src/pipeline/context.ts +108 -0
  53. package/src/pipeline/extraction.ts +153 -0
  54. package/src/pipeline/progress.ts +192 -0
  55. package/src/pipeline/runner.ts +526 -0
  56. package/src/report/html-renderer.ts +294 -0
  57. package/src/report/html-script.ts +123 -0
  58. package/src/report/html-styles.ts +1127 -0
  59. package/src/report/md-to-html.ts +153 -0
  60. package/src/report/open-browser.ts +22 -0
  61. package/src/schemas/findings.ts +48 -0
  62. package/src/schemas/report.ts +64 -0
  63. package/src/screens/ConfigScreen.tsx +271 -0
  64. package/src/screens/GenerationScreen.tsx +278 -0
  65. package/src/screens/HistoryScreen.tsx +108 -0
  66. package/src/screens/HomeScreen.tsx +143 -0
  67. package/src/screens/ViewerScreen.tsx +82 -0
  68. package/src/storage/metadata.ts +69 -0
  69. package/src/storage/report-store.ts +128 -0
  70. package/src/tools/get-file-tree.ts +157 -0
  71. package/src/tools/get-git-info.ts +123 -0
  72. package/src/tools/glob.ts +48 -0
  73. package/src/tools/grep.ts +149 -0
  74. package/src/tools/index.ts +30 -0
  75. package/src/tools/list-directory.ts +57 -0
  76. package/src/tools/read-file.ts +52 -0
  77. package/src/tools/read-package-json.ts +48 -0
  78. package/src/tools/run-command.ts +154 -0
  79. package/src/tools/shared-ignore.ts +58 -0
  80. package/src/types/index.ts +127 -0
  81. package/src/types/marked-terminal.d.ts +17 -0
  82. package/src/utils/debug.ts +25 -0
  83. package/src/utils/file-utils.ts +77 -0
  84. package/src/utils/format.ts +56 -0
  85. package/src/utils/grade-colors.ts +43 -0
  86. package/src/utils/project-detector.ts +296 -0
@@ -0,0 +1,526 @@
1
+ import { streamText } from "ai";
2
+ import type { LanguageModelV1 } from "ai";
3
+ import { ProgressTracker } from "./progress.js";
4
+ import { combineReport, countFindings, type CombineOptions } from "./combiner.js";
5
+ import { getAgentById, getAgentsForReportType } from "../agents/registry.js";
6
+ import { buildFileTree, fileTreeToString, flattenFileTree } from "../ingestion/file-tree.js";
7
+ import { getRelevantFilePaths } from "../ingestion/context-selector.js";
8
+ import { detectProject } from "../utils/project-detector.js";
9
+ import { PipelineError } from "../errors.js";
10
+ import { debugLog } from "../utils/debug.js";
11
+ import { getErrorMessage } from "../utils/format.js";
12
+ import type {
13
+ AgentId,
14
+ SubReport,
15
+ FullReport,
16
+ FileTreeNode,
17
+ ProjectClassification,
18
+ } from "../types/index.js";
19
+ import type { OpenReportConfig } from "../config/schema.js";
20
+ import { preReadAllFiles, buildContextInfo, type SharedContext } from "./context.js";
21
+ import { createBaseTools, createExtendedTools } from "../tools/index.js";
22
+ import {
23
+ runOrchestratorAgent,
24
+ runAgentWithTools,
25
+ runAgentWithCli,
26
+ withRetry,
27
+ } from "./agent-runner.js";
28
+ import { extractSubReport } from "./extraction.js";
29
+ import { isCliModel } from "../config/cli-model.js";
30
+
31
+ export interface RunPipelineOptions {
32
+ projectRoot: string;
33
+ reportType: string;
34
+ model: LanguageModelV1;
35
+ modelId: string;
36
+ config: OpenReportConfig;
37
+ progress: ProgressTracker;
38
+ signal?: AbortSignal;
39
+ generateTodoList?: boolean;
40
+ }
41
+
42
+ // Yield to event loop so React/Ink can render between phases
43
+ const tick = () => new Promise<void>((r) => setTimeout(r, 0));
44
+
45
+ // ── Phase 1: Scan project ─────────────────────────────────────────────
46
+
47
+ interface ScanResult {
48
+ fileTree: FileTreeNode[];
49
+ fileTreeString: string;
50
+ }
51
+
52
+ async function scanProject(
53
+ projectRoot: string,
54
+ config: OpenReportConfig,
55
+ progress: ProgressTracker
56
+ ): Promise<ScanResult> {
57
+ progress.setPhase("scanning");
58
+ await tick();
59
+
60
+ const fileTree = await buildFileTree(projectRoot, {
61
+ maxDepth: config.scan.maxDepth,
62
+ extraIgnore: config.scan.exclude,
63
+ maxFileSize: config.scan.maxFileSize,
64
+ });
65
+ const fileTreeString = fileTreeToString(fileTree);
66
+
67
+ return { fileTree, fileTreeString };
68
+ }
69
+
70
+ // ── Phase 2: Classify project ─────────────────────────────────────────
71
+
72
+ interface ClassifyResult {
73
+ classification: ProjectClassification;
74
+ projectName: string;
75
+ applicableAgents: AgentId[];
76
+ }
77
+
78
+ async function classifyProject(
79
+ projectRoot: string,
80
+ reportType: string,
81
+ progress: ProgressTracker
82
+ ): Promise<ClassifyResult> {
83
+ progress.setPhase("classifying");
84
+ await tick();
85
+
86
+ const classification = await detectProject(projectRoot);
87
+ const projectName = classification.framework
88
+ ? `${classification.framework} ${classification.projectType}`
89
+ : classification.language;
90
+
91
+ const agentIds = getAgentsForReportType(reportType);
92
+ const applicableAgents = agentIds.filter((id) => {
93
+ const def = getAgentById(id);
94
+ return def ? def.relevantFor(classification) : false;
95
+ });
96
+
97
+ return { classification, projectName, applicableAgents };
98
+ }
99
+
100
+ // ── Phase 3: Pre-read project files ───────────────────────────────────
101
+
102
+ async function preReadFiles(
103
+ projectRoot: string,
104
+ applicableAgents: AgentId[],
105
+ fileTree: FileTreeNode[],
106
+ fileTreeString: string,
107
+ classification: ProjectClassification,
108
+ config: OpenReportConfig,
109
+ progress: ProgressTracker
110
+ ): Promise<SharedContext> {
111
+ progress.setPhase("pre-reading");
112
+ await tick();
113
+
114
+ const allRelevantPaths = new Set<string>();
115
+ const agentFilePaths = new Map<string, string[]>();
116
+ for (const agentId of applicableAgents) {
117
+ const paths = getRelevantFilePaths(agentId, fileTree);
118
+ agentFilePaths.set(agentId, paths);
119
+ for (const p of paths) {
120
+ allRelevantPaths.add(p);
121
+ }
122
+ }
123
+
124
+ progress.setPhaseDetail(`${allRelevantPaths.size} unique files for ${applicableAgents.length} agents`);
125
+ const fileSizes = new Map<string, number>();
126
+ for (const node of flattenFileTree(fileTree)) {
127
+ if (node.size !== undefined) {
128
+ fileSizes.set(node.path, node.size);
129
+ }
130
+ }
131
+ const fileContents = await preReadAllFiles(projectRoot, [...allRelevantPaths], 30, 8000, fileSizes);
132
+ progress.setPhaseDetail(`${fileContents.size} files loaded, shared across ${applicableAgents.length} agents`);
133
+ await tick();
134
+
135
+ // Create tool instances once, shared across all agents
136
+ const baseTools = createBaseTools(projectRoot);
137
+ const extendedTools = createExtendedTools(projectRoot);
138
+
139
+ return {
140
+ classification,
141
+ fileTreeString,
142
+ fileTree,
143
+ fileContents,
144
+ contextInfo: buildContextInfo(classification, fileTreeString, [...allRelevantPaths]),
145
+ orchestratorAnalysis: "",
146
+ baseTools,
147
+ extendedTools,
148
+ agentFilePaths,
149
+ };
150
+ }
151
+
152
+ // ── Phase 4: Orchestrator global analysis ─────────────────────────────
153
+
154
+ async function runOrchestrator(
155
+ model: LanguageModelV1,
156
+ sharedContext: SharedContext,
157
+ config: OpenReportConfig,
158
+ progress: ProgressTracker,
159
+ signal?: AbortSignal
160
+ ): Promise<SharedContext> {
161
+ progress.setPhase("analyzing");
162
+ await tick();
163
+ progress.setPhaseDetail("Producing project overview...");
164
+
165
+ const orchestratorAnalysis = await runOrchestratorAgent(
166
+ model,
167
+ sharedContext,
168
+ config,
169
+ progress,
170
+ signal
171
+ );
172
+ progress.setPhaseDetail(`Done (${orchestratorAnalysis.length} chars)`);
173
+ await tick();
174
+
175
+ return { ...sharedContext, orchestratorAnalysis };
176
+ }
177
+
178
+ // ── Phase 5: Run agents ───────────────────────────────────────────────
179
+
180
+ async function executeAgents(
181
+ applicableAgents: AgentId[],
182
+ model: LanguageModelV1,
183
+ projectRoot: string,
184
+ sharedContext: SharedContext,
185
+ config: OpenReportConfig,
186
+ progress: ProgressTracker,
187
+ signal?: AbortSignal
188
+ ): Promise<SubReport[]> {
189
+ progress.setPhase("running-agents");
190
+ await tick();
191
+
192
+ // Register agents in progress tracker
193
+ for (const id of applicableAgents) {
194
+ const def = getAgentById(id);
195
+ if (def) {
196
+ progress.registerAgent(id, def.name);
197
+ }
198
+ }
199
+ await tick();
200
+
201
+ const useCli = isCliModel(model);
202
+ const maxConcurrency = config.agents.maxConcurrency;
203
+
204
+ // Choose the right runner function based on provider type
205
+ const runAgent = useCli ? runAgentWithCli : runAgentWithTools;
206
+
207
+ // Pool-based concurrency with indexed results for deterministic ordering
208
+ const results = new Map<string, SubReport>();
209
+ const executing = new Set<Promise<void>>();
210
+ for (const agentId of applicableAgents) {
211
+ if (signal?.aborted) break;
212
+
213
+ const p = runAgent(
214
+ agentId,
215
+ model,
216
+ projectRoot,
217
+ sharedContext,
218
+ config,
219
+ progress,
220
+ signal
221
+ ).then((result) => {
222
+ if (result) {
223
+ results.set(agentId, result);
224
+ }
225
+ }).catch((err) => {
226
+ debugLog("pipeline:agentPool", err);
227
+ }).finally(() => {
228
+ executing.delete(p);
229
+ });
230
+ executing.add(p);
231
+
232
+ if (executing.size >= maxConcurrency) {
233
+ await Promise.race(executing);
234
+ }
235
+ }
236
+ await Promise.all(executing);
237
+
238
+ // Return results in the original agent order
239
+ const subReports: SubReport[] = [];
240
+ for (const agentId of applicableAgents) {
241
+ const report = results.get(agentId);
242
+ if (report) {
243
+ subReports.push(report);
244
+ }
245
+ }
246
+ return subReports;
247
+ }
248
+
249
+ // ── Phase 5.5: Generate todo list from findings ───────────────────────
250
+
251
+ async function executeTodoAgent(
252
+ model: LanguageModelV1,
253
+ subReports: SubReport[],
254
+ config: OpenReportConfig,
255
+ progress: ProgressTracker,
256
+ signal?: AbortSignal
257
+ ): Promise<SubReport | null> {
258
+ const allFindings = subReports.flatMap((r) => r.findings);
259
+ if (allFindings.length === 0) {
260
+ debugLog("pipeline:todo", "No findings to generate todo list from, skipping");
261
+ return null;
262
+ }
263
+
264
+ progress.setPhase("generating-todo");
265
+ await tick();
266
+
267
+ const agentDef = getAgentById("todo-generator");
268
+ if (!agentDef) {
269
+ debugLog("pipeline:todo", "todo-generator agent not found in registry");
270
+ return null;
271
+ }
272
+
273
+ progress.registerAgent("todo-generator", agentDef.name);
274
+ progress.updateAgentStatus("todo-generator", "running", "Generating todo list...");
275
+
276
+ const findingsText = allFindings
277
+ .map(
278
+ (f, i) =>
279
+ `${i + 1}. [${f.severity.toUpperCase()}] (${f.category}) ${f.title}: ${f.description}${f.files?.length ? ` | Files: ${f.files.join(", ")}` : ""}${f.recommendation ? ` | Recommendation: ${f.recommendation}` : ""} | Effort: ${f.effort}`
280
+ )
281
+ .join("\n");
282
+
283
+ const prompt = `${agentDef.systemPrompt}
284
+
285
+ ## Findings from Analysis (${allFindings.length} total)
286
+
287
+ ${findingsText}
288
+
289
+ ## Instructions
290
+ Generate the prioritized todo list based on these findings. Wrap your entire response in <report> tags with valid JSON matching this structure:
291
+ {
292
+ "agentId": "todo-generator",
293
+ "title": "Action Plan & Todo List",
294
+ "summary": "<brief summary of the action plan>",
295
+ "sections": [
296
+ { "heading": "<section title>", "content": "<markdown content>" }
297
+ ],
298
+ "findings": []
299
+ }`;
300
+
301
+ try {
302
+ const result = await withRetry(async () => {
303
+ const response = await streamText({
304
+ model,
305
+ prompt,
306
+ maxSteps: 1,
307
+ maxTokens: config.agents.maxTokens,
308
+ temperature: config.agents.temperature,
309
+ abortSignal: signal,
310
+ });
311
+
312
+ const chunks: string[] = [];
313
+ for await (const chunk of response.textStream) {
314
+ chunks.push(chunk);
315
+ progress.addStreamText(chunk, "todo-generator");
316
+ }
317
+ return chunks.join("");
318
+ });
319
+
320
+ const report = extractSubReport("todo-generator", agentDef.name, result, 0);
321
+ progress.updateAgentStatus("todo-generator", "completed", "Done");
322
+ return report;
323
+ } catch (err) {
324
+ debugLog("pipeline:todo", err);
325
+ progress.updateAgentStatus("todo-generator", "failed", getErrorMessage(err));
326
+ return null;
327
+ }
328
+ }
329
+
330
+ // ── Phase 6: Generate executive summary ───────────────────────────────
331
+
332
+ async function generateExecutiveSummary(
333
+ model: LanguageModelV1,
334
+ subReports: SubReport[],
335
+ projectName: string,
336
+ classification: ProjectClassification,
337
+ progress: ProgressTracker,
338
+ signal?: AbortSignal
339
+ ): Promise<string> {
340
+ await tick();
341
+ progress.setPhase("combining");
342
+
343
+ if (subReports.length === 0) {
344
+ return "Report generation completed.";
345
+ }
346
+
347
+ const allFindings = subReports.flatMap((r) => r.findings);
348
+ const findingSummary = countFindings(allFindings);
349
+ progress.setPhaseDetail(`${subReports.length} sub-reports, ${allFindings.length} findings`);
350
+
351
+ const summaryPrompt = `You are a senior technical lead writing an executive summary for a code analysis report.
352
+
353
+ ## Project: ${projectName}
354
+ ## Classification
355
+ ${JSON.stringify(classification, null, 2)}
356
+
357
+ ## Sub-Report Summaries
358
+ ${subReports.map((r) => `### ${r.title}\n${r.summary}\n\nKey sections: ${r.sections.map((s) => s.heading).join(", ")}`).join("\n\n")}
359
+
360
+ ## All Findings (${allFindings.length} total)
361
+ ${allFindings.map((f) => `- [${f.severity.toUpperCase()}] ${f.title}: ${f.description} (effort: ${f.effort})`).join("\n")}
362
+
363
+ ## Finding Breakdown
364
+ - Critical: ${findingSummary.critical}
365
+ - Warning: ${findingSummary.warning}
366
+ - Info: ${findingSummary.info}
367
+ - Suggestion: ${findingSummary.suggestion}
368
+
369
+ Write a comprehensive executive summary in markdown with these sections:
370
+ 1. **Project Health Overview** - Overall assessment, grade justification
371
+ 2. **Key Strengths** - What the project does well
372
+ 3. **Critical Issues** - Top priority items requiring immediate attention
373
+ 4. **Cross-Cutting Concerns** - Patterns/issues found by multiple analysis angles
374
+ 5. **Top Recommendations** - Prioritized action items by impact/effort ratio
375
+
376
+ Be specific, reference actual findings, and provide actionable insights. Write 4-6 paragraphs.`;
377
+
378
+ try {
379
+ const result = await streamText({
380
+ model,
381
+ prompt: summaryPrompt,
382
+ maxSteps: 1,
383
+ maxTokens: 2000,
384
+ temperature: 0.3,
385
+ abortSignal: signal,
386
+ });
387
+
388
+ const chunks: string[] = [];
389
+ for await (const chunk of result.textStream) {
390
+ chunks.push(chunk);
391
+ }
392
+ return chunks.join("");
393
+ } catch (e) {
394
+ debugLog("pipeline:executiveSummary", e);
395
+ return "Executive summary generation failed. Please review individual sections below.";
396
+ }
397
+ }
398
+
399
+ // ── Phase 7: Combine and save ─────────────────────────────────────────
400
+
401
+ function combineAndFinalize(
402
+ projectName: string,
403
+ projectRoot: string,
404
+ reportType: string,
405
+ modelId: string,
406
+ executiveSummary: string,
407
+ subReports: SubReport[],
408
+ progress: ProgressTracker
409
+ ): FullReport {
410
+ progress.setPhase("saving");
411
+
412
+ const agentStatuses = progress.getProgress().agents;
413
+ const totalTokens = progress.getProgress().totalTokens;
414
+
415
+ const fullReport = combineReport({
416
+ projectName,
417
+ projectPath: projectRoot,
418
+ reportType,
419
+ model: modelId,
420
+ executiveSummary,
421
+ subReports,
422
+ agentStatuses,
423
+ startedAt: progress.getProgress().startedAt,
424
+ totalTokens,
425
+ });
426
+
427
+ progress.setPhase("done");
428
+ progress.emit("complete", progress.getProgress());
429
+
430
+ return fullReport;
431
+ }
432
+
433
+ // ── Main pipeline ───────────────────────────────────────────────────────
434
+
435
+ export async function runPipeline(
436
+ options: RunPipelineOptions
437
+ ): Promise<FullReport> {
438
+ const { projectRoot, reportType, model, modelId, config, progress, signal, generateTodoList } =
439
+ options;
440
+
441
+ const PIPELINE_TIMEOUT_MS = 10 * 60 * 1000;
442
+ const timeoutController = new AbortController();
443
+ const timeoutId = setTimeout(() => timeoutController.abort(), PIPELINE_TIMEOUT_MS);
444
+
445
+ const combinedSignal = signal
446
+ ? AbortSignal.any([signal, timeoutController.signal])
447
+ : timeoutController.signal;
448
+
449
+ try {
450
+ // Phase 1 & 2: Scan and classify project in parallel
451
+ const [scanResult, classifyResult] = await Promise.all([
452
+ scanProject(projectRoot, config, progress),
453
+ classifyProject(projectRoot, reportType, progress),
454
+ ]);
455
+ const { fileTree, fileTreeString } = scanResult;
456
+ const { classification, projectName, applicableAgents } = classifyResult;
457
+
458
+ // Phase 3: Pre-read project files
459
+ const sharedContext = await preReadFiles(
460
+ projectRoot,
461
+ applicableAgents,
462
+ fileTree,
463
+ fileTreeString,
464
+ classification,
465
+ config,
466
+ progress
467
+ );
468
+
469
+ // Phase 4: Orchestrator global analysis
470
+ const fullContext = await runOrchestrator(model, sharedContext, config, progress, combinedSignal);
471
+
472
+ // Phase 5: Run agents
473
+ const subReports = await executeAgents(
474
+ applicableAgents,
475
+ model,
476
+ projectRoot,
477
+ fullContext,
478
+ config,
479
+ progress,
480
+ combinedSignal
481
+ );
482
+
483
+ if (subReports.length === 0 && applicableAgents.length > 0) {
484
+ throw new PipelineError(
485
+ `All ${applicableAgents.length} agents failed to produce results. Check API keys and provider configuration.`
486
+ );
487
+ }
488
+
489
+ // Phase 5.5: Generate todo list (if enabled)
490
+ if (generateTodoList) {
491
+ const todoReport = await executeTodoAgent(
492
+ model,
493
+ subReports,
494
+ config,
495
+ progress,
496
+ combinedSignal
497
+ );
498
+ if (todoReport) {
499
+ subReports.push(todoReport);
500
+ }
501
+ }
502
+
503
+ // Phase 6: Generate executive summary
504
+ const executiveSummary = await generateExecutiveSummary(
505
+ model,
506
+ subReports,
507
+ projectName,
508
+ classification,
509
+ progress,
510
+ combinedSignal
511
+ );
512
+
513
+ // Phase 7: Combine and finalize
514
+ return combineAndFinalize(
515
+ projectName,
516
+ projectRoot,
517
+ reportType,
518
+ modelId,
519
+ executiveSummary,
520
+ subReports,
521
+ progress
522
+ );
523
+ } finally {
524
+ clearTimeout(timeoutId);
525
+ }
526
+ }