@hasna/brains 0.0.13 → 0.0.16

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
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- function __accessProp(key) {
9
- return this[key];
10
- }
11
- var __toESMCache_node;
12
- var __toESMCache_esm;
13
8
  var __toESM = (mod, isNodeMode, target) => {
14
- var canCache = mod != null && typeof mod === "object";
15
- if (canCache) {
16
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
- var cached = cache.get(mod);
18
- if (cached)
19
- return cached;
20
- }
21
9
  target = mod != null ? __create(__getProtoOf(mod)) : {};
22
10
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
11
  for (let key of __getOwnPropNames(mod))
24
12
  if (!__hasOwnProp.call(to, key))
25
13
  __defProp(to, key, {
26
- get: __accessProp.bind(mod, key),
14
+ get: () => mod[key],
27
15
  enumerable: true
28
16
  });
29
- if (canCache)
30
- cache.set(mod, to);
31
17
  return to;
32
18
  };
33
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
- var __returnValue = (v) => v;
35
- function __exportSetter(name, newValue) {
36
- this[name] = __returnValue.bind(null, newValue);
37
- }
38
20
  var __export = (target, all) => {
39
21
  for (var name in all)
40
22
  __defProp(target, name, {
41
23
  get: all[name],
42
24
  enumerable: true,
43
25
  configurable: true,
44
- set: __exportSetter.bind(all, name)
26
+ set: (newValue) => all[name] = () => newValue
45
27
  });
46
28
  };
47
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -2176,11 +2158,11 @@ import { join as join5 } from "path";
2176
2158
  import { join as join6, dirname } from "path";
2177
2159
  import { existsSync as existsSync6, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync3 } from "fs";
2178
2160
  import { homedir as homedir5, platform } from "os";
2179
- function __accessProp2(key) {
2161
+ function __accessProp(key) {
2180
2162
  return this[key];
2181
2163
  }
2182
- function __exportSetter2(name, newValue) {
2183
- this[name] = __returnValue2.bind(null, newValue);
2164
+ function __exportSetter(name, newValue) {
2165
+ this[name] = __returnValue.bind(null, newValue);
2184
2166
  }
2185
2167
  function translateSql(sql2, dialect) {
2186
2168
  if (dialect === "sqlite")
@@ -4816,10 +4798,10 @@ function registerCloudCommands(program2, serviceName) {
4816
4798
  }
4817
4799
  });
4818
4800
  }
4819
- var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node2, __toESMCache_esm2, __toESM2 = (mod, isNodeMode, target) => {
4801
+ var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __toESMCache_node, __toESMCache_esm, __toESM2 = (mod, isNodeMode, target) => {
4820
4802
  var canCache = mod != null && typeof mod === "object";
4821
4803
  if (canCache) {
4822
- var cache = isNodeMode ? __toESMCache_node2 ??= new WeakMap : __toESMCache_esm2 ??= new WeakMap;
4804
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
4823
4805
  var cached = cache.get(mod);
4824
4806
  if (cached)
4825
4807
  return cached;
@@ -4829,19 +4811,19 @@ var __create2, __getProtoOf2, __defProp2, __getOwnPropNames2, __hasOwnProp2, __t
4829
4811
  for (let key of __getOwnPropNames2(mod))
4830
4812
  if (!__hasOwnProp2.call(to, key))
4831
4813
  __defProp2(to, key, {
4832
- get: __accessProp2.bind(mod, key),
4814
+ get: __accessProp.bind(mod, key),
4833
4815
  enumerable: true
4834
4816
  });
4835
4817
  if (canCache)
4836
4818
  cache.set(mod, to);
4837
4819
  return to;
4838
- }, __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue2 = (v) => v, __export2 = (target, all) => {
4820
+ }, __commonJS2 = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports), __returnValue = (v) => v, __export2 = (target, all) => {
4839
4821
  for (var name in all)
4840
4822
  __defProp2(target, name, {
4841
4823
  get: all[name],
4842
4824
  enumerable: true,
4843
4825
  configurable: true,
4844
- set: __exportSetter2.bind(all, name)
4826
+ set: __exportSetter.bind(all, name)
4845
4827
  });
4846
4828
  }, __esm2 = (fn, res) => () => (fn && (res = fn(fn = 0)), res), __require2, require_postgres_array, require_arrayParser, require_postgres_date, require_mutable, require_postgres_interval, require_postgres_bytea, require_textParsers, require_pg_int8, require_binaryParsers, require_builtins, require_pg_types, require_defaults, require_utils, require_utils_legacy, require_utils_webcrypto, require_utils2, require_cert_signatures, require_sasl, require_type_overrides, require_pg_connection_string, require_connection_parameters, require_result, require_query, require_messages, require_buffer_writer, require_serializer, require_buffer_reader, require_parser, require_dist, require_empty, require_stream, require_connection, require_split2, require_helper, require_lib, require_client, require_pg_pool, require_query2, require_client2, require_lib2, import_lib, Client, Pool, Connection, types, Query, DatabaseError, escapeIdentifier, escapeLiteral, Result, TypeOverrides, defaults, esm_default, init_esm, init_adapter, util, objectUtil, ZodParsedType, getParsedType = (data) => {
4847
4829
  const t = typeof data;
@@ -13434,7 +13416,7 @@ __export(exports_sessions2, {
13434
13416
  gatherFromSessions: () => gatherFromSessions
13435
13417
  });
13436
13418
  import { readdir, readFile, stat } from "fs/promises";
13437
- import { existsSync as existsSync9 } from "fs";
13419
+ import { existsSync as existsSync10 } from "fs";
13438
13420
  import { join as join12 } from "path";
13439
13421
  import { homedir as homedir11 } from "os";
13440
13422
  function extractText(content) {
@@ -13447,7 +13429,7 @@ async function gatherFromSessions(options = {}) {
13447
13429
  const { limit: limit2 = 1000 } = options;
13448
13430
  const examples = [];
13449
13431
  const claudeDir = join12(homedir11(), ".claude", "projects");
13450
- if (!existsSync9(claudeDir)) {
13432
+ if (!existsSync10(claudeDir)) {
13451
13433
  return { source: "sessions", examples: [], count: 0 };
13452
13434
  }
13453
13435
  const projectDirs = await readdir(claudeDir).catch(() => []);
@@ -14976,10 +14958,8 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
14976
14958
  }
14977
14959
 
14978
14960
  // 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";
14961
+ import { readFileSync as readFileSync7 } from "fs";
14962
+ import { join as join14 } from "path";
14983
14963
 
14984
14964
  // node_modules/drizzle-orm/bun-sqlite/driver.js
14985
14965
  import { Database } from "bun:sqlite";
@@ -17402,6 +17382,57 @@ function getRawDb(dbPath) {
17402
17382
  return dbPath ? new SqliteAdapter(dbPath) : createDatabase({ service: "brains" });
17403
17383
  }
17404
17384
 
17385
+ // src/cli/ui.ts
17386
+ import chalk from "chalk";
17387
+ function printTable(headers, rows) {
17388
+ if (rows.length === 0) {
17389
+ console.log(chalk.dim(" (no records)"));
17390
+ return;
17391
+ }
17392
+ const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
17393
+ const separator = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
17394
+ const headerLine = headers.map((h, i) => ` ${chalk.bold(h.padEnd(colWidths[i] ?? 0))} `).join("\u2502");
17395
+ const topBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C");
17396
+ const midBorder = separator;
17397
+ const bottomBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534");
17398
+ console.log("\u250C" + topBorder + "\u2510");
17399
+ console.log("\u2502" + headerLine + "\u2502");
17400
+ console.log("\u251C" + midBorder + "\u2524");
17401
+ for (const row of rows) {
17402
+ const rowLine = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i] ?? 0)} `).join("\u2502");
17403
+ console.log("\u2502" + rowLine + "\u2502");
17404
+ }
17405
+ console.log("\u2514" + bottomBorder + "\u2518");
17406
+ }
17407
+ var STATUS_COLORS = {
17408
+ succeeded: chalk.green,
17409
+ running: chalk.cyan,
17410
+ pending: chalk.yellow,
17411
+ failed: chalk.red,
17412
+ cancelled: chalk.dim,
17413
+ queued: chalk.yellow,
17414
+ validating_files: chalk.blue
17415
+ };
17416
+ function printStatus(status) {
17417
+ const colorFn = STATUS_COLORS[status] ?? chalk.white;
17418
+ return colorFn(`\u25CF ${status}`);
17419
+ }
17420
+ function printJson(obj) {
17421
+ console.log(JSON.stringify(obj, null, 2));
17422
+ }
17423
+ function printError(message) {
17424
+ console.error(chalk.red("\u2717 Error: ") + message);
17425
+ }
17426
+ function printSuccess(message) {
17427
+ console.log(chalk.green("\u2713 ") + message);
17428
+ }
17429
+ function printInfo(message) {
17430
+ console.log(chalk.dim(" " + message));
17431
+ }
17432
+
17433
+ // src/cli/commands/models.ts
17434
+ import { randomUUID } from "crypto";
17435
+
17405
17436
  // node_modules/openai/internal/qs/formats.mjs
17406
17437
  var default_format = "RFC3986";
17407
17438
  var formatters = {
@@ -20866,11 +20897,11 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
20866
20897
  }
20867
20898
  return this._addChatCompletion(__classPrivateFieldGet6(this, _ChatCompletionStream_instances, "m", _ChatCompletionStream_endRequest).call(this));
20868
20899
  }
20869
- [(_ChatCompletionStream_params = new WeakMap, _ChatCompletionStream_choiceEventStates = new WeakMap, _ChatCompletionStream_currentChatCompletionSnapshot = new WeakMap, _ChatCompletionStream_instances = new WeakSet, _ChatCompletionStream_beginRequest = function _ChatCompletionStream_beginRequest2() {
20900
+ [(_ChatCompletionStream_params = new WeakMap, _ChatCompletionStream_choiceEventStates = new WeakMap, _ChatCompletionStream_currentChatCompletionSnapshot = new WeakMap, _ChatCompletionStream_instances = new WeakSet, _ChatCompletionStream_beginRequest = function _ChatCompletionStream_beginRequest() {
20870
20901
  if (this.ended)
20871
20902
  return;
20872
20903
  __classPrivateFieldSet5(this, _ChatCompletionStream_currentChatCompletionSnapshot, undefined, "f");
20873
- }, _ChatCompletionStream_getChoiceEventState = function _ChatCompletionStream_getChoiceEventState2(choice) {
20904
+ }, _ChatCompletionStream_getChoiceEventState = function _ChatCompletionStream_getChoiceEventState(choice) {
20874
20905
  let state = __classPrivateFieldGet6(this, _ChatCompletionStream_choiceEventStates, "f")[choice.index];
20875
20906
  if (state) {
20876
20907
  return state;
@@ -20885,7 +20916,7 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
20885
20916
  };
20886
20917
  __classPrivateFieldGet6(this, _ChatCompletionStream_choiceEventStates, "f")[choice.index] = state;
20887
20918
  return state;
20888
- }, _ChatCompletionStream_addChunk = function _ChatCompletionStream_addChunk2(chunk) {
20919
+ }, _ChatCompletionStream_addChunk = function _ChatCompletionStream_addChunk(chunk) {
20889
20920
  if (this.ended)
20890
20921
  return;
20891
20922
  const completion = __classPrivateFieldGet6(this, _ChatCompletionStream_instances, "m", _ChatCompletionStream_accumulateChatCompletion).call(this, chunk);
@@ -20952,7 +20983,7 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
20952
20983
  }
20953
20984
  }
20954
20985
  }
20955
- }, _ChatCompletionStream_emitToolCallDoneEvent = function _ChatCompletionStream_emitToolCallDoneEvent2(choiceSnapshot, toolCallIndex) {
20986
+ }, _ChatCompletionStream_emitToolCallDoneEvent = function _ChatCompletionStream_emitToolCallDoneEvent(choiceSnapshot, toolCallIndex) {
20956
20987
  const state = __classPrivateFieldGet6(this, _ChatCompletionStream_instances, "m", _ChatCompletionStream_getChoiceEventState).call(this, choiceSnapshot);
20957
20988
  if (state.done_tool_calls.has(toolCallIndex)) {
20958
20989
  return;
@@ -20975,7 +21006,7 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
20975
21006
  } else {
20976
21007
  assertNever2(toolCallSnapshot.type);
20977
21008
  }
20978
- }, _ChatCompletionStream_emitContentDoneEvents = function _ChatCompletionStream_emitContentDoneEvents2(choiceSnapshot) {
21009
+ }, _ChatCompletionStream_emitContentDoneEvents = function _ChatCompletionStream_emitContentDoneEvents(choiceSnapshot) {
20979
21010
  const state = __classPrivateFieldGet6(this, _ChatCompletionStream_instances, "m", _ChatCompletionStream_getChoiceEventState).call(this, choiceSnapshot);
20980
21011
  if (choiceSnapshot.message.content && !state.content_done) {
20981
21012
  state.content_done = true;
@@ -20997,7 +21028,7 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
20997
21028
  state.logprobs_refusal_done = true;
20998
21029
  this._emit("logprobs.refusal.done", { refusal: choiceSnapshot.logprobs.refusal });
20999
21030
  }
21000
- }, _ChatCompletionStream_endRequest = function _ChatCompletionStream_endRequest2() {
21031
+ }, _ChatCompletionStream_endRequest = function _ChatCompletionStream_endRequest() {
21001
21032
  if (this.ended) {
21002
21033
  throw new OpenAIError(`stream has ended, this shouldn't happen`);
21003
21034
  }
@@ -21008,13 +21039,13 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
21008
21039
  __classPrivateFieldSet5(this, _ChatCompletionStream_currentChatCompletionSnapshot, undefined, "f");
21009
21040
  __classPrivateFieldSet5(this, _ChatCompletionStream_choiceEventStates, [], "f");
21010
21041
  return finalizeChatCompletion(snapshot, __classPrivateFieldGet6(this, _ChatCompletionStream_params, "f"));
21011
- }, _ChatCompletionStream_getAutoParseableResponseFormat = function _ChatCompletionStream_getAutoParseableResponseFormat2() {
21042
+ }, _ChatCompletionStream_getAutoParseableResponseFormat = function _ChatCompletionStream_getAutoParseableResponseFormat() {
21012
21043
  const responseFormat = __classPrivateFieldGet6(this, _ChatCompletionStream_params, "f")?.response_format;
21013
21044
  if (isAutoParsableResponseFormat(responseFormat)) {
21014
21045
  return responseFormat;
21015
21046
  }
21016
21047
  return null;
21017
- }, _ChatCompletionStream_accumulateChatCompletion = function _ChatCompletionStream_accumulateChatCompletion2(chunk) {
21048
+ }, _ChatCompletionStream_accumulateChatCompletion = function _ChatCompletionStream_accumulateChatCompletion(chunk) {
21018
21049
  var _a, _b, _c, _d;
21019
21050
  let snapshot = __classPrivateFieldGet6(this, _ChatCompletionStream_currentChatCompletionSnapshot, "f");
21020
21051
  const { choices, ...rest } = chunk;
@@ -22240,11 +22271,11 @@ class ResponseStream extends EventStream {
22240
22271
  }
22241
22272
  return __classPrivateFieldGet7(this, _ResponseStream_instances, "m", _ResponseStream_endRequest).call(this);
22242
22273
  }
22243
- [(_ResponseStream_params = new WeakMap, _ResponseStream_currentResponseSnapshot = new WeakMap, _ResponseStream_finalResponse = new WeakMap, _ResponseStream_instances = new WeakSet, _ResponseStream_beginRequest = function _ResponseStream_beginRequest2() {
22274
+ [(_ResponseStream_params = new WeakMap, _ResponseStream_currentResponseSnapshot = new WeakMap, _ResponseStream_finalResponse = new WeakMap, _ResponseStream_instances = new WeakSet, _ResponseStream_beginRequest = function _ResponseStream_beginRequest() {
22244
22275
  if (this.ended)
22245
22276
  return;
22246
22277
  __classPrivateFieldSet6(this, _ResponseStream_currentResponseSnapshot, undefined, "f");
22247
- }, _ResponseStream_addEvent = function _ResponseStream_addEvent2(event, starting_after) {
22278
+ }, _ResponseStream_addEvent = function _ResponseStream_addEvent(event, starting_after) {
22248
22279
  if (this.ended)
22249
22280
  return;
22250
22281
  const maybeEmit = (name, event2) => {
@@ -22292,7 +22323,7 @@ class ResponseStream extends EventStream {
22292
22323
  maybeEmit(event.type, event);
22293
22324
  break;
22294
22325
  }
22295
- }, _ResponseStream_endRequest = function _ResponseStream_endRequest2() {
22326
+ }, _ResponseStream_endRequest = function _ResponseStream_endRequest() {
22296
22327
  if (this.ended) {
22297
22328
  throw new OpenAIError(`stream has ended, this shouldn't happen`);
22298
22329
  }
@@ -22304,7 +22335,7 @@ class ResponseStream extends EventStream {
22304
22335
  const parsedResponse = finalizeResponse(snapshot, __classPrivateFieldGet7(this, _ResponseStream_params, "f"));
22305
22336
  __classPrivateFieldSet6(this, _ResponseStream_finalResponse, parsedResponse, "f");
22306
22337
  return parsedResponse;
22307
- }, _ResponseStream_accumulateResponse = function _ResponseStream_accumulateResponse2(event) {
22338
+ }, _ResponseStream_accumulateResponse = function _ResponseStream_accumulateResponse(event) {
22308
22339
  let snapshot = __classPrivateFieldGet7(this, _ResponseStream_currentResponseSnapshot, "f");
22309
22340
  if (!snapshot) {
22310
22341
  if (event.type !== "response.created") {
@@ -23149,345 +23180,263 @@ class ThinkerLabsProvider {
23149
23180
  }
23150
23181
  }
23151
23182
 
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) {
23183
+ // src/cli/commands/models.ts
23184
+ function registerModelsCommands(program2) {
23185
+ const modelsCmd = program2.command("models").description("Manage tracked fine-tuned models");
23186
+ modelsCmd.command("list").description("List all tracked fine-tuned models").option("--json", "Output as JSON").action(async (opts) => {
23187
+ try {
23188
+ const db = getDb();
23189
+ const models = await db.select().from(fineTunedModels);
23234
23190
  if (opts.json) {
23235
- printJson({ error: `Model not found: ${id}` });
23236
- } else {
23237
- printError(`Model not found: ${id}`);
23191
+ printJson(models);
23192
+ return;
23193
+ }
23194
+ if (models.length === 0) {
23195
+ printInfo("No models tracked yet. Use 'brains finetune start' to train one.");
23196
+ return;
23238
23197
  }
23198
+ printTable(["ID", "Display Name", "Provider", "Status", "Collection", "Base Model"], models.map((m) => [
23199
+ m.id,
23200
+ m.displayName ?? m.name,
23201
+ m.provider,
23202
+ printStatus(m.status),
23203
+ m.collection ?? "",
23204
+ m.baseModel
23205
+ ]));
23206
+ } catch (err) {
23207
+ printError(err instanceof Error ? err.message : String(err));
23239
23208
  process.exit(1);
23240
23209
  }
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}`);
23210
+ });
23211
+ modelsCmd.command("show <id>").description("Show details of a specific model").option("--json", "Output as JSON").action(async (id, opts) => {
23212
+ try {
23213
+ const db = getDb();
23214
+ const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
23215
+ if (!model) {
23216
+ if (opts.json) {
23217
+ printJson({ error: `Model not found: ${id}` });
23218
+ } else {
23219
+ printError(`Model not found: ${id}`);
23220
+ }
23221
+ process.exit(1);
23222
+ }
23223
+ if (opts.json) {
23224
+ printJson(model);
23225
+ return;
23226
+ }
23227
+ console.log();
23228
+ const tagsList = model.tags ? JSON.parse(model.tags).join(", ") : "(none)";
23229
+ console.log(` ID: ${model.id}`);
23230
+ console.log(` Name: ${model.name}`);
23231
+ console.log(` Display Name: ${model.displayName ?? "(none)"}`);
23232
+ console.log(` Description: ${model.description ?? "(none)"}`);
23233
+ console.log(` Collection: ${model.collection ?? "(none)"}`);
23234
+ console.log(` Tags: ${tagsList}`);
23235
+ console.log(` Provider: ${model.provider}`);
23236
+ console.log(` Status: ${printStatus(model.status)}`);
23237
+ console.log(` Base Model: ${model.baseModel}`);
23238
+ console.log(` Job ID: ${model.fineTuneJobId ?? "(none)"}`);
23239
+ console.log(` Created: ${new Date(model.createdAt).toISOString()}`);
23240
+ console.log(` Updated: ${new Date(model.updatedAt).toISOString()}`);
23241
+ console.log();
23242
+ } catch (err) {
23243
+ printError(err instanceof Error ? err.message : String(err));
23291
23244
  process.exit(1);
23292
23245
  }
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}`);
23246
+ });
23247
+ modelsCmd.command("rename <id> <displayName>").description("Set the display name of a model").action(async (id, displayName) => {
23248
+ try {
23249
+ const db = getDb();
23250
+ await db.update(fineTunedModels).set({ displayName, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
23251
+ printSuccess(`Display name set to "${displayName}"`);
23252
+ } catch (err) {
23253
+ printError(err instanceof Error ? err.message : String(err));
23310
23254
  process.exit(1);
23311
23255
  }
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'.`);
23256
+ });
23257
+ modelsCmd.command("describe <id> <description>").description("Set the description of a model").action(async (id, description) => {
23258
+ try {
23259
+ const db = getDb();
23260
+ await db.update(fineTunedModels).set({ description, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
23261
+ printSuccess(`Description updated.`);
23262
+ } catch (err) {
23263
+ printError(err instanceof Error ? err.message : String(err));
23385
23264
  process.exit(1);
23386
23265
  }
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.");
23266
+ });
23267
+ modelsCmd.command("tag <id> <tag>").description("Add a tag to a model").action(async (id, tag) => {
23268
+ try {
23269
+ const db = getDb();
23270
+ const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
23271
+ if (!model) {
23272
+ printError(`Model not found: ${id}`);
23393
23273
  process.exit(1);
23394
23274
  }
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}`);
23275
+ const existing = model.tags ? JSON.parse(model.tags) : [];
23276
+ if (!existing.includes(tag)) {
23277
+ existing.push(tag);
23278
+ }
23279
+ await db.update(fineTunedModels).set({ tags: JSON.stringify(existing), updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
23280
+ printSuccess(`Tag "${tag}" added. Tags: ${existing.join(", ")}`);
23281
+ } catch (err) {
23282
+ printError(err instanceof Error ? err.message : String(err));
23400
23283
  process.exit(1);
23401
23284
  }
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));
23285
+ });
23286
+ modelsCmd.command("untag <id> <tag>").description("Remove a tag from a model").action(async (id, tag) => {
23287
+ try {
23288
+ const db = getDb();
23289
+ const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
23290
+ if (!model) {
23291
+ printError(`Model not found: ${id}`);
23292
+ process.exit(1);
23293
+ }
23294
+ const existing = model.tags ? JSON.parse(model.tags) : [];
23295
+ const updated = existing.filter((t) => t !== tag);
23296
+ await db.update(fineTunedModels).set({ tags: JSON.stringify(updated), updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
23297
+ printSuccess(`Tag "${tag}" removed. Tags: ${updated.join(", ") || "(none)"}`);
23298
+ } catch (err) {
23299
+ printError(err instanceof Error ? err.message : String(err));
23300
+ process.exit(1);
23417
23301
  }
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);
23302
+ });
23303
+ modelsCmd.command("collection <id> <collectionName>").description("Set the collection of a model").action(async (id, collectionName) => {
23304
+ try {
23305
+ const db = getDb();
23306
+ await db.update(fineTunedModels).set({ collection: collectionName, updatedAt: Date.now() }).where(eq(fineTunedModels.id, id));
23307
+ printSuccess(`Collection set to "${collectionName}"`);
23308
+ } catch (err) {
23309
+ printError(err instanceof Error ? err.message : String(err));
23310
+ process.exit(1);
23459
23311
  }
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}`);
23312
+ });
23313
+ 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) => {
23314
+ try {
23315
+ let result;
23316
+ if (opts.provider === "openai") {
23317
+ result = await getFineTuneStatus(jobId);
23318
+ } else {
23319
+ const tl = new ThinkerLabsProvider;
23320
+ result = await tl.getFineTuneStatus(jobId);
23468
23321
  }
23469
- if (result.error) {
23470
- console.log(` Error: ${result.error}`);
23322
+ const db = getDb();
23323
+ const [existing] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
23324
+ if (existing) {
23325
+ printInfo(`Model already tracked as: ${existing.id}`);
23326
+ return;
23471
23327
  }
23328
+ const modelId = randomUUID();
23329
+ const now = Date.now();
23330
+ const name = opts.name ?? result.fineTunedModel ?? `imported-${jobId}`;
23331
+ await db.insert(fineTunedModels).values({
23332
+ id: modelId,
23333
+ name,
23334
+ provider: opts.provider,
23335
+ baseModel: result.baseModel ?? "unknown",
23336
+ status: result.status,
23337
+ fineTuneJobId: jobId,
23338
+ createdAt: now,
23339
+ updatedAt: now
23340
+ });
23341
+ await db.insert(trainingJobs).values({
23342
+ id: randomUUID(),
23343
+ modelId,
23344
+ provider: opts.provider,
23345
+ status: result.status,
23346
+ startedAt: now
23347
+ });
23348
+ printSuccess(`Model imported successfully.`);
23472
23349
  console.log();
23350
+ console.log(` Local ID: ${modelId}`);
23351
+ console.log(` Job ID: ${jobId}`);
23352
+ console.log(` Name: ${name}`);
23353
+ console.log(` Status: ${printStatus(result.status)}`);
23354
+ if (result.fineTunedModel)
23355
+ console.log(` Model: ${result.fineTunedModel}`);
23356
+ console.log();
23357
+ } catch (err) {
23358
+ printError(err instanceof Error ? err.message : String(err));
23359
+ process.exit(1);
23473
23360
  }
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));
23361
+ });
23362
+ }
23363
+
23364
+ // src/cli/commands/finetune.ts
23365
+ import { randomUUID as randomUUID2 } from "crypto";
23366
+ import { existsSync as existsSync9 } from "fs";
23367
+ function registerFinetuneCommands(program2) {
23368
+ const finetuneCmd = program2.command("finetune").description("Manage fine-tuning jobs");
23369
+ 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) => {
23370
+ try {
23371
+ if (opts.provider !== "openai" && opts.provider !== "thinker-labs") {
23372
+ printError(`Unknown provider: ${opts.provider}. Use 'openai' or 'thinker-labs'.`);
23373
+ process.exit(1);
23374
+ }
23375
+ let datasetPath = opts.dataset;
23376
+ if (!datasetPath) {
23377
+ const db2 = getDb();
23378
+ const [latest] = await db2.select().from(trainingDatasets).orderBy(desc(trainingDatasets.createdAt)).limit(1);
23379
+ if (!latest?.filePath) {
23380
+ printError("No datasets found. Run 'brains data gather' first.");
23381
+ process.exit(1);
23382
+ }
23383
+ datasetPath = latest.filePath;
23384
+ printInfo(`Using latest dataset: ${datasetPath} (${latest.exampleCount} examples)`);
23385
+ }
23386
+ if (!existsSync9(datasetPath)) {
23387
+ printError(`Dataset file not found: ${datasetPath}`);
23388
+ process.exit(1);
23389
+ }
23390
+ printInfo(`Uploading training file: ${datasetPath} \u2026`);
23391
+ let fileId;
23392
+ let jobId;
23393
+ let jobStatus;
23394
+ if (opts.provider === "openai") {
23395
+ ({ fileId } = await uploadTrainingFile(datasetPath));
23396
+ printSuccess(`File uploaded. fileId = ${fileId}`);
23397
+ printInfo(`Creating fine-tune job on OpenAI \u2026`);
23398
+ ({ jobId, status: jobStatus } = await createFineTuneJob(fileId, opts.baseModel, opts.name));
23399
+ } else {
23400
+ const tl = new ThinkerLabsProvider;
23401
+ ({ fileId } = await tl.uploadTrainingFile(datasetPath));
23402
+ printSuccess(`File uploaded. fileId = ${fileId}`);
23403
+ printInfo(`Creating fine-tune job on Thinker Labs \u2026`);
23404
+ ({ jobId, status: jobStatus } = await tl.createFineTuneJob(fileId, opts.baseModel, opts.name));
23405
+ }
23406
+ const db = getDb();
23407
+ const modelId = randomUUID2();
23408
+ const now = Date.now();
23409
+ await db.insert(fineTunedModels).values({
23410
+ id: modelId,
23411
+ name: opts.name,
23412
+ provider: opts.provider,
23413
+ baseModel: opts.baseModel,
23414
+ status: "running",
23415
+ fineTuneJobId: jobId,
23416
+ createdAt: now,
23417
+ updatedAt: now
23418
+ });
23419
+ const trainingJobId = randomUUID2();
23420
+ await db.insert(trainingJobs).values({
23421
+ id: trainingJobId,
23422
+ modelId,
23423
+ provider: opts.provider,
23424
+ status: jobStatus,
23425
+ startedAt: now
23426
+ });
23427
+ printSuccess(`Fine-tune job started!`);
23428
+ console.log();
23429
+ console.log(` Model ID: ${modelId}`);
23430
+ console.log(` Job ID: ${jobId}`);
23431
+ console.log(` Status: ${printStatus(jobStatus)}`);
23432
+ console.log();
23433
+ printInfo(`Use 'brains finetune status ${jobId}' to check progress.`);
23434
+ } catch (err) {
23435
+ printError(err instanceof Error ? err.message : String(err));
23436
+ process.exit(1);
23479
23437
  }
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 () => {
23438
+ });
23439
+ 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) => {
23491
23440
  try {
23492
23441
  let result;
23493
23442
  if (opts.provider === "openai") {
@@ -23496,255 +23445,299 @@ finetuneCmd.command("watch <job-id>").description("Poll a fine-tuning job until
23496
23445
  const tl = new ThinkerLabsProvider;
23497
23446
  result = await tl.getFineTuneStatus(jobId);
23498
23447
  }
23499
- const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
23500
- process.stdout.write(` [${ts}] ${printStatus(result.status)}`);
23501
- if (result.fineTunedModel)
23502
- process.stdout.write(` model: ${result.fineTunedModel}`);
23503
- process.stdout.write(`
23504
- `);
23448
+ if (opts.json) {
23449
+ printJson(result);
23450
+ } else {
23451
+ console.log();
23452
+ console.log(` Job ID: ${result.jobId}`);
23453
+ console.log(` Status: ${printStatus(result.status)}`);
23454
+ if (result.fineTunedModel)
23455
+ console.log(` Fine-tuned model: ${result.fineTunedModel}`);
23456
+ if (result.error)
23457
+ console.log(` Error: ${result.error}`);
23458
+ console.log();
23459
+ }
23505
23460
  const db = getDb();
23506
23461
  const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
23507
23462
  if (model) {
23508
- await db.update(fineTunedModels).set({ status: result.status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
23463
+ const status = result.status;
23464
+ await db.update(fineTunedModels).set({ status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
23509
23465
  }
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 : ""}`);
23466
+ } catch (err) {
23467
+ printError(err instanceof Error ? err.message : String(err));
23468
+ process.exit(1);
23469
+ }
23470
+ });
23471
+ 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) => {
23472
+ const intervalMs = Math.max(5, parseInt(opts.interval, 10) || 30) * 1000;
23473
+ const terminalStates = new Set(["succeeded", "failed", "cancelled"]);
23474
+ printInfo(`Watching job ${jobId} (polling every ${intervalMs / 1000}s) \u2026`);
23475
+ console.log();
23476
+ const poll = async () => {
23477
+ try {
23478
+ let result;
23479
+ if (opts.provider === "openai") {
23480
+ result = await getFineTuneStatus(jobId);
23518
23481
  } else {
23519
- printInfo(`Job ${result.status}.`);
23482
+ const tl = new ThinkerLabsProvider;
23483
+ result = await tl.getFineTuneStatus(jobId);
23484
+ }
23485
+ const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
23486
+ process.stdout.write(` [${ts}] ${printStatus(result.status)}`);
23487
+ if (result.fineTunedModel)
23488
+ process.stdout.write(` model: ${result.fineTunedModel}`);
23489
+ process.stdout.write(`
23490
+ `);
23491
+ const db = getDb();
23492
+ const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
23493
+ if (model) {
23494
+ await db.update(fineTunedModels).set({ status: result.status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
23495
+ }
23496
+ if (terminalStates.has(result.status)) {
23497
+ console.log();
23498
+ if (result.status === "succeeded") {
23499
+ printSuccess(`Job completed successfully.`);
23500
+ if (result.fineTunedModel)
23501
+ printSuccess(`Fine-tuned model: ${result.fineTunedModel}`);
23502
+ } else if (result.status === "failed") {
23503
+ printError(`Job failed.${result.error ? " Error: " + result.error : ""}`);
23504
+ } else {
23505
+ printInfo(`Job ${result.status}.`);
23506
+ }
23507
+ return true;
23520
23508
  }
23521
- return true;
23509
+ return false;
23510
+ } catch (err) {
23511
+ printError(err instanceof Error ? err.message : String(err));
23512
+ return false;
23522
23513
  }
23523
- return false;
23514
+ };
23515
+ const done = await poll();
23516
+ if (!done) {
23517
+ await new Promise((resolve2) => {
23518
+ const timer = setInterval(async () => {
23519
+ const finished = await poll();
23520
+ if (finished) {
23521
+ clearInterval(timer);
23522
+ resolve2();
23523
+ }
23524
+ }, intervalMs);
23525
+ });
23526
+ }
23527
+ });
23528
+ 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) => {
23529
+ try {
23530
+ let jobs;
23531
+ if (opts.provider === "openai") {
23532
+ jobs = await listFineTunedModels();
23533
+ } else {
23534
+ const tl = new ThinkerLabsProvider;
23535
+ jobs = await tl.listFineTunedModels();
23536
+ }
23537
+ if (opts.json) {
23538
+ printJson(jobs);
23539
+ return;
23540
+ }
23541
+ if (jobs.length === 0) {
23542
+ printInfo("No fine-tuning jobs found.");
23543
+ return;
23544
+ }
23545
+ printTable(["Job ID", "Model", "Status", "Created"], jobs.map((j) => [
23546
+ j.id,
23547
+ j.model,
23548
+ printStatus(j.status),
23549
+ new Date(j.created * 1000).toISOString().split("T")[0] ?? ""
23550
+ ]));
23524
23551
  } catch (err) {
23525
23552
  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();
23553
+ process.exit(1);
23550
23554
  }
23551
- if (opts.json) {
23552
- printJson(jobs);
23553
- return;
23555
+ });
23556
+ }
23557
+
23558
+ // src/cli/commands/data.ts
23559
+ import { randomUUID as randomUUID3 } from "crypto";
23560
+ import { readFileSync as readFileSync6, existsSync as existsSync11, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
23561
+ import { join as join13 } from "path";
23562
+ import { homedir as homedir12 } from "os";
23563
+ var DEFAULT_DATASETS_DIR = join13(homedir12(), ".hasna", "brains", "datasets");
23564
+ function registerDataCommands(program2) {
23565
+ const dataCmd = program2.command("data").description("Manage training datasets");
23566
+ 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) => {
23567
+ const validSources = ["todos", "mementos", "conversations", "sessions", "all"];
23568
+ if (!validSources.includes(opts.source)) {
23569
+ printError(`Invalid source: ${opts.source}. Choose one of: ${validSources.join(", ")}`);
23570
+ process.exit(1);
23554
23571
  }
23555
- if (jobs.length === 0) {
23556
- printInfo("No fine-tuning jobs found.");
23557
- return;
23572
+ const limit2 = parseInt(opts.limit, 10);
23573
+ if (isNaN(limit2) || limit2 <= 0) {
23574
+ printError(`Invalid --limit value: ${opts.limit}`);
23575
+ process.exit(1);
23558
23576
  }
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(`
23577
+ try {
23578
+ mkdirSync6(opts.output, { recursive: true });
23579
+ const sources = opts.source === "all" ? ["todos", "mementos", "conversations", "sessions"] : [opts.source];
23580
+ const now = Date.now();
23581
+ const db = getDb();
23582
+ const gathererMap = {
23583
+ todos: (o) => Promise.resolve().then(() => (init_todos(), exports_todos)).then((m) => m.gatherFromTodos(o)),
23584
+ mementos: (o) => Promise.resolve().then(() => (init_mementos(), exports_mementos)).then((m) => m.gatherFromMementos(o)),
23585
+ conversations: (o) => Promise.resolve().then(() => (init_conversations(), exports_conversations)).then((m) => m.gatherFromConversations(o)),
23586
+ sessions: (o) => Promise.resolve().then(() => (init_sessions(), exports_sessions2)).then((m) => m.gatherFromSessions(o))
23587
+ };
23588
+ let totalExamples = 0;
23589
+ let successfulSources = 0;
23590
+ for (const source of sources) {
23591
+ printInfo(`Gathering from ${source} \u2026`);
23592
+ try {
23593
+ const gatherer = gathererMap[source];
23594
+ if (!gatherer) {
23595
+ printError(` Unknown source: ${source}`);
23596
+ continue;
23597
+ }
23598
+ const { examples, count } = await gatherer({ limit: limit2 });
23599
+ if (count === 0) {
23600
+ printInfo(` No examples found in ${source}.`);
23601
+ continue;
23602
+ }
23603
+ const fileName = `${source}-${now}.jsonl`;
23604
+ const filePath = join13(opts.output, fileName);
23605
+ writeFileSync5(filePath, examples.map((e) => JSON.stringify(e)).join(`
23612
23606
  `) + `
23613
23607
  `, "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)}`);
23608
+ await db.insert(trainingDatasets).values({
23609
+ id: randomUUID3(),
23610
+ source,
23611
+ filePath,
23612
+ exampleCount: count,
23613
+ createdAt: now
23614
+ });
23615
+ printSuccess(` \u2713 ${count} examples \u2192 ${filePath}`);
23616
+ totalExamples += count;
23617
+ successfulSources++;
23618
+ } catch (sourceErr) {
23619
+ printError(` \u2717 ${source}: ${sourceErr instanceof Error ? sourceErr.message : String(sourceErr)}`);
23620
+ }
23626
23621
  }
23622
+ console.log();
23623
+ printSuccess(`Total: ${totalExamples} examples from ${successfulSources} source(s)`);
23624
+ } catch (err) {
23625
+ printError(err instanceof Error ? err.message : String(err));
23626
+ process.exit(1);
23627
23627
  }
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
- }
23628
+ });
23629
+ dataCmd.command("preview <file>").description("Preview a JSONL training file").option("-n, --count <n>", "Number of examples to show", "5").action((file, opts) => {
23630
+ if (!existsSync11(file)) {
23631
+ printError(`File not found: ${file}`);
23632
+ process.exit(1);
23678
23633
  }
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;
23634
+ const n = parseInt(opts.count, 10);
23635
+ if (isNaN(n) || n <= 0) {
23636
+ printError(`Invalid --count value: ${opts.count}`);
23637
+ process.exit(1);
23638
+ }
23639
+ try {
23640
+ const content = readFileSync6(file, "utf8");
23641
+ const lines = content.trim().split(`
23642
+ `).filter(Boolean);
23643
+ const total = lines.length;
23644
+ const preview = lines.slice(0, n);
23645
+ console.log();
23646
+ printInfo(`File: ${file}`);
23647
+ printInfo(`Total examples: ${total}`);
23648
+ printInfo(`Showing first ${Math.min(n, total)}:`);
23649
+ console.log();
23650
+ preview.forEach((line, idx) => {
23651
+ try {
23652
+ const parsed = JSON.parse(line);
23653
+ console.log(`\u2500\u2500\u2500 Example ${idx + 1} \u2500\u2500\u2500`);
23654
+ printJson(parsed);
23655
+ console.log();
23656
+ } catch {
23657
+ printError(` Line ${idx + 1} is not valid JSON: ${line.slice(0, 80)}\u2026`);
23695
23658
  }
23696
- seen.add(line);
23697
- return true;
23698
23659
  });
23660
+ } catch (err) {
23661
+ printError(err instanceof Error ? err.message : String(err));
23662
+ process.exit(1);
23699
23663
  }
23700
- writeFileSync5(opts.output, finalLines.join(`
23664
+ });
23665
+ 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) => {
23666
+ try {
23667
+ for (const f of files) {
23668
+ if (!existsSync11(f)) {
23669
+ printError(`File not found: ${f}`);
23670
+ process.exit(1);
23671
+ }
23672
+ }
23673
+ mkdirSync6(join13(opts.output, "..").replace(/\/\.\.$/, "") || DEFAULT_DATASETS_DIR, { recursive: true });
23674
+ const allExamples = [];
23675
+ for (const f of files) {
23676
+ const lines = readFileSync6(f, "utf8").split(`
23677
+ `).map((l) => l.trim()).filter(Boolean);
23678
+ allExamples.push(...lines);
23679
+ printInfo(` Read ${lines.length} examples from ${f}`);
23680
+ }
23681
+ let finalLines = allExamples;
23682
+ let dupeCount = 0;
23683
+ if (opts.dedupe) {
23684
+ const seen = new Set;
23685
+ finalLines = allExamples.filter((line) => {
23686
+ if (seen.has(line)) {
23687
+ dupeCount++;
23688
+ return false;
23689
+ }
23690
+ seen.add(line);
23691
+ return true;
23692
+ });
23693
+ }
23694
+ writeFileSync5(opts.output, finalLines.join(`
23701
23695
  `) + `
23702
23696
  `, "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;
23697
+ const db = getDb();
23698
+ await db.insert(trainingDatasets).values({
23699
+ id: randomUUID3(),
23700
+ source: "mixed",
23701
+ filePath: opts.output,
23702
+ exampleCount: finalLines.length,
23703
+ createdAt: Date.now()
23704
+ });
23705
+ console.log();
23706
+ printSuccess(`Merged ${files.length} files \u2014 ${finalLines.length} examples \u2192 ${opts.output}`);
23707
+ if (opts.dedupe && dupeCount > 0)
23708
+ printInfo(` Removed ${dupeCount} duplicate(s)`);
23709
+ } catch (err) {
23710
+ printError(err instanceof Error ? err.message : String(err));
23711
+ process.exit(1);
23727
23712
  }
23728
- if (datasets.length === 0) {
23729
- printInfo("No datasets found. Use 'brains data gather' to create one.");
23730
- return;
23713
+ });
23714
+ dataCmd.command("list").description("List all gathered datasets").option("--json", "Output as JSON").action(async (opts) => {
23715
+ try {
23716
+ const db = getDb();
23717
+ const datasets = await db.select().from(trainingDatasets);
23718
+ if (opts.json) {
23719
+ printJson(datasets);
23720
+ return;
23721
+ }
23722
+ if (datasets.length === 0) {
23723
+ printInfo("No datasets found. Use 'brains data gather' to create one.");
23724
+ return;
23725
+ }
23726
+ printTable(["ID", "Source", "Examples", "File", "Created"], datasets.map((d) => [
23727
+ d.id,
23728
+ d.source,
23729
+ String(d.exampleCount),
23730
+ d.filePath ?? "",
23731
+ new Date(d.createdAt).toISOString().split("T")[0] ?? ""
23732
+ ]));
23733
+ } catch (err) {
23734
+ printError(err instanceof Error ? err.message : String(err));
23735
+ process.exit(1);
23731
23736
  }
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
- });
23737
+ });
23738
+ }
23739
+
23740
+ // src/cli/commands/collections.ts
23748
23741
  async function listCollections(json = false) {
23749
23742
  try {
23750
23743
  const db = getDb();
@@ -23761,45 +23754,224 @@ async function listCollections(json = false) {
23761
23754
  printInfo("No collections found. Set a collection with 'brains models set-collection'.");
23762
23755
  return;
23763
23756
  }
23764
- printTable(["Collection", "Model Count", "Models"], rows.map((r) => [
23765
- r.collection ?? "(none)",
23766
- String(r.count),
23767
- r.names ?? ""
23768
- ]));
23757
+ printTable(["Collection", "Model Count", "Models"], rows.map((r) => [r.collection ?? "(none)", String(r.count), r.names ?? ""]));
23769
23758
  } catch (err) {
23770
23759
  printError(err instanceof Error ? err.message : String(err));
23771
23760
  process.exit(1);
23772
23761
  }
23773
23762
  }
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;
23763
+ function registerCollectionsCommands(program2) {
23764
+ const collectionsCmd = program2.command("collections").description("Manage model collections");
23765
+ collectionsCmd.option("--json", "Output as JSON").action(async (opts) => {
23766
+ await listCollections(opts.json);
23767
+ });
23768
+ collectionsCmd.command("list").description("List all collections with model counts").option("--json", "Output as JSON").action(async (opts) => {
23769
+ await listCollections(opts.json);
23770
+ });
23771
+ collectionsCmd.command("show <name>").description("List all models in a collection").action(async (name) => {
23772
+ try {
23773
+ const db = getDb();
23774
+ const models = await db.select().from(fineTunedModels).where(eq(fineTunedModels.collection, name));
23775
+ if (models.length === 0) {
23776
+ printInfo(`No models found in collection '${name}'.`);
23777
+ return;
23778
+ }
23779
+ printTable(["ID", "Name", "Provider", "Status", "Base Model"], models.map((m) => [m.id, m.name, m.provider, printStatus(m.status), m.baseModel]));
23780
+ } catch (err) {
23781
+ printError(err instanceof Error ? err.message : String(err));
23782
+ process.exit(1);
23784
23783
  }
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
- });
23784
+ });
23785
+ collectionsCmd.command("rename <oldName> <newName>").description("Rename a collection across all models").action(async (oldName, newName) => {
23786
+ try {
23787
+ const db = getDb();
23788
+ const affected = await db.select({ id: fineTunedModels.id }).from(fineTunedModels).where(eq(fineTunedModels.collection, oldName));
23789
+ await db.update(fineTunedModels).set({ collection: newName, updatedAt: Date.now() }).where(eq(fineTunedModels.collection, oldName));
23790
+ printSuccess(`Renamed collection '${oldName}' \u2192 '${newName}' (${affected.length} models updated)`);
23791
+ } catch (err) {
23792
+ printError(err instanceof Error ? err.message : String(err));
23793
+ process.exit(1);
23794
+ }
23795
+ });
23796
+ }
23797
+
23798
+ // src/cli/commands/cloud.ts
23799
+ function registerCloudCommands2(program2) {
23800
+ const cloudCmd = program2.command("cloud").description("Cloud sync commands");
23801
+ cloudCmd.command("status").description("Show cloud config and connection health").option("--json", "Output as JSON").action(async (opts) => {
23802
+ try {
23803
+ 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));
23804
+ const config = getCloudConfig2();
23805
+ const info = {
23806
+ mode: config.mode,
23807
+ service: "brains",
23808
+ rds_host: config.rds?.host || "(not configured)"
23809
+ };
23810
+ if (config.rds?.host && config.rds?.username) {
23811
+ try {
23812
+ const pg = new PgAdapterAsync2(getConnectionString2("postgres"));
23813
+ await pg.get("SELECT 1 as ok");
23814
+ info.postgresql = "connected";
23815
+ await pg.close();
23816
+ } catch (err) {
23817
+ info.postgresql = `failed \u2014 ${err instanceof Error ? err.message : String(err)}`;
23818
+ }
23819
+ }
23820
+ const local = new SqliteAdapter2(getDbPath2("brains"));
23821
+ const tables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
23822
+ const syncHealth = [];
23823
+ for (const table of tables) {
23824
+ try {
23825
+ const totalRow = local.get(`SELECT COUNT(*) as c FROM "${table}"`);
23826
+ const unsyncedRow = local.get(`SELECT COUNT(*) as c FROM "${table}" WHERE synced_at IS NULL`);
23827
+ syncHealth.push({ table, total: totalRow?.c ?? 0, unsynced: unsyncedRow?.c ?? 0 });
23828
+ } catch {}
23829
+ }
23830
+ info.sync_health = syncHealth.filter((s) => s.total > 0);
23831
+ try {
23832
+ ensureConflictsTable2(local);
23833
+ const unresolved = listConflicts2(local, { resolved: false });
23834
+ info.conflicts_unresolved = unresolved.length;
23835
+ } catch {}
23836
+ local.close();
23837
+ if (opts.json) {
23838
+ console.log(JSON.stringify(info, null, 2));
23839
+ return;
23840
+ }
23841
+ printInfo(`Mode: ${info.mode}`);
23842
+ printInfo(`RDS Host: ${info.rds_host}`);
23843
+ if (info.postgresql)
23844
+ printInfo(`PostgreSQL: ${info.postgresql}`);
23845
+ for (const s of info.sync_health) {
23846
+ const pct = s.total > 0 ? Math.round((s.total - s.unsynced) / s.total * 100) : 100;
23847
+ printInfo(` ${s.table}: ${pct}% synced (${s.unsynced} unsynced / ${s.total} total)`);
23848
+ }
23849
+ if (info.conflicts_unresolved)
23850
+ printInfo(`Conflicts: ${info.conflicts_unresolved} unresolved`);
23851
+ } catch (e) {
23852
+ printError(e instanceof Error ? e.message : String(e));
23853
+ process.exit(1);
23854
+ }
23855
+ });
23856
+ 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) => {
23857
+ try {
23858
+ const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPush: syncPush2, listSqliteTables: listSqliteTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
23859
+ const config = getCloudConfig2();
23860
+ if (config.mode === "local") {
23861
+ printError("Cloud mode not configured.");
23862
+ process.exit(1);
23863
+ }
23864
+ const local = new SqliteAdapter2(getDbPath2("brains"));
23865
+ const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
23866
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : listSqliteTables2(local).filter((t) => !t.startsWith("_"));
23867
+ const results = await syncPush2(local, cloud, {
23868
+ tables: tableList,
23869
+ onProgress: (p) => {
23870
+ if (!opts.json && p.phase === "done")
23871
+ printInfo(` ${p.table}: ${p.rowsWritten} rows pushed`);
23872
+ }
23873
+ });
23874
+ local.close();
23875
+ await cloud.close();
23876
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
23877
+ if (opts.json) {
23878
+ console.log(JSON.stringify({ total, tables: results }));
23879
+ return;
23880
+ }
23881
+ printSuccess(`Done. ${total} rows pushed.`);
23882
+ } catch (e) {
23883
+ printError(e instanceof Error ? e.message : String(e));
23884
+ process.exit(1);
23885
+ }
23886
+ });
23887
+ 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) => {
23888
+ try {
23889
+ const { getCloudConfig: getCloudConfig2, getConnectionString: getConnectionString2, syncPull: syncPull2, listPgTables: listPgTables2, SqliteAdapter: SqliteAdapter2, PgAdapterAsync: PgAdapterAsync2, getDbPath: getDbPath2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
23890
+ const config = getCloudConfig2();
23891
+ if (config.mode === "local") {
23892
+ printError("Cloud mode not configured.");
23893
+ process.exit(1);
23894
+ }
23895
+ const local = new SqliteAdapter2(getDbPath2("brains"));
23896
+ const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
23897
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
23898
+ const results = await syncPull2(cloud, local, {
23899
+ tables: tableList,
23900
+ onProgress: (p) => {
23901
+ if (!opts.json && p.phase === "done")
23902
+ printInfo(` ${p.table}: ${p.rowsWritten} rows pulled`);
23903
+ }
23904
+ });
23905
+ local.close();
23906
+ await cloud.close();
23907
+ const total = results.reduce((s, r) => s + r.rowsWritten, 0);
23908
+ if (opts.json) {
23909
+ console.log(JSON.stringify({ total, tables: results }));
23910
+ return;
23911
+ }
23912
+ printSuccess(`Done. ${total} rows pulled.`);
23913
+ } catch (e) {
23914
+ printError(e instanceof Error ? e.message : String(e));
23915
+ process.exit(1);
23916
+ }
23917
+ });
23918
+ 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) => {
23919
+ try {
23920
+ 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));
23921
+ const config = getCloudConfig2();
23922
+ if (config.mode === "local") {
23923
+ printError("Cloud mode not configured.");
23924
+ process.exit(1);
23925
+ }
23926
+ const local = new SqliteAdapter2(getDbPath2("brains"));
23927
+ const cloud = new PgAdapterAsync2(getConnectionString2("brains"));
23928
+ const localTables = listSqliteTables2(local).filter((t) => !t.startsWith("_"));
23929
+ const remoteTables = (await listPgTables2(cloud)).filter((t) => !t.startsWith("_"));
23930
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : [...new Set([...localTables, ...remoteTables])];
23931
+ const pullResults = await syncPull2(cloud, local, { tables: tableList.filter((t) => remoteTables.includes(t)) });
23932
+ const pushResults = await syncPush2(local, cloud, { tables: tableList.filter((t) => localTables.includes(t)) });
23933
+ local.close();
23934
+ await cloud.close();
23935
+ const pulled = pullResults.reduce((s, r) => s + r.rowsWritten, 0);
23936
+ const pushed = pushResults.reduce((s, r) => s + r.rowsWritten, 0);
23937
+ if (opts.json) {
23938
+ console.log(JSON.stringify({ pulled, pushed }));
23939
+ return;
23940
+ }
23941
+ printSuccess(`Sync done. Pulled ${pulled} rows, pushed ${pushed} rows.`);
23942
+ } catch (e) {
23943
+ printError(e instanceof Error ? e.message : String(e));
23944
+ process.exit(1);
23945
+ }
23946
+ });
23947
+ 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) => {
23948
+ try {
23949
+ const { applyPgMigrations: applyPgMigrations3 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
23950
+ const result = await applyPgMigrations3(opts.connectionString);
23951
+ if (opts.json) {
23952
+ console.log(JSON.stringify(result));
23953
+ return;
23954
+ }
23955
+ printSuccess(`Applied ${result.applied.length} migration(s), skipped ${result.alreadyApplied.length}.`);
23956
+ if (result.errors.length > 0) {
23957
+ printError(result.errors.join(`
23958
+ `));
23959
+ process.exit(1);
23960
+ }
23961
+ } catch (e) {
23962
+ printError(e instanceof Error ? e.message : String(e));
23963
+ process.exit(1);
23964
+ }
23965
+ });
23966
+ }
23967
+
23968
+ // src/cli/index.ts
23969
+ var program2 = new Command;
23970
+ program2.name("brains").description("Fine-tuned model tracker and trainer").version("0.0.1");
23971
+ registerModelsCommands(program2);
23972
+ registerFinetuneCommands(program2);
23973
+ registerDataCommands(program2);
23974
+ registerCollectionsCommands(program2);
23803
23975
  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
23976
  const db = getDb();
23805
23977
  try {
@@ -23874,7 +24046,7 @@ var feedbackCmd = program2.command("feedback").description("Feedback commands");
23874
24046
  feedbackCmd.command("send <message>").description("Send feedback about brains").option("--email <email>", "Contact email").action(async (message, opts) => {
23875
24047
  const { sendFeedback: sendFeedback2 } = await Promise.resolve().then(() => (init_dist(), exports_dist));
23876
24048
  const rawDb = getRawDb();
23877
- const pkg = JSON.parse(readFileSync6(join13(import.meta.dir, "../../package.json"), "utf8"));
24049
+ const pkg = JSON.parse(readFileSync7(join14(import.meta.dir, "../../package.json"), "utf8"));
23878
24050
  const result = await sendFeedback2({ service: "brains", message, email: opts.email, version: pkg.version }, rawDb);
23879
24051
  rawDb.close();
23880
24052
  if (result.sent) {
@@ -23904,170 +24076,5 @@ feedbackCmd.command("list").description("List locally saved feedback").option("-
23904
24076
  e.created_at ?? ""
23905
24077
  ]));
23906
24078
  });
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
- });
24079
+ registerCloudCommands2(program2);
24073
24080
  program2.parse();