@youdie006/prodex 0.32.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.
@@ -2516,7 +2516,126 @@ export async function navigateChatGptTabTo(url, options = {}) {
2516
2516
  cdp.close();
2517
2517
  }
2518
2518
  }
2519
- /** Projects with the id their conversations are tagged with. */
2519
+ /**
2520
+ * Which conversation a delete means - or why it refuses to guess.
2521
+ *
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.
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
+ }
2592
+ export function resolveProjectToDelete(projects, request) {
2593
+ if (request.id) {
2594
+ const byId = projects.find((project) => project.id === request.id);
2595
+ return byId ? { ok: true, id: byId.id, name: byId.name } : { ok: false, reason: `No project has the id ${request.id}.` };
2596
+ }
2597
+ const name = request.name?.trim();
2598
+ if (!name)
2599
+ return { ok: false, reason: "Name the project to delete with --name, or identify it with --id." };
2600
+ const matches = projects.filter((project) => project.name === name);
2601
+ if (matches.length === 0) {
2602
+ return { ok: false, reason: `No project is named exactly "${name}". Run \`prodex pro browser projects\` to see the names as they are stored.` };
2603
+ }
2604
+ if (matches.length > 1) {
2605
+ const ids = matches.map((project) => project.id).join(", ");
2606
+ return {
2607
+ ok: false,
2608
+ reason: `More than one project is named "${name}" (${ids}). Pass --id to say which one, since deleting the wrong one cannot be undone here.`
2609
+ };
2610
+ }
2611
+ return { ok: true, id: matches[0].id, name: matches[0].name };
2612
+ }
2613
+ /** Delete one project by id. The caller is responsible for confirming intent. */
2614
+ export function deleteProjectExpression(projectId) {
2615
+ return `(async () => {
2616
+ let token = "";
2617
+ try {
2618
+ const session = await fetch("/api/auth/session", { credentials: "include" });
2619
+ if (!session.ok) return { ok: false, reason: "session_http_" + session.status };
2620
+ const parsed = await session.json();
2621
+ token = (parsed && parsed.accessToken) || "";
2622
+ } catch (error) {
2623
+ return { ok: false, reason: "session_error" };
2624
+ }
2625
+ try {
2626
+ const response = await fetch("/backend-api/gizmos/" + ${JSON.stringify(projectId)}, {
2627
+ method: "DELETE",
2628
+ credentials: "include",
2629
+ headers: token ? { Authorization: "Bearer " + token, "Content-Type": "application/json" } : { "Content-Type": "application/json" }
2630
+ });
2631
+ const body = await response.text();
2632
+ if (!response.ok) return { ok: false, reason: "delete_http_" + response.status + " " + body.slice(0, 120) };
2633
+ return { ok: true, reason: "" };
2634
+ } catch (error) {
2635
+ return { ok: false, reason: "delete_error" };
2636
+ }
2637
+ })()`;
2638
+ }
2520
2639
  export async function listChatGptProjectsWithIds(input = {}) {
2521
2640
  const port = resolveCdpPort(input.port);
2522
2641
  const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
@@ -2530,6 +2649,24 @@ export async function listChatGptProjectsWithIds(input = {}) {
2530
2649
  return [];
2531
2650
  }
2532
2651
  }
2652
+ /** Delete one project. Callers must have confirmed intent before calling. */
2653
+ export async function deleteChatGptProject(input) {
2654
+ const port = resolveCdpPort(input.port);
2655
+ const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
2656
+ if (!page.ok || !page.page) {
2657
+ throw new ChatGptBrowserBlockerError(page.blocker ?? {
2658
+ code: "browser_unreachable",
2659
+ message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
2660
+ retryable: true,
2661
+ next_step: "Run `prodex pro browser login` to reopen the dedicated window, then retry."
2662
+ });
2663
+ }
2664
+ const result = await evaluateOnPage(page.page, deleteProjectExpression(input.projectId), {
2665
+ timeoutMs: 30_000
2666
+ });
2667
+ if (!result?.ok)
2668
+ throw new Error(`ChatGPT refused to delete the project: ${result?.reason ?? "unknown reason"}`);
2669
+ }
2533
2670
  export async function listRecentChatGptConversations(input = {}) {
2534
2671
  const port = resolveCdpPort(input.port);
2535
2672
  const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
package/dist/cli-help.js CHANGED
@@ -263,6 +263,17 @@ export function printProBrowserHelp(stdout, sourceCli) {
263
263
  : "prodex pro browser projects [--source-cli /absolute/path/to/dist/cli.js] [--port 9333] [--timeout-ms 15000] # read-only: exact sidebar project names";
264
264
  // A send that outlives its budget is not a lost answer, but only if agents
265
265
  // know this exists - and this help is where onboarding sends them.
266
+ // Deleting a project takes its chats with it, so the usage line says so and
267
+ // the flag that actually deletes is spelled out rather than implied.
268
+ const projectDeleteUsage = sourceCli
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
+ : `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`;
266
277
  const recoverUsage = sourceCli
267
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`
268
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";
@@ -274,6 +285,9 @@ Commands:
274
285
  ${smokeUsage}
275
286
  ${modelsUsage}
276
287
  ${projectsUsage}
288
+ ${projectDeleteUsage}
289
+ ${chatsUsage}
290
+ ${chatDeleteUsage}
277
291
  ${askUsage}
278
292
  ${recoverUsage}
279
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, chatGptVisibilityBlocker, defaultChatGptProfileDir, formatDurationMs, getChatGptBrowserStatus, listChatGptModelOptions, 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";
@@ -463,8 +463,20 @@ export async function runProCommand(rest, io, runCliFn) {
463
463
  return 0;
464
464
  }
465
465
  io.stdout("ChatGPT sidebar projects (read-only; exact names as rendered):");
466
- for (const name of listed.projects)
467
- io.stdout(` ${name}`);
466
+ // The rendered sidebar can lag reality - it still showed a project that
467
+ // had just been deleted - so print the account's own listing when it is
468
+ // readable, and its ids, which `project-delete --id` needs to tell two
469
+ // projects of the same name apart.
470
+ const withIds = await listChatGptProjectsWithIds({
471
+ ...(projectsPort !== undefined ? { port: projectsPort } : {}),
472
+ ...(projectsTimeoutMs !== undefined ? { timeoutMs: projectsTimeoutMs } : {})
473
+ }).catch(() => []);
474
+ if (withIds.length > 0)
475
+ for (const project of withIds)
476
+ io.stdout(` ${project.name} ${project.id}`);
477
+ else
478
+ for (const name of listed.projects)
479
+ io.stdout(` ${name}`);
468
480
  io.stdout("Use with `pro browser ask --project \"<name>\"` or pin one with `prodex setup --project \"<name>\"`.");
469
481
  return 0;
470
482
  }
@@ -525,7 +537,110 @@ export async function runProCommand(rest, io, runCliFn) {
525
537
  io.stderr(`recovered: answer saved to .bridge; re-print with \`prodex pro latest --cwd ${recoverCwd}\``);
526
538
  return 0;
527
539
  }
528
- throw unknownSubcommandError("pro browser", browserSubcommand, ["login", "ask", "smoke", "check", "models", "projects", "recover"]);
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
+ }
597
+ if (browserSubcommand === "project-delete") {
598
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser project-delete", io, {
599
+ valueFlags: ["--cwd", "--port", "--timeout-ms", "--name", "--id", "--source-cli"],
600
+ booleanFlags: ["--confirm-delete"]
601
+ })) {
602
+ return 0;
603
+ }
604
+ assertOnlyOptions(browserArgs, "pro browser project-delete", ["--cwd", "--port", "--timeout-ms", "--name", "--id", "--source-cli"], ["--confirm-delete"]);
605
+ const deletePort = readPortFlag(browserArgs, "--port");
606
+ const deleteTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
607
+ const projects = await listChatGptProjectsWithIds({
608
+ ...(deletePort !== undefined ? { port: deletePort } : {}),
609
+ ...(deleteTimeoutMs !== undefined ? { timeoutMs: deleteTimeoutMs } : {})
610
+ });
611
+ const target = resolveProjectToDelete(projects, {
612
+ ...(readFlag(browserArgs, "--name") !== undefined ? { name: readFlag(browserArgs, "--name") } : {}),
613
+ ...(readFlag(browserArgs, "--id") !== undefined ? { id: readFlag(browserArgs, "--id") } : {})
614
+ });
615
+ if (!target.ok)
616
+ throw new Error(target.reason);
617
+ // Deleting a project is not undoable from here, so the default is a
618
+ // preview: say exactly what would go, and delete nothing.
619
+ if (!browserArgs.includes("--confirm-delete")) {
620
+ io.stdout(`Would delete project "${target.name}" (${target.id}) and everything filed under it.`);
621
+ io.stdout("Nothing was deleted. Re-run with --confirm-delete to go ahead.");
622
+ return 0;
623
+ }
624
+ await deleteChatGptProject({
625
+ projectId: target.id,
626
+ ...(deletePort !== undefined ? { port: deletePort } : {}),
627
+ ...(deleteTimeoutMs !== undefined ? { timeoutMs: deleteTimeoutMs } : {})
628
+ });
629
+ io.stdout(`deleted project "${target.name}" (${target.id})`);
630
+ return 0;
631
+ }
632
+ throw unknownSubcommandError("pro browser", browserSubcommand, [
633
+ "login",
634
+ "ask",
635
+ "smoke",
636
+ "check",
637
+ "models",
638
+ "projects",
639
+ "project-delete",
640
+ "chats",
641
+ "chat-delete",
642
+ "recover"
643
+ ]);
529
644
  }
530
645
  if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
531
646
  // Point at a subcommand that EXISTS: `pro status` used to say "use `pro
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.32.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",