@youdie006/prodex 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2517,13 +2517,78 @@ export async function navigateChatGptTabTo(url, options = {}) {
2517
2517
  }
2518
2518
  }
2519
2519
  /**
2520
- * Which project a delete means - or why it refuses to guess.
2520
+ * Which conversation a delete means - or why it refuses to guess.
2521
2521
  *
2522
- * Deleting a project is not undoable from here, so the request has to identify
2523
- * exactly one. Names are matched exactly (a typo must not delete a neighbour),
2524
- * and an id is accepted for the case this account actually has: two projects
2525
- * sharing a name.
2522
+ * Titles are written by ChatGPT and repeat far more often than project names,
2523
+ * so an exact title matching two chats is refused with both ids rather than
2524
+ * resolved by picking the newer one.
2526
2525
  */
2526
+ export function resolveConversationToDelete(conversations, request) {
2527
+ if (request.id) {
2528
+ const byId = conversations.find((conversation) => conversation.id === request.id);
2529
+ return byId ? { ok: true, id: byId.id, title: byId.title } : { ok: false, reason: `No recent conversation has the id ${request.id}.` };
2530
+ }
2531
+ const title = request.title?.trim();
2532
+ if (!title)
2533
+ return { ok: false, reason: "Name the chat to delete with --title, or identify it with --id." };
2534
+ const matches = conversations.filter((conversation) => conversation.title === title);
2535
+ if (matches.length === 0) {
2536
+ return { ok: false, reason: `No recent conversation is titled exactly "${title}". Run \`prodex pro browser chats\` to see them.` };
2537
+ }
2538
+ if (matches.length > 1) {
2539
+ const ids = matches.map((conversation) => conversation.id).join(", ");
2540
+ return { ok: false, reason: `More than one recent conversation is titled "${title}" (${ids}). Pass --id to say which one.` };
2541
+ }
2542
+ return { ok: true, id: matches[0].id, title: matches[0].title };
2543
+ }
2544
+ /**
2545
+ * Remove one conversation. ChatGPT deletes a chat by hiding it, which is the
2546
+ * same call its own UI makes; the caller is responsible for confirming intent.
2547
+ */
2548
+ export function deleteConversationExpression(conversationId) {
2549
+ return `(async () => {
2550
+ let token = "";
2551
+ try {
2552
+ const session = await fetch("/api/auth/session", { credentials: "include" });
2553
+ if (!session.ok) return { ok: false, reason: "session_http_" + session.status };
2554
+ const parsed = await session.json();
2555
+ token = (parsed && parsed.accessToken) || "";
2556
+ } catch (error) {
2557
+ return { ok: false, reason: "session_error" };
2558
+ }
2559
+ try {
2560
+ const response = await fetch("/backend-api/conversation/" + ${JSON.stringify(conversationId)}, {
2561
+ method: "PATCH",
2562
+ credentials: "include",
2563
+ headers: token ? { Authorization: "Bearer " + token, "Content-Type": "application/json" } : { "Content-Type": "application/json" },
2564
+ body: JSON.stringify({ is_visible: false })
2565
+ });
2566
+ const body = await response.text();
2567
+ if (!response.ok) return { ok: false, reason: "delete_http_" + response.status + " " + body.slice(0, 120) };
2568
+ return { ok: true, reason: "" };
2569
+ } catch (error) {
2570
+ return { ok: false, reason: "delete_error" };
2571
+ }
2572
+ })()`;
2573
+ }
2574
+ /** Remove one conversation. Callers must have confirmed intent before calling. */
2575
+ export async function deleteChatGptConversation(input) {
2576
+ const port = resolveCdpPort(input.port);
2577
+ const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
2578
+ if (!page.ok || !page.page) {
2579
+ throw new ChatGptBrowserBlockerError(page.blocker ?? {
2580
+ code: "browser_unreachable",
2581
+ message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
2582
+ retryable: true,
2583
+ next_step: "Run `prodex pro browser login` to reopen the dedicated window, then retry."
2584
+ });
2585
+ }
2586
+ const result = await evaluateOnPage(page.page, deleteConversationExpression(input.conversationId), {
2587
+ timeoutMs: 30_000
2588
+ });
2589
+ if (!result?.ok)
2590
+ throw new Error(`ChatGPT refused to delete the conversation: ${result?.reason ?? "unknown reason"}`);
2591
+ }
2527
2592
  export function resolveProjectToDelete(projects, request) {
2528
2593
  if (request.id) {
2529
2594
  const byId = projects.find((project) => project.id === request.id);
package/dist/cli-help.js CHANGED
@@ -268,6 +268,12 @@ export function printProBrowserHelp(stdout, sourceCli) {
268
268
  const projectDeleteUsage = sourceCli
269
269
  ? `${cli} pro browser project-delete${sourceCliOption} [--name "exact name" | --id g-p-...] [--confirm-delete] # previews unless --confirm-delete; deleting a project takes its chats with it`
270
270
  : `prodex pro browser project-delete [--source-cli /absolute/path/to/dist/cli.js] [--name "exact name" | --id g-p-...] [--confirm-delete] # previews unless --confirm-delete; deleting a project takes its chats with it`;
271
+ const chatsUsage = sourceCli
272
+ ? `${cli} pro browser chats${sourceCliOption} [--limit 10] # read-only: recent conversations with their ids`
273
+ : "prodex pro browser chats [--source-cli /absolute/path/to/dist/cli.js] [--limit 10] # read-only: recent conversations with their ids";
274
+ const chatDeleteUsage = sourceCli
275
+ ? `${cli} pro browser chat-delete${sourceCliOption} [--title "exact title" | --id <id>] [--confirm-delete] # previews unless --confirm-delete`
276
+ : `prodex pro browser chat-delete [--source-cli /absolute/path/to/dist/cli.js] [--title "exact title" | --id <id>] [--confirm-delete] # previews unless --confirm-delete`;
271
277
  const recoverUsage = sourceCli
272
278
  ? `${cli} pro browser recover${sourceCliOption} [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # fetch a finished answer (deep research reports too) from a thread whose send timed out`
273
279
  : "prodex pro browser recover [--source-cli /absolute/path/to/dist/cli.js] [--cwd /absolute/path/to/repo] [--port 9333] --target-url <thread-url> [--timeout-ms 60000] # fetch a finished answer (deep research reports too) from a thread whose send timed out";
@@ -280,6 +286,8 @@ Commands:
280
286
  ${modelsUsage}
281
287
  ${projectsUsage}
282
288
  ${projectDeleteUsage}
289
+ ${chatsUsage}
290
+ ${chatDeleteUsage}
283
291
  ${askUsage}
284
292
  ${recoverUsage}
285
293
 
package/dist/cli-pro.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { buildDryRunBundle } from "./bundle.js";
5
- import { DEFAULT_CDP_PORT, resolveCdpPort, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, deleteChatGptProject, listChatGptProjectsWithIds, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
5
+ import { DEFAULT_CDP_PORT, resolveCdpPort, resolveConversationToDelete, resolveProjectToDelete, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, deleteChatGptConversation, deleteChatGptProject, listChatGptProjectsWithIds, listRecentChatGptConversations, listChatGptSidebarProjects, normalizeChatGptTargetUrl, openChatGptBrowser, openChatGptTab, parseProMode, parseReasoningEffort, defaultTimeoutForTools, ensureVirtualDisplay, minimizeChatGptWindow, readLastBrowserLoginLaunch, resolveVirtualDisplayPreference, resolveBrowserWindowMode, resolveHeadlessPreference, recordBrowserLoginLaunch, recoverChatGptAnswerFromThread, sendChatGptPrompt } from "./chatgpt-browser.js";
6
6
  import { ASK_PRO_BOOLEAN_FLAGS, ASK_PRO_PREVIEW_VALUE_FLAGS, ASK_PRO_VALUE_FLAGS, assertHelpRequestArgs, assertNoExtraArgs, assertOnlyOptions, findHelpFlagIndexBeforePromptDelimiter, formatCliCommand, hasAskProDryRunMode, hasAskProMode, hasAskProSendMode, isHelpSubcommand, parseAskProArgs, printHelpIfRequested, readFlag, readPortFlag, readPositionalsWithOptions, readNonNegativeIntegerFlag, readPositiveIntegerFlag, readRepeatedFlag, resolveCwdFlag, resolveOptionalFileFlag, unknownSubcommandError } from "./cli-args.js";
7
7
  import { printProBrowserHelp, printProHelp } from "./cli-help.js";
8
8
  import { listRawResultsForInspection, listTasksForInspection } from "./cli-ledger.js";
@@ -537,6 +537,63 @@ export async function runProCommand(rest, io, runCliFn) {
537
537
  io.stderr(`recovered: answer saved to .bridge; re-print with \`prodex pro latest --cwd ${recoverCwd}\``);
538
538
  return 0;
539
539
  }
540
+ if (browserSubcommand === "chats") {
541
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser chats", io, { valueFlags: ["--port", "--timeout-ms", "--limit", "--source-cli"] }))
542
+ return 0;
543
+ assertOnlyOptions(browserArgs, "pro browser chats", ["--port", "--timeout-ms", "--limit", "--source-cli"]);
544
+ const chatsPort = readPortFlag(browserArgs, "--port");
545
+ const chatsTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
546
+ const chatsLimit = readPositiveIntegerFlag(browserArgs, "--limit");
547
+ const chats = await listRecentChatGptConversations({
548
+ ...(chatsPort !== undefined ? { port: chatsPort } : {}),
549
+ ...(chatsTimeoutMs !== undefined ? { timeoutMs: chatsTimeoutMs } : {}),
550
+ ...(chatsLimit !== undefined ? { limit: chatsLimit } : {})
551
+ });
552
+ if (chats.length === 0) {
553
+ io.stdout("No recent conversations were readable.");
554
+ return 0;
555
+ }
556
+ io.stdout("Recent ChatGPT conversations (newest first):");
557
+ for (const chat of chats)
558
+ io.stdout(` ${chat.title} ${chat.id}`);
559
+ io.stdout("Delete one with `pro browser chat-delete --id <id> --confirm-delete`.");
560
+ return 0;
561
+ }
562
+ if (browserSubcommand === "chat-delete") {
563
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser chat-delete", io, {
564
+ valueFlags: ["--port", "--timeout-ms", "--title", "--id", "--limit", "--source-cli"],
565
+ booleanFlags: ["--confirm-delete"]
566
+ })) {
567
+ return 0;
568
+ }
569
+ assertOnlyOptions(browserArgs, "pro browser chat-delete", ["--port", "--timeout-ms", "--title", "--id", "--limit", "--source-cli"], ["--confirm-delete"]);
570
+ const chatDeletePort = readPortFlag(browserArgs, "--port");
571
+ const chatDeleteTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
572
+ const chatDeleteLimit = readPositiveIntegerFlag(browserArgs, "--limit");
573
+ const chats = await listRecentChatGptConversations({
574
+ ...(chatDeletePort !== undefined ? { port: chatDeletePort } : {}),
575
+ ...(chatDeleteTimeoutMs !== undefined ? { timeoutMs: chatDeleteTimeoutMs } : {}),
576
+ limit: chatDeleteLimit ?? 40
577
+ });
578
+ const chatTarget = resolveConversationToDelete(chats, {
579
+ ...(readFlag(browserArgs, "--title") !== undefined ? { title: readFlag(browserArgs, "--title") } : {}),
580
+ ...(readFlag(browserArgs, "--id") !== undefined ? { id: readFlag(browserArgs, "--id") } : {})
581
+ });
582
+ if (!chatTarget.ok)
583
+ throw new Error(chatTarget.reason);
584
+ if (!browserArgs.includes("--confirm-delete")) {
585
+ io.stdout(`Would delete the conversation "${chatTarget.title}" (${chatTarget.id}).`);
586
+ io.stdout("Nothing was deleted. Re-run with --confirm-delete to go ahead.");
587
+ return 0;
588
+ }
589
+ await deleteChatGptConversation({
590
+ conversationId: chatTarget.id,
591
+ ...(chatDeletePort !== undefined ? { port: chatDeletePort } : {}),
592
+ ...(chatDeleteTimeoutMs !== undefined ? { timeoutMs: chatDeleteTimeoutMs } : {})
593
+ });
594
+ io.stdout(`deleted conversation "${chatTarget.title}" (${chatTarget.id})`);
595
+ return 0;
596
+ }
540
597
  if (browserSubcommand === "project-delete") {
541
598
  if (printProBrowserHelpIfRequested(browserArgs, "pro browser project-delete", io, {
542
599
  valueFlags: ["--cwd", "--port", "--timeout-ms", "--name", "--id", "--source-cli"],
@@ -580,6 +637,8 @@ export async function runProCommand(rest, io, runCliFn) {
580
637
  "models",
581
638
  "projects",
582
639
  "project-delete",
640
+ "chats",
641
+ "chat-delete",
583
642
  "recover"
584
643
  ]);
585
644
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",