@redstone-md/mapr 0.0.1-alpha
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 +45 -0
- package/README.md +109 -0
- package/bin/mapr +2 -0
- package/index.ts +247 -0
- package/lib/ai-analyzer.ts +598 -0
- package/lib/artifacts.ts +233 -0
- package/lib/cli-args.ts +152 -0
- package/lib/config.ts +385 -0
- package/lib/formatter.ts +109 -0
- package/lib/local-rag.ts +104 -0
- package/lib/progress.ts +10 -0
- package/lib/provider.ts +85 -0
- package/lib/reporter.ts +213 -0
- package/lib/scraper.ts +169 -0
- package/lib/swarm-prompts.ts +56 -0
- package/lib/wasm.ts +62 -0
- package/package.json +62 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { generateText, Output } from "ai";
|
|
2
|
+
import { Buffer } from "buffer";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { artifactTypeSchema } from "./artifacts";
|
|
6
|
+
import type { FormattedArtifact } from "./formatter";
|
|
7
|
+
import { LocalArtifactRag } from "./local-rag";
|
|
8
|
+
import { AiProviderClient, type AiProviderConfig } from "./provider";
|
|
9
|
+
import { SWARM_AGENT_ORDER, getGlobalMissionPrompt, getSwarmAgentPrompt, type SwarmAgentName } from "./swarm-prompts";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_CHUNK_SIZE_BYTES = 80 * 1024;
|
|
12
|
+
|
|
13
|
+
const entryPointSchema = z.object({
|
|
14
|
+
symbol: z.string().min(1),
|
|
15
|
+
description: z.string().min(1),
|
|
16
|
+
evidence: z.string().min(1),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const callGraphEdgeSchema = z.object({
|
|
20
|
+
caller: z.string().min(1),
|
|
21
|
+
callee: z.string().min(1),
|
|
22
|
+
rationale: z.string().min(1),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const renamedSymbolSchema = z.object({
|
|
26
|
+
originalName: z.string().min(1),
|
|
27
|
+
suggestedName: z.string().min(1),
|
|
28
|
+
justification: z.string().min(1),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const agentMemoSchema = z.object({
|
|
32
|
+
role: z.string().min(1),
|
|
33
|
+
summary: z.string().min(1),
|
|
34
|
+
observations: z.array(z.string().min(1)).default([]),
|
|
35
|
+
evidence: z.array(z.string().min(1)).default([]),
|
|
36
|
+
nextQuestions: z.array(z.string().min(1)).default([]),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const chunkAnalysisSchema = z.object({
|
|
40
|
+
entryPoints: z.array(entryPointSchema).default([]),
|
|
41
|
+
initializationFlow: z.array(z.string().min(1)).default([]),
|
|
42
|
+
callGraph: z.array(callGraphEdgeSchema).default([]),
|
|
43
|
+
restoredNames: z.array(renamedSymbolSchema).default([]),
|
|
44
|
+
summary: z.string().min(1),
|
|
45
|
+
notableLibraries: z.array(z.string().min(1)).default([]),
|
|
46
|
+
investigationTips: z.array(z.string().min(1)).default([]),
|
|
47
|
+
risks: z.array(z.string().min(1)).default([]),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const artifactSummarySchema = z.object({
|
|
51
|
+
url: z.string().url(),
|
|
52
|
+
type: artifactTypeSchema,
|
|
53
|
+
chunkCount: z.number().int().nonnegative(),
|
|
54
|
+
summary: z.string().min(1),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const finalAnalysisSchema = z.object({
|
|
58
|
+
overview: z.string().min(1),
|
|
59
|
+
entryPoints: z.array(entryPointSchema).default([]),
|
|
60
|
+
initializationFlow: z.array(z.string().min(1)).default([]),
|
|
61
|
+
callGraph: z.array(callGraphEdgeSchema).default([]),
|
|
62
|
+
restoredNames: z.array(renamedSymbolSchema).default([]),
|
|
63
|
+
notableLibraries: z.array(z.string().min(1)).default([]),
|
|
64
|
+
investigationTips: z.array(z.string().min(1)).default([]),
|
|
65
|
+
risks: z.array(z.string().min(1)).default([]),
|
|
66
|
+
artifactSummaries: z.array(artifactSummarySchema),
|
|
67
|
+
analyzedChunkCount: z.number().int().nonnegative(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const analyzeInputSchema = z.object({
|
|
71
|
+
pageUrl: z.string().url(),
|
|
72
|
+
artifacts: z.array(
|
|
73
|
+
z.object({
|
|
74
|
+
url: z.string().url(),
|
|
75
|
+
type: artifactTypeSchema,
|
|
76
|
+
content: z.string(),
|
|
77
|
+
formattedContent: z.string(),
|
|
78
|
+
sizeBytes: z.number().int().nonnegative(),
|
|
79
|
+
discoveredFrom: z.string().min(1),
|
|
80
|
+
formattingSkipped: z.boolean(),
|
|
81
|
+
formattingNote: z.string().optional(),
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export type BundleAnalysis = z.infer<typeof finalAnalysisSchema>;
|
|
87
|
+
export type AnalysisProgressStage = "artifact" | "chunk" | "agent";
|
|
88
|
+
export type AnalysisProgressState = "started" | "completed";
|
|
89
|
+
|
|
90
|
+
export interface AnalysisProgressEvent {
|
|
91
|
+
stage: AnalysisProgressStage;
|
|
92
|
+
state: AnalysisProgressState;
|
|
93
|
+
message: string;
|
|
94
|
+
artifactIndex: number;
|
|
95
|
+
artifactCount: number;
|
|
96
|
+
artifactUrl: string;
|
|
97
|
+
chunkIndex?: number;
|
|
98
|
+
chunkCount?: number;
|
|
99
|
+
agent?: SwarmAgentName;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface AnalyzerOptions {
|
|
103
|
+
providerConfig: AiProviderConfig;
|
|
104
|
+
chunkSizeBytes?: number;
|
|
105
|
+
localRag?: boolean;
|
|
106
|
+
onProgress?: (event: AnalysisProgressEvent) => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class PartialAnalysisError extends Error {
|
|
110
|
+
public readonly partialAnalysis: BundleAnalysis;
|
|
111
|
+
|
|
112
|
+
public constructor(message: string, partialAnalysis: BundleAnalysis) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.name = "PartialAnalysisError";
|
|
115
|
+
this.partialAnalysis = partialAnalysis;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function createPromptEnvelope(input: {
|
|
120
|
+
pageUrl: string;
|
|
121
|
+
artifact: FormattedArtifact;
|
|
122
|
+
chunk: string;
|
|
123
|
+
chunkIndex: number;
|
|
124
|
+
totalChunks: number;
|
|
125
|
+
memory?: unknown;
|
|
126
|
+
retrievedContext?: string[];
|
|
127
|
+
}): string {
|
|
128
|
+
return [
|
|
129
|
+
`Target page: ${input.pageUrl}`,
|
|
130
|
+
`Artifact URL: ${input.artifact.url}`,
|
|
131
|
+
`Artifact type: ${input.artifact.type}`,
|
|
132
|
+
`Discovered from: ${input.artifact.discoveredFrom}`,
|
|
133
|
+
`Chunk ${input.chunkIndex + 1} of ${input.totalChunks}`,
|
|
134
|
+
input.artifact.formattingNote ? `Formatting note: ${input.artifact.formattingNote}` : "Formatting note: none",
|
|
135
|
+
input.memory ? `Swarm memory:\n${JSON.stringify(input.memory, null, 2)}` : "Swarm memory: none yet",
|
|
136
|
+
input.retrievedContext && input.retrievedContext.length > 0
|
|
137
|
+
? `Local RAG evidence:\n${input.retrievedContext.map((segment, index) => `Segment ${index + 1}:\n${segment}`).join("\n\n")}`
|
|
138
|
+
: "Local RAG evidence: none",
|
|
139
|
+
"Artifact content:",
|
|
140
|
+
"```text",
|
|
141
|
+
input.chunk,
|
|
142
|
+
"```",
|
|
143
|
+
].join("\n\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function findSplitBoundary(source: string, start: number, end: number): number {
|
|
147
|
+
const minimumPreferredIndex = start + Math.max(1, Math.floor((end - start) * 0.6));
|
|
148
|
+
const preferredDelimiters = new Set(["\n", ";", "}", " ", ","]);
|
|
149
|
+
|
|
150
|
+
for (let cursor = end - 1; cursor >= minimumPreferredIndex; cursor -= 1) {
|
|
151
|
+
const character = source[cursor];
|
|
152
|
+
if (character && preferredDelimiters.has(character)) {
|
|
153
|
+
return cursor + 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return end;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function deriveChunkSizeBytes(modelContextSize: number): number {
|
|
161
|
+
const validatedContextSize = z.number().int().positive().parse(modelContextSize);
|
|
162
|
+
const derived = Math.floor(validatedContextSize * 0.9);
|
|
163
|
+
return Math.max(DEFAULT_CHUNK_SIZE_BYTES, derived);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function chunkTextByBytes(source: string, maxBytes = DEFAULT_CHUNK_SIZE_BYTES): string[] {
|
|
167
|
+
const validatedSource = z.string().parse(source);
|
|
168
|
+
const validatedMaxBytes = z.number().int().positive().parse(maxBytes);
|
|
169
|
+
|
|
170
|
+
if (validatedSource.length === 0) {
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const chunks: string[] = [];
|
|
175
|
+
let start = 0;
|
|
176
|
+
|
|
177
|
+
while (start < validatedSource.length) {
|
|
178
|
+
let end = Math.min(validatedSource.length, start + validatedMaxBytes);
|
|
179
|
+
|
|
180
|
+
while (end > start && Buffer.byteLength(validatedSource.slice(start, end), "utf8") > validatedMaxBytes) {
|
|
181
|
+
end -= 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (end <= start) {
|
|
185
|
+
end = start + 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const splitAt = end === validatedSource.length ? end : findSplitBoundary(validatedSource, start, end);
|
|
189
|
+
chunks.push(validatedSource.slice(start, splitAt));
|
|
190
|
+
start = splitAt;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return chunks;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function deduplicate<T>(items: T[], keySelector: (item: T) => string): T[] {
|
|
197
|
+
const seen = new Set<string>();
|
|
198
|
+
const deduplicated: T[] = [];
|
|
199
|
+
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
const key = keySelector(item);
|
|
202
|
+
if (seen.has(key)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
seen.add(key);
|
|
207
|
+
deduplicated.push(item);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return deduplicated;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function normalizeAiError(error: unknown): Error {
|
|
214
|
+
if (!(error instanceof Error)) {
|
|
215
|
+
return new Error("AI analysis failed with an unknown error.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const message = error.message.toLowerCase();
|
|
219
|
+
if (message.includes("rate limit")) {
|
|
220
|
+
return new Error("Provider rate limit hit during analysis. Please retry in a moment.");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (message.includes("api key")) {
|
|
224
|
+
return new Error("The configured API key was rejected by the provider.");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return error;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function buildAnalysisSnapshot(input: {
|
|
231
|
+
overview: string;
|
|
232
|
+
artifactSummaries?: Array<z.infer<typeof artifactSummarySchema>>;
|
|
233
|
+
chunkAnalyses?: Array<z.infer<typeof chunkAnalysisSchema>>;
|
|
234
|
+
}): BundleAnalysis {
|
|
235
|
+
const artifactSummaries = input.artifactSummaries ?? [];
|
|
236
|
+
const chunkAnalyses = input.chunkAnalyses ?? [];
|
|
237
|
+
|
|
238
|
+
return finalAnalysisSchema.parse({
|
|
239
|
+
overview: input.overview,
|
|
240
|
+
entryPoints: deduplicate(
|
|
241
|
+
chunkAnalyses.flatMap((analysis) => analysis.entryPoints),
|
|
242
|
+
(entryPoint) => `${entryPoint.symbol}:${entryPoint.description}`,
|
|
243
|
+
),
|
|
244
|
+
initializationFlow: deduplicate(
|
|
245
|
+
chunkAnalyses.flatMap((analysis) => analysis.initializationFlow),
|
|
246
|
+
(step) => step,
|
|
247
|
+
),
|
|
248
|
+
callGraph: deduplicate(
|
|
249
|
+
chunkAnalyses.flatMap((analysis) => analysis.callGraph),
|
|
250
|
+
(edge) => `${edge.caller}->${edge.callee}`,
|
|
251
|
+
),
|
|
252
|
+
restoredNames: deduplicate(
|
|
253
|
+
chunkAnalyses.flatMap((analysis) => analysis.restoredNames),
|
|
254
|
+
(entry) => `${entry.originalName}:${entry.suggestedName}`,
|
|
255
|
+
),
|
|
256
|
+
notableLibraries: deduplicate(
|
|
257
|
+
chunkAnalyses.flatMap((analysis) => analysis.notableLibraries),
|
|
258
|
+
(library) => library,
|
|
259
|
+
),
|
|
260
|
+
investigationTips: deduplicate(
|
|
261
|
+
chunkAnalyses.flatMap((analysis) => analysis.investigationTips),
|
|
262
|
+
(tip) => tip,
|
|
263
|
+
),
|
|
264
|
+
risks: deduplicate(
|
|
265
|
+
chunkAnalyses.flatMap((analysis) => analysis.risks),
|
|
266
|
+
(risk) => risk,
|
|
267
|
+
),
|
|
268
|
+
artifactSummaries,
|
|
269
|
+
analyzedChunkCount: chunkAnalyses.length,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export class AiBundleAnalyzer {
|
|
274
|
+
private readonly providerClient: AiProviderClient;
|
|
275
|
+
private readonly chunkSizeBytes: number;
|
|
276
|
+
private readonly localRagEnabled: boolean;
|
|
277
|
+
private readonly onProgress: ((event: AnalysisProgressEvent) => void) | undefined;
|
|
278
|
+
|
|
279
|
+
public constructor(options: AnalyzerOptions) {
|
|
280
|
+
this.providerClient = new AiProviderClient(options.providerConfig);
|
|
281
|
+
this.chunkSizeBytes = options.chunkSizeBytes ?? deriveChunkSizeBytes(options.providerConfig.modelContextSize);
|
|
282
|
+
this.localRagEnabled = options.localRag ?? false;
|
|
283
|
+
this.onProgress = options.onProgress;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
public async analyze(input: { pageUrl: string; artifacts: FormattedArtifact[] }): Promise<BundleAnalysis> {
|
|
287
|
+
const validatedInput = analyzeInputSchema.parse(input);
|
|
288
|
+
|
|
289
|
+
if (validatedInput.artifacts.length === 0) {
|
|
290
|
+
return finalAnalysisSchema.parse({
|
|
291
|
+
overview: "No analyzable website artifacts were discovered on the target page.",
|
|
292
|
+
entryPoints: [],
|
|
293
|
+
initializationFlow: [],
|
|
294
|
+
callGraph: [],
|
|
295
|
+
restoredNames: [],
|
|
296
|
+
notableLibraries: [],
|
|
297
|
+
investigationTips: [],
|
|
298
|
+
risks: [],
|
|
299
|
+
artifactSummaries: [],
|
|
300
|
+
analyzedChunkCount: 0,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const chunkAnalyses: Array<z.infer<typeof chunkAnalysisSchema>> = [];
|
|
305
|
+
const artifactSummaries: Array<z.infer<typeof artifactSummarySchema>> = [];
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const localRag = this.localRagEnabled ? new LocalArtifactRag(validatedInput.artifacts) : null;
|
|
309
|
+
|
|
310
|
+
for (let artifactIndex = 0; artifactIndex < validatedInput.artifacts.length; artifactIndex += 1) {
|
|
311
|
+
const artifact = validatedInput.artifacts[artifactIndex]!;
|
|
312
|
+
const chunks = chunkTextByBytes(artifact.formattedContent || artifact.content, this.chunkSizeBytes);
|
|
313
|
+
const perArtifactChunkAnalyses: Array<z.infer<typeof chunkAnalysisSchema>> = [];
|
|
314
|
+
|
|
315
|
+
this.emitProgress({
|
|
316
|
+
stage: "artifact",
|
|
317
|
+
state: "started",
|
|
318
|
+
message: `Starting swarm analysis for artifact ${artifactIndex + 1}/${validatedInput.artifacts.length}: ${artifact.url}`,
|
|
319
|
+
artifactIndex: artifactIndex + 1,
|
|
320
|
+
artifactCount: validatedInput.artifacts.length,
|
|
321
|
+
artifactUrl: artifact.url,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
325
|
+
this.emitProgress({
|
|
326
|
+
stage: "chunk",
|
|
327
|
+
state: "started",
|
|
328
|
+
message: `Starting chunk ${chunkIndex + 1}/${chunks.length} for ${artifact.url}`,
|
|
329
|
+
artifactIndex: artifactIndex + 1,
|
|
330
|
+
artifactCount: validatedInput.artifacts.length,
|
|
331
|
+
artifactUrl: artifact.url,
|
|
332
|
+
chunkIndex: chunkIndex + 1,
|
|
333
|
+
chunkCount: chunks.length,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const analysis = await this.analyzeChunkWithSwarm({
|
|
337
|
+
pageUrl: validatedInput.pageUrl,
|
|
338
|
+
artifact,
|
|
339
|
+
chunk: chunks[chunkIndex] ?? "",
|
|
340
|
+
chunkIndex,
|
|
341
|
+
totalChunks: chunks.length,
|
|
342
|
+
artifactIndex: artifactIndex + 1,
|
|
343
|
+
artifactCount: validatedInput.artifacts.length,
|
|
344
|
+
localRag,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
chunkAnalyses.push(analysis);
|
|
348
|
+
perArtifactChunkAnalyses.push(analysis);
|
|
349
|
+
|
|
350
|
+
this.emitProgress({
|
|
351
|
+
stage: "chunk",
|
|
352
|
+
state: "completed",
|
|
353
|
+
message: `Completed chunk ${chunkIndex + 1}/${chunks.length} for ${artifact.url}`,
|
|
354
|
+
artifactIndex: artifactIndex + 1,
|
|
355
|
+
artifactCount: validatedInput.artifacts.length,
|
|
356
|
+
artifactUrl: artifact.url,
|
|
357
|
+
chunkIndex: chunkIndex + 1,
|
|
358
|
+
chunkCount: chunks.length,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
artifactSummaries.push({
|
|
363
|
+
url: artifact.url,
|
|
364
|
+
type: artifact.type,
|
|
365
|
+
chunkCount: chunks.length,
|
|
366
|
+
summary: perArtifactChunkAnalyses.map((analysis) => analysis.summary).join(" "),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
this.emitProgress({
|
|
370
|
+
stage: "artifact",
|
|
371
|
+
state: "completed",
|
|
372
|
+
message: `Completed swarm analysis for artifact ${artifactIndex + 1}/${validatedInput.artifacts.length}: ${artifact.url}`,
|
|
373
|
+
artifactIndex: artifactIndex + 1,
|
|
374
|
+
artifactCount: validatedInput.artifacts.length,
|
|
375
|
+
artifactUrl: artifact.url,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return await this.summarizeFindings(validatedInput.pageUrl, artifactSummaries, chunkAnalyses);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
const normalizedError = normalizeAiError(error);
|
|
382
|
+
const partialAnalysis = buildAnalysisSnapshot({
|
|
383
|
+
overview:
|
|
384
|
+
chunkAnalyses.length > 0 || artifactSummaries.length > 0
|
|
385
|
+
? `Partial analysis only. Processing stopped because: ${normalizedError.message}`
|
|
386
|
+
: `Analysis aborted before any chunk completed. Cause: ${normalizedError.message}`,
|
|
387
|
+
artifactSummaries,
|
|
388
|
+
chunkAnalyses,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
throw new PartialAnalysisError(normalizedError.message, partialAnalysis);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private async analyzeChunkWithSwarm(input: {
|
|
396
|
+
pageUrl: string;
|
|
397
|
+
artifact: FormattedArtifact;
|
|
398
|
+
chunk: string;
|
|
399
|
+
chunkIndex: number;
|
|
400
|
+
totalChunks: number;
|
|
401
|
+
artifactIndex: number;
|
|
402
|
+
artifactCount: number;
|
|
403
|
+
localRag: LocalArtifactRag | null;
|
|
404
|
+
}): Promise<z.infer<typeof chunkAnalysisSchema>> {
|
|
405
|
+
const memory: Partial<Record<SwarmAgentName, z.infer<typeof agentMemoSchema> | z.infer<typeof chunkAnalysisSchema>>> = {};
|
|
406
|
+
|
|
407
|
+
for (const agent of SWARM_AGENT_ORDER) {
|
|
408
|
+
this.emitProgress({
|
|
409
|
+
stage: "agent",
|
|
410
|
+
state: "started",
|
|
411
|
+
message: `${agent} agent running on ${input.artifact.url} chunk ${input.chunkIndex + 1}/${input.totalChunks}`,
|
|
412
|
+
artifactIndex: input.artifactIndex,
|
|
413
|
+
artifactCount: input.artifactCount,
|
|
414
|
+
artifactUrl: input.artifact.url,
|
|
415
|
+
chunkIndex: input.chunkIndex + 1,
|
|
416
|
+
chunkCount: input.totalChunks,
|
|
417
|
+
agent,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
if (agent === "synthesizer") {
|
|
421
|
+
const synthesized = await this.runSynthesisAgent(input, memory, this.getRetrievedContext(agent, input, memory));
|
|
422
|
+
memory[agent] = synthesized;
|
|
423
|
+
} else {
|
|
424
|
+
const memo = await this.runMemoAgent(agent, input, memory, this.getRetrievedContext(agent, input, memory));
|
|
425
|
+
memory[agent] = memo;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.emitProgress({
|
|
429
|
+
stage: "agent",
|
|
430
|
+
state: "completed",
|
|
431
|
+
message: `${agent} agent completed ${input.artifact.url} chunk ${input.chunkIndex + 1}/${input.totalChunks}`,
|
|
432
|
+
artifactIndex: input.artifactIndex,
|
|
433
|
+
artifactCount: input.artifactCount,
|
|
434
|
+
artifactUrl: input.artifact.url,
|
|
435
|
+
chunkIndex: input.chunkIndex + 1,
|
|
436
|
+
chunkCount: input.totalChunks,
|
|
437
|
+
agent,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return chunkAnalysisSchema.parse(memory.synthesizer);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private async runMemoAgent(
|
|
445
|
+
agent: Exclude<SwarmAgentName, "synthesizer">,
|
|
446
|
+
input: {
|
|
447
|
+
pageUrl: string;
|
|
448
|
+
artifact: FormattedArtifact;
|
|
449
|
+
chunk: string;
|
|
450
|
+
chunkIndex: number;
|
|
451
|
+
totalChunks: number;
|
|
452
|
+
},
|
|
453
|
+
memory: Partial<Record<SwarmAgentName, unknown>>,
|
|
454
|
+
retrievedContext: string[],
|
|
455
|
+
): Promise<z.infer<typeof agentMemoSchema>> {
|
|
456
|
+
const result = await generateText({
|
|
457
|
+
model: this.providerClient.getModel(),
|
|
458
|
+
system: getSwarmAgentPrompt(agent),
|
|
459
|
+
prompt: createPromptEnvelope({
|
|
460
|
+
pageUrl: input.pageUrl,
|
|
461
|
+
artifact: input.artifact,
|
|
462
|
+
chunk: input.chunk,
|
|
463
|
+
chunkIndex: input.chunkIndex,
|
|
464
|
+
totalChunks: input.totalChunks,
|
|
465
|
+
memory,
|
|
466
|
+
retrievedContext,
|
|
467
|
+
}),
|
|
468
|
+
output: Output.object({ schema: agentMemoSchema }),
|
|
469
|
+
maxRetries: 2,
|
|
470
|
+
providerOptions: {
|
|
471
|
+
openai: {
|
|
472
|
+
store: false,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
return agentMemoSchema.parse(result.output);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private async runSynthesisAgent(
|
|
481
|
+
input: {
|
|
482
|
+
pageUrl: string;
|
|
483
|
+
artifact: FormattedArtifact;
|
|
484
|
+
chunk: string;
|
|
485
|
+
chunkIndex: number;
|
|
486
|
+
totalChunks: number;
|
|
487
|
+
},
|
|
488
|
+
memory: Partial<Record<SwarmAgentName, unknown>>,
|
|
489
|
+
retrievedContext: string[],
|
|
490
|
+
): Promise<z.infer<typeof chunkAnalysisSchema>> {
|
|
491
|
+
const result = await generateText({
|
|
492
|
+
model: this.providerClient.getModel(),
|
|
493
|
+
system: getSwarmAgentPrompt("synthesizer"),
|
|
494
|
+
prompt: createPromptEnvelope({
|
|
495
|
+
pageUrl: input.pageUrl,
|
|
496
|
+
artifact: input.artifact,
|
|
497
|
+
chunk: input.chunk,
|
|
498
|
+
chunkIndex: input.chunkIndex,
|
|
499
|
+
totalChunks: input.totalChunks,
|
|
500
|
+
memory,
|
|
501
|
+
retrievedContext,
|
|
502
|
+
}),
|
|
503
|
+
output: Output.object({ schema: chunkAnalysisSchema }),
|
|
504
|
+
maxRetries: 2,
|
|
505
|
+
providerOptions: {
|
|
506
|
+
openai: {
|
|
507
|
+
store: false,
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
return chunkAnalysisSchema.parse(result.output);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private async summarizeFindings(
|
|
516
|
+
pageUrl: string,
|
|
517
|
+
artifactSummaries: Array<z.infer<typeof artifactSummarySchema>>,
|
|
518
|
+
chunkAnalyses: Array<z.infer<typeof chunkAnalysisSchema>>,
|
|
519
|
+
): Promise<BundleAnalysis> {
|
|
520
|
+
try {
|
|
521
|
+
const result = await generateText({
|
|
522
|
+
model: this.providerClient.getModel(),
|
|
523
|
+
system: [
|
|
524
|
+
getGlobalMissionPrompt(),
|
|
525
|
+
"You are the lead synthesis agent for the final report.",
|
|
526
|
+
"Merge artifact summaries and chunk analyses into a coherent site-level reverse-engineering map with the strongest evidence available.",
|
|
527
|
+
].join(" "),
|
|
528
|
+
prompt: [
|
|
529
|
+
`Target page: ${pageUrl}`,
|
|
530
|
+
"Artifact summaries:",
|
|
531
|
+
JSON.stringify(artifactSummaries, null, 2),
|
|
532
|
+
"Chunk analyses:",
|
|
533
|
+
JSON.stringify(chunkAnalyses, null, 2),
|
|
534
|
+
].join("\n\n"),
|
|
535
|
+
output: Output.object({
|
|
536
|
+
schema: finalAnalysisSchema.omit({
|
|
537
|
+
artifactSummaries: true,
|
|
538
|
+
analyzedChunkCount: true,
|
|
539
|
+
}),
|
|
540
|
+
}),
|
|
541
|
+
maxRetries: 2,
|
|
542
|
+
providerOptions: {
|
|
543
|
+
openai: {
|
|
544
|
+
store: false,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return finalAnalysisSchema.parse({
|
|
550
|
+
...result.output,
|
|
551
|
+
artifactSummaries,
|
|
552
|
+
analyzedChunkCount: chunkAnalyses.length,
|
|
553
|
+
});
|
|
554
|
+
} catch {
|
|
555
|
+
return buildAnalysisSnapshot({
|
|
556
|
+
overview: artifactSummaries.map((summary) => summary.summary).join(" ").trim() || "Artifact analysis completed.",
|
|
557
|
+
artifactSummaries,
|
|
558
|
+
chunkAnalyses,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private emitProgress(event: AnalysisProgressEvent): void {
|
|
564
|
+
this.onProgress?.(event);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private getRetrievedContext(
|
|
568
|
+
agent: SwarmAgentName,
|
|
569
|
+
input: {
|
|
570
|
+
artifact: FormattedArtifact;
|
|
571
|
+
chunk: string;
|
|
572
|
+
localRag: LocalArtifactRag | null;
|
|
573
|
+
},
|
|
574
|
+
memory: Partial<Record<SwarmAgentName, unknown>>,
|
|
575
|
+
): string[] {
|
|
576
|
+
if (!input.localRag) {
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const agentKeywords: Record<SwarmAgentName, string> = {
|
|
581
|
+
scout: "imports exports framework runtime mount hydrate render boot start worker register fetch cache dom route manifest css wasm",
|
|
582
|
+
runtime: "entry init bootstrap mount hydrate listener event lifecycle schedule render message postMessage fetch call graph trigger",
|
|
583
|
+
naming: "function class variable module store client request response state cache token auth session api route handler service",
|
|
584
|
+
security: "auth token session cookie localStorage sessionStorage indexedDB cache service worker telemetry endpoint wasm trust dynamic import",
|
|
585
|
+
synthesizer: "entry points call graph restored names investigation tips risks runtime relationships architecture summary",
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const memoryText = Object.values(memory)
|
|
589
|
+
.map((entry) => JSON.stringify(entry))
|
|
590
|
+
.join(" ");
|
|
591
|
+
|
|
592
|
+
return input.localRag.query({
|
|
593
|
+
artifactUrl: input.artifact.url,
|
|
594
|
+
query: `${agentKeywords[agent]} ${input.chunk} ${memoryText}`.slice(0, 6000),
|
|
595
|
+
excludeContent: input.chunk,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|