@youdie006/prodex 0.30.0 → 0.31.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/chatgpt-browser.js +29 -1
- package/dist/cli.js +4 -0
- package/dist/tui-flow.js +6 -3
- package/dist/tui-run.js +32 -8
- package/dist/tui.js +48 -2
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -1416,18 +1416,32 @@ async function assertSelectionCommitted(cdp, label) {
|
|
|
1416
1416
|
throw new Error(`ChatGPT selection "${label}" did not commit; the model menu stayed open. Retry, or pick it manually in the visible browser.`);
|
|
1417
1417
|
}
|
|
1418
1418
|
}
|
|
1419
|
+
// Roughly three seconds of grace for a menu that is still painting.
|
|
1420
|
+
const POWER_SLIDER_APPEAR_ATTEMPTS = 5;
|
|
1419
1421
|
/**
|
|
1420
1422
|
* Move the power slider until its Effort readout is the requested step. The
|
|
1421
1423
|
* menu must already be open. Returns the quota line so the caller can warn
|
|
1422
1424
|
* when Pro runs are nearly spent.
|
|
1423
1425
|
*/
|
|
1424
1426
|
async function selectPowerStep(cdp, requested) {
|
|
1425
|
-
|
|
1427
|
+
// The menu paints a moment after it opens, and on a page that has just been
|
|
1428
|
+
// built - a project created seconds ago - that moment is longer. Failing the
|
|
1429
|
+
// whole send on the first empty look cost a real consult its answer, so give
|
|
1430
|
+
// the slider a few beats to appear.
|
|
1431
|
+
let focused = await cdp.evaluate(focusPowerSliderExpression());
|
|
1432
|
+
for (let attempt = 0; attempt < POWER_SLIDER_APPEAR_ATTEMPTS && !focused?.ok; attempt += 1) {
|
|
1433
|
+
await sleep(600);
|
|
1434
|
+
focused = await cdp.evaluate(focusPowerSliderExpression());
|
|
1435
|
+
}
|
|
1426
1436
|
if (!focused?.ok) {
|
|
1427
1437
|
throw new Error(focused?.reason ??
|
|
1428
1438
|
"ChatGPT's model picker did not expose its power slider, so the requested model/effort could not be selected.");
|
|
1429
1439
|
}
|
|
1430
1440
|
let state = await cdp.evaluate(powerSliderStateExpression());
|
|
1441
|
+
for (let attempt = 0; attempt < POWER_SLIDER_APPEAR_ATTEMPTS && !state?.ok; attempt += 1) {
|
|
1442
|
+
await sleep(600);
|
|
1443
|
+
state = await cdp.evaluate(powerSliderStateExpression());
|
|
1444
|
+
}
|
|
1431
1445
|
if (!state?.ok)
|
|
1432
1446
|
throw new Error(state?.reason ?? "Could not read ChatGPT's power slider");
|
|
1433
1447
|
const steps = (state.max ?? 4) - (state.min ?? 0) + 1;
|
|
@@ -2502,6 +2516,20 @@ export async function navigateChatGptTabTo(url, options = {}) {
|
|
|
2502
2516
|
cdp.close();
|
|
2503
2517
|
}
|
|
2504
2518
|
}
|
|
2519
|
+
/** Projects with the id their conversations are tagged with. */
|
|
2520
|
+
export async function listChatGptProjectsWithIds(input = {}) {
|
|
2521
|
+
const port = resolveCdpPort(input.port);
|
|
2522
|
+
const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
|
|
2523
|
+
if (!page.ok || !page.page)
|
|
2524
|
+
return [];
|
|
2525
|
+
const { projectsWithIdsExpression } = await import("./tui.js");
|
|
2526
|
+
try {
|
|
2527
|
+
return (await evaluateOnPage(page.page, projectsWithIdsExpression(), { timeoutMs: 30_000 })) ?? [];
|
|
2528
|
+
}
|
|
2529
|
+
catch {
|
|
2530
|
+
return [];
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2505
2533
|
export async function listRecentChatGptConversations(input = {}) {
|
|
2506
2534
|
const port = resolveCdpPort(input.port);
|
|
2507
2535
|
const page = await findChatGptPage(port, input.timeoutMs ?? 3_000);
|
package/dist/cli.js
CHANGED
|
@@ -86,6 +86,10 @@ async function runInteractiveUi(io) {
|
|
|
86
86
|
const { navigateChatGptTabTo } = await import("./chatgpt-browser.js");
|
|
87
87
|
return navigateChatGptTabTo(url, {});
|
|
88
88
|
},
|
|
89
|
+
listProjectsWithIds: async () => {
|
|
90
|
+
const { listChatGptProjectsWithIds } = await import("./chatgpt-browser.js");
|
|
91
|
+
return listChatGptProjectsWithIds({});
|
|
92
|
+
},
|
|
89
93
|
listProjects: async () => {
|
|
90
94
|
const { listChatGptSidebarProjects } = await import("./chatgpt-browser.js");
|
|
91
95
|
const listed = await listChatGptSidebarProjects({});
|
package/dist/tui-flow.js
CHANGED
|
@@ -39,11 +39,14 @@ export function destinationChoices(pinnedProject) {
|
|
|
39
39
|
* and ChatGPT deselects Pro as soon as an effort is set.
|
|
40
40
|
*/
|
|
41
41
|
export function effortChoices(pinnedModel) {
|
|
42
|
+
// Reads down from what the repo pinned: the level just under Pro sits just
|
|
43
|
+
// under "Keep", and Instant is at the bottom. Climbing up from Instant put
|
|
44
|
+
// the levels closest to Pro furthest from it.
|
|
42
45
|
return [
|
|
43
46
|
{ label: pinnedModel ? `Keep ${pinnedModel}` : "Keep the current selection", hint: "whatever this repo pinned" },
|
|
44
|
-
{ effort: "
|
|
45
|
-
{ effort: "medium", label: "Medium" },
|
|
47
|
+
{ effort: "max", label: "Extra high", hint: "strongest of the standard levels" },
|
|
46
48
|
{ effort: "high", label: "High" },
|
|
47
|
-
{ effort: "
|
|
49
|
+
{ effort: "medium", label: "Medium" },
|
|
50
|
+
{ effort: "instant", label: "Instant", hint: "seconds, for a quick question" }
|
|
48
51
|
];
|
|
49
52
|
}
|
package/dist/tui-run.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import readline from "node:readline";
|
|
10
10
|
import { renderBanner } from "./banner.js";
|
|
11
11
|
import { destinationChoices, effortChoices, SEND_KINDS } from "./tui-flow.js";
|
|
12
|
-
import { consultArgsFromChoices, conversationThreadUrl, moveCursor, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
|
|
12
|
+
import { consultArgsFromChoices, conversationsInProject, conversationThreadUrl, moveCursor, progressLabel, renderContextPanel, renderProgressBar, renderSelectList } from "./tui.js";
|
|
13
13
|
const ESC = "";
|
|
14
14
|
const CLEAR = `${ESC}[2J${ESC}[H`;
|
|
15
15
|
// The alternate screen buffer: the terminal comes back exactly as it was, so a
|
|
@@ -154,9 +154,12 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
154
154
|
const pinnedPromise = deps.pinnedProject?.().catch(() => undefined);
|
|
155
155
|
const pinnedModelPromise = deps.pinnedModel?.().catch(() => undefined);
|
|
156
156
|
const conversationsPromise = deps.listConversations?.().catch(() => []);
|
|
157
|
+
// A normal chat asks one more question than the tool kinds do, so how many
|
|
158
|
+
// steps remain is not known until this first answer is given.
|
|
159
|
+
const totalSteps = 4;
|
|
157
160
|
const kindChoice = await pick(io, {
|
|
158
161
|
title: "What kind of send is this?",
|
|
159
|
-
step: { index: 1
|
|
162
|
+
step: { index: 1 },
|
|
160
163
|
options: SEND_KINDS.map((kind) => ({ label: kind.label, ...(kind.hint ? { hint: kind.hint } : {}) }))
|
|
161
164
|
}, banner);
|
|
162
165
|
if (kindChoice === undefined)
|
|
@@ -165,10 +168,13 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
165
168
|
// Only an ordinary chat has a reasoning level to choose: the tool kinds
|
|
166
169
|
// bring their own pipeline, and an effort would deselect Pro under them.
|
|
167
170
|
let effort;
|
|
168
|
-
|
|
171
|
+
const asksReasoning = SEND_KINDS[kindChoice].id === "chat";
|
|
172
|
+
const steps = asksReasoning ? totalSteps : totalSteps - 1;
|
|
173
|
+
if (asksReasoning) {
|
|
169
174
|
const efforts = effortChoices(await pinnedModelPromise);
|
|
170
175
|
const chosen = await pick(io, {
|
|
171
176
|
title: "How much reasoning?",
|
|
177
|
+
step: { index: 2, total: steps },
|
|
172
178
|
options: efforts.map((entry) => ({ label: entry.label, ...(entry.hint ? { hint: entry.hint } : {}) }))
|
|
173
179
|
}, banner);
|
|
174
180
|
if (chosen === undefined)
|
|
@@ -184,7 +190,7 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
184
190
|
const destinations = destinationChoices(pinnedProject);
|
|
185
191
|
const destinationChoice = await pick(io, {
|
|
186
192
|
title: "Where should it go?",
|
|
187
|
-
step: { index: 2, total:
|
|
193
|
+
step: { index: asksReasoning ? 3 : 2, total: steps },
|
|
188
194
|
options: destinations.map((entry) => ({ label: entry.label, ...(entry.hint ? { hint: entry.hint } : {}) }))
|
|
189
195
|
}, header);
|
|
190
196
|
if (destinationChoice === undefined)
|
|
@@ -205,16 +211,34 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
205
211
|
targetUrl = conversationThreadUrl(conversations[chosen].id);
|
|
206
212
|
}
|
|
207
213
|
else if (destination === "project") {
|
|
208
|
-
const
|
|
214
|
+
const detailed = (await deps.listProjectsWithIds?.()) ?? [];
|
|
215
|
+
const projects = detailed.length > 0 ? detailed : (await deps.listProjects()).map((name) => ({ id: "", name }));
|
|
209
216
|
if (projects.length === 0) {
|
|
210
217
|
io.write("\nNo projects are visible in the ChatGPT sidebar.\n");
|
|
211
218
|
return 1;
|
|
212
219
|
}
|
|
213
|
-
const chosen = await pick(io, { title: "Which project?", options: projects.map((
|
|
220
|
+
const chosen = await pick(io, { title: "Which project?", options: projects.map((entry) => ({ label: entry.name })) }, header);
|
|
214
221
|
if (chosen === undefined)
|
|
215
222
|
return cancel(io);
|
|
216
223
|
projectMode = "existing";
|
|
217
|
-
projectName = projects[chosen];
|
|
224
|
+
projectName = projects[chosen].name;
|
|
225
|
+
// Entering a project usually means going back to something in it, so
|
|
226
|
+
// offer its chats before starting yet another one.
|
|
227
|
+
const inside = conversationsInProject((await conversationsPromise) ?? [], projects[chosen].id || undefined);
|
|
228
|
+
if (inside.length > 0) {
|
|
229
|
+
const options = [
|
|
230
|
+
{ label: "Start a new chat in this project" },
|
|
231
|
+
...inside.map((entry) => ({ label: entry.title }))
|
|
232
|
+
];
|
|
233
|
+
const picked = await pick(io, { title: `${projects[chosen].name}`, options }, header);
|
|
234
|
+
if (picked === undefined)
|
|
235
|
+
return cancel(io);
|
|
236
|
+
if (picked > 0) {
|
|
237
|
+
targetUrl = conversationThreadUrl(inside[picked - 1].id);
|
|
238
|
+
projectMode = "current";
|
|
239
|
+
projectName = undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
218
242
|
}
|
|
219
243
|
else if (destination === "project-new") {
|
|
220
244
|
projectName = await askLine(io, `${CLEAR}${header}New project\n\n name: `);
|
|
@@ -228,7 +252,7 @@ export async function runInteractiveConsult(io, deps) {
|
|
|
228
252
|
// The prompt comes last: what you type depends on what you just decided.
|
|
229
253
|
io.write(CLEAR + header + SHOW_CURSOR);
|
|
230
254
|
const kindLabel = SEND_KINDS[kindChoice].label;
|
|
231
|
-
const prompt = await askLine(io, `${kindLabel} Step
|
|
255
|
+
const prompt = await askLine(io, `${kindLabel} Step ${steps} of ${steps}\n\n prompt: `);
|
|
232
256
|
io.write(HIDE_CURSOR);
|
|
233
257
|
if (prompt.length === 0) {
|
|
234
258
|
io.write("Nothing to ask.\n");
|
package/dist/tui.js
CHANGED
|
@@ -57,6 +57,46 @@ export function consultArgsFromChoices(choices) {
|
|
|
57
57
|
args.push("--", prompt);
|
|
58
58
|
return args;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Projects with the id their conversations are tagged with.
|
|
62
|
+
*
|
|
63
|
+
* The sidebar gives names only, which is enough to ENTER a project but not to
|
|
64
|
+
* tell which chats live in it - and "open the project, then pick the session
|
|
65
|
+
* inside it" needs exactly that link.
|
|
66
|
+
*/
|
|
67
|
+
export function projectsWithIdsExpression() {
|
|
68
|
+
return `(async () => {
|
|
69
|
+
let token = "";
|
|
70
|
+
try {
|
|
71
|
+
const session = await fetch("/api/auth/session", { credentials: "include" });
|
|
72
|
+
if (!session.ok) return [];
|
|
73
|
+
const parsed = await session.json();
|
|
74
|
+
token = (parsed && parsed.accessToken) || "";
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch("/backend-api/gizmos/snorlax/sidebar", {
|
|
80
|
+
credentials: "include",
|
|
81
|
+
headers: token ? { Authorization: "Bearer " + token } : {}
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) return [];
|
|
84
|
+
const listed = await response.json();
|
|
85
|
+
return ((listed && listed.items) || [])
|
|
86
|
+
.map((item) => item && item.gizmo)
|
|
87
|
+
.filter((gizmo) => gizmo && gizmo.id)
|
|
88
|
+
.map((gizmo) => ({ id: gizmo.id, name: ((gizmo.display && gizmo.display.name) || gizmo.name || "").trim() || gizmo.id }));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
})()`;
|
|
93
|
+
}
|
|
94
|
+
/** The conversations that live inside a project, or all of them without one. */
|
|
95
|
+
export function conversationsInProject(conversations, projectId) {
|
|
96
|
+
if (!projectId)
|
|
97
|
+
return conversations;
|
|
98
|
+
return conversations.filter((conversation) => conversation.gizmoId === projectId);
|
|
99
|
+
}
|
|
60
100
|
/**
|
|
61
101
|
* Recent conversations with their titles, for the "continue an existing chat"
|
|
62
102
|
* list. Only the sidebar listing is fetched - the transcripts themselves are
|
|
@@ -82,7 +122,11 @@ export function recentConversationTitlesExpression(limit = 10) {
|
|
|
82
122
|
const listed = await response.json();
|
|
83
123
|
return ((listed && listed.items) || [])
|
|
84
124
|
.filter((item) => item && item.id)
|
|
85
|
-
.map((item) => ({
|
|
125
|
+
.map((item) => ({
|
|
126
|
+
id: item.id,
|
|
127
|
+
title: (item.title || "").trim() || "Untitled",
|
|
128
|
+
...(item.gizmo_id ? { gizmoId: item.gizmo_id } : {})
|
|
129
|
+
}));
|
|
86
130
|
} catch (error) {
|
|
87
131
|
return [];
|
|
88
132
|
}
|
|
@@ -107,7 +151,9 @@ export function renderSelectList(input) {
|
|
|
107
151
|
const color = input.color ?? true;
|
|
108
152
|
const width = input.width ?? 100;
|
|
109
153
|
const selected = new Set(input.selected ?? []);
|
|
110
|
-
const step = input.step
|
|
154
|
+
const step = input.step
|
|
155
|
+
? paint(` Step ${input.step.index}${input.step.total ? ` of ${input.step.total}` : ""}`, DIM, color)
|
|
156
|
+
: "";
|
|
111
157
|
const lines = [`${paint(input.title, BOLD, color)}${step}`, ""];
|
|
112
158
|
// Hints line up in their own column; ragged hints read as noise next to the
|
|
113
159
|
// labels they belong to.
|