@youdie006/prodex 0.32.0 → 0.33.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,61 @@ 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 project a delete means - or why it refuses to guess.
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.
2526
+ */
2527
+ export function resolveProjectToDelete(projects, request) {
2528
+ if (request.id) {
2529
+ const byId = projects.find((project) => project.id === request.id);
2530
+ return byId ? { ok: true, id: byId.id, name: byId.name } : { ok: false, reason: `No project has the id ${request.id}.` };
2531
+ }
2532
+ const name = request.name?.trim();
2533
+ if (!name)
2534
+ return { ok: false, reason: "Name the project to delete with --name, or identify it with --id." };
2535
+ const matches = projects.filter((project) => project.name === name);
2536
+ if (matches.length === 0) {
2537
+ return { ok: false, reason: `No project is named exactly "${name}". Run \`prodex pro browser projects\` to see the names as they are stored.` };
2538
+ }
2539
+ if (matches.length > 1) {
2540
+ const ids = matches.map((project) => project.id).join(", ");
2541
+ return {
2542
+ ok: false,
2543
+ reason: `More than one project is named "${name}" (${ids}). Pass --id to say which one, since deleting the wrong one cannot be undone here.`
2544
+ };
2545
+ }
2546
+ return { ok: true, id: matches[0].id, name: matches[0].name };
2547
+ }
2548
+ /** Delete one project by id. The caller is responsible for confirming intent. */
2549
+ export function deleteProjectExpression(projectId) {
2550
+ return `(async () => {
2551
+ let token = "";
2552
+ try {
2553
+ const session = await fetch("/api/auth/session", { credentials: "include" });
2554
+ if (!session.ok) return { ok: false, reason: "session_http_" + session.status };
2555
+ const parsed = await session.json();
2556
+ token = (parsed && parsed.accessToken) || "";
2557
+ } catch (error) {
2558
+ return { ok: false, reason: "session_error" };
2559
+ }
2560
+ try {
2561
+ const response = await fetch("/backend-api/gizmos/" + ${JSON.stringify(projectId)}, {
2562
+ method: "DELETE",
2563
+ credentials: "include",
2564
+ headers: token ? { Authorization: "Bearer " + token, "Content-Type": "application/json" } : { "Content-Type": "application/json" }
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
+ }
2520
2574
  export async function listChatGptProjectsWithIds(input = {}) {
2521
2575
  const port = resolveCdpPort(input.port);
2522
2576
  const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
@@ -2530,6 +2584,24 @@ export async function listChatGptProjectsWithIds(input = {}) {
2530
2584
  return [];
2531
2585
  }
2532
2586
  }
2587
+ /** Delete one project. Callers must have confirmed intent before calling. */
2588
+ export async function deleteChatGptProject(input) {
2589
+ const port = resolveCdpPort(input.port);
2590
+ const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
2591
+ if (!page.ok || !page.page) {
2592
+ throw new ChatGptBrowserBlockerError(page.blocker ?? {
2593
+ code: "browser_unreachable",
2594
+ message: `No Chrome DevTools endpoint is reachable on 127.0.0.1:${port}.`,
2595
+ retryable: true,
2596
+ next_step: "Run `prodex pro browser login` to reopen the dedicated window, then retry."
2597
+ });
2598
+ }
2599
+ const result = await evaluateOnPage(page.page, deleteProjectExpression(input.projectId), {
2600
+ timeoutMs: 30_000
2601
+ });
2602
+ if (!result?.ok)
2603
+ throw new Error(`ChatGPT refused to delete the project: ${result?.reason ?? "unknown reason"}`);
2604
+ }
2533
2605
  export async function listRecentChatGptConversations(input = {}) {
2534
2606
  const port = resolveCdpPort(input.port);
2535
2607
  const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
package/dist/cli-help.js CHANGED
@@ -263,6 +263,11 @@ 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`;
266
271
  const recoverUsage = sourceCli
267
272
  ? `${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
273
  : "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 +279,7 @@ Commands:
274
279
  ${smokeUsage}
275
280
  ${modelsUsage}
276
281
  ${projectsUsage}
282
+ ${projectDeleteUsage}
277
283
  ${askUsage}
278
284
  ${recoverUsage}
279
285
 
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, 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";
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,51 @@ 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 === "project-delete") {
541
+ if (printProBrowserHelpIfRequested(browserArgs, "pro browser project-delete", io, {
542
+ valueFlags: ["--cwd", "--port", "--timeout-ms", "--name", "--id", "--source-cli"],
543
+ booleanFlags: ["--confirm-delete"]
544
+ })) {
545
+ return 0;
546
+ }
547
+ assertOnlyOptions(browserArgs, "pro browser project-delete", ["--cwd", "--port", "--timeout-ms", "--name", "--id", "--source-cli"], ["--confirm-delete"]);
548
+ const deletePort = readPortFlag(browserArgs, "--port");
549
+ const deleteTimeoutMs = readPositiveIntegerFlag(browserArgs, "--timeout-ms");
550
+ const projects = await listChatGptProjectsWithIds({
551
+ ...(deletePort !== undefined ? { port: deletePort } : {}),
552
+ ...(deleteTimeoutMs !== undefined ? { timeoutMs: deleteTimeoutMs } : {})
553
+ });
554
+ const target = resolveProjectToDelete(projects, {
555
+ ...(readFlag(browserArgs, "--name") !== undefined ? { name: readFlag(browserArgs, "--name") } : {}),
556
+ ...(readFlag(browserArgs, "--id") !== undefined ? { id: readFlag(browserArgs, "--id") } : {})
557
+ });
558
+ if (!target.ok)
559
+ throw new Error(target.reason);
560
+ // Deleting a project is not undoable from here, so the default is a
561
+ // preview: say exactly what would go, and delete nothing.
562
+ if (!browserArgs.includes("--confirm-delete")) {
563
+ io.stdout(`Would delete project "${target.name}" (${target.id}) and everything filed under it.`);
564
+ io.stdout("Nothing was deleted. Re-run with --confirm-delete to go ahead.");
565
+ return 0;
566
+ }
567
+ await deleteChatGptProject({
568
+ projectId: target.id,
569
+ ...(deletePort !== undefined ? { port: deletePort } : {}),
570
+ ...(deleteTimeoutMs !== undefined ? { timeoutMs: deleteTimeoutMs } : {})
571
+ });
572
+ io.stdout(`deleted project "${target.name}" (${target.id})`);
573
+ return 0;
574
+ }
575
+ throw unknownSubcommandError("pro browser", browserSubcommand, [
576
+ "login",
577
+ "ask",
578
+ "smoke",
579
+ "check",
580
+ "models",
581
+ "projects",
582
+ "project-delete",
583
+ "recover"
584
+ ]);
529
585
  }
530
586
  if (subcommand === "open" || subcommand === "status" || subcommand === "smoke" || subcommand === "check" || subcommand === "doctor") {
531
587
  // 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.33.0",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",