@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 ADDED
@@ -0,0 +1,45 @@
1
+ Mapr Closed Source-Available License
2
+
3
+ Copyright (c) 2026 redstone-md
4
+ All rights reserved.
5
+
6
+ 1. Grant of Limited Use
7
+
8
+ Permission is granted to download, run, and use this software for personal, internal, evaluation, research, and contribution purposes only, subject to the conditions below.
9
+
10
+ 2. Restrictions
11
+
12
+ You may not:
13
+
14
+ - sell, sublicense, lease, distribute, publish, or commercially exploit this software or any derivative work;
15
+ - offer this software as a hosted service or as part of a paid or revenue-generating product or service;
16
+ - remove or alter copyright, attribution, or license notices;
17
+ - claim this software or any substantial portion of it as your own work.
18
+
19
+ 3. Contribution-Only Modification Rights
20
+
21
+ You may modify the software only for your own internal use or for the purpose of preparing contributions back to the original project repository or maintainer.
22
+
23
+ You may not distribute modified versions, forks, or derivative works to any third party without prior written permission from the copyright holder.
24
+
25
+ 4. Contributions
26
+
27
+ If you submit code, documentation, ideas, fixes, or other materials to the project, you grant the copyright holder a perpetual, worldwide, irrevocable, sublicensable, transferable, royalty-free right to use, copy, modify, distribute, relicense, and commercialize those contributions in any form.
28
+
29
+ Unless explicitly agreed in writing, you receive no ownership interest in the project by contributing.
30
+
31
+ 5. No Trademark Rights
32
+
33
+ This license does not grant any right to use project names, brands, or logos except as required for accurate attribution.
34
+
35
+ 6. Termination
36
+
37
+ Any use of the software outside these terms automatically terminates the rights granted by this license.
38
+
39
+ 7. Warranty Disclaimer
40
+
41
+ THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT.
42
+
43
+ 8. Limitation of Liability
44
+
45
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # Mapr
2
+
3
+ Mapr is a Bun-native CLI/TUI for reverse-engineering frontend websites and build outputs. It crawls a target site, downloads related code artifacts, formats them for readability, runs a communicating AI swarm over chunked artifact content, and produces a Markdown analysis report with entry points, initialization flow, inferred call graph edges, restored names, investigation tips, and artifact summaries.
4
+
5
+ ## What It Analyzes
6
+
7
+ - HTML entry pages and linked same-origin pages
8
+ - JavaScript bundles and imported chunks
9
+ - Service workers and worker scripts
10
+ - Stylesheets and manifests
11
+ - Referenced WASM modules through binary summaries
12
+ - Cross-linked website artifacts discovered from page code
13
+ - Optional local lexical RAG for oversized artifacts such as multi-megabyte bundles
14
+
15
+ ## Runtime
16
+
17
+ - Bun only
18
+ - TypeScript in strict mode
19
+ - Interactive terminal UX with `@clack/prompts`
20
+ - AI analysis through Vercel AI SDK using OpenAI or OpenAI-compatible providers
21
+ - Headless CLI mode for automation
22
+ - Live swarm progress with agent-level tracking and progress bars
23
+
24
+ ## Workflow
25
+
26
+ 1. Load or configure AI provider settings from `~/.mapr/config.json`
27
+ 2. Discover models from the provider `/models` endpoint
28
+ 3. Let the user search and select a model, then save the model context size
29
+ 4. Crawl the target website and fetch related artifacts
30
+ 5. Format analyzable content where possible
31
+ 6. Optionally build a local lexical RAG index for oversized artifacts
32
+ 7. Run a communicating swarm of analysis agents over chunked artifact content
33
+ 8. Generate a Markdown report in the current working directory
34
+
35
+ ## Quick Start
36
+
37
+ ```bash
38
+ bun install
39
+ bun run index.ts
40
+ ```
41
+
42
+ If the package is published and Bun is installed locally:
43
+
44
+ ```bash
45
+ npx @redstone-md/mapr --help
46
+ ```
47
+
48
+ ## Headless Examples
49
+
50
+ ```bash
51
+ npx @redstone-md/mapr \
52
+ --headless \
53
+ --url http://localhost:5178 \
54
+ --provider-type openai-compatible \
55
+ --provider-name "Local vLLM" \
56
+ --api-key secret \
57
+ --base-url http://localhost:8000/v1 \
58
+ --model qwen2.5-coder \
59
+ --context-size 512000 \
60
+ --local-rag
61
+ ```
62
+
63
+ ```bash
64
+ npx @redstone-md/mapr --list-models --headless --provider-type openai-compatible --api-key secret --base-url http://localhost:8000/v1
65
+ ```
66
+
67
+ ## Swarm Design
68
+
69
+ Mapr uses a communicating agent swarm per chunk:
70
+
71
+ - `scout`: maps artifact surface area and runtime clues
72
+ - `runtime`: reconstructs initialization flow and call relationships
73
+ - `naming`: restores variable and function names from context
74
+ - `security`: identifies risks, persistence, caching, and operator tips
75
+ - `synthesizer`: merges the upstream notes into the final chunk analysis
76
+
77
+ Progress is shown as a task progress bar plus agent/chunk status updates.
78
+
79
+ ## Large Bundle Handling
80
+
81
+ - Mapr stores the selected model context size and derives a larger chunk budget from it.
82
+ - Optional `--local-rag` mode builds a local lexical retrieval index so very large artifacts such as 5 MB bundles can feed more relevant sibling segments into the swarm without forcing the whole file into one prompt.
83
+ - Formatting no longer has a hard artifact-size cutoff. If formatting fails, Mapr falls back to raw content instead of skipping by size.
84
+
85
+ ## Output
86
+
87
+ Each run writes a file named like:
88
+
89
+ ```text
90
+ report-example.com-2026-03-15T12-34-56-789Z.md
91
+ ```
92
+
93
+ ## Disclaimer
94
+
95
+ - Mapr produces assisted reverse-engineering output, not a formal proof of program behavior.
96
+ - AI-generated call graphs, renamed symbols, summaries, and tips are inference-based and may be incomplete or wrong.
97
+ - Website analysis may include proprietary or sensitive code. Use Mapr only when you are authorized to inspect the target.
98
+ - WASM support is summary-based unless you extend the project with deeper binary lifting or disassembly.
99
+
100
+ ## Contribution Terms
101
+
102
+ - This project is source-available and closed-license, not open source.
103
+ - Contributions are accepted only under the repository owner’s terms.
104
+ - By submitting a contribution, you agree that the maintainer may use, modify, relicense, and redistribute your contribution as part of Mapr without compensation.
105
+ - Do not submit code unless you have the rights to contribute it.
106
+
107
+ ## License
108
+
109
+ Use of this project is governed by the custom license in [LICENSE](./LICENSE).
package/bin/mapr ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../index.ts";
package/index.ts ADDED
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cancel, confirm, intro, isCancel, log, outro, spinner, text } from "@clack/prompts";
4
+ import pc from "picocolors";
5
+ import packageJson from "./package.json";
6
+
7
+ import { AiBundleAnalyzer, PartialAnalysisError, buildAnalysisSnapshot, chunkTextByBytes, deriveChunkSizeBytes } from "./lib/ai-analyzer";
8
+ import { parseCliArgs, getConfigOverrides, renderHelpText } from "./lib/cli-args";
9
+ import { ConfigManager } from "./lib/config";
10
+ import { BundleFormatter } from "./lib/formatter";
11
+ import { renderProgressBar } from "./lib/progress";
12
+ import { ReportWriter } from "./lib/reporter";
13
+ import { BundleScraper } from "./lib/scraper";
14
+ import { SWARM_AGENT_ORDER } from "./lib/swarm-prompts";
15
+
16
+ function exitIfCancelled<T>(value: T): T {
17
+ if (isCancel(value)) {
18
+ cancel("Operation cancelled.");
19
+ process.exit(0);
20
+ }
21
+
22
+ return value;
23
+ }
24
+
25
+ function formatError(error: unknown): string {
26
+ return error instanceof Error ? error.message : "An unknown error occurred.";
27
+ }
28
+
29
+ function formatAnalysisProgress(completed: number, total: number, message: string): string {
30
+ return `${renderProgressBar(completed, total)} ${message}`;
31
+ }
32
+
33
+ async function resolveTargetUrl(headless: boolean, prefilledUrl?: string): Promise<string> {
34
+ if (prefilledUrl) {
35
+ return prefilledUrl;
36
+ }
37
+
38
+ if (headless) {
39
+ throw new Error("Headless mode requires --url.");
40
+ }
41
+
42
+ return String(
43
+ exitIfCancelled(
44
+ await text({
45
+ message: "Target URL to analyze",
46
+ placeholder: "http://localhost:5173 or https://example.com",
47
+ validate(value) {
48
+ if (!value) {
49
+ return "Enter a valid URL.";
50
+ }
51
+
52
+ try {
53
+ const parsed = new URL(value);
54
+ return /^https?:$/.test(parsed.protocol) ? undefined : "URL must start with http:// or https://.";
55
+ } catch {
56
+ return "Enter a valid URL.";
57
+ }
58
+ },
59
+ }),
60
+ ),
61
+ );
62
+ }
63
+
64
+ async function run(): Promise<void> {
65
+ const args = parseCliArgs(process.argv.slice(2));
66
+
67
+ if (args.help) {
68
+ console.log(renderHelpText());
69
+ return;
70
+ }
71
+
72
+ if (args.version) {
73
+ console.log(packageJson.version);
74
+ return;
75
+ }
76
+
77
+ const headless = args.headless;
78
+ if (!headless) {
79
+ intro(`${pc.bgCyan(pc.black(" mapr "))} ${pc.bold("Website reverse-engineering for Bun")}`);
80
+ }
81
+
82
+ const configManager = new ConfigManager();
83
+ const configOverrides = getConfigOverrides(args);
84
+ const existingConfig = await configManager.readConfig();
85
+ let forceReconfigure = args.reconfigure;
86
+
87
+ if (!headless && existingConfig && !args.reconfigure && Object.keys(configOverrides).length === 0) {
88
+ forceReconfigure = Boolean(
89
+ exitIfCancelled(
90
+ await confirm({
91
+ message: `Reconfigure AI provider? Current: ${existingConfig.providerName} / ${existingConfig.model}`,
92
+ active: "Reconfigure",
93
+ inactive: "Keep saved config",
94
+ initialValue: false,
95
+ }),
96
+ ),
97
+ );
98
+ }
99
+
100
+ if (args.listModels) {
101
+ const models = await configManager.listModels(await configManager.resolveConfigDraft(configOverrides));
102
+ console.log(models.join("\n"));
103
+ return;
104
+ }
105
+
106
+ const config = await configManager.ensureConfig({
107
+ forceReconfigure,
108
+ headless,
109
+ overrides: configOverrides,
110
+ });
111
+
112
+ const targetUrl = await resolveTargetUrl(headless, args.url);
113
+
114
+ const scrapeStep = spinner({ indicator: "timer" });
115
+ scrapeStep.start("Crawling HTML, scripts, service workers, WASM, and related website artifacts");
116
+ const scraper = new BundleScraper(fetch, {
117
+ maxPages: args.maxPages,
118
+ maxArtifacts: args.maxArtifacts,
119
+ });
120
+ const scrapeResult = await scraper.scrape(targetUrl);
121
+ scrapeStep.stop(
122
+ `Discovered ${scrapeResult.artifacts.length} artifact(s) across ${scrapeResult.htmlPages.length} page(s)`,
123
+ );
124
+
125
+ const formatStep = spinner({ indicator: "timer" });
126
+ formatStep.start("Formatting downloaded artifacts for analysis");
127
+ const formatter = new BundleFormatter();
128
+ const formattedArtifacts = await formatter.formatArtifacts(scrapeResult.artifacts);
129
+ const skippedCount = formattedArtifacts.filter((artifact) => artifact.formattingSkipped).length;
130
+ formatStep.stop(
131
+ skippedCount > 0
132
+ ? `Prepared ${formattedArtifacts.length} artifact(s); formatting fallback used for ${skippedCount} item(s)`
133
+ : `Prepared ${formattedArtifacts.length} artifact(s) for analysis`,
134
+ );
135
+
136
+ const totalChunks = formattedArtifacts.reduce(
137
+ (sum, artifact) =>
138
+ sum + chunkTextByBytes(artifact.formattedContent || artifact.content, deriveChunkSizeBytes(config.modelContextSize)).length,
139
+ 0,
140
+ );
141
+ const totalAgentTasks = Math.max(1, totalChunks * SWARM_AGENT_ORDER.length);
142
+ let completedAgentTasks = 0;
143
+
144
+ const analysisStep = spinner({ indicator: "timer" });
145
+ analysisStep.start(formatAnalysisProgress(0, totalAgentTasks, "Starting swarm analysis"));
146
+
147
+ const analyzer = new AiBundleAnalyzer({
148
+ providerConfig: config,
149
+ localRag: args.localRag,
150
+ onProgress(event) {
151
+ if (event.stage === "agent" && event.state === "completed") {
152
+ completedAgentTasks += 1;
153
+ }
154
+
155
+ const progressLine = formatAnalysisProgress(completedAgentTasks, totalAgentTasks, event.message);
156
+ analysisStep.message(progressLine);
157
+
158
+ if (args.verboseAgents && event.stage === "agent" && event.state === "completed") {
159
+ log.step(progressLine);
160
+ }
161
+ },
162
+ });
163
+
164
+ let analysisError: string | undefined;
165
+ let partialReport = false;
166
+ let analysis = await (async () => {
167
+ try {
168
+ const completedAnalysis = await analyzer.analyze({
169
+ pageUrl: scrapeResult.pageUrl,
170
+ artifacts: formattedArtifacts,
171
+ });
172
+
173
+ analysisStep.stop(
174
+ formatAnalysisProgress(
175
+ totalAgentTasks,
176
+ totalAgentTasks,
177
+ `Analyzed ${completedAnalysis.analyzedChunkCount} chunk(s) across ${formattedArtifacts.length} artifact(s)`,
178
+ ),
179
+ );
180
+
181
+ return completedAnalysis;
182
+ } catch (error) {
183
+ analysisError = formatError(error);
184
+ partialReport = true;
185
+ analysisStep.error(formatAnalysisProgress(completedAgentTasks, totalAgentTasks, `Analysis interrupted: ${analysisError}`));
186
+
187
+ if (error instanceof PartialAnalysisError) {
188
+ return error.partialAnalysis;
189
+ }
190
+
191
+ return buildAnalysisSnapshot({
192
+ overview: `Partial report only. Analysis failed before completion: ${analysisError}`,
193
+ });
194
+ }
195
+ })();
196
+ const reportStatus: "complete" | "partial" = partialReport ? "partial" : "complete";
197
+
198
+ const reportStep = spinner({ indicator: "timer" });
199
+ reportStep.start(reportStatus === "partial" ? "Writing partial Markdown report after analysis error" : "Generating Markdown report");
200
+ const reportWriter = new ReportWriter();
201
+ const reportPath = await reportWriter.writeReport({
202
+ targetUrl: scrapeResult.pageUrl,
203
+ htmlPages: scrapeResult.htmlPages,
204
+ reportStatus,
205
+ ...(analysisError !== undefined ? { analysisError } : {}),
206
+ artifacts: formattedArtifacts,
207
+ analysis,
208
+ ...(args.output !== undefined ? { outputPathOverride: args.output } : {}),
209
+ });
210
+ reportStep.stop(reportStatus === "partial" ? "Partial report written to disk" : "Report written to disk");
211
+
212
+ const summaryLines = [
213
+ reportStatus === "partial" ? `${pc.yellow("Analysis incomplete.")}` : `${pc.green("Analysis complete.")}`,
214
+ `${pc.bold("Status:")} ${reportStatus === "partial" ? "partial report saved after error" : "complete"}`,
215
+ `${pc.bold("Target:")} ${scrapeResult.pageUrl}`,
216
+ `${pc.bold("Provider:")} ${config.providerName} (${config.model})`,
217
+ `${pc.bold("Context size:")} ${config.modelContextSize.toLocaleString()} tokens`,
218
+ `${pc.bold("Local RAG:")} ${args.localRag ? "enabled" : "disabled"}`,
219
+ `${pc.bold("Pages:")} ${scrapeResult.htmlPages.length}`,
220
+ `${pc.bold("Artifacts:")} ${formattedArtifacts.length}`,
221
+ `${pc.bold("Chunks analyzed:")} ${analysis.analyzedChunkCount}`,
222
+ ...(analysisError !== undefined ? [`${pc.bold("Analysis error:")} ${analysisError}`] : []),
223
+ `${pc.bold("Report:")} ${pc.underline(reportPath)}`,
224
+ ].join("\n");
225
+
226
+ if (headless) {
227
+ if (reportStatus === "partial") {
228
+ log.error(summaryLines);
229
+ process.exit(1);
230
+ }
231
+
232
+ log.success(summaryLines);
233
+ return;
234
+ }
235
+
236
+ if (reportStatus === "partial") {
237
+ cancel(summaryLines);
238
+ process.exit(1);
239
+ }
240
+
241
+ outro(summaryLines);
242
+ }
243
+
244
+ run().catch((error) => {
245
+ cancel(pc.red(formatError(error)));
246
+ process.exit(1);
247
+ });