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.
- package/LICENSE +21 -0
- package/README.md +309 -0
- package/SKILL.md +276 -0
- package/bin/openbuilder.mjs +290 -0
- package/package.json +55 -0
- package/scripts/builder-auth.ts +265 -0
- package/scripts/builder-config.ts +166 -0
- package/scripts/builder-join.ts +1613 -0
- package/scripts/builder-report.ts +166 -0
- package/scripts/builder-screenshot.ts +80 -0
- package/scripts/builder-summarize.ts +142 -0
- package/scripts/builder-transcript.ts +62 -0
- package/src/ai/claude.ts +59 -0
- package/src/ai/openai.ts +54 -0
- package/src/ai/prompts.ts +95 -0
- package/src/ai/provider.ts +39 -0
- package/src/analytics/speaker-stats.ts +104 -0
- package/src/audio/capture.ts +374 -0
- package/src/audio/pipeline.ts +189 -0
- package/src/audio/transcriber.ts +126 -0
- package/src/report/generator.ts +149 -0
- package/src/utils/config.ts +102 -0
- package/src/utils/transcript-parser.ts +116 -0
|
@@ -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
|
+
}
|