ai-browser-bridge 0.4.0 → 0.5.2
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/README.md +30 -0
- package/dist/bridge.js +1148 -186
- package/dist/bridge.js.map +1 -1
- package/package.json +1 -1
package/dist/bridge.js
CHANGED
|
@@ -257,9 +257,9 @@ var ProviderConfig = Config.string("BRIDGE_PROVIDER").pipe(
|
|
|
257
257
|
|
|
258
258
|
// src/features/terminal/cliOperations.ts
|
|
259
259
|
import { execFile as execFile3 } from "child_process";
|
|
260
|
-
import { mkdir as
|
|
261
|
-
import { basename as basename3, dirname as
|
|
262
|
-
import { Schema as
|
|
260
|
+
import { mkdir as mkdir8, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
261
|
+
import { basename as basename3, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute3, join as join12, relative as relative3, resolve as resolve8 } from "path";
|
|
262
|
+
import { Schema as Schema9 } from "effect";
|
|
263
263
|
import { render } from "ink";
|
|
264
264
|
import React from "react";
|
|
265
265
|
|
|
@@ -289,89 +289,6 @@ var isSameChatGptConversation = (pageUrl, targetIdOrUrl) => {
|
|
|
289
289
|
return targetId === currentId;
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
-
// src/features/providers/chatgpt/chatgptPage.ts
|
|
293
|
-
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
294
|
-
import { homedir } from "os";
|
|
295
|
-
import path from "path";
|
|
296
|
-
|
|
297
|
-
// src/features/providers/providerErrors.ts
|
|
298
|
-
import { Data } from "effect";
|
|
299
|
-
var UnknownProviderError = class extends Data.TaggedError("UnknownProviderError") {
|
|
300
|
-
get message() {
|
|
301
|
-
return `Unknown provider "${this.value}". Valid providers: ${this.validProviders.join(", ")}.`;
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
var GuestSessionError = class extends Data.TaggedError("GuestSessionError") {
|
|
305
|
-
get message() {
|
|
306
|
-
return `${this.providerId} is not signed in: ${this.reason}`;
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// src/features/providers/renderStallWatchdog.ts
|
|
311
|
-
var stallReloadWatchdogFor = (options = {}) => {
|
|
312
|
-
const stallMs = options.stallMs === void 0 ? RENDER_STALL_RELOAD_MS : options.stallMs;
|
|
313
|
-
const maxReloads = options.maxReloads === void 0 ? MAX_STALL_RELOADS : options.maxReloads;
|
|
314
|
-
const now = options.now === void 0 ? Date.now : options.now;
|
|
315
|
-
let lastProgressAt = now();
|
|
316
|
-
let reloadsUsed = 0;
|
|
317
|
-
return {
|
|
318
|
-
noteProgress() {
|
|
319
|
-
lastProgressAt = now();
|
|
320
|
-
},
|
|
321
|
-
async maybeReload(page) {
|
|
322
|
-
if (reloadsUsed >= maxReloads) return false;
|
|
323
|
-
if (now() - lastProgressAt < stallMs) return false;
|
|
324
|
-
try {
|
|
325
|
-
await page.reload({ waitUntil: "domcontentloaded" });
|
|
326
|
-
} catch {
|
|
327
|
-
}
|
|
328
|
-
if (options.waitAfterReload !== void 0) {
|
|
329
|
-
try {
|
|
330
|
-
await options.waitAfterReload(page);
|
|
331
|
-
} catch {
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
reloadsUsed += 1;
|
|
335
|
-
lastProgressAt = now();
|
|
336
|
-
if (options.onReload !== void 0) {
|
|
337
|
-
options.onReload(reloadsUsed);
|
|
338
|
-
}
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
// src/features/providers/streamingGuard.ts
|
|
345
|
-
var IDLE_POLL_MS = 300;
|
|
346
|
-
var IDLE_CONFIRMATIONS = 2;
|
|
347
|
-
var DEFAULT_IDLE_TIMEOUT_MS = 3e5;
|
|
348
|
-
var isResponseGenerating = async (page, stopSelector) => {
|
|
349
|
-
if (stopSelector.length === 0) return false;
|
|
350
|
-
try {
|
|
351
|
-
return await page.locator(stopSelector).first().isVisible();
|
|
352
|
-
} catch {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
var waitForResponseIdle = async (page, stopSelector, timeoutMs = DEFAULT_IDLE_TIMEOUT_MS) => {
|
|
357
|
-
if (stopSelector.length === 0) return;
|
|
358
|
-
const startedAt = Date.now();
|
|
359
|
-
let clearStreak = 0;
|
|
360
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
361
|
-
if (await isResponseGenerating(page, stopSelector)) {
|
|
362
|
-
clearStreak = 0;
|
|
363
|
-
} else {
|
|
364
|
-
clearStreak += 1;
|
|
365
|
-
if (clearStreak >= IDLE_CONFIRMATIONS) return;
|
|
366
|
-
}
|
|
367
|
-
try {
|
|
368
|
-
await page.waitForTimeout(IDLE_POLL_MS);
|
|
369
|
-
} catch {
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
throw new Error("Timed out waiting for the current response to finish before sending.");
|
|
373
|
-
};
|
|
374
|
-
|
|
375
292
|
// src/features/providers/chatgpt/chatgptConversationSearch.ts
|
|
376
293
|
var CHATGPT_SEARCH = {
|
|
377
294
|
trigger: 'button[aria-label="Search"]',
|
|
@@ -420,19 +337,40 @@ var openChatSearch = async (page) => {
|
|
|
420
337
|
return dialog;
|
|
421
338
|
};
|
|
422
339
|
var searchEntriesIn = async (dialog) => {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
340
|
+
return dialog.locator(CHATGPT_SEARCH.resultLink).evaluateAll(
|
|
341
|
+
(links, titleSelector) => links.flatMap((link) => {
|
|
342
|
+
const href = link.getAttribute("href");
|
|
343
|
+
if (href === null) return [];
|
|
344
|
+
const titleNode = link.querySelector(titleSelector);
|
|
345
|
+
const fallbackTitle = link instanceof HTMLElement ? link.innerText.split("\n")[0] : "";
|
|
346
|
+
let rawTitle = fallbackTitle;
|
|
347
|
+
if (titleNode !== null && titleNode.textContent !== null) rawTitle = titleNode.textContent;
|
|
348
|
+
if (rawTitle === void 0) rawTitle = "";
|
|
349
|
+
const title = rawTitle.trim();
|
|
350
|
+
return title ? [{ href, title }] : [];
|
|
351
|
+
}),
|
|
352
|
+
CHATGPT_SEARCH.resultTitle
|
|
353
|
+
);
|
|
432
354
|
};
|
|
433
355
|
var searchEntriesSignature = async (dialog) => {
|
|
434
356
|
return JSON.stringify(await searchEntriesIn(dialog));
|
|
435
357
|
};
|
|
358
|
+
var acknowledgeChatGptHistoryRateLimit = async (page) => {
|
|
359
|
+
if (page.isClosed()) return false;
|
|
360
|
+
const modal = page.locator(CHATGPT_SEARCH.rateLimit).first();
|
|
361
|
+
if (!await modal.isVisible()) return false;
|
|
362
|
+
const acknowledge = modal.getByRole("button", { name: "Got it", exact: true }).first();
|
|
363
|
+
if (await acknowledge.isVisible()) {
|
|
364
|
+
await acknowledge.evaluate((button) => {
|
|
365
|
+
if (button instanceof HTMLElement) button.click();
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
await modal.waitFor({ state: "hidden", timeout: 3e3 });
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
return true;
|
|
373
|
+
};
|
|
436
374
|
var assertSearchAvailable = async (page) => {
|
|
437
375
|
if (!await page.locator(CHATGPT_SEARCH.rateLimit).isVisible()) return;
|
|
438
376
|
throw new Error(
|
|
@@ -528,6 +466,89 @@ var searchChatGptConversations = async (page, input) => {
|
|
|
528
466
|
}
|
|
529
467
|
};
|
|
530
468
|
|
|
469
|
+
// src/features/providers/chatgpt/chatgptPage.ts
|
|
470
|
+
import { mkdir, readFile, stat, writeFile } from "fs/promises";
|
|
471
|
+
import { homedir } from "os";
|
|
472
|
+
import path from "path";
|
|
473
|
+
|
|
474
|
+
// src/features/providers/providerErrors.ts
|
|
475
|
+
import { Data } from "effect";
|
|
476
|
+
var UnknownProviderError = class extends Data.TaggedError("UnknownProviderError") {
|
|
477
|
+
get message() {
|
|
478
|
+
return `Unknown provider "${this.value}". Valid providers: ${this.validProviders.join(", ")}.`;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var GuestSessionError = class extends Data.TaggedError("GuestSessionError") {
|
|
482
|
+
get message() {
|
|
483
|
+
return `${this.providerId} is not signed in: ${this.reason}`;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// src/features/providers/renderStallWatchdog.ts
|
|
488
|
+
var stallReloadWatchdogFor = (options = {}) => {
|
|
489
|
+
const stallMs = options.stallMs === void 0 ? RENDER_STALL_RELOAD_MS : options.stallMs;
|
|
490
|
+
const maxReloads = options.maxReloads === void 0 ? MAX_STALL_RELOADS : options.maxReloads;
|
|
491
|
+
const now = options.now === void 0 ? Date.now : options.now;
|
|
492
|
+
let lastProgressAt = now();
|
|
493
|
+
let reloadsUsed = 0;
|
|
494
|
+
return {
|
|
495
|
+
noteProgress() {
|
|
496
|
+
lastProgressAt = now();
|
|
497
|
+
},
|
|
498
|
+
async maybeReload(page) {
|
|
499
|
+
if (reloadsUsed >= maxReloads) return false;
|
|
500
|
+
if (now() - lastProgressAt < stallMs) return false;
|
|
501
|
+
try {
|
|
502
|
+
await page.reload({ waitUntil: "domcontentloaded" });
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
if (options.waitAfterReload !== void 0) {
|
|
506
|
+
try {
|
|
507
|
+
await options.waitAfterReload(page);
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
reloadsUsed += 1;
|
|
512
|
+
lastProgressAt = now();
|
|
513
|
+
if (options.onReload !== void 0) {
|
|
514
|
+
options.onReload(reloadsUsed);
|
|
515
|
+
}
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// src/features/providers/streamingGuard.ts
|
|
522
|
+
var IDLE_POLL_MS = 300;
|
|
523
|
+
var IDLE_CONFIRMATIONS = 2;
|
|
524
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 3e5;
|
|
525
|
+
var isResponseGenerating = async (page, stopSelector) => {
|
|
526
|
+
if (stopSelector.length === 0) return false;
|
|
527
|
+
try {
|
|
528
|
+
return await page.locator(stopSelector).first().isVisible();
|
|
529
|
+
} catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
var waitForResponseIdle = async (page, stopSelector, timeoutMs = DEFAULT_IDLE_TIMEOUT_MS) => {
|
|
534
|
+
if (stopSelector.length === 0) return;
|
|
535
|
+
const startedAt = Date.now();
|
|
536
|
+
let clearStreak = 0;
|
|
537
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
538
|
+
if (await isResponseGenerating(page, stopSelector)) {
|
|
539
|
+
clearStreak = 0;
|
|
540
|
+
} else {
|
|
541
|
+
clearStreak += 1;
|
|
542
|
+
if (clearStreak >= IDLE_CONFIRMATIONS) return;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
await page.waitForTimeout(IDLE_POLL_MS);
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
throw new Error("Timed out waiting for the current response to finish before sending.");
|
|
550
|
+
};
|
|
551
|
+
|
|
531
552
|
// src/features/providers/chatgpt/chatgptPage.ts
|
|
532
553
|
var EDIT_BUTTON_SELECTORS = [
|
|
533
554
|
'button[data-testid="edit-turn-button"]',
|
|
@@ -1835,8 +1856,8 @@ var deleteConnectorAppBySummary = async (ctx) => {
|
|
|
1835
1856
|
if (entry.name !== ctx.target.name) continue;
|
|
1836
1857
|
await entry.button.click({ timeout: 3e3, force: true });
|
|
1837
1858
|
await ctx.page.waitForTimeout(1e3);
|
|
1838
|
-
const
|
|
1839
|
-
if (!
|
|
1859
|
+
const open2 = await readOpenConnectorSummary({ page: ctx.page });
|
|
1860
|
+
if (!open2 || !sameConnectorApp({ a: open2, b: ctx.target })) continue;
|
|
1840
1861
|
return deleteOpenConnectorIfPresent({ page: ctx.page });
|
|
1841
1862
|
}
|
|
1842
1863
|
}
|
|
@@ -2414,19 +2435,122 @@ var newConversation = async (page) => {
|
|
|
2414
2435
|
await page.goto("https://chatgpt.com/");
|
|
2415
2436
|
await page.waitForSelector("#prompt-textarea, [contenteditable]", { timeout: 3e4 });
|
|
2416
2437
|
};
|
|
2438
|
+
var parseChatGptSidebarConversationLink = (link, options = {}) => {
|
|
2439
|
+
const href = link.href === null ? "" : link.href.trim();
|
|
2440
|
+
const title = link.title.trim();
|
|
2441
|
+
if (!href || !title) return null;
|
|
2442
|
+
const sidebarLabel = link.ariaLabel === null ? "" : link.ariaLabel;
|
|
2443
|
+
const outsideLooseChats = href.includes("/g/") || /\bchat in project\b/i.test(sidebarLabel) || /\bpinned conversation\b/i.test(sidebarLabel);
|
|
2444
|
+
if (options.orphans && outsideLooseChats) return null;
|
|
2445
|
+
const parsedUrl = new URL(href, "https://chatgpt.com");
|
|
2446
|
+
const pathParts = parsedUrl.pathname.split("/").filter(Boolean);
|
|
2447
|
+
const lastPathPart = pathParts.pop();
|
|
2448
|
+
const id2 = lastPathPart === void 0 ? "" : lastPathPart;
|
|
2449
|
+
if (!id2) return null;
|
|
2450
|
+
return { id: id2, title, url: parsedUrl.toString() };
|
|
2451
|
+
};
|
|
2417
2452
|
var parseSidebarLink = async (ctx) => {
|
|
2418
2453
|
const href = await ctx.link.getAttribute("href");
|
|
2419
2454
|
const title = await ctx.link.innerText();
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2455
|
+
const ariaLabel = await ctx.link.getAttribute("aria-label");
|
|
2456
|
+
return parseChatGptSidebarConversationLink({ href, title, ariaLabel }, { orphans: ctx.orphans });
|
|
2457
|
+
};
|
|
2458
|
+
var scanOrphanSidebarConversations = async (page) => {
|
|
2459
|
+
await page.locator(`${SELECTORS.sidebarConversation}:not([aria-label*="pinned conversation" i])`).first().waitFor({ state: "attached", timeout: 1e4 }).catch(() => {
|
|
2460
|
+
});
|
|
2461
|
+
const sidebar = page.locator('nav[aria-label="Chat history"]').first();
|
|
2462
|
+
if (await sidebar.count() === 0) return [];
|
|
2463
|
+
const collectScrollAndReadState = (element) => {
|
|
2464
|
+
const scan = element.__bridgeOrphanScan;
|
|
2465
|
+
if (!scan) throw new Error("ChatGPT sidebar scan was not initialized.");
|
|
2466
|
+
const sidebarRect = element.getBoundingClientRect();
|
|
2467
|
+
for (const link of element.querySelectorAll('a[href^="/c/"]')) {
|
|
2468
|
+
const linkRect = link.getBoundingClientRect();
|
|
2469
|
+
if (linkRect.bottom < sidebarRect.top || linkRect.top > sidebarRect.bottom) continue;
|
|
2470
|
+
const href = link.getAttribute("href");
|
|
2471
|
+
if (!href) continue;
|
|
2472
|
+
let title = link.innerText;
|
|
2473
|
+
if (!title) {
|
|
2474
|
+
const textContent = link.textContent;
|
|
2475
|
+
title = textContent === null ? "" : textContent;
|
|
2476
|
+
}
|
|
2477
|
+
scan.links.set(href, {
|
|
2478
|
+
href,
|
|
2479
|
+
title,
|
|
2480
|
+
ariaLabel: link.getAttribute("aria-label")
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
const state = {
|
|
2484
|
+
size: scan.links.size,
|
|
2485
|
+
scrollHeight: element.scrollHeight,
|
|
2486
|
+
atBottom: element.scrollTop + element.clientHeight >= element.scrollHeight - 4
|
|
2487
|
+
};
|
|
2488
|
+
const step = Math.max(Math.floor(element.clientHeight * 0.8), 320);
|
|
2489
|
+
element.scrollTop = Math.min(element.scrollTop + step, element.scrollHeight);
|
|
2490
|
+
return state;
|
|
2491
|
+
};
|
|
2492
|
+
await sidebar.evaluate((element) => {
|
|
2493
|
+
element.__bridgeOrphanScan = {
|
|
2494
|
+
originalScrollTop: element.scrollTop,
|
|
2495
|
+
links: /* @__PURE__ */ new Map()
|
|
2496
|
+
};
|
|
2497
|
+
element.scrollTop = 0;
|
|
2498
|
+
});
|
|
2499
|
+
await new Promise((resolve9) => setTimeout(resolve9, 250));
|
|
2500
|
+
let stableBottomPasses = 0;
|
|
2501
|
+
let links = [];
|
|
2502
|
+
try {
|
|
2503
|
+
let reachedStableBottom = false;
|
|
2504
|
+
let before = await sidebar.evaluate(collectScrollAndReadState);
|
|
2505
|
+
for (let pass = 0; pass < 240; pass += 1) {
|
|
2506
|
+
await new Promise((resolve9) => setTimeout(resolve9, 300));
|
|
2507
|
+
const after = await sidebar.evaluate(collectScrollAndReadState);
|
|
2508
|
+
const unchanged = after.atBottom && after.size === before.size && after.scrollHeight <= before.scrollHeight + 1;
|
|
2509
|
+
stableBottomPasses = unchanged ? stableBottomPasses + 1 : 0;
|
|
2510
|
+
if (stableBottomPasses >= 8) {
|
|
2511
|
+
reachedStableBottom = true;
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2514
|
+
before = after;
|
|
2515
|
+
}
|
|
2516
|
+
if (!reachedStableBottom) {
|
|
2517
|
+
throw new Error("ChatGPT sidebar scan did not reach the end of the chat history.");
|
|
2518
|
+
}
|
|
2519
|
+
links = await sidebar.evaluate((element) => {
|
|
2520
|
+
const scan = element.__bridgeOrphanScan;
|
|
2521
|
+
return scan ? [...scan.links.values()] : [];
|
|
2522
|
+
});
|
|
2523
|
+
} finally {
|
|
2524
|
+
await sidebar.evaluate((element) => {
|
|
2525
|
+
const scan = element.__bridgeOrphanScan;
|
|
2526
|
+
if (scan) element.scrollTop = scan.originalScrollTop;
|
|
2527
|
+
delete element.__bridgeOrphanScan;
|
|
2528
|
+
}).catch(() => {
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
2532
|
+
for (const link of links) {
|
|
2533
|
+
const conversation = parseChatGptSidebarConversationLink(link, { orphans: true });
|
|
2534
|
+
if (conversation) conversations.set(conversation.id, conversation);
|
|
2535
|
+
}
|
|
2536
|
+
return [...conversations.values()];
|
|
2537
|
+
};
|
|
2538
|
+
var readAllOrphanSidebarConversations = async (page) => {
|
|
2539
|
+
const scanPage = await page.context().newPage();
|
|
2540
|
+
try {
|
|
2541
|
+
await scanPage.goto("https://chatgpt.com/", { waitUntil: "domcontentloaded" });
|
|
2542
|
+
return await scanOrphanSidebarConversations(scanPage);
|
|
2543
|
+
} finally {
|
|
2544
|
+
await scanPage.close().catch(() => {
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2424
2547
|
};
|
|
2425
|
-
var readSidebarConversations = async (page) => {
|
|
2548
|
+
var readSidebarConversations = async (page, options = {}) => {
|
|
2549
|
+
if (options.orphans) return readAllOrphanSidebarConversations(page);
|
|
2426
2550
|
const links = await page.locator(SELECTORS.sidebarConversation).all();
|
|
2427
2551
|
const conversations = [];
|
|
2428
2552
|
for (const link of links) {
|
|
2429
|
-
const entry = await parseSidebarLink({ link });
|
|
2553
|
+
const entry = await parseSidebarLink({ link, orphans: Boolean(options.orphans) });
|
|
2430
2554
|
if (entry) conversations.push(entry);
|
|
2431
2555
|
}
|
|
2432
2556
|
return conversations;
|
|
@@ -3076,10 +3200,20 @@ var WORKSPACE = {
|
|
|
3076
3200
|
newProjectButton: 'button[aria-label="New project"]',
|
|
3077
3201
|
projectNameInput: 'input[name="projectName"]',
|
|
3078
3202
|
projectFolderIcon: '[data-testid="project-folder-icon"]',
|
|
3079
|
-
|
|
3203
|
+
projectDirectoryRow: '[data-testid="project-directory-scroll-root"] [role="row"]:has([role="gridcell"])',
|
|
3204
|
+
projectConversationLink: 'main a[href*="/c/"]',
|
|
3205
|
+
chatLink: 'nav a[href*="/c/"]',
|
|
3080
3206
|
activeConversationMenu: '[data-testid="conversation-options-button"]',
|
|
3081
3207
|
menuItem: '[role="menuitem"]'
|
|
3082
3208
|
};
|
|
3209
|
+
var projectNameFromDirectoryRowText = (text) => {
|
|
3210
|
+
const [name, detail] = text.split("\n");
|
|
3211
|
+
if (name === void 0) return null;
|
|
3212
|
+
const projectName = name.trim();
|
|
3213
|
+
if (!projectName) return null;
|
|
3214
|
+
if (projectName === "Name" && detail !== void 0 && detail.trim() === "Modified") return null;
|
|
3215
|
+
return projectName;
|
|
3216
|
+
};
|
|
3083
3217
|
var PROJECT_ROWS_SOURCE = String.raw`
|
|
3084
3218
|
(() => {
|
|
3085
3219
|
const clean = (s) => (s || "").replace(/\s+/g, " ").trim();
|
|
@@ -3121,13 +3255,146 @@ var chatGptProjectRemovalState = (namedRemoval, genericRemoval) => {
|
|
|
3121
3255
|
if (genericRemoval) return "current-project-unknown";
|
|
3122
3256
|
return "not-filed";
|
|
3123
3257
|
};
|
|
3258
|
+
var chatGptProjectNameFromConversationAriaLabel = (label) => {
|
|
3259
|
+
const marker = ", chat in project ";
|
|
3260
|
+
const markerIndex = label.lastIndexOf(marker);
|
|
3261
|
+
if (markerIndex < 0) return null;
|
|
3262
|
+
const project = label.slice(markerIndex + marker.length).trim();
|
|
3263
|
+
if (!project) return null;
|
|
3264
|
+
return project;
|
|
3265
|
+
};
|
|
3124
3266
|
var listProjects = async (page) => {
|
|
3125
3267
|
await ensureOnProjectsPage(page);
|
|
3268
|
+
const directoryRows = page.locator(WORKSPACE.projectDirectoryRow);
|
|
3269
|
+
await directoryRows.first().waitFor({ timeout: 5e3 }).catch(() => {
|
|
3270
|
+
});
|
|
3271
|
+
const directoryNames = (await directoryRows.allInnerTexts()).map(projectNameFromDirectoryRowText).filter((name) => name !== null);
|
|
3272
|
+
if (directoryNames.length > 0) {
|
|
3273
|
+
return Array.from(new Set(directoryNames)).map((name) => ({ name }));
|
|
3274
|
+
}
|
|
3126
3275
|
await page.locator(WORKSPACE.projectFolderIcon).first().waitFor({ timeout: 5e3 }).catch(() => {
|
|
3127
3276
|
});
|
|
3128
3277
|
const names = await page.evaluate(PROJECT_ROWS_SOURCE);
|
|
3129
3278
|
return names.map((name) => ({ name }));
|
|
3130
3279
|
};
|
|
3280
|
+
var openProject = async (page, project) => {
|
|
3281
|
+
await ensureOnProjectsPage(page);
|
|
3282
|
+
const directoryRows = page.locator(WORKSPACE.projectDirectoryRow);
|
|
3283
|
+
await directoryRows.first().waitFor({ state: "visible", timeout: 8e3 }).catch(() => {
|
|
3284
|
+
});
|
|
3285
|
+
const directoryIndex = (await directoryRows.allInnerTexts()).findIndex(
|
|
3286
|
+
(rowText) => projectNameFromDirectoryRowText(rowText) === project
|
|
3287
|
+
);
|
|
3288
|
+
if (directoryIndex >= 0) {
|
|
3289
|
+
await directoryRows.nth(directoryIndex).evaluate((row) => {
|
|
3290
|
+
if (row instanceof HTMLElement) row.click();
|
|
3291
|
+
});
|
|
3292
|
+
const heading = page.locator("main").getByText(project, { exact: true }).first();
|
|
3293
|
+
try {
|
|
3294
|
+
await heading.waitFor({ state: "visible", timeout: 8e3 });
|
|
3295
|
+
return true;
|
|
3296
|
+
} catch {
|
|
3297
|
+
return false;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
try {
|
|
3301
|
+
await page.locator(WORKSPACE.projectFolderIcon).first().waitFor({
|
|
3302
|
+
state: "visible",
|
|
3303
|
+
timeout: 8e3
|
|
3304
|
+
});
|
|
3305
|
+
} catch {
|
|
3306
|
+
return false;
|
|
3307
|
+
}
|
|
3308
|
+
const projectRows = page.locator(WORKSPACE.projectFolderIcon).locator('xpath=ancestor::*[@role="row" or self::a or self::li][1]');
|
|
3309
|
+
const rowCount = await projectRows.count();
|
|
3310
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
3311
|
+
const projectRow = projectRows.nth(index);
|
|
3312
|
+
const [projectName] = (await projectRow.innerText()).split("\n");
|
|
3313
|
+
if (projectName?.trim() !== project) continue;
|
|
3314
|
+
await projectRow.evaluate((row) => {
|
|
3315
|
+
if (row instanceof HTMLElement) row.click();
|
|
3316
|
+
});
|
|
3317
|
+
const heading = page.locator("main").getByText(project, { exact: true }).first();
|
|
3318
|
+
try {
|
|
3319
|
+
await heading.waitFor({ state: "visible", timeout: 8e3 });
|
|
3320
|
+
return true;
|
|
3321
|
+
} catch {
|
|
3322
|
+
return false;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
return false;
|
|
3326
|
+
};
|
|
3327
|
+
var projectPageContainsConversation = async (page, conversation) => {
|
|
3328
|
+
const identifier = stripConversationId(conversation);
|
|
3329
|
+
const identifierIsId = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(identifier);
|
|
3330
|
+
for (let poll = 0; poll < 50; poll += 1) {
|
|
3331
|
+
const found = await page.locator(WORKSPACE.projectConversationLink).evaluateAll(
|
|
3332
|
+
(links, target) => {
|
|
3333
|
+
for (const link of links) {
|
|
3334
|
+
const href = link.getAttribute("href");
|
|
3335
|
+
if (href !== null && target.identifierIsId && href.includes(`/c/${target.identifier}`)) {
|
|
3336
|
+
return true;
|
|
3337
|
+
}
|
|
3338
|
+
const text = link instanceof HTMLElement ? link.innerText : link.textContent;
|
|
3339
|
+
let normalizedText = "";
|
|
3340
|
+
if (text !== null) normalizedText = text;
|
|
3341
|
+
const title = normalizedText.split("\n")[0]?.trim();
|
|
3342
|
+
if (!target.identifierIsId && title === target.identifier) return true;
|
|
3343
|
+
}
|
|
3344
|
+
return false;
|
|
3345
|
+
},
|
|
3346
|
+
{ identifier, identifierIsId }
|
|
3347
|
+
);
|
|
3348
|
+
if (found) return true;
|
|
3349
|
+
const loadMore = page.getByText("Load more conversations", { exact: true }).first();
|
|
3350
|
+
if (!await loadMore.isVisible()) {
|
|
3351
|
+
if (poll >= 19) return false;
|
|
3352
|
+
await page.waitForTimeout(500);
|
|
3353
|
+
continue;
|
|
3354
|
+
}
|
|
3355
|
+
await loadMore.evaluate((label) => {
|
|
3356
|
+
let clickTarget = label;
|
|
3357
|
+
const button = label.closest("button");
|
|
3358
|
+
if (button !== null) clickTarget = button;
|
|
3359
|
+
if (clickTarget instanceof HTMLElement) clickTarget.click();
|
|
3360
|
+
});
|
|
3361
|
+
await page.waitForTimeout(700);
|
|
3362
|
+
}
|
|
3363
|
+
return false;
|
|
3364
|
+
};
|
|
3365
|
+
var projectContainsConversation = async (page, project, conversation) => {
|
|
3366
|
+
const originalUrl = page.url();
|
|
3367
|
+
try {
|
|
3368
|
+
if (!await openProject(page, project)) return false;
|
|
3369
|
+
await page.waitForTimeout(800);
|
|
3370
|
+
return await projectPageContainsConversation(page, conversation);
|
|
3371
|
+
} finally {
|
|
3372
|
+
if (page.url() !== originalUrl) await gotoStable(page, originalUrl);
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
var conversationLinkIdentifiesProject = async (page, conversation, project) => {
|
|
3376
|
+
const identifier = stripConversationId(conversation);
|
|
3377
|
+
const identifierIsId = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(identifier);
|
|
3378
|
+
let links = page.locator(WORKSPACE.chatLink);
|
|
3379
|
+
if (identifierIsId) links = page.locator(`nav a[href*="/c/${identifier}"]`);
|
|
3380
|
+
else links = links.filter({ hasText: exactName(identifier) });
|
|
3381
|
+
const count = await links.count();
|
|
3382
|
+
for (let index = 0; index < count; index += 1) {
|
|
3383
|
+
const ariaLabel = await links.nth(index).getAttribute("aria-label");
|
|
3384
|
+
if (ariaLabel === null) continue;
|
|
3385
|
+
if (chatGptProjectNameFromConversationAriaLabel(ariaLabel) === project) return true;
|
|
3386
|
+
}
|
|
3387
|
+
return false;
|
|
3388
|
+
};
|
|
3389
|
+
var activeConversationHeaderIdentifiesProject = async (page, project) => {
|
|
3390
|
+
if (!/\/g\/g-p-[^/]+\/c\//i.test(page.url())) return false;
|
|
3391
|
+
try {
|
|
3392
|
+
await page.locator("header").getByText(project, { exact: true }).first().waitFor({ state: "visible", timeout: 2e3 });
|
|
3393
|
+
return true;
|
|
3394
|
+
} catch {
|
|
3395
|
+
return false;
|
|
3396
|
+
}
|
|
3397
|
+
};
|
|
3131
3398
|
var gotoStable = async (page, url) => {
|
|
3132
3399
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3133
3400
|
try {
|
|
@@ -3277,38 +3544,65 @@ var deleteProject = async (page, project) => {
|
|
|
3277
3544
|
};
|
|
3278
3545
|
var moveChatToProject = async (page, input) => {
|
|
3279
3546
|
const base = { chat: input.chat, project: input.project, moved: false };
|
|
3547
|
+
const conversationId = stripConversationId(input.chat);
|
|
3548
|
+
const identifierIsId = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(conversationId);
|
|
3549
|
+
if (identifierIsId && chatGptConversationIdFromUrl(page.url()) !== conversationId) {
|
|
3550
|
+
await gotoStable(page, `https://chatgpt.com/c/${conversationId}`);
|
|
3551
|
+
await waitForActiveConversationMenu(page, conversationId);
|
|
3552
|
+
}
|
|
3553
|
+
if (await activeConversationHeaderIdentifiesProject(page, input.project) || await conversationLinkIdentifiesProject(page, input.chat, input.project)) {
|
|
3554
|
+
return {
|
|
3555
|
+
...base,
|
|
3556
|
+
alreadyFiled: true,
|
|
3557
|
+
reason: `already in project "${input.project}"`
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3280
3560
|
if (!await openConversationMenu(page, input.chat)) {
|
|
3281
3561
|
return { ...base, reason: "could not open the chat \u22EF menu" };
|
|
3282
3562
|
}
|
|
3283
3563
|
const visibleMenuItems2 = page.locator(`${WORKSPACE.menuItem}:visible`);
|
|
3284
3564
|
const moveItem = visibleMenuItems2.filter({ hasText: /^Move to project$/i }).first();
|
|
3285
|
-
|
|
3565
|
+
try {
|
|
3566
|
+
await moveItem.waitFor({ state: "visible", timeout: 5e3 });
|
|
3567
|
+
} catch {
|
|
3286
3568
|
await dismissMenu(page);
|
|
3287
3569
|
return { ...base, reason: "no 'Move to project' option (GPT- or project-owned chat)" };
|
|
3288
3570
|
}
|
|
3289
3571
|
const targetOf = () => page.locator(`${WORKSPACE.menuItem}:visible`).filter({ hasText: exactName(input.project) }).first();
|
|
3572
|
+
const removalStateOf = async () => {
|
|
3573
|
+
const namedRemoval = page.locator(`${WORKSPACE.menuItem}:visible`).filter({
|
|
3574
|
+
hasText: new RegExp(`^Remove from ${escapeRegExp(input.project)}$`, "i")
|
|
3575
|
+
});
|
|
3576
|
+
const genericRemoval = page.locator(`${WORKSPACE.menuItem}:visible`).filter({ hasText: /^Remove from project$/i });
|
|
3577
|
+
return chatGptProjectRemovalState(
|
|
3578
|
+
await namedRemoval.count() > 0,
|
|
3579
|
+
await genericRemoval.count() > 0
|
|
3580
|
+
);
|
|
3581
|
+
};
|
|
3582
|
+
const newProjectItem = page.locator(`${WORKSPACE.menuItem}:visible`).filter({ hasText: /^New project$/i });
|
|
3290
3583
|
await moveItem.hover().catch(() => {
|
|
3291
3584
|
});
|
|
3292
3585
|
let target = targetOf();
|
|
3293
3586
|
let visible = false;
|
|
3294
|
-
|
|
3295
|
-
|
|
3587
|
+
let removalState = "not-filed";
|
|
3588
|
+
let submenuVisible = false;
|
|
3589
|
+
for (let attempt = 0; attempt < 2 && !visible && removalState === "not-filed"; attempt += 1) {
|
|
3590
|
+
if (attempt > 0 && !submenuVisible) await moveItem.click().catch(() => {
|
|
3296
3591
|
});
|
|
3297
|
-
for (let poll = 0; poll <
|
|
3298
|
-
|
|
3299
|
-
|
|
3592
|
+
for (let poll = 0; poll < 40 && !visible && removalState === "not-filed"; poll += 1) {
|
|
3593
|
+
try {
|
|
3594
|
+
await target.waitFor({ state: "visible", timeout: 350 });
|
|
3595
|
+
visible = true;
|
|
3596
|
+
} catch {
|
|
3597
|
+
visible = false;
|
|
3598
|
+
}
|
|
3599
|
+
removalState = await removalStateOf();
|
|
3600
|
+
submenuVisible = submenuVisible || await newProjectItem.count() > 0;
|
|
3601
|
+
if (!visible && removalState === "not-filed") await page.waitForTimeout(150);
|
|
3300
3602
|
target = targetOf();
|
|
3301
3603
|
}
|
|
3302
3604
|
}
|
|
3303
3605
|
if (!visible) {
|
|
3304
|
-
const namedRemoval = page.locator(`${WORKSPACE.menuItem}:visible`).filter({
|
|
3305
|
-
hasText: new RegExp(`^Remove from ${escapeRegExp(input.project)}$`, "i")
|
|
3306
|
-
});
|
|
3307
|
-
const genericRemoval = page.locator(`${WORKSPACE.menuItem}:visible`).filter({ hasText: /^Remove from project$/i });
|
|
3308
|
-
const removalState = chatGptProjectRemovalState(
|
|
3309
|
-
await namedRemoval.count() > 0,
|
|
3310
|
-
await genericRemoval.count() > 0
|
|
3311
|
-
);
|
|
3312
3606
|
await dismissMenu(page);
|
|
3313
3607
|
if (removalState === "already-filed") {
|
|
3314
3608
|
return {
|
|
@@ -3318,6 +3612,16 @@ var moveChatToProject = async (page, input) => {
|
|
|
3318
3612
|
};
|
|
3319
3613
|
}
|
|
3320
3614
|
if (removalState === "current-project-unknown") {
|
|
3615
|
+
let confirmed2 = await conversationLinkIdentifiesProject(page, input.chat, input.project);
|
|
3616
|
+
if (!confirmed2)
|
|
3617
|
+
confirmed2 = await projectContainsConversation(page, input.project, input.chat);
|
|
3618
|
+
if (confirmed2) {
|
|
3619
|
+
return {
|
|
3620
|
+
...base,
|
|
3621
|
+
alreadyFiled: true,
|
|
3622
|
+
reason: `already in project "${input.project}"`
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3321
3625
|
return {
|
|
3322
3626
|
...base,
|
|
3323
3627
|
reason: `chat is already in a project, but ChatGPT did not identify which one`
|
|
@@ -3336,6 +3640,16 @@ var moveChatToProject = async (page, input) => {
|
|
|
3336
3640
|
return { ...base, reason: `could not select project "${input.project}"` };
|
|
3337
3641
|
}
|
|
3338
3642
|
await page.waitForTimeout(800);
|
|
3643
|
+
let confirmed = await activeConversationHeaderIdentifiesProject(page, input.project);
|
|
3644
|
+
if (!confirmed)
|
|
3645
|
+
confirmed = await conversationLinkIdentifiesProject(page, input.chat, input.project);
|
|
3646
|
+
if (!confirmed) confirmed = await projectContainsConversation(page, input.project, input.chat);
|
|
3647
|
+
if (!confirmed) {
|
|
3648
|
+
return {
|
|
3649
|
+
...base,
|
|
3650
|
+
reason: `ChatGPT did not confirm the move into project "${input.project}"`
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3339
3653
|
return { ...base, moved: true };
|
|
3340
3654
|
};
|
|
3341
3655
|
var archiveChat = async (page, chat) => {
|
|
@@ -3354,24 +3668,19 @@ var archiveChat = async (page, chat) => {
|
|
|
3354
3668
|
};
|
|
3355
3669
|
var findChatRow = async (page, chat) => {
|
|
3356
3670
|
const id2 = stripConversationId(chat);
|
|
3357
|
-
const byHref = page.locator(`nav a[href
|
|
3671
|
+
const byHref = page.locator(`nav a[href*="/c/${id2}"]`).first();
|
|
3358
3672
|
const byTitle = page.locator(WORKSPACE.chatLink, { hasText: chat }).first();
|
|
3359
3673
|
if (await byHref.count() > 0) return byHref;
|
|
3360
3674
|
const looksLikeId = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(id2);
|
|
3361
3675
|
if (looksLikeId && !page.url().includes(`/c/${id2}`)) {
|
|
3362
3676
|
await gotoStable(page, `https://chatgpt.com/c/${id2}`);
|
|
3363
|
-
await page.waitForTimeout(700);
|
|
3364
3677
|
if (await byHref.count() > 0) return byHref;
|
|
3678
|
+
if (await waitForActiveConversationMenu(page, id2)) return null;
|
|
3365
3679
|
}
|
|
3366
3680
|
if (await byTitle.count() > 0) return byTitle;
|
|
3367
|
-
|
|
3368
|
-
if (activeConversationId === id2) {
|
|
3369
|
-
const activeMenu = page.locator(WORKSPACE.activeConversationMenu).first();
|
|
3370
|
-
const activeMenuVisible = await activeMenu.waitFor({ state: "visible", timeout: 2e3 }).then(() => true).catch(() => false);
|
|
3371
|
-
if (activeMenuVisible) return null;
|
|
3372
|
-
}
|
|
3681
|
+
if (looksLikeId && await waitForActiveConversationMenu(page, id2)) return null;
|
|
3373
3682
|
await page.evaluate(() => {
|
|
3374
|
-
const anchor = document.querySelector('nav a[href
|
|
3683
|
+
const anchor = document.querySelector('nav a[href*="/c/"]');
|
|
3375
3684
|
let el = anchor ? anchor.closest("nav") : document.querySelector("nav");
|
|
3376
3685
|
while (el && el.scrollHeight <= el.clientHeight + 20) el = el.parentElement;
|
|
3377
3686
|
if (el !== null) {
|
|
@@ -3387,7 +3696,7 @@ var findChatRow = async (page, chat) => {
|
|
|
3387
3696
|
if (await byHref.count() > 0) return byHref;
|
|
3388
3697
|
if (await byTitle.count() > 0) return byTitle;
|
|
3389
3698
|
await page.evaluate(() => {
|
|
3390
|
-
const anchor = document.querySelector('nav a[href
|
|
3699
|
+
const anchor = document.querySelector('nav a[href*="/c/"]');
|
|
3391
3700
|
let el = anchor ? anchor.closest("nav") : document.querySelector("nav");
|
|
3392
3701
|
while (el && el.scrollHeight <= el.clientHeight + 20) el = el.parentElement;
|
|
3393
3702
|
if (el !== null) {
|
|
@@ -3428,13 +3737,25 @@ var openChatMenu = async (page, row) => {
|
|
|
3428
3737
|
}
|
|
3429
3738
|
};
|
|
3430
3739
|
var openConversationMenu = async (page, chat) => {
|
|
3740
|
+
let targetConversationId = stripConversationId(chat);
|
|
3741
|
+
const targetLooksLikeId = /^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(targetConversationId);
|
|
3742
|
+
if (targetLooksLikeId && chatGptConversationIdFromUrl(page.url()) === targetConversationId && await openActiveConversationMenu(page, targetConversationId)) {
|
|
3743
|
+
return true;
|
|
3744
|
+
}
|
|
3431
3745
|
const row = await findChatRow(page, chat);
|
|
3432
3746
|
if (row !== null) return openChatMenu(page, row);
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3747
|
+
if (!targetLooksLikeId) {
|
|
3748
|
+
const results = await searchChatGptConversations(page, { query: chat, limit: 20 });
|
|
3749
|
+
const exactResult = results.find((result) => result.title.trim() === chat.trim());
|
|
3750
|
+
if (exactResult === void 0) return false;
|
|
3751
|
+
targetConversationId = exactResult.id;
|
|
3752
|
+
await gotoStable(page, exactResult.url);
|
|
3753
|
+
}
|
|
3754
|
+
return openActiveConversationMenu(page, targetConversationId);
|
|
3755
|
+
};
|
|
3756
|
+
var openActiveConversationMenu = async (page, targetConversationId) => {
|
|
3757
|
+
if (!await waitForActiveConversationMenu(page, targetConversationId)) return false;
|
|
3436
3758
|
const trigger = page.locator(WORKSPACE.activeConversationMenu).first();
|
|
3437
|
-
if (!await trigger.isVisible()) return false;
|
|
3438
3759
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
3439
3760
|
if (attempt === 0) await trigger.click().catch(() => {
|
|
3440
3761
|
});
|
|
@@ -3451,11 +3772,32 @@ var openConversationMenu = async (page, chat) => {
|
|
|
3451
3772
|
).catch(() => {
|
|
3452
3773
|
});
|
|
3453
3774
|
}
|
|
3454
|
-
|
|
3775
|
+
let menuVisible = false;
|
|
3776
|
+
try {
|
|
3777
|
+
await page.locator(`${WORKSPACE.menuItem}:visible`).first().waitFor({ state: "visible", timeout: 4e3 });
|
|
3778
|
+
menuVisible = true;
|
|
3779
|
+
} catch {
|
|
3780
|
+
menuVisible = false;
|
|
3781
|
+
}
|
|
3455
3782
|
if (menuVisible) return true;
|
|
3456
3783
|
}
|
|
3457
3784
|
return false;
|
|
3458
3785
|
};
|
|
3786
|
+
var waitForActiveConversationMenu = async (page, conversationId) => {
|
|
3787
|
+
const trigger = page.locator(WORKSPACE.activeConversationMenu).first();
|
|
3788
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
3789
|
+
const activeConversationId = chatGptConversationIdFromUrl(page.url());
|
|
3790
|
+
if (activeConversationId === conversationId) {
|
|
3791
|
+
try {
|
|
3792
|
+
await trigger.waitFor({ state: "visible", timeout: 500 });
|
|
3793
|
+
return true;
|
|
3794
|
+
} catch {
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
await page.waitForTimeout(250);
|
|
3798
|
+
}
|
|
3799
|
+
return false;
|
|
3800
|
+
};
|
|
3459
3801
|
var dismissMenu = async (page) => {
|
|
3460
3802
|
await page.keyboard.press("Escape").catch(() => {
|
|
3461
3803
|
});
|
|
@@ -3532,16 +3874,16 @@ var listClips = async (page) => {
|
|
|
3532
3874
|
return clips;
|
|
3533
3875
|
};
|
|
3534
3876
|
var downloadClip = async (page, clipId, outDir) => {
|
|
3535
|
-
const { mkdir:
|
|
3536
|
-
const { join:
|
|
3877
|
+
const { mkdir: mkdir9, writeFile: writeFile7 } = await import("fs/promises");
|
|
3878
|
+
const { join: join13 } = await import("path");
|
|
3537
3879
|
const clipResponse = await page.request.get(clipUrlFromId(clipId));
|
|
3538
3880
|
if (!clipResponse.ok()) {
|
|
3539
3881
|
throw new Error(`Flow clip download failed (${clipResponse.status()}) for ${clipId}`);
|
|
3540
3882
|
}
|
|
3541
3883
|
const clipBytes = await clipResponse.body();
|
|
3542
|
-
await
|
|
3543
|
-
const dest =
|
|
3544
|
-
await
|
|
3884
|
+
await mkdir9(outDir, { recursive: true });
|
|
3885
|
+
const dest = join13(outDir, `${clipId}.mp4`);
|
|
3886
|
+
await writeFile7(dest, clipBytes);
|
|
3545
3887
|
return dest;
|
|
3546
3888
|
};
|
|
3547
3889
|
var tagAndClick = async (input) => {
|
|
@@ -3764,8 +4106,8 @@ var CLIP_SETTLE_MS = 1e4;
|
|
|
3764
4106
|
var RENDER_FAILURE_CONFIRM_MS = 25e3;
|
|
3765
4107
|
var MIN_GRID_CLIP_PX = 100;
|
|
3766
4108
|
var CLIP_HOLD_MS = 3e3;
|
|
3767
|
-
var delay = (ms) => new Promise((
|
|
3768
|
-
setTimeout(
|
|
4109
|
+
var delay = (ms) => new Promise((resolve9) => {
|
|
4110
|
+
setTimeout(resolve9, ms);
|
|
3769
4111
|
});
|
|
3770
4112
|
var pollFor = async (predicate, options) => {
|
|
3771
4113
|
const startedAt = Date.now();
|
|
@@ -8968,7 +9310,7 @@ var attachTunnelLifecycle = (handlers) => {
|
|
|
8968
9310
|
});
|
|
8969
9311
|
});
|
|
8970
9312
|
};
|
|
8971
|
-
var waitForTunnelUrl = (cloudflared) => new Promise((
|
|
9313
|
+
var waitForTunnelUrl = (cloudflared) => new Promise((resolve9, reject) => {
|
|
8972
9314
|
const state = { publicUrl: void 0, settled: false };
|
|
8973
9315
|
const settle = (outcome) => {
|
|
8974
9316
|
if (state.settled) return;
|
|
@@ -8977,7 +9319,7 @@ var waitForTunnelUrl = (cloudflared) => new Promise((resolve8, reject) => {
|
|
|
8977
9319
|
reject(outcome.error);
|
|
8978
9320
|
return;
|
|
8979
9321
|
}
|
|
8980
|
-
|
|
9322
|
+
resolve9(outcome.url);
|
|
8981
9323
|
};
|
|
8982
9324
|
const urlTimeout = setTimeout(() => {
|
|
8983
9325
|
settle({ kind: "error", error: new Error("Timed out waiting for tunnel URL") });
|
|
@@ -9473,9 +9815,9 @@ var Orchestrator = class {
|
|
|
9473
9815
|
}
|
|
9474
9816
|
});
|
|
9475
9817
|
}
|
|
9476
|
-
async listConversations() {
|
|
9818
|
+
async listConversations(options) {
|
|
9477
9819
|
if (this.page === null) return [];
|
|
9478
|
-
return this.provider.readSidebarConversations(this.page);
|
|
9820
|
+
return this.provider.readSidebarConversations(this.page, options);
|
|
9479
9821
|
}
|
|
9480
9822
|
async searchConversations(input) {
|
|
9481
9823
|
if (this.page === null) return [];
|
|
@@ -9566,18 +9908,18 @@ var mcpConnectorUrl = (tunnelUrl) => {
|
|
|
9566
9908
|
return `${tunnelUrl.replace(/\/+$/, "")}/mcp`;
|
|
9567
9909
|
};
|
|
9568
9910
|
var currentGitBranch = (repoPath) => {
|
|
9569
|
-
return new Promise((
|
|
9911
|
+
return new Promise((resolve9) => {
|
|
9570
9912
|
execFile2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoPath }, (error, stdout) => {
|
|
9571
9913
|
if (error) {
|
|
9572
|
-
|
|
9914
|
+
resolve9(void 0);
|
|
9573
9915
|
return;
|
|
9574
9916
|
}
|
|
9575
9917
|
const branch = stdout.trim();
|
|
9576
9918
|
if (branch.length === 0) {
|
|
9577
|
-
|
|
9919
|
+
resolve9(void 0);
|
|
9578
9920
|
return;
|
|
9579
9921
|
}
|
|
9580
|
-
|
|
9922
|
+
resolve9(branch);
|
|
9581
9923
|
});
|
|
9582
9924
|
});
|
|
9583
9925
|
};
|
|
@@ -10084,12 +10426,12 @@ var DEFAULT_FANOUT_TIMEOUT_MS = 3e5;
|
|
|
10084
10426
|
var DEFAULT_FANOUT_MAX_REPLY_CHARS = 2e3;
|
|
10085
10427
|
var DEFAULT_FANOUT_LIMIT = 20;
|
|
10086
10428
|
var withTimeout = (promise, ms, label) => {
|
|
10087
|
-
return new Promise((
|
|
10429
|
+
return new Promise((resolve9, reject) => {
|
|
10088
10430
|
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
10089
10431
|
promise.then(
|
|
10090
10432
|
(value) => {
|
|
10091
10433
|
clearTimeout(timer);
|
|
10092
|
-
|
|
10434
|
+
resolve9(value);
|
|
10093
10435
|
},
|
|
10094
10436
|
(error) => {
|
|
10095
10437
|
clearTimeout(timer);
|
|
@@ -10796,15 +11138,303 @@ var serveAskGatewayStdio = async (deps) => {
|
|
|
10796
11138
|
const mcpServer = askGatewayServerFor(deps);
|
|
10797
11139
|
const transport = new StdioServerTransport();
|
|
10798
11140
|
await mcpServer.connect(transport);
|
|
10799
|
-
await new Promise((
|
|
11141
|
+
await new Promise((resolve9) => {
|
|
10800
11142
|
const priorOnClose = transport.onclose;
|
|
10801
11143
|
transport.onclose = () => {
|
|
10802
11144
|
if (priorOnClose !== void 0) priorOnClose();
|
|
10803
|
-
|
|
11145
|
+
resolve9();
|
|
10804
11146
|
};
|
|
10805
11147
|
});
|
|
10806
11148
|
};
|
|
10807
11149
|
|
|
11150
|
+
// src/features/terminal/chatOrganizationQueue.ts
|
|
11151
|
+
import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
|
|
11152
|
+
import { existsSync } from "fs";
|
|
11153
|
+
import { mkdir as mkdir7, open, readFile as readFile10, rename, unlink, writeFile as writeFile5 } from "fs/promises";
|
|
11154
|
+
import { dirname as dirname3, isAbsolute, join as join11, resolve as resolve7 } from "path";
|
|
11155
|
+
import { Schema as Schema8 } from "effect";
|
|
11156
|
+
var ChatOrganizationTaskSchema = Schema8.Struct({
|
|
11157
|
+
conversation: Schema8.String.pipe(Schema8.minLength(1)),
|
|
11158
|
+
project: Schema8.String.pipe(Schema8.minLength(1))
|
|
11159
|
+
});
|
|
11160
|
+
var ChatOrganizationTasksSchema = Schema8.Array(ChatOrganizationTaskSchema).pipe(
|
|
11161
|
+
Schema8.minItems(1)
|
|
11162
|
+
);
|
|
11163
|
+
var PendingQueueItemSchema = Schema8.Struct({
|
|
11164
|
+
status: Schema8.Literal("pending"),
|
|
11165
|
+
conversation: Schema8.String,
|
|
11166
|
+
project: Schema8.String,
|
|
11167
|
+
attempts: Schema8.Number.pipe(Schema8.int(), Schema8.nonNegative()),
|
|
11168
|
+
lastReason: Schema8.optional(Schema8.String)
|
|
11169
|
+
});
|
|
11170
|
+
var CompletedQueueItemSchema = Schema8.Struct({
|
|
11171
|
+
status: Schema8.Literal("moved", "already-filed"),
|
|
11172
|
+
conversation: Schema8.String,
|
|
11173
|
+
project: Schema8.String,
|
|
11174
|
+
attempts: Schema8.Number.pipe(Schema8.int(), Schema8.positive()),
|
|
11175
|
+
completedAt: Schema8.String
|
|
11176
|
+
});
|
|
11177
|
+
var FailedQueueItemSchema = Schema8.Struct({
|
|
11178
|
+
status: Schema8.Literal("failed"),
|
|
11179
|
+
conversation: Schema8.String,
|
|
11180
|
+
project: Schema8.String,
|
|
11181
|
+
attempts: Schema8.Number.pipe(Schema8.int(), Schema8.positive()),
|
|
11182
|
+
reason: Schema8.String,
|
|
11183
|
+
completedAt: Schema8.String
|
|
11184
|
+
});
|
|
11185
|
+
var ChatOrganizationQueueItemSchema = Schema8.Union(
|
|
11186
|
+
PendingQueueItemSchema,
|
|
11187
|
+
CompletedQueueItemSchema,
|
|
11188
|
+
FailedQueueItemSchema
|
|
11189
|
+
);
|
|
11190
|
+
var ChatOrganizationQueueSchema = Schema8.Struct({
|
|
11191
|
+
version: Schema8.Literal(1),
|
|
11192
|
+
fingerprint: Schema8.String,
|
|
11193
|
+
createdAt: Schema8.String,
|
|
11194
|
+
updatedAt: Schema8.String,
|
|
11195
|
+
items: Schema8.Array(ChatOrganizationQueueItemSchema).pipe(Schema8.minItems(1))
|
|
11196
|
+
});
|
|
11197
|
+
var ORGANIZATION_INTERVAL = /^(?<minimum>\d+)(?:-(?<maximum>\d+))?$/u;
|
|
11198
|
+
var CLOSED_BROWSER_SESSION = /(?:target page, context or browser has been closed|browser has been closed|context has been closed|page has been closed)/iu;
|
|
11199
|
+
var chatOrganizationSessionWasClosed = (reason) => {
|
|
11200
|
+
if (reason === void 0) return false;
|
|
11201
|
+
return CLOSED_BROWSER_SESSION.test(reason);
|
|
11202
|
+
};
|
|
11203
|
+
var chatOrganizationIntervalFrom = (value, defaultSeconds) => {
|
|
11204
|
+
if (value === void 0) {
|
|
11205
|
+
return { minimumSeconds: defaultSeconds, maximumSeconds: defaultSeconds };
|
|
11206
|
+
}
|
|
11207
|
+
const match = ORGANIZATION_INTERVAL.exec(value.trim());
|
|
11208
|
+
const minimumText = match?.groups?.minimum;
|
|
11209
|
+
const maximumText = match?.groups?.maximum;
|
|
11210
|
+
if (minimumText === void 0) {
|
|
11211
|
+
throw new Error("--interval must be whole seconds or a range such as 10-20.");
|
|
11212
|
+
}
|
|
11213
|
+
const minimumSeconds = Number.parseInt(minimumText, 10);
|
|
11214
|
+
let maximumSeconds = minimumSeconds;
|
|
11215
|
+
if (maximumText !== void 0) maximumSeconds = Number.parseInt(maximumText, 10);
|
|
11216
|
+
if (minimumSeconds <= 0 || maximumSeconds <= 0 || minimumSeconds > maximumSeconds) {
|
|
11217
|
+
throw new Error("--interval must be a positive ascending range such as 10-20.");
|
|
11218
|
+
}
|
|
11219
|
+
return { minimumSeconds, maximumSeconds };
|
|
11220
|
+
};
|
|
11221
|
+
var nextChatOrganizationIntervalSeconds = (interval, randomFraction) => {
|
|
11222
|
+
if (interval.minimumSeconds === interval.maximumSeconds) return interval.minimumSeconds;
|
|
11223
|
+
const width = interval.maximumSeconds - interval.minimumSeconds + 1;
|
|
11224
|
+
return interval.minimumSeconds + Math.floor(randomFraction * width);
|
|
11225
|
+
};
|
|
11226
|
+
var chatOrganizationIntervalLabel = (interval) => {
|
|
11227
|
+
if (interval.minimumSeconds === interval.maximumSeconds) return `${interval.minimumSeconds}s`;
|
|
11228
|
+
return `${interval.minimumSeconds}-${interval.maximumSeconds}s`;
|
|
11229
|
+
};
|
|
11230
|
+
var readOrganizationPlanSource = async (spec, launchDirectory) => {
|
|
11231
|
+
const trimmed = spec.trim();
|
|
11232
|
+
if (trimmed.startsWith("[")) return trimmed;
|
|
11233
|
+
let sourcePath = trimmed;
|
|
11234
|
+
if (trimmed.startsWith("@")) sourcePath = trimmed.slice(1);
|
|
11235
|
+
let absolutePath = sourcePath;
|
|
11236
|
+
if (!isAbsolute(sourcePath)) absolutePath = resolve7(launchDirectory, sourcePath);
|
|
11237
|
+
return readFile10(absolutePath, "utf8");
|
|
11238
|
+
};
|
|
11239
|
+
var normalizedOrganizationTasks = (tasks) => {
|
|
11240
|
+
const normalized = [];
|
|
11241
|
+
for (const task of tasks) {
|
|
11242
|
+
const conversation = task.conversation.trim();
|
|
11243
|
+
const project = task.project.trim();
|
|
11244
|
+
if (!conversation) throw new Error("Every organization task needs a Conversation id or title.");
|
|
11245
|
+
if (!project) throw new Error("Every organization task needs a Project name.");
|
|
11246
|
+
normalized.push({ conversation, project });
|
|
11247
|
+
}
|
|
11248
|
+
return normalized;
|
|
11249
|
+
};
|
|
11250
|
+
var loadChatOrganizationTasks = async (spec, launchDirectory) => {
|
|
11251
|
+
if (!spec.trim()) throw new Error("--plan needs a file, @file, or inline JSON array.");
|
|
11252
|
+
const source = await readOrganizationPlanSource(spec, launchDirectory);
|
|
11253
|
+
let parsed;
|
|
11254
|
+
try {
|
|
11255
|
+
parsed = JSON.parse(source);
|
|
11256
|
+
} catch (error) {
|
|
11257
|
+
throw new Error(
|
|
11258
|
+
`Organization plan is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
11259
|
+
);
|
|
11260
|
+
}
|
|
11261
|
+
const tasks = Schema8.decodeUnknownSync(ChatOrganizationTasksSchema)(parsed);
|
|
11262
|
+
return normalizedOrganizationTasks(tasks);
|
|
11263
|
+
};
|
|
11264
|
+
var organizationPlanFingerprint = (tasks) => {
|
|
11265
|
+
return createHash2("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
|
|
11266
|
+
};
|
|
11267
|
+
var chatOrganizationQueuePath = (repoRoot, tasks) => {
|
|
11268
|
+
return join11(
|
|
11269
|
+
bridgeDir(repoRoot),
|
|
11270
|
+
"chat-organization-queues",
|
|
11271
|
+
`${organizationPlanFingerprint(tasks)}.json`
|
|
11272
|
+
);
|
|
11273
|
+
};
|
|
11274
|
+
var newChatOrganizationQueue = (tasks, timestamp) => {
|
|
11275
|
+
return {
|
|
11276
|
+
version: 1,
|
|
11277
|
+
fingerprint: organizationPlanFingerprint(tasks),
|
|
11278
|
+
createdAt: timestamp,
|
|
11279
|
+
updatedAt: timestamp,
|
|
11280
|
+
items: tasks.map((task) => ({
|
|
11281
|
+
status: "pending",
|
|
11282
|
+
conversation: task.conversation,
|
|
11283
|
+
project: task.project,
|
|
11284
|
+
attempts: 0
|
|
11285
|
+
}))
|
|
11286
|
+
};
|
|
11287
|
+
};
|
|
11288
|
+
var persistChatOrganizationQueue = async (queuePath, queue) => {
|
|
11289
|
+
await mkdir7(dirname3(queuePath), { recursive: true });
|
|
11290
|
+
const pendingPath = `${queuePath}.${process.pid}.${randomUUID4()}.pending`;
|
|
11291
|
+
try {
|
|
11292
|
+
await writeFile5(pendingPath, `${JSON.stringify(queue, null, 2)}
|
|
11293
|
+
`, "utf8");
|
|
11294
|
+
await rename(pendingPath, queuePath);
|
|
11295
|
+
} finally {
|
|
11296
|
+
try {
|
|
11297
|
+
await unlink(pendingPath);
|
|
11298
|
+
} catch {
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
};
|
|
11302
|
+
var errorCodeOf = (error) => {
|
|
11303
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
11304
|
+
if (typeof error.code !== "string") return void 0;
|
|
11305
|
+
return error.code;
|
|
11306
|
+
};
|
|
11307
|
+
var removeStaleQueueLock = async (lockPath) => {
|
|
11308
|
+
let owner;
|
|
11309
|
+
try {
|
|
11310
|
+
owner = JSON.parse(await readFile10(lockPath, "utf8"));
|
|
11311
|
+
} catch {
|
|
11312
|
+
return false;
|
|
11313
|
+
}
|
|
11314
|
+
if (typeof owner !== "object" || owner === null || !("pid" in owner)) return false;
|
|
11315
|
+
if (typeof owner.pid !== "number" || !Number.isInteger(owner.pid)) return false;
|
|
11316
|
+
try {
|
|
11317
|
+
process.kill(owner.pid, 0);
|
|
11318
|
+
return false;
|
|
11319
|
+
} catch (error) {
|
|
11320
|
+
if (errorCodeOf(error) !== "ESRCH") return false;
|
|
11321
|
+
}
|
|
11322
|
+
await unlink(lockPath);
|
|
11323
|
+
return true;
|
|
11324
|
+
};
|
|
11325
|
+
var acquireChatOrganizationQueueLock = async (queuePath) => {
|
|
11326
|
+
await mkdir7(dirname3(queuePath), { recursive: true });
|
|
11327
|
+
const lockPath = `${queuePath}.lock`;
|
|
11328
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
11329
|
+
try {
|
|
11330
|
+
const handle = await open(lockPath, "wx");
|
|
11331
|
+
await handle.writeFile(
|
|
11332
|
+
`${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
11333
|
+
`
|
|
11334
|
+
);
|
|
11335
|
+
let released = false;
|
|
11336
|
+
return async () => {
|
|
11337
|
+
if (released) return;
|
|
11338
|
+
released = true;
|
|
11339
|
+
await handle.close();
|
|
11340
|
+
await unlink(lockPath);
|
|
11341
|
+
};
|
|
11342
|
+
} catch (error) {
|
|
11343
|
+
if (errorCodeOf(error) !== "EEXIST") throw error;
|
|
11344
|
+
if (attempt === 0 && await removeStaleQueueLock(lockPath)) continue;
|
|
11345
|
+
throw new Error(`Organization queue is already running: ${lockPath}`);
|
|
11346
|
+
}
|
|
11347
|
+
}
|
|
11348
|
+
throw new Error(`Could not acquire organization queue lock: ${lockPath}`);
|
|
11349
|
+
};
|
|
11350
|
+
var openChatOrganizationQueue = async (input) => {
|
|
11351
|
+
const path2 = chatOrganizationQueuePath(input.repoRoot, input.tasks);
|
|
11352
|
+
if (!input.restart && existsSync(path2)) {
|
|
11353
|
+
const stored = JSON.parse(await readFile10(path2, "utf8"));
|
|
11354
|
+
const queue2 = Schema8.decodeUnknownSync(ChatOrganizationQueueSchema)(stored);
|
|
11355
|
+
const expectedFingerprint = organizationPlanFingerprint(input.tasks);
|
|
11356
|
+
if (queue2.fingerprint !== expectedFingerprint) {
|
|
11357
|
+
throw new Error(`Stored queue fingerprint does not match ${path2}. Use --restart.`);
|
|
11358
|
+
}
|
|
11359
|
+
return { path: path2, queue: queue2 };
|
|
11360
|
+
}
|
|
11361
|
+
const queue = newChatOrganizationQueue(input.tasks, input.timestamp);
|
|
11362
|
+
await persistChatOrganizationQueue(path2, queue);
|
|
11363
|
+
return { path: path2, queue };
|
|
11364
|
+
};
|
|
11365
|
+
var nextChatOrganizationQueueIndex = (queue) => {
|
|
11366
|
+
const index = queue.items.findIndex((item) => item.status === "pending");
|
|
11367
|
+
if (index < 0) return void 0;
|
|
11368
|
+
return index;
|
|
11369
|
+
};
|
|
11370
|
+
var completeChatOrganizationQueueItem = (input) => {
|
|
11371
|
+
const current = input.queue.items[input.index];
|
|
11372
|
+
if (current === void 0 || current.status !== "pending") return input.queue;
|
|
11373
|
+
const items = [...input.queue.items];
|
|
11374
|
+
items[input.index] = {
|
|
11375
|
+
status: input.status,
|
|
11376
|
+
conversation: current.conversation,
|
|
11377
|
+
project: current.project,
|
|
11378
|
+
attempts: current.attempts + 1,
|
|
11379
|
+
completedAt: input.timestamp
|
|
11380
|
+
};
|
|
11381
|
+
return { ...input.queue, updatedAt: input.timestamp, items };
|
|
11382
|
+
};
|
|
11383
|
+
var deferChatOrganizationQueueItem = (input) => {
|
|
11384
|
+
const current = input.queue.items[input.index];
|
|
11385
|
+
if (current === void 0 || current.status !== "pending") return input.queue;
|
|
11386
|
+
const items = [...input.queue.items];
|
|
11387
|
+
items[input.index] = {
|
|
11388
|
+
status: "pending",
|
|
11389
|
+
conversation: current.conversation,
|
|
11390
|
+
project: current.project,
|
|
11391
|
+
attempts: current.attempts,
|
|
11392
|
+
lastReason: input.reason
|
|
11393
|
+
};
|
|
11394
|
+
return { ...input.queue, updatedAt: input.timestamp, items };
|
|
11395
|
+
};
|
|
11396
|
+
var failChatOrganizationQueueItem = (input) => {
|
|
11397
|
+
const current = input.queue.items[input.index];
|
|
11398
|
+
if (current === void 0 || current.status !== "pending") return input.queue;
|
|
11399
|
+
const attempts = current.attempts + 1;
|
|
11400
|
+
const items = [...input.queue.items];
|
|
11401
|
+
if (attempts >= input.maxAttempts) {
|
|
11402
|
+
items[input.index] = {
|
|
11403
|
+
status: "failed",
|
|
11404
|
+
conversation: current.conversation,
|
|
11405
|
+
project: current.project,
|
|
11406
|
+
attempts,
|
|
11407
|
+
reason: input.reason,
|
|
11408
|
+
completedAt: input.timestamp
|
|
11409
|
+
};
|
|
11410
|
+
} else {
|
|
11411
|
+
items[input.index] = {
|
|
11412
|
+
status: "pending",
|
|
11413
|
+
conversation: current.conversation,
|
|
11414
|
+
project: current.project,
|
|
11415
|
+
attempts,
|
|
11416
|
+
lastReason: input.reason
|
|
11417
|
+
};
|
|
11418
|
+
}
|
|
11419
|
+
return { ...input.queue, updatedAt: input.timestamp, items };
|
|
11420
|
+
};
|
|
11421
|
+
var summarizeChatOrganizationQueue = (queue) => {
|
|
11422
|
+
const summary = {
|
|
11423
|
+
total: queue.items.length,
|
|
11424
|
+
pending: 0,
|
|
11425
|
+
moved: 0,
|
|
11426
|
+
alreadyFiled: 0,
|
|
11427
|
+
failed: 0
|
|
11428
|
+
};
|
|
11429
|
+
for (const item of queue.items) {
|
|
11430
|
+
if (item.status === "pending") summary.pending += 1;
|
|
11431
|
+
else if (item.status === "moved") summary.moved += 1;
|
|
11432
|
+
else if (item.status === "already-filed") summary.alreadyFiled += 1;
|
|
11433
|
+
else summary.failed += 1;
|
|
11434
|
+
}
|
|
11435
|
+
return summary;
|
|
11436
|
+
};
|
|
11437
|
+
|
|
10808
11438
|
// src/features/terminal/providerLabel.ts
|
|
10809
11439
|
var providerDisplayName = (id2) => {
|
|
10810
11440
|
return PROVIDER_CONFIG[id2].displayName;
|
|
@@ -11385,7 +12015,7 @@ var IGNORED_COMPLETION_ENTRIES = /* @__PURE__ */ new Set([
|
|
|
11385
12015
|
]);
|
|
11386
12016
|
|
|
11387
12017
|
// src/features/terminal/tui/composer/fileMentionCompletion.ts
|
|
11388
|
-
import { isAbsolute, sep as sep2 } from "path";
|
|
12018
|
+
import { isAbsolute as isAbsolute2, sep as sep2 } from "path";
|
|
11389
12019
|
var findActiveFileMention = (input) => {
|
|
11390
12020
|
const position = input.cursor === void 0 ? input.input.length : input.cursor;
|
|
11391
12021
|
return parseActiveFileMention({ text: input.input, position });
|
|
@@ -11411,7 +12041,7 @@ var normalizePartialPath = (partial) => {
|
|
|
11411
12041
|
return partial.replaceAll("\\", "/").replaceAll(sep2, "/");
|
|
11412
12042
|
};
|
|
11413
12043
|
var isUnsafePartial = (partial) => {
|
|
11414
|
-
if (
|
|
12044
|
+
if (isAbsolute2(partial) || partial.startsWith("/")) return true;
|
|
11415
12045
|
return partial.split("/").filter(Boolean).includes("..");
|
|
11416
12046
|
};
|
|
11417
12047
|
var compareCompletionMatches = (left, right) => {
|
|
@@ -12927,8 +13557,8 @@ var sessionIdFrom = async (params) => {
|
|
|
12927
13557
|
return latest.metadata.id;
|
|
12928
13558
|
};
|
|
12929
13559
|
var repositoryFileFrom = (params) => {
|
|
12930
|
-
if (
|
|
12931
|
-
const rel = relative3(
|
|
13560
|
+
if (isAbsolute3(params.input)) {
|
|
13561
|
+
const rel = relative3(resolve8(params.repoRoot), resolve8(params.input));
|
|
12932
13562
|
return repositoryPath(params.repoRoot, rel || ".");
|
|
12933
13563
|
}
|
|
12934
13564
|
return repositoryPath(params.repoRoot, params.input);
|
|
@@ -12965,8 +13595,8 @@ var parseCaptureUrl = (url) => {
|
|
|
12965
13595
|
};
|
|
12966
13596
|
var prepareScreenshotDir = async (repoPath) => {
|
|
12967
13597
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12968
|
-
const dir =
|
|
12969
|
-
await
|
|
13598
|
+
const dir = join12(screenshotsDir(repoPath), stamp);
|
|
13599
|
+
await mkdir8(dir, { recursive: true });
|
|
12970
13600
|
return dir;
|
|
12971
13601
|
};
|
|
12972
13602
|
var captureWithPlaywright = async (params) => {
|
|
@@ -12998,7 +13628,7 @@ var captureViewport = async (params) => {
|
|
|
12998
13628
|
return file;
|
|
12999
13629
|
};
|
|
13000
13630
|
var writeViewportScreenshot = async (input) => {
|
|
13001
|
-
const file =
|
|
13631
|
+
const file = join12(input.dir, `${input.viewport.name}.png`);
|
|
13002
13632
|
await input.page.screenshot({ path: file, fullPage: true });
|
|
13003
13633
|
return file;
|
|
13004
13634
|
};
|
|
@@ -13016,16 +13646,16 @@ var sessionExportFromParts = async (input) => {
|
|
|
13016
13646
|
if (session !== void 0 && session !== null) {
|
|
13017
13647
|
return {
|
|
13018
13648
|
sessionId: session.metadata.id,
|
|
13019
|
-
outputPath: input.parts[1] ?
|
|
13649
|
+
outputPath: input.parts[1] ? resolve8(input.parts[1]) : void 0
|
|
13020
13650
|
};
|
|
13021
13651
|
}
|
|
13022
13652
|
return {
|
|
13023
13653
|
sessionId: await sessionIdFrom({ args: "", ctx: input.ctx }),
|
|
13024
|
-
outputPath:
|
|
13654
|
+
outputPath: resolve8(first)
|
|
13025
13655
|
};
|
|
13026
13656
|
};
|
|
13027
13657
|
var defaultExportPath = (params) => {
|
|
13028
|
-
return
|
|
13658
|
+
return join12(exportsDir(params.repoPath), `${params.sessionId}.md`);
|
|
13029
13659
|
};
|
|
13030
13660
|
var exportContentForPath = (params) => {
|
|
13031
13661
|
const extension = extname2(params.path).toLowerCase();
|
|
@@ -13329,8 +13959,8 @@ var writeSessionExport = async (params) => {
|
|
|
13329
13959
|
};
|
|
13330
13960
|
var persistSessionExport = async (input) => {
|
|
13331
13961
|
const content = exportContentForPath({ path: input.targetPath, exported: input.exported });
|
|
13332
|
-
await
|
|
13333
|
-
await
|
|
13962
|
+
await mkdir8(dirname4(input.targetPath), { recursive: true });
|
|
13963
|
+
await writeFile6(input.targetPath, content, "utf-8");
|
|
13334
13964
|
console.log(`Exported ${input.sessionId} to ${input.targetPath}`);
|
|
13335
13965
|
};
|
|
13336
13966
|
var listCheckpointsCommand = async (_args, ctx) => {
|
|
@@ -13710,7 +14340,7 @@ var runBrowserStatus = async (options = {}) => {
|
|
|
13710
14340
|
process.exit(0);
|
|
13711
14341
|
};
|
|
13712
14342
|
var cacheProfileRoot = (options) => {
|
|
13713
|
-
return options.profile ?
|
|
14343
|
+
return options.profile ? resolve8(options.profile) : bridgeChromeProfileRoot();
|
|
13714
14344
|
};
|
|
13715
14345
|
var runCacheList = async (options) => {
|
|
13716
14346
|
const inventory = await inventoryChromeCache({
|
|
@@ -13850,8 +14480,8 @@ var readFanoutSource = async (spec) => {
|
|
|
13850
14480
|
const trimmed = spec.trim();
|
|
13851
14481
|
if (trimmed.startsWith("[")) return trimmed;
|
|
13852
14482
|
const path2 = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
|
13853
|
-
const absolute =
|
|
13854
|
-
return
|
|
14483
|
+
const absolute = isAbsolute3(path2) ? path2 : resolve8(process.cwd(), path2);
|
|
14484
|
+
return readFile11(absolute, "utf8");
|
|
13855
14485
|
};
|
|
13856
14486
|
var loadFanoutTasks = async (spec) => {
|
|
13857
14487
|
if (!spec.trim()) return fail("--fan-out needs a task file, @file, or inline JSON array.");
|
|
@@ -13865,7 +14495,7 @@ var loadFanoutTasks = async (spec) => {
|
|
|
13865
14495
|
return fail(`--fan-out is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
13866
14496
|
}
|
|
13867
14497
|
try {
|
|
13868
|
-
return
|
|
14498
|
+
return Schema9.decodeUnknownSync(FanoutTasksSchema)(parsed);
|
|
13869
14499
|
} catch (err) {
|
|
13870
14500
|
return fail(
|
|
13871
14501
|
`--fan-out does not match the task schema (need a non-empty array of {prompt, provider?, conversation?, label?, isolate?}): ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -14100,7 +14730,7 @@ var finishAskRun = async (input) => {
|
|
|
14100
14730
|
var startAskEngine = async (input) => {
|
|
14101
14731
|
const withTools = Boolean(input.options.tools) && input.supportsMcpConnector;
|
|
14102
14732
|
return startEngine({
|
|
14103
|
-
repoPath: input.options.repo ?
|
|
14733
|
+
repoPath: input.options.repo ? resolve8(input.options.repo) : void 0,
|
|
14104
14734
|
provider: input.provider,
|
|
14105
14735
|
mcpPort: input.options.port ? Number(input.options.port) : void 0,
|
|
14106
14736
|
withBrowser: true,
|
|
@@ -14125,7 +14755,7 @@ var debugPortFromOption = (value) => {
|
|
|
14125
14755
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
14126
14756
|
};
|
|
14127
14757
|
var profileRootFromOption = (value) => {
|
|
14128
|
-
return value ?
|
|
14758
|
+
return value ? resolve8(value) : void 0;
|
|
14129
14759
|
};
|
|
14130
14760
|
var runAskTurn = async (input) => {
|
|
14131
14761
|
await applyAskPreflight({ engine: input.engine, options: input.options });
|
|
@@ -14143,7 +14773,7 @@ var attachAskFiles = async (input) => {
|
|
|
14143
14773
|
const resolved = paths.map((target) => {
|
|
14144
14774
|
const rel = repositoryFileFrom({ repoRoot, input: target });
|
|
14145
14775
|
assertImagePath(rel);
|
|
14146
|
-
return
|
|
14776
|
+
return resolve8(repoRoot, rel);
|
|
14147
14777
|
});
|
|
14148
14778
|
await input.engine.getOrchestrator().attachFiles(resolved);
|
|
14149
14779
|
};
|
|
@@ -14250,7 +14880,7 @@ var downloadAfterExtract = async (input) => {
|
|
|
14250
14880
|
};
|
|
14251
14881
|
var startDownloadEngine = async (options) => {
|
|
14252
14882
|
return startEngine({
|
|
14253
|
-
repoPath: options.repo ?
|
|
14883
|
+
repoPath: options.repo ? resolve8(options.repo) : void 0,
|
|
14254
14884
|
provider: providerIdFrom(options.provider),
|
|
14255
14885
|
mcpPort: options.port ? Number(options.port) : void 0,
|
|
14256
14886
|
withBrowser: true,
|
|
@@ -14279,7 +14909,7 @@ var assertChatgptWorkspace = (options) => {
|
|
|
14279
14909
|
};
|
|
14280
14910
|
var startWorkspaceSession = async (options) => {
|
|
14281
14911
|
const engine = await startEngine({
|
|
14282
|
-
repoPath: options.repo ?
|
|
14912
|
+
repoPath: options.repo ? resolve8(options.repo) : void 0,
|
|
14283
14913
|
provider: "chatgpt",
|
|
14284
14914
|
mcpPort: options.port ? Number(options.port) : void 0,
|
|
14285
14915
|
withBrowser: true,
|
|
@@ -14351,13 +14981,20 @@ var runProjectDelete = async (name, options) => {
|
|
|
14351
14981
|
var runChatList = async (options) => {
|
|
14352
14982
|
assertChatgptWorkspace(options);
|
|
14353
14983
|
const { engine } = await startWorkspaceSession(options);
|
|
14354
|
-
const chats = await engine.getOrchestrator().listConversations();
|
|
14984
|
+
const chats = await engine.getOrchestrator().listConversations({ orphans: Boolean(options.orphans) });
|
|
14355
14985
|
await engine.shutdown({ closeBrowser: false });
|
|
14356
|
-
|
|
14357
|
-
|
|
14358
|
-
|
|
14359
|
-
else
|
|
14360
|
-
`)
|
|
14986
|
+
let output = "";
|
|
14987
|
+
if (options.json) output = `${JSON.stringify(chats)}
|
|
14988
|
+
`;
|
|
14989
|
+
else if (chats.length === 0) output = "No conversations.\n";
|
|
14990
|
+
else output = `${chats.map((chat) => `${chat.id} ${chat.title}`).join("\n")}
|
|
14991
|
+
`;
|
|
14992
|
+
await new Promise((resolve9, reject) => {
|
|
14993
|
+
process.stdout.write(output, (error) => {
|
|
14994
|
+
if (error) reject(error);
|
|
14995
|
+
else resolve9();
|
|
14996
|
+
});
|
|
14997
|
+
});
|
|
14361
14998
|
process.exit(0);
|
|
14362
14999
|
};
|
|
14363
15000
|
var runChatSearch = async (query, options) => {
|
|
@@ -14444,6 +15081,322 @@ var runChatMove = async (chat, options) => {
|
|
|
14444
15081
|
writeMoveOutcomes(outcomes, options);
|
|
14445
15082
|
process.exit(outcomes.every((outcome) => outcome.moved || outcome.alreadyFiled) ? 0 : 1);
|
|
14446
15083
|
};
|
|
15084
|
+
var DEFAULT_ORGANIZATION_INTERVAL_SECONDS = 60;
|
|
15085
|
+
var DEFAULT_ORGANIZATION_COOLDOWN_SECONDS = 600;
|
|
15086
|
+
var DEFAULT_ORGANIZATION_MAX_ATTEMPTS = 4;
|
|
15087
|
+
var MAX_CONSECUTIVE_ORGANIZATION_COOLDOWNS = 12;
|
|
15088
|
+
var RATE_LIMIT_REASON = "ChatGPT temporarily limited access to Conversation history.";
|
|
15089
|
+
var CLOSED_SESSION_REASON = "ChatGPT browser session closed; reopening before retry.";
|
|
15090
|
+
var positiveIntegerOption = (value, optionName, defaultValue) => {
|
|
15091
|
+
if (value === void 0) return defaultValue;
|
|
15092
|
+
if (!/^[1-9]\d*$/u.test(value.trim())) {
|
|
15093
|
+
return fail(`${optionName} must be a positive whole number.`);
|
|
15094
|
+
}
|
|
15095
|
+
const parsed = Number.parseInt(value, 10);
|
|
15096
|
+
return parsed;
|
|
15097
|
+
};
|
|
15098
|
+
var organizationIntervalOption = (value) => {
|
|
15099
|
+
try {
|
|
15100
|
+
return chatOrganizationIntervalFrom(value, DEFAULT_ORGANIZATION_INTERVAL_SECONDS);
|
|
15101
|
+
} catch (error) {
|
|
15102
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
15103
|
+
}
|
|
15104
|
+
};
|
|
15105
|
+
var waitForOrganizationInterval = async (seconds) => {
|
|
15106
|
+
await new Promise((complete) => setTimeout(complete, seconds * 1e3));
|
|
15107
|
+
};
|
|
15108
|
+
var writeChatOrganizationProgress = (message, options) => {
|
|
15109
|
+
if (options.json) {
|
|
15110
|
+
process.stderr.write(`${message}
|
|
15111
|
+
`);
|
|
15112
|
+
return;
|
|
15113
|
+
}
|
|
15114
|
+
process.stdout.write(`${message}
|
|
15115
|
+
`);
|
|
15116
|
+
};
|
|
15117
|
+
var moveFailureReason = (outcome) => {
|
|
15118
|
+
if (outcome.reason?.trim()) return outcome.reason;
|
|
15119
|
+
return "ChatGPT did not confirm the Conversation move.";
|
|
15120
|
+
};
|
|
15121
|
+
var writeChatOrganizationSummary = (input) => {
|
|
15122
|
+
const summary = summarizeChatOrganizationQueue(input.queue);
|
|
15123
|
+
if (input.options.json) {
|
|
15124
|
+
process.stdout.write(`${JSON.stringify({ queuePath: input.queuePath, summary })}
|
|
15125
|
+
`);
|
|
15126
|
+
return;
|
|
15127
|
+
}
|
|
15128
|
+
process.stdout.write(
|
|
15129
|
+
`${[
|
|
15130
|
+
"Organization queue finished.",
|
|
15131
|
+
`Moved: ${summary.moved}`,
|
|
15132
|
+
`Already filed: ${summary.alreadyFiled}`,
|
|
15133
|
+
`Failed: ${summary.failed}`,
|
|
15134
|
+
`Queue state: ${input.queuePath}`
|
|
15135
|
+
].join("\n")}
|
|
15136
|
+
`
|
|
15137
|
+
);
|
|
15138
|
+
};
|
|
15139
|
+
var writeChatOrganizationDryRun = (input) => {
|
|
15140
|
+
const preview = {
|
|
15141
|
+
dryRun: true,
|
|
15142
|
+
queuePath: input.queuePath,
|
|
15143
|
+
interval: input.interval,
|
|
15144
|
+
cooldownSeconds: input.cooldownSeconds,
|
|
15145
|
+
maxAttempts: input.maxAttempts,
|
|
15146
|
+
tasks: input.tasks
|
|
15147
|
+
};
|
|
15148
|
+
if (input.options.json) {
|
|
15149
|
+
process.stdout.write(`${JSON.stringify(preview)}
|
|
15150
|
+
`);
|
|
15151
|
+
return;
|
|
15152
|
+
}
|
|
15153
|
+
process.stdout.write(
|
|
15154
|
+
`${[
|
|
15155
|
+
`Organization queue dry run: ${input.tasks.length} Conversations`,
|
|
15156
|
+
`Interval: ${chatOrganizationIntervalLabel(input.interval)}`,
|
|
15157
|
+
`Rate-limit cooldown: ${input.cooldownSeconds}s`,
|
|
15158
|
+
`Attempts per Conversation: ${input.maxAttempts}`,
|
|
15159
|
+
`Resume state: ${input.queuePath}`,
|
|
15160
|
+
...input.tasks.map((task) => ` ${task.conversation} -> ${task.project}`)
|
|
15161
|
+
].join("\n")}
|
|
15162
|
+
`
|
|
15163
|
+
);
|
|
15164
|
+
};
|
|
15165
|
+
var recordChatOrganizationAttempt = async (input) => {
|
|
15166
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
15167
|
+
let queue;
|
|
15168
|
+
if (input.sessionClosed) {
|
|
15169
|
+
queue = deferChatOrganizationQueueItem({
|
|
15170
|
+
queue: input.queue,
|
|
15171
|
+
index: input.index,
|
|
15172
|
+
reason: CLOSED_SESSION_REASON,
|
|
15173
|
+
timestamp
|
|
15174
|
+
});
|
|
15175
|
+
} else if (input.rateLimited) {
|
|
15176
|
+
queue = deferChatOrganizationQueueItem({
|
|
15177
|
+
queue: input.queue,
|
|
15178
|
+
index: input.index,
|
|
15179
|
+
reason: RATE_LIMIT_REASON,
|
|
15180
|
+
timestamp
|
|
15181
|
+
});
|
|
15182
|
+
} else if (input.moveOutcome?.moved) {
|
|
15183
|
+
queue = completeChatOrganizationQueueItem({
|
|
15184
|
+
queue: input.queue,
|
|
15185
|
+
index: input.index,
|
|
15186
|
+
status: "moved",
|
|
15187
|
+
timestamp
|
|
15188
|
+
});
|
|
15189
|
+
} else if (input.moveOutcome?.alreadyFiled) {
|
|
15190
|
+
queue = completeChatOrganizationQueueItem({
|
|
15191
|
+
queue: input.queue,
|
|
15192
|
+
index: input.index,
|
|
15193
|
+
status: "already-filed",
|
|
15194
|
+
timestamp
|
|
15195
|
+
});
|
|
15196
|
+
} else {
|
|
15197
|
+
let reason = "ChatGPT did not complete the Conversation move.";
|
|
15198
|
+
if (input.moveOutcome !== void 0) reason = moveFailureReason(input.moveOutcome);
|
|
15199
|
+
else if (input.thrownReason !== void 0) reason = input.thrownReason;
|
|
15200
|
+
queue = failChatOrganizationQueueItem({
|
|
15201
|
+
queue: input.queue,
|
|
15202
|
+
index: input.index,
|
|
15203
|
+
reason,
|
|
15204
|
+
maxAttempts: input.maxAttempts,
|
|
15205
|
+
timestamp
|
|
15206
|
+
});
|
|
15207
|
+
}
|
|
15208
|
+
await persistChatOrganizationQueue(input.queuePath, queue);
|
|
15209
|
+
return queue;
|
|
15210
|
+
};
|
|
15211
|
+
var runChatOrganize = async (options) => {
|
|
15212
|
+
assertChatgptWorkspace(options);
|
|
15213
|
+
const plan = options.plan;
|
|
15214
|
+
if (plan === void 0 || !plan.trim()) {
|
|
15215
|
+
return fail("Usage: bridge chat organize --plan <fileOrJson>");
|
|
15216
|
+
}
|
|
15217
|
+
const interval = organizationIntervalOption(options.interval);
|
|
15218
|
+
const cooldownSeconds = positiveIntegerOption(
|
|
15219
|
+
options.cooldown,
|
|
15220
|
+
"--cooldown",
|
|
15221
|
+
DEFAULT_ORGANIZATION_COOLDOWN_SECONDS
|
|
15222
|
+
);
|
|
15223
|
+
const maxAttempts = positiveIntegerOption(
|
|
15224
|
+
options.maxAttempts,
|
|
15225
|
+
"--max-attempts",
|
|
15226
|
+
DEFAULT_ORGANIZATION_MAX_ATTEMPTS
|
|
15227
|
+
);
|
|
15228
|
+
let tasks;
|
|
15229
|
+
try {
|
|
15230
|
+
tasks = await loadChatOrganizationTasks(plan, process.cwd());
|
|
15231
|
+
} catch (error) {
|
|
15232
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
15233
|
+
}
|
|
15234
|
+
const repoRoot = repositoryRoot(options.repo);
|
|
15235
|
+
const queuePath = chatOrganizationQueuePath(repoRoot, tasks);
|
|
15236
|
+
if (options.dryRun) {
|
|
15237
|
+
writeChatOrganizationDryRun({
|
|
15238
|
+
tasks,
|
|
15239
|
+
queuePath,
|
|
15240
|
+
interval,
|
|
15241
|
+
cooldownSeconds,
|
|
15242
|
+
maxAttempts,
|
|
15243
|
+
options
|
|
15244
|
+
});
|
|
15245
|
+
process.exit(0);
|
|
15246
|
+
}
|
|
15247
|
+
let releaseQueueLock;
|
|
15248
|
+
try {
|
|
15249
|
+
releaseQueueLock = await acquireChatOrganizationQueueLock(queuePath);
|
|
15250
|
+
} catch (error) {
|
|
15251
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
15252
|
+
}
|
|
15253
|
+
let queue;
|
|
15254
|
+
try {
|
|
15255
|
+
const opened = await openChatOrganizationQueue({
|
|
15256
|
+
repoRoot,
|
|
15257
|
+
tasks,
|
|
15258
|
+
restart: Boolean(options.restart),
|
|
15259
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
15260
|
+
});
|
|
15261
|
+
queue = opened.queue;
|
|
15262
|
+
} catch (error) {
|
|
15263
|
+
await releaseQueueLock();
|
|
15264
|
+
return fail(
|
|
15265
|
+
`Could not open the organization queue: ${error instanceof Error ? error.message : String(error)}`
|
|
15266
|
+
);
|
|
15267
|
+
}
|
|
15268
|
+
const initialSummary = summarizeChatOrganizationQueue(queue);
|
|
15269
|
+
writeChatOrganizationProgress(
|
|
15270
|
+
`Organization queue: ${initialSummary.pending} pending of ${initialSummary.total}; state ${queuePath}`,
|
|
15271
|
+
options
|
|
15272
|
+
);
|
|
15273
|
+
if (initialSummary.pending === 0) {
|
|
15274
|
+
writeChatOrganizationSummary({ queuePath, queue, options });
|
|
15275
|
+
await releaseQueueLock();
|
|
15276
|
+
process.exit(initialSummary.failed === 0 ? 0 : 1);
|
|
15277
|
+
}
|
|
15278
|
+
let workspace;
|
|
15279
|
+
try {
|
|
15280
|
+
workspace = await startWorkspaceSession(options);
|
|
15281
|
+
} catch (error) {
|
|
15282
|
+
await releaseQueueLock();
|
|
15283
|
+
throw error;
|
|
15284
|
+
}
|
|
15285
|
+
let workspaceWasShutdown = false;
|
|
15286
|
+
try {
|
|
15287
|
+
let index = nextChatOrganizationQueueIndex(queue);
|
|
15288
|
+
let consecutiveRateLimitCooldowns = 0;
|
|
15289
|
+
while (index !== void 0) {
|
|
15290
|
+
const item = queue.items[index];
|
|
15291
|
+
if (item === void 0 || item.status !== "pending") {
|
|
15292
|
+
index = nextChatOrganizationQueueIndex(queue);
|
|
15293
|
+
continue;
|
|
15294
|
+
}
|
|
15295
|
+
if (await acknowledgeChatGptHistoryRateLimit(workspace.page)) {
|
|
15296
|
+
consecutiveRateLimitCooldowns += 1;
|
|
15297
|
+
if (consecutiveRateLimitCooldowns >= MAX_CONSECUTIVE_ORGANIZATION_COOLDOWNS) {
|
|
15298
|
+
writeChatOrganizationProgress(
|
|
15299
|
+
`ChatGPT remained rate-limited after ${consecutiveRateLimitCooldowns} cooldowns; leaving the queue pending for a later resume.`,
|
|
15300
|
+
options
|
|
15301
|
+
);
|
|
15302
|
+
break;
|
|
15303
|
+
}
|
|
15304
|
+
writeChatOrganizationProgress(
|
|
15305
|
+
`ChatGPT rate limit detected; cooling down for ${cooldownSeconds}s before continuing.`,
|
|
15306
|
+
options
|
|
15307
|
+
);
|
|
15308
|
+
await waitForOrganizationInterval(cooldownSeconds);
|
|
15309
|
+
continue;
|
|
15310
|
+
}
|
|
15311
|
+
consecutiveRateLimitCooldowns = 0;
|
|
15312
|
+
writeChatOrganizationProgress(
|
|
15313
|
+
`Moving "${item.conversation}" -> ${item.project} (attempt ${item.attempts + 1}/${maxAttempts})`,
|
|
15314
|
+
options
|
|
15315
|
+
);
|
|
15316
|
+
let moveOutcome;
|
|
15317
|
+
let thrownReason;
|
|
15318
|
+
try {
|
|
15319
|
+
moveOutcome = await moveChatToProject(workspace.page, {
|
|
15320
|
+
chat: item.conversation,
|
|
15321
|
+
project: item.project
|
|
15322
|
+
});
|
|
15323
|
+
} catch (error) {
|
|
15324
|
+
thrownReason = error instanceof Error ? error.message : String(error);
|
|
15325
|
+
}
|
|
15326
|
+
const sessionClosed = chatOrganizationSessionWasClosed(thrownReason);
|
|
15327
|
+
const rateLimited = await acknowledgeChatGptHistoryRateLimit(workspace.page);
|
|
15328
|
+
queue = await recordChatOrganizationAttempt({
|
|
15329
|
+
queue,
|
|
15330
|
+
queuePath,
|
|
15331
|
+
index,
|
|
15332
|
+
maxAttempts,
|
|
15333
|
+
sessionClosed,
|
|
15334
|
+
rateLimited,
|
|
15335
|
+
moveOutcome,
|
|
15336
|
+
thrownReason
|
|
15337
|
+
});
|
|
15338
|
+
if (sessionClosed) {
|
|
15339
|
+
writeChatOrganizationProgress(
|
|
15340
|
+
"ChatGPT browser session closed; reopening it without consuming a retry attempt.",
|
|
15341
|
+
options
|
|
15342
|
+
);
|
|
15343
|
+
await workspace.engine.shutdown({ closeBrowser: false });
|
|
15344
|
+
workspaceWasShutdown = true;
|
|
15345
|
+
try {
|
|
15346
|
+
workspace = await startWorkspaceSession(options);
|
|
15347
|
+
workspaceWasShutdown = false;
|
|
15348
|
+
} catch (error) {
|
|
15349
|
+
writeChatOrganizationProgress(
|
|
15350
|
+
`Could not reopen the ChatGPT browser session; leaving the queue pending: ${error instanceof Error ? error.message : String(error)}`,
|
|
15351
|
+
options
|
|
15352
|
+
);
|
|
15353
|
+
break;
|
|
15354
|
+
}
|
|
15355
|
+
index = nextChatOrganizationQueueIndex(queue);
|
|
15356
|
+
continue;
|
|
15357
|
+
}
|
|
15358
|
+
const recorded = queue.items[index];
|
|
15359
|
+
if (recorded?.status === "moved") {
|
|
15360
|
+
writeChatOrganizationProgress(`Moved "${recorded.conversation}".`, options);
|
|
15361
|
+
} else if (recorded?.status === "already-filed") {
|
|
15362
|
+
writeChatOrganizationProgress(`Already filed "${recorded.conversation}".`, options);
|
|
15363
|
+
} else if (recorded?.status === "failed") {
|
|
15364
|
+
writeChatOrganizationProgress(
|
|
15365
|
+
`Failed "${recorded.conversation}" after ${recorded.attempts} attempts: ${recorded.reason}`,
|
|
15366
|
+
options
|
|
15367
|
+
);
|
|
15368
|
+
} else if (recorded?.status === "pending") {
|
|
15369
|
+
let reason = "unknown failure";
|
|
15370
|
+
if (recorded.lastReason !== void 0) reason = recorded.lastReason;
|
|
15371
|
+
writeChatOrganizationProgress(`Will retry "${recorded.conversation}": ${reason}`, options);
|
|
15372
|
+
}
|
|
15373
|
+
let waitSeconds = nextChatOrganizationIntervalSeconds(interval, Math.random());
|
|
15374
|
+
if (rateLimited) {
|
|
15375
|
+
consecutiveRateLimitCooldowns += 1;
|
|
15376
|
+
waitSeconds = cooldownSeconds;
|
|
15377
|
+
}
|
|
15378
|
+
index = nextChatOrganizationQueueIndex(queue);
|
|
15379
|
+
if (index !== void 0) {
|
|
15380
|
+
writeChatOrganizationProgress(
|
|
15381
|
+
`Waiting ${waitSeconds}s before the next UI operation.`,
|
|
15382
|
+
options
|
|
15383
|
+
);
|
|
15384
|
+
await waitForOrganizationInterval(waitSeconds);
|
|
15385
|
+
}
|
|
15386
|
+
}
|
|
15387
|
+
} finally {
|
|
15388
|
+
try {
|
|
15389
|
+
if (!workspaceWasShutdown) await workspace.engine.shutdown({ closeBrowser: false });
|
|
15390
|
+
} finally {
|
|
15391
|
+
await releaseQueueLock();
|
|
15392
|
+
}
|
|
15393
|
+
}
|
|
15394
|
+
const summary = summarizeChatOrganizationQueue(queue);
|
|
15395
|
+
writeChatOrganizationSummary({ queuePath, queue, options });
|
|
15396
|
+
let exitCode = 0;
|
|
15397
|
+
if (summary.failed > 0 || summary.pending > 0) exitCode = 1;
|
|
15398
|
+
process.exit(exitCode);
|
|
15399
|
+
};
|
|
14447
15400
|
var writeArchiveOutcomes = (outcomes, options) => {
|
|
14448
15401
|
if (options.json) {
|
|
14449
15402
|
process.stdout.write(`${JSON.stringify(outcomes)}
|
|
@@ -14556,7 +15509,7 @@ var runTui = async (opts) => {
|
|
|
14556
15509
|
console.log(`
|
|
14557
15510
|
Starting ai-browser-bridge (${label})...`);
|
|
14558
15511
|
const engine = await startEngine({
|
|
14559
|
-
repoPath: opts.repo ?
|
|
15512
|
+
repoPath: opts.repo ? resolve8(opts.repo) : void 0,
|
|
14560
15513
|
provider,
|
|
14561
15514
|
mcpPort: opts.port ? Number(opts.port) : void 0,
|
|
14562
15515
|
withBrowser: opts.browser !== false,
|
|
@@ -14679,7 +15632,7 @@ var runChatgptInspect = async (options) => {
|
|
|
14679
15632
|
};
|
|
14680
15633
|
var startFlowSession = async (options) => {
|
|
14681
15634
|
const engine = await startEngine({
|
|
14682
|
-
repoPath: options.repo ?
|
|
15635
|
+
repoPath: options.repo ? resolve8(options.repo) : void 0,
|
|
14683
15636
|
provider: "flow",
|
|
14684
15637
|
mcpPort: options.port ? Number(options.port) : void 0,
|
|
14685
15638
|
withBrowser: true,
|
|
@@ -14689,7 +15642,7 @@ var startFlowSession = async (options) => {
|
|
|
14689
15642
|
});
|
|
14690
15643
|
return { engine, page: requireBrowserPage(engine) };
|
|
14691
15644
|
};
|
|
14692
|
-
var defaultFlowOutDir = (repoRoot) =>
|
|
15645
|
+
var defaultFlowOutDir = (repoRoot) => join12(downloadsDir(repoRoot), "flow");
|
|
14693
15646
|
var requireClipId = (options, verb) => {
|
|
14694
15647
|
const id2 = options.id?.[0];
|
|
14695
15648
|
if (id2 === void 0 || id2 === "") return fail(`Usage: bridge flow ${verb} --id <clipId>`);
|
|
@@ -14721,7 +15674,7 @@ var runFlowDownload = async (options) => {
|
|
|
14721
15674
|
const { engine, page } = await startFlowSession(options);
|
|
14722
15675
|
const clips = await listClips(page);
|
|
14723
15676
|
const targets = options.id && options.id.length > 0 ? options.id : clips.map((clip) => clip.id);
|
|
14724
|
-
const outDir = options.out ?
|
|
15677
|
+
const outDir = options.out ? resolve8(options.out) : defaultFlowOutDir(engine.config.repoPath);
|
|
14725
15678
|
const results = [];
|
|
14726
15679
|
for (const id2 of targets) {
|
|
14727
15680
|
try {
|
|
@@ -14746,13 +15699,13 @@ var runFlowDownload = async (options) => {
|
|
|
14746
15699
|
process.exit(results.every((download) => download.ok) ? 0 : 1);
|
|
14747
15700
|
};
|
|
14748
15701
|
var runFlowGenerate = async (options) => {
|
|
14749
|
-
const startFramePath = options.start ?
|
|
15702
|
+
const startFramePath = options.start ? resolve8(options.start) : "";
|
|
14750
15703
|
const prompt = options.prompt === void 0 ? "" : options.prompt.trim();
|
|
14751
15704
|
if (!startFramePath || !prompt) {
|
|
14752
15705
|
fail("Usage: bridge flow generate --start <imagePath> --prompt <text> [--out <dir>]");
|
|
14753
15706
|
}
|
|
14754
15707
|
const { engine, page } = await startFlowSession(options);
|
|
14755
|
-
const outDir = options.out ?
|
|
15708
|
+
const outDir = options.out ? resolve8(options.out) : defaultFlowOutDir(engine.config.repoPath);
|
|
14756
15709
|
try {
|
|
14757
15710
|
const clip = await generateClipFromFrame(page, {
|
|
14758
15711
|
startFramePath,
|
|
@@ -14902,7 +15855,7 @@ var subcommandOpts = (command) => {
|
|
|
14902
15855
|
// src/features/terminal/registerCli.ts
|
|
14903
15856
|
var PROVIDER_OPTION = `Browser provider: ${PROVIDER_IDS.join(", ")} (default: ${DEFAULT_PROVIDER})`;
|
|
14904
15857
|
var registerCliCommands = (program) => {
|
|
14905
|
-
program.name("bridge").description("Terminal CLI that bridges ChatGPT or Gemini with local tools via MCP").version("0.
|
|
15858
|
+
program.name("bridge").description("Terminal CLI that bridges ChatGPT or Gemini with local tools via MCP").version("0.5.2").option("-r, --repo <path>", "Path to the target repository (default: cwd)").option("-p, --port <number>", "MCP server port (default: 8765)").option("--provider <name>", PROVIDER_OPTION).option("--no-browser", "Skip Chrome browser connection").action(async (_options, command) => {
|
|
14906
15859
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
14907
15860
|
command.outputHelp({ error: true });
|
|
14908
15861
|
process.exitCode = 1;
|
|
@@ -15031,6 +15984,15 @@ var registerWorkspaceCommands = (program) => {
|
|
|
15031
15984
|
withWorkspaceFlags(chat.command("archive [idOrTitle...]")).description("Archive one or more conversations (reversible \u2014 hides from the sidebar)").option("--id <id...>", "Archive several conversations by id in one session").action(
|
|
15032
15985
|
(chatParts, _options, command) => runChatArchive(chatParts.join(" "), command.optsWithGlobals())
|
|
15033
15986
|
);
|
|
15987
|
+
withWorkspaceFlags(chat.command("organize")).description("Run a resumable, rate-safe Conversation organization queue").requiredOption(
|
|
15988
|
+
"--plan <fileOrJson>",
|
|
15989
|
+
"JSON array of {conversation, project} tasks (inline, @file, or a path)"
|
|
15990
|
+
).option(
|
|
15991
|
+
"--interval <secondsOrRange>",
|
|
15992
|
+
"Seconds between UI operations, fixed or random range such as 60-90 (default: 60)"
|
|
15993
|
+
).option("--cooldown <seconds>", "Seconds to wait after rate limiting (default: 600)").option("--max-attempts <count>", "Attempts per Conversation before failing (default: 4)").option("--restart", "Restart this plan instead of resuming its persisted queue").option("--dry-run", "Validate and show the queue without opening Chrome or writing state").action(
|
|
15994
|
+
(_options, command) => runChatOrganize(command.optsWithGlobals())
|
|
15995
|
+
);
|
|
15034
15996
|
const task = program.command("task").description("List or schedule ChatGPT Tasks (ChatGPT only)");
|
|
15035
15997
|
withWorkspaceFlags(task.command("list")).description("List ChatGPT Scheduled tasks").action(
|
|
15036
15998
|
(_options, command) => runTaskList(command.optsWithGlobals())
|