zefiro 0.8.1 → 0.10.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/README.md +5 -14
- package/dist/cli-5w708rbb.js +167 -0
- package/dist/cli-6qr9gvkp.js +508 -0
- package/dist/cli-7hvj61ht.js +150 -0
- package/dist/cli-b26q1e27.js +264 -0
- package/dist/cli-ewbvvxm8.js +145 -0
- package/dist/cli-g7n2me6z.js +133 -0
- package/dist/cli-kjyet1n8.js +384 -0
- package/dist/cli-m1ghts25.js +133 -0
- package/dist/cli-mm7ct90g.js +110 -0
- package/dist/cli.js +224 -131
- package/dist/explorer-ckk9bkff.js +11 -0
- package/dist/explorer-g6bmbak1.js +10 -0
- package/dist/explorer-svj2p317.js +10 -0
- package/dist/graph-builder-ca71yjkc.js +10 -0
- package/dist/index.js +1 -1
- package/dist/mcp.js +4274 -269
- package/dist/report-sdtah1f4.js +11 -0
- package/dist/report-wh0mnnxb.js +12 -0
- package/package.json +2 -6
- package/skills/zefiro-copilot/SKILL.md +103 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ bun add -g zefiro
|
|
|
17
17
|
zefiro init
|
|
18
18
|
|
|
19
19
|
# 2. Explore a running application
|
|
20
|
-
zefiro
|
|
20
|
+
zefiro explore
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
---
|
|
@@ -52,27 +52,18 @@ On re-run (`.qai/zefiro/agents/` already exists), preserves your existing setup
|
|
|
52
52
|
**After init:**
|
|
53
53
|
1. Generate `.qai/zefiro/context.md` using the `init-agent` prompt in your AI tool, or via MCP (`zefiro_scan_codebase`)
|
|
54
54
|
2. Review the generated context
|
|
55
|
-
3. Run `zefiro
|
|
55
|
+
3. Run `zefiro explore`
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
-
### `zefiro
|
|
59
|
+
### `zefiro explore`
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Explore a running web application via browser automation. Navigates pages, captures screenshots, and generates structured documentation.
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
zefiro
|
|
65
|
-
zefiro scan --scan-dir src/app
|
|
66
|
-
zefiro scan --output custom-scan.json
|
|
67
|
-
zefiro scan --no-cache
|
|
64
|
+
zefiro explore
|
|
68
65
|
```
|
|
69
66
|
|
|
70
|
-
| Option | Description |
|
|
71
|
-
|--------|-------------|
|
|
72
|
-
| `--output <file>` | Output file (default: `.qai/zefiro/ast-scan.json`) |
|
|
73
|
-
| `--scan-dir <dir>` | Directory to scan (overrides config) |
|
|
74
|
-
| `--no-cache` | Disable file-level MD5 caching |
|
|
75
|
-
|
|
76
67
|
---
|
|
77
68
|
|
|
78
69
|
### `zefiro auth`
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// src/browser/url-tracker.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
var SKIP_EXTENSIONS = new Set([
|
|
4
|
+
".pdf",
|
|
5
|
+
".zip",
|
|
6
|
+
".csv",
|
|
7
|
+
".xlsx",
|
|
8
|
+
".xls",
|
|
9
|
+
".doc",
|
|
10
|
+
".docx",
|
|
11
|
+
".png",
|
|
12
|
+
".jpg",
|
|
13
|
+
".jpeg",
|
|
14
|
+
".gif",
|
|
15
|
+
".svg",
|
|
16
|
+
".ico",
|
|
17
|
+
".webp",
|
|
18
|
+
".mp3",
|
|
19
|
+
".mp4",
|
|
20
|
+
".wav",
|
|
21
|
+
".avi",
|
|
22
|
+
".mov"
|
|
23
|
+
]);
|
|
24
|
+
var SKIP_PATTERNS = [
|
|
25
|
+
/\/logout/i,
|
|
26
|
+
/\/signout/i,
|
|
27
|
+
/\/api\//i,
|
|
28
|
+
/\/auth\/callback/i,
|
|
29
|
+
/\/oauth/i,
|
|
30
|
+
/javascript:/i,
|
|
31
|
+
/mailto:/i,
|
|
32
|
+
/tel:/i,
|
|
33
|
+
/#$/
|
|
34
|
+
];
|
|
35
|
+
function normalizeUrl(url, baseUrl) {
|
|
36
|
+
try {
|
|
37
|
+
const resolved = new URL(url, baseUrl);
|
|
38
|
+
resolved.hash = "";
|
|
39
|
+
let normalized = resolved.href;
|
|
40
|
+
if (normalized.endsWith("/") && resolved.pathname !== "/") {
|
|
41
|
+
normalized = normalized.slice(0, -1);
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
} catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function shouldVisit(url, baseUrl, excludePatterns, visited) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = new URL(url);
|
|
51
|
+
const base = new URL(baseUrl);
|
|
52
|
+
if (parsed.origin !== base.origin)
|
|
53
|
+
return false;
|
|
54
|
+
const normalized = normalizeUrl(url, baseUrl);
|
|
55
|
+
if (visited.has(normalized))
|
|
56
|
+
return false;
|
|
57
|
+
const ext = parsed.pathname.match(/\.\w+$/)?.[0]?.toLowerCase();
|
|
58
|
+
if (ext && SKIP_EXTENSIONS.has(ext))
|
|
59
|
+
return false;
|
|
60
|
+
for (const pattern of SKIP_PATTERNS) {
|
|
61
|
+
if (pattern.test(url))
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
for (const pattern of excludePatterns) {
|
|
65
|
+
try {
|
|
66
|
+
if (new RegExp(pattern).test(url))
|
|
67
|
+
return false;
|
|
68
|
+
} catch {
|
|
69
|
+
if (url.includes(pattern))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function extractHrefsFromElements(elements) {
|
|
79
|
+
return elements.filter((el) => el.role === "link").map((el) => el.ref);
|
|
80
|
+
}
|
|
81
|
+
function urlToSlug(url, baseUrl) {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(url, baseUrl);
|
|
84
|
+
const path = parsed.pathname;
|
|
85
|
+
if (path === "/" || path === "")
|
|
86
|
+
return "home";
|
|
87
|
+
return path.replace(/^\/|\/$/g, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
88
|
+
} catch {
|
|
89
|
+
return "unknown";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function buildTopology(pages) {
|
|
93
|
+
const root = { path: "/", slug: "root", stableId: generatePageId("root"), title: "Application", children: [] };
|
|
94
|
+
const nodeMap = new Map;
|
|
95
|
+
nodeMap.set("/", root);
|
|
96
|
+
const sortedPages = Array.from(pages.values()).filter((p) => p.status === "visited").sort((a, b) => a.depth - b.depth);
|
|
97
|
+
for (const page of sortedPages) {
|
|
98
|
+
const segments = page.path.replace(/^\/|\/$/g, "").split("/").filter(Boolean);
|
|
99
|
+
const node = {
|
|
100
|
+
path: page.path,
|
|
101
|
+
slug: page.slug,
|
|
102
|
+
stableId: page.stableId,
|
|
103
|
+
title: page.title || page.slug,
|
|
104
|
+
children: []
|
|
105
|
+
};
|
|
106
|
+
let parentPath = "/";
|
|
107
|
+
if (segments.length > 1) {
|
|
108
|
+
parentPath = "/" + segments.slice(0, -1).join("/");
|
|
109
|
+
}
|
|
110
|
+
const parent = nodeMap.get(parentPath) ?? root;
|
|
111
|
+
parent.children.push(node);
|
|
112
|
+
nodeMap.set(page.path, node);
|
|
113
|
+
}
|
|
114
|
+
return root.children;
|
|
115
|
+
}
|
|
116
|
+
function labelHash(label) {
|
|
117
|
+
return createHash("sha256").update(label.trim().toLowerCase()).digest("hex").slice(0, 8);
|
|
118
|
+
}
|
|
119
|
+
function toKebab(s) {
|
|
120
|
+
return s.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
121
|
+
}
|
|
122
|
+
function generatePageId(slug) {
|
|
123
|
+
return `page:${slug}`;
|
|
124
|
+
}
|
|
125
|
+
function generateSectionId(pageSlug, sectionName) {
|
|
126
|
+
return `section:${pageSlug}:${toKebab(sectionName)}`;
|
|
127
|
+
}
|
|
128
|
+
function generateElementId(pageSlug, role, label) {
|
|
129
|
+
return `element:${pageSlug}:${role}:${labelHash(label)}`;
|
|
130
|
+
}
|
|
131
|
+
function generateEdgeId(sourceId, targetId) {
|
|
132
|
+
return `edge:${sourceId}->${targetId}`;
|
|
133
|
+
}
|
|
134
|
+
function classifySectionType(sectionType) {
|
|
135
|
+
const t = sectionType.toLowerCase();
|
|
136
|
+
if (t.includes("modal") || t.includes("dialog") || t.includes("popup"))
|
|
137
|
+
return "dialog";
|
|
138
|
+
if (t.includes("form"))
|
|
139
|
+
return "form";
|
|
140
|
+
if (t.includes("table") || t.includes("list") || t.includes("grid"))
|
|
141
|
+
return "table";
|
|
142
|
+
if (t.includes("tab"))
|
|
143
|
+
return "tab";
|
|
144
|
+
if (t.includes("filter") || t.includes("search"))
|
|
145
|
+
return "filter";
|
|
146
|
+
if (t.includes("action") || t.includes("toolbar") || t.includes("button"))
|
|
147
|
+
return "action";
|
|
148
|
+
if (t.includes("panel") || t.includes("card") || t.includes("section"))
|
|
149
|
+
return "page";
|
|
150
|
+
return "page";
|
|
151
|
+
}
|
|
152
|
+
function topologyToAscii(nodes, prefix = "") {
|
|
153
|
+
const lines = [];
|
|
154
|
+
for (let i = 0;i < nodes.length; i++) {
|
|
155
|
+
const isLast = i === nodes.length - 1;
|
|
156
|
+
const connector = isLast ? "└── " : "├── ";
|
|
157
|
+
const childPrefix = isLast ? " " : "│ ";
|
|
158
|
+
lines.push(`${prefix}${connector}${nodes[i].path} — ${nodes[i].title}`);
|
|
159
|
+
if (nodes[i].children.length > 0) {
|
|
160
|
+
lines.push(topologyToAscii(nodes[i].children, prefix + childPrefix));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return lines.join(`
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { normalizeUrl, shouldVisit, extractHrefsFromElements, urlToSlug, buildTopology, generatePageId, generateSectionId, generateElementId, generateEdgeId, classifySectionType, topologyToAscii };
|