@alook/app 0.0.16 → 0.0.18

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.
Files changed (68) hide show
  1. package/bundled/web/.open-next/.build/durable-objects/queue.js +4 -4
  2. package/bundled/web/.open-next/assets/BUILD_ID +1 -1
  3. package/bundled/web/.open-next/assets/_next/static/chunks/0-ak0pl36y6db.js +20 -0
  4. package/bundled/web/.open-next/assets/_next/static/chunks/03ttku7i148lq.js +1 -0
  5. package/bundled/web/.open-next/assets/_next/static/chunks/{0yj9pmm88z1pn.js → 0~d77nakvw-pm.js} +1 -1
  6. package/bundled/web/.open-next/assets/_next/static/chunks/{07oq~wuv265o6.js → 17trnn..dwzu7.js} +1 -1
  7. package/bundled/web/.open-next/cache/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/_global-error.cache +1 -1
  8. package/bundled/web/.open-next/cache/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/_not-found.cache +1 -1
  9. package/bundled/web/.open-next/cache/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/sitemap.xml.cache +1 -1
  10. package/bundled/web/.open-next/cloudflare/cache-assets-manifest.sql +1 -1
  11. package/bundled/web/.open-next/cloudflare/init.js +1 -1
  12. package/bundled/web/.open-next/dynamodb-provider/dynamodb-cache.json +1 -1
  13. package/bundled/web/.open-next/middleware/handler.mjs +2 -2
  14. package/bundled/web/.open-next/server-functions/default/src/web/.next/BUILD_ID +1 -1
  15. package/bundled/web/.open-next/server-functions/default/src/web/.next/build-manifest.json +3 -3
  16. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/invite/[token]/page_client-reference-manifest.js +1 -1
  17. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/studio/new/page_client-reference-manifest.js +1 -1
  18. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/activity/page_client-reference-manifest.js +1 -1
  19. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/chat/[convId]/page_client-reference-manifest.js +1 -1
  20. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/chat/page_client-reference-manifest.js +1 -1
  21. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/email/page_client-reference-manifest.js +1 -1
  22. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/files/page_client-reference-manifest.js +1 -1
  23. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/meetings/page_client-reference-manifest.js +1 -1
  24. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/[id]/page_client-reference-manifest.js +1 -1
  25. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/new/page_client-reference-manifest.js +1 -1
  26. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/agents/page_client-reference-manifest.js +1 -1
  27. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/calendar/page_client-reference-manifest.js +1 -1
  28. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/help/email-setup/page_client-reference-manifest.js +1 -1
  29. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/home/page_client-reference-manifest.js +1 -1
  30. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/issues/page_client-reference-manifest.js +1 -1
  31. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/runtimes/page_client-reference-manifest.js +1 -1
  32. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/settings/page_client-reference-manifest.js +1 -1
  33. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/threads/[traceId]/page_client-reference-manifest.js +1 -1
  34. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/threads/page_client-reference-manifest.js +1 -1
  35. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/w/[slug]/unread/page_client-reference-manifest.js +1 -1
  36. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(app)/workspaces/page_client-reference-manifest.js +1 -1
  37. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/(auth)/sign-in/page_client-reference-manifest.js +1 -1
  38. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  39. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/templates/[id]/page_client-reference-manifest.js +1 -1
  41. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/app/templates/page_client-reference-manifest.js +1 -1
  42. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/[turbopack]_runtime.js +3 -3
  43. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/[root-of-the-server]__0u~3z5r._.js +3 -0
  44. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/[turbopack]_runtime.js +3 -3
  45. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/_0o6ydx5._.js +1 -1
  46. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/_0vhi1.k._.js +3 -0
  47. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/_0yeokgf._.js +3 -0
  48. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/src_web_src_app_(auth)_sign-in_page_tsx_0jdlb-h._.js +1 -1
  49. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/middleware-build-manifest.js +3 -3
  50. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/middleware-manifest.json +1 -1
  51. package/bundled/web/.open-next/server-functions/default/src/web/handler.mjs +8 -8
  52. package/bundled/web/.open-next/server-functions/default/src/web/handler.mjs.meta.json +63 -63
  53. package/bundled/web/.open-next/server-functions/default/src/web/index.mjs +2 -2
  54. package/bundled/web/custom-worker.ts +2 -2
  55. package/dist/cli/index.js +19660 -0
  56. package/dist/cli/meeting-runner.js +888 -0
  57. package/dist/cli/session-runner.js +18794 -0
  58. package/dist/index.js +15 -13
  59. package/package.json +8 -5
  60. package/bundled/web/.open-next/assets/_next/static/chunks/0tfsuh0extc-0.js +0 -1
  61. package/bundled/web/.open-next/assets/_next/static/chunks/12x34w3ifvehm.js +0 -20
  62. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/[root-of-the-server]__10ubs43._.js +0 -3
  63. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/_0js75xc._.js +0 -3
  64. package/bundled/web/.open-next/server-functions/default/src/web/.next/server/chunks/ssr/_0yxlebv._.js +0 -3
  65. /package/bundled/web/.open-next/assets/_next/static/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/_buildManifest.js +0 -0
  66. /package/bundled/web/.open-next/assets/_next/static/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/_clientMiddlewareManifest.js +0 -0
  67. /package/bundled/web/.open-next/assets/_next/static/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/_ssgManifest.js +0 -0
  68. /package/bundled/web/.open-next/cache/{z2Xve2qYI6cufaKgk6mWN → 1KhJxRMQgsspHvsEB0i7h}/robots.txt.cache +0 -0
@@ -0,0 +1,888 @@
1
+ // daemon/meeting-runner.ts
2
+ import { chromium } from "playwright-core";
3
+
4
+ // ../shared/src/browser/caption-scraper.ts
5
+ function stripHtml(html) {
6
+ return html.replace(/<[^>]*>/g, "").trim();
7
+ }
8
+ function parseCaptionElements(elements) {
9
+ const results = [];
10
+ for (const el of elements) {
11
+ const speaker = stripHtml(el.speakerHtml);
12
+ const text = stripHtml(el.textHtml);
13
+ if (!speaker || !text)
14
+ continue;
15
+ results.push({ speaker, text });
16
+ }
17
+ return results;
18
+ }
19
+ function buildCaptionObserverScript() {
20
+ return `
21
+ (() => {
22
+ if (window.__alookCaptionObserver) return;
23
+ window.__alookCaptionObserver = true;
24
+ window.__alookCaptions = [];
25
+ window.__alookLastCaption = '';
26
+
27
+ const observer = new MutationObserver(() => {
28
+ // On any DOM change, scan for caption content.
29
+ // Google Meet renders captions as overlays with img (avatar) + text.
30
+ // The text mutates in-place (characterData), so we snapshot on every change.
31
+ const imgs = document.querySelectorAll('img');
32
+ for (const img of imgs) {
33
+ let entry = img.parentElement;
34
+ // Walk up to find the caption entry container
35
+ for (let i = 0; i < 4 && entry; i++) {
36
+ const text = entry.textContent || '';
37
+ if (text.length > 3 && entry.querySelectorAll('img').length === 1) break;
38
+ entry = entry.parentElement;
39
+ }
40
+ if (!entry) continue;
41
+
42
+ // Check this looks like a caption (has img + non-button text)
43
+ const parts = [];
44
+ const walker = document.createTreeWalker(entry, NodeFilter.SHOW_TEXT);
45
+ let node;
46
+ while (node = walker.nextNode()) {
47
+ let inBtn = false;
48
+ let p = node.parentElement;
49
+ while (p && p !== entry) {
50
+ if (p.tagName === 'BUTTON') { inBtn = true; break; }
51
+ p = p.parentElement;
52
+ }
53
+ if (inBtn) continue;
54
+ const t = node.textContent.trim();
55
+ if (t.length > 0 && t.length < 500) parts.push(t);
56
+ }
57
+
58
+ if (parts.length < 2 || parts[0].length > 40) continue;
59
+
60
+ const speaker = parts[0];
61
+ const text = parts.slice(1).join(' ');
62
+ const key = speaker + '::' + text;
63
+
64
+ // Filter out UI elements misidentified as captions
65
+ const lower = text.toLowerCase();
66
+ if (lower.includes('background') || lower.includes('effects') || lower.includes('devices') ||
67
+ lower.includes('more options') || lower.includes('still see your') ||
68
+ lower.includes('settings') || lower.includes('reframe')) continue;
69
+
70
+ // Only record if text changed from last snapshot
71
+ if (key !== window.__alookLastCaption) {
72
+ window.__alookLastCaption = key;
73
+ window.__alookCaptions.push({
74
+ speakerHtml: speaker,
75
+ textHtml: text,
76
+ ts: Date.now(),
77
+ });
78
+ }
79
+ }
80
+ });
81
+
82
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
83
+ })()
84
+ `.trim();
85
+ }
86
+ function buildCaptionScrapeScript() {
87
+ return `
88
+ (() => {
89
+ const result = window.__alookCaptions || [];
90
+ window.__alookCaptions = [];
91
+ return result.map(c => ({ speakerHtml: c.speakerHtml, textHtml: c.textHtml }));
92
+ })()
93
+ `.trim();
94
+ }
95
+ // ../shared/src/browser/transcript.ts
96
+ function createTimestamp(startMs, currentMs) {
97
+ const elapsed = Math.max(0, currentMs - startMs);
98
+ const totalSeconds = Math.floor(elapsed / 1000);
99
+ const hours = Math.floor(totalSeconds / 3600);
100
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
101
+ const seconds = totalSeconds % 60;
102
+ return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
103
+ }
104
+ function deduplicateCaptions(existing, incoming, meetingStartMs, nowMs) {
105
+ const timestamp = createTimestamp(meetingStartMs, nowMs);
106
+ const newEntries = [];
107
+ for (const cap of incoming) {
108
+ const last = existing.length > 0 ? existing[existing.length - 1] : null;
109
+ if (last && last.speaker === cap.speaker && last.text === cap.text) {
110
+ continue;
111
+ }
112
+ if (last && last.speaker === cap.speaker && cap.text.startsWith(last.text)) {
113
+ last.text = cap.text;
114
+ continue;
115
+ }
116
+ newEntries.push({
117
+ speaker: cap.speaker,
118
+ text: cap.text,
119
+ timestamp
120
+ });
121
+ }
122
+ return [...existing, ...newEntries];
123
+ }
124
+ function groupIntoBlocks(entries) {
125
+ const blocks = [];
126
+ for (const entry of entries) {
127
+ const lastBlock = blocks.length > 0 ? blocks[blocks.length - 1] : null;
128
+ if (lastBlock && lastBlock.speaker === entry.speaker) {
129
+ lastBlock.lines.push(entry.text);
130
+ } else {
131
+ blocks.push({
132
+ speaker: entry.speaker,
133
+ lines: [entry.text],
134
+ startTimestamp: entry.timestamp
135
+ });
136
+ }
137
+ }
138
+ return blocks;
139
+ }
140
+ function formatTranscript(entries) {
141
+ if (entries.length === 0)
142
+ return "";
143
+ const blocks = groupIntoBlocks(entries);
144
+ return blocks.map((block) => `[${block.startTimestamp}] ${block.speaker}:
145
+ ${block.lines.join(`
146
+ `)}`).join(`
147
+
148
+ `);
149
+ }
150
+ // ../shared/src/browser/meet-navigator.ts
151
+ function delay(ms) {
152
+ return new Promise((resolve) => setTimeout(resolve, ms));
153
+ }
154
+ async function dismissDialogs(page) {
155
+ for (let i = 0;i < 3; i++) {
156
+ try {
157
+ const dialogBtn = await page.$('[role="dialog"] button, [role="alertdialog"] button, [role="alert"] button');
158
+ if (dialogBtn) {
159
+ await dialogBtn.click();
160
+ await delay(300);
161
+ continue;
162
+ }
163
+ } catch {}
164
+ break;
165
+ }
166
+ }
167
+ async function detectBlocked(page) {
168
+ const blocked = await page.evaluate(() => {
169
+ const text = document.body?.innerText || "";
170
+ if (text.includes("can't join") || text.includes("unable to join") || text.includes("无法加入")) {
171
+ return text.slice(0, 200);
172
+ }
173
+ return null;
174
+ });
175
+ if (blocked) {
176
+ throw new Error(`Blocked from joining: ${blocked}`);
177
+ }
178
+ }
179
+ async function joinMeeting(page, meetingUrl, botName) {
180
+ await page.goto(meetingUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
181
+ await delay(2000);
182
+ await detectBlocked(page);
183
+ await dismissDialogs(page);
184
+ try {
185
+ const nameInput = await page.waitForSelector('input[aria-label="Your name"]', { timeout: 1e4 });
186
+ if (nameInput) {
187
+ await nameInput.click({ clickCount: 3 });
188
+ await nameInput.type(botName);
189
+ }
190
+ } catch {}
191
+ await page.evaluate(() => {
192
+ const btns = document.querySelectorAll("button");
193
+ for (const btn of btns) {
194
+ const label = (btn.getAttribute("aria-label") || "").toLowerCase();
195
+ const muted = btn.getAttribute("data-is-muted");
196
+ if (muted === "false" && (label.includes("microphone") || label.includes("camera"))) {
197
+ btn.click();
198
+ continue;
199
+ }
200
+ if (label.startsWith("turn off") && (label.includes("microphone") || label.includes("camera"))) {
201
+ btn.click();
202
+ }
203
+ }
204
+ });
205
+ const joined = await page.evaluate(() => {
206
+ const deadline = Date.now() + 15000;
207
+ return new Promise((resolve) => {
208
+ const check = () => {
209
+ const btns = document.querySelectorAll("button:not([disabled])");
210
+ for (const btn of btns) {
211
+ const text = (btn.textContent || "").toLowerCase();
212
+ if (text.includes("join") || text.includes("加入")) {
213
+ if (text.includes("other") || text.includes("其他"))
214
+ continue;
215
+ btn.click();
216
+ return resolve(true);
217
+ }
218
+ }
219
+ if (Date.now() < deadline)
220
+ setTimeout(check, 500);
221
+ else
222
+ resolve(false);
223
+ };
224
+ check();
225
+ });
226
+ });
227
+ if (!joined)
228
+ throw new Error("Join button not found or remained disabled");
229
+ await delay(3000);
230
+ await detectBlocked(page);
231
+ }
232
+ async function enableCaptions(page) {
233
+ const clicked = await page.evaluate(() => {
234
+ const btns = document.querySelectorAll("button");
235
+ for (const btn of btns) {
236
+ const label = (btn.getAttribute("aria-label") || "").toLowerCase();
237
+ const text = (btn.textContent || "").toLowerCase();
238
+ if (label.includes("caption") || label.includes("subtitle") || label.includes("字幕") || text.includes("closed_caption")) {
239
+ btn.click();
240
+ return true;
241
+ }
242
+ }
243
+ return false;
244
+ });
245
+ if (clicked) {
246
+ await delay(2000);
247
+ }
248
+ }
249
+ async function waitForMeetingReady(page, timeoutMs = 60000) {
250
+ const deadline = Date.now() + timeoutMs;
251
+ while (Date.now() < deadline) {
252
+ const state = await page.evaluate(() => {
253
+ const text = document.body?.innerText || "";
254
+ if (text.includes("Please wait") || text.includes("请等待"))
255
+ return "waiting";
256
+ if (text.includes("can't join") || text.includes("无法加入"))
257
+ return "blocked";
258
+ const inCall = document.querySelector('button[aria-label*="Leave call" i], button[aria-label="退出通话"]');
259
+ const waiting = document.querySelector('[aria-label*="wait" i]');
260
+ if (inCall && !waiting)
261
+ return "ready";
262
+ return "loading";
263
+ });
264
+ if (state === "ready")
265
+ return;
266
+ if (state === "blocked")
267
+ throw new Error("Blocked from joining meeting");
268
+ await delay(2000);
269
+ }
270
+ throw new Error("Timed out waiting to be admitted to meeting");
271
+ }
272
+ function buildAloneDetectorScript() {
273
+ return `
274
+ (() => {
275
+ if (window.__alookAloneDetector) return;
276
+ window.__alookAloneDetector = true;
277
+ window.__alookAlone = false;
278
+
279
+ const keywords = ['only one here', 'no one else', 'everyone has left',
280
+ '只有你', '没有其他人', '所有人都已离开'];
281
+
282
+ const observer = new MutationObserver((mutations) => {
283
+ for (const m of mutations) {
284
+ for (const node of m.addedNodes) {
285
+ if (node.nodeType !== 1) continue;
286
+ const text = (node.textContent || '').toLowerCase();
287
+ for (const kw of keywords) {
288
+ if (text.includes(kw)) {
289
+ window.__alookAlone = true;
290
+ return;
291
+ }
292
+ }
293
+ }
294
+ }
295
+ });
296
+ observer.observe(document.body, { childList: true, subtree: true });
297
+ })()
298
+ `.trim();
299
+ }
300
+ async function isMeetingActive(page) {
301
+ try {
302
+ return await page.evaluate(() => {
303
+ const leaveBtn = document.querySelector('button[aria-label="Leave call" i], button[aria-label*="hang up" i]');
304
+ if (!leaveBtn)
305
+ return false;
306
+ if (window.__alookAlone)
307
+ return false;
308
+ return true;
309
+ });
310
+ } catch {
311
+ return false;
312
+ }
313
+ }
314
+ async function leaveMeeting(page) {
315
+ try {
316
+ const leaveButton = await page.$('button[aria-label="Leave call" i], button[aria-label*="hang up" i]');
317
+ if (leaveButton) {
318
+ await leaveButton.click();
319
+ await delay(2000);
320
+ }
321
+ } catch {}
322
+ }
323
+ // ../shared/src/browser/chrome-finder.ts
324
+ import { existsSync } from "node:fs";
325
+ import { execSync } from "node:child_process";
326
+ var CHROME_PATHS = {
327
+ darwin: [
328
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
329
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
330
+ "/Applications/Chromium.app/Contents/MacOS/Chromium"
331
+ ],
332
+ linux: [
333
+ "/usr/bin/google-chrome",
334
+ "/usr/bin/google-chrome-stable",
335
+ "/usr/bin/chromium-browser",
336
+ "/usr/bin/chromium",
337
+ "/snap/bin/chromium"
338
+ ],
339
+ win32: [
340
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
341
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
342
+ `${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`
343
+ ]
344
+ };
345
+ function findChrome() {
346
+ const platform = process.platform;
347
+ const candidates = CHROME_PATHS[platform] ?? [];
348
+ for (const p of candidates) {
349
+ if (existsSync(p))
350
+ return p;
351
+ }
352
+ if (platform === "linux") {
353
+ try {
354
+ const result = execSync("which google-chrome || which chromium", { encoding: "utf8" }).trim();
355
+ if (result)
356
+ return result;
357
+ } catch {}
358
+ }
359
+ return findPlaywrightChromium();
360
+ }
361
+ function findPlaywrightChromium() {
362
+ try {
363
+ const result = execSync("npx playwright install --dry-run chromium 2>&1", { encoding: "utf8" });
364
+ const match = result.match(/browser binaries.*?:\s*(.+)/i);
365
+ if (match) {
366
+ const dir = match[1].trim();
367
+ const chromePaths = [
368
+ `${dir}/chrome-linux/chrome`,
369
+ `${dir}/chrome-mac/Chromium.app/Contents/MacOS/Chromium`,
370
+ `${dir}/chrome-win/chrome.exe`
371
+ ];
372
+ for (const p of chromePaths) {
373
+ if (existsSync(p))
374
+ return p;
375
+ }
376
+ }
377
+ } catch {}
378
+ return null;
379
+ }
380
+ function ensureChrome() {
381
+ const existing = findChrome();
382
+ if (existing)
383
+ return existing;
384
+ execSync("npx playwright install chromium", {
385
+ stdio: "inherit",
386
+ timeout: 120000
387
+ });
388
+ const installed = findChrome();
389
+ if (!installed)
390
+ throw new Error("Failed to install Chromium via Playwright");
391
+ return installed;
392
+ }
393
+ // daemon/meeting-runner.ts
394
+ import { join as join3 } from "path";
395
+ import { mkdirSync as mkdirSync2 } from "fs";
396
+
397
+ // lib/platform.ts
398
+ import { tmpdir } from "os";
399
+ import { join, sep } from "path";
400
+ var isWindows = process.platform === "win32";
401
+ function tempDir(subdir) {
402
+ return join(tmpdir(), subdir);
403
+ }
404
+
405
+ // lib/logger.ts
406
+ var LEVELS = {
407
+ debug: 0,
408
+ info: 1,
409
+ warn: 2,
410
+ error: 3,
411
+ silent: 4
412
+ };
413
+ var LABELS = {
414
+ debug: "DEBUG",
415
+ info: "INFO ",
416
+ warn: "WARN ",
417
+ error: "ERROR"
418
+ };
419
+ var COLORS = {
420
+ debug: "\x1B[90m",
421
+ info: "\x1B[36m",
422
+ warn: "\x1B[33m",
423
+ error: "\x1B[31m"
424
+ };
425
+ var RESET = "\x1B[0m";
426
+ var DIM = "\x1B[2m";
427
+ var BOLD = "\x1B[1m";
428
+ function useColor() {
429
+ if (process.env.NO_COLOR !== undefined)
430
+ return false;
431
+ if (process.env.FORCE_COLOR !== undefined)
432
+ return true;
433
+ return process.stdout.isTTY === true;
434
+ }
435
+ function timestamp() {
436
+ const d = new Date;
437
+ const Y = d.getFullYear();
438
+ const M = String(d.getMonth() + 1).padStart(2, "0");
439
+ const D = String(d.getDate()).padStart(2, "0");
440
+ const h = String(d.getHours()).padStart(2, "0");
441
+ const m = String(d.getMinutes()).padStart(2, "0");
442
+ const s = String(d.getSeconds()).padStart(2, "0");
443
+ return `${Y}-${M}-${D} ${h}:${m}:${s}`;
444
+ }
445
+
446
+ class Logger {
447
+ level;
448
+ color;
449
+ module;
450
+ constructor(opts = {}) {
451
+ const envLevel = process.env.ALOOK_LOG_LEVEL;
452
+ this.level = LEVELS[opts.level ?? envLevel ?? "info"];
453
+ this.color = useColor();
454
+ this.module = opts.module;
455
+ }
456
+ setLevel(level) {
457
+ this.level = LEVELS[level];
458
+ }
459
+ child(module) {
460
+ const child = new Logger({ level: this.levelName(), module });
461
+ return child;
462
+ }
463
+ debug(msg, ...args) {
464
+ this.write("debug", msg, args);
465
+ }
466
+ info(msg, ...args) {
467
+ this.write("info", msg, args);
468
+ }
469
+ warn(msg, ...args) {
470
+ this.write("warn", msg, args);
471
+ }
472
+ error(msg, ...args) {
473
+ this.write("error", msg, args);
474
+ }
475
+ levelName() {
476
+ for (const [name, num] of Object.entries(LEVELS)) {
477
+ if (num === this.level)
478
+ return name;
479
+ }
480
+ return "info";
481
+ }
482
+ write(level, msg, args) {
483
+ if (LEVELS[level] < this.level)
484
+ return;
485
+ const ts = timestamp();
486
+ const label = LABELS[level];
487
+ const mod = this.module ? `[${this.module}]` : "";
488
+ let line;
489
+ if (this.color) {
490
+ const c = COLORS[level];
491
+ const modStr = mod ? ` ${BOLD}${mod}${RESET}` : "";
492
+ line = `${DIM}${ts}${RESET} ${c}${label}${RESET}${modStr} ${msg}`;
493
+ } else {
494
+ const modStr = mod ? ` ${mod}` : "";
495
+ line = `${ts} ${label}${modStr} ${msg}`;
496
+ }
497
+ const dest = level === "error" ? process.stderr : process.stdout;
498
+ dest.write(line + `
499
+ `);
500
+ for (const a of args) {
501
+ if (a instanceof Error) {
502
+ dest.write(` ${a.message}
503
+ `);
504
+ if (a.stack && this.level <= LEVELS.debug) {
505
+ dest.write(` ${a.stack}
506
+ `);
507
+ }
508
+ } else if (a !== null && typeof a === "object") {
509
+ const pairs = Object.entries(a).map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`).join(" ");
510
+ if (pairs)
511
+ dest.write(` ${pairs}
512
+ `);
513
+ } else if (a !== undefined) {
514
+ dest.write(` ${String(a)}
515
+ `);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ function createLogger(opts) {
521
+ return new Logger(opts);
522
+ }
523
+ var log = createLogger();
524
+
525
+ // daemon/execenv/timeline.ts
526
+ import { appendFileSync, readFileSync, writeFileSync, renameSync } from "fs";
527
+ import { join as join2 } from "path";
528
+
529
+ // daemon/execenv/filelock.ts
530
+ import { mkdirSync, rmdirSync, statSync } from "fs";
531
+ var DEFAULT_STALE_MS = 3600000;
532
+ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
533
+ try {
534
+ mkdirSync(lockPath);
535
+ return true;
536
+ } catch {
537
+ try {
538
+ const stat = statSync(lockPath);
539
+ if (Date.now() - stat.mtimeMs > staleMs) {
540
+ rmdirSync(lockPath);
541
+ try {
542
+ mkdirSync(lockPath);
543
+ return true;
544
+ } catch {
545
+ return false;
546
+ }
547
+ }
548
+ } catch {
549
+ try {
550
+ mkdirSync(lockPath);
551
+ return true;
552
+ } catch {
553
+ return false;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+ }
559
+ function releaseLock(lockPath) {
560
+ try {
561
+ rmdirSync(lockPath);
562
+ } catch {}
563
+ }
564
+
565
+ // daemon/execenv/timeline.ts
566
+ var log2 = createLogger({ module: "timeline" });
567
+ function filenameForDate(date) {
568
+ const y = date.getFullYear();
569
+ const m = String(date.getMonth() + 1).padStart(2, "0");
570
+ const d = String(date.getDate()).padStart(2, "0");
571
+ return `${y}-${m}-${d}.jsonl`;
572
+ }
573
+ function todayFilename() {
574
+ return filenameForDate(new Date);
575
+ }
576
+ function recentFilenames(maxDays) {
577
+ const filenames = [];
578
+ const now = new Date;
579
+ for (let i = 0;i < maxDays; i++) {
580
+ const d = new Date(now);
581
+ d.setDate(d.getDate() - i);
582
+ filenames.push(filenameForDate(d));
583
+ }
584
+ return filenames;
585
+ }
586
+ function localISOString() {
587
+ const now = new Date;
588
+ const tzOffset = -now.getTimezoneOffset();
589
+ const sign = tzOffset >= 0 ? "+" : "-";
590
+ const absOffset = Math.abs(tzOffset);
591
+ const hh = String(Math.floor(absOffset / 60)).padStart(2, "0");
592
+ const mm = String(absOffset % 60).padStart(2, "0");
593
+ const y = now.getFullYear();
594
+ const mo = String(now.getMonth() + 1).padStart(2, "0");
595
+ const d = String(now.getDate()).padStart(2, "0");
596
+ const h = String(now.getHours()).padStart(2, "0");
597
+ const mi = String(now.getMinutes()).padStart(2, "0");
598
+ const s = String(now.getSeconds()).padStart(2, "0");
599
+ return `${y}-${mo}-${d}T${h}:${mi}:${s}${sign}${hh}:${mm}`;
600
+ }
601
+ function lockPathFor(timelineDir, filename) {
602
+ return join2(timelineDir, `.${filename}.lock`);
603
+ }
604
+ function initEntry(timelineDir, entry) {
605
+ const filename = todayFilename();
606
+ const filePath = join2(timelineDir, filename);
607
+ const lockPath = lockPathFor(timelineDir, filename);
608
+ try {
609
+ const acquired = acquireLock(lockPath);
610
+ if (!acquired) {
611
+ log2.debug(`Timeline initEntry: could not acquire lock for ${filename}`);
612
+ return;
613
+ }
614
+ try {
615
+ appendFileSync(filePath, JSON.stringify(entry) + `
616
+ `);
617
+ } finally {
618
+ releaseLock(lockPath);
619
+ }
620
+ } catch (err) {
621
+ log2.debug("Timeline initEntry failed", err);
622
+ }
623
+ }
624
+ function updateEntry(timelineDir, taskId, updater) {
625
+ for (const filename of recentFilenames(7)) {
626
+ const filePath = join2(timelineDir, filename);
627
+ const lockPath = lockPathFor(timelineDir, filename);
628
+ try {
629
+ const acquired = acquireLock(lockPath);
630
+ if (!acquired) {
631
+ log2.debug(`Timeline updateEntry: lock held for ${filename}, skipping`);
632
+ continue;
633
+ }
634
+ try {
635
+ let content;
636
+ try {
637
+ content = readFileSync(filePath, "utf-8");
638
+ } catch {
639
+ continue;
640
+ }
641
+ const lines = content.trimEnd().split(`
642
+ `);
643
+ let found = false;
644
+ const updated = lines.map((line) => {
645
+ const entry = JSON.parse(line);
646
+ if (entry.task_id === taskId) {
647
+ found = true;
648
+ updater(entry);
649
+ }
650
+ return JSON.stringify(entry);
651
+ });
652
+ if (!found)
653
+ continue;
654
+ const tmpPath = join2(timelineDir, `.${filename}.tmp`);
655
+ writeFileSync(tmpPath, updated.join(`
656
+ `) + `
657
+ `);
658
+ renameSync(tmpPath, filePath);
659
+ return;
660
+ } finally {
661
+ releaseLock(lockPath);
662
+ }
663
+ } catch (err) {
664
+ log2.debug(`Timeline updateEntry failed for ${filename}`, err);
665
+ }
666
+ }
667
+ log2.debug(`Timeline updateEntry: task_id ${taskId} not found in last 7 days`);
668
+ }
669
+ function createTimelineEntry(taskId, prompt, type, sessionId, pid, provider, contextKey, detailedLog) {
670
+ return {
671
+ task_id: taskId,
672
+ context_key: contextKey ?? null,
673
+ session_id: sessionId || null,
674
+ pid: pid ?? null,
675
+ status: "running",
676
+ datetime: localISOString(),
677
+ type,
678
+ prompt,
679
+ agent_responses: [],
680
+ errmsg: null,
681
+ provider: provider ?? null,
682
+ detailed_log: detailedLog ?? null
683
+ };
684
+ }
685
+ var RESUME_MAX_AGE_MS = 72 * 60 * 60 * 1000;
686
+
687
+ // daemon/meeting-runner.ts
688
+ var log3 = createLogger({ module: "meeting-runner" });
689
+ var SCRAPE_INTERVAL_MS = 3000;
690
+ var DEFAULT_BOT_NAME = "Alook Meeting Bot";
691
+ var MAX_RETRY_DURATION_MS = 30 * 60 * 1000;
692
+ var RETRY_BACKOFF = [30000, 60000, 120000, 300000];
693
+ async function callbackWeb(input, status, transcript, error) {
694
+ const payload = JSON.stringify({
695
+ meetingId: input.meetingId,
696
+ workspaceId: input.workspaceId,
697
+ status,
698
+ transcript: transcript || undefined,
699
+ error: error || undefined
700
+ });
701
+ try {
702
+ const res = await fetch(`${input.callbackUrl}/api/meeting/callback`, {
703
+ method: "POST",
704
+ headers: {
705
+ "Content-Type": "application/json",
706
+ Authorization: `Bearer ${input.authToken}`
707
+ },
708
+ body: payload
709
+ });
710
+ log3.info(`callback ${status} → HTTP ${res.status}`, { meeting: input.meetingId });
711
+ } catch (err) {
712
+ log3.error(`callback failed: ${err instanceof Error ? err.message : err}`, { meeting: input.meetingId });
713
+ }
714
+ }
715
+ function launchBrowser(chromePath) {
716
+ return chromium.launch({
717
+ executablePath: chromePath,
718
+ headless: false,
719
+ args: [
720
+ "--lang=en-US",
721
+ "--disable-blink-features=AutomationControlled",
722
+ "--use-fake-ui-for-media-stream",
723
+ "--use-fake-device-for-media-stream",
724
+ "--disable-audio-output"
725
+ ]
726
+ });
727
+ }
728
+ async function tryJoinAndRecord(input, chromePath) {
729
+ const browser = await launchBrowser(chromePath);
730
+ const context = browser.contexts()[0];
731
+ if (context) {
732
+ await context.addInitScript(() => {
733
+ Object.defineProperty(navigator, "webdriver", { get: () => false });
734
+ });
735
+ }
736
+ const page = await browser.newPage({ locale: "en-US" });
737
+ const meetingStartMs = Date.now();
738
+ let transcript = [];
739
+ try {
740
+ const botName = input.agentName ? `${input.agentName} (Alook)` : DEFAULT_BOT_NAME;
741
+ await joinMeeting(page, input.meetingUrl, botName);
742
+ log3.info("joined meeting, waiting for UI ready...", { meeting: input.meetingId });
743
+ await waitForMeetingReady(page);
744
+ await page.evaluate(() => {
745
+ for (const btn of document.querySelectorAll("button")) {
746
+ const label = (btn.getAttribute("aria-label") || "").toLowerCase();
747
+ if (label.startsWith("turn off") && (label.includes("microphone") || label.includes("camera"))) {
748
+ btn.click();
749
+ }
750
+ }
751
+ });
752
+ log3.info("meeting ready, enabling captions...", { meeting: input.meetingId });
753
+ await enableCaptions(page);
754
+ await page.evaluate(buildCaptionObserverScript());
755
+ await page.evaluate(buildAloneDetectorScript());
756
+ log3.info("captions enabled, scraping loop started", { meeting: input.meetingId });
757
+ let scrapeCount = 0;
758
+ while (true) {
759
+ try {
760
+ const active = await isMeetingActive(page);
761
+ if (!active) {
762
+ const finalRaw = await page.evaluate(buildCaptionScrapeScript());
763
+ const finalCaptions = parseCaptionElements(finalRaw);
764
+ if (finalCaptions.length > 0) {
765
+ transcript = deduplicateCaptions(transcript, finalCaptions, meetingStartMs, Date.now());
766
+ log3.debug(`final scrape: ${finalCaptions.length} caption(s), total ${transcript.length}`, { meeting: input.meetingId });
767
+ }
768
+ log3.info("meeting ended (no longer active)", { meeting: input.meetingId });
769
+ break;
770
+ }
771
+ const rawElements = await page.evaluate(buildCaptionScrapeScript());
772
+ const captions = parseCaptionElements(rawElements);
773
+ scrapeCount++;
774
+ if (captions.length > 0) {
775
+ const prevLen = transcript.length;
776
+ transcript = deduplicateCaptions(transcript, captions, meetingStartMs, Date.now());
777
+ if (transcript.length > prevLen) {
778
+ log3.debug(`caption: ${captions[captions.length - 1].speaker}: "${captions[captions.length - 1].text}" (total ${transcript.length})`, { meeting: input.meetingId });
779
+ }
780
+ } else if (scrapeCount <= 5) {
781
+ log3.debug(`scrape #${scrapeCount}: no captions yet`, { meeting: input.meetingId });
782
+ }
783
+ } catch (err) {
784
+ log3.error(`scrape error: ${err instanceof Error ? err.message : err}`, { meeting: input.meetingId });
785
+ break;
786
+ }
787
+ await new Promise((resolve) => setTimeout(resolve, SCRAPE_INTERVAL_MS));
788
+ }
789
+ await leaveMeeting(page);
790
+ return { status: "completed", transcript };
791
+ } catch (err) {
792
+ const msg = err instanceof Error ? err.message : String(err);
793
+ if (msg.includes("Blocked from joining")) {
794
+ const screenshotPath = join3(tempDir("alook-meetings"), `meeting-${input.meetingId}-blocked.png`);
795
+ await page.screenshot({ path: screenshotPath }).catch(() => {});
796
+ log3.warn(`blocked from joining, screenshot saved: ${screenshotPath}`, { meeting: input.meetingId });
797
+ return { status: "blocked", transcript, error: msg };
798
+ }
799
+ log3.error(`unexpected error: ${msg}`, { meeting: input.meetingId });
800
+ return { status: "error", transcript, error: msg };
801
+ } finally {
802
+ await page.close().catch(() => {});
803
+ await browser.close().catch(() => {});
804
+ }
805
+ }
806
+ function writeTimeline(input, status, responses, errmsg) {
807
+ if (!input.timelineDir)
808
+ return;
809
+ try {
810
+ mkdirSync2(input.timelineDir, { recursive: true });
811
+ const taskId = `meeting-${input.meetingId}`;
812
+ if (status === "running") {
813
+ const meetingLabel = input.title || input.meetingUrl;
814
+ const entry = createTimelineEntry(taskId, `Meeting: ${meetingLabel} (participants: ${input.participants.join(", ")})`, "meeting", undefined, process.pid);
815
+ initEntry(input.timelineDir, entry);
816
+ } else {
817
+ updateEntry(input.timelineDir, taskId, (entry) => {
818
+ entry.status = status;
819
+ entry.pid = null;
820
+ if (responses)
821
+ entry.agent_responses = responses;
822
+ if (errmsg)
823
+ entry.errmsg = errmsg;
824
+ });
825
+ }
826
+ } catch (err) {
827
+ log3.debug(`timeline write failed: ${err instanceof Error ? err.message : err}`);
828
+ }
829
+ }
830
+ async function run(input) {
831
+ log3.info(`starting meeting: url=${input.meetingUrl}`, { meeting: input.meetingId, workspace: input.workspaceId });
832
+ writeTimeline(input, "running");
833
+ let chromePath;
834
+ try {
835
+ chromePath = ensureChrome();
836
+ log3.debug(`chrome found: ${chromePath}`);
837
+ } catch (err) {
838
+ const msg = err instanceof Error ? err.message : String(err);
839
+ log3.error(`chrome setup failed: ${msg}`, { meeting: input.meetingId });
840
+ writeTimeline(input, "failed", undefined, `Chrome setup failed: ${msg}`);
841
+ await callbackWeb(input, "failed", undefined, `Chrome setup failed: ${msg}`);
842
+ process.exit(1);
843
+ }
844
+ const startTime = Date.now();
845
+ let attempt = 0;
846
+ while (true) {
847
+ attempt++;
848
+ log3.info(attempt > 1 ? `retry attempt #${attempt}` : "launching browser (en-US, stealth)...", { meeting: input.meetingId });
849
+ const result = await tryJoinAndRecord(input, chromePath);
850
+ if (result.status === "completed") {
851
+ const transcriptText = formatTranscript(result.transcript);
852
+ log3.info(`completed: ${result.transcript.length} transcript entries`, { meeting: input.meetingId });
853
+ const transcriptR2Key = `meetings/${input.meetingId}/transcript`;
854
+ writeTimeline(input, "completed", [
855
+ `Meeting completed. ${result.transcript.length} transcript entries captured.`,
856
+ `Transcript stored at: ${transcriptR2Key}`
857
+ ]);
858
+ await callbackWeb(input, "completed", transcriptText);
859
+ return;
860
+ }
861
+ if (result.status === "blocked") {
862
+ const elapsed = Date.now() - startTime;
863
+ if (elapsed >= MAX_RETRY_DURATION_MS) {
864
+ log3.warn(`giving up after ${Math.round(elapsed / 60000)}min of retries`, { meeting: input.meetingId });
865
+ writeTimeline(input, "failed", undefined, result.error);
866
+ await callbackWeb(input, "failed", undefined, result.error);
867
+ return;
868
+ }
869
+ const backoff = RETRY_BACKOFF[Math.min(attempt - 1, RETRY_BACKOFF.length - 1)];
870
+ log3.info(`blocked, retrying in ${backoff / 1000}s (attempt=${attempt}, elapsed=${Math.round(elapsed / 60000)}min)`, { meeting: input.meetingId });
871
+ await new Promise((resolve) => setTimeout(resolve, backoff));
872
+ continue;
873
+ }
874
+ writeTimeline(input, "failed", undefined, result.error);
875
+ await callbackWeb(input, "failed", undefined, result.error);
876
+ return;
877
+ }
878
+ }
879
+ var encoded = process.argv[2];
880
+ if (!encoded) {
881
+ console.error("Usage: meeting-runner <base64-encoded-input>");
882
+ process.exit(1);
883
+ }
884
+ var input = JSON.parse(Buffer.from(encoded, "base64").toString("utf-8"));
885
+ run(input).then(() => process.exit(0)).catch((err) => {
886
+ log3.error(`fatal: ${err instanceof Error ? err.message : err}`);
887
+ process.exit(1);
888
+ });