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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/openreport.ts +6 -0
- package/package.json +61 -0
- package/src/agents/api-documentation.ts +66 -0
- package/src/agents/architecture-analyst.ts +46 -0
- package/src/agents/code-quality-reviewer.ts +59 -0
- package/src/agents/dependency-analyzer.ts +51 -0
- package/src/agents/onboarding-guide.ts +59 -0
- package/src/agents/orchestrator.ts +41 -0
- package/src/agents/performance-analyzer.ts +57 -0
- package/src/agents/registry.ts +50 -0
- package/src/agents/security-auditor.ts +61 -0
- package/src/agents/test-coverage-analyst.ts +58 -0
- package/src/agents/todo-generator.ts +50 -0
- package/src/app/App.tsx +151 -0
- package/src/app/theme.ts +54 -0
- package/src/cli.ts +145 -0
- package/src/commands/init.ts +81 -0
- package/src/commands/interactive.tsx +29 -0
- package/src/commands/list.ts +53 -0
- package/src/commands/run.ts +168 -0
- package/src/commands/view.tsx +52 -0
- package/src/components/generation/AgentStatusItem.tsx +125 -0
- package/src/components/generation/AgentStatusList.tsx +70 -0
- package/src/components/generation/ProgressSummary.tsx +107 -0
- package/src/components/generation/StreamingOutput.tsx +154 -0
- package/src/components/layout/Container.tsx +24 -0
- package/src/components/layout/Footer.tsx +52 -0
- package/src/components/layout/Header.tsx +50 -0
- package/src/components/report/MarkdownRenderer.tsx +50 -0
- package/src/components/report/ReportCard.tsx +31 -0
- package/src/components/report/ScrollableView.tsx +164 -0
- package/src/config/cli-detection.ts +130 -0
- package/src/config/cli-model.ts +397 -0
- package/src/config/cli-prompt-formatter.ts +129 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +168 -0
- package/src/config/ollama.ts +48 -0
- package/src/config/providers.ts +199 -0
- package/src/config/resolve-provider.ts +62 -0
- package/src/config/saver.ts +50 -0
- package/src/config/schema.ts +51 -0
- package/src/errors.ts +34 -0
- package/src/hooks/useReportGeneration.ts +199 -0
- package/src/hooks/useTerminalSize.ts +35 -0
- package/src/ingestion/context-selector.ts +247 -0
- package/src/ingestion/file-tree.ts +227 -0
- package/src/ingestion/token-budget.ts +52 -0
- package/src/pipeline/agent-runner.ts +360 -0
- package/src/pipeline/combiner.ts +199 -0
- package/src/pipeline/context.ts +108 -0
- package/src/pipeline/extraction.ts +153 -0
- package/src/pipeline/progress.ts +192 -0
- package/src/pipeline/runner.ts +526 -0
- package/src/report/html-renderer.ts +294 -0
- package/src/report/html-script.ts +123 -0
- package/src/report/html-styles.ts +1127 -0
- package/src/report/md-to-html.ts +153 -0
- package/src/report/open-browser.ts +22 -0
- package/src/schemas/findings.ts +48 -0
- package/src/schemas/report.ts +64 -0
- package/src/screens/ConfigScreen.tsx +271 -0
- package/src/screens/GenerationScreen.tsx +278 -0
- package/src/screens/HistoryScreen.tsx +108 -0
- package/src/screens/HomeScreen.tsx +143 -0
- package/src/screens/ViewerScreen.tsx +82 -0
- package/src/storage/metadata.ts +69 -0
- package/src/storage/report-store.ts +128 -0
- package/src/tools/get-file-tree.ts +157 -0
- package/src/tools/get-git-info.ts +123 -0
- package/src/tools/glob.ts +48 -0
- package/src/tools/grep.ts +149 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/list-directory.ts +57 -0
- package/src/tools/read-file.ts +52 -0
- package/src/tools/read-package-json.ts +48 -0
- package/src/tools/run-command.ts +154 -0
- package/src/tools/shared-ignore.ts +58 -0
- package/src/types/index.ts +127 -0
- package/src/types/marked-terminal.d.ts +17 -0
- package/src/utils/debug.ts +25 -0
- package/src/utils/file-utils.ts +77 -0
- package/src/utils/format.ts +56 -0
- package/src/utils/grade-colors.ts +43 -0
- package/src/utils/project-detector.ts +296 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { spawn as spawnChild, execFile } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import type {
|
|
5
|
+
LanguageModelV1,
|
|
6
|
+
LanguageModelV1CallOptions,
|
|
7
|
+
LanguageModelV1StreamPart,
|
|
8
|
+
} from "ai";
|
|
9
|
+
import { estimateTokens } from "../ingestion/token-budget.js";
|
|
10
|
+
import { debugLog } from "../utils/debug.js";
|
|
11
|
+
import {
|
|
12
|
+
CLI_TOOLS,
|
|
13
|
+
getCliCommand,
|
|
14
|
+
validateCliCommand,
|
|
15
|
+
type CliToolDef,
|
|
16
|
+
} from "./cli-detection.js";
|
|
17
|
+
import {
|
|
18
|
+
buildFullPrompt,
|
|
19
|
+
processCliOutput,
|
|
20
|
+
parseToolCalls,
|
|
21
|
+
} from "./cli-prompt-formatter.js";
|
|
22
|
+
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
|
|
25
|
+
async function resolveWindowsCommand(command: string): Promise<{ cmd: string; args: string[]; needsShell: boolean }> {
|
|
26
|
+
if (os.platform() !== "win32") {
|
|
27
|
+
return { cmd: command, args: [], needsShell: false };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const { stdout } = await execFileAsync("where.exe", [command], { encoding: "utf-8", timeout: 3000 });
|
|
31
|
+
const fullPath = stdout.trim().split("\n")[0].trim();
|
|
32
|
+
if (fullPath.endsWith(".cmd") || fullPath.endsWith(".bat")) {
|
|
33
|
+
return { cmd: process.env.ComSpec || "cmd.exe", args: ["/c", fullPath], needsShell: false };
|
|
34
|
+
}
|
|
35
|
+
return { cmd: fullPath, args: [], needsShell: false };
|
|
36
|
+
} catch {
|
|
37
|
+
return { cmd: command, args: [], needsShell: true };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── CLI model marker for provider detection ─────────────────────────────
|
|
42
|
+
|
|
43
|
+
const cliModels = new WeakSet<LanguageModelV1>();
|
|
44
|
+
|
|
45
|
+
/** Check whether a LanguageModelV1 was created by `createCliModel`. */
|
|
46
|
+
export function isCliModel(model: LanguageModelV1): boolean {
|
|
47
|
+
return cliModels.has(model);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Async CLI execution (non-blocking) ──────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export async function runCliAsync(
|
|
53
|
+
tool: CliToolDef,
|
|
54
|
+
fullPrompt: string,
|
|
55
|
+
model: string | null,
|
|
56
|
+
signal?: AbortSignal
|
|
57
|
+
): Promise<string> {
|
|
58
|
+
const command = getCliCommand(tool.id);
|
|
59
|
+
validateCliCommand(command);
|
|
60
|
+
const args = tool.buildArgs(model);
|
|
61
|
+
|
|
62
|
+
const resolved = await resolveWindowsCommand(command);
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const proc = spawnChild(resolved.cmd, [...resolved.args, ...args], {
|
|
66
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
67
|
+
...(resolved.needsShell ? { shell: true } : {}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Propagate abort signal to kill child process
|
|
71
|
+
const onAbort = () => { proc.kill(); };
|
|
72
|
+
if (signal) {
|
|
73
|
+
if (signal.aborted) {
|
|
74
|
+
proc.kill();
|
|
75
|
+
reject(new Error("Aborted"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
proc.stdin.write(fullPrompt);
|
|
82
|
+
proc.stdin.end();
|
|
83
|
+
|
|
84
|
+
const stdoutChunks: string[] = [];
|
|
85
|
+
const stderrChunks: string[] = [];
|
|
86
|
+
|
|
87
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
88
|
+
stdoutChunks.push(chunk.toString("utf-8"));
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
92
|
+
stderrChunks.push(chunk.toString("utf-8"));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
proc.on("close", (code) => {
|
|
96
|
+
signal?.removeEventListener("abort", onAbort);
|
|
97
|
+
const stderr = stderrChunks.join("");
|
|
98
|
+
if (code !== 0 && stderr) {
|
|
99
|
+
reject(new Error(`CLI stderr: ${stderr.slice(0, 500)}`));
|
|
100
|
+
} else {
|
|
101
|
+
resolve(stdoutChunks.join("").trim());
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
proc.on("error", (err) => {
|
|
106
|
+
signal?.removeEventListener("abort", onAbort);
|
|
107
|
+
reject(new Error(`CLI error: ${err.message}`));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Stream event parsing helpers ─────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
interface ParsedStreamEvent {
|
|
115
|
+
type: "text" | "usage" | "unknown";
|
|
116
|
+
text?: string;
|
|
117
|
+
usage?: { input_tokens?: number; output_tokens?: number };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseStreamEvent(line: string): ParsedStreamEvent | null {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
if (!trimmed) return null;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const event = JSON.parse(trimmed);
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
event.type === "stream_event" &&
|
|
129
|
+
event.event?.delta?.type === "text_delta" &&
|
|
130
|
+
event.event.delta.text
|
|
131
|
+
) {
|
|
132
|
+
return { type: "text", text: event.event.delta.text };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (event.usage) {
|
|
136
|
+
return { type: "usage", usage: event.usage };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { type: "unknown" };
|
|
140
|
+
} catch (e) {
|
|
141
|
+
debugLog("cli:streamParseChunk", e);
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function processRemainingBuffer(
|
|
147
|
+
buffer: string,
|
|
148
|
+
textChunks: string[],
|
|
149
|
+
controller: ReadableStreamDefaultController<LanguageModelV1StreamPart>,
|
|
150
|
+
usage: { inputTokens: number; outputTokens: number },
|
|
151
|
+
): void {
|
|
152
|
+
if (!buffer.trim()) return;
|
|
153
|
+
|
|
154
|
+
const parsed = parseStreamEvent(buffer);
|
|
155
|
+
if (!parsed) return;
|
|
156
|
+
|
|
157
|
+
if (parsed.type === "text" && parsed.text) {
|
|
158
|
+
textChunks.push(parsed.text);
|
|
159
|
+
controller.enqueue({ type: "text-delta", textDelta: parsed.text });
|
|
160
|
+
}
|
|
161
|
+
if (parsed.usage) {
|
|
162
|
+
if (parsed.usage.input_tokens) usage.inputTokens = parsed.usage.input_tokens;
|
|
163
|
+
if (parsed.usage.output_tokens) usage.outputTokens = parsed.usage.output_tokens;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function buildStreamFinishResult(
|
|
168
|
+
textChunks: string[],
|
|
169
|
+
usage: { inputTokens: number; outputTokens: number },
|
|
170
|
+
fullPrompt: string,
|
|
171
|
+
): { finishParts: LanguageModelV1StreamPart[] } {
|
|
172
|
+
const totalText = textChunks.join("");
|
|
173
|
+
const toolCalls = parseToolCalls(totalText);
|
|
174
|
+
const parts: LanguageModelV1StreamPart[] = [];
|
|
175
|
+
|
|
176
|
+
if (toolCalls) {
|
|
177
|
+
for (const tc of toolCalls) {
|
|
178
|
+
parts.push({
|
|
179
|
+
type: "tool-call",
|
|
180
|
+
toolCallType: "function",
|
|
181
|
+
toolCallId: tc.toolCallId,
|
|
182
|
+
toolName: tc.toolName,
|
|
183
|
+
args: tc.args,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
parts.push({
|
|
189
|
+
type: "finish",
|
|
190
|
+
finishReason: toolCalls ? "tool-calls" : "stop",
|
|
191
|
+
usage: {
|
|
192
|
+
promptTokens: usage.inputTokens || estimateTokens(fullPrompt),
|
|
193
|
+
completionTokens: usage.outputTokens || estimateTokens(totalText),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return { finishParts: parts };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Streaming execution for Claude Code ─────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async function streamClaude(
|
|
203
|
+
tool: CliToolDef,
|
|
204
|
+
fullPrompt: string,
|
|
205
|
+
model: string | null,
|
|
206
|
+
signal?: AbortSignal
|
|
207
|
+
): Promise<ReadableStream<LanguageModelV1StreamPart>> {
|
|
208
|
+
const command = getCliCommand(tool.id);
|
|
209
|
+
validateCliCommand(command);
|
|
210
|
+
const args = tool.buildStreamArgs(model);
|
|
211
|
+
|
|
212
|
+
const resolved = await resolveWindowsCommand(command);
|
|
213
|
+
|
|
214
|
+
return new ReadableStream<LanguageModelV1StreamPart>({
|
|
215
|
+
start(controller) {
|
|
216
|
+
const proc = spawnChild(resolved.cmd, [...resolved.args, ...args], {
|
|
217
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
218
|
+
...(resolved.needsShell ? { shell: true } : {}),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const onAbort = () => { proc.kill(); };
|
|
222
|
+
if (signal) {
|
|
223
|
+
if (signal.aborted) {
|
|
224
|
+
proc.kill();
|
|
225
|
+
controller.close();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
proc.stdin.write(fullPrompt);
|
|
232
|
+
proc.stdin.end();
|
|
233
|
+
|
|
234
|
+
let buffer = "";
|
|
235
|
+
const textChunks: string[] = [];
|
|
236
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
237
|
+
|
|
238
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
239
|
+
buffer += chunk.toString("utf-8");
|
|
240
|
+
const lines = buffer.split("\n");
|
|
241
|
+
buffer = lines.pop() || "";
|
|
242
|
+
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
const parsed = parseStreamEvent(line);
|
|
245
|
+
if (!parsed) continue;
|
|
246
|
+
|
|
247
|
+
if (parsed.type === "text" && parsed.text) {
|
|
248
|
+
textChunks.push(parsed.text);
|
|
249
|
+
controller.enqueue({ type: "text-delta", textDelta: parsed.text });
|
|
250
|
+
}
|
|
251
|
+
if (parsed.usage) {
|
|
252
|
+
if (parsed.usage.input_tokens) usage.inputTokens = parsed.usage.input_tokens;
|
|
253
|
+
if (parsed.usage.output_tokens) usage.outputTokens = parsed.usage.output_tokens;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
let stderrOutput = "";
|
|
259
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
260
|
+
stderrOutput += chunk.toString("utf-8");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
proc.on("close", (code) => {
|
|
264
|
+
if (code !== 0 && stderrOutput) {
|
|
265
|
+
debugLog("cli:stderr", stderrOutput.slice(0, 500));
|
|
266
|
+
}
|
|
267
|
+
signal?.removeEventListener("abort", onAbort);
|
|
268
|
+
|
|
269
|
+
processRemainingBuffer(buffer, textChunks, controller, usage);
|
|
270
|
+
|
|
271
|
+
const { finishParts } = buildStreamFinishResult(textChunks, usage, fullPrompt);
|
|
272
|
+
for (const part of finishParts) {
|
|
273
|
+
controller.enqueue(part);
|
|
274
|
+
}
|
|
275
|
+
controller.close();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
proc.on("error", (err) => {
|
|
279
|
+
signal?.removeEventListener("abort", onAbort);
|
|
280
|
+
controller.error(new Error(`CLI spawn error: ${err.message}`));
|
|
281
|
+
});
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── LanguageModelV1 implementation ──────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
export function createCliModel(
|
|
289
|
+
toolId: string,
|
|
290
|
+
modelOverride?: string
|
|
291
|
+
): LanguageModelV1 {
|
|
292
|
+
const tool = CLI_TOOLS.find((t) => t.id === toolId);
|
|
293
|
+
if (!tool) throw new Error(`Unknown CLI tool: ${toolId}`);
|
|
294
|
+
|
|
295
|
+
const modelId = modelOverride || tool.defaultModel;
|
|
296
|
+
|
|
297
|
+
const model = {
|
|
298
|
+
specificationVersion: "v1" as const,
|
|
299
|
+
provider: tool.id,
|
|
300
|
+
modelId,
|
|
301
|
+
defaultObjectGenerationMode: "json" as const,
|
|
302
|
+
|
|
303
|
+
async doGenerate(options: LanguageModelV1CallOptions) {
|
|
304
|
+
const fullPrompt = buildFullPrompt(options);
|
|
305
|
+
|
|
306
|
+
// Use async spawn to avoid blocking the event loop
|
|
307
|
+
const output = await runCliAsync(tool, fullPrompt, modelOverride || null, options.abortSignal);
|
|
308
|
+
|
|
309
|
+
const { cleanText, toolCalls } = processCliOutput(output);
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
text: cleanText || undefined,
|
|
313
|
+
toolCalls,
|
|
314
|
+
finishReason: toolCalls
|
|
315
|
+
? ("tool-calls" as const)
|
|
316
|
+
: ("stop" as const),
|
|
317
|
+
usage: {
|
|
318
|
+
promptTokens: estimateTokens(fullPrompt),
|
|
319
|
+
completionTokens: estimateTokens(output),
|
|
320
|
+
},
|
|
321
|
+
rawCall: {
|
|
322
|
+
rawPrompt: fullPrompt,
|
|
323
|
+
rawSettings: { toolId, modelId },
|
|
324
|
+
},
|
|
325
|
+
rawResponse: { headers: {} },
|
|
326
|
+
warnings: [],
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
async doStream(options: LanguageModelV1CallOptions) {
|
|
331
|
+
const fullPrompt = buildFullPrompt(options);
|
|
332
|
+
|
|
333
|
+
// Use native streaming for Claude Code
|
|
334
|
+
if (tool.supportsStreaming && tool.id === "claude-code") {
|
|
335
|
+
const stream = await streamClaude(tool, fullPrompt, modelOverride || null, options.abortSignal);
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
stream,
|
|
339
|
+
rawCall: {
|
|
340
|
+
rawPrompt: fullPrompt,
|
|
341
|
+
rawSettings: { toolId, modelId },
|
|
342
|
+
},
|
|
343
|
+
rawResponse: { headers: {} },
|
|
344
|
+
warnings: [],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Fallback: async execution then emit as single chunk
|
|
349
|
+
const output = await runCliAsync(tool, fullPrompt, modelOverride || null, options.abortSignal);
|
|
350
|
+
|
|
351
|
+
const { cleanText, toolCalls } = processCliOutput(output);
|
|
352
|
+
|
|
353
|
+
const stream = new ReadableStream<LanguageModelV1StreamPart>({
|
|
354
|
+
start(controller) {
|
|
355
|
+
if (cleanText) {
|
|
356
|
+
controller.enqueue({ type: "text-delta", textDelta: cleanText });
|
|
357
|
+
}
|
|
358
|
+
if (toolCalls) {
|
|
359
|
+
for (const tc of toolCalls) {
|
|
360
|
+
controller.enqueue({
|
|
361
|
+
type: "tool-call",
|
|
362
|
+
toolCallType: "function",
|
|
363
|
+
toolCallId: tc.toolCallId,
|
|
364
|
+
toolName: tc.toolName,
|
|
365
|
+
args: tc.args,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
controller.enqueue({
|
|
370
|
+
type: "finish",
|
|
371
|
+
finishReason: toolCalls ? "tool-calls" : "stop",
|
|
372
|
+
usage: {
|
|
373
|
+
promptTokens: estimateTokens(fullPrompt),
|
|
374
|
+
completionTokens: estimateTokens(output),
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
controller.close();
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
stream,
|
|
383
|
+
rawCall: {
|
|
384
|
+
rawPrompt: fullPrompt,
|
|
385
|
+
rawSettings: { toolId, modelId },
|
|
386
|
+
},
|
|
387
|
+
rawResponse: { headers: {} },
|
|
388
|
+
warnings: [],
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Mark as CLI model for provider detection (see isCliModel)
|
|
394
|
+
cliModels.add(model);
|
|
395
|
+
|
|
396
|
+
return model;
|
|
397
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { LanguageModelV1CallOptions } from "ai";
|
|
2
|
+
import { debugLog } from "../utils/debug.js";
|
|
3
|
+
|
|
4
|
+
// ── Prompt formatting ───────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function formatUserPrompt(
|
|
7
|
+
prompt: LanguageModelV1CallOptions["prompt"]
|
|
8
|
+
): string {
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
|
|
11
|
+
for (const msg of prompt) {
|
|
12
|
+
switch (msg.role) {
|
|
13
|
+
case "system":
|
|
14
|
+
parts.push(`<system_instructions>\n${msg.content}\n</system_instructions>`);
|
|
15
|
+
break;
|
|
16
|
+
case "user":
|
|
17
|
+
for (const part of msg.content) {
|
|
18
|
+
if (part.type === "text") {
|
|
19
|
+
parts.push(part.text);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
break;
|
|
23
|
+
case "assistant":
|
|
24
|
+
for (const part of msg.content) {
|
|
25
|
+
if (part.type === "text") {
|
|
26
|
+
parts.push(`[Assistant response]\n${part.text}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case "tool":
|
|
31
|
+
for (const part of msg.content) {
|
|
32
|
+
parts.push(
|
|
33
|
+
`[Tool result for ${part.toolCallId}]\n${JSON.stringify(part.result)}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return parts.join("\n\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatToolsForPrompt(
|
|
44
|
+
mode: LanguageModelV1CallOptions["mode"]
|
|
45
|
+
): string {
|
|
46
|
+
if (mode.type !== "regular" || !mode.tools || mode.tools.length === 0) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let text = "\n\n<available_tools>\n";
|
|
51
|
+
text +=
|
|
52
|
+
"When you need to use a tool, respond ONLY with a JSON block like:\n";
|
|
53
|
+
text +=
|
|
54
|
+
'```json\n{"tool_calls": [{"id": "call_1", "name": "toolName", "arguments": {...}}]}\n```\n\n';
|
|
55
|
+
|
|
56
|
+
for (const tool of mode.tools) {
|
|
57
|
+
if (tool.type === "function") {
|
|
58
|
+
text += `### ${tool.name}\n${tool.description || ""}\nParameters: ${JSON.stringify(tool.parameters)}\n\n`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
text += "</available_tools>\n";
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Tool call parsing ────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export type ParsedToolCall = {
|
|
69
|
+
toolCallType: "function";
|
|
70
|
+
toolCallId: string;
|
|
71
|
+
toolName: string;
|
|
72
|
+
args: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function parseToolCalls(text: string): ParsedToolCall[] | undefined {
|
|
76
|
+
const jsonMatch = text.match(
|
|
77
|
+
/```json\s*\n?\s*(\{[\s\S]*?"tool_calls"[\s\S]*?\})\s*\n?\s*```/
|
|
78
|
+
);
|
|
79
|
+
if (!jsonMatch) return undefined;
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
83
|
+
if (!Array.isArray(parsed.tool_calls)) return undefined;
|
|
84
|
+
|
|
85
|
+
return parsed.tool_calls.map(
|
|
86
|
+
(tc: { id?: string; name: string; arguments: unknown }, i: number) => ({
|
|
87
|
+
toolCallType: "function" as const,
|
|
88
|
+
toolCallId: tc.id || `call_${i}`,
|
|
89
|
+
toolName: tc.name,
|
|
90
|
+
args: JSON.stringify(tc.arguments || {}),
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
} catch (e) {
|
|
94
|
+
debugLog("cli:parseToolCalls", e);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Shared helpers for doGenerate / doStream ─────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the full prompt from SDK call options.
|
|
103
|
+
* Shared between doGenerate and doStream to avoid duplication.
|
|
104
|
+
*/
|
|
105
|
+
export function buildFullPrompt(options: LanguageModelV1CallOptions): string {
|
|
106
|
+
const toolsText = formatToolsForPrompt(options.mode);
|
|
107
|
+
return formatUserPrompt(options.prompt) + toolsText;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Process raw CLI output: parse tool calls and strip the tool_calls JSON block.
|
|
112
|
+
* Shared between doGenerate and doStream (fallback path).
|
|
113
|
+
*/
|
|
114
|
+
export function processCliOutput(output: string): {
|
|
115
|
+
cleanText: string;
|
|
116
|
+
toolCalls: ParsedToolCall[] | undefined;
|
|
117
|
+
} {
|
|
118
|
+
const toolCalls = parseToolCalls(output);
|
|
119
|
+
const cleanText = toolCalls
|
|
120
|
+
? output
|
|
121
|
+
.replace(
|
|
122
|
+
/```json\s*\n?\s*\{[\s\S]*?"tool_calls"[\s\S]*?\}\s*\n?\s*```/,
|
|
123
|
+
""
|
|
124
|
+
)
|
|
125
|
+
.trim()
|
|
126
|
+
: output;
|
|
127
|
+
|
|
128
|
+
return { cleanText, toolCalls };
|
|
129
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { OpenReportConfig } from "./schema.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_CONFIG: OpenReportConfig = {
|
|
4
|
+
defaultProvider: "claude-code",
|
|
5
|
+
defaultModel: "sonnet",
|
|
6
|
+
providers: {},
|
|
7
|
+
output: {
|
|
8
|
+
directory: ".openreport/reports",
|
|
9
|
+
format: "markdown",
|
|
10
|
+
includeMetadata: true,
|
|
11
|
+
},
|
|
12
|
+
agents: {
|
|
13
|
+
maxConcurrency: 3,
|
|
14
|
+
maxStepsOverride: {},
|
|
15
|
+
temperature: 0.3,
|
|
16
|
+
maxTokens: 8192,
|
|
17
|
+
},
|
|
18
|
+
scan: {
|
|
19
|
+
exclude: [],
|
|
20
|
+
maxFileSize: 50_000,
|
|
21
|
+
maxDepth: 10,
|
|
22
|
+
},
|
|
23
|
+
ui: {
|
|
24
|
+
theme: "auto",
|
|
25
|
+
showTokenCount: true,
|
|
26
|
+
streamOutput: true,
|
|
27
|
+
},
|
|
28
|
+
features: {
|
|
29
|
+
todoList: false,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const REPORT_TYPES = {
|
|
34
|
+
"full-audit": {
|
|
35
|
+
id: "full-audit",
|
|
36
|
+
name: "Full Project Audit",
|
|
37
|
+
description: "Comprehensive analysis covering all aspects of the project",
|
|
38
|
+
agents: [
|
|
39
|
+
"architecture-analyst",
|
|
40
|
+
"security-auditor",
|
|
41
|
+
"code-quality-reviewer",
|
|
42
|
+
"dependency-analyzer",
|
|
43
|
+
"performance-analyzer",
|
|
44
|
+
"test-coverage-analyst",
|
|
45
|
+
] as const,
|
|
46
|
+
},
|
|
47
|
+
"quick-health": {
|
|
48
|
+
id: "quick-health",
|
|
49
|
+
name: "Quick Health Check",
|
|
50
|
+
description: "Fast overview of code quality and dependencies",
|
|
51
|
+
agents: ["code-quality-reviewer", "dependency-analyzer"] as const,
|
|
52
|
+
},
|
|
53
|
+
"security-review": {
|
|
54
|
+
id: "security-review",
|
|
55
|
+
name: "Security Review",
|
|
56
|
+
description: "Focused security audit of the codebase",
|
|
57
|
+
agents: ["security-auditor", "dependency-analyzer"] as const,
|
|
58
|
+
},
|
|
59
|
+
architecture: {
|
|
60
|
+
id: "architecture",
|
|
61
|
+
name: "Architecture Documentation",
|
|
62
|
+
description: "Architecture analysis with diagrams and patterns",
|
|
63
|
+
agents: ["architecture-analyst"] as const,
|
|
64
|
+
},
|
|
65
|
+
dependencies: {
|
|
66
|
+
id: "dependencies",
|
|
67
|
+
name: "Dependency Report",
|
|
68
|
+
description: "Dependency health, vulnerabilities, and licenses",
|
|
69
|
+
agents: ["dependency-analyzer"] as const,
|
|
70
|
+
},
|
|
71
|
+
onboarding: {
|
|
72
|
+
id: "onboarding",
|
|
73
|
+
name: "Onboarding Guide",
|
|
74
|
+
description: "New developer onboarding guide for the project",
|
|
75
|
+
agents: ["onboarding-guide", "architecture-analyst"] as const,
|
|
76
|
+
},
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
export type ReportTypeId = keyof typeof REPORT_TYPES;
|