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/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 mkdir7, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
261
- import { basename as basename3, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join11, relative as relative3, resolve as resolve7 } from "path";
262
- import { Schema as Schema8 } from "effect";
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
- const links = await dialog.locator(CHATGPT_SEARCH.resultLink).all();
424
- const entries = [];
425
- for (const link of links) {
426
- const href = await link.getAttribute("href");
427
- if (href === null) continue;
428
- const title = await link.locator(CHATGPT_SEARCH.resultTitle).first().innerText();
429
- entries.push({ href, title });
430
- }
431
- return entries;
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 open = await readOpenConnectorSummary({ page: ctx.page });
1839
- if (!open || !sameConnectorApp({ a: open, b: ctx.target })) continue;
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
- if (!href || !title) return null;
2421
- const pathSegment = href.split("/").pop();
2422
- const id2 = pathSegment === void 0 ? "" : pathSegment;
2423
- return { id: id2, title: title.trim(), url: `https://chatgpt.com${href}` };
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
- chatLink: 'nav a[href^="/c/"]',
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
- if (!await moveItem.isVisible({ timeout: 2e3 }).catch(() => false)) {
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
- for (let attempt = 0; attempt < 2 && !visible; attempt += 1) {
3295
- if (attempt > 0) await moveItem.click().catch(() => {
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 < 8 && !visible; poll += 1) {
3298
- visible = await target.isVisible({ timeout: 350 }).catch(() => false);
3299
- if (!visible) await page.waitForTimeout(150);
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^="/c/${id2}"]`).first();
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
- const activeConversationId = chatGptConversationIdFromUrl(page.url());
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^="/c/"]');
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^="/c/"]');
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
- const targetConversationId = stripConversationId(chat);
3434
- const activeConversationId = chatGptConversationIdFromUrl(page.url());
3435
- if (activeConversationId !== targetConversationId) return false;
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
- const menuVisible = await page.locator(`${WORKSPACE.menuItem}:visible`).first().waitFor({ state: "visible", timeout: 2e3 }).then(() => true).catch(() => false);
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: mkdir8, writeFile: writeFile6 } = await import("fs/promises");
3536
- const { join: join12 } = await import("path");
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 mkdir8(outDir, { recursive: true });
3543
- const dest = join12(outDir, `${clipId}.mp4`);
3544
- await writeFile6(dest, clipBytes);
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((resolve8) => {
3768
- setTimeout(resolve8, ms);
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((resolve8, reject) => {
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
- resolve8(outcome.url);
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((resolve8) => {
9911
+ return new Promise((resolve9) => {
9570
9912
  execFile2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoPath }, (error, stdout) => {
9571
9913
  if (error) {
9572
- resolve8(void 0);
9914
+ resolve9(void 0);
9573
9915
  return;
9574
9916
  }
9575
9917
  const branch = stdout.trim();
9576
9918
  if (branch.length === 0) {
9577
- resolve8(void 0);
9919
+ resolve9(void 0);
9578
9920
  return;
9579
9921
  }
9580
- resolve8(branch);
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((resolve8, reject) => {
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
- resolve8(value);
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((resolve8) => {
11141
+ await new Promise((resolve9) => {
10800
11142
  const priorOnClose = transport.onclose;
10801
11143
  transport.onclose = () => {
10802
11144
  if (priorOnClose !== void 0) priorOnClose();
10803
- resolve8();
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 (isAbsolute(partial) || partial.startsWith("/")) return true;
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 (isAbsolute2(params.input)) {
12931
- const rel = relative3(resolve7(params.repoRoot), resolve7(params.input));
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 = join11(screenshotsDir(repoPath), stamp);
12969
- await mkdir7(dir, { recursive: true });
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 = join11(input.dir, `${input.viewport.name}.png`);
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] ? resolve7(input.parts[1]) : void 0
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: resolve7(first)
13654
+ outputPath: resolve8(first)
13025
13655
  };
13026
13656
  };
13027
13657
  var defaultExportPath = (params) => {
13028
- return join11(exportsDir(params.repoPath), `${params.sessionId}.md`);
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 mkdir7(dirname3(input.targetPath), { recursive: true });
13333
- await writeFile5(input.targetPath, content, "utf-8");
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 ? resolve7(options.profile) : bridgeChromeProfileRoot();
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 = isAbsolute2(path2) ? path2 : resolve7(process.cwd(), path2);
13854
- return readFile10(absolute, "utf8");
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 Schema8.decodeUnknownSync(FanoutTasksSchema)(parsed);
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 ? resolve7(input.options.repo) : void 0,
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 ? resolve7(value) : void 0;
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 resolve7(repoRoot, rel);
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 ? resolve7(options.repo) : void 0,
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 ? resolve7(options.repo) : void 0,
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
- if (options.json) process.stdout.write(`${JSON.stringify(chats)}
14357
- `);
14358
- else if (chats.length === 0) process.stdout.write("No conversations.\n");
14359
- else for (const chat of chats) process.stdout.write(`${chat.id} ${chat.title}
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 ? resolve7(opts.repo) : void 0,
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 ? resolve7(options.repo) : void 0,
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) => join11(downloadsDir(repoRoot), "flow");
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 ? resolve7(options.out) : defaultFlowOutDir(engine.config.repoPath);
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 ? resolve7(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 ? resolve7(options.out) : defaultFlowOutDir(engine.config.repoPath);
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.4.0").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) => {
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())