theclawbay 0.5.1 → 0.6.2
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/commands/codex.js +2 -0
- package/dist/commands/logout.js +3 -0
- package/dist/commands/setup.js +75 -14
- package/dist/commands/usage.js +54 -9
- package/dist/lib/base-command.d.ts +1 -7
- package/dist/lib/base-command.js +12 -65
- package/dist/lib/clients/codex.d.ts +8 -3
- package/dist/lib/clients/codex.js +26 -30
- package/dist/lib/codex-history-migration.d.ts +26 -0
- package/dist/lib/codex-history-migration.js +109 -40
- package/dist/lib/main-menu.js +4 -1
- package/dist/lib/shared/app-shell.d.ts +11 -0
- package/dist/lib/shared/app-shell.js +96 -0
- package/dist/lib/shared/prompts.d.ts +8 -0
- package/dist/lib/shared/prompts.js +75 -0
- package/dist/lib/shared/tui.d.ts +34 -1
- package/dist/lib/shared/tui.js +182 -32
- package/package.json +1 -1
- package/theclawbay-supported-models.json +35 -2
|
@@ -3,12 +3,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildConversationScope = buildConversationScope;
|
|
6
7
|
exports.migrateSessionProviders = migrateSessionProviders;
|
|
7
8
|
exports.migrateStateDbProviders = migrateStateDbProviders;
|
|
8
9
|
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
11
|
const node_child_process_1 = require("node:child_process");
|
|
11
12
|
const STATE_DB_FILE_PATTERN = /^state_\d+\.sqlite$/;
|
|
13
|
+
// Rough IO model for the "~time" labels. Each session is read and, when it
|
|
14
|
+
// carries a provider tag we neutralize, rewritten — so budget ~1.4x its bytes,
|
|
15
|
+
// plus a small per-file syscall overhead. Deliberately conservative.
|
|
16
|
+
const MIGRATION_BYTES_PER_SEC = 45 * 1024 * 1024;
|
|
17
|
+
const MIGRATION_PER_FILE_SECONDS = 0.004;
|
|
18
|
+
function estimateMigrationSeconds(count, bytes) {
|
|
19
|
+
if (count <= 0)
|
|
20
|
+
return 0;
|
|
21
|
+
return (bytes * 1.4) / MIGRATION_BYTES_PER_SEC + count * MIGRATION_PER_FILE_SECONDS;
|
|
22
|
+
}
|
|
12
23
|
function resolvePythonCommand() {
|
|
13
24
|
const candidates = [
|
|
14
25
|
{ command: "python3", args: ["-c", "import sqlite3"] },
|
|
@@ -26,16 +37,6 @@ function resolvePythonCommand() {
|
|
|
26
37
|
}
|
|
27
38
|
return null;
|
|
28
39
|
}
|
|
29
|
-
function renderProgressBar(current, total) {
|
|
30
|
-
if (total <= 0)
|
|
31
|
-
return "";
|
|
32
|
-
const width = 24;
|
|
33
|
-
const ratio = Math.max(0, Math.min(1, current / total));
|
|
34
|
-
const filled = Math.round(width * ratio);
|
|
35
|
-
return `[${"#".repeat(filled)}${"-".repeat(width - filled)}] ${Math.round(ratio * 100)
|
|
36
|
-
.toString()
|
|
37
|
-
.padStart(3, " ")}%`;
|
|
38
|
-
}
|
|
39
40
|
function inferRolloutUpdatedAt(source) {
|
|
40
41
|
const lines = source.split(/\r?\n/);
|
|
41
42
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -84,6 +85,51 @@ async function listSessionRollouts(root) {
|
|
|
84
85
|
await walk(root);
|
|
85
86
|
return results;
|
|
86
87
|
}
|
|
88
|
+
// Newest-first rollout paths. Codex names sessions
|
|
89
|
+
// `rollout-<ISO timestamp>-<uuid>.jsonl` inside sessions/YYYY/MM/DD/, so a
|
|
90
|
+
// descending lexicographic sort is chronological without statting every file.
|
|
91
|
+
function sortNewestFirst(paths) {
|
|
92
|
+
return [...paths].sort((a, b) => (a < b ? 1 : a > b ? -1 : 0));
|
|
93
|
+
}
|
|
94
|
+
async function statCandidates(paths) {
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const p of paths) {
|
|
97
|
+
try {
|
|
98
|
+
const stats = await promises_1.default.stat(p);
|
|
99
|
+
out.push({ path: p, size: stats.size, mtimeMs: stats.mtimeMs });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// ignore transient missing file / race
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
// Inspect the local Codex session store and produce the pickable scope tiers
|
|
108
|
+
// (Last 10 / 30 / 100 / All) with per-tier time estimates. Only a bounded
|
|
109
|
+
// sample is statted so this stays instant even for huge histories.
|
|
110
|
+
async function buildConversationScope(codexHome) {
|
|
111
|
+
const sessionsRoot = node_path_1.default.join(codexHome, "sessions");
|
|
112
|
+
const paths = sortNewestFirst(await listSessionRollouts(sessionsRoot));
|
|
113
|
+
const total = paths.length;
|
|
114
|
+
const sampleSize = Math.min(total, 100);
|
|
115
|
+
const sample = await statCandidates(paths.slice(0, sampleSize));
|
|
116
|
+
const sampledBytes = sample.reduce((sum, candidate) => sum + candidate.size, 0);
|
|
117
|
+
const avgSize = sample.length > 0 ? sampledBytes / sample.length : 0;
|
|
118
|
+
const bytesForCount = (count) => {
|
|
119
|
+
if (count <= sample.length) {
|
|
120
|
+
return sample.slice(0, count).reduce((sum, candidate) => sum + candidate.size, 0);
|
|
121
|
+
}
|
|
122
|
+
return sampledBytes + avgSize * (count - sample.length);
|
|
123
|
+
};
|
|
124
|
+
const options = [];
|
|
125
|
+
for (const limit of [10, 30, 100]) {
|
|
126
|
+
if (total <= limit)
|
|
127
|
+
break; // a tier that already covers everything is just "All"
|
|
128
|
+
options.push({ limit, count: limit, estSeconds: estimateMigrationSeconds(limit, bytesForCount(limit)) });
|
|
129
|
+
}
|
|
130
|
+
options.push({ limit: null, count: total, estSeconds: estimateMigrationSeconds(total, bytesForCount(total)) });
|
|
131
|
+
return { total, paths, options };
|
|
132
|
+
}
|
|
87
133
|
async function readMigrationState(migrationStateFile) {
|
|
88
134
|
try {
|
|
89
135
|
const raw = await promises_1.default.readFile(migrationStateFile, "utf8");
|
|
@@ -126,27 +172,45 @@ async function computeSessionSnapshot(files) {
|
|
|
126
172
|
}
|
|
127
173
|
async function migrateSessionProviders(params) {
|
|
128
174
|
const sessionsRoot = node_path_1.default.join(params.codexHome, "sessions");
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
const scoped = Array.isArray(params.files);
|
|
176
|
+
const files = scoped ? params.files : await listSessionRollouts(sessionsRoot);
|
|
177
|
+
// The mtime/count fast-skip only makes sense for a full pass — a bounded
|
|
178
|
+
// subset should always run so a user who picked "Last 30" isn't told there's
|
|
179
|
+
// nothing to do just because a prior full pass recorded the snapshot.
|
|
180
|
+
if (!scoped) {
|
|
181
|
+
const snapshot = await computeSessionSnapshot(files);
|
|
182
|
+
const previousState = await readMigrationState(params.migrationStateFile);
|
|
183
|
+
if (previousState &&
|
|
184
|
+
previousState.fileCount === snapshot.fileCount &&
|
|
185
|
+
previousState.maxMtimeMs === snapshot.maxMtimeMs) {
|
|
186
|
+
return { scanned: 0, rewritten: 0, skipped: 0, retimed: 0, fastSkipped: true };
|
|
187
|
+
}
|
|
136
188
|
}
|
|
137
189
|
let rewritten = 0;
|
|
138
190
|
let skipped = 0;
|
|
139
191
|
let retimed = 0;
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
192
|
+
const startedAt = Date.now();
|
|
193
|
+
let lastEmit = 0;
|
|
194
|
+
const emit = (processed, currentFile) => {
|
|
195
|
+
if (!params.onProgress)
|
|
143
196
|
return;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
if (processed < files.length && now - lastEmit < 60)
|
|
199
|
+
return; // throttle repaints
|
|
200
|
+
lastEmit = now;
|
|
201
|
+
const elapsedMs = now - startedAt;
|
|
202
|
+
const perFile = processed > 0 ? elapsedMs / processed : 0;
|
|
203
|
+
const etaSeconds = perFile > 0 ? (perFile * (files.length - processed)) / 1000 : 0;
|
|
204
|
+
params.onProgress({
|
|
205
|
+
processed,
|
|
206
|
+
total: files.length,
|
|
207
|
+
rewritten,
|
|
208
|
+
elapsedMs,
|
|
209
|
+
etaSeconds,
|
|
210
|
+
currentFile,
|
|
211
|
+
});
|
|
148
212
|
};
|
|
149
|
-
|
|
213
|
+
emit(0, "");
|
|
150
214
|
for (let i = 0; i < files.length; i++) {
|
|
151
215
|
const file = files[i];
|
|
152
216
|
let source = "";
|
|
@@ -157,7 +221,7 @@ async function migrateSessionProviders(params) {
|
|
|
157
221
|
}
|
|
158
222
|
catch {
|
|
159
223
|
skipped++;
|
|
160
|
-
|
|
224
|
+
emit(i + 1, file);
|
|
161
225
|
continue;
|
|
162
226
|
}
|
|
163
227
|
const desiredMtime = inferRolloutUpdatedAt(source) ?? originalStats.mtime;
|
|
@@ -177,14 +241,14 @@ async function migrateSessionProviders(params) {
|
|
|
177
241
|
if (!source.trim()) {
|
|
178
242
|
skipped++;
|
|
179
243
|
await maybeRetainMtime();
|
|
180
|
-
|
|
244
|
+
emit(i + 1, file);
|
|
181
245
|
continue;
|
|
182
246
|
}
|
|
183
247
|
const lines = source.split(/\r?\n/);
|
|
184
248
|
const firstNonEmptyIndex = lines.findIndex((line) => line.trim().length > 0);
|
|
185
249
|
if (firstNonEmptyIndex < 0) {
|
|
186
250
|
skipped++;
|
|
187
|
-
|
|
251
|
+
emit(i + 1, file);
|
|
188
252
|
continue;
|
|
189
253
|
}
|
|
190
254
|
let parsed;
|
|
@@ -193,29 +257,29 @@ async function migrateSessionProviders(params) {
|
|
|
193
257
|
}
|
|
194
258
|
catch {
|
|
195
259
|
skipped++;
|
|
196
|
-
|
|
260
|
+
emit(i + 1, file);
|
|
197
261
|
continue;
|
|
198
262
|
}
|
|
199
263
|
if ((parsed.type ?? "") !== "session_meta") {
|
|
200
264
|
skipped++;
|
|
201
|
-
|
|
265
|
+
emit(i + 1, file);
|
|
202
266
|
continue;
|
|
203
267
|
}
|
|
204
268
|
const payload = parsed.payload;
|
|
205
269
|
if (!payload || typeof payload !== "object") {
|
|
206
270
|
skipped++;
|
|
207
|
-
|
|
271
|
+
emit(i + 1, file);
|
|
208
272
|
continue;
|
|
209
273
|
}
|
|
210
274
|
const currentProvider = payload.model_provider;
|
|
211
275
|
if (typeof currentProvider !== "string") {
|
|
212
276
|
skipped++;
|
|
213
|
-
|
|
277
|
+
emit(i + 1, file);
|
|
214
278
|
continue;
|
|
215
279
|
}
|
|
216
280
|
if (!params.neutralizeSources.has(currentProvider)) {
|
|
217
281
|
skipped++;
|
|
218
|
-
|
|
282
|
+
emit(i + 1, file);
|
|
219
283
|
continue;
|
|
220
284
|
}
|
|
221
285
|
// Remove explicit provider tag so the same thread stays visible across provider contexts.
|
|
@@ -225,19 +289,24 @@ async function migrateSessionProviders(params) {
|
|
|
225
289
|
if (next === source) {
|
|
226
290
|
skipped++;
|
|
227
291
|
await maybeRetainMtime();
|
|
228
|
-
|
|
292
|
+
emit(i + 1, file);
|
|
229
293
|
continue;
|
|
230
294
|
}
|
|
231
295
|
await promises_1.default.writeFile(file, next, "utf8");
|
|
232
296
|
await maybeRetainMtime();
|
|
233
297
|
rewritten++;
|
|
234
|
-
|
|
298
|
+
emit(i + 1, file);
|
|
299
|
+
}
|
|
300
|
+
// Only record the fast-skip snapshot after a full pass; a bounded subset
|
|
301
|
+
// hasn't seen every session, so it must not claim the whole store is done.
|
|
302
|
+
if (!scoped) {
|
|
303
|
+
const snapshot = await computeSessionSnapshot(files);
|
|
304
|
+
await writeMigrationState(params.migrationStateFile, {
|
|
305
|
+
version: 1,
|
|
306
|
+
fileCount: snapshot.fileCount,
|
|
307
|
+
maxMtimeMs: snapshot.maxMtimeMs,
|
|
308
|
+
});
|
|
235
309
|
}
|
|
236
|
-
await writeMigrationState(params.migrationStateFile, {
|
|
237
|
-
version: 1,
|
|
238
|
-
fileCount: snapshot.fileCount,
|
|
239
|
-
maxMtimeMs: snapshot.maxMtimeMs,
|
|
240
|
-
});
|
|
241
310
|
return { scanned: files.length, rewritten, skipped, retimed, fastSkipped: false };
|
|
242
311
|
}
|
|
243
312
|
async function listStateDbFiles(codexHome) {
|
package/dist/lib/main-menu.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.promptMainMenu = promptMainMenu;
|
|
4
4
|
const prompts_1 = require("./shared/prompts");
|
|
5
|
+
const app_shell_1 = require("./shared/app-shell");
|
|
5
6
|
const tui_1 = require("./shared/tui");
|
|
6
7
|
const ITEMS = [
|
|
7
8
|
{ icon: tui_1.glyph.claw, label: "Set up / connect tools", desc: "Point Codex, Claude & others at The Claw Bay", argv: ["setup"] },
|
|
@@ -12,6 +13,7 @@ const ITEMS = [
|
|
|
12
13
|
async function promptMainMenu() {
|
|
13
14
|
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
14
15
|
return null;
|
|
16
|
+
app_shell_1.AppShell.printHeaderOnce("Your relay for Codex, Claude & friends");
|
|
15
17
|
let cursor = 0;
|
|
16
18
|
const quitIndex = ITEMS.length;
|
|
17
19
|
const total = ITEMS.length + 1;
|
|
@@ -29,7 +31,8 @@ async function promptMainMenu() {
|
|
|
29
31
|
return [
|
|
30
32
|
"",
|
|
31
33
|
...(0, tui_1.renderBox)({
|
|
32
|
-
title: "
|
|
34
|
+
title: "Home",
|
|
35
|
+
accent: "coral",
|
|
33
36
|
lines: [
|
|
34
37
|
tui_1.tint.dim("What would you like to do?"),
|
|
35
38
|
"",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ProgressHandle = {
|
|
2
|
+
update(message: string): void;
|
|
3
|
+
succeed(message?: string): void;
|
|
4
|
+
fail(message?: string): void;
|
|
5
|
+
stop(): void;
|
|
6
|
+
};
|
|
7
|
+
export declare const AppShell: {
|
|
8
|
+
printHeaderOnce(subtitle: string): void;
|
|
9
|
+
progress(enabled: boolean): ProgressHandle;
|
|
10
|
+
commit(lines: string[]): void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppShell = void 0;
|
|
4
|
+
const tui_1 = require("./tui");
|
|
5
|
+
const NOOP_PROGRESS = {
|
|
6
|
+
update() { },
|
|
7
|
+
succeed() { },
|
|
8
|
+
fail() { },
|
|
9
|
+
stop() { },
|
|
10
|
+
};
|
|
11
|
+
let headerPrinted = false;
|
|
12
|
+
function createSpinnerProgress() {
|
|
13
|
+
const stdout = process.stdout;
|
|
14
|
+
let label = "";
|
|
15
|
+
let frameIndex = 0;
|
|
16
|
+
let timer = null;
|
|
17
|
+
let live = false;
|
|
18
|
+
const frame = () => tui_1.SPINNER_FRAMES[frameIndex % tui_1.SPINNER_FRAMES.length] ?? tui_1.SPINNER_FRAMES[0];
|
|
19
|
+
const renderLive = () => stdout.write(`\r\x1b[2K ${tui_1.tint.accent(frame())} ${tui_1.tint.foam(label)}`);
|
|
20
|
+
const startTimer = () => {
|
|
21
|
+
if (timer)
|
|
22
|
+
return;
|
|
23
|
+
timer = setInterval(() => {
|
|
24
|
+
frameIndex = (frameIndex + 1) % tui_1.SPINNER_FRAMES.length;
|
|
25
|
+
renderLive();
|
|
26
|
+
}, 80);
|
|
27
|
+
if (typeof timer.unref === "function")
|
|
28
|
+
timer.unref();
|
|
29
|
+
};
|
|
30
|
+
const stopTimer = () => {
|
|
31
|
+
if (!timer)
|
|
32
|
+
return;
|
|
33
|
+
clearInterval(timer);
|
|
34
|
+
timer = null;
|
|
35
|
+
};
|
|
36
|
+
// Freeze the active line into permanent scrollback with a final glyph.
|
|
37
|
+
const commit = (mark, text) => {
|
|
38
|
+
stopTimer();
|
|
39
|
+
stdout.write(`\r\x1b[2K ${mark} ${text}\n`);
|
|
40
|
+
live = false;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
update(message) {
|
|
44
|
+
if (live)
|
|
45
|
+
commit(tui_1.tint.green(tui_1.glyph.tick), tui_1.tint.foam(label));
|
|
46
|
+
label = message;
|
|
47
|
+
live = true;
|
|
48
|
+
startTimer();
|
|
49
|
+
renderLive();
|
|
50
|
+
},
|
|
51
|
+
succeed(message) {
|
|
52
|
+
// Finalize the active step under its own name, then add the headline —
|
|
53
|
+
// never rename the last step to the summary text.
|
|
54
|
+
if (live)
|
|
55
|
+
commit(tui_1.tint.green(tui_1.glyph.tick), tui_1.tint.foam(label));
|
|
56
|
+
if (message)
|
|
57
|
+
stdout.write(` ${tui_1.tint.green(tui_1.glyph.tick)} ${tui_1.tint.bold(message)}\n`);
|
|
58
|
+
},
|
|
59
|
+
fail(message) {
|
|
60
|
+
// Mark the step that actually failed with its own name.
|
|
61
|
+
if (live)
|
|
62
|
+
commit(tui_1.tint.red(tui_1.glyph.cross), tui_1.tint.red(label));
|
|
63
|
+
if (message)
|
|
64
|
+
stdout.write(` ${tui_1.tint.red(tui_1.glyph.cross)} ${tui_1.tint.red(message)}\n`);
|
|
65
|
+
},
|
|
66
|
+
stop() {
|
|
67
|
+
if (live)
|
|
68
|
+
commit(tui_1.tint.green(tui_1.glyph.tick), tui_1.tint.foam(label));
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
exports.AppShell = {
|
|
73
|
+
// Print the brand lockup a single time per process. The menu and every
|
|
74
|
+
// command call this; only the first one actually paints, so launching setup
|
|
75
|
+
// from the menu never double-stamps the header.
|
|
76
|
+
printHeaderOnce(subtitle) {
|
|
77
|
+
if (headerPrinted)
|
|
78
|
+
return;
|
|
79
|
+
headerPrinted = true;
|
|
80
|
+
if (!process.stdout.isTTY)
|
|
81
|
+
return;
|
|
82
|
+
process.stdout.write(`\n${(0, tui_1.banner)(subtitle)}\n\n`);
|
|
83
|
+
},
|
|
84
|
+
// A growing, persistent checklist backed by a single animated line.
|
|
85
|
+
progress(enabled) {
|
|
86
|
+
if (!enabled || !process.stdout.isTTY)
|
|
87
|
+
return NOOP_PROGRESS;
|
|
88
|
+
return createSpinnerProgress();
|
|
89
|
+
},
|
|
90
|
+
// Commit standalone lines straight into scrollback (used by summaries and
|
|
91
|
+
// one-off notices) so they share the shell's left-gutter rhythm.
|
|
92
|
+
commit(lines) {
|
|
93
|
+
for (const line of lines)
|
|
94
|
+
process.stdout.write(`${line}\n`);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { SetupClient, SetupClientId } from "./client-registry";
|
|
2
2
|
import { ConfiguredModelSelection, ModelResolution } from "./models";
|
|
3
|
+
import { ConversationScopeOption } from "../codex-history-migration";
|
|
4
|
+
export type ConversationScopeChoice = {
|
|
5
|
+
limit: number | null;
|
|
6
|
+
} | null;
|
|
7
|
+
export declare function promptConversationScope(scope: {
|
|
8
|
+
total: number;
|
|
9
|
+
options: ConversationScopeOption[];
|
|
10
|
+
}): Promise<ConversationScopeChoice>;
|
|
3
11
|
export type SetupAuthMethod = "device-session" | "api-key";
|
|
4
12
|
export declare function promptAuthMethod(): Promise<SetupAuthMethod>;
|
|
5
13
|
export declare function promptApiKey(): Promise<string>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.promptConversationScope = promptConversationScope;
|
|
3
4
|
exports.promptAuthMethod = promptAuthMethod;
|
|
4
5
|
exports.promptApiKey = promptApiKey;
|
|
5
6
|
exports.formatSetupClientLabel = formatSetupClientLabel;
|
|
@@ -17,6 +18,80 @@ const promises_1 = require("node:readline/promises");
|
|
|
17
18
|
const fsx_1 = require("./fsx");
|
|
18
19
|
const models_1 = require("./models");
|
|
19
20
|
const tui_1 = require("./tui");
|
|
21
|
+
// Ask how much Codex history to convert. Every tier shows a count and a time
|
|
22
|
+
// estimate up front, so nobody unknowingly kicks off an hours-long full pass —
|
|
23
|
+
// the runaway that used to look like a hang. "Skip" is always first, and the
|
|
24
|
+
// smallest bounded tier is preselected as the safe default.
|
|
25
|
+
async function promptConversationScope(scope) {
|
|
26
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || scope.total === 0)
|
|
27
|
+
return null;
|
|
28
|
+
const items = [
|
|
29
|
+
{ limit: 0, label: "Skip for now", hint: "Leave existing Codex history untouched", recommended: false },
|
|
30
|
+
];
|
|
31
|
+
// Preselect the first bounded tier (e.g. "Last 10/30") — fast and safe.
|
|
32
|
+
const firstBounded = scope.options.find((option) => option.limit !== null)?.limit ?? null;
|
|
33
|
+
for (const option of scope.options) {
|
|
34
|
+
const isAll = option.limit === null;
|
|
35
|
+
const label = isAll ? `All ${(0, tui_1.formatCount)(option.count)} conversations` : `Last ${option.limit} conversations`;
|
|
36
|
+
const est = `~${(0, tui_1.formatEta)(option.estSeconds)}`;
|
|
37
|
+
const hint = isAll
|
|
38
|
+
? `${(0, tui_1.formatCount)(option.count)} total · ${est}${option.estSeconds > 120 ? " — this can take a while" : ""}`
|
|
39
|
+
: `${est} to convert`;
|
|
40
|
+
items.push({ limit: option.limit, label, hint, recommended: option.limit === firstBounded });
|
|
41
|
+
}
|
|
42
|
+
let cursor = Math.max(0, items.findIndex((item) => item.recommended));
|
|
43
|
+
if (cursor < 0)
|
|
44
|
+
cursor = 0;
|
|
45
|
+
const render = () => {
|
|
46
|
+
const rows = items.map((item, index) => {
|
|
47
|
+
const active = index === cursor;
|
|
48
|
+
const pointer = active ? tui_1.tint.accentBold(tui_1.glyph.pointer) : " ";
|
|
49
|
+
const mark = active ? tui_1.tint.accent(tui_1.glyph.radioOn) : tui_1.tint.gray(tui_1.glyph.radioOff);
|
|
50
|
+
const label = active ? tui_1.tint.accentBold(item.label) : tui_1.tint.foam(item.label);
|
|
51
|
+
const badge = item.recommended ? ` ${tui_1.tint.sea("recommended")}` : "";
|
|
52
|
+
const body = `${pointer} ${mark} ${label}${badge}`;
|
|
53
|
+
const detail = tui_1.tint.dim(`— ${item.hint}`);
|
|
54
|
+
const width = (0, tui_1.terminalColumns)() - 6;
|
|
55
|
+
const pad = Math.max(1, width - (0, tui_1.visibleWidth)(body) - (0, tui_1.visibleWidth)(detail));
|
|
56
|
+
return `${body}${" ".repeat(pad)}${detail}`;
|
|
57
|
+
});
|
|
58
|
+
return [
|
|
59
|
+
"",
|
|
60
|
+
...(0, tui_1.renderBox)({
|
|
61
|
+
title: "Convert old Codex conversations?",
|
|
62
|
+
accent: "coral",
|
|
63
|
+
lines: [
|
|
64
|
+
tui_1.tint.dim("Keeps past threads visible under The Claw Bay. Newest conversations are converted first."),
|
|
65
|
+
"",
|
|
66
|
+
...rows,
|
|
67
|
+
],
|
|
68
|
+
footer: (0, tui_1.keyHints)([
|
|
69
|
+
["↑↓", "move"],
|
|
70
|
+
["enter", "select"],
|
|
71
|
+
]),
|
|
72
|
+
}),
|
|
73
|
+
];
|
|
74
|
+
};
|
|
75
|
+
return runKeyLoop({
|
|
76
|
+
render,
|
|
77
|
+
onKey: (key, finish, repaint) => {
|
|
78
|
+
if (key.name === "up" || key.name === "k") {
|
|
79
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
80
|
+
repaint();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (key.name === "down" || key.name === "j") {
|
|
84
|
+
cursor = (cursor + 1) % items.length;
|
|
85
|
+
repaint();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (key.name === "return" || key.name === "enter") {
|
|
89
|
+
const chosen = items[cursor];
|
|
90
|
+
finish(chosen.limit === 0 ? null : { limit: chosen.limit });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
20
95
|
// Ask how the user wants to authenticate this machine. Browser sign-in is the
|
|
21
96
|
// recommended default; an API key is offered for people who prefer to paste one.
|
|
22
97
|
async function promptAuthMethod() {
|
package/dist/lib/shared/tui.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export declare const tint: {
|
|
2
2
|
accent: (text: string) => string;
|
|
3
3
|
accentBold: (text: string) => string;
|
|
4
|
+
sea: (text: string) => string;
|
|
5
|
+
seaBold: (text: string) => string;
|
|
6
|
+
ink: (text: string) => string;
|
|
7
|
+
foam: (text: string) => string;
|
|
4
8
|
bold: (text: string) => string;
|
|
5
9
|
dim: (text: string) => string;
|
|
6
10
|
gray: (text: string) => string;
|
|
11
|
+
line: (text: string) => string;
|
|
7
12
|
green: (text: string) => string;
|
|
8
13
|
yellow: (text: string) => string;
|
|
9
14
|
red: (text: string) => string;
|
|
@@ -21,21 +26,49 @@ export declare const glyph: {
|
|
|
21
26
|
readonly cross: "✗";
|
|
22
27
|
readonly bullet: "·";
|
|
23
28
|
readonly claw: "🦞";
|
|
29
|
+
readonly wave: "≈";
|
|
30
|
+
readonly diamond: "◆";
|
|
31
|
+
readonly diamondOpen: "◇";
|
|
32
|
+
readonly arrow: "→";
|
|
33
|
+
readonly barFull: "█";
|
|
34
|
+
readonly barEmpty: "░";
|
|
35
|
+
readonly dotActive: "◈";
|
|
24
36
|
};
|
|
37
|
+
export declare const SPINNER_FRAMES: readonly ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
25
38
|
export declare function visibleWidth(text: string): number;
|
|
26
39
|
export declare function terminalColumns(): number;
|
|
27
40
|
export declare function truncateVisible(text: string, maxWidth: number): string;
|
|
28
|
-
export
|
|
41
|
+
export declare function padVisible(text: string, width: number): string;
|
|
42
|
+
export type BoxAccent = "coral" | "sea" | "danger" | "line";
|
|
29
43
|
export declare function renderBox(params: {
|
|
30
44
|
title?: string;
|
|
31
45
|
lines: string[];
|
|
32
46
|
footer?: string;
|
|
33
47
|
width?: number;
|
|
48
|
+
accent?: BoxAccent;
|
|
34
49
|
}): string[];
|
|
50
|
+
export declare function brandWordmark(text?: string): string;
|
|
35
51
|
export declare function banner(subtitle: string): string;
|
|
52
|
+
export declare function gauge(params: {
|
|
53
|
+
value: number;
|
|
54
|
+
max: number;
|
|
55
|
+
width?: number;
|
|
56
|
+
color?: (text: string) => string;
|
|
57
|
+
}): string;
|
|
58
|
+
export declare function formatEta(seconds: number): string;
|
|
59
|
+
export declare function formatCount(n: number): string;
|
|
60
|
+
export declare function formatBytes(bytes: number): string;
|
|
61
|
+
export type StepState = "pending" | "active" | "done" | "failed" | "skipped";
|
|
62
|
+
export type RailStep = {
|
|
63
|
+
label: string;
|
|
64
|
+
state: StepState;
|
|
65
|
+
detail?: string;
|
|
66
|
+
};
|
|
67
|
+
export declare function renderStepRail(steps: RailStep[], spinnerFrame: string): string[];
|
|
36
68
|
export declare function createFramePainter(stdout: NodeJS.WriteStream): {
|
|
37
69
|
paint: (lines: string[]) => void;
|
|
38
70
|
clear: () => void;
|
|
71
|
+
commit: (lines: string[]) => void;
|
|
39
72
|
done: () => void;
|
|
40
73
|
};
|
|
41
74
|
export declare function keyHints(pairs: Array<[string, string]>): string;
|