@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/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 existsSync9 } from "fs";
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 (!existsSync9(claudeDir)) {
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 { randomUUID } from "crypto";
14980
- import { readFileSync as readFileSync6, existsSync as existsSync10, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
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/ui.ts
23153
- import chalk from "chalk";
23154
- function printTable(headers, rows) {
23155
- if (rows.length === 0) {
23156
- console.log(chalk.dim(" (no records)"));
23157
- return;
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({ error: `Model not found: ${id}` });
23236
- } else {
23237
- printError(`Model not found: ${id}`);
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
- 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));
23262
- process.exit(1);
23263
- }
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));
23272
- process.exit(1);
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
- 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));
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
- 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);
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
- let datasetPath = opts.dataset;
23388
- if (!datasetPath) {
23389
- const db2 = getDb();
23390
- const [latest] = await db2.select().from(trainingDatasets).orderBy(desc(trainingDatasets.createdAt)).limit(1);
23391
- if (!latest?.filePath) {
23392
- printError("No datasets found. Run 'brains data gather' first.");
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
- datasetPath = latest.filePath;
23396
- printInfo(`Using latest dataset: ${datasetPath} (${latest.exampleCount} examples)`);
23397
- }
23398
- if (!existsSync10(datasetPath)) {
23399
- printError(`Dataset file not found: ${datasetPath}`);
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
- printInfo(`Uploading training file: ${datasetPath} \u2026`);
23403
- let fileId;
23404
- let jobId;
23405
- let jobStatus;
23406
- if (opts.provider === "openai") {
23407
- ({ fileId } = await uploadTrainingFile(datasetPath));
23408
- printSuccess(`File uploaded. fileId = ${fileId}`);
23409
- printInfo(`Creating fine-tune job on OpenAI \u2026`);
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
- console.log();
23473
- }
23474
- const db = getDb();
23475
- const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
23476
- if (model) {
23477
- const status = result.status;
23478
- await db.update(fineTunedModels).set({ status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
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
- } catch (err) {
23481
- printError(err instanceof Error ? err.message : String(err));
23482
- process.exit(1);
23483
- }
23484
- });
23485
- 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) => {
23486
- const intervalMs = Math.max(5, parseInt(opts.interval, 10) || 30) * 1000;
23487
- const terminalStates = new Set(["succeeded", "failed", "cancelled"]);
23488
- printInfo(`Watching job ${jobId} (polling every ${intervalMs / 1000}s) \u2026`);
23489
- console.log();
23490
- const poll = async () => {
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 ts = new Date().toISOString().replace("T", " ").slice(0, 19);
23500
- process.stdout.write(` [${ts}] ${printStatus(result.status)}`);
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
- process.stdout.write(` model: ${result.fineTunedModel}`);
23503
- process.stdout.write(`
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
- await db.update(fineTunedModels).set({ status: result.status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
23481
+ const status = result.status;
23482
+ await db.update(fineTunedModels).set({ status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
23509
23483
  }
23510
- if (terminalStates.has(result.status)) {
23511
- console.log();
23512
- if (result.status === "succeeded") {
23513
- printSuccess(`Job completed successfully.`);
23514
- if (result.fineTunedModel)
23515
- printSuccess(`Fine-tuned model: ${result.fineTunedModel}`);
23516
- } else if (result.status === "failed") {
23517
- printError(`Job failed.${result.error ? " Error: " + result.error : ""}`);
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
- printInfo(`Job ${result.status}.`);
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 true;
23527
+ return false;
23528
+ } catch (err) {
23529
+ printError(err instanceof Error ? err.message : String(err));
23530
+ return false;
23522
23531
  }
23523
- return false;
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
- return false;
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
- if (opts.json) {
23552
- printJson(jobs);
23553
- return;
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
- if (jobs.length === 0) {
23556
- printInfo("No fine-tuning jobs found.");
23557
- return;
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
- printTable(["Job ID", "Model", "Status", "Created"], jobs.map((j) => [
23560
- j.id,
23561
- j.model,
23562
- printStatus(j.status),
23563
- new Date(j.created * 1000).toISOString().split("T")[0] ?? ""
23564
- ]));
23565
- } catch (err) {
23566
- printError(err instanceof Error ? err.message : String(err));
23567
- process.exit(1);
23568
- }
23569
- });
23570
- var DEFAULT_DATASETS_DIR = join13(homedir12(), ".hasna", "brains", "datasets");
23571
- var dataCmd = program2.command("data").description("Manage training datasets");
23572
- 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) => {
23573
- const validSources = ["todos", "mementos", "conversations", "sessions", "all"];
23574
- if (!validSources.includes(opts.source)) {
23575
- printError(`Invalid source: ${opts.source}. Choose one of: ${validSources.join(", ")}`);
23576
- process.exit(1);
23577
- }
23578
- const limit2 = parseInt(opts.limit, 10);
23579
- if (isNaN(limit2) || limit2 <= 0) {
23580
- printError(`Invalid --limit value: ${opts.limit}`);
23581
- process.exit(1);
23582
- }
23583
- try {
23584
- mkdirSync6(opts.output, { recursive: true });
23585
- const sources = opts.source === "all" ? ["todos", "mementos", "conversations", "sessions"] : [opts.source];
23586
- const now = Date.now();
23587
- const db = getDb();
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
- await db.insert(trainingDatasets).values({
23615
- id: randomUUID(),
23616
- source,
23617
- filePath,
23618
- exampleCount: count,
23619
- createdAt: now
23620
- });
23621
- printSuccess(` \u2713 ${count} examples \u2192 ${filePath}`);
23622
- totalExamples += count;
23623
- successfulSources++;
23624
- } catch (sourceErr) {
23625
- printError(` \u2717 ${source}: ${sourceErr instanceof Error ? sourceErr.message : String(sourceErr)}`);
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
- console.log();
23629
- printSuccess(`Total: ${totalExamples} examples from ${successfulSources} source(s)`);
23630
- } catch (err) {
23631
- printError(err instanceof Error ? err.message : String(err));
23632
- process.exit(1);
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
- mkdirSync6(join13(opts.output, "..").replace(/\/\.\.$/, "") || DEFAULT_DATASETS_DIR, { recursive: true });
23680
- const allExamples = [];
23681
- for (const f of files) {
23682
- const lines = readFileSync6(f, "utf8").split(`
23683
- `).map((l) => l.trim()).filter(Boolean);
23684
- allExamples.push(...lines);
23685
- printInfo(` Read ${lines.length} examples from ${f}`);
23686
- }
23687
- let finalLines = allExamples;
23688
- let dupeCount = 0;
23689
- if (opts.dedupe) {
23690
- const seen = new Set;
23691
- finalLines = allExamples.filter((line) => {
23692
- if (seen.has(line)) {
23693
- dupeCount++;
23694
- return false;
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
- writeFileSync5(opts.output, finalLines.join(`
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
- const db = getDb();
23704
- await db.insert(trainingDatasets).values({
23705
- id: randomUUID(),
23706
- source: "mixed",
23707
- filePath: opts.output,
23708
- exampleCount: finalLines.length,
23709
- createdAt: Date.now()
23710
- });
23711
- console.log();
23712
- printSuccess(`Merged ${files.length} files \u2014 ${finalLines.length} examples \u2192 ${opts.output}`);
23713
- if (opts.dedupe && dupeCount > 0)
23714
- printInfo(` Removed ${dupeCount} duplicate(s)`);
23715
- } catch (err) {
23716
- printError(err instanceof Error ? err.message : String(err));
23717
- process.exit(1);
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
- if (datasets.length === 0) {
23729
- printInfo("No datasets found. Use 'brains data gather' to create one.");
23730
- return;
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
- printTable(["ID", "Source", "Examples", "File", "Created"], datasets.map((d) => [
23733
- d.id,
23734
- d.source,
23735
- String(d.exampleCount),
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
- collectionsCmd.command("list").description("List all collections with model counts").option("--json", "Output as JSON").action(async (opts) => {
23775
- await listCollections(opts.json);
23776
- });
23777
- collectionsCmd.command("show <name>").description("List all models in a collection").action(async (name) => {
23778
- try {
23779
- const db = getDb();
23780
- const models = await db.select().from(fineTunedModels).where(eq(fineTunedModels.collection, name));
23781
- if (models.length === 0) {
23782
- printInfo(`No models found in collection '${name}'.`);
23783
- return;
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
- printTable(["ID", "Name", "Provider", "Status", "Base Model"], models.map((m) => [m.id, m.name, m.provider, printStatus(m.status), m.baseModel]));
23786
- } catch (err) {
23787
- printError(err instanceof Error ? err.message : String(err));
23788
- process.exit(1);
23789
- }
23790
- });
23791
- collectionsCmd.command("rename <oldName> <newName>").description("Rename a collection across all models").action(async (oldName, newName) => {
23792
- try {
23793
- const db = getDb();
23794
- const affected = await db.select({ id: fineTunedModels.id }).from(fineTunedModels).where(eq(fineTunedModels.collection, oldName));
23795
- const count = affected.length;
23796
- await db.update(fineTunedModels).set({ collection: newName, updatedAt: Date.now() }).where(eq(fineTunedModels.collection, oldName));
23797
- printSuccess(`Renamed collection '${oldName}' \u2192 '${newName}' (${count} models updated)`);
23798
- } catch (err) {
23799
- printError(err instanceof Error ? err.message : String(err));
23800
- process.exit(1);
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(readFileSync6(join13(import.meta.dir, "../../package.json"), "utf8"));
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
- var cloudCmd = program2.command("cloud").description("Cloud sync commands");
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();