@hasna/brains 0.0.13 → 0.0.14
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/cli/commands/cloud.d.ts +3 -0
- package/dist/cli/commands/cloud.d.ts.map +1 -0
- package/dist/cli/commands/collections.d.ts +3 -0
- package/dist/cli/commands/collections.d.ts.map +1 -0
- package/dist/cli/commands/data.d.ts +3 -0
- package/dist/cli/commands/data.d.ts.map +1 -0
- package/dist/cli/commands/finetune.d.ts +3 -0
- package/dist/cli/commands/finetune.d.ts.map +1 -0
- package/dist/cli/commands/models.d.ts +3 -0
- package/dist/cli/commands/models.d.ts.map +1 -0
- package/dist/cli/index.js +778 -753
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -13434,7 +13434,7 @@ __export(exports_sessions2, {
|
|
|
13434
13434
|
gatherFromSessions: () => gatherFromSessions
|
|
13435
13435
|
});
|
|
13436
13436
|
import { readdir, readFile, stat } from "fs/promises";
|
|
13437
|
-
import { existsSync as
|
|
13437
|
+
import { existsSync as existsSync10 } from "fs";
|
|
13438
13438
|
import { join as join12 } from "path";
|
|
13439
13439
|
import { homedir as homedir11 } from "os";
|
|
13440
13440
|
function extractText(content) {
|
|
@@ -13447,7 +13447,7 @@ async function gatherFromSessions(options = {}) {
|
|
|
13447
13447
|
const { limit: limit2 = 1000 } = options;
|
|
13448
13448
|
const examples = [];
|
|
13449
13449
|
const claudeDir = join12(homedir11(), ".claude", "projects");
|
|
13450
|
-
if (!
|
|
13450
|
+
if (!existsSync10(claudeDir)) {
|
|
13451
13451
|
return { source: "sessions", examples: [], count: 0 };
|
|
13452
13452
|
}
|
|
13453
13453
|
const projectDirs = await readdir(claudeDir).catch(() => []);
|
|
@@ -14976,10 +14976,8 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
|
|
|
14976
14976
|
}
|
|
14977
14977
|
|
|
14978
14978
|
// src/cli/index.ts
|
|
14979
|
-
import {
|
|
14980
|
-
import {
|
|
14981
|
-
import { join as join13 } from "path";
|
|
14982
|
-
import { homedir as homedir12 } from "os";
|
|
14979
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
14980
|
+
import { join as join14 } from "path";
|
|
14983
14981
|
|
|
14984
14982
|
// node_modules/drizzle-orm/bun-sqlite/driver.js
|
|
14985
14983
|
import { Database } from "bun:sqlite";
|
|
@@ -17402,6 +17400,57 @@ function getRawDb(dbPath) {
|
|
|
17402
17400
|
return dbPath ? new SqliteAdapter(dbPath) : createDatabase({ service: "brains" });
|
|
17403
17401
|
}
|
|
17404
17402
|
|
|
17403
|
+
// src/cli/ui.ts
|
|
17404
|
+
import chalk from "chalk";
|
|
17405
|
+
function printTable(headers, rows) {
|
|
17406
|
+
if (rows.length === 0) {
|
|
17407
|
+
console.log(chalk.dim(" (no records)"));
|
|
17408
|
+
return;
|
|
17409
|
+
}
|
|
17410
|
+
const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
17411
|
+
const separator = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
17412
|
+
const headerLine = headers.map((h, i) => ` ${chalk.bold(h.padEnd(colWidths[i] ?? 0))} `).join("\u2502");
|
|
17413
|
+
const topBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C");
|
|
17414
|
+
const midBorder = separator;
|
|
17415
|
+
const bottomBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534");
|
|
17416
|
+
console.log("\u250C" + topBorder + "\u2510");
|
|
17417
|
+
console.log("\u2502" + headerLine + "\u2502");
|
|
17418
|
+
console.log("\u251C" + midBorder + "\u2524");
|
|
17419
|
+
for (const row of rows) {
|
|
17420
|
+
const rowLine = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i] ?? 0)} `).join("\u2502");
|
|
17421
|
+
console.log("\u2502" + rowLine + "\u2502");
|
|
17422
|
+
}
|
|
17423
|
+
console.log("\u2514" + bottomBorder + "\u2518");
|
|
17424
|
+
}
|
|
17425
|
+
var STATUS_COLORS = {
|
|
17426
|
+
succeeded: chalk.green,
|
|
17427
|
+
running: chalk.cyan,
|
|
17428
|
+
pending: chalk.yellow,
|
|
17429
|
+
failed: chalk.red,
|
|
17430
|
+
cancelled: chalk.dim,
|
|
17431
|
+
queued: chalk.yellow,
|
|
17432
|
+
validating_files: chalk.blue
|
|
17433
|
+
};
|
|
17434
|
+
function printStatus(status) {
|
|
17435
|
+
const colorFn = STATUS_COLORS[status] ?? chalk.white;
|
|
17436
|
+
return colorFn(`\u25CF ${status}`);
|
|
17437
|
+
}
|
|
17438
|
+
function printJson(obj) {
|
|
17439
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
17440
|
+
}
|
|
17441
|
+
function printError(message) {
|
|
17442
|
+
console.error(chalk.red("\u2717 Error: ") + message);
|
|
17443
|
+
}
|
|
17444
|
+
function printSuccess(message) {
|
|
17445
|
+
console.log(chalk.green("\u2713 ") + message);
|
|
17446
|
+
}
|
|
17447
|
+
function printInfo(message) {
|
|
17448
|
+
console.log(chalk.dim(" " + message));
|
|
17449
|
+
}
|
|
17450
|
+
|
|
17451
|
+
// src/cli/commands/models.ts
|
|
17452
|
+
import { randomUUID } from "crypto";
|
|
17453
|
+
|
|
17405
17454
|
// node_modules/openai/internal/qs/formats.mjs
|
|
17406
17455
|
var default_format = "RFC3986";
|
|
17407
17456
|
var formatters = {
|
|
@@ -23149,345 +23198,137 @@ class ThinkerLabsProvider {
|
|
|
23149
23198
|
}
|
|
23150
23199
|
}
|
|
23151
23200
|
|
|
23152
|
-
// src/cli/
|
|
23153
|
-
|
|
23154
|
-
|
|
23155
|
-
|
|
23156
|
-
|
|
23157
|
-
|
|
23158
|
-
|
|
23159
|
-
const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
23160
|
-
const separator = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
23161
|
-
const headerLine = headers.map((h, i) => ` ${chalk.bold(h.padEnd(colWidths[i] ?? 0))} `).join("\u2502");
|
|
23162
|
-
const topBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C");
|
|
23163
|
-
const midBorder = separator;
|
|
23164
|
-
const bottomBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534");
|
|
23165
|
-
console.log("\u250C" + topBorder + "\u2510");
|
|
23166
|
-
console.log("\u2502" + headerLine + "\u2502");
|
|
23167
|
-
console.log("\u251C" + midBorder + "\u2524");
|
|
23168
|
-
for (const row of rows) {
|
|
23169
|
-
const rowLine = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i] ?? 0)} `).join("\u2502");
|
|
23170
|
-
console.log("\u2502" + rowLine + "\u2502");
|
|
23171
|
-
}
|
|
23172
|
-
console.log("\u2514" + bottomBorder + "\u2518");
|
|
23173
|
-
}
|
|
23174
|
-
var STATUS_COLORS = {
|
|
23175
|
-
succeeded: chalk.green,
|
|
23176
|
-
running: chalk.cyan,
|
|
23177
|
-
pending: chalk.yellow,
|
|
23178
|
-
failed: chalk.red,
|
|
23179
|
-
cancelled: chalk.dim,
|
|
23180
|
-
queued: chalk.yellow,
|
|
23181
|
-
validating_files: chalk.blue
|
|
23182
|
-
};
|
|
23183
|
-
function printStatus(status) {
|
|
23184
|
-
const colorFn = STATUS_COLORS[status] ?? chalk.white;
|
|
23185
|
-
return colorFn(`\u25CF ${status}`);
|
|
23186
|
-
}
|
|
23187
|
-
function printJson(obj) {
|
|
23188
|
-
console.log(JSON.stringify(obj, null, 2));
|
|
23189
|
-
}
|
|
23190
|
-
function printError(message) {
|
|
23191
|
-
console.error(chalk.red("\u2717 Error: ") + message);
|
|
23192
|
-
}
|
|
23193
|
-
function printSuccess(message) {
|
|
23194
|
-
console.log(chalk.green("\u2713 ") + message);
|
|
23195
|
-
}
|
|
23196
|
-
function printInfo(message) {
|
|
23197
|
-
console.log(chalk.dim(" " + message));
|
|
23198
|
-
}
|
|
23199
|
-
|
|
23200
|
-
// src/cli/index.ts
|
|
23201
|
-
var program2 = new Command;
|
|
23202
|
-
program2.name("brains").description("Fine-tuned model tracker and trainer").version("0.0.1");
|
|
23203
|
-
var modelsCmd = program2.command("models").description("Manage tracked fine-tuned models");
|
|
23204
|
-
modelsCmd.command("list").description("List all tracked fine-tuned models").option("--json", "Output as JSON").action(async (opts) => {
|
|
23205
|
-
try {
|
|
23206
|
-
const db = getDb();
|
|
23207
|
-
const models = await db.select().from(fineTunedModels);
|
|
23208
|
-
if (opts.json) {
|
|
23209
|
-
printJson(models);
|
|
23210
|
-
return;
|
|
23211
|
-
}
|
|
23212
|
-
if (models.length === 0) {
|
|
23213
|
-
printInfo("No models tracked yet. Use 'brains finetune start' to train one.");
|
|
23214
|
-
return;
|
|
23215
|
-
}
|
|
23216
|
-
printTable(["ID", "Display Name", "Provider", "Status", "Collection", "Base Model"], models.map((m) => [
|
|
23217
|
-
m.id,
|
|
23218
|
-
m.displayName ?? m.name,
|
|
23219
|
-
m.provider,
|
|
23220
|
-
printStatus(m.status),
|
|
23221
|
-
m.collection ?? "",
|
|
23222
|
-
m.baseModel
|
|
23223
|
-
]));
|
|
23224
|
-
} catch (err) {
|
|
23225
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23226
|
-
process.exit(1);
|
|
23227
|
-
}
|
|
23228
|
-
});
|
|
23229
|
-
modelsCmd.command("show <id>").description("Show details of a specific model").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
23230
|
-
try {
|
|
23231
|
-
const db = getDb();
|
|
23232
|
-
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
|
|
23233
|
-
if (!model) {
|
|
23201
|
+
// src/cli/commands/models.ts
|
|
23202
|
+
function registerModelsCommands(program2) {
|
|
23203
|
+
const modelsCmd = program2.command("models").description("Manage tracked fine-tuned models");
|
|
23204
|
+
modelsCmd.command("list").description("List all tracked fine-tuned models").option("--json", "Output as JSON").action(async (opts) => {
|
|
23205
|
+
try {
|
|
23206
|
+
const db = getDb();
|
|
23207
|
+
const models = await db.select().from(fineTunedModels);
|
|
23234
23208
|
if (opts.json) {
|
|
23235
|
-
printJson(
|
|
23236
|
-
|
|
23237
|
-
|
|
23209
|
+
printJson(models);
|
|
23210
|
+
return;
|
|
23211
|
+
}
|
|
23212
|
+
if (models.length === 0) {
|
|
23213
|
+
printInfo("No models tracked yet. Use 'brains finetune start' to train one.");
|
|
23214
|
+
return;
|
|
23238
23215
|
}
|
|
23216
|
+
printTable(["ID", "Display Name", "Provider", "Status", "Collection", "Base Model"], models.map((m) => [
|
|
23217
|
+
m.id,
|
|
23218
|
+
m.displayName ?? m.name,
|
|
23219
|
+
m.provider,
|
|
23220
|
+
printStatus(m.status),
|
|
23221
|
+
m.collection ?? "",
|
|
23222
|
+
m.baseModel
|
|
23223
|
+
]));
|
|
23224
|
+
} catch (err) {
|
|
23225
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23239
23226
|
process.exit(1);
|
|
23240
23227
|
}
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
}
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
}
|
|
23264
|
-
});
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
modelsCmd.command("describe <id> <description>").description("Set the description of a model").action(async (id, description) => {
|
|
23276
|
-
try {
|
|
23277
|
-
const db = getDb();
|
|
23278
|
-
await db.update(fineTunedModels).set({ description, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23279
|
-
printSuccess(`Description updated.`);
|
|
23280
|
-
} catch (err) {
|
|
23281
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23282
|
-
process.exit(1);
|
|
23283
|
-
}
|
|
23284
|
-
});
|
|
23285
|
-
modelsCmd.command("tag <id> <tag>").description("Add a tag to a model").action(async (id, tag) => {
|
|
23286
|
-
try {
|
|
23287
|
-
const db = getDb();
|
|
23288
|
-
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
|
|
23289
|
-
if (!model) {
|
|
23290
|
-
printError(`Model not found: ${id}`);
|
|
23228
|
+
});
|
|
23229
|
+
modelsCmd.command("show <id>").description("Show details of a specific model").option("--json", "Output as JSON").action(async (id, opts) => {
|
|
23230
|
+
try {
|
|
23231
|
+
const db = getDb();
|
|
23232
|
+
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
|
|
23233
|
+
if (!model) {
|
|
23234
|
+
if (opts.json) {
|
|
23235
|
+
printJson({ error: `Model not found: ${id}` });
|
|
23236
|
+
} else {
|
|
23237
|
+
printError(`Model not found: ${id}`);
|
|
23238
|
+
}
|
|
23239
|
+
process.exit(1);
|
|
23240
|
+
}
|
|
23241
|
+
if (opts.json) {
|
|
23242
|
+
printJson(model);
|
|
23243
|
+
return;
|
|
23244
|
+
}
|
|
23245
|
+
console.log();
|
|
23246
|
+
const tagsList = model.tags ? JSON.parse(model.tags).join(", ") : "(none)";
|
|
23247
|
+
console.log(` ID: ${model.id}`);
|
|
23248
|
+
console.log(` Name: ${model.name}`);
|
|
23249
|
+
console.log(` Display Name: ${model.displayName ?? "(none)"}`);
|
|
23250
|
+
console.log(` Description: ${model.description ?? "(none)"}`);
|
|
23251
|
+
console.log(` Collection: ${model.collection ?? "(none)"}`);
|
|
23252
|
+
console.log(` Tags: ${tagsList}`);
|
|
23253
|
+
console.log(` Provider: ${model.provider}`);
|
|
23254
|
+
console.log(` Status: ${printStatus(model.status)}`);
|
|
23255
|
+
console.log(` Base Model: ${model.baseModel}`);
|
|
23256
|
+
console.log(` Job ID: ${model.fineTuneJobId ?? "(none)"}`);
|
|
23257
|
+
console.log(` Created: ${new Date(model.createdAt).toISOString()}`);
|
|
23258
|
+
console.log(` Updated: ${new Date(model.updatedAt).toISOString()}`);
|
|
23259
|
+
console.log();
|
|
23260
|
+
} catch (err) {
|
|
23261
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23291
23262
|
process.exit(1);
|
|
23292
23263
|
}
|
|
23293
|
-
|
|
23294
|
-
|
|
23295
|
-
|
|
23296
|
-
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
|
|
23300
|
-
|
|
23301
|
-
process.exit(1);
|
|
23302
|
-
}
|
|
23303
|
-
});
|
|
23304
|
-
modelsCmd.command("untag <id> <tag>").description("Remove a tag from a model").action(async (id, tag) => {
|
|
23305
|
-
try {
|
|
23306
|
-
const db = getDb();
|
|
23307
|
-
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
|
|
23308
|
-
if (!model) {
|
|
23309
|
-
printError(`Model not found: ${id}`);
|
|
23264
|
+
});
|
|
23265
|
+
modelsCmd.command("rename <id> <displayName>").description("Set the display name of a model").action(async (id, displayName) => {
|
|
23266
|
+
try {
|
|
23267
|
+
const db = getDb();
|
|
23268
|
+
await db.update(fineTunedModels).set({ displayName, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23269
|
+
printSuccess(`Display name set to "${displayName}"`);
|
|
23270
|
+
} catch (err) {
|
|
23271
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23310
23272
|
process.exit(1);
|
|
23311
23273
|
}
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
|
|
23316
|
-
|
|
23317
|
-
|
|
23318
|
-
|
|
23319
|
-
|
|
23320
|
-
});
|
|
23321
|
-
modelsCmd.command("collection <id> <collectionName>").description("Set the collection of a model").action(async (id, collectionName) => {
|
|
23322
|
-
try {
|
|
23323
|
-
const db = getDb();
|
|
23324
|
-
await db.update(fineTunedModels).set({ collection: collectionName, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23325
|
-
printSuccess(`Collection set to "${collectionName}"`);
|
|
23326
|
-
} catch (err) {
|
|
23327
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23328
|
-
process.exit(1);
|
|
23329
|
-
}
|
|
23330
|
-
});
|
|
23331
|
-
modelsCmd.command("import <job-id>").description("Import an externally created fine-tuned model into local tracking").option("--provider <provider>", "Provider (openai|thinker-labs)", "openai").option("--name <name>", "Display name for the model").action(async (jobId, opts) => {
|
|
23332
|
-
try {
|
|
23333
|
-
let result;
|
|
23334
|
-
if (opts.provider === "openai") {
|
|
23335
|
-
result = await getFineTuneStatus(jobId);
|
|
23336
|
-
} else {
|
|
23337
|
-
const tl = new ThinkerLabsProvider;
|
|
23338
|
-
result = await tl.getFineTuneStatus(jobId);
|
|
23339
|
-
}
|
|
23340
|
-
const db = getDb();
|
|
23341
|
-
const [existing] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
|
|
23342
|
-
if (existing) {
|
|
23343
|
-
printInfo(`Model already tracked as: ${existing.id}`);
|
|
23344
|
-
return;
|
|
23345
|
-
}
|
|
23346
|
-
const modelId = randomUUID();
|
|
23347
|
-
const now = Date.now();
|
|
23348
|
-
const name = opts.name ?? result.fineTunedModel ?? `imported-${jobId}`;
|
|
23349
|
-
await db.insert(fineTunedModels).values({
|
|
23350
|
-
id: modelId,
|
|
23351
|
-
name,
|
|
23352
|
-
provider: opts.provider,
|
|
23353
|
-
baseModel: result.baseModel ?? "unknown",
|
|
23354
|
-
status: result.status,
|
|
23355
|
-
fineTuneJobId: jobId,
|
|
23356
|
-
createdAt: now,
|
|
23357
|
-
updatedAt: now
|
|
23358
|
-
});
|
|
23359
|
-
await db.insert(trainingJobs).values({
|
|
23360
|
-
id: randomUUID(),
|
|
23361
|
-
modelId,
|
|
23362
|
-
provider: opts.provider,
|
|
23363
|
-
status: result.status,
|
|
23364
|
-
startedAt: now
|
|
23365
|
-
});
|
|
23366
|
-
printSuccess(`Model imported successfully.`);
|
|
23367
|
-
console.log();
|
|
23368
|
-
console.log(` Local ID: ${modelId}`);
|
|
23369
|
-
console.log(` Job ID: ${jobId}`);
|
|
23370
|
-
console.log(` Name: ${name}`);
|
|
23371
|
-
console.log(` Status: ${printStatus(result.status)}`);
|
|
23372
|
-
if (result.fineTunedModel)
|
|
23373
|
-
console.log(` Model: ${result.fineTunedModel}`);
|
|
23374
|
-
console.log();
|
|
23375
|
-
} catch (err) {
|
|
23376
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23377
|
-
process.exit(1);
|
|
23378
|
-
}
|
|
23379
|
-
});
|
|
23380
|
-
var finetuneCmd = program2.command("finetune").description("Manage fine-tuning jobs");
|
|
23381
|
-
finetuneCmd.command("start").description("Start a fine-tuning job").requiredOption("--provider <provider>", "Provider to use (openai|thinker-labs)").requiredOption("--base-model <model>", "Base model to fine-tune (e.g. gpt-4o-mini-2024-07-18)").option("--dataset <path>", "Path to the JSONL training dataset (auto-detects latest if omitted)").requiredOption("--name <name>", "Human-readable name for this fine-tuned model").action(async (opts) => {
|
|
23382
|
-
try {
|
|
23383
|
-
if (opts.provider !== "openai" && opts.provider !== "thinker-labs") {
|
|
23384
|
-
printError(`Unknown provider: ${opts.provider}. Use 'openai' or 'thinker-labs'.`);
|
|
23274
|
+
});
|
|
23275
|
+
modelsCmd.command("describe <id> <description>").description("Set the description of a model").action(async (id, description) => {
|
|
23276
|
+
try {
|
|
23277
|
+
const db = getDb();
|
|
23278
|
+
await db.update(fineTunedModels).set({ description, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23279
|
+
printSuccess(`Description updated.`);
|
|
23280
|
+
} catch (err) {
|
|
23281
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23385
23282
|
process.exit(1);
|
|
23386
23283
|
}
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
const
|
|
23391
|
-
|
|
23392
|
-
|
|
23284
|
+
});
|
|
23285
|
+
modelsCmd.command("tag <id> <tag>").description("Add a tag to a model").action(async (id, tag) => {
|
|
23286
|
+
try {
|
|
23287
|
+
const db = getDb();
|
|
23288
|
+
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
|
|
23289
|
+
if (!model) {
|
|
23290
|
+
printError(`Model not found: ${id}`);
|
|
23393
23291
|
process.exit(1);
|
|
23394
23292
|
}
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23293
|
+
const existing = model.tags ? JSON.parse(model.tags) : [];
|
|
23294
|
+
if (!existing.includes(tag)) {
|
|
23295
|
+
existing.push(tag);
|
|
23296
|
+
}
|
|
23297
|
+
await db.update(fineTunedModels).set({ tags: JSON.stringify(existing), updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23298
|
+
printSuccess(`Tag "${tag}" added. Tags: ${existing.join(", ")}`);
|
|
23299
|
+
} catch (err) {
|
|
23300
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23400
23301
|
process.exit(1);
|
|
23401
23302
|
}
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
({ jobId, status: jobStatus } = await createFineTuneJob(fileId, opts.baseModel, opts.name));
|
|
23411
|
-
} else {
|
|
23412
|
-
const tl = new ThinkerLabsProvider;
|
|
23413
|
-
({ fileId } = await tl.uploadTrainingFile(datasetPath));
|
|
23414
|
-
printSuccess(`File uploaded. fileId = ${fileId}`);
|
|
23415
|
-
printInfo(`Creating fine-tune job on Thinker Labs \u2026`);
|
|
23416
|
-
({ jobId, status: jobStatus } = await tl.createFineTuneJob(fileId, opts.baseModel, opts.name));
|
|
23417
|
-
}
|
|
23418
|
-
const db = getDb();
|
|
23419
|
-
const modelId = randomUUID();
|
|
23420
|
-
const now = Date.now();
|
|
23421
|
-
await db.insert(fineTunedModels).values({
|
|
23422
|
-
id: modelId,
|
|
23423
|
-
name: opts.name,
|
|
23424
|
-
provider: opts.provider,
|
|
23425
|
-
baseModel: opts.baseModel,
|
|
23426
|
-
status: "running",
|
|
23427
|
-
fineTuneJobId: jobId,
|
|
23428
|
-
createdAt: now,
|
|
23429
|
-
updatedAt: now
|
|
23430
|
-
});
|
|
23431
|
-
const trainingJobId = randomUUID();
|
|
23432
|
-
await db.insert(trainingJobs).values({
|
|
23433
|
-
id: trainingJobId,
|
|
23434
|
-
modelId,
|
|
23435
|
-
provider: opts.provider,
|
|
23436
|
-
status: jobStatus,
|
|
23437
|
-
startedAt: now
|
|
23438
|
-
});
|
|
23439
|
-
printSuccess(`Fine-tune job started!`);
|
|
23440
|
-
console.log();
|
|
23441
|
-
console.log(` Model ID: ${modelId}`);
|
|
23442
|
-
console.log(` Job ID: ${jobId}`);
|
|
23443
|
-
console.log(` Status: ${printStatus(jobStatus)}`);
|
|
23444
|
-
console.log();
|
|
23445
|
-
printInfo(`Use 'brains finetune status ${jobId}' to check progress.`);
|
|
23446
|
-
} catch (err) {
|
|
23447
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23448
|
-
process.exit(1);
|
|
23449
|
-
}
|
|
23450
|
-
});
|
|
23451
|
-
finetuneCmd.command("status <job-id>").description("Get the status of a fine-tuning job").option("--provider <provider>", "Provider (openai|thinker-labs)", "openai").option("--json", "Output as JSON").action(async (jobId, opts) => {
|
|
23452
|
-
try {
|
|
23453
|
-
let result;
|
|
23454
|
-
if (opts.provider === "openai") {
|
|
23455
|
-
result = await getFineTuneStatus(jobId);
|
|
23456
|
-
} else {
|
|
23457
|
-
const tl = new ThinkerLabsProvider;
|
|
23458
|
-
result = await tl.getFineTuneStatus(jobId);
|
|
23459
|
-
}
|
|
23460
|
-
if (opts.json) {
|
|
23461
|
-
printJson(result);
|
|
23462
|
-
} else {
|
|
23463
|
-
console.log();
|
|
23464
|
-
console.log(` Job ID: ${result.jobId}`);
|
|
23465
|
-
console.log(` Status: ${printStatus(result.status)}`);
|
|
23466
|
-
if (result.fineTunedModel) {
|
|
23467
|
-
console.log(` Fine-tuned model: ${result.fineTunedModel}`);
|
|
23468
|
-
}
|
|
23469
|
-
if (result.error) {
|
|
23470
|
-
console.log(` Error: ${result.error}`);
|
|
23303
|
+
});
|
|
23304
|
+
modelsCmd.command("untag <id> <tag>").description("Remove a tag from a model").action(async (id, tag) => {
|
|
23305
|
+
try {
|
|
23306
|
+
const db = getDb();
|
|
23307
|
+
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
|
|
23308
|
+
if (!model) {
|
|
23309
|
+
printError(`Model not found: ${id}`);
|
|
23310
|
+
process.exit(1);
|
|
23471
23311
|
}
|
|
23472
|
-
|
|
23473
|
-
|
|
23474
|
-
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
|
|
23478
|
-
|
|
23312
|
+
const existing = model.tags ? JSON.parse(model.tags) : [];
|
|
23313
|
+
const updated = existing.filter((t) => t !== tag);
|
|
23314
|
+
await db.update(fineTunedModels).set({ tags: JSON.stringify(updated), updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23315
|
+
printSuccess(`Tag "${tag}" removed. Tags: ${updated.join(", ") || "(none)"}`);
|
|
23316
|
+
} catch (err) {
|
|
23317
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23318
|
+
process.exit(1);
|
|
23479
23319
|
}
|
|
23480
|
-
}
|
|
23481
|
-
|
|
23482
|
-
|
|
23483
|
-
|
|
23484
|
-
});
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
23489
|
-
|
|
23490
|
-
|
|
23320
|
+
});
|
|
23321
|
+
modelsCmd.command("collection <id> <collectionName>").description("Set the collection of a model").action(async (id, collectionName) => {
|
|
23322
|
+
try {
|
|
23323
|
+
const db = getDb();
|
|
23324
|
+
await db.update(fineTunedModels).set({ collection: collectionName, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
|
|
23325
|
+
printSuccess(`Collection set to "${collectionName}"`);
|
|
23326
|
+
} catch (err) {
|
|
23327
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23328
|
+
process.exit(1);
|
|
23329
|
+
}
|
|
23330
|
+
});
|
|
23331
|
+
modelsCmd.command("import <job-id>").description("Import an externally created fine-tuned model into local tracking").option("--provider <provider>", "Provider (openai|thinker-labs)", "openai").option("--name <name>", "Display name for the model").action(async (jobId, opts) => {
|
|
23491
23332
|
try {
|
|
23492
23333
|
let result;
|
|
23493
23334
|
if (opts.provider === "openai") {
|
|
@@ -23496,255 +23337,425 @@ finetuneCmd.command("watch <job-id>").description("Poll a fine-tuning job until
|
|
|
23496
23337
|
const tl = new ThinkerLabsProvider;
|
|
23497
23338
|
result = await tl.getFineTuneStatus(jobId);
|
|
23498
23339
|
}
|
|
23499
|
-
const
|
|
23500
|
-
|
|
23340
|
+
const db = getDb();
|
|
23341
|
+
const [existing] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
|
|
23342
|
+
if (existing) {
|
|
23343
|
+
printInfo(`Model already tracked as: ${existing.id}`);
|
|
23344
|
+
return;
|
|
23345
|
+
}
|
|
23346
|
+
const modelId = randomUUID();
|
|
23347
|
+
const now = Date.now();
|
|
23348
|
+
const name = opts.name ?? result.fineTunedModel ?? `imported-${jobId}`;
|
|
23349
|
+
await db.insert(fineTunedModels).values({
|
|
23350
|
+
id: modelId,
|
|
23351
|
+
name,
|
|
23352
|
+
provider: opts.provider,
|
|
23353
|
+
baseModel: result.baseModel ?? "unknown",
|
|
23354
|
+
status: result.status,
|
|
23355
|
+
fineTuneJobId: jobId,
|
|
23356
|
+
createdAt: now,
|
|
23357
|
+
updatedAt: now
|
|
23358
|
+
});
|
|
23359
|
+
await db.insert(trainingJobs).values({
|
|
23360
|
+
id: randomUUID(),
|
|
23361
|
+
modelId,
|
|
23362
|
+
provider: opts.provider,
|
|
23363
|
+
status: result.status,
|
|
23364
|
+
startedAt: now
|
|
23365
|
+
});
|
|
23366
|
+
printSuccess(`Model imported successfully.`);
|
|
23367
|
+
console.log();
|
|
23368
|
+
console.log(` Local ID: ${modelId}`);
|
|
23369
|
+
console.log(` Job ID: ${jobId}`);
|
|
23370
|
+
console.log(` Name: ${name}`);
|
|
23371
|
+
console.log(` Status: ${printStatus(result.status)}`);
|
|
23501
23372
|
if (result.fineTunedModel)
|
|
23502
|
-
|
|
23503
|
-
|
|
23504
|
-
|
|
23373
|
+
console.log(` Model: ${result.fineTunedModel}`);
|
|
23374
|
+
console.log();
|
|
23375
|
+
} catch (err) {
|
|
23376
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23377
|
+
process.exit(1);
|
|
23378
|
+
}
|
|
23379
|
+
});
|
|
23380
|
+
}
|
|
23381
|
+
|
|
23382
|
+
// src/cli/commands/finetune.ts
|
|
23383
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
23384
|
+
import { existsSync as existsSync9 } from "fs";
|
|
23385
|
+
function registerFinetuneCommands(program2) {
|
|
23386
|
+
const finetuneCmd = program2.command("finetune").description("Manage fine-tuning jobs");
|
|
23387
|
+
finetuneCmd.command("start").description("Start a fine-tuning job").requiredOption("--provider <provider>", "Provider to use (openai|thinker-labs)").requiredOption("--base-model <model>", "Base model to fine-tune (e.g. gpt-4o-mini-2024-07-18)").option("--dataset <path>", "Path to the JSONL training dataset (auto-detects latest if omitted)").requiredOption("--name <name>", "Human-readable name for this fine-tuned model").action(async (opts) => {
|
|
23388
|
+
try {
|
|
23389
|
+
if (opts.provider !== "openai" && opts.provider !== "thinker-labs") {
|
|
23390
|
+
printError(`Unknown provider: ${opts.provider}. Use 'openai' or 'thinker-labs'.`);
|
|
23391
|
+
process.exit(1);
|
|
23392
|
+
}
|
|
23393
|
+
let datasetPath = opts.dataset;
|
|
23394
|
+
if (!datasetPath) {
|
|
23395
|
+
const db2 = getDb();
|
|
23396
|
+
const [latest] = await db2.select().from(trainingDatasets).orderBy(desc(trainingDatasets.createdAt)).limit(1);
|
|
23397
|
+
if (!latest?.filePath) {
|
|
23398
|
+
printError("No datasets found. Run 'brains data gather' first.");
|
|
23399
|
+
process.exit(1);
|
|
23400
|
+
}
|
|
23401
|
+
datasetPath = latest.filePath;
|
|
23402
|
+
printInfo(`Using latest dataset: ${datasetPath} (${latest.exampleCount} examples)`);
|
|
23403
|
+
}
|
|
23404
|
+
if (!existsSync9(datasetPath)) {
|
|
23405
|
+
printError(`Dataset file not found: ${datasetPath}`);
|
|
23406
|
+
process.exit(1);
|
|
23407
|
+
}
|
|
23408
|
+
printInfo(`Uploading training file: ${datasetPath} \u2026`);
|
|
23409
|
+
let fileId;
|
|
23410
|
+
let jobId;
|
|
23411
|
+
let jobStatus;
|
|
23412
|
+
if (opts.provider === "openai") {
|
|
23413
|
+
({ fileId } = await uploadTrainingFile(datasetPath));
|
|
23414
|
+
printSuccess(`File uploaded. fileId = ${fileId}`);
|
|
23415
|
+
printInfo(`Creating fine-tune job on OpenAI \u2026`);
|
|
23416
|
+
({ jobId, status: jobStatus } = await createFineTuneJob(fileId, opts.baseModel, opts.name));
|
|
23417
|
+
} else {
|
|
23418
|
+
const tl = new ThinkerLabsProvider;
|
|
23419
|
+
({ fileId } = await tl.uploadTrainingFile(datasetPath));
|
|
23420
|
+
printSuccess(`File uploaded. fileId = ${fileId}`);
|
|
23421
|
+
printInfo(`Creating fine-tune job on Thinker Labs \u2026`);
|
|
23422
|
+
({ jobId, status: jobStatus } = await tl.createFineTuneJob(fileId, opts.baseModel, opts.name));
|
|
23423
|
+
}
|
|
23424
|
+
const db = getDb();
|
|
23425
|
+
const modelId = randomUUID2();
|
|
23426
|
+
const now = Date.now();
|
|
23427
|
+
await db.insert(fineTunedModels).values({
|
|
23428
|
+
id: modelId,
|
|
23429
|
+
name: opts.name,
|
|
23430
|
+
provider: opts.provider,
|
|
23431
|
+
baseModel: opts.baseModel,
|
|
23432
|
+
status: "running",
|
|
23433
|
+
fineTuneJobId: jobId,
|
|
23434
|
+
createdAt: now,
|
|
23435
|
+
updatedAt: now
|
|
23436
|
+
});
|
|
23437
|
+
const trainingJobId = randomUUID2();
|
|
23438
|
+
await db.insert(trainingJobs).values({
|
|
23439
|
+
id: trainingJobId,
|
|
23440
|
+
modelId,
|
|
23441
|
+
provider: opts.provider,
|
|
23442
|
+
status: jobStatus,
|
|
23443
|
+
startedAt: now
|
|
23444
|
+
});
|
|
23445
|
+
printSuccess(`Fine-tune job started!`);
|
|
23446
|
+
console.log();
|
|
23447
|
+
console.log(` Model ID: ${modelId}`);
|
|
23448
|
+
console.log(` Job ID: ${jobId}`);
|
|
23449
|
+
console.log(` Status: ${printStatus(jobStatus)}`);
|
|
23450
|
+
console.log();
|
|
23451
|
+
printInfo(`Use 'brains finetune status ${jobId}' to check progress.`);
|
|
23452
|
+
} catch (err) {
|
|
23453
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23454
|
+
process.exit(1);
|
|
23455
|
+
}
|
|
23456
|
+
});
|
|
23457
|
+
finetuneCmd.command("status <job-id>").description("Get the status of a fine-tuning job").option("--provider <provider>", "Provider (openai|thinker-labs)", "openai").option("--json", "Output as JSON").action(async (jobId, opts) => {
|
|
23458
|
+
try {
|
|
23459
|
+
let result;
|
|
23460
|
+
if (opts.provider === "openai") {
|
|
23461
|
+
result = await getFineTuneStatus(jobId);
|
|
23462
|
+
} else {
|
|
23463
|
+
const tl = new ThinkerLabsProvider;
|
|
23464
|
+
result = await tl.getFineTuneStatus(jobId);
|
|
23465
|
+
}
|
|
23466
|
+
if (opts.json) {
|
|
23467
|
+
printJson(result);
|
|
23468
|
+
} else {
|
|
23469
|
+
console.log();
|
|
23470
|
+
console.log(` Job ID: ${result.jobId}`);
|
|
23471
|
+
console.log(` Status: ${printStatus(result.status)}`);
|
|
23472
|
+
if (result.fineTunedModel)
|
|
23473
|
+
console.log(` Fine-tuned model: ${result.fineTunedModel}`);
|
|
23474
|
+
if (result.error)
|
|
23475
|
+
console.log(` Error: ${result.error}`);
|
|
23476
|
+
console.log();
|
|
23477
|
+
}
|
|
23505
23478
|
const db = getDb();
|
|
23506
23479
|
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
|
|
23507
23480
|
if (model) {
|
|
23508
|
-
|
|
23481
|
+
const status = result.status;
|
|
23482
|
+
await db.update(fineTunedModels).set({ status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
|
|
23509
23483
|
}
|
|
23510
|
-
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
|
|
23514
|
-
|
|
23515
|
-
|
|
23516
|
-
|
|
23517
|
-
|
|
23484
|
+
} catch (err) {
|
|
23485
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23486
|
+
process.exit(1);
|
|
23487
|
+
}
|
|
23488
|
+
});
|
|
23489
|
+
finetuneCmd.command("watch <job-id>").description("Poll a fine-tuning job until it completes or fails").option("--provider <provider>", "Provider (openai|thinker-labs)", "openai").option("--interval <seconds>", "Poll interval in seconds", "30").action(async (jobId, opts) => {
|
|
23490
|
+
const intervalMs = Math.max(5, parseInt(opts.interval, 10) || 30) * 1000;
|
|
23491
|
+
const terminalStates = new Set(["succeeded", "failed", "cancelled"]);
|
|
23492
|
+
printInfo(`Watching job ${jobId} (polling every ${intervalMs / 1000}s) \u2026`);
|
|
23493
|
+
console.log();
|
|
23494
|
+
const poll = async () => {
|
|
23495
|
+
try {
|
|
23496
|
+
let result;
|
|
23497
|
+
if (opts.provider === "openai") {
|
|
23498
|
+
result = await getFineTuneStatus(jobId);
|
|
23518
23499
|
} else {
|
|
23519
|
-
|
|
23500
|
+
const tl = new ThinkerLabsProvider;
|
|
23501
|
+
result = await tl.getFineTuneStatus(jobId);
|
|
23502
|
+
}
|
|
23503
|
+
const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
23504
|
+
process.stdout.write(` [${ts}] ${printStatus(result.status)}`);
|
|
23505
|
+
if (result.fineTunedModel)
|
|
23506
|
+
process.stdout.write(` model: ${result.fineTunedModel}`);
|
|
23507
|
+
process.stdout.write(`
|
|
23508
|
+
`);
|
|
23509
|
+
const db = getDb();
|
|
23510
|
+
const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
|
|
23511
|
+
if (model) {
|
|
23512
|
+
await db.update(fineTunedModels).set({ status: result.status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
|
|
23513
|
+
}
|
|
23514
|
+
if (terminalStates.has(result.status)) {
|
|
23515
|
+
console.log();
|
|
23516
|
+
if (result.status === "succeeded") {
|
|
23517
|
+
printSuccess(`Job completed successfully.`);
|
|
23518
|
+
if (result.fineTunedModel)
|
|
23519
|
+
printSuccess(`Fine-tuned model: ${result.fineTunedModel}`);
|
|
23520
|
+
} else if (result.status === "failed") {
|
|
23521
|
+
printError(`Job failed.${result.error ? " Error: " + result.error : ""}`);
|
|
23522
|
+
} else {
|
|
23523
|
+
printInfo(`Job ${result.status}.`);
|
|
23524
|
+
}
|
|
23525
|
+
return true;
|
|
23520
23526
|
}
|
|
23521
|
-
return
|
|
23527
|
+
return false;
|
|
23528
|
+
} catch (err) {
|
|
23529
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23530
|
+
return false;
|
|
23522
23531
|
}
|
|
23523
|
-
|
|
23532
|
+
};
|
|
23533
|
+
const done = await poll();
|
|
23534
|
+
if (!done) {
|
|
23535
|
+
await new Promise((resolve2) => {
|
|
23536
|
+
const timer = setInterval(async () => {
|
|
23537
|
+
const finished = await poll();
|
|
23538
|
+
if (finished) {
|
|
23539
|
+
clearInterval(timer);
|
|
23540
|
+
resolve2();
|
|
23541
|
+
}
|
|
23542
|
+
}, intervalMs);
|
|
23543
|
+
});
|
|
23544
|
+
}
|
|
23545
|
+
});
|
|
23546
|
+
finetuneCmd.command("list").description("List all fine-tuning jobs").option("--provider <provider>", "Provider to query (openai|thinker-labs)", "openai").option("--json", "Output as JSON").action(async (opts) => {
|
|
23547
|
+
try {
|
|
23548
|
+
let jobs;
|
|
23549
|
+
if (opts.provider === "openai") {
|
|
23550
|
+
jobs = await listFineTunedModels();
|
|
23551
|
+
} else {
|
|
23552
|
+
const tl = new ThinkerLabsProvider;
|
|
23553
|
+
jobs = await tl.listFineTunedModels();
|
|
23554
|
+
}
|
|
23555
|
+
if (opts.json) {
|
|
23556
|
+
printJson(jobs);
|
|
23557
|
+
return;
|
|
23558
|
+
}
|
|
23559
|
+
if (jobs.length === 0) {
|
|
23560
|
+
printInfo("No fine-tuning jobs found.");
|
|
23561
|
+
return;
|
|
23562
|
+
}
|
|
23563
|
+
printTable(["Job ID", "Model", "Status", "Created"], jobs.map((j) => [
|
|
23564
|
+
j.id,
|
|
23565
|
+
j.model,
|
|
23566
|
+
printStatus(j.status),
|
|
23567
|
+
new Date(j.created * 1000).toISOString().split("T")[0] ?? ""
|
|
23568
|
+
]));
|
|
23524
23569
|
} catch (err) {
|
|
23525
23570
|
printError(err instanceof Error ? err.message : String(err));
|
|
23526
|
-
|
|
23527
|
-
}
|
|
23528
|
-
};
|
|
23529
|
-
const done = await poll();
|
|
23530
|
-
if (!done) {
|
|
23531
|
-
await new Promise((resolve2) => {
|
|
23532
|
-
const timer = setInterval(async () => {
|
|
23533
|
-
const finished = await poll();
|
|
23534
|
-
if (finished) {
|
|
23535
|
-
clearInterval(timer);
|
|
23536
|
-
resolve2();
|
|
23537
|
-
}
|
|
23538
|
-
}, intervalMs);
|
|
23539
|
-
});
|
|
23540
|
-
}
|
|
23541
|
-
});
|
|
23542
|
-
finetuneCmd.command("list").description("List all fine-tuning jobs").option("--provider <provider>", "Provider to query (openai|thinker-labs)", "openai").option("--json", "Output as JSON").action(async (opts) => {
|
|
23543
|
-
try {
|
|
23544
|
-
let jobs;
|
|
23545
|
-
if (opts.provider === "openai") {
|
|
23546
|
-
jobs = await listFineTunedModels();
|
|
23547
|
-
} else {
|
|
23548
|
-
const tl = new ThinkerLabsProvider;
|
|
23549
|
-
jobs = await tl.listFineTunedModels();
|
|
23571
|
+
process.exit(1);
|
|
23550
23572
|
}
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23573
|
+
});
|
|
23574
|
+
}
|
|
23575
|
+
|
|
23576
|
+
// src/cli/commands/data.ts
|
|
23577
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
23578
|
+
import { readFileSync as readFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
|
|
23579
|
+
import { join as join13 } from "path";
|
|
23580
|
+
import { homedir as homedir12 } from "os";
|
|
23581
|
+
var DEFAULT_DATASETS_DIR = join13(homedir12(), ".hasna", "brains", "datasets");
|
|
23582
|
+
function registerDataCommands(program2) {
|
|
23583
|
+
const dataCmd = program2.command("data").description("Manage training datasets");
|
|
23584
|
+
dataCmd.command("gather").description("Gather training data from agent memory sources").option("--source <source>", "Data source: todos|mementos|conversations|sessions|all", "all").option("--output <dir>", "Output directory", DEFAULT_DATASETS_DIR).option("--limit <n>", "Maximum number of examples to gather", "500").action(async (opts) => {
|
|
23585
|
+
const validSources = ["todos", "mementos", "conversations", "sessions", "all"];
|
|
23586
|
+
if (!validSources.includes(opts.source)) {
|
|
23587
|
+
printError(`Invalid source: ${opts.source}. Choose one of: ${validSources.join(", ")}`);
|
|
23588
|
+
process.exit(1);
|
|
23554
23589
|
}
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23590
|
+
const limit2 = parseInt(opts.limit, 10);
|
|
23591
|
+
if (isNaN(limit2) || limit2 <= 0) {
|
|
23592
|
+
printError(`Invalid --limit value: ${opts.limit}`);
|
|
23593
|
+
process.exit(1);
|
|
23558
23594
|
}
|
|
23559
|
-
|
|
23560
|
-
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23567
|
-
|
|
23568
|
-
|
|
23569
|
-
}
|
|
23570
|
-
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
|
|
23575
|
-
|
|
23576
|
-
|
|
23577
|
-
}
|
|
23578
|
-
|
|
23579
|
-
|
|
23580
|
-
|
|
23581
|
-
|
|
23582
|
-
}
|
|
23583
|
-
|
|
23584
|
-
|
|
23585
|
-
|
|
23586
|
-
|
|
23587
|
-
|
|
23588
|
-
const gathererMap = {
|
|
23589
|
-
todos: (o) => Promise.resolve().then(() => (init_todos(), exports_todos)).then((m) => m.gatherFromTodos(o)),
|
|
23590
|
-
mementos: (o) => Promise.resolve().then(() => (init_mementos(), exports_mementos)).then((m) => m.gatherFromMementos(o)),
|
|
23591
|
-
conversations: (o) => Promise.resolve().then(() => (init_conversations(), exports_conversations)).then((m) => m.gatherFromConversations(o)),
|
|
23592
|
-
sessions: (o) => Promise.resolve().then(() => (init_sessions(), exports_sessions2)).then((m) => m.gatherFromSessions(o))
|
|
23593
|
-
};
|
|
23594
|
-
let totalExamples = 0;
|
|
23595
|
-
let successfulSources = 0;
|
|
23596
|
-
for (const source of sources) {
|
|
23597
|
-
printInfo(`Gathering from ${source} \u2026`);
|
|
23598
|
-
try {
|
|
23599
|
-
const gatherer = gathererMap[source];
|
|
23600
|
-
if (!gatherer) {
|
|
23601
|
-
printError(` Unknown source: ${source}`);
|
|
23602
|
-
continue;
|
|
23603
|
-
}
|
|
23604
|
-
const { examples, count } = await gatherer({ limit: limit2 });
|
|
23605
|
-
if (count === 0) {
|
|
23606
|
-
printInfo(` No examples found in ${source}.`);
|
|
23607
|
-
continue;
|
|
23608
|
-
}
|
|
23609
|
-
const fileName = `${source}-${now}.jsonl`;
|
|
23610
|
-
const filePath = join13(opts.output, fileName);
|
|
23611
|
-
writeFileSync5(filePath, examples.map((e) => JSON.stringify(e)).join(`
|
|
23595
|
+
try {
|
|
23596
|
+
mkdirSync6(opts.output, { recursive: true });
|
|
23597
|
+
const sources = opts.source === "all" ? ["todos", "mementos", "conversations", "sessions"] : [opts.source];
|
|
23598
|
+
const now = Date.now();
|
|
23599
|
+
const db = getDb();
|
|
23600
|
+
const gathererMap = {
|
|
23601
|
+
todos: (o) => Promise.resolve().then(() => (init_todos(), exports_todos)).then((m) => m.gatherFromTodos(o)),
|
|
23602
|
+
mementos: (o) => Promise.resolve().then(() => (init_mementos(), exports_mementos)).then((m) => m.gatherFromMementos(o)),
|
|
23603
|
+
conversations: (o) => Promise.resolve().then(() => (init_conversations(), exports_conversations)).then((m) => m.gatherFromConversations(o)),
|
|
23604
|
+
sessions: (o) => Promise.resolve().then(() => (init_sessions(), exports_sessions2)).then((m) => m.gatherFromSessions(o))
|
|
23605
|
+
};
|
|
23606
|
+
let totalExamples = 0;
|
|
23607
|
+
let successfulSources = 0;
|
|
23608
|
+
for (const source of sources) {
|
|
23609
|
+
printInfo(`Gathering from ${source} \u2026`);
|
|
23610
|
+
try {
|
|
23611
|
+
const gatherer = gathererMap[source];
|
|
23612
|
+
if (!gatherer) {
|
|
23613
|
+
printError(` Unknown source: ${source}`);
|
|
23614
|
+
continue;
|
|
23615
|
+
}
|
|
23616
|
+
const { examples, count } = await gatherer({ limit: limit2 });
|
|
23617
|
+
if (count === 0) {
|
|
23618
|
+
printInfo(` No examples found in ${source}.`);
|
|
23619
|
+
continue;
|
|
23620
|
+
}
|
|
23621
|
+
const fileName = `${source}-${now}.jsonl`;
|
|
23622
|
+
const filePath = join13(opts.output, fileName);
|
|
23623
|
+
writeFileSync5(filePath, examples.map((e) => JSON.stringify(e)).join(`
|
|
23612
23624
|
`) + `
|
|
23613
23625
|
`, "utf8");
|
|
23614
|
-
|
|
23615
|
-
|
|
23616
|
-
|
|
23617
|
-
|
|
23618
|
-
|
|
23619
|
-
|
|
23620
|
-
|
|
23621
|
-
|
|
23622
|
-
|
|
23623
|
-
|
|
23624
|
-
|
|
23625
|
-
|
|
23626
|
+
await db.insert(trainingDatasets).values({
|
|
23627
|
+
id: randomUUID3(),
|
|
23628
|
+
source,
|
|
23629
|
+
filePath,
|
|
23630
|
+
exampleCount: count,
|
|
23631
|
+
createdAt: now
|
|
23632
|
+
});
|
|
23633
|
+
printSuccess(` \u2713 ${count} examples \u2192 ${filePath}`);
|
|
23634
|
+
totalExamples += count;
|
|
23635
|
+
successfulSources++;
|
|
23636
|
+
} catch (sourceErr) {
|
|
23637
|
+
printError(` \u2717 ${source}: ${sourceErr instanceof Error ? sourceErr.message : String(sourceErr)}`);
|
|
23638
|
+
}
|
|
23626
23639
|
}
|
|
23640
|
+
console.log();
|
|
23641
|
+
printSuccess(`Total: ${totalExamples} examples from ${successfulSources} source(s)`);
|
|
23642
|
+
} catch (err) {
|
|
23643
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23644
|
+
process.exit(1);
|
|
23627
23645
|
}
|
|
23628
|
-
|
|
23629
|
-
|
|
23630
|
-
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
}
|
|
23634
|
-
});
|
|
23635
|
-
dataCmd.command("preview <file>").description("Preview a JSONL training file").option("-n, --count <n>", "Number of examples to show", "5").action((file, opts) => {
|
|
23636
|
-
if (!existsSync10(file)) {
|
|
23637
|
-
printError(`File not found: ${file}`);
|
|
23638
|
-
process.exit(1);
|
|
23639
|
-
}
|
|
23640
|
-
const n = parseInt(opts.count, 10);
|
|
23641
|
-
if (isNaN(n) || n <= 0) {
|
|
23642
|
-
printError(`Invalid --count value: ${opts.count}`);
|
|
23643
|
-
process.exit(1);
|
|
23644
|
-
}
|
|
23645
|
-
try {
|
|
23646
|
-
const content = readFileSync6(file, "utf8");
|
|
23647
|
-
const lines = content.trim().split(`
|
|
23648
|
-
`).filter(Boolean);
|
|
23649
|
-
const total = lines.length;
|
|
23650
|
-
const preview = lines.slice(0, n);
|
|
23651
|
-
console.log();
|
|
23652
|
-
printInfo(`File: ${file}`);
|
|
23653
|
-
printInfo(`Total examples: ${total}`);
|
|
23654
|
-
printInfo(`Showing first ${Math.min(n, total)}:`);
|
|
23655
|
-
console.log();
|
|
23656
|
-
preview.forEach((line, idx) => {
|
|
23657
|
-
try {
|
|
23658
|
-
const parsed = JSON.parse(line);
|
|
23659
|
-
console.log(`\u2500\u2500\u2500 Example ${idx + 1} \u2500\u2500\u2500`);
|
|
23660
|
-
printJson(parsed);
|
|
23661
|
-
console.log();
|
|
23662
|
-
} catch {
|
|
23663
|
-
printError(` Line ${idx + 1} is not valid JSON: ${line.slice(0, 80)}\u2026`);
|
|
23664
|
-
}
|
|
23665
|
-
});
|
|
23666
|
-
} catch (err) {
|
|
23667
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23668
|
-
process.exit(1);
|
|
23669
|
-
}
|
|
23670
|
-
});
|
|
23671
|
-
dataCmd.command("merge <files...>").description("Merge multiple JSONL datasets into one").option("--output <path>", "Output file path", join13(DEFAULT_DATASETS_DIR, `merged-${Date.now()}.jsonl`)).option("--no-dedupe", "Skip deduplication").action(async (files, opts) => {
|
|
23672
|
-
try {
|
|
23673
|
-
for (const f of files) {
|
|
23674
|
-
if (!existsSync10(f)) {
|
|
23675
|
-
printError(`File not found: ${f}`);
|
|
23676
|
-
process.exit(1);
|
|
23677
|
-
}
|
|
23646
|
+
});
|
|
23647
|
+
dataCmd.command("preview <file>").description("Preview a JSONL training file").option("-n, --count <n>", "Number of examples to show", "5").action((file, opts) => {
|
|
23648
|
+
if (!existsSync11(file)) {
|
|
23649
|
+
printError(`File not found: ${file}`);
|
|
23650
|
+
process.exit(1);
|
|
23678
23651
|
}
|
|
23679
|
-
|
|
23680
|
-
|
|
23681
|
-
|
|
23682
|
-
|
|
23683
|
-
|
|
23684
|
-
|
|
23685
|
-
|
|
23686
|
-
|
|
23687
|
-
|
|
23688
|
-
|
|
23689
|
-
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
|
|
23652
|
+
const n = parseInt(opts.count, 10);
|
|
23653
|
+
if (isNaN(n) || n <= 0) {
|
|
23654
|
+
printError(`Invalid --count value: ${opts.count}`);
|
|
23655
|
+
process.exit(1);
|
|
23656
|
+
}
|
|
23657
|
+
try {
|
|
23658
|
+
const content = readFileSync6(file, "utf8");
|
|
23659
|
+
const lines = content.trim().split(`
|
|
23660
|
+
`).filter(Boolean);
|
|
23661
|
+
const total = lines.length;
|
|
23662
|
+
const preview = lines.slice(0, n);
|
|
23663
|
+
console.log();
|
|
23664
|
+
printInfo(`File: ${file}`);
|
|
23665
|
+
printInfo(`Total examples: ${total}`);
|
|
23666
|
+
printInfo(`Showing first ${Math.min(n, total)}:`);
|
|
23667
|
+
console.log();
|
|
23668
|
+
preview.forEach((line, idx) => {
|
|
23669
|
+
try {
|
|
23670
|
+
const parsed = JSON.parse(line);
|
|
23671
|
+
console.log(`\u2500\u2500\u2500 Example ${idx + 1} \u2500\u2500\u2500`);
|
|
23672
|
+
printJson(parsed);
|
|
23673
|
+
console.log();
|
|
23674
|
+
} catch {
|
|
23675
|
+
printError(` Line ${idx + 1} is not valid JSON: ${line.slice(0, 80)}\u2026`);
|
|
23695
23676
|
}
|
|
23696
|
-
seen.add(line);
|
|
23697
|
-
return true;
|
|
23698
23677
|
});
|
|
23678
|
+
} catch (err) {
|
|
23679
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23680
|
+
process.exit(1);
|
|
23699
23681
|
}
|
|
23700
|
-
|
|
23682
|
+
});
|
|
23683
|
+
dataCmd.command("merge <files...>").description("Merge multiple JSONL datasets into one").option("--output <path>", "Output file path", join13(DEFAULT_DATASETS_DIR, `merged-${Date.now()}.jsonl`)).option("--no-dedupe", "Skip deduplication").action(async (files, opts) => {
|
|
23684
|
+
try {
|
|
23685
|
+
for (const f of files) {
|
|
23686
|
+
if (!existsSync11(f)) {
|
|
23687
|
+
printError(`File not found: ${f}`);
|
|
23688
|
+
process.exit(1);
|
|
23689
|
+
}
|
|
23690
|
+
}
|
|
23691
|
+
mkdirSync6(join13(opts.output, "..").replace(/\/\.\.$/, "") || DEFAULT_DATASETS_DIR, { recursive: true });
|
|
23692
|
+
const allExamples = [];
|
|
23693
|
+
for (const f of files) {
|
|
23694
|
+
const lines = readFileSync6(f, "utf8").split(`
|
|
23695
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
23696
|
+
allExamples.push(...lines);
|
|
23697
|
+
printInfo(` Read ${lines.length} examples from ${f}`);
|
|
23698
|
+
}
|
|
23699
|
+
let finalLines = allExamples;
|
|
23700
|
+
let dupeCount = 0;
|
|
23701
|
+
if (opts.dedupe) {
|
|
23702
|
+
const seen = new Set;
|
|
23703
|
+
finalLines = allExamples.filter((line) => {
|
|
23704
|
+
if (seen.has(line)) {
|
|
23705
|
+
dupeCount++;
|
|
23706
|
+
return false;
|
|
23707
|
+
}
|
|
23708
|
+
seen.add(line);
|
|
23709
|
+
return true;
|
|
23710
|
+
});
|
|
23711
|
+
}
|
|
23712
|
+
writeFileSync5(opts.output, finalLines.join(`
|
|
23701
23713
|
`) + `
|
|
23702
23714
|
`, "utf8");
|
|
23703
|
-
|
|
23704
|
-
|
|
23705
|
-
|
|
23706
|
-
|
|
23707
|
-
|
|
23708
|
-
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23715
|
-
|
|
23716
|
-
|
|
23717
|
-
|
|
23718
|
-
}
|
|
23719
|
-
});
|
|
23720
|
-
dataCmd.command("list").description("List all gathered datasets").option("--json", "Output as JSON").action(async (opts) => {
|
|
23721
|
-
try {
|
|
23722
|
-
const db = getDb();
|
|
23723
|
-
const datasets = await db.select().from(trainingDatasets);
|
|
23724
|
-
if (opts.json) {
|
|
23725
|
-
printJson(datasets);
|
|
23726
|
-
return;
|
|
23715
|
+
const db = getDb();
|
|
23716
|
+
await db.insert(trainingDatasets).values({
|
|
23717
|
+
id: randomUUID3(),
|
|
23718
|
+
source: "mixed",
|
|
23719
|
+
filePath: opts.output,
|
|
23720
|
+
exampleCount: finalLines.length,
|
|
23721
|
+
createdAt: Date.now()
|
|
23722
|
+
});
|
|
23723
|
+
console.log();
|
|
23724
|
+
printSuccess(`Merged ${files.length} files \u2014 ${finalLines.length} examples \u2192 ${opts.output}`);
|
|
23725
|
+
if (opts.dedupe && dupeCount > 0)
|
|
23726
|
+
printInfo(` Removed ${dupeCount} duplicate(s)`);
|
|
23727
|
+
} catch (err) {
|
|
23728
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23729
|
+
process.exit(1);
|
|
23727
23730
|
}
|
|
23728
|
-
|
|
23729
|
-
|
|
23730
|
-
|
|
23731
|
+
});
|
|
23732
|
+
dataCmd.command("list").description("List all gathered datasets").option("--json", "Output as JSON").action(async (opts) => {
|
|
23733
|
+
try {
|
|
23734
|
+
const db = getDb();
|
|
23735
|
+
const datasets = await db.select().from(trainingDatasets);
|
|
23736
|
+
if (opts.json) {
|
|
23737
|
+
printJson(datasets);
|
|
23738
|
+
return;
|
|
23739
|
+
}
|
|
23740
|
+
if (datasets.length === 0) {
|
|
23741
|
+
printInfo("No datasets found. Use 'brains data gather' to create one.");
|
|
23742
|
+
return;
|
|
23743
|
+
}
|
|
23744
|
+
printTable(["ID", "Source", "Examples", "File", "Created"], datasets.map((d) => [
|
|
23745
|
+
d.id,
|
|
23746
|
+
d.source,
|
|
23747
|
+
String(d.exampleCount),
|
|
23748
|
+
d.filePath ?? "",
|
|
23749
|
+
new Date(d.createdAt).toISOString().split("T")[0] ?? ""
|
|
23750
|
+
]));
|
|
23751
|
+
} catch (err) {
|
|
23752
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23753
|
+
process.exit(1);
|
|
23731
23754
|
}
|
|
23732
|
-
|
|
23733
|
-
|
|
23734
|
-
|
|
23735
|
-
|
|
23736
|
-
d.filePath ?? "",
|
|
23737
|
-
new Date(d.createdAt).toISOString().split("T")[0] ?? ""
|
|
23738
|
-
]));
|
|
23739
|
-
} catch (err) {
|
|
23740
|
-
printError(err instanceof Error ? err.message : String(err));
|
|
23741
|
-
process.exit(1);
|
|
23742
|
-
}
|
|
23743
|
-
});
|
|
23744
|
-
var collectionsCmd = program2.command("collections").description("Manage model collections");
|
|
23745
|
-
collectionsCmd.option("--json", "Output as JSON").action(async (opts) => {
|
|
23746
|
-
await listCollections(opts.json);
|
|
23747
|
-
});
|
|
23755
|
+
});
|
|
23756
|
+
}
|
|
23757
|
+
|
|
23758
|
+
// src/cli/commands/collections.ts
|
|
23748
23759
|
async function listCollections(json = false) {
|
|
23749
23760
|
try {
|
|
23750
23761
|
const db = getDb();
|
|
@@ -23761,45 +23772,224 @@ async function listCollections(json = false) {
|
|
|
23761
23772
|
printInfo("No collections found. Set a collection with 'brains models set-collection'.");
|
|
23762
23773
|
return;
|
|
23763
23774
|
}
|
|
23764
|
-
printTable(["Collection", "Model Count", "Models"], rows.map((r) => [
|
|
23765
|
-
r.collection ?? "(none)",
|
|
23766
|
-
String(r.count),
|
|
23767
|
-
r.names ?? ""
|
|
23768
|
-
]));
|
|
23775
|
+
printTable(["Collection", "Model Count", "Models"], rows.map((r) => [r.collection ?? "(none)", String(r.count), r.names ?? ""]));
|
|
23769
23776
|
} catch (err) {
|
|
23770
23777
|
printError(err instanceof Error ? err.message : String(err));
|
|
23771
23778
|
process.exit(1);
|
|
23772
23779
|
}
|
|
23773
23780
|
}
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
|
|
23781
|
+
function registerCollectionsCommands(program2) {
|
|
23782
|
+
const collectionsCmd = program2.command("collections").description("Manage model collections");
|
|
23783
|
+
collectionsCmd.option("--json", "Output as JSON").action(async (opts) => {
|
|
23784
|
+
await listCollections(opts.json);
|
|
23785
|
+
});
|
|
23786
|
+
collectionsCmd.command("list").description("List all collections with model counts").option("--json", "Output as JSON").action(async (opts) => {
|
|
23787
|
+
await listCollections(opts.json);
|
|
23788
|
+
});
|
|
23789
|
+
collectionsCmd.command("show <name>").description("List all models in a collection").action(async (name) => {
|
|
23790
|
+
try {
|
|
23791
|
+
const db = getDb();
|
|
23792
|
+
const models = await db.select().from(fineTunedModels).where(eq(fineTunedModels.collection, name));
|
|
23793
|
+
if (models.length === 0) {
|
|
23794
|
+
printInfo(`No models found in collection '${name}'.`);
|
|
23795
|
+
return;
|
|
23796
|
+
}
|
|
23797
|
+
printTable(["ID", "Name", "Provider", "Status", "Base Model"], models.map((m) => [m.id, m.name, m.provider, printStatus(m.status), m.baseModel]));
|
|
23798
|
+
} catch (err) {
|
|
23799
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23800
|
+
process.exit(1);
|
|
23784
23801
|
}
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
});
|
|
23791
|
-
|
|
23792
|
-
|
|
23793
|
-
|
|
23794
|
-
|
|
23795
|
-
|
|
23796
|
-
|
|
23797
|
-
|
|
23798
|
-
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
|
|
23802
|
-
|
|
23802
|
+
});
|
|
23803
|
+
collectionsCmd.command("rename <oldName> <newName>").description("Rename a collection across all models").action(async (oldName, newName) => {
|
|
23804
|
+
try {
|
|
23805
|
+
const db = getDb();
|
|
23806
|
+
const affected = await db.select({ id: fineTunedModels.id }).from(fineTunedModels).where(eq(fineTunedModels.collection, oldName));
|
|
23807
|
+
await db.update(fineTunedModels).set({ collection: newName, updatedAt: Date.now() }).where(eq(fineTunedModels.collection, oldName));
|
|
23808
|
+
printSuccess(`Renamed collection '${oldName}' \u2192 '${newName}' (${affected.length} models updated)`);
|
|
23809
|
+
} catch (err) {
|
|
23810
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
23811
|
+
process.exit(1);
|
|
23812
|
+
}
|
|
23813
|
+
});
|
|
23814
|
+
}
|
|
23815
|
+
|
|
23816
|
+
// src/cli/commands/cloud.ts
|
|
23817
|
+
function registerCloudCommands2(program2) {
|
|
23818
|
+
const cloudCmd = program2.command("cloud").description("Cloud sync commands");
|
|
23819
|
+
cloudCmd.command("status").description("Show cloud config and connection health").option("--json", "Output as JSON").action(async (opts) => {
|
|
23820
|
+
try {
|
|
23821
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, PgAdapterAsync: PgAdapterAsync2, SqliteAdapter: SqliteAdapter2, getDbPath: getDbPath2, listSqliteTables: listSqliteTables2, ensureConflictsTable: ensureConflictsTable2, listConflicts: listConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23822
|
+
const config = getCloudConfig2();
|
|
23823
|
+
const info = {
|
|
23824
|
+
mode: config.mode,
|
|
23825
|
+
service: "brains",
|
|
23826
|
+
rds_host: config.rds?.host || "(not configured)"
|
|
23827
|
+
};
|
|
23828
|
+
if (config.rds?.host && config.rds?.username) {
|
|
23829
|
+
try {
|
|
23830
|
+
const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
|
|
23831
|
+
await pg.get("SELECT 1 as ok");
|
|
23832
|
+
info.postgresql = "connected";
|
|
23833
|
+
await pg.close();
|
|
23834
|
+
} catch (err) {
|
|
23835
|
+
info.postgresql = `failed \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
23836
|
+
}
|
|
23837
|
+
}
|
|
23838
|
+
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
23839
|
+
const tables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
23840
|
+
const syncHealth = [];
|
|
23841
|
+
for (const table of tables) {
|
|
23842
|
+
try {
|
|
23843
|
+
const totalRow = local.get(`SELECT COUNT(*) as c FROM "${table}"`);
|
|
23844
|
+
const unsyncedRow = local.get(`SELECT COUNT(*) as c FROM "${table}" WHERE synced_at IS NULL`);
|
|
23845
|
+
syncHealth.push({ table, total: totalRow?.c ?? 0, unsynced: unsyncedRow?.c ?? 0 });
|
|
23846
|
+
} catch {}
|
|
23847
|
+
}
|
|
23848
|
+
info.sync_health = syncHealth.filter((s) => s.total > 0);
|
|
23849
|
+
try {
|
|
23850
|
+
ensureConflictsTable2(local);
|
|
23851
|
+
const unresolved = listConflicts2(local, { resolved: false });
|
|
23852
|
+
info.conflicts_unresolved = unresolved.length;
|
|
23853
|
+
} catch {}
|
|
23854
|
+
local.close();
|
|
23855
|
+
if (opts.json) {
|
|
23856
|
+
console.log(JSON.stringify(info, null, 2));
|
|
23857
|
+
return;
|
|
23858
|
+
}
|
|
23859
|
+
printInfo(`Mode: ${info.mode}`);
|
|
23860
|
+
printInfo(`RDS Host: ${info.rds_host}`);
|
|
23861
|
+
if (info.postgresql)
|
|
23862
|
+
printInfo(`PostgreSQL: ${info.postgresql}`);
|
|
23863
|
+
for (const s of info.sync_health) {
|
|
23864
|
+
const pct = s.total > 0 ? Math.round((s.total - s.unsynced) / s.total * 100) : 100;
|
|
23865
|
+
printInfo(` ${s.table}: ${pct}% synced (${s.unsynced} unsynced / ${s.total} total)`);
|
|
23866
|
+
}
|
|
23867
|
+
if (info.conflicts_unresolved)
|
|
23868
|
+
printInfo(`Conflicts: ${info.conflicts_unresolved} unresolved`);
|
|
23869
|
+
} catch (e) {
|
|
23870
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
23871
|
+
process.exit(1);
|
|
23872
|
+
}
|
|
23873
|
+
});
|
|
23874
|
+
cloudCmd.command("push").description("Push local data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
23875
|
+
try {
|
|
23876
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, listSqliteTables: listSqliteTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23877
|
+
const config = getCloudConfig2();
|
|
23878
|
+
if (config.mode === "local") {
|
|
23879
|
+
printError("Cloud mode not configured.");
|
|
23880
|
+
process.exit(1);
|
|
23881
|
+
}
|
|
23882
|
+
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
23883
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
|
|
23884
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
23885
|
+
const results = await syncPush2(local, cloud, {
|
|
23886
|
+
tables: tableList,
|
|
23887
|
+
onProgress: (p) => {
|
|
23888
|
+
if (!opts.json && p.phase === "done")
|
|
23889
|
+
printInfo(` ${p.table}: ${p.rowsWritten} rows pushed`);
|
|
23890
|
+
}
|
|
23891
|
+
});
|
|
23892
|
+
local.close();
|
|
23893
|
+
await cloud.close();
|
|
23894
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23895
|
+
if (opts.json) {
|
|
23896
|
+
console.log(JSON.stringify({ total, tables: results }));
|
|
23897
|
+
return;
|
|
23898
|
+
}
|
|
23899
|
+
printSuccess(`Done. ${total} rows pushed.`);
|
|
23900
|
+
} catch (e) {
|
|
23901
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
23902
|
+
process.exit(1);
|
|
23903
|
+
}
|
|
23904
|
+
});
|
|
23905
|
+
cloudCmd.command("pull").description("Pull cloud data to local \u2014 merges by primary key").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
23906
|
+
try {
|
|
23907
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPull: syncPull2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23908
|
+
const config = getCloudConfig2();
|
|
23909
|
+
if (config.mode === "local") {
|
|
23910
|
+
printError("Cloud mode not configured.");
|
|
23911
|
+
process.exit(1);
|
|
23912
|
+
}
|
|
23913
|
+
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
23914
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
|
|
23915
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
|
|
23916
|
+
const results = await syncPull2(cloud, local, {
|
|
23917
|
+
tables: tableList,
|
|
23918
|
+
onProgress: (p) => {
|
|
23919
|
+
if (!opts.json && p.phase === "done")
|
|
23920
|
+
printInfo(` ${p.table}: ${p.rowsWritten} rows pulled`);
|
|
23921
|
+
}
|
|
23922
|
+
});
|
|
23923
|
+
local.close();
|
|
23924
|
+
await cloud.close();
|
|
23925
|
+
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23926
|
+
if (opts.json) {
|
|
23927
|
+
console.log(JSON.stringify({ total, tables: results }));
|
|
23928
|
+
return;
|
|
23929
|
+
}
|
|
23930
|
+
printSuccess(`Done. ${total} rows pulled.`);
|
|
23931
|
+
} catch (e) {
|
|
23932
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
23933
|
+
process.exit(1);
|
|
23934
|
+
}
|
|
23935
|
+
});
|
|
23936
|
+
cloudCmd.command("sync").description("Bidirectional sync \u2014 pull then push").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
23937
|
+
try {
|
|
23938
|
+
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, syncPull: syncPull2, listSqliteTables: listSqliteTables2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23939
|
+
const config = getCloudConfig2();
|
|
23940
|
+
if (config.mode === "local") {
|
|
23941
|
+
printError("Cloud mode not configured.");
|
|
23942
|
+
process.exit(1);
|
|
23943
|
+
}
|
|
23944
|
+
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
23945
|
+
const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
|
|
23946
|
+
const localTables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
23947
|
+
const remoteTables = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
|
|
23948
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : [...new Set([...localTables, ...remoteTables])];
|
|
23949
|
+
const pullResults = await syncPull2(cloud, local, { tables: tableList.filter((t) => remoteTables.includes(t)) });
|
|
23950
|
+
const pushResults = await syncPush2(local, cloud, { tables: tableList.filter((t) => localTables.includes(t)) });
|
|
23951
|
+
local.close();
|
|
23952
|
+
await cloud.close();
|
|
23953
|
+
const pulled = pullResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23954
|
+
const pushed = pushResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23955
|
+
if (opts.json) {
|
|
23956
|
+
console.log(JSON.stringify({ pulled, pushed }));
|
|
23957
|
+
return;
|
|
23958
|
+
}
|
|
23959
|
+
printSuccess(`Sync done. Pulled ${pulled} rows, pushed ${pushed} rows.`);
|
|
23960
|
+
} catch (e) {
|
|
23961
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
23962
|
+
process.exit(1);
|
|
23963
|
+
}
|
|
23964
|
+
});
|
|
23965
|
+
cloudCmd.command("migrate-pg").description("Apply PostgreSQL migrations to the cloud database").requiredOption("--connection-string <connStr>", "PostgreSQL connection string").option("--json", "Output as JSON").action(async (opts) => {
|
|
23966
|
+
try {
|
|
23967
|
+
const { applyPgMigrations: applyPgMigrations3 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
|
|
23968
|
+
const result = await applyPgMigrations3(opts.connectionString);
|
|
23969
|
+
if (opts.json) {
|
|
23970
|
+
console.log(JSON.stringify(result));
|
|
23971
|
+
return;
|
|
23972
|
+
}
|
|
23973
|
+
printSuccess(`Applied ${result.applied.length} migration(s), skipped ${result.alreadyApplied.length}.`);
|
|
23974
|
+
if (result.errors.length > 0) {
|
|
23975
|
+
printError(result.errors.join(`
|
|
23976
|
+
`));
|
|
23977
|
+
process.exit(1);
|
|
23978
|
+
}
|
|
23979
|
+
} catch (e) {
|
|
23980
|
+
printError(e instanceof Error ? e.message : String(e));
|
|
23981
|
+
process.exit(1);
|
|
23982
|
+
}
|
|
23983
|
+
});
|
|
23984
|
+
}
|
|
23985
|
+
|
|
23986
|
+
// src/cli/index.ts
|
|
23987
|
+
var program2 = new Command;
|
|
23988
|
+
program2.name("brains").description("Fine-tuned model tracker and trainer").version("0.0.1");
|
|
23989
|
+
registerModelsCommands(program2);
|
|
23990
|
+
registerFinetuneCommands(program2);
|
|
23991
|
+
registerDataCommands(program2);
|
|
23992
|
+
registerCollectionsCommands(program2);
|
|
23803
23993
|
program2.command("remove <id>").alias("rm").alias("uninstall").description("Remove a fine-tuned model or training job by ID").option("--type <type>", "Type: model | job (default: auto-detect)").action(async (id, opts) => {
|
|
23804
23994
|
const db = getDb();
|
|
23805
23995
|
try {
|
|
@@ -23874,7 +24064,7 @@ var feedbackCmd = program2.command("feedback").description("Feedback commands");
|
|
|
23874
24064
|
feedbackCmd.command("send <message>").description("Send feedback about brains").option("--email <email>", "Contact email").action(async (message, opts) => {
|
|
23875
24065
|
const { sendFeedback: sendFeedback2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23876
24066
|
const rawDb = getRawDb();
|
|
23877
|
-
const pkg = JSON.parse(
|
|
24067
|
+
const pkg = JSON.parse(readFileSync7(join14(import.meta.dir, "../../package.json"), "utf8"));
|
|
23878
24068
|
const result = await sendFeedback2({ service: "brains", message, email: opts.email, version: pkg.version }, rawDb);
|
|
23879
24069
|
rawDb.close();
|
|
23880
24070
|
if (result.sent) {
|
|
@@ -23904,170 +24094,5 @@ feedbackCmd.command("list").description("List locally saved feedback").option("-
|
|
|
23904
24094
|
e.created_at ?? ""
|
|
23905
24095
|
]));
|
|
23906
24096
|
});
|
|
23907
|
-
|
|
23908
|
-
cloudCmd.command("status").description("Show cloud config and connection health").option("--json", "Output as JSON").action(async (opts) => {
|
|
23909
|
-
try {
|
|
23910
|
-
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, PgAdapterAsync: PgAdapterAsync2, SqliteAdapter: SqliteAdapter2, getDbPath: getDbPath2, listSqliteTables: listSqliteTables2, ensureConflictsTable: ensureConflictsTable2, listConflicts: listConflicts2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23911
|
-
const config = getCloudConfig2();
|
|
23912
|
-
const info = {
|
|
23913
|
-
mode: config.mode,
|
|
23914
|
-
service: "brains",
|
|
23915
|
-
rds_host: config.rds?.host || "(not configured)"
|
|
23916
|
-
};
|
|
23917
|
-
if (config.rds?.host && config.rds?.username) {
|
|
23918
|
-
try {
|
|
23919
|
-
const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
|
|
23920
|
-
await pg.get("SELECT 1 as ok");
|
|
23921
|
-
info.postgresql = "connected";
|
|
23922
|
-
await pg.close();
|
|
23923
|
-
} catch (err) {
|
|
23924
|
-
info.postgresql = `failed \u2014 ${err instanceof Error ? err.message : String(err)}`;
|
|
23925
|
-
}
|
|
23926
|
-
}
|
|
23927
|
-
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
23928
|
-
const tables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
23929
|
-
const syncHealth = [];
|
|
23930
|
-
for (const table of tables) {
|
|
23931
|
-
try {
|
|
23932
|
-
const totalRow = local.get(`SELECT COUNT(*) as c FROM "${table}"`);
|
|
23933
|
-
const unsyncedRow = local.get(`SELECT COUNT(*) as c FROM "${table}" WHERE synced_at IS NULL`);
|
|
23934
|
-
syncHealth.push({ table, total: totalRow?.c ?? 0, unsynced: unsyncedRow?.c ?? 0 });
|
|
23935
|
-
} catch {}
|
|
23936
|
-
}
|
|
23937
|
-
info.sync_health = syncHealth.filter((s) => s.total > 0);
|
|
23938
|
-
try {
|
|
23939
|
-
ensureConflictsTable2(local);
|
|
23940
|
-
const unresolved = listConflicts2(local, { resolved: false });
|
|
23941
|
-
info.conflicts_unresolved = unresolved.length;
|
|
23942
|
-
} catch {}
|
|
23943
|
-
local.close();
|
|
23944
|
-
if (opts.json) {
|
|
23945
|
-
console.log(JSON.stringify(info, null, 2));
|
|
23946
|
-
return;
|
|
23947
|
-
}
|
|
23948
|
-
printInfo(`Mode: ${info.mode}`);
|
|
23949
|
-
printInfo(`RDS Host: ${info.rds_host}`);
|
|
23950
|
-
if (info.postgresql)
|
|
23951
|
-
printInfo(`PostgreSQL: ${info.postgresql}`);
|
|
23952
|
-
for (const s of info.sync_health) {
|
|
23953
|
-
const pct = s.total > 0 ? Math.round((s.total - s.unsynced) / s.total * 100) : 100;
|
|
23954
|
-
printInfo(` ${s.table}: ${pct}% synced (${s.unsynced} unsynced / ${s.total} total)`);
|
|
23955
|
-
}
|
|
23956
|
-
if (info.conflicts_unresolved)
|
|
23957
|
-
printInfo(`Conflicts: ${info.conflicts_unresolved} unresolved`);
|
|
23958
|
-
} catch (e) {
|
|
23959
|
-
printError(e instanceof Error ? e.message : String(e));
|
|
23960
|
-
process.exit(1);
|
|
23961
|
-
}
|
|
23962
|
-
});
|
|
23963
|
-
cloudCmd.command("push").description("Push local data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
23964
|
-
try {
|
|
23965
|
-
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, listSqliteTables: listSqliteTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23966
|
-
const config = getCloudConfig2();
|
|
23967
|
-
if (config.mode === "local") {
|
|
23968
|
-
printError("Cloud mode not configured.");
|
|
23969
|
-
process.exit(1);
|
|
23970
|
-
}
|
|
23971
|
-
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
23972
|
-
const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
|
|
23973
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
23974
|
-
const results = await syncPush2(local, cloud, {
|
|
23975
|
-
tables: tableList,
|
|
23976
|
-
onProgress: (p) => {
|
|
23977
|
-
if (!opts.json && p.phase === "done")
|
|
23978
|
-
printInfo(` ${p.table}: ${p.rowsWritten} rows pushed`);
|
|
23979
|
-
}
|
|
23980
|
-
});
|
|
23981
|
-
local.close();
|
|
23982
|
-
await cloud.close();
|
|
23983
|
-
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
23984
|
-
if (opts.json) {
|
|
23985
|
-
console.log(JSON.stringify({ total, tables: results }));
|
|
23986
|
-
return;
|
|
23987
|
-
}
|
|
23988
|
-
printSuccess(`Done. ${total} rows pushed.`);
|
|
23989
|
-
} catch (e) {
|
|
23990
|
-
printError(e instanceof Error ? e.message : String(e));
|
|
23991
|
-
process.exit(1);
|
|
23992
|
-
}
|
|
23993
|
-
});
|
|
23994
|
-
cloudCmd.command("pull").description("Pull cloud data to local \u2014 merges by primary key").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
23995
|
-
try {
|
|
23996
|
-
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPull: syncPull2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
23997
|
-
const config = getCloudConfig2();
|
|
23998
|
-
if (config.mode === "local") {
|
|
23999
|
-
printError("Cloud mode not configured.");
|
|
24000
|
-
process.exit(1);
|
|
24001
|
-
}
|
|
24002
|
-
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
24003
|
-
const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
|
|
24004
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
|
|
24005
|
-
const results = await syncPull2(cloud, local, {
|
|
24006
|
-
tables: tableList,
|
|
24007
|
-
onProgress: (p) => {
|
|
24008
|
-
if (!opts.json && p.phase === "done")
|
|
24009
|
-
printInfo(` ${p.table}: ${p.rowsWritten} rows pulled`);
|
|
24010
|
-
}
|
|
24011
|
-
});
|
|
24012
|
-
local.close();
|
|
24013
|
-
await cloud.close();
|
|
24014
|
-
const total = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
24015
|
-
if (opts.json) {
|
|
24016
|
-
console.log(JSON.stringify({ total, tables: results }));
|
|
24017
|
-
return;
|
|
24018
|
-
}
|
|
24019
|
-
printSuccess(`Done. ${total} rows pulled.`);
|
|
24020
|
-
} catch (e) {
|
|
24021
|
-
printError(e instanceof Error ? e.message : String(e));
|
|
24022
|
-
process.exit(1);
|
|
24023
|
-
}
|
|
24024
|
-
});
|
|
24025
|
-
cloudCmd.command("sync").description("Bidirectional sync \u2014 pull then push").option("--tables <tables>", "Comma-separated table names (default: all)").option("--json", "Output as JSON").action(async (opts) => {
|
|
24026
|
-
try {
|
|
24027
|
-
const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, syncPull: syncPull2, listSqliteTables: listSqliteTables2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
|
|
24028
|
-
const config = getCloudConfig2();
|
|
24029
|
-
if (config.mode === "local") {
|
|
24030
|
-
printError("Cloud mode not configured.");
|
|
24031
|
-
process.exit(1);
|
|
24032
|
-
}
|
|
24033
|
-
const local = new SqliteAdapter2(getDbPath2("brains"));
|
|
24034
|
-
const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
|
|
24035
|
-
const localTables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
|
|
24036
|
-
const remoteTables = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
|
|
24037
|
-
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : [...new Set([...localTables, ...remoteTables])];
|
|
24038
|
-
const pullResults = await syncPull2(cloud, local, { tables: tableList.filter((t) => remoteTables.includes(t)) });
|
|
24039
|
-
const pushResults = await syncPush2(local, cloud, { tables: tableList.filter((t) => localTables.includes(t)) });
|
|
24040
|
-
local.close();
|
|
24041
|
-
await cloud.close();
|
|
24042
|
-
const pulled = pullResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
24043
|
-
const pushed = pushResults.reduce((s, r) => s + r.rowsWritten, 0);
|
|
24044
|
-
if (opts.json) {
|
|
24045
|
-
console.log(JSON.stringify({ pulled, pushed }));
|
|
24046
|
-
return;
|
|
24047
|
-
}
|
|
24048
|
-
printSuccess(`Sync done. Pulled ${pulled} rows, pushed ${pushed} rows.`);
|
|
24049
|
-
} catch (e) {
|
|
24050
|
-
printError(e instanceof Error ? e.message : String(e));
|
|
24051
|
-
process.exit(1);
|
|
24052
|
-
}
|
|
24053
|
-
});
|
|
24054
|
-
cloudCmd.command("migrate-pg").description("Apply PostgreSQL migrations to the cloud database").requiredOption("--connection-string <connStr>", "PostgreSQL connection string").option("--json", "Output as JSON").action(async (opts) => {
|
|
24055
|
-
try {
|
|
24056
|
-
const { applyPgMigrations: applyPgMigrations3 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
|
|
24057
|
-
const result = await applyPgMigrations3(opts.connectionString);
|
|
24058
|
-
if (opts.json) {
|
|
24059
|
-
console.log(JSON.stringify(result));
|
|
24060
|
-
return;
|
|
24061
|
-
}
|
|
24062
|
-
printSuccess(`Applied ${result.applied.length} migration(s), skipped ${result.alreadyApplied.length}.`);
|
|
24063
|
-
if (result.errors.length > 0) {
|
|
24064
|
-
printError(result.errors.join(`
|
|
24065
|
-
`));
|
|
24066
|
-
process.exit(1);
|
|
24067
|
-
}
|
|
24068
|
-
} catch (e) {
|
|
24069
|
-
printError(e instanceof Error ? e.message : String(e));
|
|
24070
|
-
process.exit(1);
|
|
24071
|
-
}
|
|
24072
|
-
});
|
|
24097
|
+
registerCloudCommands2(program2);
|
|
24073
24098
|
program2.parse();
|