openbuilder 0.1.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,290 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * openbuilder — CLI entry point
5
+ *
6
+ * Open-source AI meeting assistant. Join Google Meet meetings,
7
+ * capture transcripts, and generate AI-powered meeting reports.
8
+ */
9
+
10
+ import { spawnSync } from "node:child_process";
11
+ import { createRequire } from "node:module";
12
+ import { cpSync, existsSync, mkdirSync } from "node:fs";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const pkgRoot = resolve(__dirname, "..");
19
+ const skillTargetDirDefault = join(homedir(), ".openclaw", "skills", "openbuilder");
20
+ const require = createRequire(import.meta.url);
21
+ const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
22
+
23
+ function printHelp() {
24
+ console.log(`OpenBuilder — AI Meeting Assistant
25
+
26
+ Usage:
27
+ npx openbuilder Install skill + Chromium
28
+ npx openbuilder install Install skill + Chromium
29
+ npx openbuilder join <meet-url> [options] Join a Google Meet
30
+ npx openbuilder auth Save Google session
31
+ npx openbuilder transcript [--last N] Print latest transcript
32
+ npx openbuilder screenshot Request on-demand screenshot
33
+ npx openbuilder summarize [transcript-path] AI summary of a transcript
34
+ npx openbuilder report [transcript-path] Full AI meeting report
35
+ npx openbuilder config [set|get|delete] [...] Manage configuration
36
+ npx openbuilder help Show this help
37
+
38
+ Join options:
39
+ --auth Join using saved Google account (~/.openbuilder/auth.json)
40
+ --anon Join as a guest (requires --bot-name)
41
+ --bot-name Guest display name (required with --anon)
42
+ --duration Auto-leave after duration (e.g. 30m, 1h)
43
+ --audio Force audio capture mode (PulseAudio + Whisper)
44
+ --captions Force caption scraping mode (DOM-based fallback)
45
+ --headed Show browser window for debugging
46
+ --camera Join with camera on (default: off)
47
+ --mic Join with microphone on (default: off)
48
+ --no-report Skip auto-report generation after meeting ends
49
+ --verbose Show real-time transcript output
50
+ --channel OpenClaw channel for sending status messages
51
+ --target OpenClaw target for sending status messages
52
+
53
+ By default, capture mode is "auto": uses audio capture if PulseAudio,
54
+ ffmpeg, and OPENAI_API_KEY are available, otherwise falls back to captions.
55
+
56
+ Config:
57
+ openbuilder config Show all settings
58
+ openbuilder config set <key> <value> Set a value
59
+ openbuilder config get <key> Get a value
60
+ openbuilder config delete <key> Remove a value
61
+
62
+ Keys: aiProvider, anthropicApiKey, openaiApiKey, botName, defaultDuration,
63
+ captureMode, whisperModel
64
+ Env: OPENBUILDER_AI_PROVIDER, ANTHROPIC_API_KEY, OPENAI_API_KEY,
65
+ OPENBUILDER_BOT_NAME, OPENBUILDER_DEFAULT_DURATION,
66
+ OPENBUILDER_CAPTURE_MODE, OPENBUILDER_WHISPER_MODEL
67
+
68
+ Examples:
69
+ npx openbuilder join https://meet.google.com/abc-defg-hij --anon --bot-name "Meeting Bot"
70
+ npx openbuilder join https://meet.google.com/abc-defg-hij --auth --duration 60m
71
+ npx openbuilder join https://meet.google.com/abc-defg-hij --auth --audio
72
+ npx openbuilder summarize ~/transcript.txt
73
+ npx openbuilder report ~/transcript.txt
74
+ npx openbuilder config set openaiApiKey sk-...
75
+ npx openbuilder config set captureMode audio`);
76
+ }
77
+
78
+ function resolveInstallTarget(rawArgs) {
79
+ const idx = rawArgs.indexOf("--target-dir");
80
+ if (idx >= 0) {
81
+ const value = rawArgs[idx + 1];
82
+ if (!value) {
83
+ console.error("Missing value for --target-dir");
84
+ process.exit(1);
85
+ }
86
+ return resolve(value);
87
+ }
88
+ return skillTargetDirDefault;
89
+ }
90
+
91
+ function stripInstallFlags(rawArgs) {
92
+ const next = [];
93
+ for (let i = 0; i < rawArgs.length; i++) {
94
+ if (rawArgs[i] === "--target-dir") {
95
+ i += 1;
96
+ continue;
97
+ }
98
+ next.push(rawArgs[i]);
99
+ }
100
+ return next;
101
+ }
102
+
103
+ function checkOpenClaw() {
104
+ const result = spawnSync("openclaw", ["--version"], { stdio: "ignore" });
105
+ return result.status === 0;
106
+ }
107
+
108
+ function runNodeCommand(args, opts = {}) {
109
+ const result = spawnSync(process.execPath, args, {
110
+ stdio: "inherit",
111
+ cwd: pkgRoot,
112
+ ...opts,
113
+ });
114
+
115
+ if (result.error) {
116
+ throw result.error;
117
+ }
118
+
119
+ return result.status ?? 1;
120
+ }
121
+
122
+ function runPlaywrightCommand(args) {
123
+ try {
124
+ const playwrightPackageJson = require.resolve("playwright-core/package.json");
125
+ const playwrightCliPath = join(dirname(playwrightPackageJson), "cli.js");
126
+ return runNodeCommand([playwrightCliPath, ...args]);
127
+ } catch {
128
+ const result = spawnSync(npxBin, ["-y", "playwright-core", ...args], {
129
+ stdio: "inherit",
130
+ cwd: pkgRoot,
131
+ });
132
+
133
+ if (result.error) {
134
+ throw result.error;
135
+ }
136
+
137
+ return result.status ?? 1;
138
+ }
139
+ }
140
+
141
+ function verifyChromiumLaunch() {
142
+ const script = `
143
+ import { chromium } from "playwright-core";
144
+ const browser = await chromium.launch({ headless: true });
145
+ await browser.close();
146
+ console.log("Chromium launch check passed.");
147
+ `;
148
+
149
+ return spawnSync(process.execPath, ["--input-type=module", "-e", script], {
150
+ cwd: pkgRoot,
151
+ encoding: "utf8",
152
+ });
153
+ }
154
+
155
+ function isLinuxRoot() {
156
+ return process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
157
+ }
158
+
159
+ function isMissingLinuxRuntimeLib(stderr = "", stdout = "") {
160
+ const output = `${stdout}\n${stderr}`;
161
+ return /error while loading shared libraries|libnspr4\.so|libnss3\.so|libatk-bridge|libxkbcommon|libgbm|libgtk-3/i.test(
162
+ output,
163
+ );
164
+ }
165
+
166
+ function ensureChromiumReady() {
167
+ console.log("Installing Chromium via Playwright...");
168
+ const installCode = runPlaywrightCommand(["install", "chromium"]);
169
+ if (installCode !== 0) {
170
+ console.error("Failed to install Chromium.");
171
+ process.exit(installCode);
172
+ }
173
+
174
+ let launchCheck = verifyChromiumLaunch();
175
+ if (launchCheck.status === 0) {
176
+ console.log("Chromium is ready.");
177
+ return;
178
+ }
179
+
180
+ if (isMissingLinuxRuntimeLib(launchCheck.stderr, launchCheck.stdout) && process.platform === "linux") {
181
+ console.log("Chromium is installed, but Linux runtime libraries are missing.");
182
+
183
+ if (isLinuxRoot()) {
184
+ console.log("Attempting to install Chromium system dependencies...");
185
+ const depsCode = runPlaywrightCommand(["install-deps", "chromium"]);
186
+ if (depsCode !== 0) {
187
+ console.error("Failed to install Linux Chromium dependencies automatically.");
188
+ process.exit(depsCode);
189
+ }
190
+
191
+ launchCheck = verifyChromiumLaunch();
192
+ if (launchCheck.status === 0) {
193
+ console.log("Chromium system dependencies installed successfully.");
194
+ return;
195
+ }
196
+ } else {
197
+ console.error("Linux Chromium dependencies are missing and this installer is not running as root.");
198
+ console.error("Run one of these commands, then retry:");
199
+ console.error(" sudo npx playwright-core install-deps chromium");
200
+ console.error(
201
+ " sudo apt-get update && sudo apt-get install -y libnspr4 libnss3 libatk-bridge2.0-0 libxkbcommon0 libxcomposite1 libxcursor1 libxdamage1 libxi6 libxtst6 libcups2 libdrm2 libgbm1 libgtk-3-0 libpango-1.0-0 libpangocairo-1.0-0 libasound2",
202
+ );
203
+ process.exit(1);
204
+ }
205
+ }
206
+
207
+ console.error("Chromium launch check failed.");
208
+ if (launchCheck.stderr?.trim()) {
209
+ console.error(launchCheck.stderr.trim());
210
+ } else if (launchCheck.stdout?.trim()) {
211
+ console.error(launchCheck.stdout.trim());
212
+ }
213
+ process.exit(1);
214
+ }
215
+
216
+ function installSkill(targetDir) {
217
+ mkdirSync(targetDir, { recursive: true });
218
+ cpSync(join(pkgRoot, "SKILL.md"), join(targetDir, "SKILL.md"));
219
+ cpSync(join(pkgRoot, "scripts"), join(targetDir, "scripts"), { recursive: true });
220
+
221
+ // Also copy src directory (needed by scripts)
222
+ if (existsSync(join(pkgRoot, "src"))) {
223
+ cpSync(join(pkgRoot, "src"), join(targetDir, "src"), { recursive: true });
224
+ }
225
+
226
+ ensureChromiumReady();
227
+
228
+ console.log(`\nInstalled OpenBuilder to ${targetDir}`);
229
+ if (!checkOpenClaw()) {
230
+ console.log("Warning: `openclaw` was not found in PATH. Install OpenClaw before using the skill.");
231
+ }
232
+ console.log("Start a new OpenClaw session to pick it up.");
233
+ console.log("\nOptional setup:");
234
+ console.log(" npx openbuilder auth — Sign in for authenticated joins");
235
+ console.log(" npx openbuilder config set anthropicApiKey ... — Enable AI reports");
236
+ }
237
+
238
+ function runScript(scriptName, args) {
239
+ const scriptPath = join(pkgRoot, "scripts", scriptName);
240
+ if (!existsSync(scriptPath)) {
241
+ console.error(`Missing script: ${scriptPath}`);
242
+ process.exit(1);
243
+ }
244
+
245
+ const result = spawnSync(process.execPath, ["--import", "tsx", scriptPath, ...args], {
246
+ stdio: "inherit",
247
+ });
248
+
249
+ if (result.error) {
250
+ console.error(result.error.message);
251
+ process.exit(1);
252
+ }
253
+
254
+ process.exit(result.status ?? 0);
255
+ }
256
+
257
+ // ── Main CLI routing ───────────────────────────────────────────────────
258
+
259
+ const rawArgs = process.argv.slice(2);
260
+ const command = rawArgs[0];
261
+
262
+ if (!command || command === "install") {
263
+ installSkill(resolveInstallTarget(rawArgs));
264
+ } else if (command === "auth") {
265
+ runScript("builder-auth.ts", rawArgs.slice(1));
266
+ } else if (command === "join") {
267
+ runScript("builder-join.ts", rawArgs.slice(1));
268
+ } else if (command === "transcript") {
269
+ runScript("builder-transcript.ts", rawArgs.slice(1));
270
+ } else if (command === "screenshot") {
271
+ runScript("builder-screenshot.ts", rawArgs.slice(1));
272
+ } else if (command === "summarize") {
273
+ runScript("builder-summarize.ts", rawArgs.slice(1));
274
+ } else if (command === "report") {
275
+ runScript("builder-report.ts", rawArgs.slice(1));
276
+ } else if (command === "config") {
277
+ runScript("builder-config.ts", rawArgs.slice(1));
278
+ } else if (command === "help" || command === "--help" || command === "-h") {
279
+ printHelp();
280
+ } else {
281
+ const remaining = stripInstallFlags(rawArgs);
282
+ if (remaining.length === 0) {
283
+ installSkill(resolveInstallTarget(rawArgs));
284
+ } else {
285
+ console.error(`Unknown command: ${command}`);
286
+ console.log("");
287
+ printHelp();
288
+ process.exit(1);
289
+ }
290
+ }
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "openbuilder",
3
+ "version": "0.1.0",
4
+ "description": "Open-source AI meeting assistant — join Google Meet, capture transcripts, generate AI-powered meeting reports with summaries, action items, and speaker analytics",
5
+ "type": "module",
6
+ "bin": {
7
+ "openbuilder": "bin/openbuilder.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "scripts",
12
+ "src",
13
+ "SKILL.md"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/superliangbot/openbuilder.git"
18
+ },
19
+ "homepage": "https://github.com/superliangbot/openbuilder",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@anthropic-ai/sdk": "^0.78.0",
29
+ "dotenv": "^17.3.1",
30
+ "playwright-core": "^1.58.2",
31
+ "tsx": "^4.21.0"
32
+ },
33
+ "peerDependencies": {
34
+ "openai": "^6.27.0"
35
+ },
36
+ "peerDependenciesMeta": {
37
+ "@anthropic-ai/sdk": {
38
+ "optional": true
39
+ },
40
+ "openai": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "keywords": [
45
+ "meeting",
46
+ "assistant",
47
+ "ai",
48
+ "google-meet",
49
+ "transcript",
50
+ "summary",
51
+ "action-items",
52
+ "meeting-notes",
53
+ "openclaw"
54
+ ]
55
+ }
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * builder-auth.ts — Automated Google sign-in via Playwright
4
+ *
5
+ * Usage:
6
+ * npx openbuilder auth # Interactive (headed browser)
7
+ * npx openbuilder auth --auto # Automated using GOOGLE_EMAIL + GOOGLE_PASSWORD from .env
8
+ *
9
+ * Saves session to ~/.openbuilder/auth.json via Playwright's storageState.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { createInterface } from "node:readline";
14
+ import { config as dotenvConfig } from "dotenv";
15
+ import { join, dirname } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ import {
19
+ AUTH_FILE,
20
+ AUTH_META_FILE,
21
+ OPENBUILDER_DIR,
22
+ } from "../src/utils/config.js";
23
+
24
+ // Load .env from project root
25
+ const __dirname2 = dirname(fileURLToPath(import.meta.url));
26
+ dotenvConfig({ path: join(__dirname2, "..", ".env") });
27
+
28
+ type PlaywrightMod = typeof import("playwright-core");
29
+ type Page = import("playwright-core").Page;
30
+
31
+ async function waitForEnter(prompt: string): Promise<void> {
32
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
33
+ return new Promise((resolve) => {
34
+ rl.question(prompt, () => {
35
+ rl.close();
36
+ resolve();
37
+ });
38
+ });
39
+ }
40
+
41
+ async function sleep(ms: number) {
42
+ return new Promise((r) => setTimeout(r, ms));
43
+ }
44
+
45
+ async function autoSignIn(page: Page, email: string, password: string): Promise<boolean> {
46
+ console.log("Navigating to Google sign-in...");
47
+ await page.goto("https://accounts.google.com/signin", { waitUntil: "domcontentloaded" });
48
+ await sleep(2000);
49
+
50
+ // Screenshot for debugging
51
+ const ssDir = join(OPENBUILDER_DIR, "auth-debug");
52
+ mkdirSync(ssDir, { recursive: true });
53
+
54
+ // Enter email
55
+ console.log("Entering email...");
56
+ const emailInput = page.locator('input[type="email"]');
57
+ await emailInput.waitFor({ state: "visible", timeout: 15000 });
58
+ await emailInput.fill(email);
59
+ await sleep(500);
60
+
61
+ // Click Next
62
+ const nextBtn = page.locator('#identifierNext button, button:has-text("Next")').first();
63
+ await nextBtn.click();
64
+ await sleep(3000);
65
+
66
+ await page.screenshot({ path: join(ssDir, "after-email.png") });
67
+
68
+ // Check for CAPTCHA or challenge
69
+ const pageText = await page.textContent("body").catch(() => "");
70
+ if (pageText?.includes("Verify it's you") || pageText?.includes("confirm your identity")) {
71
+ console.error("\n⚠️ Google is requesting additional verification (CAPTCHA/challenge).");
72
+ console.error("You may need to run `npx openbuilder auth` interactively (without --auto).");
73
+ await page.screenshot({ path: join(ssDir, "challenge.png") });
74
+ return false;
75
+ }
76
+
77
+ // Enter password
78
+ console.log("Entering password...");
79
+ const passwordInput = page.locator('input[type="password"]');
80
+ try {
81
+ await passwordInput.waitFor({ state: "visible", timeout: 10000 });
82
+ } catch {
83
+ console.error("Password field not found. Google may be showing a challenge.");
84
+ await page.screenshot({ path: join(ssDir, "no-password-field.png") });
85
+ return false;
86
+ }
87
+ await passwordInput.fill(password);
88
+ await sleep(500);
89
+
90
+ // Click Next for password
91
+ const passNext = page.locator('#passwordNext button, button:has-text("Next")').first();
92
+ await passNext.click();
93
+ await sleep(5000);
94
+
95
+ await page.screenshot({ path: join(ssDir, "after-password.png") });
96
+
97
+ // Check if we landed on myaccount or got challenged
98
+ const url = page.url();
99
+ const bodyText = await page.textContent("body").catch(() => "");
100
+
101
+ if (url.includes("myaccount.google.com") || url.includes("accounts.google.com/signin/v2/challenge") === false) {
102
+ // Try navigating to myaccount to confirm
103
+ await page.goto("https://myaccount.google.com", { waitUntil: "domcontentloaded", timeout: 15000 });
104
+ await sleep(2000);
105
+ await page.screenshot({ path: join(ssDir, "myaccount.png") });
106
+
107
+ const finalUrl = page.url();
108
+ if (finalUrl.includes("myaccount.google.com") && !finalUrl.includes("signin")) {
109
+ console.log("✅ Sign-in successful!");
110
+ return true;
111
+ }
112
+ }
113
+
114
+ // Check for 2FA
115
+ if (bodyText?.includes("2-Step Verification") || bodyText?.includes("Verify it")) {
116
+ console.error("\n⚠️ 2-Step Verification required.");
117
+ console.error("Either disable 2FA on this account or use interactive mode.");
118
+ await page.screenshot({ path: join(ssDir, "2fa.png") });
119
+ return false;
120
+ }
121
+
122
+ // Check for wrong password
123
+ if (bodyText?.includes("Wrong password") || bodyText?.includes("Couldn't sign you in")) {
124
+ console.error("\n❌ Wrong password or sign-in rejected.");
125
+ await page.screenshot({ path: join(ssDir, "wrong-password.png") });
126
+ return false;
127
+ }
128
+
129
+ // Might have succeeded anyway — try to save
130
+ console.log("Sign-in status unclear, attempting to save session...");
131
+ await page.screenshot({ path: join(ssDir, "unclear.png") });
132
+ return true;
133
+ }
134
+
135
+ async function main() {
136
+ mkdirSync(OPENBUILDER_DIR, { recursive: true });
137
+
138
+ const args = process.argv.slice(2);
139
+ const autoMode = args.includes("--auto");
140
+ const headed = args.includes("--headed") || !autoMode; // Auto mode can run headless
141
+
142
+ let pw: PlaywrightMod;
143
+ try {
144
+ pw = await import("playwright-core");
145
+ } catch {
146
+ console.error("playwright-core not found. Run `npm install`.");
147
+ process.exit(1);
148
+ }
149
+
150
+ console.log("OpenBuilder — Google Account Login\n");
151
+
152
+ if (existsSync(AUTH_FILE)) {
153
+ console.log(`Existing auth found at ${AUTH_FILE}`);
154
+ console.log("This will overwrite it with a new session.\n");
155
+ }
156
+
157
+ // For auto mode on headless server, use Xvfb
158
+ const headless = !headed;
159
+ const chromiumArgs = [
160
+ "--disable-blink-features=AutomationControlled",
161
+ "--no-first-run",
162
+ "--no-default-browser-check",
163
+ "--window-size=1280,720",
164
+ ];
165
+
166
+ // Find full Chrome for headed mode
167
+ let executablePath: string | undefined;
168
+ const fullChromePath = join(
169
+ process.env.HOME || "~",
170
+ ".cache/ms-playwright/chromium-1208/chrome-linux64/chrome"
171
+ );
172
+ if (existsSync(fullChromePath)) {
173
+ executablePath = fullChromePath;
174
+ }
175
+
176
+ const browser = await pw.chromium.launch({
177
+ headless,
178
+ executablePath,
179
+ args: chromiumArgs,
180
+ ignoreDefaultArgs: ["--enable-automation"],
181
+ });
182
+
183
+ const context = await browser.newContext({
184
+ viewport: { width: 1280, height: 720 },
185
+ userAgent:
186
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
187
+ });
188
+
189
+ const page = await context.newPage();
190
+ let email = "unknown";
191
+
192
+ if (autoMode) {
193
+ const gEmail = process.env.GOOGLE_EMAIL;
194
+ const gPassword = process.env.GOOGLE_PASSWORD;
195
+
196
+ if (!gEmail || !gPassword) {
197
+ console.error("--auto requires GOOGLE_EMAIL and GOOGLE_PASSWORD in .env");
198
+ await browser.close();
199
+ process.exit(1);
200
+ }
201
+
202
+ console.log(`Attempting automated sign-in for ${gEmail}...\n`);
203
+ const success = await autoSignIn(page, gEmail, gPassword);
204
+
205
+ if (!success) {
206
+ console.error("\nAutomated sign-in failed. Check screenshots in ~/.openbuilder/auth-debug/");
207
+ await browser.close();
208
+ process.exit(1);
209
+ }
210
+
211
+ email = gEmail;
212
+ } else {
213
+ // Interactive mode
214
+ await page.goto("https://accounts.google.com", { waitUntil: "domcontentloaded" });
215
+ console.log("Browser opened — sign into Google now.\n");
216
+ await waitForEnter("Press Enter after you've signed in to Google... ");
217
+
218
+ // Extract email
219
+ try {
220
+ await page.goto("https://myaccount.google.com", { waitUntil: "domcontentloaded", timeout: 10000 });
221
+ await sleep(2000);
222
+ email = await page.evaluate(() => {
223
+ const emailEl = document.querySelector("[data-email]");
224
+ if (emailEl) return emailEl.getAttribute("data-email") || "";
225
+ const profileBtn = document.querySelector('[aria-label*="@"]');
226
+ if (profileBtn) {
227
+ const match = profileBtn.getAttribute("aria-label")?.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
228
+ if (match) return match[0];
229
+ }
230
+ return "";
231
+ });
232
+ } catch { /* not critical */ }
233
+ }
234
+
235
+ if (email && email !== "unknown") {
236
+ console.log(`\nSigned in as: ${email}`);
237
+ }
238
+
239
+ // Save session
240
+ await context.storageState({ path: AUTH_FILE });
241
+ console.log(`Session saved to ${AUTH_FILE}`);
242
+
243
+ const meta = { email: email || "unknown", savedAt: new Date().toISOString() };
244
+ writeFileSync(AUTH_META_FILE, JSON.stringify(meta, null, 2));
245
+
246
+ try {
247
+ const state = JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
248
+ const cookieCount = state.cookies?.length ?? 0;
249
+ const originCount = state.origins?.length ?? 0;
250
+ console.log(` ${cookieCount} cookies, ${originCount} origins saved`);
251
+ } catch { /* not critical */ }
252
+
253
+ await browser.close();
254
+
255
+ console.log("\nDone! The bot will now join meetings as an authenticated user.");
256
+ console.log("Run: npx openbuilder join <meet-url> --auth");
257
+ }
258
+
259
+ const isMain = process.argv[1]?.endsWith("builder-auth.ts");
260
+ if (isMain) {
261
+ main().catch((err) => {
262
+ console.error("Fatal:", err instanceof Error ? err.message : String(err));
263
+ process.exit(1);
264
+ });
265
+ }