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,168 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { OpenReportConfigSchema, type OpenReportConfig } from "./schema.js";
|
|
4
|
+
import { DEFAULT_CONFIG } from "./defaults.js";
|
|
5
|
+
import { ConfigError } from "../errors.js";
|
|
6
|
+
import { debugLog } from "../utils/debug.js";
|
|
7
|
+
|
|
8
|
+
async function readJsonFile(filePath: string): Promise<Record<string, unknown> | null> {
|
|
9
|
+
try {
|
|
10
|
+
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
11
|
+
return JSON.parse(content);
|
|
12
|
+
} catch (e) {
|
|
13
|
+
debugLog("config:readJsonFile", e);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getGlobalConfigPath(): string {
|
|
19
|
+
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
20
|
+
return path.join(home, ".openreport", "config.json");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getProjectConfigPath(projectRoot: string): string {
|
|
24
|
+
return path.join(projectRoot, ".openreport", "config.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ENV_KEY_MAP: [string, string][] = [
|
|
28
|
+
["ANTHROPIC_API_KEY", "anthropic"],
|
|
29
|
+
["OPENAI_API_KEY", "openai"],
|
|
30
|
+
["GOOGLE_GENERATIVE_AI_API_KEY", "google"],
|
|
31
|
+
["MISTRAL_API_KEY", "mistral"],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function envToConfig(): Partial<OpenReportConfig> {
|
|
35
|
+
const config: Record<string, unknown> = {};
|
|
36
|
+
|
|
37
|
+
if (process.env.OPENREPORT_PROVIDER) {
|
|
38
|
+
config.defaultProvider = process.env.OPENREPORT_PROVIDER;
|
|
39
|
+
}
|
|
40
|
+
if (process.env.OPENREPORT_MODEL) {
|
|
41
|
+
config.defaultModel = process.env.OPENREPORT_MODEL;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const [envKey, providerName] of ENV_KEY_MAP) {
|
|
45
|
+
const apiKey = process.env[envKey];
|
|
46
|
+
if (apiKey) {
|
|
47
|
+
config.providers = {
|
|
48
|
+
...((config.providers as Record<string, unknown>) || {}),
|
|
49
|
+
[providerName]: { apiKey },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return config as Partial<OpenReportConfig>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LoadConfigOptions {
|
|
58
|
+
projectRoot?: string;
|
|
59
|
+
cliFlags?: Partial<OpenReportConfig>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function stripSensitiveFields(config: Record<string, unknown>): Record<string, unknown> {
|
|
63
|
+
const result = { ...config };
|
|
64
|
+
if (result.providers && typeof result.providers === "object") {
|
|
65
|
+
const providers = { ...(result.providers as Record<string, unknown>) };
|
|
66
|
+
for (const key of Object.keys(providers)) {
|
|
67
|
+
if (providers[key] && typeof providers[key] === "object") {
|
|
68
|
+
const provider = { ...(providers[key] as Record<string, unknown>) };
|
|
69
|
+
delete provider.baseURL;
|
|
70
|
+
delete provider.apiKey;
|
|
71
|
+
providers[key] = provider;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
result.providers = providers;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function capProjectConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
80
|
+
const result = { ...config };
|
|
81
|
+
|
|
82
|
+
// Cap scan settings
|
|
83
|
+
if (result.scan && typeof result.scan === "object") {
|
|
84
|
+
const scan = { ...(result.scan as Record<string, unknown>) };
|
|
85
|
+
if (typeof scan.maxDepth === "number" && scan.maxDepth > 15) scan.maxDepth = 15;
|
|
86
|
+
if (typeof scan.maxFileSize === "number" && scan.maxFileSize > 200000) scan.maxFileSize = 200000;
|
|
87
|
+
result.scan = scan;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Cap agent settings
|
|
91
|
+
if (result.agents && typeof result.agents === "object") {
|
|
92
|
+
const agents = { ...(result.agents as Record<string, unknown>) };
|
|
93
|
+
if (typeof agents.maxConcurrency === "number" && agents.maxConcurrency > 5) agents.maxConcurrency = 5;
|
|
94
|
+
if (typeof agents.maxTokens === "number" && agents.maxTokens > 16384) agents.maxTokens = 16384;
|
|
95
|
+
if (agents.maxStepsOverride && typeof agents.maxStepsOverride === "object") {
|
|
96
|
+
const steps = { ...(agents.maxStepsOverride as Record<string, unknown>) };
|
|
97
|
+
for (const key of Object.keys(steps)) {
|
|
98
|
+
if (typeof steps[key] === "number" && (steps[key] as number) > 20) steps[key] = 20;
|
|
99
|
+
}
|
|
100
|
+
agents.maxStepsOverride = steps;
|
|
101
|
+
}
|
|
102
|
+
result.agents = agents;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function loadConfig(options: LoadConfigOptions = {}): Promise<OpenReportConfig> {
|
|
109
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
110
|
+
|
|
111
|
+
// Note: Bun automatically loads .env from CWD.
|
|
112
|
+
// We intentionally do NOT load .env from the target project root,
|
|
113
|
+
// as a malicious repo could override API keys or redirect API calls.
|
|
114
|
+
|
|
115
|
+
// Cascade: defaults < global config < project config < env vars < CLI flags
|
|
116
|
+
const globalConfig = (await readJsonFile(getGlobalConfigPath())) || {};
|
|
117
|
+
const projectConfig = (await readJsonFile(getProjectConfigPath(projectRoot))) || {};
|
|
118
|
+
const envConfig = envToConfig();
|
|
119
|
+
|
|
120
|
+
const merged = deepMerge(
|
|
121
|
+
DEFAULT_CONFIG,
|
|
122
|
+
globalConfig,
|
|
123
|
+
capProjectConfig(stripSensitiveFields(projectConfig)),
|
|
124
|
+
envConfig,
|
|
125
|
+
options.cliFlags || {}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
return OpenReportConfigSchema.parse(merged);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
throw new ConfigError(
|
|
132
|
+
`Invalid configuration: ${e instanceof Error ? e.message : String(e)}`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Recursively merges plain JSON objects (not class instances, arrays, or Maps).
|
|
139
|
+
* Later values override earlier ones; nested plain objects are merged recursively.
|
|
140
|
+
* Arrays and non-plain-object values are replaced wholesale.
|
|
141
|
+
*/
|
|
142
|
+
function deepMerge(...objects: Record<string, unknown>[]): Record<string, unknown> {
|
|
143
|
+
const result: Record<string, unknown> = {};
|
|
144
|
+
|
|
145
|
+
for (const obj of objects) {
|
|
146
|
+
for (const key of Object.keys(obj)) {
|
|
147
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
148
|
+
const val = obj[key];
|
|
149
|
+
if (
|
|
150
|
+
val &&
|
|
151
|
+
typeof val === "object" &&
|
|
152
|
+
!Array.isArray(val) &&
|
|
153
|
+
result[key] &&
|
|
154
|
+
typeof result[key] === "object" &&
|
|
155
|
+
!Array.isArray(result[key])
|
|
156
|
+
) {
|
|
157
|
+
result[key] = deepMerge(
|
|
158
|
+
result[key] as Record<string, unknown>,
|
|
159
|
+
val as Record<string, unknown>
|
|
160
|
+
);
|
|
161
|
+
} else if (val !== undefined) {
|
|
162
|
+
result[key] = val;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface OllamaModel {
|
|
2
|
+
name: string;
|
|
3
|
+
size: number;
|
|
4
|
+
parameterSize: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function fetchOllamaModels(
|
|
8
|
+
baseURL = "http://localhost:11434"
|
|
9
|
+
): Promise<OllamaModel[]> {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`${baseURL}/api/tags`, {
|
|
12
|
+
signal: AbortSignal.timeout(3000),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!response.ok) return [];
|
|
16
|
+
|
|
17
|
+
const data = (await response.json()) as {
|
|
18
|
+
models?: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
size: number;
|
|
21
|
+
details?: { parameter_size?: string };
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (!data.models) return [];
|
|
26
|
+
|
|
27
|
+
return data.models.map((m) => ({
|
|
28
|
+
name: m.name,
|
|
29
|
+
size: m.size,
|
|
30
|
+
parameterSize: m.details?.parameter_size || "",
|
|
31
|
+
}));
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function isOllamaRunning(
|
|
38
|
+
baseURL = "http://localhost:11434"
|
|
39
|
+
): Promise<boolean> {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(baseURL, {
|
|
42
|
+
signal: AbortSignal.timeout(2000),
|
|
43
|
+
});
|
|
44
|
+
return response.ok;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { LanguageModelV1 } from "ai";
|
|
2
|
+
import type { OpenReportConfig } from "./schema.js";
|
|
3
|
+
import { createCliModel } from "./cli-model.js";
|
|
4
|
+
import { CLI_TOOLS } from "./cli-detection.js";
|
|
5
|
+
import { ProviderError } from "../errors.js";
|
|
6
|
+
|
|
7
|
+
export interface ProviderEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
createModel: (modelId: string) => LanguageModelV1;
|
|
10
|
+
defaultModel: string;
|
|
11
|
+
type: "api" | "local" | "cli";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PROVIDER_DEFAULTS: Record<string, string> = {
|
|
15
|
+
anthropic: "claude-sonnet-4-5-20250929",
|
|
16
|
+
openai: "gpt-4o",
|
|
17
|
+
google: "gemini-2.0-flash",
|
|
18
|
+
mistral: "mistral-large-latest",
|
|
19
|
+
ollama: "qwen2.5-coder:14b",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ── Lazy-loaded SDK factory ──────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
type SdkFactory = (modelId: string) => LanguageModelV1;
|
|
25
|
+
type SdkCreator<T> = (opts: Record<string, unknown>) => T;
|
|
26
|
+
|
|
27
|
+
function createSdkFactory<T>(
|
|
28
|
+
importFn: () => Promise<SdkCreator<T>>,
|
|
29
|
+
buildConfig: (config: OpenReportConfig) => Record<string, unknown>,
|
|
30
|
+
): (config: OpenReportConfig) => Promise<T> {
|
|
31
|
+
let cached: T | null = null;
|
|
32
|
+
return async (config: OpenReportConfig) => {
|
|
33
|
+
if (!cached) {
|
|
34
|
+
const createFn = await importFn();
|
|
35
|
+
cached = createFn(buildConfig(config));
|
|
36
|
+
}
|
|
37
|
+
return cached;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function standardConfig(
|
|
42
|
+
providerKey: keyof OpenReportConfig["providers"],
|
|
43
|
+
envKey: string,
|
|
44
|
+
) {
|
|
45
|
+
return (config: OpenReportConfig): Record<string, unknown> => {
|
|
46
|
+
const provConfig = config.providers[providerKey] || {};
|
|
47
|
+
return {
|
|
48
|
+
apiKey: provConfig.apiKey || process.env[envKey],
|
|
49
|
+
...(provConfig.baseURL ? { baseURL: provConfig.baseURL } : {}),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getAnthropicSdk = createSdkFactory(
|
|
55
|
+
async () => (await import("@ai-sdk/anthropic")).createAnthropic as unknown as SdkCreator<SdkFactory>,
|
|
56
|
+
standardConfig("anthropic", "ANTHROPIC_API_KEY"),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const getOpenAISdk = createSdkFactory(
|
|
60
|
+
async () => (await import("@ai-sdk/openai")).createOpenAI as unknown as SdkCreator<SdkFactory>,
|
|
61
|
+
standardConfig("openai", "OPENAI_API_KEY"),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const getGoogleSdk = createSdkFactory(
|
|
65
|
+
async () => (await import("@ai-sdk/google")).createGoogleGenerativeAI as unknown as SdkCreator<SdkFactory>,
|
|
66
|
+
standardConfig("google", "GOOGLE_GENERATIVE_AI_API_KEY"),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const getMistralSdk = createSdkFactory(
|
|
70
|
+
async () => (await import("@ai-sdk/mistral")).createMistral as unknown as SdkCreator<SdkFactory>,
|
|
71
|
+
standardConfig("mistral", "MISTRAL_API_KEY"),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const getOllamaSdk = createSdkFactory(
|
|
75
|
+
async () => (await import("@ai-sdk/openai")).createOpenAI as unknown as SdkCreator<SdkFactory>,
|
|
76
|
+
(config: OpenReportConfig) => {
|
|
77
|
+
const ollamaConfig = config.providers.ollama || {};
|
|
78
|
+
return {
|
|
79
|
+
apiKey: "ollama",
|
|
80
|
+
baseURL: ollamaConfig.baseURL || process.env.OLLAMA_BASE_URL || "http://localhost:11434/v1",
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// ── Cloud provider definitions (data-driven) ────────────────────────────
|
|
86
|
+
|
|
87
|
+
interface CloudProviderDef {
|
|
88
|
+
id: string;
|
|
89
|
+
getSdk: (config: OpenReportConfig) => Promise<SdkFactory>;
|
|
90
|
+
type: "api" | "local";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const CLOUD_PROVIDERS: CloudProviderDef[] = [
|
|
94
|
+
{ id: "anthropic", getSdk: getAnthropicSdk, type: "api" },
|
|
95
|
+
{ id: "openai", getSdk: getOpenAISdk, type: "api" },
|
|
96
|
+
{ id: "google", getSdk: getGoogleSdk, type: "api" },
|
|
97
|
+
{ id: "mistral", getSdk: getMistralSdk, type: "api" },
|
|
98
|
+
{ id: "ollama", getSdk: getOllamaSdk, type: "local" },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
// ── Registry ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function createProviderRegistry(config: OpenReportConfig) {
|
|
104
|
+
const providers = new Map<string, ProviderEntry>();
|
|
105
|
+
|
|
106
|
+
// ── Cloud/local API providers (lazy-loaded on first createModel call) ──
|
|
107
|
+
|
|
108
|
+
for (const def of CLOUD_PROVIDERS) {
|
|
109
|
+
providers.set(def.id, {
|
|
110
|
+
id: def.id,
|
|
111
|
+
createModel: (modelId: string) => {
|
|
112
|
+
return createLazyModel(() => def.getSdk(config), modelId, def.id);
|
|
113
|
+
},
|
|
114
|
+
defaultModel: PROVIDER_DEFAULTS[def.id],
|
|
115
|
+
type: def.type,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── CLI-based providers (auto-detected) ───────────────────────────
|
|
120
|
+
|
|
121
|
+
for (const cliTool of CLI_TOOLS) {
|
|
122
|
+
providers.set(cliTool.id, {
|
|
123
|
+
id: cliTool.id,
|
|
124
|
+
createModel: (modelId: string) => createCliModel(cliTool.id, modelId),
|
|
125
|
+
defaultModel: cliTool.defaultModel,
|
|
126
|
+
type: "cli",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
getProvider(id: string): ProviderEntry | undefined {
|
|
132
|
+
return providers.get(id);
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
getModel(config: OpenReportConfig): LanguageModelV1 {
|
|
136
|
+
const provider = providers.get(config.defaultProvider);
|
|
137
|
+
if (!provider) {
|
|
138
|
+
throw new ProviderError(`Unknown provider: ${config.defaultProvider}`);
|
|
139
|
+
}
|
|
140
|
+
return provider.createModel(config.defaultModel || provider.defaultModel);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
listProviders(): string[] {
|
|
144
|
+
return Array.from(providers.keys());
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
getProvidersByType(type: "api" | "local" | "cli"): ProviderEntry[] {
|
|
148
|
+
return Array.from(providers.values()).filter((p) => p.type === type);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type ProviderRegistry = ReturnType<typeof createProviderRegistry>;
|
|
154
|
+
|
|
155
|
+
// ── Lazy model proxy ────────────────────────────────────────────────────
|
|
156
|
+
// Creates a LanguageModelV1 that only loads the SDK when doGenerate/doStream is called.
|
|
157
|
+
|
|
158
|
+
function createLazyModel(
|
|
159
|
+
getSdk: () => Promise<SdkFactory>,
|
|
160
|
+
modelId: string,
|
|
161
|
+
providerId: string,
|
|
162
|
+
): LanguageModelV1 {
|
|
163
|
+
let _resolved: LanguageModelV1 | null = null;
|
|
164
|
+
|
|
165
|
+
async function resolve(): Promise<LanguageModelV1> {
|
|
166
|
+
if (!_resolved) {
|
|
167
|
+
const sdk = await getSdk();
|
|
168
|
+
_resolved = sdk(modelId) as LanguageModelV1;
|
|
169
|
+
}
|
|
170
|
+
return _resolved;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const base: LanguageModelV1 = {
|
|
174
|
+
specificationVersion: "v1",
|
|
175
|
+
provider: providerId,
|
|
176
|
+
modelId,
|
|
177
|
+
defaultObjectGenerationMode: undefined,
|
|
178
|
+
|
|
179
|
+
async doGenerate(options) {
|
|
180
|
+
const model = await resolve();
|
|
181
|
+
return model.doGenerate(options);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async doStream(options) {
|
|
185
|
+
const model = await resolve();
|
|
186
|
+
return model.doStream(options);
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
return new Proxy(base, {
|
|
191
|
+
get(target, prop, receiver) {
|
|
192
|
+
if (_resolved && prop in _resolved) {
|
|
193
|
+
const value = (_resolved as Record<string | symbol, unknown>)[prop];
|
|
194
|
+
return typeof value === "function" ? value.bind(_resolved) : value;
|
|
195
|
+
}
|
|
196
|
+
return Reflect.get(target, prop, receiver);
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LanguageModelV1 } from "ai";
|
|
2
|
+
import type { OpenReportConfig } from "./schema.js";
|
|
3
|
+
import type { ProviderRegistry } from "./providers.js";
|
|
4
|
+
import { detectInstalledClis } from "./cli-detection.js";
|
|
5
|
+
import { ProviderError } from "../errors.js";
|
|
6
|
+
import { debugLog } from "../utils/debug.js";
|
|
7
|
+
|
|
8
|
+
export interface ResolvedProvider {
|
|
9
|
+
model: LanguageModelV1;
|
|
10
|
+
effectiveProvider: string;
|
|
11
|
+
effectiveModel: string;
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function resolveProvider(
|
|
16
|
+
config: OpenReportConfig,
|
|
17
|
+
registry: ProviderRegistry,
|
|
18
|
+
): Promise<ResolvedProvider> {
|
|
19
|
+
const detectedClis = await detectInstalledClis();
|
|
20
|
+
const warnings: string[] = [];
|
|
21
|
+
|
|
22
|
+
let effectiveProvider = config.defaultProvider;
|
|
23
|
+
let effectiveModel = config.defaultModel;
|
|
24
|
+
|
|
25
|
+
const isConfiguredAvailable = detectedClis.some(
|
|
26
|
+
(c) => c.id === config.defaultProvider,
|
|
27
|
+
);
|
|
28
|
+
if (!isConfiguredAvailable && detectedClis.length > 0) {
|
|
29
|
+
const msg = `Configured provider '${config.defaultProvider}' unavailable, using CLI fallback: ${detectedClis[0].id}`;
|
|
30
|
+
debugLog("provider:fallback", msg);
|
|
31
|
+
warnings.push(msg);
|
|
32
|
+
effectiveProvider = detectedClis[0].id;
|
|
33
|
+
effectiveModel = detectedClis[0].defaultModel;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let model: LanguageModelV1;
|
|
37
|
+
try {
|
|
38
|
+
const provider = registry.getProvider(effectiveProvider);
|
|
39
|
+
if (provider) {
|
|
40
|
+
model = provider.createModel(effectiveModel);
|
|
41
|
+
} else {
|
|
42
|
+
model = registry.getModel(config);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
if (detectedClis.length > 0) {
|
|
46
|
+
const fallbackProvider = registry.getProvider(detectedClis[0].id);
|
|
47
|
+
if (!fallbackProvider) {
|
|
48
|
+
throw new ProviderError(`Provider "${detectedClis[0].id}" not found in registry`);
|
|
49
|
+
}
|
|
50
|
+
model = fallbackProvider.createModel(detectedClis[0].defaultModel);
|
|
51
|
+
const msg = `Provider '${effectiveProvider}' failed to initialize, using CLI fallback: ${detectedClis[0].id}`;
|
|
52
|
+
debugLog("provider:fallback", msg);
|
|
53
|
+
warnings.push(msg);
|
|
54
|
+
effectiveProvider = detectedClis[0].id;
|
|
55
|
+
effectiveModel = detectedClis[0].defaultModel;
|
|
56
|
+
} else {
|
|
57
|
+
throw new ProviderError("No code agents found. Install one of: claude, gemini, codex");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { model, effectiveProvider, effectiveModel, warnings };
|
|
62
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { debugLog } from "../utils/debug.js";
|
|
4
|
+
|
|
5
|
+
export interface SaveableConfig {
|
|
6
|
+
defaultProvider: string;
|
|
7
|
+
defaultModel: string;
|
|
8
|
+
agents?: {
|
|
9
|
+
maxConcurrency?: number;
|
|
10
|
+
temperature?: number;
|
|
11
|
+
};
|
|
12
|
+
features?: {
|
|
13
|
+
todoList?: boolean;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function saveProjectConfig(
|
|
18
|
+
projectRoot: string,
|
|
19
|
+
config: SaveableConfig
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const configDir = path.join(projectRoot, ".openreport");
|
|
22
|
+
const configFile = path.join(configDir, "config.json");
|
|
23
|
+
|
|
24
|
+
await fs.promises.mkdir(configDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
// Merge with existing config if present
|
|
27
|
+
let existing: Record<string, unknown> = {};
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.promises.readFile(configFile, "utf-8");
|
|
30
|
+
existing = JSON.parse(content);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
debugLog("config:readExisting", e);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const merged = {
|
|
36
|
+
...existing,
|
|
37
|
+
defaultProvider: config.defaultProvider,
|
|
38
|
+
defaultModel: config.defaultModel,
|
|
39
|
+
agents: {
|
|
40
|
+
...((existing.agents as Record<string, unknown>) || {}),
|
|
41
|
+
...(config.agents || {}),
|
|
42
|
+
},
|
|
43
|
+
features: {
|
|
44
|
+
...((existing.features as Record<string, unknown>) || {}),
|
|
45
|
+
...(config.features || {}),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
await fs.promises.writeFile(configFile, JSON.stringify(merged, null, 2), "utf-8");
|
|
50
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const ProviderConfigSchema = z.object({
|
|
4
|
+
apiKey: z.string().optional(),
|
|
5
|
+
baseURL: z.string().optional(),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const OpenReportConfigSchema = z.object({
|
|
9
|
+
defaultProvider: z.string().default("claude-code"),
|
|
10
|
+
defaultModel: z.string().default("sonnet"),
|
|
11
|
+
providers: z
|
|
12
|
+
.record(z.string(), ProviderConfigSchema)
|
|
13
|
+
.default({}),
|
|
14
|
+
output: z
|
|
15
|
+
.object({
|
|
16
|
+
directory: z.string().default(".openreport/reports"),
|
|
17
|
+
format: z.enum(["markdown", "json"]).default("markdown"),
|
|
18
|
+
includeMetadata: z.boolean().default(true),
|
|
19
|
+
})
|
|
20
|
+
.default({}),
|
|
21
|
+
agents: z
|
|
22
|
+
.object({
|
|
23
|
+
maxConcurrency: z.number().min(1).max(10).default(3),
|
|
24
|
+
maxStepsOverride: z.record(z.string(), z.number()).default({}),
|
|
25
|
+
temperature: z.number().min(0).max(1).default(0.3),
|
|
26
|
+
maxTokens: z.number().default(8192),
|
|
27
|
+
})
|
|
28
|
+
.default({}),
|
|
29
|
+
modelTokenLimits: z.record(z.string(), z.number()).default({}).optional(),
|
|
30
|
+
scan: z
|
|
31
|
+
.object({
|
|
32
|
+
exclude: z.array(z.string()).default([]),
|
|
33
|
+
maxFileSize: z.number().default(50000),
|
|
34
|
+
maxDepth: z.number().default(10),
|
|
35
|
+
})
|
|
36
|
+
.default({}),
|
|
37
|
+
ui: z
|
|
38
|
+
.object({
|
|
39
|
+
theme: z.enum(["auto", "dark", "light"]).default("auto"),
|
|
40
|
+
showTokenCount: z.boolean().default(true),
|
|
41
|
+
streamOutput: z.boolean().default(true),
|
|
42
|
+
})
|
|
43
|
+
.default({}),
|
|
44
|
+
features: z
|
|
45
|
+
.object({
|
|
46
|
+
todoList: z.boolean().default(false),
|
|
47
|
+
})
|
|
48
|
+
.default({}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type OpenReportConfig = z.infer<typeof OpenReportConfigSchema>;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class OpenReportError extends Error {
|
|
2
|
+
constructor(message: string, public readonly code: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "OpenReportError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ProviderError extends OpenReportError {
|
|
9
|
+
constructor(message: string) {
|
|
10
|
+
super(message, "PROVIDER_ERROR");
|
|
11
|
+
this.name = "ProviderError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ExtractionError extends OpenReportError {
|
|
16
|
+
constructor(message: string) {
|
|
17
|
+
super(message, "EXTRACTION_ERROR");
|
|
18
|
+
this.name = "ExtractionError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PipelineError extends OpenReportError {
|
|
23
|
+
constructor(message: string) {
|
|
24
|
+
super(message, "PIPELINE_ERROR");
|
|
25
|
+
this.name = "PipelineError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ConfigError extends OpenReportError {
|
|
30
|
+
constructor(message: string) {
|
|
31
|
+
super(message, "CONFIG_ERROR");
|
|
32
|
+
this.name = "ConfigError";
|
|
33
|
+
}
|
|
34
|
+
}
|