theclawbay 0.2.13 → 0.3.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.
@@ -0,0 +1,23 @@
1
+ export type SessionProviderMigrationResult = {
2
+ scanned: number;
3
+ rewritten: number;
4
+ skipped: number;
5
+ retimed: number;
6
+ fastSkipped: boolean;
7
+ };
8
+ export type StateDbProviderMigrationResult = {
9
+ found: number;
10
+ updated: number;
11
+ failed: string[];
12
+ warning?: string;
13
+ };
14
+ export declare function migrateSessionProviders(params: {
15
+ codexHome: string;
16
+ migrationStateFile: string;
17
+ neutralizeSources: Set<string>;
18
+ }): Promise<SessionProviderMigrationResult>;
19
+ export declare function migrateStateDbProviders(params: {
20
+ codexHome: string;
21
+ targetProvider: string;
22
+ sourceProviders: string[];
23
+ }): Promise<StateDbProviderMigrationResult>;
@@ -0,0 +1,334 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.migrateSessionProviders = migrateSessionProviders;
7
+ exports.migrateStateDbProviders = migrateStateDbProviders;
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ const STATE_DB_FILE_PATTERN = /^state_\d+\.sqlite$/;
12
+ function resolvePythonCommand() {
13
+ const candidates = [
14
+ { command: "python3", args: ["-c", "import sqlite3"] },
15
+ { command: "python", args: ["-c", "import sqlite3"] },
16
+ { command: "py", args: ["-3", "-c", "import sqlite3"] },
17
+ ];
18
+ for (const candidate of candidates) {
19
+ const result = (0, node_child_process_1.spawnSync)(candidate.command, candidate.args, { stdio: "ignore" });
20
+ if (result.status === 0) {
21
+ return {
22
+ command: candidate.command,
23
+ args: candidate.args.slice(0, -2),
24
+ };
25
+ }
26
+ }
27
+ return null;
28
+ }
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
+ function inferRolloutUpdatedAt(source) {
40
+ const lines = source.split(/\r?\n/);
41
+ for (let i = lines.length - 1; i >= 0; i--) {
42
+ const trimmed = (lines[i] ?? "").trim();
43
+ if (!trimmed)
44
+ continue;
45
+ try {
46
+ const parsed = JSON.parse(trimmed);
47
+ if (typeof parsed.timestamp !== "string")
48
+ continue;
49
+ const ms = Date.parse(parsed.timestamp);
50
+ if (!Number.isFinite(ms))
51
+ continue;
52
+ return new Date(ms);
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ async function listSessionRollouts(root) {
61
+ const results = [];
62
+ const walk = async (dir) => {
63
+ let entries = [];
64
+ try {
65
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
66
+ }
67
+ catch (error) {
68
+ const err = error;
69
+ if (err.code === "ENOENT")
70
+ return;
71
+ throw error;
72
+ }
73
+ for (const entry of entries) {
74
+ const nextPath = node_path_1.default.join(dir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ await walk(nextPath);
77
+ continue;
78
+ }
79
+ if (entry.isFile() && nextPath.endsWith(".jsonl")) {
80
+ results.push(nextPath);
81
+ }
82
+ }
83
+ };
84
+ await walk(root);
85
+ return results;
86
+ }
87
+ async function readMigrationState(migrationStateFile) {
88
+ try {
89
+ const raw = await promises_1.default.readFile(migrationStateFile, "utf8");
90
+ const parsed = JSON.parse(raw);
91
+ if (parsed.version === 1 &&
92
+ typeof parsed.fileCount === "number" &&
93
+ Number.isFinite(parsed.fileCount) &&
94
+ typeof parsed.maxMtimeMs === "number" &&
95
+ Number.isFinite(parsed.maxMtimeMs)) {
96
+ return {
97
+ version: 1,
98
+ fileCount: Math.max(0, Math.floor(parsed.fileCount)),
99
+ maxMtimeMs: Math.max(0, Math.floor(parsed.maxMtimeMs)),
100
+ };
101
+ }
102
+ return null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ async function writeMigrationState(migrationStateFile, state) {
109
+ await promises_1.default.mkdir(node_path_1.default.dirname(migrationStateFile), { recursive: true });
110
+ await promises_1.default.writeFile(migrationStateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
111
+ }
112
+ async function computeSessionSnapshot(files) {
113
+ let maxMtimeMs = 0;
114
+ for (const file of files) {
115
+ try {
116
+ const stats = await promises_1.default.stat(file);
117
+ const mtimeMs = Math.floor(stats.mtimeMs);
118
+ if (mtimeMs > maxMtimeMs)
119
+ maxMtimeMs = mtimeMs;
120
+ }
121
+ catch {
122
+ // ignore transient missing file/race
123
+ }
124
+ }
125
+ return { fileCount: files.length, maxMtimeMs };
126
+ }
127
+ async function migrateSessionProviders(params) {
128
+ const sessionsRoot = node_path_1.default.join(params.codexHome, "sessions");
129
+ const files = await listSessionRollouts(sessionsRoot);
130
+ const snapshot = await computeSessionSnapshot(files);
131
+ const previousState = await readMigrationState(params.migrationStateFile);
132
+ if (previousState &&
133
+ previousState.fileCount === snapshot.fileCount &&
134
+ previousState.maxMtimeMs === snapshot.maxMtimeMs) {
135
+ return { scanned: 0, rewritten: 0, skipped: 0, retimed: 0, fastSkipped: true };
136
+ }
137
+ let rewritten = 0;
138
+ let skipped = 0;
139
+ let retimed = 0;
140
+ const useProgress = process.stdout.isTTY && files.length > 0;
141
+ const drawProgress = (current) => {
142
+ if (!useProgress)
143
+ return;
144
+ const bar = renderProgressBar(current, files.length);
145
+ process.stdout.write(`\rMigrating conversations ${current}/${files.length} ${bar}`);
146
+ if (current >= files.length)
147
+ process.stdout.write("\n");
148
+ };
149
+ drawProgress(0);
150
+ for (let i = 0; i < files.length; i++) {
151
+ const file = files[i];
152
+ let source = "";
153
+ let originalStats = null;
154
+ try {
155
+ source = await promises_1.default.readFile(file, "utf8");
156
+ originalStats = await promises_1.default.stat(file);
157
+ }
158
+ catch {
159
+ skipped++;
160
+ drawProgress(i + 1);
161
+ continue;
162
+ }
163
+ const desiredMtime = inferRolloutUpdatedAt(source) ?? originalStats.mtime;
164
+ const desiredAtime = originalStats.atime;
165
+ const maybeRetainMtime = async () => {
166
+ const diffMs = Math.abs(desiredMtime.getTime() - originalStats.mtime.getTime());
167
+ if (diffMs <= 1500)
168
+ return;
169
+ try {
170
+ await promises_1.default.utimes(file, desiredAtime, desiredMtime);
171
+ retimed++;
172
+ }
173
+ catch {
174
+ // best-effort only
175
+ }
176
+ };
177
+ if (!source.trim()) {
178
+ skipped++;
179
+ await maybeRetainMtime();
180
+ drawProgress(i + 1);
181
+ continue;
182
+ }
183
+ const lines = source.split(/\r?\n/);
184
+ const firstNonEmptyIndex = lines.findIndex((line) => line.trim().length > 0);
185
+ if (firstNonEmptyIndex < 0) {
186
+ skipped++;
187
+ drawProgress(i + 1);
188
+ continue;
189
+ }
190
+ let parsed;
191
+ try {
192
+ parsed = JSON.parse(lines[firstNonEmptyIndex] ?? "");
193
+ }
194
+ catch {
195
+ skipped++;
196
+ drawProgress(i + 1);
197
+ continue;
198
+ }
199
+ if ((parsed.type ?? "") !== "session_meta") {
200
+ skipped++;
201
+ drawProgress(i + 1);
202
+ continue;
203
+ }
204
+ const payload = parsed.payload;
205
+ if (!payload || typeof payload !== "object") {
206
+ skipped++;
207
+ drawProgress(i + 1);
208
+ continue;
209
+ }
210
+ const currentProvider = payload.model_provider;
211
+ if (typeof currentProvider !== "string") {
212
+ skipped++;
213
+ drawProgress(i + 1);
214
+ continue;
215
+ }
216
+ if (!params.neutralizeSources.has(currentProvider)) {
217
+ skipped++;
218
+ drawProgress(i + 1);
219
+ continue;
220
+ }
221
+ // Remove explicit provider tag so the same thread stays visible across provider contexts.
222
+ delete payload.model_provider;
223
+ lines[firstNonEmptyIndex] = JSON.stringify(parsed);
224
+ const next = lines.join("\n");
225
+ if (next === source) {
226
+ skipped++;
227
+ await maybeRetainMtime();
228
+ drawProgress(i + 1);
229
+ continue;
230
+ }
231
+ await promises_1.default.writeFile(file, next, "utf8");
232
+ await maybeRetainMtime();
233
+ rewritten++;
234
+ drawProgress(i + 1);
235
+ }
236
+ await writeMigrationState(params.migrationStateFile, {
237
+ version: 1,
238
+ fileCount: snapshot.fileCount,
239
+ maxMtimeMs: snapshot.maxMtimeMs,
240
+ });
241
+ return { scanned: files.length, rewritten, skipped, retimed, fastSkipped: false };
242
+ }
243
+ async function listStateDbFiles(codexHome) {
244
+ let entries = [];
245
+ try {
246
+ entries = await promises_1.default.readdir(codexHome, { withFileTypes: true });
247
+ }
248
+ catch (error) {
249
+ const err = error;
250
+ if (err.code === "ENOENT")
251
+ return [];
252
+ throw error;
253
+ }
254
+ return entries
255
+ .filter((entry) => entry.isFile() && STATE_DB_FILE_PATTERN.test(entry.name))
256
+ .map((entry) => node_path_1.default.join(codexHome, entry.name))
257
+ .sort();
258
+ }
259
+ async function migrateStateDbProviders(params) {
260
+ const dbPaths = await listStateDbFiles(params.codexHome);
261
+ if (dbPaths.length === 0) {
262
+ return { found: 0, updated: 0, failed: [] };
263
+ }
264
+ const python = resolvePythonCommand();
265
+ if (!python) {
266
+ return {
267
+ found: dbPaths.length,
268
+ updated: 0,
269
+ failed: dbPaths,
270
+ warning: "Python with sqlite3 support was not found.",
271
+ };
272
+ }
273
+ const script = `
274
+ import json
275
+ import sqlite3
276
+ import sys
277
+
278
+ db_paths = json.loads(sys.argv[1])
279
+ target_provider = sys.argv[2]
280
+ source_providers = json.loads(sys.argv[3])
281
+
282
+ result = {"updated": 0, "failed": []}
283
+
284
+ for db_path in db_paths:
285
+ try:
286
+ conn = sqlite3.connect(db_path, timeout=2.0)
287
+ conn.execute("PRAGMA busy_timeout = 2000")
288
+ table_exists = conn.execute(
289
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='threads' LIMIT 1"
290
+ ).fetchone()
291
+ if not table_exists:
292
+ conn.close()
293
+ continue
294
+
295
+ placeholders = ",".join("?" for _ in source_providers)
296
+ sql = f"UPDATE threads SET model_provider = ? WHERE model_provider IN ({placeholders})"
297
+ cursor = conn.execute(sql, [target_provider, *source_providers])
298
+ conn.commit()
299
+ result["updated"] += max(cursor.rowcount, 0)
300
+ conn.close()
301
+ except Exception:
302
+ result["failed"].append(db_path)
303
+
304
+ print(json.dumps(result))
305
+ `.trim();
306
+ const result = (0, node_child_process_1.spawnSync)(python.command, [...python.args, "-c", script, JSON.stringify(dbPaths), params.targetProvider, JSON.stringify(params.sourceProviders)], { encoding: "utf8" });
307
+ if (result.status !== 0) {
308
+ return {
309
+ found: dbPaths.length,
310
+ updated: 0,
311
+ failed: dbPaths,
312
+ warning: (result.stderr || result.stdout || "SQLite migration failed.").trim(),
313
+ };
314
+ }
315
+ try {
316
+ const parsed = JSON.parse(result.stdout || "{}");
317
+ const failed = Array.isArray(parsed.failed)
318
+ ? parsed.failed.filter((value) => typeof value === "string" && value.length > 0)
319
+ : [];
320
+ return {
321
+ found: dbPaths.length,
322
+ updated: typeof parsed.updated === "number" && Number.isFinite(parsed.updated) ? Math.max(0, Math.floor(parsed.updated)) : 0,
323
+ failed,
324
+ };
325
+ }
326
+ catch {
327
+ return {
328
+ found: dbPaths.length,
329
+ updated: 0,
330
+ failed: dbPaths,
331
+ warning: "SQLite migration returned unreadable output.",
332
+ };
333
+ }
334
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.2.13",
4
- "description": "The Claw Bay CLI: one-time API-key setup for direct Codex access, with optional relay fallback.",
3
+ "version": "0.3.2",
4
+ "description": "The Claw Bay CLI: one-time API-key setup for direct Codex access.",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "theclawbay": "dist/index.js"
@@ -14,6 +14,7 @@
14
14
  "release:patch": "./scripts/release-npm.sh patch",
15
15
  "release:minor": "./scripts/release-npm.sh minor",
16
16
  "release:major": "./scripts/release-npm.sh major",
17
+ "release:publish": "./scripts/release-npm.sh none",
17
18
  "release:dry-run": "./scripts/release-npm.sh patch --dry-run"
18
19
  },
19
20
  "engines": {
@@ -30,9 +31,7 @@
30
31
  "cli",
31
32
  "setup",
32
33
  "wan",
33
- "proxy",
34
- "api-key",
35
- "relay"
34
+ "api-key"
36
35
  ],
37
36
  "preferGlobal": true,
38
37
  "dependencies": {
@@ -1,13 +0,0 @@
1
- import { BaseCommand } from "../lib/base-command";
2
- export default class ProxyCommand extends BaseCommand {
3
- static description: string;
4
- static flags: {
5
- host: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
6
- port: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
7
- backend: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
- "api-key": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
9
- "base-path": import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
10
- client: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
11
- };
12
- run(): Promise<void>;
13
- }