@qabyai/qli 1.0.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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,494 @@
1
+ #!/usr/bin/env node
2
+ import yargs from "yargs";
3
+ import { hideBin } from "yargs/helpers";
4
+ import { chromium } from "playwright";
5
+ import { chmodSync, createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "fs";
6
+ import { spawn } from "child_process";
7
+ import { pipeline } from "stream/promises";
8
+ import { basename, dirname, extname, join, resolve } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import https from "https";
11
+ import chalk from "chalk";
12
+ import { createTRPCClient, httpBatchLink } from "@trpc/client";
13
+ import superjson from "superjson";
14
+
15
+ //#region rolldown:runtime
16
+ var __defProp = Object.defineProperty;
17
+ var __export = (all) => {
18
+ let target = {};
19
+ for (var name in all) __defProp(target, name, {
20
+ get: all[name],
21
+ enumerable: true
22
+ });
23
+ return target;
24
+ };
25
+
26
+ //#endregion
27
+ //#region src/utils/ngrok-binary.ts
28
+ const NGROK_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "bin");
29
+ const NGROK_BINARY = join(NGROK_DIR, process.platform === "win32" ? "ngrok.exe" : "ngrok");
30
+ function getPlatformInfo() {
31
+ const platform = process.platform;
32
+ const arch = process.arch;
33
+ if (platform === "darwin") if (arch === "arm64") return {
34
+ downloadUrl: "https://bin.ngrok.com/c/bNyj1mQVY4c/ngrok-v3-stable-darwin-arm64.zip",
35
+ isArchive: true,
36
+ archiveType: "zip"
37
+ };
38
+ else return {
39
+ downloadUrl: "https://bin.ngrok.com/c/bNyj1mQVY4c/ngrok-v3-stable-darwin-amd64.zip",
40
+ isArchive: true,
41
+ archiveType: "zip"
42
+ };
43
+ else if (platform === "linux") return {
44
+ downloadUrl: "https://bin.ngrok.com/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz",
45
+ isArchive: true,
46
+ archiveType: "tgz"
47
+ };
48
+ else if (platform === "win32") return {
49
+ downloadUrl: "https://bin.ngrok.com/c/bNyj1mQVY4c/ngrok-v3-stable-windows-amd64.zip",
50
+ isArchive: true,
51
+ archiveType: "zip"
52
+ };
53
+ else throw new Error(`Unsupported platform: ${platform}`);
54
+ }
55
+ async function downloadFile(url, filePath) {
56
+ return new Promise((resolve$1, reject) => {
57
+ https.get(url, (response) => {
58
+ if (response.statusCode !== 200) {
59
+ reject(/* @__PURE__ */ new Error(`Failed to download: ${response.statusCode}`));
60
+ return;
61
+ }
62
+ pipeline(response, createWriteStream(filePath)).then(() => resolve$1()).catch(reject);
63
+ }).on("error", reject);
64
+ });
65
+ }
66
+ async function extractArchive(archivePath, extractDir, type) {
67
+ return new Promise((resolve$1, reject) => {
68
+ let command$2;
69
+ let args;
70
+ if (type === "zip") {
71
+ command$2 = "unzip";
72
+ args = [
73
+ "-o",
74
+ archivePath,
75
+ "-d",
76
+ extractDir
77
+ ];
78
+ } else {
79
+ command$2 = "tar";
80
+ args = [
81
+ "-xzf",
82
+ archivePath,
83
+ "-C",
84
+ extractDir
85
+ ];
86
+ }
87
+ const process$1 = spawn(command$2, args);
88
+ process$1.on("close", (code) => {
89
+ if (code === 0) resolve$1();
90
+ else reject(/* @__PURE__ */ new Error(`Extraction failed with code ${code}`));
91
+ });
92
+ process$1.on("error", reject);
93
+ });
94
+ }
95
+ async function ensureNgrokBinary() {
96
+ if (existsSync(NGROK_BINARY)) return NGROK_BINARY;
97
+ if (!existsSync(NGROK_DIR)) mkdirSync(NGROK_DIR, { recursive: true });
98
+ const platformInfo = getPlatformInfo();
99
+ const tempArchivePath = join(NGROK_DIR, `ngrok.${platformInfo.archiveType}`);
100
+ try {
101
+ console.log("📦 Downloading ngrok binary...");
102
+ await downloadFile(platformInfo.downloadUrl, tempArchivePath);
103
+ console.log("📂 Extracting ngrok binary...");
104
+ await extractArchive(tempArchivePath, NGROK_DIR, platformInfo.archiveType);
105
+ if (existsSync(NGROK_BINARY)) {
106
+ chmodSync(NGROK_BINARY, 493);
107
+ rmSync(tempArchivePath);
108
+ return NGROK_BINARY;
109
+ } else throw new Error(`Binary not found after extraction. Expected: ${NGROK_BINARY}`);
110
+ } catch (error) {
111
+ if (existsSync(tempArchivePath)) rmSync(tempArchivePath);
112
+ throw error;
113
+ }
114
+ }
115
+ async function startNgrokTunnel(port, authtoken) {
116
+ return new Promise((resolve$1, reject) => {
117
+ const initTunnel = async () => {
118
+ try {
119
+ const ngrokProcess = spawn(await ensureNgrokBinary(), [
120
+ "http",
121
+ port.toString(),
122
+ "--authtoken",
123
+ authtoken,
124
+ "--log=stdout",
125
+ "--log-format=json"
126
+ ], { stdio: [
127
+ "pipe",
128
+ "pipe",
129
+ "pipe"
130
+ ] });
131
+ let output = "";
132
+ let errorOutput = "";
133
+ let tunnelUrl = "";
134
+ let resolved = false;
135
+ ngrokProcess.stdout?.on("data", (data) => {
136
+ const chunk = data.toString();
137
+ output += chunk;
138
+ const urlMatch = chunk.match(/"url":"(https:\/\/[^"]+)"/);
139
+ if (urlMatch && !tunnelUrl && !resolved) {
140
+ tunnelUrl = urlMatch[1];
141
+ resolved = true;
142
+ resolve$1({
143
+ url: tunnelUrl,
144
+ process: ngrokProcess
145
+ });
146
+ }
147
+ });
148
+ ngrokProcess.stderr?.on("data", (data) => {
149
+ const chunk = data.toString();
150
+ errorOutput += chunk;
151
+ if (chunk.includes("ERRO") || chunk.includes("failed to")) {
152
+ if (!resolved) {
153
+ resolved = true;
154
+ reject(/* @__PURE__ */ new Error(`Ngrok error: ${chunk}`));
155
+ }
156
+ }
157
+ });
158
+ ngrokProcess.on("error", (error) => {
159
+ if (!resolved) {
160
+ resolved = true;
161
+ reject(/* @__PURE__ */ new Error(`Failed to start ngrok: ${error.message}`));
162
+ }
163
+ });
164
+ ngrokProcess.on("exit", (code) => {
165
+ if (code !== 0 && !resolved) {
166
+ resolved = true;
167
+ reject(/* @__PURE__ */ new Error(`Ngrok exited with code ${code}`));
168
+ }
169
+ });
170
+ setTimeout(() => {
171
+ if (!resolved) {
172
+ ngrokProcess.kill();
173
+ resolved = true;
174
+ reject(/* @__PURE__ */ new Error("Timeout waiting for ngrok tunnel URL"));
175
+ }
176
+ }, 2e4);
177
+ } catch (error) {
178
+ reject(error);
179
+ }
180
+ };
181
+ initTunnel();
182
+ });
183
+ }
184
+
185
+ //#endregion
186
+ //#region src/commands/browser.ts
187
+ var browser_exports = /* @__PURE__ */ __export({
188
+ builder: () => builder$1,
189
+ command: () => command$1,
190
+ desc: () => desc$1,
191
+ handler: () => handler$1
192
+ });
193
+ const command$1 = "browser";
194
+ const desc$1 = "Start Playwright browser server with ngrok tunnel";
195
+ const builder$1 = (yargs$1) => yargs$1.option("port", {
196
+ type: "number",
197
+ default: 9222,
198
+ describe: "Port to run the browser server on"
199
+ }).option("headless", {
200
+ type: "boolean",
201
+ default: false,
202
+ describe: "Run browser in headless mode"
203
+ });
204
+ const handler$1 = async (argv) => {
205
+ const { port = 9222, headless = false } = argv;
206
+ try {
207
+ console.log(chalk.gray("[debug] Starting Playwright browser server..."));
208
+ const browserServer = await chromium.launchServer({
209
+ headless,
210
+ port,
211
+ args: [
212
+ "--no-sandbox",
213
+ "--disable-setuid-sandbox",
214
+ "--disable-web-security",
215
+ "--disable-features=VizDisplayCompositor"
216
+ ]
217
+ });
218
+ const wsEndpoint = browserServer.wsEndpoint();
219
+ console.log(chalk.gray("[debug] Browser server started successfully!"));
220
+ console.log(chalk.gray(`[debug] WebSocket endpoint: ${wsEndpoint}`));
221
+ const sessionId = wsEndpoint.split("/").pop() || "";
222
+ let tunnelUrl;
223
+ let ngrokProcess;
224
+ try {
225
+ console.log(chalk.gray("[debug] Creating secure tunnel..."));
226
+ const result = await startNgrokTunnel(port, "38bZ22nMOQFcUhqpTi3SFIphrjX_68vsN6T945JmNVaNGYjHs");
227
+ tunnelUrl = result.url;
228
+ ngrokProcess = result.process;
229
+ console.log(chalk.gray("[debug] Tunnel established!"));
230
+ const completeWsUrl = `${tunnelUrl.replace("https://", "wss://")}/${sessionId}`;
231
+ console.log(chalk.green.bold(`\n${completeWsUrl}\n`));
232
+ console.log(chalk.cyan("💡 Copy the URL above and paste it in your browser"));
233
+ } catch (error) {
234
+ console.error(chalk.red("[debug] Failed to create tunnel:"));
235
+ console.error(chalk.red(`[debug] ${error.message || error}`));
236
+ console.log(chalk.gray("[debug] → Browser server will continue running locally only"));
237
+ console.log(chalk.gray("[debug] → Check your internet connection or try again later"));
238
+ console.log(chalk.green.bold(`\nws://localhost:${port}/${sessionId}\n`));
239
+ console.log(chalk.cyan("💡 Copy the URL above and paste it in your browser (local only)"));
240
+ }
241
+ console.log(chalk.dim("\nPress Ctrl+C to stop"));
242
+ const shutdown = async () => {
243
+ console.log(chalk.gray("\n[debug] Shutting down..."));
244
+ if (ngrokProcess) try {
245
+ ngrokProcess.kill("SIGTERM");
246
+ console.log(chalk.gray("[debug] ✅ Tunnel closed"));
247
+ } catch (error) {
248
+ console.log(chalk.gray(`[debug] ⚠️ Error closing tunnel: ${error instanceof Error ? error.message : "Unknown error"}`));
249
+ }
250
+ try {
251
+ await browserServer.close();
252
+ console.log(chalk.gray("[debug] ✅ Browser server closed"));
253
+ } catch (error) {
254
+ console.log(chalk.gray(`[debug] ❌ Error closing browser server: ${error instanceof Error ? error.message : "Unknown error"}`));
255
+ }
256
+ console.log(chalk.gray("\nGoodbye! 👋"));
257
+ process.exit(0);
258
+ };
259
+ process.on("SIGINT", shutdown);
260
+ process.on("SIGTERM", shutdown);
261
+ } catch (error) {
262
+ console.error(chalk.red("❌ Failed to start browser server:"), error);
263
+ process.exit(1);
264
+ }
265
+ };
266
+
267
+ //#endregion
268
+ //#region src/api/client.ts
269
+ function createClient(apiKey, apiUrl) {
270
+ return createTRPCClient({ links: [httpBatchLink({
271
+ url: `${apiUrl}/trpc`,
272
+ headers() {
273
+ return { "x-api-key": apiKey };
274
+ },
275
+ transformer: superjson
276
+ })] });
277
+ }
278
+
279
+ //#endregion
280
+ //#region src/commands/context.ts
281
+ var context_exports = /* @__PURE__ */ __export({
282
+ builder: () => builder,
283
+ command: () => command,
284
+ desc: () => desc,
285
+ handler: () => handler
286
+ });
287
+ const command = "context";
288
+ const desc = "Scan and upload codebase to generate context documents";
289
+ const builder = (yargs$1) => yargs$1.option("api-key", {
290
+ type: "string",
291
+ describe: "API key for authentication (or set QABYAI_API_KEY env var)"
292
+ }).option("dir", {
293
+ type: "string",
294
+ describe: "Directory to scan (defaults to current directory)"
295
+ });
296
+ const INCLUDE_DIR_PATTERNS = [
297
+ "src/",
298
+ "app/",
299
+ "pages/",
300
+ "lib/",
301
+ "routes/",
302
+ "components/",
303
+ "models/",
304
+ "api/"
305
+ ];
306
+ const INCLUDE_FILE_EXACT = [
307
+ "package.json",
308
+ "tsconfig.json",
309
+ "README.md",
310
+ ".env.example"
311
+ ];
312
+ const INCLUDE_FILE_SUFFIXES = [
313
+ ".config.ts",
314
+ ".config.js",
315
+ ".config.mjs"
316
+ ];
317
+ const EXCLUDE_DIRS = new Set([
318
+ "node_modules",
319
+ "dist",
320
+ "build",
321
+ ".next",
322
+ ".git",
323
+ "__snapshots__",
324
+ ".turbo",
325
+ ".cache",
326
+ "coverage"
327
+ ]);
328
+ const EXCLUDE_EXTENSIONS = new Set([
329
+ ".lock",
330
+ ".png",
331
+ ".jpg",
332
+ ".jpeg",
333
+ ".gif",
334
+ ".ico",
335
+ ".svg",
336
+ ".woff",
337
+ ".woff2",
338
+ ".ttf",
339
+ ".eot",
340
+ ".mp4",
341
+ ".mp3",
342
+ ".webm",
343
+ ".webp",
344
+ ".avif",
345
+ ".snap",
346
+ ".map"
347
+ ]);
348
+ const EXCLUDE_FILES = new Set([
349
+ "pnpm-lock.yaml",
350
+ "package-lock.json",
351
+ "yarn.lock"
352
+ ]);
353
+ const MAX_FILE_SIZE = 50 * 1024;
354
+ const MAX_TOTAL_SIZE = 5 * 1024 * 1024;
355
+ function shouldIncludeFile(relativePath, fileSize) {
356
+ const name = basename(relativePath);
357
+ const ext = extname(relativePath).toLowerCase();
358
+ if (EXCLUDE_FILES.has(name)) return false;
359
+ if (EXCLUDE_EXTENSIONS.has(ext)) return false;
360
+ for (const dir of EXCLUDE_DIRS) if (relativePath.includes(dir + "/")) return false;
361
+ if (fileSize > MAX_FILE_SIZE) return false;
362
+ for (const dir of INCLUDE_DIR_PATTERNS) if (relativePath.startsWith(dir) || relativePath.includes(`/${dir}`)) return true;
363
+ if (INCLUDE_FILE_EXACT.includes(name)) return true;
364
+ for (const suffix of INCLUDE_FILE_SUFFIXES) if (name.endsWith(suffix)) return true;
365
+ return false;
366
+ }
367
+ function scanDirectory(rootDir) {
368
+ const files = [];
369
+ let totalSize = 0;
370
+ function walk(dir, relativeBase) {
371
+ let entries;
372
+ try {
373
+ entries = readdirSync(dir, { withFileTypes: true });
374
+ } catch {
375
+ return;
376
+ }
377
+ for (const entry of entries) if (entry.isDirectory()) {
378
+ if (EXCLUDE_DIRS.has(entry.name)) continue;
379
+ walk(join(dir, entry.name), relativeBase ? `${relativeBase}/${entry.name}` : entry.name);
380
+ } else if (entry.isFile()) {
381
+ const fullPath = join(dir, entry.name);
382
+ const relativePath = relativeBase ? `${relativeBase}/${entry.name}` : entry.name;
383
+ let stat;
384
+ try {
385
+ stat = statSync(fullPath);
386
+ } catch {
387
+ continue;
388
+ }
389
+ if (totalSize + stat.size > MAX_TOTAL_SIZE) continue;
390
+ if (shouldIncludeFile(relativePath, stat.size)) try {
391
+ const content = readFileSync(fullPath, "utf-8");
392
+ files.push({
393
+ path: relativePath,
394
+ content
395
+ });
396
+ totalSize += stat.size;
397
+ } catch {}
398
+ }
399
+ }
400
+ walk(rootDir, "");
401
+ return files;
402
+ }
403
+ function sleep(ms) {
404
+ return new Promise((resolve$1) => setTimeout(resolve$1, ms));
405
+ }
406
+ const handler = async (argv) => {
407
+ const apiKey = argv["api-key"] || process.env.QABYAI_API_KEY;
408
+ if (!apiKey) {
409
+ console.error(chalk.red("API key required. Use --api-key flag or set QABYAI_API_KEY env var."));
410
+ console.error(chalk.gray("Get your API key at https://qaby.ai/settings/api-keys"));
411
+ process.exit(1);
412
+ }
413
+ const dir = resolve(argv.dir || process.cwd());
414
+ const apiUrl = process.env.QABYAI_API_URL || "https://qaby.ai";
415
+ const dirName = basename(dir);
416
+ const client = createClient(apiKey, apiUrl);
417
+ console.log(chalk.gray(`Scanning ${dir}...\n`));
418
+ const files = scanDirectory(dir);
419
+ if (files.length === 0) {
420
+ console.error(chalk.red("No matching files found. Make sure you are in a project directory."));
421
+ process.exit(1);
422
+ }
423
+ let jobId;
424
+ try {
425
+ jobId = (await client.context.cliScan.mutate({
426
+ dirName,
427
+ files
428
+ })).jobId;
429
+ } catch (err) {
430
+ const message = err instanceof Error ? err.message : "Unknown error";
431
+ console.error(chalk.red(`Failed: ${message}`));
432
+ process.exit(1);
433
+ }
434
+ console.log(chalk.white.bold("Generating knowledge base...\n"));
435
+ const spinnerFrames = [
436
+ "⠋",
437
+ "⠙",
438
+ "⠹",
439
+ "⠸",
440
+ "⠼",
441
+ "⠴",
442
+ "⠦",
443
+ "⠧",
444
+ "⠇",
445
+ "⠏"
446
+ ];
447
+ let frame = 0;
448
+ let lastStatus = "";
449
+ while (true) {
450
+ await sleep(3e3);
451
+ try {
452
+ const status = await client.context.cliScanStatus.query({ jobId });
453
+ if (status.status !== lastStatus) {
454
+ if (lastStatus) process.stdout.write("\r\x1B[K");
455
+ if (status.status === "pending") console.log(chalk.gray(` Queued for processing`));
456
+ else if (status.status === "running") console.log(chalk.gray(` Analyzing codebase and generating docs`));
457
+ lastStatus = status.status;
458
+ }
459
+ if (status.status === "completed") {
460
+ process.stdout.write("\r\x1B[K");
461
+ const docCount = status.generatedDocuments ?? 0;
462
+ console.log(chalk.green(` Created ${docCount} document${docCount !== 1 ? "s" : ""}`));
463
+ console.log(chalk.green("\nDone!"));
464
+ console.log(chalk.cyan("View documents at: https://qaby.ai/context/documents"));
465
+ break;
466
+ }
467
+ if (status.status === "failed") {
468
+ process.stdout.write("\r\x1B[K");
469
+ console.error(chalk.red("\nKnowledge generation failed."));
470
+ if (status.error) console.error(chalk.red(status.error));
471
+ process.exit(1);
472
+ }
473
+ process.stdout.write(`\r ${chalk.blue(spinnerFrames[frame++ % spinnerFrames.length])} ${chalk.gray("Processing...")}`);
474
+ } catch {
475
+ process.stdout.write(`\r ${chalk.blue(spinnerFrames[frame++ % spinnerFrames.length])} ${chalk.gray("Processing...")}`);
476
+ }
477
+ }
478
+ };
479
+
480
+ //#endregion
481
+ //#region src/index.ts
482
+ const __dirname = dirname(fileURLToPath(import.meta.url));
483
+ const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
484
+ async function main() {
485
+ console.log(chalk.blue.bold(`🤖 QAbyAI CLI v${packageJson.version}\n`));
486
+ yargs(hideBin(process.argv)).scriptName("qli").usage("$0 <command> [options]").command(browser_exports).command(context_exports).demandCommand(1, "You need to specify a command").strict().help().alias("help", "h").version(packageJson.version).alias("version", "v").completion().parse();
487
+ }
488
+ main().catch((error) => {
489
+ console.error(chalk.red("❌ Fatal error:"), error);
490
+ process.exit(1);
491
+ });
492
+
493
+ //#endregion
494
+ export { };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@qabyai/qli",
3
+ "version": "1.0.0",
4
+ "description": "QAbyAI CLI tool for browser automation",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "engines": {
9
+ "node": ">=14.0.0"
10
+ },
11
+ "bin": {
12
+ "qli": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "type": "module",
18
+ "dependencies": {
19
+ "@trpc/client": "^11.6.0",
20
+ "chalk": "^5.3.0",
21
+ "playwright": "^1.58.0",
22
+ "superjson": "^2.2.6",
23
+ "yargs": "^17.7.2"
24
+ },
25
+ "devDependencies": {
26
+ "@miloas/tsdown": "^0.13.0",
27
+ "@types/node": "^20.11.20",
28
+ "@types/yargs": "^17.0.32",
29
+ "typescript": "^5.9.3",
30
+ "backend": "1.0.0"
31
+ },
32
+ "scripts": {
33
+ "check": "tsgo --noEmit",
34
+ "build": "tsdown",
35
+ "serve:build": "tsdown",
36
+ "serve:watch": "tsdown --watch",
37
+ "dev": "node dist/index.js",
38
+ "cli": "node dist/index.js"
39
+ }
40
+ }