zefiro 0.9.0 → 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.
@@ -0,0 +1,384 @@
1
+ import {
2
+ topologyToAscii
3
+ } from "./cli-5w708rbb.js";
4
+
5
+ // src/browser/report.ts
6
+ function generateSectionMarkdown(page, relatedPages, annotations) {
7
+ const analysis = page.analysis;
8
+ const lines = [];
9
+ const name = analysis?.pageName ?? page.title ?? page.slug;
10
+ lines.push(`# ${name} (\`${page.path}\`)`);
11
+ lines.push("");
12
+ if (analysis?.description) {
13
+ lines.push(`> ${analysis.description}`);
14
+ lines.push("");
15
+ }
16
+ lines.push(`**Route:** \`${page.path}\``);
17
+ if (analysis?.navigation?.tabs?.length) {
18
+ lines.push(`**Tabs:** ${analysis.navigation.tabs.join(", ")}`);
19
+ }
20
+ lines.push("");
21
+ lines.push("---");
22
+ lines.push("");
23
+ lines.push(`![${name}](../screenshots/${page.slug}/01-default.png)`);
24
+ lines.push("");
25
+ if (analysis?.sections) {
26
+ for (const section of analysis.sections) {
27
+ lines.push(`## ${section.name}`);
28
+ lines.push("");
29
+ if (section.elements?.length) {
30
+ const hasRequired = section.elements.some((e) => e.required !== undefined);
31
+ if (hasRequired) {
32
+ lines.push("| Field | Type | Required | Description |");
33
+ lines.push("|-------|------|----------|-------------|");
34
+ for (const el of section.elements) {
35
+ lines.push(`| ${el.name} | ${el.type} | ${el.required ? "Yes" : "No"} | ${el.description} |`);
36
+ }
37
+ } else {
38
+ lines.push("| Control | Type | Description |");
39
+ lines.push("|---------|------|-------------|");
40
+ for (const el of section.elements) {
41
+ lines.push(`| ${el.name} | ${el.type} | ${el.description} |`);
42
+ }
43
+ }
44
+ lines.push("");
45
+ }
46
+ if (section.tableColumns?.length) {
47
+ lines.push("### Table Columns");
48
+ lines.push("");
49
+ lines.push("| Column | Sortable | Description |");
50
+ lines.push("|--------|----------|-------------|");
51
+ for (const col of section.tableColumns) {
52
+ lines.push(`| ${col.name} | ${col.sortable ? "Yes" : "No"} | ${col.description} |`);
53
+ }
54
+ lines.push("");
55
+ }
56
+ lines.push("---");
57
+ lines.push("");
58
+ }
59
+ }
60
+ if (analysis?.actions?.length) {
61
+ lines.push("## Available Actions");
62
+ lines.push("");
63
+ for (const action of analysis.actions) {
64
+ lines.push(`- ${action}`);
65
+ }
66
+ lines.push("");
67
+ }
68
+ if (analysis?.observations?.length) {
69
+ lines.push("## Key Behaviors");
70
+ lines.push("");
71
+ for (const obs of analysis.observations) {
72
+ lines.push(`- ${obs}`);
73
+ }
74
+ lines.push("");
75
+ }
76
+ const related = relatedPages.filter((p) => p.slug !== page.slug);
77
+ if (related.length > 0) {
78
+ lines.push("## Related Pages");
79
+ lines.push("");
80
+ for (const rp of related) {
81
+ lines.push(`- [${rp.name}](./${rp.slug}.md)`);
82
+ }
83
+ lines.push("");
84
+ }
85
+ if (annotations?.length) {
86
+ lines.push("## Annotations (voice)");
87
+ lines.push("");
88
+ for (const ann of annotations) {
89
+ lines.push(`- "${ann.text}" — ${ann.timestamp}`);
90
+ }
91
+ lines.push("");
92
+ }
93
+ return lines.join(`
94
+ `);
95
+ }
96
+ function generateReadme(state) {
97
+ const lines = [];
98
+ const visited = Array.from(state.pages.values()).filter((p) => p.status === "visited");
99
+ const errors = Array.from(state.pages.values()).filter((p) => p.status === "error");
100
+ lines.push("# Application Map");
101
+ lines.push("");
102
+ lines.push(`> **Scan Date:** ${new Date(state.startedAt).toISOString().split("T")[0]}`);
103
+ lines.push(`> **URL:** \`${state.baseUrl}\``);
104
+ lines.push(`> **Pages Explored:** ${visited.length}`);
105
+ lines.push("");
106
+ lines.push("---");
107
+ lines.push("");
108
+ lines.push("## Application Structure");
109
+ lines.push("");
110
+ lines.push("```");
111
+ lines.push(`Application (${state.baseUrl})`);
112
+ if (state.topology.length > 0) {
113
+ lines.push(topologyToAscii(state.topology));
114
+ }
115
+ lines.push("```");
116
+ lines.push("");
117
+ lines.push("---");
118
+ lines.push("");
119
+ lines.push("## Section Reports");
120
+ lines.push("");
121
+ lines.push("| Section | Report | Screenshots |");
122
+ lines.push("|---------|--------|-------------|");
123
+ for (const page of visited) {
124
+ const name = page.analysis?.pageName ?? page.title ?? page.slug;
125
+ lines.push(`| ${name} | [${page.slug}.md](./sections/${page.slug}.md) | [screenshots/${page.slug}/](./screenshots/${page.slug}/) |`);
126
+ }
127
+ lines.push("");
128
+ lines.push("---");
129
+ lines.push("");
130
+ lines.push("## Route Map");
131
+ lines.push("");
132
+ lines.push("| Route | Section | Elements | Links |");
133
+ lines.push("|-------|---------|----------|-------|");
134
+ for (const page of visited) {
135
+ const name = page.analysis?.pageName ?? page.title ?? page.slug;
136
+ lines.push(`| \`${page.path}\` | ${name} | ${page.elements.length} | ${page.outgoingLinks.length} |`);
137
+ }
138
+ lines.push("");
139
+ if (errors.length > 0) {
140
+ lines.push("---");
141
+ lines.push("");
142
+ lines.push("## Errors");
143
+ lines.push("");
144
+ for (const page of errors) {
145
+ lines.push(`- \`${page.url}\`: ${page.error}`);
146
+ }
147
+ lines.push("");
148
+ }
149
+ lines.push("---");
150
+ lines.push("");
151
+ lines.push("## Screenshot Inventory");
152
+ lines.push("");
153
+ lines.push(`| Section | Count |`);
154
+ lines.push(`|---------|-------|`);
155
+ let total = 0;
156
+ for (const page of visited) {
157
+ lines.push(`| ${page.analysis?.pageName ?? page.slug} | 1 |`);
158
+ total++;
159
+ }
160
+ lines.push(`| **Total** | **${total}** |`);
161
+ lines.push("");
162
+ return lines.join(`
163
+ `);
164
+ }
165
+ function generateIndexHtml(state) {
166
+ const visited = Array.from(state.pages.values()).filter((p) => p.status === "visited");
167
+ const screensData = {};
168
+ for (const page of visited) {
169
+ const name = page.analysis?.pageName ?? page.title ?? page.slug;
170
+ screensData[page.slug] = [{
171
+ src: page.screenshot,
172
+ label: name,
173
+ desc: page.analysis?.description ?? `Page at ${page.path}`
174
+ }];
175
+ }
176
+ const sidebarItems = visited.map((page) => {
177
+ const name = page.analysis?.pageName ?? page.title ?? page.slug;
178
+ return `<div class="sidebar-item${visited[0] === page ? " active" : ""}" data-section="${page.slug}" onclick="showSection('${page.slug}')">
179
+ <span class="icon">\uD83D\uDCC4</span>${name}<span class="count">1</span>
180
+ </div>`;
181
+ }).join(`
182
+ `);
183
+ const sections = visited.map((page) => {
184
+ const name = page.analysis?.pageName ?? page.title ?? page.slug;
185
+ const isFirst = visited[0] === page;
186
+ return `<div id="sec-${page.slug}" class="flow-section${isFirst ? " active" : ""}">
187
+ <div class="flow-title">${name}</div>
188
+ <div class="flow-subtitle">${page.analysis?.description ?? ""}</div>
189
+ <div class="flow-route">${page.path}</div>
190
+ <div class="flow-row">
191
+ <div class="flow-card" onclick="openLightbox('${page.slug}', 0)">
192
+ <div class="card-thumb" style="background-image:url('${page.screenshot}')">
193
+ <span class="tag tag-page">Page</span>
194
+ </div>
195
+ <div class="card-body">
196
+ <div class="card-label">${name}</div>
197
+ <div class="card-desc">${page.analysis?.description ?? ""}</div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>`;
202
+ }).join(`
203
+ `);
204
+ return `<!DOCTYPE html>
205
+ <html lang="en">
206
+ <head>
207
+ <meta charset="UTF-8">
208
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
209
+ <title>Application Map</title>
210
+ <style>
211
+ :root {
212
+ --bg: #0f1117;
213
+ --surface: #1a1d27;
214
+ --surface2: #242837;
215
+ --border: #2e3348;
216
+ --text: #e1e4ed;
217
+ --text-dim: #8b90a5;
218
+ --accent: #6366f1;
219
+ --accent-glow: rgba(99,102,241,0.25);
220
+ --green: #22c55e;
221
+ --amber: #f59e0b;
222
+ --red: #ef4444;
223
+ --cyan: #06b6d4;
224
+ --pink: #ec4899;
225
+ }
226
+ * { margin: 0; padding: 0; box-sizing: border-box; }
227
+ body {
228
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
229
+ background: var(--bg); color: var(--text);
230
+ overflow: hidden; height: 100vh;
231
+ }
232
+ .topbar {
233
+ position: fixed; top: 0; left: 0; right: 0; z-index: 100;
234
+ height: 56px; background: var(--surface);
235
+ border-bottom: 1px solid var(--border);
236
+ display: flex; align-items: center; padding: 0 24px; gap: 16px;
237
+ }
238
+ .topbar h1 { font-size: 16px; font-weight: 600; }
239
+ .topbar .badge {
240
+ font-size: 11px; padding: 3px 10px; border-radius: 20px;
241
+ background: var(--accent); color: #fff; font-weight: 500;
242
+ }
243
+ .topbar .sep { flex: 1; }
244
+ .main { display: flex; height: 100vh; padding-top: 56px; }
245
+ .sidebar {
246
+ width: 260px; min-width: 260px;
247
+ background: var(--surface); border-right: 1px solid var(--border);
248
+ overflow-y: auto; padding: 16px 0;
249
+ }
250
+ .sidebar-item {
251
+ display: flex; align-items: center; gap: 10px;
252
+ padding: 8px 20px; border-radius: 8px; cursor: pointer;
253
+ font-size: 13px; color: var(--text-dim); margin: 0 8px 2px;
254
+ }
255
+ .sidebar-item:hover { background: var(--surface2); color: var(--text); }
256
+ .sidebar-item.active { background: var(--accent-glow); color: var(--accent); font-weight: 500; }
257
+ .sidebar-item .icon { font-size: 16px; width: 20px; text-align: center; }
258
+ .sidebar-item .count {
259
+ margin-left: auto; font-size: 11px; background: var(--surface2);
260
+ padding: 1px 7px; border-radius: 10px; color: var(--text-dim);
261
+ }
262
+ .canvas-area { flex: 1; overflow: hidden; }
263
+ .canvas-scroll { width: 100%; height: 100%; overflow: auto; padding: 40px; }
264
+ .flow-section { display: none; }
265
+ .flow-section.active { display: block; }
266
+ .flow-title { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
267
+ .flow-subtitle { font-size: 14px; color: var(--text-dim); margin-bottom: 8px; }
268
+ .flow-route {
269
+ display: inline-block; font-size: 12px; font-family: monospace;
270
+ background: var(--surface2); padding: 3px 10px; border-radius: 6px;
271
+ color: var(--text-dim); margin-bottom: 24px;
272
+ }
273
+ .flow-row { display: flex; gap: 16px; flex-wrap: wrap; }
274
+ .flow-card {
275
+ width: 320px; background: var(--surface); border: 1px solid var(--border);
276
+ border-radius: 12px; overflow: hidden; cursor: pointer; transition: border-color 0.15s;
277
+ }
278
+ .flow-card:hover { border-color: var(--accent); }
279
+ .card-thumb {
280
+ height: 180px; background-size: cover; background-position: top center;
281
+ position: relative;
282
+ }
283
+ .tag {
284
+ position: absolute; top: 8px; right: 8px;
285
+ font-size: 10px; padding: 2px 8px; border-radius: 4px;
286
+ font-weight: 600; text-transform: uppercase;
287
+ }
288
+ .tag-page { background: var(--accent); color: #fff; }
289
+ .tag-modal { background: var(--pink); color: #fff; }
290
+ .tag-action { background: var(--amber); color: #000; }
291
+ .card-body { padding: 12px 16px; }
292
+ .card-label { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
293
+ .card-desc { font-size: 12px; color: var(--text-dim); }
294
+ /* Lightbox */
295
+ .lightbox {
296
+ display: none; position: fixed; inset: 0; z-index: 200;
297
+ background: rgba(0,0,0,0.9); align-items: center; justify-content: center;
298
+ }
299
+ .lightbox.open { display: flex; }
300
+ .lightbox img { max-width: 90vw; max-height: 80vh; border-radius: 8px; }
301
+ .lb-caption {
302
+ position: fixed; bottom: 40px; left: 50%; transform: translateX(-50%);
303
+ text-align: center; color: var(--text);
304
+ }
305
+ .lb-counter { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
306
+ .lb-label { font-size: 16px; font-weight: 600; }
307
+ .lb-close {
308
+ position: fixed; top: 20px; right: 24px; font-size: 24px;
309
+ color: var(--text-dim); cursor: pointer; z-index: 201;
310
+ }
311
+ .lb-nav {
312
+ position: fixed; top: 50%; font-size: 32px; color: var(--text-dim);
313
+ cursor: pointer; padding: 20px; z-index: 201;
314
+ }
315
+ .lb-prev { left: 10px; }
316
+ .lb-next { right: 10px; }
317
+ </style>
318
+ </head>
319
+ <body>
320
+ <div class="topbar">
321
+ <h1>Application Map</h1>
322
+ <span class="badge">Zefiro</span>
323
+ <span class="sep"></span>
324
+ <span style="font-size:12px;color:var(--text-dim)">${visited.length} pages</span>
325
+ </div>
326
+ <div class="main">
327
+ <div class="sidebar">
328
+ ${sidebarItems}
329
+ </div>
330
+ <div class="canvas-area">
331
+ <div class="canvas-scroll">
332
+ ${sections}
333
+ </div>
334
+ </div>
335
+ </div>
336
+ <div class="lightbox" id="lightbox" onclick="if(event.target===this)closeLightbox()">
337
+ <span class="lb-close" onclick="closeLightbox()">✕</span>
338
+ <span class="lb-nav lb-prev" onclick="navLightbox(-1)">‹</span>
339
+ <img id="lb-img" src="" alt="">
340
+ <span class="lb-nav lb-next" onclick="navLightbox(1)">›</span>
341
+ <div class="lb-caption">
342
+ <div class="lb-counter" id="lb-counter"></div>
343
+ <div class="lb-label" id="lb-label"></div>
344
+ </div>
345
+ </div>
346
+ <script>
347
+ const screens = ${JSON.stringify(screensData, null, 2)};
348
+ let lbSection = null, lbIndex = 0;
349
+ function showSection(slug) {
350
+ document.querySelectorAll('.flow-section').forEach(s => s.classList.remove('active'));
351
+ document.querySelectorAll('.sidebar-item').forEach(s => s.classList.remove('active'));
352
+ const sec = document.getElementById('sec-' + slug);
353
+ if (sec) sec.classList.add('active');
354
+ const item = document.querySelector('[data-section="' + slug + '"]');
355
+ if (item) item.classList.add('active');
356
+ }
357
+ function openLightbox(section, index) {
358
+ lbSection = section; lbIndex = index;
359
+ const items = screens[section] || [];
360
+ if (!items[index]) return;
361
+ document.getElementById('lb-img').src = items[index].src;
362
+ document.getElementById('lb-label').textContent = items[index].label;
363
+ document.getElementById('lb-counter').textContent = (index + 1) + ' / ' + items.length;
364
+ document.getElementById('lightbox').classList.add('open');
365
+ }
366
+ function closeLightbox() {
367
+ document.getElementById('lightbox').classList.remove('open');
368
+ }
369
+ function navLightbox(dir) {
370
+ const items = screens[lbSection] || [];
371
+ lbIndex = (lbIndex + dir + items.length) % items.length;
372
+ openLightbox(lbSection, lbIndex);
373
+ }
374
+ document.addEventListener('keydown', e => {
375
+ if (e.key === 'Escape') closeLightbox();
376
+ if (e.key === 'ArrowLeft') navLightbox(-1);
377
+ if (e.key === 'ArrowRight') navLightbox(1);
378
+ });
379
+ </script>
380
+ </body>
381
+ </html>`;
382
+ }
383
+
384
+ export { generateSectionMarkdown, generateReadme, generateIndexHtml };
@@ -0,0 +1,133 @@
1
+ import {
2
+ AgentBrowser,
3
+ authenticate,
4
+ error,
5
+ info,
6
+ success,
7
+ verbose
8
+ } from "./cli-b26q1e27.js";
9
+ import {
10
+ buildTopology,
11
+ extractHrefsFromElements,
12
+ generateSectionMarkdown,
13
+ normalizeUrl,
14
+ shouldVisit,
15
+ urlToSlug
16
+ } from "./cli-6qr9gvkp.js";
17
+ import {
18
+ ensureDir,
19
+ writeFile
20
+ } from "./cli-zvk8gwe4.js";
21
+
22
+ // src/browser/explorer.ts
23
+ import { join } from "node:path";
24
+ async function runExploration(config) {
25
+ const state = {
26
+ baseUrl: config.baseUrl,
27
+ startedAt: new Date().toISOString(),
28
+ pages: new Map,
29
+ queue: [],
30
+ topology: []
31
+ };
32
+ const browser = new AgentBrowser({
33
+ session: config.sessionName,
34
+ headed: config.headed || config.auth.method === "manual"
35
+ });
36
+ const visited = new Set;
37
+ const screenshotsDir = join(config.outputDir, "screenshots");
38
+ const sectionsDir = join(config.outputDir, "sections");
39
+ ensureDir(screenshotsDir);
40
+ ensureDir(sectionsDir);
41
+ try {
42
+ await authenticate(browser, config);
43
+ await browser.setViewport(1440, 900);
44
+ const startUrl = normalizeUrl(config.baseUrl, config.baseUrl);
45
+ state.queue.push(startUrl);
46
+ let pageCount = 0;
47
+ while (state.queue.length > 0 && pageCount < config.maxPages) {
48
+ const url = state.queue.shift();
49
+ const normalized = normalizeUrl(url, config.baseUrl);
50
+ if (visited.has(normalized))
51
+ continue;
52
+ if (!shouldVisit(url, config.baseUrl, config.excludePatterns, visited)) {
53
+ continue;
54
+ }
55
+ visited.add(normalized);
56
+ const slug = urlToSlug(url, config.baseUrl);
57
+ info(`[${pageCount + 1}/${config.maxPages}] Exploring: ${url}`);
58
+ const page = {
59
+ url: normalized,
60
+ path: new URL(normalized).pathname,
61
+ title: "",
62
+ slug,
63
+ depth: 0,
64
+ screenshot: "",
65
+ elements: [],
66
+ outgoingLinks: [],
67
+ status: "pending"
68
+ };
69
+ try {
70
+ await browser.open(url);
71
+ await browser.waitForLoad(config.waitStrategy, config.waitDelay);
72
+ const actualUrl = await browser.getUrl();
73
+ const actualNormalized = normalizeUrl(actualUrl, config.baseUrl);
74
+ if (actualNormalized !== normalized) {
75
+ page.url = actualNormalized;
76
+ page.path = new URL(actualNormalized).pathname;
77
+ page.slug = urlToSlug(actualUrl, config.baseUrl);
78
+ if (visited.has(actualNormalized)) {
79
+ verbose(` Redirected to already-visited ${actualUrl}, skipping`);
80
+ continue;
81
+ }
82
+ visited.add(actualNormalized);
83
+ }
84
+ page.title = await browser.getTitle();
85
+ const slugDir = join(screenshotsDir, page.slug);
86
+ ensureDir(slugDir);
87
+ const screenshotPath = join(slugDir, "01-default.png");
88
+ await browser.screenshot(screenshotPath);
89
+ page.screenshot = `screenshots/${page.slug}/01-default.png`;
90
+ page.elements = await browser.snapshot();
91
+ const linkRefs = extractHrefsFromElements(page.elements);
92
+ for (const ref of linkRefs) {
93
+ try {
94
+ const href = await browser.getAttribute(ref, "href");
95
+ if (href) {
96
+ const linkUrl = normalizeUrl(href, actualUrl);
97
+ page.outgoingLinks.push(linkUrl);
98
+ if (shouldVisit(linkUrl, config.baseUrl, config.excludePatterns, visited)) {
99
+ state.queue.push(linkUrl);
100
+ }
101
+ }
102
+ } catch {}
103
+ }
104
+ page.depth = calculateDepth(page.path);
105
+ const relatedPages = Array.from(state.pages.values()).filter((p) => p.status === "visited").map((p) => ({ slug: p.slug, name: p.analysis?.pageName ?? p.title, path: p.path }));
106
+ const markdown = generateSectionMarkdown(page, relatedPages);
107
+ writeFile(join(sectionsDir, `${page.slug}.md`), markdown);
108
+ page.status = "visited";
109
+ page.visitedAt = new Date().toISOString();
110
+ pageCount++;
111
+ success(` ${page.title || page.slug} — ${page.elements.length} elements, ${page.outgoingLinks.length} links`);
112
+ } catch (err) {
113
+ page.status = "error";
114
+ page.error = err.message;
115
+ error(` Error: ${err.message}`);
116
+ }
117
+ state.pages.set(page.url, page);
118
+ }
119
+ state.topology = buildTopology(state.pages);
120
+ state.completedAt = new Date().toISOString();
121
+ await browser.close();
122
+ } catch (err) {
123
+ error(`Exploration failed: ${err.message}`);
124
+ await browser.close();
125
+ throw err;
126
+ }
127
+ return state;
128
+ }
129
+ function calculateDepth(path) {
130
+ const segments = path.replace(/^\/|\/$/g, "").split("/").filter(Boolean);
131
+ return segments.length;
132
+ }
133
+ export { runExploration };
@@ -0,0 +1,110 @@
1
+ import {
2
+ exports_external
3
+ } from "./cli-z2krvkcq.js";
4
+
5
+ // src/config/schema.ts
6
+ function nested(shape) {
7
+ const inner = exports_external.object(shape);
8
+ return exports_external.preprocess((val) => val ?? {}, inner);
9
+ }
10
+ var PathsSchema = exports_external.object({
11
+ workingDir: exports_external.string().default(".qai/zefiro"),
12
+ outputDir: exports_external.string().default("app-report")
13
+ });
14
+ var BrowserSchema = exports_external.object({
15
+ sessionName: exports_external.string().default("zefiro"),
16
+ headed: exports_external.boolean().default(false),
17
+ viewport: exports_external.object({
18
+ width: exports_external.number().default(1440),
19
+ height: exports_external.number().default(900)
20
+ }).default({ width: 1440, height: 900 }),
21
+ waitStrategy: exports_external.enum(["networkidle", "load", "domcontentloaded"]).default("networkidle"),
22
+ waitDelay: exports_external.number().default(1500),
23
+ maxPages: exports_external.number().default(100),
24
+ pageTimeout: exports_external.number().default(30000),
25
+ excludePatterns: exports_external.array(exports_external.string()).default([])
26
+ });
27
+ var AuthSchema = exports_external.object({
28
+ method: exports_external.enum(["state-file", "manual"]).default("manual"),
29
+ stateFile: exports_external.string().nullable().default(null)
30
+ });
31
+ var QaiConfigSchema = exports_external.object({
32
+ baseUrl: exports_external.string().nullable().default(null),
33
+ paths: nested(PathsSchema.shape),
34
+ browser: nested(BrowserSchema.shape),
35
+ auth: nested(AuthSchema.shape),
36
+ contextFile: exports_external.string().default(".qai/zefiro/context.md")
37
+ });
38
+ function defineConfig(config) {
39
+ return config;
40
+ }
41
+
42
+ // src/config/loader.ts
43
+ import { existsSync } from "node:fs";
44
+ import { dirname, join, resolve } from "node:path";
45
+ import { pathToFileURL } from "node:url";
46
+ var CONFIG_DIR = ".qai/zefiro";
47
+ var CONFIG_FILENAMES = ["config.ts", "config.js", "config.mjs"];
48
+ var cachedConfig = null;
49
+ var cachedProjectRoot = null;
50
+ function findConfigDir(startDir) {
51
+ let dir = resolve(startDir);
52
+ const root = dirname(dir) === dir ? dir : undefined;
53
+ while (true) {
54
+ const candidate = join(dir, CONFIG_DIR);
55
+ if (existsSync(join(candidate, "agents")) || existsSync(join(candidate, "context.md"))) {
56
+ return dir;
57
+ }
58
+ if (existsSync(candidate)) {
59
+ return dir;
60
+ }
61
+ for (const name of CONFIG_FILENAMES) {
62
+ if (existsSync(join(candidate, name))) {
63
+ return dir;
64
+ }
65
+ }
66
+ const parent = dirname(dir);
67
+ if (parent === dir || dir === root)
68
+ return null;
69
+ dir = parent;
70
+ }
71
+ }
72
+ function getProjectRoot() {
73
+ if (cachedProjectRoot)
74
+ return cachedProjectRoot;
75
+ const found = findConfigDir(process.cwd());
76
+ cachedProjectRoot = found ?? process.cwd();
77
+ return cachedProjectRoot;
78
+ }
79
+ function getPackageRoot() {
80
+ let dir = import.meta.dirname;
81
+ while (!existsSync(join(dir, "package.json"))) {
82
+ const parent = dirname(dir);
83
+ if (parent === dir)
84
+ return dir;
85
+ dir = parent;
86
+ }
87
+ return dir;
88
+ }
89
+ async function loadConfig() {
90
+ if (cachedConfig)
91
+ return cachedConfig;
92
+ const projectRoot = getProjectRoot();
93
+ let userConfig = {};
94
+ const dir = join(projectRoot, CONFIG_DIR);
95
+ for (const name of CONFIG_FILENAMES) {
96
+ const configPath = join(dir, name);
97
+ if (existsSync(configPath)) {
98
+ try {
99
+ const fileUrl = pathToFileURL(configPath).href;
100
+ const mod = await import(fileUrl);
101
+ userConfig = mod.default ?? mod;
102
+ break;
103
+ } catch {}
104
+ }
105
+ }
106
+ cachedConfig = QaiConfigSchema.parse(userConfig);
107
+ return cachedConfig;
108
+ }
109
+
110
+ export { defineConfig, CONFIG_DIR, getProjectRoot, getPackageRoot, loadConfig };