devlensio 0.2.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 +674 -0
- package/dist/clustering/index.d.ts +27 -0
- package/dist/clustering/index.js +149 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +78 -0
- package/dist/config/providers/file.d.ts +19 -0
- package/dist/config/providers/file.js +215 -0
- package/dist/config/providers/request.d.ts +2 -0
- package/dist/config/providers/request.js +72 -0
- package/dist/config/types.d.ts +46 -0
- package/dist/config/types.js +81 -0
- package/dist/config/writer.d.ts +29 -0
- package/dist/config/writer.js +103 -0
- package/dist/filesystem/appRouter.d.ts +2 -0
- package/dist/filesystem/appRouter.js +126 -0
- package/dist/filesystem/backendRoutes.d.ts +2 -0
- package/dist/filesystem/backendRoutes.js +161 -0
- package/dist/filesystem/index.d.ts +2 -0
- package/dist/filesystem/index.js +28 -0
- package/dist/filesystem/index.test.d.ts +1 -0
- package/dist/filesystem/index.test.js +178 -0
- package/dist/filesystem/pagesRouter.d.ts +2 -0
- package/dist/filesystem/pagesRouter.js +109 -0
- package/dist/fingerprint/detectors.d.ts +8 -0
- package/dist/fingerprint/detectors.js +174 -0
- package/dist/fingerprint/index.d.ts +2 -0
- package/dist/fingerprint/index.js +41 -0
- package/dist/fingerprint/index.test.d.ts +1 -0
- package/dist/fingerprint/index.test.js +148 -0
- package/dist/graph/buildLookup.d.ts +10 -0
- package/dist/graph/buildLookup.js +32 -0
- package/dist/graph/edges/callEdges.d.ts +7 -0
- package/dist/graph/edges/callEdges.js +145 -0
- package/dist/graph/edges/eventEdges.d.ts +7 -0
- package/dist/graph/edges/eventEdges.js +203 -0
- package/dist/graph/edges/guardEdges.d.ts +3 -0
- package/dist/graph/edges/guardEdges.js +232 -0
- package/dist/graph/edges/hookEdges.d.ts +3 -0
- package/dist/graph/edges/hookEdges.js +54 -0
- package/dist/graph/edges/importEdges.d.ts +8 -0
- package/dist/graph/edges/importEdges.js +224 -0
- package/dist/graph/edges/propEdges.d.ts +3 -0
- package/dist/graph/edges/propEdges.js +142 -0
- package/dist/graph/edges/routeEdge.d.ts +3 -0
- package/dist/graph/edges/routeEdge.js +124 -0
- package/dist/graph/edges/stateEdges.d.ts +3 -0
- package/dist/graph/edges/stateEdges.js +206 -0
- package/dist/graph/edges/testEdges.d.ts +3 -0
- package/dist/graph/edges/testEdges.js +143 -0
- package/dist/graph/edges/utils.d.ts +2 -0
- package/dist/graph/edges/utils.js +25 -0
- package/dist/graph/index.d.ts +6 -0
- package/dist/graph/index.js +65 -0
- package/dist/graph/index.test.d.ts +1 -0
- package/dist/graph/index.test.js +542 -0
- package/dist/graph/thirdPartyLibs.d.ts +8 -0
- package/dist/graph/thirdPartyLibs.js +162 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/jobs/index.d.ts +5 -0
- package/dist/jobs/index.js +11 -0
- package/dist/jobs/queue/interface.d.ts +13 -0
- package/dist/jobs/queue/interface.js +1 -0
- package/dist/jobs/queue/memory.d.ts +24 -0
- package/dist/jobs/queue/memory.js +291 -0
- package/dist/jobs/runner.d.ts +3 -0
- package/dist/jobs/runner.js +136 -0
- package/dist/jobs/types.d.ts +112 -0
- package/dist/jobs/types.js +33 -0
- package/dist/parser/directives.d.ts +4 -0
- package/dist/parser/directives.js +31 -0
- package/dist/parser/extractors/components.d.ts +5 -0
- package/dist/parser/extractors/components.js +240 -0
- package/dist/parser/extractors/functions.d.ts +4 -0
- package/dist/parser/extractors/functions.js +240 -0
- package/dist/parser/extractors/hooks.d.ts +4 -0
- package/dist/parser/extractors/hooks.js +128 -0
- package/dist/parser/extractors/stores.d.ts +3 -0
- package/dist/parser/extractors/stores.js +181 -0
- package/dist/parser/index.d.ts +14 -0
- package/dist/parser/index.js +168 -0
- package/dist/parser/index.test.d.ts +1 -0
- package/dist/parser/index.test.js +319 -0
- package/dist/parser/typeUtils.d.ts +9 -0
- package/dist/parser/typeUtils.js +46 -0
- package/dist/pipeline/index.d.ts +50 -0
- package/dist/pipeline/index.js +249 -0
- package/dist/scoring/connectionCounter.d.ts +28 -0
- package/dist/scoring/connectionCounter.js +134 -0
- package/dist/scoring/fileScorer.d.ts +2 -0
- package/dist/scoring/fileScorer.js +44 -0
- package/dist/scoring/index.d.ts +22 -0
- package/dist/scoring/index.js +130 -0
- package/dist/scoring/index.test.d.ts +1 -0
- package/dist/scoring/index.test.js +453 -0
- package/dist/scoring/nodeScorer.d.ts +3 -0
- package/dist/scoring/nodeScorer.js +108 -0
- package/dist/scoring/noiseFilter.d.ts +18 -0
- package/dist/scoring/noiseFilter.js +92 -0
- package/dist/storage/fileStorage.d.ts +117 -0
- package/dist/storage/fileStorage.js +616 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/interface.d.ts +27 -0
- package/dist/storage/interface.js +1 -0
- package/dist/summarizer/checkpoint.d.ts +15 -0
- package/dist/summarizer/checkpoint.js +110 -0
- package/dist/summarizer/index.d.ts +2 -0
- package/dist/summarizer/index.js +281 -0
- package/dist/summarizer/mapreduce.d.ts +4 -0
- package/dist/summarizer/mapreduce.js +87 -0
- package/dist/summarizer/prompts.d.ts +22 -0
- package/dist/summarizer/prompts.js +205 -0
- package/dist/summarizer/providers/anthropic.d.ts +9 -0
- package/dist/summarizer/providers/anthropic.js +78 -0
- package/dist/summarizer/providers/gemini.d.ts +9 -0
- package/dist/summarizer/providers/gemini.js +79 -0
- package/dist/summarizer/providers/index.d.ts +3 -0
- package/dist/summarizer/providers/index.js +43 -0
- package/dist/summarizer/providers/ollama.d.ts +9 -0
- package/dist/summarizer/providers/ollama.js +23 -0
- package/dist/summarizer/providers/openRouter.d.ts +9 -0
- package/dist/summarizer/providers/openRouter.js +19 -0
- package/dist/summarizer/providers/openai.d.ts +9 -0
- package/dist/summarizer/providers/openai.js +72 -0
- package/dist/summarizer/providers/types.d.ts +32 -0
- package/dist/summarizer/providers/types.js +1 -0
- package/dist/summarizer/retry.d.ts +7 -0
- package/dist/summarizer/retry.js +51 -0
- package/dist/summarizer/topological.d.ts +3 -0
- package/dist/summarizer/topological.js +105 -0
- package/dist/summarizer/types.d.ts +57 -0
- package/dist/summarizer/types.js +17 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Call once before the batch loop.
|
|
2
|
+
export function buildEdgeIndex(edges) {
|
|
3
|
+
const outgoing = new Map();
|
|
4
|
+
const incoming = new Map();
|
|
5
|
+
const getOrCreate = (map, nodeId, edgeType) => {
|
|
6
|
+
if (!map.has(nodeId))
|
|
7
|
+
map.set(nodeId, new Map());
|
|
8
|
+
const inner = map.get(nodeId);
|
|
9
|
+
if (!inner.has(edgeType))
|
|
10
|
+
inner.set(edgeType, []);
|
|
11
|
+
return inner.get(edgeType);
|
|
12
|
+
};
|
|
13
|
+
for (const edge of edges) {
|
|
14
|
+
getOrCreate(outgoing, edge.from, edge.type).push(edge.to);
|
|
15
|
+
getOrCreate(incoming, edge.to, edge.type).push(edge.from);
|
|
16
|
+
}
|
|
17
|
+
return { outgoing, incoming };
|
|
18
|
+
}
|
|
19
|
+
// Call once before the batch loop.
|
|
20
|
+
export function buildRouteIndex(routes) {
|
|
21
|
+
const byFilePath = new Map();
|
|
22
|
+
for (const route of routes) {
|
|
23
|
+
byFilePath.set(route.filePath, route);
|
|
24
|
+
}
|
|
25
|
+
return { byFilePath };
|
|
26
|
+
}
|
|
27
|
+
// ─── System Prompt ────────────────────────────────────────────────────────────
|
|
28
|
+
//
|
|
29
|
+
// Built once per summarization run — same for every node.
|
|
30
|
+
// Fingerprint gives the LLM project-level context for better business summaries.
|
|
31
|
+
const BASE_SYSTEM_PROMPT = `You are a senior software engineer analyzing a codebase.
|
|
32
|
+
Your job is to summarize a single code node based on its source code and context.
|
|
33
|
+
|
|
34
|
+
Respond ONLY in this exact XML format — no preamble, no explanation outside the tags:
|
|
35
|
+
|
|
36
|
+
<technical>
|
|
37
|
+
HTML content describing what this code does: its inputs, outputs, side effects, and key logic.
|
|
38
|
+
</technical>
|
|
39
|
+
<business>
|
|
40
|
+
HTML content describing what problem this solves in the product and what feature or domain it belongs to.
|
|
41
|
+
</business>
|
|
42
|
+
<security_severity>none|low|medium|high</security_severity>
|
|
43
|
+
<security_summary>
|
|
44
|
+
HTML content describing the vulnerability (if severity is not none): what data is at risk and how it could be exploited.
|
|
45
|
+
Leave empty if severity is none.
|
|
46
|
+
</security_summary>
|
|
47
|
+
|
|
48
|
+
## Formatting rules for content inside the XML tags
|
|
49
|
+
Use HTML markup — the output is rendered in a browser tooltip, sidebar panel, and detail panel. Follow these conventions:
|
|
50
|
+
|
|
51
|
+
- Inline code, function names, variable names, parameter names, file paths:
|
|
52
|
+
<code>functionName()</code> or <code>someVariable</code>
|
|
53
|
+
- Multi-line code blocks (more than one line of code):
|
|
54
|
+
<pre><code>const x = 1;\nreturn x;</code></pre>
|
|
55
|
+
- Key points / bullet lists (preferred for 3+ items):
|
|
56
|
+
<ul><li>First point.</li><li>Second point.</li></ul>
|
|
57
|
+
- Numbered steps (only when order matters):
|
|
58
|
+
<ol><li>Step one.</li><li>Step two.</li></ol>
|
|
59
|
+
- Bold for important terms or labels: <strong>important</strong>
|
|
60
|
+
- No <h1>/<h2> headings — use <strong> inline labels instead.
|
|
61
|
+
- No inline styles. No <div> wrappers. No Markdown (no **, no backticks, no #).
|
|
62
|
+
- Write in plain prose sentences; use lists only when there are three or more distinct items.
|
|
63
|
+
- Keep paragraphs short — summaries are displayed in compact UI containers (200-300px wide).
|
|
64
|
+
- Avoid long paragraphs; break complex ideas into <ul> lists for scanability.
|
|
65
|
+
|
|
66
|
+
## Security summary guidelines
|
|
67
|
+
The security_summary is displayed in a compact sidebar and a detail panel. Follow these rules:
|
|
68
|
+
- Lead with the specific vulnerability type (e.g. "SQL injection", "XSS", "exposed secret").
|
|
69
|
+
- Use <ul> lists when describing multiple risks or attack vectors.
|
|
70
|
+
- Mention the affected data or resource (e.g. "user credentials", "database contents").
|
|
71
|
+
- Keep it under 3 sentences for scanability.
|
|
72
|
+
- Example: <ul><li><strong>SQL injection</strong>: User input in <code>query</code> is passed directly to <code>db.execute()</code> without parameterization.</li><li>Attackers can extract or modify database contents.</li></ul>
|
|
73
|
+
|
|
74
|
+
## Severity guide
|
|
75
|
+
none — no security concerns
|
|
76
|
+
low — minor issue, limited impact (e.g. verbose error messages)
|
|
77
|
+
medium — potential vulnerability, needs attention (e.g. missing input validation)
|
|
78
|
+
high — serious vulnerability, could be exploited (e.g. SQL injection, exposed secrets, missing auth)`;
|
|
79
|
+
export function buildSystemPrompt(fingerprint) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(`Framework: ${fingerprint.framework}`);
|
|
82
|
+
lines.push(`Language: ${fingerprint.language}`);
|
|
83
|
+
lines.push(`Project type: ${fingerprint.projectType}`);
|
|
84
|
+
if (fingerprint.stateManagement.length > 0)
|
|
85
|
+
lines.push(`State: ${fingerprint.stateManagement.join(", ")}`);
|
|
86
|
+
if (fingerprint.databases.length > 0)
|
|
87
|
+
lines.push(`Databases: ${fingerprint.databases.join(", ")}`);
|
|
88
|
+
if (fingerprint.dataFetching.length > 0)
|
|
89
|
+
lines.push(`Data fetching: ${fingerprint.dataFetching.join(", ")}`);
|
|
90
|
+
return `${BASE_SYSTEM_PROMPT}\n\n## Project Context\n${lines.join("\n")}`;
|
|
91
|
+
}
|
|
92
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
93
|
+
export const EDGE_LABELS = {
|
|
94
|
+
CALLS: "Calls",
|
|
95
|
+
READS_FROM: "Reads from",
|
|
96
|
+
WRITES_TO: "Writes to",
|
|
97
|
+
IMPORTS: "Imports",
|
|
98
|
+
PROP_PASS: "Passes props to",
|
|
99
|
+
EMITS: "Emits event",
|
|
100
|
+
LISTENS: "Listens to",
|
|
101
|
+
WRAPPED_BY: "Wrapped by",
|
|
102
|
+
GUARDS: "Guards",
|
|
103
|
+
HANDLES: "Handles",
|
|
104
|
+
TESTS: "Tests",
|
|
105
|
+
USES: "Uses"
|
|
106
|
+
};
|
|
107
|
+
// Renders a dep summary line — uses technicalSummary if available, else just the name.
|
|
108
|
+
function depLine(depNode) {
|
|
109
|
+
const summary = depNode.technicalSummary
|
|
110
|
+
? ` — ${depNode.technicalSummary.slice(0, 120)}`
|
|
111
|
+
: "";
|
|
112
|
+
return ` ${depNode.name} [${depNode.type}]${summary}`;
|
|
113
|
+
}
|
|
114
|
+
// ─── User Prompt Builder ──────────────────────────────────────────────────────
|
|
115
|
+
function buildUserPrompt(ctx) {
|
|
116
|
+
const { node, allNodes, edgeIndex, routeIndex } = ctx;
|
|
117
|
+
const parts = [];
|
|
118
|
+
// ── Source code ───────────────────────────────────────────────
|
|
119
|
+
parts.push(`## Node: ${node.name} [${node.type}]`);
|
|
120
|
+
parts.push(`File: ${node.filePath}`);
|
|
121
|
+
parts.push("");
|
|
122
|
+
parts.push("### Source Code");
|
|
123
|
+
parts.push("```");
|
|
124
|
+
parts.push(node.rawCode ?? "(source not available)");
|
|
125
|
+
parts.push("```");
|
|
126
|
+
// ── Dependencies (outgoing edges) — O(1) lookup ───────────────
|
|
127
|
+
const outgoing = edgeIndex.outgoing.get(node.id);
|
|
128
|
+
if (outgoing && outgoing.size > 0) {
|
|
129
|
+
const depLines = [];
|
|
130
|
+
for (const [edgeType, targetIds] of outgoing) {
|
|
131
|
+
const label = EDGE_LABELS[edgeType] ?? edgeType;
|
|
132
|
+
for (const targetId of targetIds) {
|
|
133
|
+
const depNode = allNodes.get(targetId);
|
|
134
|
+
if (depNode)
|
|
135
|
+
depLines.push(`${label}:\n${depLine(depNode)}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (depLines.length > 0) {
|
|
139
|
+
parts.push("");
|
|
140
|
+
parts.push("### Dependencies");
|
|
141
|
+
parts.push(depLines.join("\n"));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ── Used by (incoming edges) — O(1) lookup ────────────────────
|
|
145
|
+
const incoming = edgeIndex.incoming.get(node.id);
|
|
146
|
+
if (incoming && incoming.size > 0) {
|
|
147
|
+
const usedByLines = [];
|
|
148
|
+
for (const [edgeType, sourceIds] of incoming) {
|
|
149
|
+
const label = EDGE_LABELS[edgeType] ?? edgeType;
|
|
150
|
+
for (const sourceId of sourceIds) {
|
|
151
|
+
const sourceNode = allNodes.get(sourceId);
|
|
152
|
+
if (sourceNode)
|
|
153
|
+
usedByLines.push(`${label} by: ${sourceNode.name} [${sourceNode.type}]`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (usedByLines.length > 0) {
|
|
157
|
+
parts.push("");
|
|
158
|
+
parts.push("### Used By");
|
|
159
|
+
parts.push(usedByLines.join("\n"));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// ── Route context — O(1) lookup ───────────────────────────────
|
|
163
|
+
const route = routeIndex.byFilePath.get(node.filePath);
|
|
164
|
+
if (route) {
|
|
165
|
+
parts.push("");
|
|
166
|
+
parts.push("### Route Context");
|
|
167
|
+
if ("httpMethod" in route) {
|
|
168
|
+
parts.push(`HTTP ${route.httpMethod} ${route.urlPath}`);
|
|
169
|
+
if (route.params && route.params.length > 0)
|
|
170
|
+
parts.push(`Dynamic params: ${route.params.join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
parts.push(`${route.type} — ${route.urlPath}`);
|
|
174
|
+
if (route.isDynamic && route.params && route.params.length > 0)
|
|
175
|
+
parts.push(`Dynamic params: ${route.params.join(", ")}`);
|
|
176
|
+
if (route.httpMethods && route.httpMethods.length > 0)
|
|
177
|
+
parts.push(`HTTP methods: ${route.httpMethods.join(", ")}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return parts.join("\n");
|
|
181
|
+
}
|
|
182
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
183
|
+
// Builds the full message array for a single node — O(degree) not O(n).
|
|
184
|
+
export function buildPrompt(ctx) {
|
|
185
|
+
return [
|
|
186
|
+
{ role: "system", content: ctx.systemPrompt },
|
|
187
|
+
{ role: "user", content: buildUserPrompt(ctx) },
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
// Builds a grouped prompt for a cycle group.
|
|
191
|
+
// Used when cycleGroup.size <= MAX_GROUP_SUMMARY_SIZE.
|
|
192
|
+
export function buildCycleGroupPrompt(nodeIds, ctx) {
|
|
193
|
+
const nodes = nodeIds.map(id => ctx.allNodes.get(id)).filter(Boolean);
|
|
194
|
+
const userContent = nodes
|
|
195
|
+
.map(node => buildUserPrompt({ ...ctx, node }))
|
|
196
|
+
.join("\n\n---\n\n");
|
|
197
|
+
const systemWithNote = ctx.systemPrompt + "\n\n" +
|
|
198
|
+
`Note: The nodes above form a circular dependency group. ` +
|
|
199
|
+
`Summarize each one, keeping their mutual relationship in mind. ` +
|
|
200
|
+
`Repeat the XML block once per node, preceded by: <!-- node: {name} -->`;
|
|
201
|
+
return [
|
|
202
|
+
{ role: "system", content: systemWithNote },
|
|
203
|
+
{ role: "user", content: userContent },
|
|
204
|
+
];
|
|
205
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
|
|
2
|
+
export declare class AnthropicClient implements LLMClient {
|
|
3
|
+
readonly provider: "anthropic";
|
|
4
|
+
readonly model: string;
|
|
5
|
+
private client;
|
|
6
|
+
constructor(apiKey: string, model: string);
|
|
7
|
+
summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
|
|
8
|
+
validateConnection(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
// ─── Response Parser ──────────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// LLM is prompted to return strict XML — simple and reliable to parse.
|
|
5
|
+
// Falls back gracefully if a tag is missing rather than throwing.
|
|
6
|
+
//
|
|
7
|
+
// Expected format:
|
|
8
|
+
// <technical>...</technical>
|
|
9
|
+
// <business>...</business>
|
|
10
|
+
// <security_severity>none|low|medium|high</security_severity>
|
|
11
|
+
// <security_summary>...</security_summary>
|
|
12
|
+
const VALID_SEVERITIES = new Set(["none", "low", "medium", "high"]);
|
|
13
|
+
function parseXmlTag(text, tag) {
|
|
14
|
+
const match = text.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
|
|
15
|
+
return match ? match[1].trim() : "";
|
|
16
|
+
}
|
|
17
|
+
function parseResponse(raw) {
|
|
18
|
+
const technicalSummary = parseXmlTag(raw, "technical");
|
|
19
|
+
const businessSummary = parseXmlTag(raw, "business");
|
|
20
|
+
const severityRaw = parseXmlTag(raw, "security_severity").toLowerCase();
|
|
21
|
+
const securitySummary = parseXmlTag(raw, "security_summary");
|
|
22
|
+
const severity = VALID_SEVERITIES.has(severityRaw)
|
|
23
|
+
? severityRaw
|
|
24
|
+
: "none";
|
|
25
|
+
return {
|
|
26
|
+
technicalSummary: technicalSummary || raw.trim(), // fallback: use raw if parse fails
|
|
27
|
+
businessSummary: businessSummary || "",
|
|
28
|
+
security: {
|
|
29
|
+
severity,
|
|
30
|
+
summary: severity === "none" ? "" : securitySummary,
|
|
31
|
+
},
|
|
32
|
+
tokensUsed: 0, // populated after API call
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export class AnthropicClient {
|
|
36
|
+
constructor(apiKey, model) {
|
|
37
|
+
this.provider = "anthropic";
|
|
38
|
+
this.model = model;
|
|
39
|
+
this.client = new Anthropic({ apiKey });
|
|
40
|
+
}
|
|
41
|
+
async summarize(request) {
|
|
42
|
+
const systemMessage = request.messages.find(m => m.role === "system");
|
|
43
|
+
const userMessages = request.messages.filter(m => m.role !== "system");
|
|
44
|
+
const response = await this.client.messages.create({
|
|
45
|
+
model: this.model,
|
|
46
|
+
max_tokens: request.maxTokens ?? 2048,
|
|
47
|
+
temperature: request.temperature ?? 0,
|
|
48
|
+
system: systemMessage?.content,
|
|
49
|
+
messages: userMessages.map(m => ({ role: m.role, content: m.content })),
|
|
50
|
+
});
|
|
51
|
+
const raw = response.content[0].type === "text" ? response.content[0].text : "";
|
|
52
|
+
const result = parseResponse(raw);
|
|
53
|
+
result.tokensUsed = (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async validateConnection() {
|
|
57
|
+
try {
|
|
58
|
+
// Minimal call — 1 token in, 1 token out, just to verify key + model are valid
|
|
59
|
+
await this.client.messages.create({
|
|
60
|
+
model: this.model,
|
|
61
|
+
max_tokens: 10,
|
|
62
|
+
messages: [{ role: "user", content: "hi" }],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const status = err?.status ?? err?.statusCode;
|
|
67
|
+
if (status === 401)
|
|
68
|
+
throw new Error(`Anthropic API key is invalid or missing. Check your key in config.`);
|
|
69
|
+
if (status === 403)
|
|
70
|
+
throw new Error(`Anthropic API key does not have permission to use model "${this.model}".`);
|
|
71
|
+
if (status === 404)
|
|
72
|
+
throw new Error(`Anthropic model "${this.model}" not found. Check model name in config.`);
|
|
73
|
+
if (status === 429)
|
|
74
|
+
throw new Error(`Anthropic rate limit hit during connection check. Try again shortly.`);
|
|
75
|
+
throw new Error(`Anthropic connection failed: ${err?.message ?? "unknown error"}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
|
|
2
|
+
export declare class GeminiClient implements LLMClient {
|
|
3
|
+
readonly provider: "gemini";
|
|
4
|
+
readonly model: string;
|
|
5
|
+
private ai;
|
|
6
|
+
constructor(apiKey: string, model: string);
|
|
7
|
+
summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
|
|
8
|
+
validateConnection(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GoogleGenAI } from "@google/genai";
|
|
2
|
+
// ─── Response Parser ──────────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// Same XML format as all other providers — consistent across the board.
|
|
5
|
+
const VALID_SEVERITIES = new Set(["none", "low", "medium", "high"]);
|
|
6
|
+
function parseXmlTag(text, tag) {
|
|
7
|
+
const match = text.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
|
|
8
|
+
return match ? match[1].trim() : "";
|
|
9
|
+
}
|
|
10
|
+
function parseResponse(raw) {
|
|
11
|
+
const technicalSummary = parseXmlTag(raw, "technical");
|
|
12
|
+
const businessSummary = parseXmlTag(raw, "business");
|
|
13
|
+
const severityRaw = parseXmlTag(raw, "security_severity").toLowerCase();
|
|
14
|
+
const securitySummary = parseXmlTag(raw, "security_summary");
|
|
15
|
+
const severity = VALID_SEVERITIES.has(severityRaw)
|
|
16
|
+
? severityRaw
|
|
17
|
+
: "none";
|
|
18
|
+
return {
|
|
19
|
+
technicalSummary: technicalSummary || raw.trim(),
|
|
20
|
+
businessSummary: businessSummary || "",
|
|
21
|
+
security: {
|
|
22
|
+
severity,
|
|
23
|
+
summary: severity === "none" ? "" : securitySummary,
|
|
24
|
+
},
|
|
25
|
+
tokensUsed: 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export class GeminiClient {
|
|
29
|
+
constructor(apiKey, model) {
|
|
30
|
+
this.provider = "gemini";
|
|
31
|
+
this.model = model;
|
|
32
|
+
this.ai = new GoogleGenAI({ apiKey });
|
|
33
|
+
}
|
|
34
|
+
async summarize(request) {
|
|
35
|
+
const systemMessage = request.messages.find(m => m.role === "system");
|
|
36
|
+
const userMessages = request.messages.filter(m => m.role !== "system");
|
|
37
|
+
const contents = userMessages.map(m => ({
|
|
38
|
+
role: "user",
|
|
39
|
+
parts: [{ text: m.content }],
|
|
40
|
+
}));
|
|
41
|
+
const response = await this.ai.models.generateContent({
|
|
42
|
+
model: this.model,
|
|
43
|
+
contents,
|
|
44
|
+
config: {
|
|
45
|
+
systemInstruction: systemMessage?.content,
|
|
46
|
+
temperature: request.temperature ?? 0,
|
|
47
|
+
maxOutputTokens: request.maxTokens ?? 2048,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const raw = response.text ?? "";
|
|
51
|
+
const result = parseResponse(raw);
|
|
52
|
+
result.tokensUsed =
|
|
53
|
+
(response.usageMetadata?.promptTokenCount ?? 0) +
|
|
54
|
+
(response.usageMetadata?.candidatesTokenCount ?? 0);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
async validateConnection() {
|
|
58
|
+
try {
|
|
59
|
+
await this.ai.models.generateContent({
|
|
60
|
+
model: this.model,
|
|
61
|
+
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
|
62
|
+
config: { maxOutputTokens: 10 },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const status = err?.status ?? err?.statusCode ?? err?.code;
|
|
67
|
+
const message = err?.message ?? "";
|
|
68
|
+
if (status === 400 || message.includes("API_KEY_INVALID") || message.includes("invalid api key"))
|
|
69
|
+
throw new Error(`Gemini API key is invalid or missing. Check your key in config.`);
|
|
70
|
+
if (status === 403 || message.includes("PERMISSION_DENIED"))
|
|
71
|
+
throw new Error(`Gemini API key does not have permission to use model "${this.model}".`);
|
|
72
|
+
if (status === 404 || message.includes("models/") || message.includes("not found"))
|
|
73
|
+
throw new Error(`Gemini model "${this.model}" not found. Check model name in config.`);
|
|
74
|
+
if (status === 429 || message.includes("RESOURCE_EXHAUSTED"))
|
|
75
|
+
throw new Error(`Gemini rate limit hit during connection check. Try again shortly.`);
|
|
76
|
+
throw new Error(`Gemini connection failed: ${message || "unknown error"}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// providers/index.ts has one job — given the config, return the right LLMClient instance. It's the factory that the batch loop calls so it never has to know which provider is being used.
|
|
2
|
+
import { AnthropicClient } from "./anthropic.js";
|
|
3
|
+
import { GeminiClient } from "./gemini.js";
|
|
4
|
+
import { OllamaClient } from "./ollama.js";
|
|
5
|
+
import { OpenAIClient } from "./openai.js";
|
|
6
|
+
import { OpenRouterClient } from "./openRouter.js";
|
|
7
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Single entry point for the batch loop — returns the right LLMClient
|
|
10
|
+
// based on config. Caller never imports individual provider classes.
|
|
11
|
+
//
|
|
12
|
+
// Throws clearly if:
|
|
13
|
+
// - provider is unknown
|
|
14
|
+
// - apiKey is missing for cloud providers (fail fast before the batch loop)
|
|
15
|
+
export function createLLMClient(config) {
|
|
16
|
+
const { provider, model, apiKey, baseUrl } = config;
|
|
17
|
+
switch (provider) {
|
|
18
|
+
case "anthropic":
|
|
19
|
+
if (!apiKey)
|
|
20
|
+
throw new Error("Anthropic provider requires an API key");
|
|
21
|
+
return new AnthropicClient(apiKey, model);
|
|
22
|
+
case "openai":
|
|
23
|
+
if (!apiKey)
|
|
24
|
+
throw new Error("OpenAI provider requires an API key");
|
|
25
|
+
return new OpenAIClient(apiKey, model);
|
|
26
|
+
case "openrouter":
|
|
27
|
+
if (!apiKey)
|
|
28
|
+
throw new Error("OpenRouter provider requires an API key");
|
|
29
|
+
return new OpenRouterClient(apiKey, model);
|
|
30
|
+
case "gemini":
|
|
31
|
+
if (!apiKey)
|
|
32
|
+
throw new Error("Gemini provider requires an API key");
|
|
33
|
+
return new GeminiClient(apiKey, model);
|
|
34
|
+
case "ollama":
|
|
35
|
+
return new OllamaClient(model, baseUrl);
|
|
36
|
+
case "managed":
|
|
37
|
+
// Cloud SaaS only — platform injects the key via request headers.
|
|
38
|
+
// Should never reach here in local mode.
|
|
39
|
+
throw new Error("Managed provider is only available in cloud mode");
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
|
|
2
|
+
export declare class OllamaClient implements LLMClient {
|
|
3
|
+
readonly provider: "ollama";
|
|
4
|
+
readonly model: string;
|
|
5
|
+
private inner;
|
|
6
|
+
constructor(model: string, baseURL?: string);
|
|
7
|
+
summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
|
|
8
|
+
validateConnection(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { OpenAIClient } from "./openai.js";
|
|
2
|
+
const OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434";
|
|
3
|
+
// ─── OllamaClient ─────────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Ollama exposes an OpenAI-compatible API at /v1 — wraps OpenAIClient.
|
|
6
|
+
// No API key needed for local usage — passes a placeholder to satisfy the SDK.
|
|
7
|
+
// baseURL is configurable for users running Ollama on a non-default port or host.
|
|
8
|
+
// /v1 is always appended — user-provided baseURL should not include it.
|
|
9
|
+
export class OllamaClient {
|
|
10
|
+
constructor(model, baseURL) {
|
|
11
|
+
this.provider = "ollama";
|
|
12
|
+
this.model = model;
|
|
13
|
+
const base = (baseURL ?? OLLAMA_DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
14
|
+
this.inner = new OpenAIClient("ollama", // placeholder — Ollama doesn't validate the key
|
|
15
|
+
model, `${base}/v1`);
|
|
16
|
+
}
|
|
17
|
+
summarize(request) {
|
|
18
|
+
return this.inner.summarize(request);
|
|
19
|
+
}
|
|
20
|
+
validateConnection() {
|
|
21
|
+
return this.inner.validateConnection();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
|
|
2
|
+
export declare class OpenRouterClient implements LLMClient {
|
|
3
|
+
readonly provider: "openrouter";
|
|
4
|
+
readonly model: string;
|
|
5
|
+
private inner;
|
|
6
|
+
constructor(apiKey: string, model: string);
|
|
7
|
+
summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
|
|
8
|
+
validateConnection(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { OpenAIClient } from "./openai.js";
|
|
2
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
3
|
+
// ─── OpenRouterClient ─────────────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// OpenRouter exposes an OpenAI-compatible API — no new logic needed.
|
|
6
|
+
// Just wraps OpenAIClient with the correct baseURL and overrides provider. (OpenAI actually set the industry standard for the response of the LLMs through API.Everyone follows the OpenAI standard, Aside of course Anthropic, because its anthropic, who even go against pentagon.)
|
|
7
|
+
export class OpenRouterClient {
|
|
8
|
+
constructor(apiKey, model) {
|
|
9
|
+
this.provider = "openrouter";
|
|
10
|
+
this.model = model;
|
|
11
|
+
this.inner = new OpenAIClient(apiKey, model, OPENROUTER_BASE_URL);
|
|
12
|
+
}
|
|
13
|
+
summarize(request) {
|
|
14
|
+
return this.inner.summarize(request);
|
|
15
|
+
}
|
|
16
|
+
validateConnection() {
|
|
17
|
+
return this.inner.validateConnection();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LLMClient, LLMRequest, NodeSummaryOutput } from "./types.js";
|
|
2
|
+
export declare class OpenAIClient implements LLMClient {
|
|
3
|
+
readonly provider: "openai";
|
|
4
|
+
readonly model: string;
|
|
5
|
+
private client;
|
|
6
|
+
constructor(apiKey: string, model: string, baseURL?: string);
|
|
7
|
+
summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
|
|
8
|
+
validateConnection(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
// ─── Response Parser ──────────────────────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// Same XML format as anthropic.ts — consistent across all providers.
|
|
5
|
+
// Prompt in prompts.ts always requests this format regardless of provider.
|
|
6
|
+
const VALID_SEVERITIES = new Set(["none", "low", "medium", "high"]);
|
|
7
|
+
function parseXmlTag(text, tag) {
|
|
8
|
+
const match = text.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`));
|
|
9
|
+
return match ? match[1].trim() : "";
|
|
10
|
+
}
|
|
11
|
+
function parseResponse(raw) {
|
|
12
|
+
const technicalSummary = parseXmlTag(raw, "technical");
|
|
13
|
+
const businessSummary = parseXmlTag(raw, "business");
|
|
14
|
+
const severityRaw = parseXmlTag(raw, "security_severity").toLowerCase();
|
|
15
|
+
const securitySummary = parseXmlTag(raw, "security_summary");
|
|
16
|
+
const severity = VALID_SEVERITIES.has(severityRaw)
|
|
17
|
+
? severityRaw
|
|
18
|
+
: "none";
|
|
19
|
+
return {
|
|
20
|
+
technicalSummary: technicalSummary || raw.trim(),
|
|
21
|
+
businessSummary: businessSummary || "",
|
|
22
|
+
security: {
|
|
23
|
+
severity,
|
|
24
|
+
summary: severity === "none" ? "" : securitySummary,
|
|
25
|
+
},
|
|
26
|
+
tokensUsed: 0,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Also used as the base for OpenRouter and Ollama —
|
|
30
|
+
// both expose OpenAI-compatible APIs, just with a different baseURL.
|
|
31
|
+
export class OpenAIClient {
|
|
32
|
+
constructor(apiKey, model, baseURL) {
|
|
33
|
+
this.provider = "openai";
|
|
34
|
+
this.model = model;
|
|
35
|
+
this.client = new OpenAI({ apiKey, ...(baseURL ? { baseURL } : {}) });
|
|
36
|
+
}
|
|
37
|
+
async summarize(request) {
|
|
38
|
+
const response = await this.client.chat.completions.create({
|
|
39
|
+
model: this.model,
|
|
40
|
+
temperature: request.temperature ?? 0,
|
|
41
|
+
max_tokens: request.maxTokens ?? 2048,
|
|
42
|
+
messages: request.messages.map(m => ({ role: m.role, content: m.content })),
|
|
43
|
+
});
|
|
44
|
+
const raw = response.choices[0]?.message?.content ?? "";
|
|
45
|
+
const result = parseResponse(raw);
|
|
46
|
+
result.tokensUsed = response.usage
|
|
47
|
+
? (response.usage.prompt_tokens + response.usage.completion_tokens)
|
|
48
|
+
: 0;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
async validateConnection() {
|
|
52
|
+
try {
|
|
53
|
+
await this.client.chat.completions.create({
|
|
54
|
+
model: this.model,
|
|
55
|
+
max_tokens: 10,
|
|
56
|
+
messages: [{ role: "user", content: "hi" }],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const status = err?.status ?? err?.statusCode;
|
|
61
|
+
if (status === 401)
|
|
62
|
+
throw new Error(`API key is invalid or missing. Check your key in config.`);
|
|
63
|
+
if (status === 403)
|
|
64
|
+
throw new Error(`API key does not have permission to use model "${this.model}".`);
|
|
65
|
+
if (status === 404)
|
|
66
|
+
throw new Error(`Model "${this.model}" not found. Check model name in config.`);
|
|
67
|
+
if (status === 429)
|
|
68
|
+
throw new Error(`Rate limit hit during connection check. Try again shortly.`);
|
|
69
|
+
throw new Error(`LLM connection failed: ${err?.message ?? "unknown error"}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { LLMProvider } from "../../config/types.js";
|
|
2
|
+
export interface LLMMessage {
|
|
3
|
+
role: "user" | "assistant" | "system";
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LLMRequest {
|
|
7
|
+
messages: LLMMessage[];
|
|
8
|
+
temperature?: number;
|
|
9
|
+
maxTokens?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SecuritySummary {
|
|
12
|
+
severity: "none" | "low" | "medium" | "high";
|
|
13
|
+
summary: string;
|
|
14
|
+
}
|
|
15
|
+
export interface NodeSummaryOutput {
|
|
16
|
+
technicalSummary: string;
|
|
17
|
+
businessSummary: string;
|
|
18
|
+
security: SecuritySummary;
|
|
19
|
+
tokensUsed: number;
|
|
20
|
+
}
|
|
21
|
+
export interface LLMClient {
|
|
22
|
+
readonly provider: LLMProvider;
|
|
23
|
+
readonly model: string;
|
|
24
|
+
summarize(request: LLMRequest): Promise<NodeSummaryOutput>;
|
|
25
|
+
validateConnection(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export type LLMClientFactory = (config: {
|
|
28
|
+
provider: LLMProvider;
|
|
29
|
+
model: string;
|
|
30
|
+
apiKey?: string;
|
|
31
|
+
baseUrl?: string;
|
|
32
|
+
}) => LLMClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface RetryOptions {
|
|
2
|
+
maxAttempts: number;
|
|
3
|
+
baseDelayMs: number;
|
|
4
|
+
maxDelayMs: number;
|
|
5
|
+
}
|
|
6
|
+
export declare const DEFAULT_RETRY_OPTIONS: RetryOptions;
|
|
7
|
+
export declare function withRetry<T>(fn: () => Promise<T>, opts?: RetryOptions, label?: string): Promise<T>;
|