ai-browser-bridge 0.4.0 → 0.5.1

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()];
2424
2537
  };
2425
- var readSidebarConversations = async (page) => {
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
+ }
2547
+ };
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,298 @@ 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 chatOrganizationIntervalFrom = (value, defaultSeconds) => {
11199
+ if (value === void 0) {
11200
+ return { minimumSeconds: defaultSeconds, maximumSeconds: defaultSeconds };
11201
+ }
11202
+ const match = ORGANIZATION_INTERVAL.exec(value.trim());
11203
+ const minimumText = match?.groups?.minimum;
11204
+ const maximumText = match?.groups?.maximum;
11205
+ if (minimumText === void 0) {
11206
+ throw new Error("--interval must be whole seconds or a range such as 10-20.");
11207
+ }
11208
+ const minimumSeconds = Number.parseInt(minimumText, 10);
11209
+ let maximumSeconds = minimumSeconds;
11210
+ if (maximumText !== void 0) maximumSeconds = Number.parseInt(maximumText, 10);
11211
+ if (minimumSeconds <= 0 || maximumSeconds <= 0 || minimumSeconds > maximumSeconds) {
11212
+ throw new Error("--interval must be a positive ascending range such as 10-20.");
11213
+ }
11214
+ return { minimumSeconds, maximumSeconds };
11215
+ };
11216
+ var nextChatOrganizationIntervalSeconds = (interval, randomFraction) => {
11217
+ if (interval.minimumSeconds === interval.maximumSeconds) return interval.minimumSeconds;
11218
+ const width = interval.maximumSeconds - interval.minimumSeconds + 1;
11219
+ return interval.minimumSeconds + Math.floor(randomFraction * width);
11220
+ };
11221
+ var chatOrganizationIntervalLabel = (interval) => {
11222
+ if (interval.minimumSeconds === interval.maximumSeconds) return `${interval.minimumSeconds}s`;
11223
+ return `${interval.minimumSeconds}-${interval.maximumSeconds}s`;
11224
+ };
11225
+ var readOrganizationPlanSource = async (spec, launchDirectory) => {
11226
+ const trimmed = spec.trim();
11227
+ if (trimmed.startsWith("[")) return trimmed;
11228
+ let sourcePath = trimmed;
11229
+ if (trimmed.startsWith("@")) sourcePath = trimmed.slice(1);
11230
+ let absolutePath = sourcePath;
11231
+ if (!isAbsolute(sourcePath)) absolutePath = resolve7(launchDirectory, sourcePath);
11232
+ return readFile10(absolutePath, "utf8");
11233
+ };
11234
+ var normalizedOrganizationTasks = (tasks) => {
11235
+ const normalized = [];
11236
+ for (const task of tasks) {
11237
+ const conversation = task.conversation.trim();
11238
+ const project = task.project.trim();
11239
+ if (!conversation) throw new Error("Every organization task needs a Conversation id or title.");
11240
+ if (!project) throw new Error("Every organization task needs a Project name.");
11241
+ normalized.push({ conversation, project });
11242
+ }
11243
+ return normalized;
11244
+ };
11245
+ var loadChatOrganizationTasks = async (spec, launchDirectory) => {
11246
+ if (!spec.trim()) throw new Error("--plan needs a file, @file, or inline JSON array.");
11247
+ const source = await readOrganizationPlanSource(spec, launchDirectory);
11248
+ let parsed;
11249
+ try {
11250
+ parsed = JSON.parse(source);
11251
+ } catch (error) {
11252
+ throw new Error(
11253
+ `Organization plan is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
11254
+ );
11255
+ }
11256
+ const tasks = Schema8.decodeUnknownSync(ChatOrganizationTasksSchema)(parsed);
11257
+ return normalizedOrganizationTasks(tasks);
11258
+ };
11259
+ var organizationPlanFingerprint = (tasks) => {
11260
+ return createHash2("sha256").update(JSON.stringify(tasks)).digest("hex").slice(0, 16);
11261
+ };
11262
+ var chatOrganizationQueuePath = (repoRoot, tasks) => {
11263
+ return join11(
11264
+ bridgeDir(repoRoot),
11265
+ "chat-organization-queues",
11266
+ `${organizationPlanFingerprint(tasks)}.json`
11267
+ );
11268
+ };
11269
+ var newChatOrganizationQueue = (tasks, timestamp) => {
11270
+ return {
11271
+ version: 1,
11272
+ fingerprint: organizationPlanFingerprint(tasks),
11273
+ createdAt: timestamp,
11274
+ updatedAt: timestamp,
11275
+ items: tasks.map((task) => ({
11276
+ status: "pending",
11277
+ conversation: task.conversation,
11278
+ project: task.project,
11279
+ attempts: 0
11280
+ }))
11281
+ };
11282
+ };
11283
+ var persistChatOrganizationQueue = async (queuePath, queue) => {
11284
+ await mkdir7(dirname3(queuePath), { recursive: true });
11285
+ const pendingPath = `${queuePath}.${process.pid}.${randomUUID4()}.pending`;
11286
+ try {
11287
+ await writeFile5(pendingPath, `${JSON.stringify(queue, null, 2)}
11288
+ `, "utf8");
11289
+ await rename(pendingPath, queuePath);
11290
+ } finally {
11291
+ try {
11292
+ await unlink(pendingPath);
11293
+ } catch {
11294
+ }
11295
+ }
11296
+ };
11297
+ var errorCodeOf = (error) => {
11298
+ if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
11299
+ if (typeof error.code !== "string") return void 0;
11300
+ return error.code;
11301
+ };
11302
+ var removeStaleQueueLock = async (lockPath) => {
11303
+ let owner;
11304
+ try {
11305
+ owner = JSON.parse(await readFile10(lockPath, "utf8"));
11306
+ } catch {
11307
+ return false;
11308
+ }
11309
+ if (typeof owner !== "object" || owner === null || !("pid" in owner)) return false;
11310
+ if (typeof owner.pid !== "number" || !Number.isInteger(owner.pid)) return false;
11311
+ try {
11312
+ process.kill(owner.pid, 0);
11313
+ return false;
11314
+ } catch (error) {
11315
+ if (errorCodeOf(error) !== "ESRCH") return false;
11316
+ }
11317
+ await unlink(lockPath);
11318
+ return true;
11319
+ };
11320
+ var acquireChatOrganizationQueueLock = async (queuePath) => {
11321
+ await mkdir7(dirname3(queuePath), { recursive: true });
11322
+ const lockPath = `${queuePath}.lock`;
11323
+ for (let attempt = 0; attempt < 2; attempt += 1) {
11324
+ try {
11325
+ const handle = await open(lockPath, "wx");
11326
+ await handle.writeFile(
11327
+ `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString() })}
11328
+ `
11329
+ );
11330
+ let released = false;
11331
+ return async () => {
11332
+ if (released) return;
11333
+ released = true;
11334
+ await handle.close();
11335
+ await unlink(lockPath);
11336
+ };
11337
+ } catch (error) {
11338
+ if (errorCodeOf(error) !== "EEXIST") throw error;
11339
+ if (attempt === 0 && await removeStaleQueueLock(lockPath)) continue;
11340
+ throw new Error(`Organization queue is already running: ${lockPath}`);
11341
+ }
11342
+ }
11343
+ throw new Error(`Could not acquire organization queue lock: ${lockPath}`);
11344
+ };
11345
+ var openChatOrganizationQueue = async (input) => {
11346
+ const path2 = chatOrganizationQueuePath(input.repoRoot, input.tasks);
11347
+ if (!input.restart && existsSync(path2)) {
11348
+ const stored = JSON.parse(await readFile10(path2, "utf8"));
11349
+ const queue2 = Schema8.decodeUnknownSync(ChatOrganizationQueueSchema)(stored);
11350
+ const expectedFingerprint = organizationPlanFingerprint(input.tasks);
11351
+ if (queue2.fingerprint !== expectedFingerprint) {
11352
+ throw new Error(`Stored queue fingerprint does not match ${path2}. Use --restart.`);
11353
+ }
11354
+ return { path: path2, queue: queue2 };
11355
+ }
11356
+ const queue = newChatOrganizationQueue(input.tasks, input.timestamp);
11357
+ await persistChatOrganizationQueue(path2, queue);
11358
+ return { path: path2, queue };
11359
+ };
11360
+ var nextChatOrganizationQueueIndex = (queue) => {
11361
+ const index = queue.items.findIndex((item) => item.status === "pending");
11362
+ if (index < 0) return void 0;
11363
+ return index;
11364
+ };
11365
+ var completeChatOrganizationQueueItem = (input) => {
11366
+ const current = input.queue.items[input.index];
11367
+ if (current === void 0 || current.status !== "pending") return input.queue;
11368
+ const items = [...input.queue.items];
11369
+ items[input.index] = {
11370
+ status: input.status,
11371
+ conversation: current.conversation,
11372
+ project: current.project,
11373
+ attempts: current.attempts + 1,
11374
+ completedAt: input.timestamp
11375
+ };
11376
+ return { ...input.queue, updatedAt: input.timestamp, items };
11377
+ };
11378
+ var deferChatOrganizationQueueItem = (input) => {
11379
+ const current = input.queue.items[input.index];
11380
+ if (current === void 0 || current.status !== "pending") return input.queue;
11381
+ const items = [...input.queue.items];
11382
+ items[input.index] = {
11383
+ status: "pending",
11384
+ conversation: current.conversation,
11385
+ project: current.project,
11386
+ attempts: current.attempts,
11387
+ lastReason: input.reason
11388
+ };
11389
+ return { ...input.queue, updatedAt: input.timestamp, items };
11390
+ };
11391
+ var failChatOrganizationQueueItem = (input) => {
11392
+ const current = input.queue.items[input.index];
11393
+ if (current === void 0 || current.status !== "pending") return input.queue;
11394
+ const attempts = current.attempts + 1;
11395
+ const items = [...input.queue.items];
11396
+ if (attempts >= input.maxAttempts) {
11397
+ items[input.index] = {
11398
+ status: "failed",
11399
+ conversation: current.conversation,
11400
+ project: current.project,
11401
+ attempts,
11402
+ reason: input.reason,
11403
+ completedAt: input.timestamp
11404
+ };
11405
+ } else {
11406
+ items[input.index] = {
11407
+ status: "pending",
11408
+ conversation: current.conversation,
11409
+ project: current.project,
11410
+ attempts,
11411
+ lastReason: input.reason
11412
+ };
11413
+ }
11414
+ return { ...input.queue, updatedAt: input.timestamp, items };
11415
+ };
11416
+ var summarizeChatOrganizationQueue = (queue) => {
11417
+ const summary = {
11418
+ total: queue.items.length,
11419
+ pending: 0,
11420
+ moved: 0,
11421
+ alreadyFiled: 0,
11422
+ failed: 0
11423
+ };
11424
+ for (const item of queue.items) {
11425
+ if (item.status === "pending") summary.pending += 1;
11426
+ else if (item.status === "moved") summary.moved += 1;
11427
+ else if (item.status === "already-filed") summary.alreadyFiled += 1;
11428
+ else summary.failed += 1;
11429
+ }
11430
+ return summary;
11431
+ };
11432
+
10808
11433
  // src/features/terminal/providerLabel.ts
10809
11434
  var providerDisplayName = (id2) => {
10810
11435
  return PROVIDER_CONFIG[id2].displayName;
@@ -11385,7 +12010,7 @@ var IGNORED_COMPLETION_ENTRIES = /* @__PURE__ */ new Set([
11385
12010
  ]);
11386
12011
 
11387
12012
  // src/features/terminal/tui/composer/fileMentionCompletion.ts
11388
- import { isAbsolute, sep as sep2 } from "path";
12013
+ import { isAbsolute as isAbsolute2, sep as sep2 } from "path";
11389
12014
  var findActiveFileMention = (input) => {
11390
12015
  const position = input.cursor === void 0 ? input.input.length : input.cursor;
11391
12016
  return parseActiveFileMention({ text: input.input, position });
@@ -11411,7 +12036,7 @@ var normalizePartialPath = (partial) => {
11411
12036
  return partial.replaceAll("\\", "/").replaceAll(sep2, "/");
11412
12037
  };
11413
12038
  var isUnsafePartial = (partial) => {
11414
- if (isAbsolute(partial) || partial.startsWith("/")) return true;
12039
+ if (isAbsolute2(partial) || partial.startsWith("/")) return true;
11415
12040
  return partial.split("/").filter(Boolean).includes("..");
11416
12041
  };
11417
12042
  var compareCompletionMatches = (left, right) => {
@@ -12927,8 +13552,8 @@ var sessionIdFrom = async (params) => {
12927
13552
  return latest.metadata.id;
12928
13553
  };
12929
13554
  var repositoryFileFrom = (params) => {
12930
- if (isAbsolute2(params.input)) {
12931
- const rel = relative3(resolve7(params.repoRoot), resolve7(params.input));
13555
+ if (isAbsolute3(params.input)) {
13556
+ const rel = relative3(resolve8(params.repoRoot), resolve8(params.input));
12932
13557
  return repositoryPath(params.repoRoot, rel || ".");
12933
13558
  }
12934
13559
  return repositoryPath(params.repoRoot, params.input);
@@ -12965,8 +13590,8 @@ var parseCaptureUrl = (url) => {
12965
13590
  };
12966
13591
  var prepareScreenshotDir = async (repoPath) => {
12967
13592
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
12968
- const dir = join11(screenshotsDir(repoPath), stamp);
12969
- await mkdir7(dir, { recursive: true });
13593
+ const dir = join12(screenshotsDir(repoPath), stamp);
13594
+ await mkdir8(dir, { recursive: true });
12970
13595
  return dir;
12971
13596
  };
12972
13597
  var captureWithPlaywright = async (params) => {
@@ -12998,7 +13623,7 @@ var captureViewport = async (params) => {
12998
13623
  return file;
12999
13624
  };
13000
13625
  var writeViewportScreenshot = async (input) => {
13001
- const file = join11(input.dir, `${input.viewport.name}.png`);
13626
+ const file = join12(input.dir, `${input.viewport.name}.png`);
13002
13627
  await input.page.screenshot({ path: file, fullPage: true });
13003
13628
  return file;
13004
13629
  };
@@ -13016,16 +13641,16 @@ var sessionExportFromParts = async (input) => {
13016
13641
  if (session !== void 0 && session !== null) {
13017
13642
  return {
13018
13643
  sessionId: session.metadata.id,
13019
- outputPath: input.parts[1] ? resolve7(input.parts[1]) : void 0
13644
+ outputPath: input.parts[1] ? resolve8(input.parts[1]) : void 0
13020
13645
  };
13021
13646
  }
13022
13647
  return {
13023
13648
  sessionId: await sessionIdFrom({ args: "", ctx: input.ctx }),
13024
- outputPath: resolve7(first)
13649
+ outputPath: resolve8(first)
13025
13650
  };
13026
13651
  };
13027
13652
  var defaultExportPath = (params) => {
13028
- return join11(exportsDir(params.repoPath), `${params.sessionId}.md`);
13653
+ return join12(exportsDir(params.repoPath), `${params.sessionId}.md`);
13029
13654
  };
13030
13655
  var exportContentForPath = (params) => {
13031
13656
  const extension = extname2(params.path).toLowerCase();
@@ -13329,8 +13954,8 @@ var writeSessionExport = async (params) => {
13329
13954
  };
13330
13955
  var persistSessionExport = async (input) => {
13331
13956
  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");
13957
+ await mkdir8(dirname4(input.targetPath), { recursive: true });
13958
+ await writeFile6(input.targetPath, content, "utf-8");
13334
13959
  console.log(`Exported ${input.sessionId} to ${input.targetPath}`);
13335
13960
  };
13336
13961
  var listCheckpointsCommand = async (_args, ctx) => {
@@ -13710,7 +14335,7 @@ var runBrowserStatus = async (options = {}) => {
13710
14335
  process.exit(0);
13711
14336
  };
13712
14337
  var cacheProfileRoot = (options) => {
13713
- return options.profile ? resolve7(options.profile) : bridgeChromeProfileRoot();
14338
+ return options.profile ? resolve8(options.profile) : bridgeChromeProfileRoot();
13714
14339
  };
13715
14340
  var runCacheList = async (options) => {
13716
14341
  const inventory = await inventoryChromeCache({
@@ -13850,8 +14475,8 @@ var readFanoutSource = async (spec) => {
13850
14475
  const trimmed = spec.trim();
13851
14476
  if (trimmed.startsWith("[")) return trimmed;
13852
14477
  const path2 = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
13853
- const absolute = isAbsolute2(path2) ? path2 : resolve7(process.cwd(), path2);
13854
- return readFile10(absolute, "utf8");
14478
+ const absolute = isAbsolute3(path2) ? path2 : resolve8(process.cwd(), path2);
14479
+ return readFile11(absolute, "utf8");
13855
14480
  };
13856
14481
  var loadFanoutTasks = async (spec) => {
13857
14482
  if (!spec.trim()) return fail("--fan-out needs a task file, @file, or inline JSON array.");
@@ -13865,7 +14490,7 @@ var loadFanoutTasks = async (spec) => {
13865
14490
  return fail(`--fan-out is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
13866
14491
  }
13867
14492
  try {
13868
- return Schema8.decodeUnknownSync(FanoutTasksSchema)(parsed);
14493
+ return Schema9.decodeUnknownSync(FanoutTasksSchema)(parsed);
13869
14494
  } catch (err) {
13870
14495
  return fail(
13871
14496
  `--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 +14725,7 @@ var finishAskRun = async (input) => {
14100
14725
  var startAskEngine = async (input) => {
14101
14726
  const withTools = Boolean(input.options.tools) && input.supportsMcpConnector;
14102
14727
  return startEngine({
14103
- repoPath: input.options.repo ? resolve7(input.options.repo) : void 0,
14728
+ repoPath: input.options.repo ? resolve8(input.options.repo) : void 0,
14104
14729
  provider: input.provider,
14105
14730
  mcpPort: input.options.port ? Number(input.options.port) : void 0,
14106
14731
  withBrowser: true,
@@ -14125,7 +14750,7 @@ var debugPortFromOption = (value) => {
14125
14750
  return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
14126
14751
  };
14127
14752
  var profileRootFromOption = (value) => {
14128
- return value ? resolve7(value) : void 0;
14753
+ return value ? resolve8(value) : void 0;
14129
14754
  };
14130
14755
  var runAskTurn = async (input) => {
14131
14756
  await applyAskPreflight({ engine: input.engine, options: input.options });
@@ -14143,7 +14768,7 @@ var attachAskFiles = async (input) => {
14143
14768
  const resolved = paths.map((target) => {
14144
14769
  const rel = repositoryFileFrom({ repoRoot, input: target });
14145
14770
  assertImagePath(rel);
14146
- return resolve7(repoRoot, rel);
14771
+ return resolve8(repoRoot, rel);
14147
14772
  });
14148
14773
  await input.engine.getOrchestrator().attachFiles(resolved);
14149
14774
  };
@@ -14250,7 +14875,7 @@ var downloadAfterExtract = async (input) => {
14250
14875
  };
14251
14876
  var startDownloadEngine = async (options) => {
14252
14877
  return startEngine({
14253
- repoPath: options.repo ? resolve7(options.repo) : void 0,
14878
+ repoPath: options.repo ? resolve8(options.repo) : void 0,
14254
14879
  provider: providerIdFrom(options.provider),
14255
14880
  mcpPort: options.port ? Number(options.port) : void 0,
14256
14881
  withBrowser: true,
@@ -14279,7 +14904,7 @@ var assertChatgptWorkspace = (options) => {
14279
14904
  };
14280
14905
  var startWorkspaceSession = async (options) => {
14281
14906
  const engine = await startEngine({
14282
- repoPath: options.repo ? resolve7(options.repo) : void 0,
14907
+ repoPath: options.repo ? resolve8(options.repo) : void 0,
14283
14908
  provider: "chatgpt",
14284
14909
  mcpPort: options.port ? Number(options.port) : void 0,
14285
14910
  withBrowser: true,
@@ -14351,13 +14976,20 @@ var runProjectDelete = async (name, options) => {
14351
14976
  var runChatList = async (options) => {
14352
14977
  assertChatgptWorkspace(options);
14353
14978
  const { engine } = await startWorkspaceSession(options);
14354
- const chats = await engine.getOrchestrator().listConversations();
14979
+ const chats = await engine.getOrchestrator().listConversations({ orphans: Boolean(options.orphans) });
14355
14980
  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
- `);
14981
+ let output = "";
14982
+ if (options.json) output = `${JSON.stringify(chats)}
14983
+ `;
14984
+ else if (chats.length === 0) output = "No conversations.\n";
14985
+ else output = `${chats.map((chat) => `${chat.id} ${chat.title}`).join("\n")}
14986
+ `;
14987
+ await new Promise((resolve9, reject) => {
14988
+ process.stdout.write(output, (error) => {
14989
+ if (error) reject(error);
14990
+ else resolve9();
14991
+ });
14992
+ });
14361
14993
  process.exit(0);
14362
14994
  };
14363
14995
  var runChatSearch = async (query, options) => {
@@ -14444,6 +15076,292 @@ var runChatMove = async (chat, options) => {
14444
15076
  writeMoveOutcomes(outcomes, options);
14445
15077
  process.exit(outcomes.every((outcome) => outcome.moved || outcome.alreadyFiled) ? 0 : 1);
14446
15078
  };
15079
+ var DEFAULT_ORGANIZATION_INTERVAL_SECONDS = 30;
15080
+ var DEFAULT_ORGANIZATION_COOLDOWN_SECONDS = 300;
15081
+ var DEFAULT_ORGANIZATION_MAX_ATTEMPTS = 3;
15082
+ var MAX_CONSECUTIVE_ORGANIZATION_COOLDOWNS = 12;
15083
+ var RATE_LIMIT_REASON = "ChatGPT temporarily limited access to Conversation history.";
15084
+ var positiveIntegerOption = (value, optionName, defaultValue) => {
15085
+ if (value === void 0) return defaultValue;
15086
+ if (!/^[1-9]\d*$/u.test(value.trim())) {
15087
+ return fail(`${optionName} must be a positive whole number.`);
15088
+ }
15089
+ const parsed = Number.parseInt(value, 10);
15090
+ return parsed;
15091
+ };
15092
+ var organizationIntervalOption = (value) => {
15093
+ try {
15094
+ return chatOrganizationIntervalFrom(value, DEFAULT_ORGANIZATION_INTERVAL_SECONDS);
15095
+ } catch (error) {
15096
+ return fail(error instanceof Error ? error.message : String(error));
15097
+ }
15098
+ };
15099
+ var waitForOrganizationInterval = async (seconds) => {
15100
+ await new Promise((complete) => setTimeout(complete, seconds * 1e3));
15101
+ };
15102
+ var writeChatOrganizationProgress = (message, options) => {
15103
+ if (options.json) {
15104
+ process.stderr.write(`${message}
15105
+ `);
15106
+ return;
15107
+ }
15108
+ process.stdout.write(`${message}
15109
+ `);
15110
+ };
15111
+ var moveFailureReason = (outcome) => {
15112
+ if (outcome.reason?.trim()) return outcome.reason;
15113
+ return "ChatGPT did not confirm the Conversation move.";
15114
+ };
15115
+ var writeChatOrganizationSummary = (input) => {
15116
+ const summary = summarizeChatOrganizationQueue(input.queue);
15117
+ if (input.options.json) {
15118
+ process.stdout.write(`${JSON.stringify({ queuePath: input.queuePath, summary })}
15119
+ `);
15120
+ return;
15121
+ }
15122
+ process.stdout.write(
15123
+ `${[
15124
+ "Organization queue finished.",
15125
+ `Moved: ${summary.moved}`,
15126
+ `Already filed: ${summary.alreadyFiled}`,
15127
+ `Failed: ${summary.failed}`,
15128
+ `Queue state: ${input.queuePath}`
15129
+ ].join("\n")}
15130
+ `
15131
+ );
15132
+ };
15133
+ var writeChatOrganizationDryRun = (input) => {
15134
+ const preview = {
15135
+ dryRun: true,
15136
+ queuePath: input.queuePath,
15137
+ interval: input.interval,
15138
+ cooldownSeconds: input.cooldownSeconds,
15139
+ maxAttempts: input.maxAttempts,
15140
+ tasks: input.tasks
15141
+ };
15142
+ if (input.options.json) {
15143
+ process.stdout.write(`${JSON.stringify(preview)}
15144
+ `);
15145
+ return;
15146
+ }
15147
+ process.stdout.write(
15148
+ `${[
15149
+ `Organization queue dry run: ${input.tasks.length} Conversations`,
15150
+ `Interval: ${chatOrganizationIntervalLabel(input.interval)}`,
15151
+ `Rate-limit cooldown: ${input.cooldownSeconds}s`,
15152
+ `Attempts per Conversation: ${input.maxAttempts}`,
15153
+ `Resume state: ${input.queuePath}`,
15154
+ ...input.tasks.map((task) => ` ${task.conversation} -> ${task.project}`)
15155
+ ].join("\n")}
15156
+ `
15157
+ );
15158
+ };
15159
+ var recordChatOrganizationAttempt = async (input) => {
15160
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
15161
+ let queue;
15162
+ if (input.rateLimited) {
15163
+ queue = deferChatOrganizationQueueItem({
15164
+ queue: input.queue,
15165
+ index: input.index,
15166
+ reason: RATE_LIMIT_REASON,
15167
+ timestamp
15168
+ });
15169
+ } else if (input.moveOutcome?.moved) {
15170
+ queue = completeChatOrganizationQueueItem({
15171
+ queue: input.queue,
15172
+ index: input.index,
15173
+ status: "moved",
15174
+ timestamp
15175
+ });
15176
+ } else if (input.moveOutcome?.alreadyFiled) {
15177
+ queue = completeChatOrganizationQueueItem({
15178
+ queue: input.queue,
15179
+ index: input.index,
15180
+ status: "already-filed",
15181
+ timestamp
15182
+ });
15183
+ } else {
15184
+ let reason = "ChatGPT did not complete the Conversation move.";
15185
+ if (input.moveOutcome !== void 0) reason = moveFailureReason(input.moveOutcome);
15186
+ else if (input.thrownReason !== void 0) reason = input.thrownReason;
15187
+ queue = failChatOrganizationQueueItem({
15188
+ queue: input.queue,
15189
+ index: input.index,
15190
+ reason,
15191
+ maxAttempts: input.maxAttempts,
15192
+ timestamp
15193
+ });
15194
+ }
15195
+ await persistChatOrganizationQueue(input.queuePath, queue);
15196
+ return queue;
15197
+ };
15198
+ var runChatOrganize = async (options) => {
15199
+ assertChatgptWorkspace(options);
15200
+ const plan = options.plan;
15201
+ if (plan === void 0 || !plan.trim()) {
15202
+ return fail("Usage: bridge chat organize --plan <fileOrJson>");
15203
+ }
15204
+ const interval = organizationIntervalOption(options.interval);
15205
+ const cooldownSeconds = positiveIntegerOption(
15206
+ options.cooldown,
15207
+ "--cooldown",
15208
+ DEFAULT_ORGANIZATION_COOLDOWN_SECONDS
15209
+ );
15210
+ const maxAttempts = positiveIntegerOption(
15211
+ options.maxAttempts,
15212
+ "--max-attempts",
15213
+ DEFAULT_ORGANIZATION_MAX_ATTEMPTS
15214
+ );
15215
+ let tasks;
15216
+ try {
15217
+ tasks = await loadChatOrganizationTasks(plan, process.cwd());
15218
+ } catch (error) {
15219
+ return fail(error instanceof Error ? error.message : String(error));
15220
+ }
15221
+ const repoRoot = repositoryRoot(options.repo);
15222
+ const queuePath = chatOrganizationQueuePath(repoRoot, tasks);
15223
+ if (options.dryRun) {
15224
+ writeChatOrganizationDryRun({
15225
+ tasks,
15226
+ queuePath,
15227
+ interval,
15228
+ cooldownSeconds,
15229
+ maxAttempts,
15230
+ options
15231
+ });
15232
+ process.exit(0);
15233
+ }
15234
+ let releaseQueueLock;
15235
+ try {
15236
+ releaseQueueLock = await acquireChatOrganizationQueueLock(queuePath);
15237
+ } catch (error) {
15238
+ return fail(error instanceof Error ? error.message : String(error));
15239
+ }
15240
+ let queue;
15241
+ try {
15242
+ const opened = await openChatOrganizationQueue({
15243
+ repoRoot,
15244
+ tasks,
15245
+ restart: Boolean(options.restart),
15246
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
15247
+ });
15248
+ queue = opened.queue;
15249
+ } catch (error) {
15250
+ await releaseQueueLock();
15251
+ return fail(
15252
+ `Could not open the organization queue: ${error instanceof Error ? error.message : String(error)}`
15253
+ );
15254
+ }
15255
+ const initialSummary = summarizeChatOrganizationQueue(queue);
15256
+ writeChatOrganizationProgress(
15257
+ `Organization queue: ${initialSummary.pending} pending of ${initialSummary.total}; state ${queuePath}`,
15258
+ options
15259
+ );
15260
+ if (initialSummary.pending === 0) {
15261
+ writeChatOrganizationSummary({ queuePath, queue, options });
15262
+ await releaseQueueLock();
15263
+ process.exit(initialSummary.failed === 0 ? 0 : 1);
15264
+ }
15265
+ let workspace;
15266
+ try {
15267
+ workspace = await startWorkspaceSession(options);
15268
+ } catch (error) {
15269
+ await releaseQueueLock();
15270
+ throw error;
15271
+ }
15272
+ const { engine, page } = workspace;
15273
+ try {
15274
+ let index = nextChatOrganizationQueueIndex(queue);
15275
+ let consecutiveRateLimitCooldowns = 0;
15276
+ while (index !== void 0) {
15277
+ const item = queue.items[index];
15278
+ if (item === void 0 || item.status !== "pending") {
15279
+ index = nextChatOrganizationQueueIndex(queue);
15280
+ continue;
15281
+ }
15282
+ if (await acknowledgeChatGptHistoryRateLimit(page)) {
15283
+ consecutiveRateLimitCooldowns += 1;
15284
+ if (consecutiveRateLimitCooldowns >= MAX_CONSECUTIVE_ORGANIZATION_COOLDOWNS) {
15285
+ writeChatOrganizationProgress(
15286
+ `ChatGPT remained rate-limited after ${consecutiveRateLimitCooldowns} cooldowns; leaving the queue pending for a later resume.`,
15287
+ options
15288
+ );
15289
+ break;
15290
+ }
15291
+ writeChatOrganizationProgress(
15292
+ `ChatGPT rate limit detected; cooling down for ${cooldownSeconds}s before continuing.`,
15293
+ options
15294
+ );
15295
+ await waitForOrganizationInterval(cooldownSeconds);
15296
+ continue;
15297
+ }
15298
+ consecutiveRateLimitCooldowns = 0;
15299
+ writeChatOrganizationProgress(
15300
+ `Moving "${item.conversation}" -> ${item.project} (attempt ${item.attempts + 1}/${maxAttempts})`,
15301
+ options
15302
+ );
15303
+ let moveOutcome;
15304
+ let thrownReason;
15305
+ try {
15306
+ moveOutcome = await moveChatToProject(page, {
15307
+ chat: item.conversation,
15308
+ project: item.project
15309
+ });
15310
+ } catch (error) {
15311
+ thrownReason = error instanceof Error ? error.message : String(error);
15312
+ }
15313
+ const rateLimited = await acknowledgeChatGptHistoryRateLimit(page);
15314
+ queue = await recordChatOrganizationAttempt({
15315
+ queue,
15316
+ queuePath,
15317
+ index,
15318
+ maxAttempts,
15319
+ rateLimited,
15320
+ moveOutcome,
15321
+ thrownReason
15322
+ });
15323
+ const recorded = queue.items[index];
15324
+ if (recorded?.status === "moved") {
15325
+ writeChatOrganizationProgress(`Moved "${recorded.conversation}".`, options);
15326
+ } else if (recorded?.status === "already-filed") {
15327
+ writeChatOrganizationProgress(`Already filed "${recorded.conversation}".`, options);
15328
+ } else if (recorded?.status === "failed") {
15329
+ writeChatOrganizationProgress(
15330
+ `Failed "${recorded.conversation}" after ${recorded.attempts} attempts: ${recorded.reason}`,
15331
+ options
15332
+ );
15333
+ } else if (recorded?.status === "pending") {
15334
+ let reason = "unknown failure";
15335
+ if (recorded.lastReason !== void 0) reason = recorded.lastReason;
15336
+ writeChatOrganizationProgress(`Will retry "${recorded.conversation}": ${reason}`, options);
15337
+ }
15338
+ let waitSeconds = nextChatOrganizationIntervalSeconds(interval, Math.random());
15339
+ if (rateLimited) {
15340
+ consecutiveRateLimitCooldowns += 1;
15341
+ waitSeconds = cooldownSeconds;
15342
+ }
15343
+ index = nextChatOrganizationQueueIndex(queue);
15344
+ if (index !== void 0) {
15345
+ writeChatOrganizationProgress(
15346
+ `Waiting ${waitSeconds}s before the next UI operation.`,
15347
+ options
15348
+ );
15349
+ await waitForOrganizationInterval(waitSeconds);
15350
+ }
15351
+ }
15352
+ } finally {
15353
+ try {
15354
+ await engine.shutdown({ closeBrowser: false });
15355
+ } finally {
15356
+ await releaseQueueLock();
15357
+ }
15358
+ }
15359
+ const summary = summarizeChatOrganizationQueue(queue);
15360
+ writeChatOrganizationSummary({ queuePath, queue, options });
15361
+ let exitCode = 0;
15362
+ if (summary.failed > 0 || summary.pending > 0) exitCode = 1;
15363
+ process.exit(exitCode);
15364
+ };
14447
15365
  var writeArchiveOutcomes = (outcomes, options) => {
14448
15366
  if (options.json) {
14449
15367
  process.stdout.write(`${JSON.stringify(outcomes)}
@@ -14556,7 +15474,7 @@ var runTui = async (opts) => {
14556
15474
  console.log(`
14557
15475
  Starting ai-browser-bridge (${label})...`);
14558
15476
  const engine = await startEngine({
14559
- repoPath: opts.repo ? resolve7(opts.repo) : void 0,
15477
+ repoPath: opts.repo ? resolve8(opts.repo) : void 0,
14560
15478
  provider,
14561
15479
  mcpPort: opts.port ? Number(opts.port) : void 0,
14562
15480
  withBrowser: opts.browser !== false,
@@ -14679,7 +15597,7 @@ var runChatgptInspect = async (options) => {
14679
15597
  };
14680
15598
  var startFlowSession = async (options) => {
14681
15599
  const engine = await startEngine({
14682
- repoPath: options.repo ? resolve7(options.repo) : void 0,
15600
+ repoPath: options.repo ? resolve8(options.repo) : void 0,
14683
15601
  provider: "flow",
14684
15602
  mcpPort: options.port ? Number(options.port) : void 0,
14685
15603
  withBrowser: true,
@@ -14689,7 +15607,7 @@ var startFlowSession = async (options) => {
14689
15607
  });
14690
15608
  return { engine, page: requireBrowserPage(engine) };
14691
15609
  };
14692
- var defaultFlowOutDir = (repoRoot) => join11(downloadsDir(repoRoot), "flow");
15610
+ var defaultFlowOutDir = (repoRoot) => join12(downloadsDir(repoRoot), "flow");
14693
15611
  var requireClipId = (options, verb) => {
14694
15612
  const id2 = options.id?.[0];
14695
15613
  if (id2 === void 0 || id2 === "") return fail(`Usage: bridge flow ${verb} --id <clipId>`);
@@ -14721,7 +15639,7 @@ var runFlowDownload = async (options) => {
14721
15639
  const { engine, page } = await startFlowSession(options);
14722
15640
  const clips = await listClips(page);
14723
15641
  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);
15642
+ const outDir = options.out ? resolve8(options.out) : defaultFlowOutDir(engine.config.repoPath);
14725
15643
  const results = [];
14726
15644
  for (const id2 of targets) {
14727
15645
  try {
@@ -14746,13 +15664,13 @@ var runFlowDownload = async (options) => {
14746
15664
  process.exit(results.every((download) => download.ok) ? 0 : 1);
14747
15665
  };
14748
15666
  var runFlowGenerate = async (options) => {
14749
- const startFramePath = options.start ? resolve7(options.start) : "";
15667
+ const startFramePath = options.start ? resolve8(options.start) : "";
14750
15668
  const prompt = options.prompt === void 0 ? "" : options.prompt.trim();
14751
15669
  if (!startFramePath || !prompt) {
14752
15670
  fail("Usage: bridge flow generate --start <imagePath> --prompt <text> [--out <dir>]");
14753
15671
  }
14754
15672
  const { engine, page } = await startFlowSession(options);
14755
- const outDir = options.out ? resolve7(options.out) : defaultFlowOutDir(engine.config.repoPath);
15673
+ const outDir = options.out ? resolve8(options.out) : defaultFlowOutDir(engine.config.repoPath);
14756
15674
  try {
14757
15675
  const clip = await generateClipFromFrame(page, {
14758
15676
  startFramePath,
@@ -14902,7 +15820,7 @@ var subcommandOpts = (command) => {
14902
15820
  // src/features/terminal/registerCli.ts
14903
15821
  var PROVIDER_OPTION = `Browser provider: ${PROVIDER_IDS.join(", ")} (default: ${DEFAULT_PROVIDER})`;
14904
15822
  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) => {
15823
+ program.name("bridge").description("Terminal CLI that bridges ChatGPT or Gemini with local tools via MCP").version("0.5.1").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
15824
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
14907
15825
  command.outputHelp({ error: true });
14908
15826
  process.exitCode = 1;
@@ -15031,6 +15949,15 @@ var registerWorkspaceCommands = (program) => {
15031
15949
  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
15950
  (chatParts, _options, command) => runChatArchive(chatParts.join(" "), command.optsWithGlobals())
15033
15951
  );
15952
+ withWorkspaceFlags(chat.command("organize")).description("Run a resumable, rate-safe Conversation organization queue").requiredOption(
15953
+ "--plan <fileOrJson>",
15954
+ "JSON array of {conversation, project} tasks (inline, @file, or a path)"
15955
+ ).option(
15956
+ "--interval <secondsOrRange>",
15957
+ "Seconds between UI operations, fixed or random range such as 10-20 (default: 30)"
15958
+ ).option("--cooldown <seconds>", "Seconds to wait after rate limiting (default: 300)").option("--max-attempts <count>", "Attempts per Conversation before failing (default: 3)").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(
15959
+ (_options, command) => runChatOrganize(command.optsWithGlobals())
15960
+ );
15034
15961
  const task = program.command("task").description("List or schedule ChatGPT Tasks (ChatGPT only)");
15035
15962
  withWorkspaceFlags(task.command("list")).description("List ChatGPT Scheduled tasks").action(
15036
15963
  (_options, command) => runTaskList(command.optsWithGlobals())