@hasna/brains 0.0.9 → 0.0.10

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.
Files changed (39) hide show
  1. package/dist/cli/index.js +1055 -632
  2. package/dist/db/index.d.ts.map +1 -1
  3. package/dist/index.js +4325 -127
  4. package/dist/lib/config.d.ts +11 -0
  5. package/dist/lib/config.d.ts.map +1 -0
  6. package/dist/lib/gatherers/assistants.d.ts +3 -0
  7. package/dist/lib/gatherers/assistants.d.ts.map +1 -0
  8. package/dist/lib/gatherers/economy.d.ts +3 -0
  9. package/dist/lib/gatherers/economy.d.ts.map +1 -0
  10. package/dist/lib/gatherers/index.d.ts +3 -0
  11. package/dist/lib/gatherers/index.d.ts.map +1 -1
  12. package/dist/lib/gatherers/protocol.d.ts +29 -0
  13. package/dist/lib/gatherers/protocol.d.ts.map +1 -0
  14. package/dist/lib/gatherers/recordings.d.ts +3 -0
  15. package/dist/lib/gatherers/recordings.d.ts.map +1 -0
  16. package/dist/lib/gatherers/registry.d.ts +7 -0
  17. package/dist/lib/gatherers/registry.d.ts.map +1 -0
  18. package/dist/lib/gatherers/researcher.d.ts +3 -0
  19. package/dist/lib/gatherers/researcher.d.ts.map +1 -0
  20. package/dist/lib/gatherers/styles.d.ts +3 -0
  21. package/dist/lib/gatherers/styles.d.ts.map +1 -0
  22. package/dist/lib/gatherers/tickets.d.ts +3 -0
  23. package/dist/lib/gatherers/tickets.d.ts.map +1 -0
  24. package/dist/lib/index.d.ts +3 -0
  25. package/dist/lib/index.d.ts.map +1 -1
  26. package/dist/lib/providers/openai.d.ts +2 -0
  27. package/dist/lib/providers/openai.d.ts.map +1 -1
  28. package/dist/lib/providers/thinker-labs.d.ts +1 -0
  29. package/dist/lib/providers/thinker-labs.d.ts.map +1 -1
  30. package/dist/lib/retry.d.ts +7 -0
  31. package/dist/lib/retry.d.ts.map +1 -0
  32. package/dist/lib/schemas.d.ts +87 -0
  33. package/dist/lib/schemas.d.ts.map +1 -0
  34. package/dist/mcp/index.d.ts.map +1 -1
  35. package/dist/mcp/index.js +4619 -199
  36. package/dist/server/index.d.ts +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +4648 -9
  39. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -44,6 +44,7 @@ var __export = (target, all) => {
44
44
  set: __exportSetter.bind(all, name)
45
45
  });
46
46
  };
47
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
48
  var __require = import.meta.require;
48
49
 
49
50
  // node_modules/commander/lib/error.js
@@ -2080,6 +2081,352 @@ var require_commander = __commonJS((exports) => {
2080
2081
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2081
2082
  });
2082
2083
 
2084
+ // src/lib/gatherers/todos.ts
2085
+ var exports_todos = {};
2086
+ __export(exports_todos, {
2087
+ gatherFromTodos: () => gatherFromTodos
2088
+ });
2089
+ import { Database as Database3 } from "bun:sqlite";
2090
+ import { homedir as homedir3 } from "os";
2091
+ import { join as join3 } from "path";
2092
+ function taskToCreateExample(task) {
2093
+ const userMsg = `Create a task: ${task.title}${task.description ? `
2094
+
2095
+ Description: ${task.description}` : ""}`;
2096
+ const taskDetails = {
2097
+ id: task.short_id ?? task.id,
2098
+ title: task.title,
2099
+ description: task.description ?? "",
2100
+ status: task.status,
2101
+ priority: task.priority,
2102
+ tags: JSON.parse(task.tags ?? "[]"),
2103
+ created_at: task.created_at
2104
+ };
2105
+ return {
2106
+ messages: [
2107
+ { role: "system", content: SYSTEM_PROMPT },
2108
+ { role: "user", content: userMsg },
2109
+ { role: "assistant", content: `Created task: ${JSON.stringify(taskDetails, null, 2)}` }
2110
+ ]
2111
+ };
2112
+ }
2113
+ function taskToStatusUpdateExample(task) {
2114
+ if (!task.completed_at && task.status === "pending")
2115
+ return null;
2116
+ const id = task.short_id ?? task.id;
2117
+ return {
2118
+ messages: [
2119
+ { role: "system", content: SYSTEM_PROMPT },
2120
+ { role: "user", content: `Mark task ${id} as ${task.status}` },
2121
+ { role: "assistant", content: `Task ${id} has been updated to status: ${task.status}. ${task.completed_at ? `Completed at: ${task.completed_at}` : ""}`.trim() }
2122
+ ]
2123
+ };
2124
+ }
2125
+ function taskToSearchExample(tasks, query) {
2126
+ const matched = tasks.filter((t) => t.title.toLowerCase().includes(query.toLowerCase())).slice(0, 5);
2127
+ return {
2128
+ messages: [
2129
+ { role: "system", content: SYSTEM_PROMPT },
2130
+ { role: "user", content: `Search tasks for: "${query}"` },
2131
+ {
2132
+ role: "assistant",
2133
+ content: matched.length > 0 ? `Found ${matched.length} task(s):
2134
+ ${matched.map((t) => `- [${t.short_id ?? t.id}] ${t.title} (${t.status})`).join(`
2135
+ `)}` : `No tasks found matching "${query}".`
2136
+ }
2137
+ ]
2138
+ };
2139
+ }
2140
+ async function gatherFromTodos(options = {}) {
2141
+ const dbPath = join3(homedir3(), ".todos", "todos.db");
2142
+ const db = new Database3(dbPath, { readonly: true, create: false });
2143
+ try {
2144
+ let query = "SELECT * FROM tasks WHERE 1=1";
2145
+ const params = [];
2146
+ if (options.since) {
2147
+ query += " AND created_at >= ?";
2148
+ params.push(options.since.toISOString());
2149
+ }
2150
+ query += " ORDER BY created_at DESC";
2151
+ if (options.limit) {
2152
+ query += " LIMIT ?";
2153
+ params.push(options.limit * 2);
2154
+ }
2155
+ const tasks = db.query(query).all(...params);
2156
+ const examples = [];
2157
+ for (const task of tasks) {
2158
+ examples.push(taskToCreateExample(task));
2159
+ const statusEx = taskToStatusUpdateExample(task);
2160
+ if (statusEx)
2161
+ examples.push(statusEx);
2162
+ }
2163
+ const searchTerms = ["urgent", "fix", "implement", "create", "update", "review"];
2164
+ for (const term of searchTerms) {
2165
+ examples.push(taskToSearchExample(tasks, term));
2166
+ }
2167
+ const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
2168
+ return {
2169
+ source: "todos",
2170
+ examples: finalExamples,
2171
+ count: finalExamples.length
2172
+ };
2173
+ } finally {
2174
+ db.close();
2175
+ }
2176
+ }
2177
+ var SYSTEM_PROMPT = "You are a task management assistant that helps users create, update, search, and manage tasks and projects.";
2178
+ var init_todos = () => {};
2179
+
2180
+ // src/lib/gatherers/mementos.ts
2181
+ var exports_mementos = {};
2182
+ __export(exports_mementos, {
2183
+ gatherFromMementos: () => gatherFromMementos
2184
+ });
2185
+ import { Database as Database4 } from "bun:sqlite";
2186
+ import { homedir as homedir4 } from "os";
2187
+ import { join as join4 } from "path";
2188
+ function memoryToRecallExample(memory) {
2189
+ return {
2190
+ messages: [
2191
+ { role: "system", content: SYSTEM_PROMPT2 },
2192
+ { role: "user", content: `What do you remember about "${memory.key}"?` },
2193
+ {
2194
+ role: "assistant",
2195
+ content: memory.summary ? `${memory.value}
2196
+
2197
+ Summary: ${memory.summary}` : memory.value
2198
+ }
2199
+ ]
2200
+ };
2201
+ }
2202
+ function memoryToSaveExample(memory) {
2203
+ const tags = JSON.parse(memory.tags ?? "[]");
2204
+ return {
2205
+ messages: [
2206
+ { role: "system", content: SYSTEM_PROMPT2 },
2207
+ {
2208
+ role: "user",
2209
+ content: `Remember this for me: ${memory.key} = ${memory.value}${tags.length ? ` (tags: ${tags.join(", ")})` : ""}`
2210
+ },
2211
+ {
2212
+ role: "assistant",
2213
+ content: `Saved to memory: "${memory.key}" with ${memory.category} category, importance ${memory.importance}/10, scope: ${memory.scope}.`
2214
+ }
2215
+ ]
2216
+ };
2217
+ }
2218
+ function memoryToSearchExample(memories, category) {
2219
+ const matched = memories.filter((m) => m.category === category && m.status === "active").slice(0, 5);
2220
+ return {
2221
+ messages: [
2222
+ { role: "system", content: SYSTEM_PROMPT2 },
2223
+ { role: "user", content: `What ${category} memories do you have?` },
2224
+ {
2225
+ role: "assistant",
2226
+ content: matched.length > 0 ? `Here are my ${category} memories:
2227
+ ${matched.map((m) => `- ${m.key}: ${m.value.slice(0, 120)}${m.value.length > 120 ? "..." : ""}`).join(`
2228
+ `)}` : `I don't have any ${category} memories stored yet.`
2229
+ }
2230
+ ]
2231
+ };
2232
+ }
2233
+ async function gatherFromMementos(options = {}) {
2234
+ const dbPath = join4(homedir4(), ".mementos", "mementos.db");
2235
+ const db = new Database4(dbPath, { readonly: true, create: false });
2236
+ try {
2237
+ let query = "SELECT * FROM memories WHERE status = 'active'";
2238
+ const params = [];
2239
+ if (options.since) {
2240
+ query += " AND created_at >= ?";
2241
+ params.push(options.since.toISOString());
2242
+ }
2243
+ query += " ORDER BY importance DESC, created_at DESC";
2244
+ if (options.limit) {
2245
+ query += " LIMIT ?";
2246
+ params.push(options.limit * 3);
2247
+ }
2248
+ const memories = db.query(query).all(...params);
2249
+ const examples = [];
2250
+ for (const memory of memories) {
2251
+ examples.push(memoryToRecallExample(memory));
2252
+ examples.push(memoryToSaveExample(memory));
2253
+ }
2254
+ const categories = [...new Set(memories.map((m) => m.category))];
2255
+ for (const category of categories) {
2256
+ examples.push(memoryToSearchExample(memories, category));
2257
+ }
2258
+ const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
2259
+ return {
2260
+ source: "mementos",
2261
+ examples: finalExamples,
2262
+ count: finalExamples.length
2263
+ };
2264
+ } finally {
2265
+ db.close();
2266
+ }
2267
+ }
2268
+ var SYSTEM_PROMPT2 = "You are an AI assistant with persistent memory. You can remember and recall information across sessions to provide better, more personalized assistance.";
2269
+ var init_mementos = () => {};
2270
+
2271
+ // src/lib/gatherers/conversations.ts
2272
+ var exports_conversations = {};
2273
+ __export(exports_conversations, {
2274
+ gatherFromConversations: () => gatherFromConversations
2275
+ });
2276
+ import { Database as Database5 } from "bun:sqlite";
2277
+ import { homedir as homedir5 } from "os";
2278
+ import { join as join5 } from "path";
2279
+ function windowToExample(window2) {
2280
+ if (window2.length < 2)
2281
+ return null;
2282
+ const messages = [
2283
+ { role: "system", content: SYSTEM_PROMPT3 }
2284
+ ];
2285
+ for (let i = 0;i < window2.length - 1; i++) {
2286
+ const msg = window2[i];
2287
+ if (!msg)
2288
+ continue;
2289
+ const role = i % 2 === 0 ? "user" : "assistant";
2290
+ messages.push({
2291
+ role,
2292
+ content: `[${msg.from_agent} \u2192 ${msg.to_agent ?? msg.space ?? "all"}]: ${msg.content}`
2293
+ });
2294
+ }
2295
+ const last = window2[window2.length - 1];
2296
+ if (!last)
2297
+ return null;
2298
+ messages.push({
2299
+ role: "assistant",
2300
+ content: `[${last.from_agent} \u2192 ${last.to_agent ?? last.space ?? "all"}]: ${last.content}`
2301
+ });
2302
+ return { messages };
2303
+ }
2304
+ async function gatherFromConversations(options = {}) {
2305
+ const dbPath = join5(homedir5(), ".conversations", "messages.db");
2306
+ const db = new Database5(dbPath, { readonly: true, create: false });
2307
+ try {
2308
+ let query = "SELECT * FROM messages WHERE 1=1";
2309
+ const params = [];
2310
+ if (options.since) {
2311
+ query += " AND created_at >= ?";
2312
+ params.push(options.since.toISOString());
2313
+ }
2314
+ query += " ORDER BY session_id, created_at ASC";
2315
+ const allMessages = db.query(query).all(...params);
2316
+ const sessions = new Map;
2317
+ for (const msg of allMessages) {
2318
+ const msgs = sessions.get(msg.session_id) ?? [];
2319
+ msgs.push(msg);
2320
+ sessions.set(msg.session_id, msgs);
2321
+ }
2322
+ const examples = [];
2323
+ const windowSize = 4;
2324
+ for (const [, sessionMsgs] of sessions) {
2325
+ if (sessionMsgs.length < 2)
2326
+ continue;
2327
+ for (let start = 0;start <= sessionMsgs.length - 2; start++) {
2328
+ const end = Math.min(start + windowSize, sessionMsgs.length);
2329
+ const window2 = sessionMsgs.slice(start, end);
2330
+ const example = windowToExample(window2);
2331
+ if (example)
2332
+ examples.push(example);
2333
+ }
2334
+ }
2335
+ const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
2336
+ return {
2337
+ source: "conversations",
2338
+ examples: finalExamples,
2339
+ count: finalExamples.length
2340
+ };
2341
+ } finally {
2342
+ db.close();
2343
+ }
2344
+ }
2345
+ var SYSTEM_PROMPT3 = "You are a helpful AI assistant participating in multi-agent conversations. You communicate clearly and collaboratively with other agents and users.";
2346
+ var init_conversations = () => {};
2347
+
2348
+ // src/lib/gatherers/sessions.ts
2349
+ var exports_sessions2 = {};
2350
+ __export(exports_sessions2, {
2351
+ gatherFromSessions: () => gatherFromSessions
2352
+ });
2353
+ import { readdir, readFile, stat } from "fs/promises";
2354
+ import { existsSync as existsSync3 } from "fs";
2355
+ import { join as join6 } from "path";
2356
+ import { homedir as homedir6 } from "os";
2357
+ function extractText(content) {
2358
+ if (typeof content === "string")
2359
+ return content;
2360
+ return content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join(`
2361
+ `).trim();
2362
+ }
2363
+ async function gatherFromSessions(options = {}) {
2364
+ const { limit: limit2 = 1000 } = options;
2365
+ const examples = [];
2366
+ const claudeDir = join6(homedir6(), ".claude", "projects");
2367
+ if (!existsSync3(claudeDir)) {
2368
+ return { source: "sessions", examples: [], count: 0 };
2369
+ }
2370
+ const projectDirs = await readdir(claudeDir).catch(() => []);
2371
+ for (const projectDir of projectDirs) {
2372
+ if (examples.length >= limit2)
2373
+ break;
2374
+ const projectPath = join6(claudeDir, projectDir);
2375
+ const files = await readdir(projectPath).catch(() => []);
2376
+ for (const file of files) {
2377
+ if (examples.length >= limit2)
2378
+ break;
2379
+ if (!file.endsWith(".jsonl"))
2380
+ continue;
2381
+ const filePath = join6(projectPath, file);
2382
+ if (options.since) {
2383
+ const fileStat = await stat(filePath).catch(() => null);
2384
+ if (fileStat && fileStat.mtime < options.since)
2385
+ continue;
2386
+ }
2387
+ const content = await readFile(filePath, "utf-8").catch(() => "");
2388
+ if (!content.trim())
2389
+ continue;
2390
+ const lines = content.trim().split(`
2391
+ `);
2392
+ const turns = [];
2393
+ for (const line of lines) {
2394
+ try {
2395
+ const entry = JSON.parse(line);
2396
+ if ((entry.type === "user" || entry.type === "human") && entry.message?.content) {
2397
+ const text2 = extractText(entry.message.content);
2398
+ if (text2.trim())
2399
+ turns.push({ role: "user", content: text2.trim() });
2400
+ } else if (entry.type === "assistant" && entry.message?.content) {
2401
+ const text2 = extractText(entry.message.content);
2402
+ if (text2.trim())
2403
+ turns.push({ role: "assistant", content: text2.trim() });
2404
+ }
2405
+ } catch {}
2406
+ }
2407
+ const windowSize = 6;
2408
+ for (let start = 0;start < turns.length - 1 && examples.length < limit2; start++) {
2409
+ const window2 = turns.slice(start, start + windowSize);
2410
+ if (!window2[0] || window2[0].role !== "user")
2411
+ continue;
2412
+ const lastAssistantIdx = window2.map((t) => t.role).lastIndexOf("assistant");
2413
+ if (lastAssistantIdx < 1)
2414
+ continue;
2415
+ const usedTurns = window2.slice(0, lastAssistantIdx + 1);
2416
+ examples.push({
2417
+ messages: [
2418
+ { role: "system", content: SYSTEM_PROMPT4 },
2419
+ ...usedTurns
2420
+ ]
2421
+ });
2422
+ }
2423
+ }
2424
+ }
2425
+ return { source: "sessions", examples, count: examples.length };
2426
+ }
2427
+ var SYSTEM_PROMPT4 = "You are Claude Code, an AI assistant built by Anthropic that helps developers with coding, architecture, debugging, and software engineering tasks.";
2428
+ var init_sessions = () => {};
2429
+
2083
2430
  // node_modules/commander/esm.mjs
2084
2431
  var import__ = __toESM(require_commander(), 1);
2085
2432
  var {
@@ -3454,9 +3801,9 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
3454
3801
 
3455
3802
  // src/cli/index.ts
3456
3803
  import { randomUUID } from "crypto";
3457
- import { readFileSync as readFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync } from "fs";
3458
- import { join as join6 } from "path";
3459
- import { homedir as homedir6 } from "os";
3804
+ import { readFileSync as readFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
3805
+ import { join as join7 } from "path";
3806
+ import { homedir as homedir7 } from "os";
3460
3807
 
3461
3808
  // node_modules/drizzle-orm/bun-sqlite/driver.js
3462
3809
  import { Database } from "bun:sqlite";
@@ -5739,10 +6086,48 @@ function drizzle(...params) {
5739
6086
  drizzle2.mock = mock;
5740
6087
  })(drizzle || (drizzle = {}));
5741
6088
 
6089
+ // node_modules/drizzle-orm/migrator.js
6090
+ import crypto from "crypto";
6091
+ import fs from "fs";
6092
+ function readMigrationFiles(config) {
6093
+ const migrationFolderTo = config.migrationsFolder;
6094
+ const migrationQueries = [];
6095
+ const journalPath = `${migrationFolderTo}/meta/_journal.json`;
6096
+ if (!fs.existsSync(journalPath)) {
6097
+ throw new Error(`Can't find meta/_journal.json file`);
6098
+ }
6099
+ const journalAsString = fs.readFileSync(`${migrationFolderTo}/meta/_journal.json`).toString();
6100
+ const journal = JSON.parse(journalAsString);
6101
+ for (const journalEntry of journal.entries) {
6102
+ const migrationPath = `${migrationFolderTo}/${journalEntry.tag}.sql`;
6103
+ try {
6104
+ const query = fs.readFileSync(`${migrationFolderTo}/${journalEntry.tag}.sql`).toString();
6105
+ const result = query.split("--> statement-breakpoint").map((it) => {
6106
+ return it;
6107
+ });
6108
+ migrationQueries.push({
6109
+ sql: result,
6110
+ bps: journalEntry.breakpoints,
6111
+ folderMillis: journalEntry.when,
6112
+ hash: crypto.createHash("sha256").update(query).digest("hex")
6113
+ });
6114
+ } catch {
6115
+ throw new Error(`No file ${migrationPath} found in ${migrationFolderTo} folder`);
6116
+ }
6117
+ }
6118
+ return migrationQueries;
6119
+ }
6120
+
6121
+ // node_modules/drizzle-orm/bun-sqlite/migrator.js
6122
+ function migrate(db, config) {
6123
+ const migrations = readMigrationFiles(config);
6124
+ db.dialect.migrate(migrations, db.session, config);
6125
+ }
6126
+
5742
6127
  // src/db/index.ts
5743
6128
  import { Database as Database2 } from "bun:sqlite";
5744
- import { mkdirSync } from "fs";
5745
- import { dirname, join } from "path";
6129
+ import { mkdirSync, existsSync, readdirSync, copyFileSync, statSync } from "fs";
6130
+ import { dirname, join, resolve } from "path";
5746
6131
  import { homedir } from "os";
5747
6132
 
5748
6133
  // src/db/schema.ts
@@ -5790,56 +6175,80 @@ var trainingDatasets = sqliteTable("training_datasets", {
5790
6175
  });
5791
6176
 
5792
6177
  // src/db/index.ts
5793
- var DEFAULT_DB_PATH = join(homedir(), ".brains", "brains.db");
6178
+ function resolveDefaultDbPath() {
6179
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
6180
+ const newDir = join(home, ".hasna", "brains");
6181
+ const oldDir = join(home, ".brains");
6182
+ if (existsSync(oldDir) && !existsSync(newDir)) {
6183
+ mkdirSync(newDir, { recursive: true });
6184
+ try {
6185
+ for (const file of readdirSync(oldDir)) {
6186
+ const oldPath = join(oldDir, file);
6187
+ const newPath = join(newDir, file);
6188
+ try {
6189
+ if (statSync(oldPath).isFile()) {
6190
+ copyFileSync(oldPath, newPath);
6191
+ }
6192
+ } catch {}
6193
+ }
6194
+ } catch {}
6195
+ }
6196
+ mkdirSync(newDir, { recursive: true });
6197
+ return join(newDir, "brains.db");
6198
+ }
6199
+ var DEFAULT_DB_PATH = resolveDefaultDbPath();
5794
6200
  function ensureDir(filePath) {
5795
6201
  mkdirSync(dirname(filePath), { recursive: true });
5796
6202
  }
5797
- function createTables(sqlite) {
5798
- sqlite.exec(`
5799
- CREATE TABLE IF NOT EXISTS fine_tuned_models (
5800
- id TEXT PRIMARY KEY,
5801
- base_model TEXT NOT NULL,
5802
- name TEXT NOT NULL,
5803
- provider TEXT NOT NULL,
5804
- status TEXT NOT NULL DEFAULT 'pending',
5805
- fine_tune_job_id TEXT,
5806
- display_name TEXT,
5807
- description TEXT,
5808
- collection TEXT,
5809
- tags TEXT,
5810
- created_at INTEGER NOT NULL,
5811
- updated_at INTEGER NOT NULL
5812
- );
5813
-
5814
- CREATE TABLE IF NOT EXISTS training_jobs (
5815
- id TEXT PRIMARY KEY,
5816
- model_id TEXT NOT NULL REFERENCES fine_tuned_models(id),
5817
- provider TEXT NOT NULL,
5818
- status TEXT NOT NULL,
5819
- started_at INTEGER NOT NULL,
5820
- finished_at INTEGER,
5821
- metrics TEXT,
5822
- error TEXT
5823
- );
5824
-
5825
- CREATE TABLE IF NOT EXISTS training_datasets (
5826
- id TEXT PRIMARY KEY,
5827
- source TEXT NOT NULL,
5828
- file_path TEXT NOT NULL,
5829
- example_count INTEGER NOT NULL,
5830
- created_at INTEGER NOT NULL,
5831
- used_in_job_id TEXT REFERENCES training_jobs(id)
5832
- );
5833
- `);
5834
- }
5835
6203
  function getDb(dbPath) {
5836
6204
  const resolvedPath = dbPath ?? DEFAULT_DB_PATH;
5837
6205
  ensureDir(resolvedPath);
5838
6206
  const sqlite = new Database2(resolvedPath);
5839
6207
  sqlite.run("PRAGMA journal_mode = WAL");
5840
6208
  sqlite.run("PRAGMA foreign_keys = ON");
5841
- createTables(sqlite);
5842
- return drizzle(sqlite, { schema: exports_schema });
6209
+ const db = drizzle(sqlite, { schema: exports_schema });
6210
+ try {
6211
+ const migrationsFolder = resolve(import.meta.dir, "../../drizzle");
6212
+ migrate(db, { migrationsFolder });
6213
+ } catch {
6214
+ sqlite.exec(`
6215
+ CREATE TABLE IF NOT EXISTS fine_tuned_models (
6216
+ id TEXT PRIMARY KEY,
6217
+ base_model TEXT NOT NULL,
6218
+ name TEXT NOT NULL,
6219
+ provider TEXT NOT NULL,
6220
+ status TEXT NOT NULL DEFAULT 'pending',
6221
+ fine_tune_job_id TEXT,
6222
+ display_name TEXT,
6223
+ description TEXT,
6224
+ collection TEXT,
6225
+ tags TEXT,
6226
+ created_at INTEGER NOT NULL,
6227
+ updated_at INTEGER NOT NULL
6228
+ );
6229
+
6230
+ CREATE TABLE IF NOT EXISTS training_jobs (
6231
+ id TEXT PRIMARY KEY,
6232
+ model_id TEXT NOT NULL REFERENCES fine_tuned_models(id),
6233
+ provider TEXT NOT NULL,
6234
+ status TEXT NOT NULL,
6235
+ started_at INTEGER NOT NULL,
6236
+ finished_at INTEGER,
6237
+ metrics TEXT,
6238
+ error TEXT
6239
+ );
6240
+
6241
+ CREATE TABLE IF NOT EXISTS training_datasets (
6242
+ id TEXT PRIMARY KEY,
6243
+ source TEXT NOT NULL,
6244
+ file_path TEXT NOT NULL,
6245
+ example_count INTEGER NOT NULL,
6246
+ created_at INTEGER NOT NULL,
6247
+ used_in_job_id TEXT REFERENCES training_jobs(id)
6248
+ );
6249
+ `);
6250
+ }
6251
+ return db;
5843
6252
  }
5844
6253
 
5845
6254
  // node_modules/openai/internal/qs/formats.mjs
@@ -6930,8 +7339,8 @@ function _addRequestID(value, response) {
6930
7339
 
6931
7340
  class APIPromise extends Promise {
6932
7341
  constructor(responsePromise, parseResponse = defaultParseResponse) {
6933
- super((resolve) => {
6934
- resolve(null);
7342
+ super((resolve2) => {
7343
+ resolve2(null);
6935
7344
  });
6936
7345
  this.responsePromise = responsePromise;
6937
7346
  this.parseResponse = parseResponse;
@@ -7442,7 +7851,7 @@ var startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i;
7442
7851
  var isAbsoluteURL = (url) => {
7443
7852
  return startsWithSchemeRegexp.test(url);
7444
7853
  };
7445
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
7854
+ var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
7446
7855
  var validatePositiveInteger = (name, n) => {
7447
7856
  if (typeof n !== "number" || !Number.isInteger(n)) {
7448
7857
  throw new OpenAIError(`${name} must be an integer`);
@@ -7807,12 +8216,12 @@ class EventStream {
7807
8216
  _EventStream_errored.set(this, false);
7808
8217
  _EventStream_aborted.set(this, false);
7809
8218
  _EventStream_catchingPromiseCreated.set(this, false);
7810
- __classPrivateFieldSet3(this, _EventStream_connectedPromise, new Promise((resolve, reject) => {
7811
- __classPrivateFieldSet3(this, _EventStream_resolveConnectedPromise, resolve, "f");
8219
+ __classPrivateFieldSet3(this, _EventStream_connectedPromise, new Promise((resolve2, reject) => {
8220
+ __classPrivateFieldSet3(this, _EventStream_resolveConnectedPromise, resolve2, "f");
7812
8221
  __classPrivateFieldSet3(this, _EventStream_rejectConnectedPromise, reject, "f");
7813
8222
  }), "f");
7814
- __classPrivateFieldSet3(this, _EventStream_endPromise, new Promise((resolve, reject) => {
7815
- __classPrivateFieldSet3(this, _EventStream_resolveEndPromise, resolve, "f");
8223
+ __classPrivateFieldSet3(this, _EventStream_endPromise, new Promise((resolve2, reject) => {
8224
+ __classPrivateFieldSet3(this, _EventStream_resolveEndPromise, resolve2, "f");
7816
8225
  __classPrivateFieldSet3(this, _EventStream_rejectEndPromise, reject, "f");
7817
8226
  }), "f");
7818
8227
  __classPrivateFieldGet3(this, _EventStream_connectedPromise, "f").catch(() => {});
@@ -7864,11 +8273,11 @@ class EventStream {
7864
8273
  return this;
7865
8274
  }
7866
8275
  emitted(event) {
7867
- return new Promise((resolve, reject) => {
8276
+ return new Promise((resolve2, reject) => {
7868
8277
  __classPrivateFieldSet3(this, _EventStream_catchingPromiseCreated, true, "f");
7869
8278
  if (event !== "error")
7870
8279
  this.once("error", reject);
7871
- this.once(event, resolve);
8280
+ this.once(event, resolve2);
7872
8281
  });
7873
8282
  }
7874
8283
  async done() {
@@ -8026,7 +8435,7 @@ class AssistantStream extends EventStream {
8026
8435
  if (done) {
8027
8436
  return { value: undefined, done: true };
8028
8437
  }
8029
- return new Promise((resolve, reject) => readQueue.push({ resolve, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: undefined, done: true });
8438
+ return new Promise((resolve2, reject) => readQueue.push({ resolve: resolve2, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: undefined, done: true });
8030
8439
  }
8031
8440
  const chunk = pushQueue.shift();
8032
8441
  return { value: chunk, done: false };
@@ -9591,7 +10000,7 @@ class ChatCompletionStream extends AbstractChatCompletionRunner {
9591
10000
  if (done) {
9592
10001
  return { value: undefined, done: true };
9593
10002
  }
9594
- return new Promise((resolve, reject) => readQueue.push({ resolve, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: undefined, done: true });
10003
+ return new Promise((resolve2, reject) => readQueue.push({ resolve: resolve2, reject })).then((chunk2) => chunk2 ? { value: chunk2, done: false } : { value: undefined, done: true });
9595
10004
  }
9596
10005
  const chunk = pushQueue.shift();
9597
10006
  return { value: chunk, done: false };
@@ -10840,7 +11249,7 @@ class ResponseStream extends EventStream {
10840
11249
  if (done) {
10841
11250
  return { value: undefined, done: true };
10842
11251
  }
10843
- return new Promise((resolve, reject) => readQueue.push({ resolve, reject })).then((event2) => event2 ? { value: event2, done: false } : { value: undefined, done: true });
11252
+ return new Promise((resolve2, reject) => readQueue.push({ resolve: resolve2, reject })).then((event2) => event2 ? { value: event2, done: false } : { value: undefined, done: true });
10844
11253
  }
10845
11254
  const event = pushQueue.shift();
10846
11255
  return { value: event, done: false };
@@ -11322,563 +11731,333 @@ var _deployments_endpoints = new Set([
11322
11731
  "/audio/translations",
11323
11732
  "/audio/speech",
11324
11733
  "/images/generations",
11325
- "/images/edits"
11326
- ]);
11327
- var openai_default = OpenAI;
11328
-
11329
- // src/lib/providers/openai.ts
11330
- import { readFileSync } from "fs";
11331
- function getClient() {
11332
- const apiKey = process.env.OPENAI_API_KEY;
11333
- if (!apiKey) {
11334
- throw new Error("OPENAI_API_KEY environment variable is required");
11335
- }
11336
- return new openai_default({ apiKey });
11337
- }
11338
- async function uploadTrainingFile(filePath) {
11339
- const client = getClient();
11340
- const fileContent = readFileSync(filePath);
11341
- const blob2 = new Blob([fileContent], { type: "application/jsonl" });
11342
- const file = new File([blob2], filePath.split("/").pop() ?? "training.jsonl", { type: "application/jsonl" });
11343
- const response = await client.files.create({
11344
- file,
11345
- purpose: "fine-tune"
11346
- });
11347
- return { fileId: response.id };
11348
- }
11349
- async function createFineTuneJob(fileId, baseModel, suffix) {
11350
- const client = getClient();
11351
- const params = {
11352
- training_file: fileId,
11353
- model: baseModel
11354
- };
11355
- if (suffix) {
11356
- params.suffix = suffix;
11357
- }
11358
- const response = await client.fineTuning.jobs.create(params);
11359
- return { jobId: response.id, status: response.status };
11360
- }
11361
- async function getFineTuneStatus(jobId) {
11362
- const client = getClient();
11363
- const response = await client.fineTuning.jobs.retrieve(jobId);
11364
- return {
11365
- jobId: response.id,
11366
- status: response.status,
11367
- fineTunedModel: response.fine_tuned_model ?? undefined,
11368
- error: response.error?.message ?? undefined
11369
- };
11370
- }
11371
- async function listFineTunedModels() {
11372
- const client = getClient();
11373
- const jobs = await client.fineTuning.jobs.list();
11374
- return jobs.data.map((job) => ({
11375
- id: job.id,
11376
- model: job.fine_tuned_model ?? job.model,
11377
- status: job.status,
11378
- created: job.created_at
11379
- }));
11380
- }
11381
-
11382
- // src/lib/providers/thinker-labs.ts
11383
- var DEFAULT_BASE_URL = "https://api.thinkerlabs.ai/v1";
11384
- function getConfig() {
11385
- const apiKey = process.env.THINKER_LABS_API_KEY;
11386
- const baseUrl = process.env.THINKER_LABS_BASE_URL ?? DEFAULT_BASE_URL;
11387
- if (!apiKey)
11388
- throw new Error("THINKER_LABS_API_KEY environment variable is required");
11389
- return { apiKey, baseUrl };
11390
- }
11391
- async function request(method, path, body, file) {
11392
- const { apiKey, baseUrl } = getConfig();
11393
- const headers = { Authorization: `Bearer ${apiKey}` };
11394
- let fetchBody;
11395
- if (file) {
11396
- const form = new FormData;
11397
- form.append("file", new Blob([file.data], { type: "text/plain" }), file.name);
11398
- if (body) {
11399
- for (const [k, v] of Object.entries(body)) {
11400
- form.append(k, v);
11401
- }
11402
- }
11403
- fetchBody = form;
11404
- } else if (body) {
11405
- headers["Content-Type"] = "application/json";
11406
- fetchBody = JSON.stringify(body);
11407
- }
11408
- const res = await fetch(`${baseUrl}${path}`, { method, headers, body: fetchBody });
11409
- if (!res.ok) {
11410
- const text2 = await res.text();
11411
- throw new Error(`Thinker Labs API error ${res.status}: ${text2}`);
11412
- }
11413
- if (res.status === 204)
11414
- return;
11415
- return res.json();
11416
- }
11417
- async function uploadTrainingData(filePath) {
11418
- const fileContent = await Bun.file(filePath).text();
11419
- const fileName = filePath.split("/").pop() ?? "training.jsonl";
11420
- const result = await request("POST", "/datasets/upload", undefined, { name: fileName, data: fileContent });
11421
- return { datasetId: result.id };
11422
- }
11423
- async function startFineTune(datasetId, baseModel, name) {
11424
- const result = await request("POST", "/fine-tunes", {
11425
- dataset_id: datasetId,
11426
- base_model: baseModel,
11427
- ...name ? { name } : {}
11428
- });
11429
- return { jobId: result.id, status: result.status };
11430
- }
11431
- async function getStatus(jobId) {
11432
- const result = await request("GET", `/fine-tunes/${jobId}`);
11433
- return {
11434
- jobId: result.id,
11435
- status: result.status,
11436
- modelId: result.model_id,
11437
- error: result.error
11438
- };
11439
- }
11440
- async function listModels() {
11441
- const result = await request("GET", "/fine-tunes");
11442
- return (result.data ?? []).map((m) => ({
11443
- id: m.id,
11444
- name: m.name,
11445
- status: m.status,
11446
- baseModel: m.base_model,
11447
- createdAt: m.created_at
11448
- }));
11449
- }
11450
- async function cancelJob(jobId) {
11451
- await request("DELETE", `/fine-tunes/${jobId}`);
11452
- }
11453
-
11454
- class ThinkerLabsProvider {
11455
- uploadTrainingData = uploadTrainingData;
11456
- startFineTune = startFineTune;
11457
- getStatus = getStatus;
11458
- listModels = listModels;
11459
- cancelJob = cancelJob;
11460
- async uploadTrainingFile(filePath) {
11461
- const { datasetId } = await uploadTrainingData(filePath);
11462
- return { fileId: datasetId };
11463
- }
11464
- async createFineTuneJob(fileId, baseModel, suffix) {
11465
- return startFineTune(fileId, baseModel, suffix);
11466
- }
11467
- async getFineTuneStatus(jobId) {
11468
- const result = await getStatus(jobId);
11469
- return { jobId: result.jobId, status: result.status, fineTunedModel: result.modelId, error: result.error };
11470
- }
11471
- async listFineTunedModels() {
11472
- const models = await listModels();
11473
- return models.map((m) => ({ id: m.id, model: m.baseModel, status: m.status, created: m.createdAt }));
11474
- }
11475
- async cancelFineTuneJob(jobId) {
11476
- return cancelJob(jobId);
11477
- }
11478
- }
11479
-
11480
- // src/cli/ui.ts
11481
- import chalk from "chalk";
11482
- function printTable(headers, rows) {
11483
- if (rows.length === 0) {
11484
- console.log(chalk.dim(" (no records)"));
11485
- return;
11486
- }
11487
- const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
11488
- const separator = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
11489
- const headerLine = headers.map((h, i) => ` ${chalk.bold(h.padEnd(colWidths[i] ?? 0))} `).join("\u2502");
11490
- const topBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C");
11491
- const midBorder = separator;
11492
- const bottomBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534");
11493
- console.log("\u250C" + topBorder + "\u2510");
11494
- console.log("\u2502" + headerLine + "\u2502");
11495
- console.log("\u251C" + midBorder + "\u2524");
11496
- for (const row of rows) {
11497
- const rowLine = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i] ?? 0)} `).join("\u2502");
11498
- console.log("\u2502" + rowLine + "\u2502");
11499
- }
11500
- console.log("\u2514" + bottomBorder + "\u2518");
11501
- }
11502
- var STATUS_COLORS = {
11503
- succeeded: chalk.green,
11504
- running: chalk.cyan,
11505
- pending: chalk.yellow,
11506
- failed: chalk.red,
11507
- cancelled: chalk.dim,
11508
- queued: chalk.yellow,
11509
- validating_files: chalk.blue
11510
- };
11511
- function printStatus(status) {
11512
- const colorFn = STATUS_COLORS[status] ?? chalk.white;
11513
- return colorFn(`\u25CF ${status}`);
11514
- }
11515
- function printJson(obj) {
11516
- console.log(JSON.stringify(obj, null, 2));
11517
- }
11518
- function printError(message) {
11519
- console.error(chalk.red("\u2717 Error: ") + message);
11520
- }
11521
- function printSuccess(message) {
11522
- console.log(chalk.green("\u2713 ") + message);
11523
- }
11524
- function printInfo(message) {
11525
- console.log(chalk.dim(" " + message));
11526
- }
11527
-
11528
- // src/lib/gatherers/todos.ts
11529
- import { Database as Database3 } from "bun:sqlite";
11530
- import { homedir as homedir2 } from "os";
11531
- import { join as join2 } from "path";
11532
- var SYSTEM_PROMPT = "You are a task management assistant that helps users create, update, search, and manage tasks and projects.";
11533
- function taskToCreateExample(task) {
11534
- const userMsg = `Create a task: ${task.title}${task.description ? `
11535
-
11536
- Description: ${task.description}` : ""}`;
11537
- const taskDetails = {
11538
- id: task.short_id ?? task.id,
11539
- title: task.title,
11540
- description: task.description ?? "",
11541
- status: task.status,
11542
- priority: task.priority,
11543
- tags: JSON.parse(task.tags ?? "[]"),
11544
- created_at: task.created_at
11545
- };
11546
- return {
11547
- messages: [
11548
- { role: "system", content: SYSTEM_PROMPT },
11549
- { role: "user", content: userMsg },
11550
- { role: "assistant", content: `Created task: ${JSON.stringify(taskDetails, null, 2)}` }
11551
- ]
11552
- };
11553
- }
11554
- function taskToStatusUpdateExample(task) {
11555
- if (!task.completed_at && task.status === "pending")
11556
- return null;
11557
- const id = task.short_id ?? task.id;
11558
- return {
11559
- messages: [
11560
- { role: "system", content: SYSTEM_PROMPT },
11561
- { role: "user", content: `Mark task ${id} as ${task.status}` },
11562
- { role: "assistant", content: `Task ${id} has been updated to status: ${task.status}. ${task.completed_at ? `Completed at: ${task.completed_at}` : ""}`.trim() }
11563
- ]
11564
- };
11565
- }
11566
- function taskToSearchExample(tasks, query) {
11567
- const matched = tasks.filter((t) => t.title.toLowerCase().includes(query.toLowerCase())).slice(0, 5);
11568
- return {
11569
- messages: [
11570
- { role: "system", content: SYSTEM_PROMPT },
11571
- { role: "user", content: `Search tasks for: "${query}"` },
11572
- {
11573
- role: "assistant",
11574
- content: matched.length > 0 ? `Found ${matched.length} task(s):
11575
- ${matched.map((t) => `- [${t.short_id ?? t.id}] ${t.title} (${t.status})`).join(`
11576
- `)}` : `No tasks found matching "${query}".`
11734
+ "/images/edits"
11735
+ ]);
11736
+ var openai_default = OpenAI;
11737
+
11738
+ // src/lib/providers/openai.ts
11739
+ import { readFileSync as readFileSync2 } from "fs";
11740
+
11741
+ // src/lib/config.ts
11742
+ import { readFileSync, writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync as readdirSync2, copyFileSync as copyFileSync2, statSync as statSync2 } from "fs";
11743
+ import { join as join2, dirname as dirname2 } from "path";
11744
+ import { homedir as homedir2 } from "os";
11745
+ var CONFIG_KEYS = ["OPENAI_API_KEY", "THINKER_LABS_API_KEY", "THINKER_LABS_BASE_URL"];
11746
+ function resolveConfigPath() {
11747
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
11748
+ const newDir = join2(home, ".hasna", "brains");
11749
+ const oldDir = join2(home, ".brains");
11750
+ if (existsSync2(oldDir) && !existsSync2(newDir)) {
11751
+ mkdirSync2(newDir, { recursive: true });
11752
+ try {
11753
+ for (const file of readdirSync2(oldDir)) {
11754
+ const oldPath = join2(oldDir, file);
11755
+ const newPath = join2(newDir, file);
11756
+ try {
11757
+ if (statSync2(oldPath).isFile()) {
11758
+ copyFileSync2(oldPath, newPath);
11759
+ }
11760
+ } catch {}
11577
11761
  }
11578
- ]
11579
- };
11762
+ } catch {}
11763
+ }
11764
+ mkdirSync2(newDir, { recursive: true });
11765
+ return join2(newDir, "config.json");
11580
11766
  }
11581
- async function gatherFromTodos(options = {}) {
11582
- const dbPath = join2(homedir2(), ".todos", "todos.db");
11583
- const db = new Database3(dbPath, { readonly: true, create: false });
11767
+ var CONFIG_PATH = resolveConfigPath();
11768
+ function readConfigFile() {
11769
+ if (!existsSync2(CONFIG_PATH))
11770
+ return {};
11584
11771
  try {
11585
- let query = "SELECT * FROM tasks WHERE 1=1";
11586
- const params = [];
11587
- if (options.since) {
11588
- query += " AND created_at >= ?";
11589
- params.push(options.since.toISOString());
11590
- }
11591
- query += " ORDER BY created_at DESC";
11592
- if (options.limit) {
11593
- query += " LIMIT ?";
11594
- params.push(options.limit * 2);
11595
- }
11596
- const tasks = db.query(query).all(...params);
11597
- const examples = [];
11598
- for (const task of tasks) {
11599
- examples.push(taskToCreateExample(task));
11600
- const statusEx = taskToStatusUpdateExample(task);
11601
- if (statusEx)
11602
- examples.push(statusEx);
11603
- }
11604
- const searchTerms = ["urgent", "fix", "implement", "create", "update", "review"];
11605
- for (const term of searchTerms) {
11606
- examples.push(taskToSearchExample(tasks, term));
11607
- }
11608
- const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
11609
- return {
11610
- source: "todos",
11611
- examples: finalExamples,
11612
- count: finalExamples.length
11613
- };
11614
- } finally {
11615
- db.close();
11772
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
11773
+ } catch {
11774
+ return {};
11616
11775
  }
11617
11776
  }
11618
-
11619
- // src/lib/gatherers/mementos.ts
11620
- import { Database as Database4 } from "bun:sqlite";
11621
- import { homedir as homedir3 } from "os";
11622
- import { join as join3 } from "path";
11623
- var SYSTEM_PROMPT2 = "You are an AI assistant with persistent memory. You can remember and recall information across sessions to provide better, more personalized assistance.";
11624
- function memoryToRecallExample(memory) {
11625
- return {
11626
- messages: [
11627
- { role: "system", content: SYSTEM_PROMPT2 },
11628
- { role: "user", content: `What do you remember about "${memory.key}"?` },
11629
- {
11630
- role: "assistant",
11631
- content: memory.summary ? `${memory.value}
11632
-
11633
- Summary: ${memory.summary}` : memory.value
11634
- }
11635
- ]
11636
- };
11637
- }
11638
- function memoryToSaveExample(memory) {
11639
- const tags = JSON.parse(memory.tags ?? "[]");
11640
- return {
11641
- messages: [
11642
- { role: "system", content: SYSTEM_PROMPT2 },
11643
- {
11644
- role: "user",
11645
- content: `Remember this for me: ${memory.key} = ${memory.value}${tags.length ? ` (tags: ${tags.join(", ")})` : ""}`
11646
- },
11647
- {
11648
- role: "assistant",
11649
- content: `Saved to memory: "${memory.key}" with ${memory.category} category, importance ${memory.importance}/10, scope: ${memory.scope}.`
11650
- }
11651
- ]
11652
- };
11777
+ function writeConfigFile(data) {
11778
+ mkdirSync2(dirname2(CONFIG_PATH), { recursive: true });
11779
+ writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2) + `
11780
+ `, "utf-8");
11781
+ }
11782
+ function getConfigValue(key) {
11783
+ if (process.env[key])
11784
+ return process.env[key];
11785
+ return readConfigFile()[key];
11786
+ }
11787
+ function setConfigValue(key, value) {
11788
+ const data = readConfigFile();
11789
+ data[key] = value;
11790
+ writeConfigFile(data);
11791
+ }
11792
+ function listConfig() {
11793
+ const file = readConfigFile();
11794
+ return CONFIG_KEYS.map((key) => {
11795
+ if (process.env[key])
11796
+ return { key, value: process.env[key], source: "env" };
11797
+ if (file[key])
11798
+ return { key, value: file[key], source: "file" };
11799
+ return { key, value: "", source: "unset" };
11800
+ });
11653
11801
  }
11654
- function memoryToSearchExample(memories, category) {
11655
- const matched = memories.filter((m) => m.category === category && m.status === "active").slice(0, 5);
11656
- return {
11657
- messages: [
11658
- { role: "system", content: SYSTEM_PROMPT2 },
11659
- { role: "user", content: `What ${category} memories do you have?` },
11660
- {
11661
- role: "assistant",
11662
- content: matched.length > 0 ? `Here are my ${category} memories:
11663
- ${matched.map((m) => `- ${m.key}: ${m.value.slice(0, 120)}${m.value.length > 120 ? "..." : ""}`).join(`
11664
- `)}` : `I don't have any ${category} memories stored yet.`
11665
- }
11666
- ]
11667
- };
11802
+ function deleteConfigValue(key) {
11803
+ const data = readConfigFile();
11804
+ delete data[key];
11805
+ writeConfigFile(data);
11668
11806
  }
11669
- async function gatherFromMementos(options = {}) {
11670
- const dbPath = join3(homedir3(), ".mementos", "mementos.db");
11671
- const db = new Database4(dbPath, { readonly: true, create: false });
11672
- try {
11673
- let query = "SELECT * FROM memories WHERE status = 'active'";
11674
- const params = [];
11675
- if (options.since) {
11676
- query += " AND created_at >= ?";
11677
- params.push(options.since.toISOString());
11807
+
11808
+ // src/lib/retry.ts
11809
+ var RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
11810
+ function isRetryableError(err) {
11811
+ if (err instanceof Error) {
11812
+ if (err.message.includes("fetch failed") || err.message.includes("ECONNRESET") || err.message.includes("ETIMEDOUT")) {
11813
+ return true;
11678
11814
  }
11679
- query += " ORDER BY importance DESC, created_at DESC";
11680
- if (options.limit) {
11681
- query += " LIMIT ?";
11682
- params.push(options.limit * 3);
11815
+ const match = err.message.match(/^(\d{3})\s/);
11816
+ if (match) {
11817
+ const status = parseInt(match[1], 10);
11818
+ return RETRYABLE_STATUS_CODES.has(status);
11683
11819
  }
11684
- const memories = db.query(query).all(...params);
11685
- const examples = [];
11686
- for (const memory of memories) {
11687
- examples.push(memoryToRecallExample(memory));
11688
- examples.push(memoryToSaveExample(memory));
11820
+ const tlMatch = err.message.match(/API error (\d{3})/);
11821
+ if (tlMatch) {
11822
+ const status = parseInt(tlMatch[1], 10);
11823
+ return RETRYABLE_STATUS_CODES.has(status);
11689
11824
  }
11690
- const categories = [...new Set(memories.map((m) => m.category))];
11691
- for (const category of categories) {
11692
- examples.push(memoryToSearchExample(memories, category));
11825
+ }
11826
+ return false;
11827
+ }
11828
+ async function withRetry(fn, options = {}) {
11829
+ const { maxAttempts = 3, baseDelayMs = 1000, onRetry } = options;
11830
+ let lastError = new Error("Unknown error");
11831
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
11832
+ try {
11833
+ return await fn();
11834
+ } catch (err) {
11835
+ lastError = err instanceof Error ? err : new Error(String(err));
11836
+ if (attempt === maxAttempts || !isRetryableError(lastError)) {
11837
+ throw lastError;
11838
+ }
11839
+ const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
11840
+ onRetry?.(attempt, delayMs, lastError);
11841
+ await new Promise((resolve2) => setTimeout(resolve2, delayMs));
11693
11842
  }
11694
- const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
11695
- return {
11696
- source: "mementos",
11697
- examples: finalExamples,
11698
- count: finalExamples.length
11699
- };
11700
- } finally {
11701
- db.close();
11702
11843
  }
11844
+ throw lastError;
11703
11845
  }
11704
11846
 
11705
- // src/lib/gatherers/conversations.ts
11706
- import { Database as Database5 } from "bun:sqlite";
11707
- import { homedir as homedir4 } from "os";
11708
- import { join as join4 } from "path";
11709
- var SYSTEM_PROMPT3 = "You are a helpful AI assistant participating in multi-agent conversations. You communicate clearly and collaboratively with other agents and users.";
11710
- function windowToExample(window2) {
11711
- if (window2.length < 2)
11712
- return null;
11713
- const messages = [
11714
- { role: "system", content: SYSTEM_PROMPT3 }
11715
- ];
11716
- for (let i = 0;i < window2.length - 1; i++) {
11717
- const msg = window2[i];
11718
- if (!msg)
11719
- continue;
11720
- const role = i % 2 === 0 ? "user" : "assistant";
11721
- messages.push({
11722
- role,
11723
- content: `[${msg.from_agent} \u2192 ${msg.to_agent ?? msg.space ?? "all"}]: ${msg.content}`
11724
- });
11847
+ // src/lib/providers/openai.ts
11848
+ function getClient() {
11849
+ const apiKey = getConfigValue("OPENAI_API_KEY");
11850
+ if (!apiKey) {
11851
+ throw new Error("OPENAI_API_KEY is not set. Run: brains config set OPENAI_API_KEY <key>");
11725
11852
  }
11726
- const last = window2[window2.length - 1];
11727
- if (!last)
11728
- return null;
11729
- messages.push({
11730
- role: "assistant",
11731
- content: `[${last.from_agent} \u2192 ${last.to_agent ?? last.space ?? "all"}]: ${last.content}`
11853
+ return new openai_default({ apiKey });
11854
+ }
11855
+ async function uploadTrainingFile(filePath) {
11856
+ return withRetry(async () => {
11857
+ const client = getClient();
11858
+ const fileContent = readFileSync2(filePath);
11859
+ const blob2 = new Blob([fileContent], { type: "application/jsonl" });
11860
+ const file = new File([blob2], filePath.split("/").pop() ?? "training.jsonl", { type: "application/jsonl" });
11861
+ const response = await client.files.create({ file, purpose: "fine-tune" });
11862
+ return { fileId: response.id };
11732
11863
  });
11733
- return { messages };
11734
11864
  }
11735
- async function gatherFromConversations(options = {}) {
11736
- const dbPath = join4(homedir4(), ".conversations", "messages.db");
11737
- const db = new Database5(dbPath, { readonly: true, create: false });
11738
- try {
11739
- let query = "SELECT * FROM messages WHERE 1=1";
11740
- const params = [];
11741
- if (options.since) {
11742
- query += " AND created_at >= ?";
11743
- params.push(options.since.toISOString());
11744
- }
11745
- query += " ORDER BY session_id, created_at ASC";
11746
- const allMessages = db.query(query).all(...params);
11747
- const sessions = new Map;
11748
- for (const msg of allMessages) {
11749
- const msgs = sessions.get(msg.session_id) ?? [];
11750
- msgs.push(msg);
11751
- sessions.set(msg.session_id, msgs);
11752
- }
11753
- const examples = [];
11754
- const windowSize = 4;
11755
- for (const [, sessionMsgs] of sessions) {
11756
- if (sessionMsgs.length < 2)
11757
- continue;
11758
- for (let start = 0;start <= sessionMsgs.length - 2; start++) {
11759
- const end = Math.min(start + windowSize, sessionMsgs.length);
11760
- const window2 = sessionMsgs.slice(start, end);
11761
- const example = windowToExample(window2);
11762
- if (example)
11763
- examples.push(example);
11764
- }
11765
- }
11766
- const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
11865
+ async function createFineTuneJob(fileId, baseModel, suffix) {
11866
+ return withRetry(async () => {
11867
+ const client = getClient();
11868
+ const params = { training_file: fileId, model: baseModel };
11869
+ if (suffix)
11870
+ params.suffix = suffix;
11871
+ const response = await client.fineTuning.jobs.create(params);
11872
+ return { jobId: response.id, status: response.status };
11873
+ });
11874
+ }
11875
+ async function getFineTuneStatus(jobId) {
11876
+ return withRetry(async () => {
11877
+ const client = getClient();
11878
+ const response = await client.fineTuning.jobs.retrieve(jobId);
11767
11879
  return {
11768
- source: "conversations",
11769
- examples: finalExamples,
11770
- count: finalExamples.length
11880
+ jobId: response.id,
11881
+ status: response.status,
11882
+ fineTunedModel: response.fine_tuned_model ?? undefined,
11883
+ baseModel: response.model ?? undefined,
11884
+ error: response.error?.message ?? undefined
11771
11885
  };
11772
- } finally {
11773
- db.close();
11774
- }
11886
+ });
11887
+ }
11888
+ async function listFineTunedModels() {
11889
+ return withRetry(async () => {
11890
+ const client = getClient();
11891
+ const jobs = await client.fineTuning.jobs.list();
11892
+ return jobs.data.map((job) => ({
11893
+ id: job.id,
11894
+ model: job.fine_tuned_model ?? job.model,
11895
+ status: job.status,
11896
+ created: job.created_at
11897
+ }));
11898
+ });
11899
+ }
11900
+
11901
+ // src/lib/providers/thinker-labs.ts
11902
+ var DEFAULT_BASE_URL = "https://api.thinkerlabs.ai/v1";
11903
+ function getConfig() {
11904
+ const apiKey = getConfigValue("THINKER_LABS_API_KEY");
11905
+ const baseUrl = getConfigValue("THINKER_LABS_BASE_URL") ?? DEFAULT_BASE_URL;
11906
+ if (!apiKey)
11907
+ throw new Error("THINKER_LABS_API_KEY is not set. Run: brains config set THINKER_LABS_API_KEY <key>");
11908
+ return { apiKey, baseUrl };
11909
+ }
11910
+ async function request(method, path, body, file) {
11911
+ return withRetry(async () => {
11912
+ const { apiKey, baseUrl } = getConfig();
11913
+ const headers = { Authorization: `Bearer ${apiKey}` };
11914
+ let fetchBody;
11915
+ if (file) {
11916
+ const form = new FormData;
11917
+ form.append("file", new Blob([file.data], { type: "text/plain" }), file.name);
11918
+ if (body) {
11919
+ for (const [k, v] of Object.entries(body)) {
11920
+ form.append(k, v);
11921
+ }
11922
+ }
11923
+ fetchBody = form;
11924
+ } else if (body) {
11925
+ headers["Content-Type"] = "application/json";
11926
+ fetchBody = JSON.stringify(body);
11927
+ }
11928
+ const res = await fetch(`${baseUrl}${path}`, { method, headers, body: fetchBody });
11929
+ if (!res.ok) {
11930
+ const text2 = await res.text();
11931
+ throw new Error(`Thinker Labs API error ${res.status}: ${text2}`);
11932
+ }
11933
+ if (res.status === 204)
11934
+ return;
11935
+ return res.json();
11936
+ });
11937
+ }
11938
+ async function uploadTrainingData(filePath) {
11939
+ const fileContent = await Bun.file(filePath).text();
11940
+ const fileName = filePath.split("/").pop() ?? "training.jsonl";
11941
+ const result = await request("POST", "/datasets/upload", undefined, { name: fileName, data: fileContent });
11942
+ return { datasetId: result.id };
11943
+ }
11944
+ async function startFineTune(datasetId, baseModel, name) {
11945
+ const result = await request("POST", "/fine-tunes", {
11946
+ dataset_id: datasetId,
11947
+ base_model: baseModel,
11948
+ ...name ? { name } : {}
11949
+ });
11950
+ return { jobId: result.id, status: result.status };
11951
+ }
11952
+ async function getStatus(jobId) {
11953
+ const result = await request("GET", `/fine-tunes/${jobId}`);
11954
+ return {
11955
+ jobId: result.id,
11956
+ status: result.status,
11957
+ modelId: result.model_id,
11958
+ error: result.error
11959
+ };
11960
+ }
11961
+ async function listModels() {
11962
+ const result = await request("GET", "/fine-tunes");
11963
+ return (result.data ?? []).map((m) => ({
11964
+ id: m.id,
11965
+ name: m.name,
11966
+ status: m.status,
11967
+ baseModel: m.base_model,
11968
+ createdAt: m.created_at
11969
+ }));
11970
+ }
11971
+ async function cancelJob(jobId) {
11972
+ await request("DELETE", `/fine-tunes/${jobId}`);
11775
11973
  }
11776
11974
 
11777
- // src/lib/gatherers/sessions.ts
11778
- import { readdir, readFile, stat } from "fs/promises";
11779
- import { existsSync } from "fs";
11780
- import { join as join5 } from "path";
11781
- import { homedir as homedir5 } from "os";
11782
- var SYSTEM_PROMPT4 = "You are Claude Code, an AI assistant built by Anthropic that helps developers with coding, architecture, debugging, and software engineering tasks.";
11783
- function extractText(content) {
11784
- if (typeof content === "string")
11785
- return content;
11786
- return content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join(`
11787
- `).trim();
11975
+ class ThinkerLabsProvider {
11976
+ uploadTrainingData = uploadTrainingData;
11977
+ startFineTune = startFineTune;
11978
+ getStatus = getStatus;
11979
+ listModels = listModels;
11980
+ cancelJob = cancelJob;
11981
+ async uploadTrainingFile(filePath) {
11982
+ const { datasetId } = await uploadTrainingData(filePath);
11983
+ return { fileId: datasetId };
11984
+ }
11985
+ async createFineTuneJob(fileId, baseModel, suffix) {
11986
+ return startFineTune(fileId, baseModel, suffix);
11987
+ }
11988
+ async getFineTuneStatus(jobId) {
11989
+ const result = await getStatus(jobId);
11990
+ return { jobId: result.jobId, status: result.status, fineTunedModel: result.modelId, error: result.error };
11991
+ }
11992
+ async listFineTunedModels() {
11993
+ const models = await listModels();
11994
+ return models.map((m) => ({ id: m.id, model: m.baseModel, status: m.status, created: m.createdAt }));
11995
+ }
11996
+ async cancelFineTuneJob(jobId) {
11997
+ return cancelJob(jobId);
11998
+ }
11788
11999
  }
11789
- async function gatherFromSessions(options = {}) {
11790
- const { limit: limit2 = 1000 } = options;
11791
- const examples = [];
11792
- const claudeDir = join5(homedir5(), ".claude", "projects");
11793
- if (!existsSync(claudeDir)) {
11794
- return { source: "sessions", examples: [], count: 0 };
12000
+
12001
+ // src/cli/ui.ts
12002
+ import chalk from "chalk";
12003
+ function printTable(headers, rows) {
12004
+ if (rows.length === 0) {
12005
+ console.log(chalk.dim(" (no records)"));
12006
+ return;
11795
12007
  }
11796
- const projectDirs = await readdir(claudeDir).catch(() => []);
11797
- for (const projectDir of projectDirs) {
11798
- if (examples.length >= limit2)
11799
- break;
11800
- const projectPath = join5(claudeDir, projectDir);
11801
- const files = await readdir(projectPath).catch(() => []);
11802
- for (const file of files) {
11803
- if (examples.length >= limit2)
11804
- break;
11805
- if (!file.endsWith(".jsonl"))
11806
- continue;
11807
- const filePath = join5(projectPath, file);
11808
- if (options.since) {
11809
- const fileStat = await stat(filePath).catch(() => null);
11810
- if (fileStat && fileStat.mtime < options.since)
11811
- continue;
11812
- }
11813
- const content = await readFile(filePath, "utf-8").catch(() => "");
11814
- if (!content.trim())
11815
- continue;
11816
- const lines = content.trim().split(`
11817
- `);
11818
- const turns = [];
11819
- for (const line of lines) {
11820
- try {
11821
- const entry = JSON.parse(line);
11822
- if ((entry.type === "user" || entry.type === "human") && entry.message?.content) {
11823
- const text2 = extractText(entry.message.content);
11824
- if (text2.trim())
11825
- turns.push({ role: "user", content: text2.trim() });
11826
- } else if (entry.type === "assistant" && entry.message?.content) {
11827
- const text2 = extractText(entry.message.content);
11828
- if (text2.trim())
11829
- turns.push({ role: "assistant", content: text2.trim() });
11830
- }
11831
- } catch {}
11832
- }
11833
- const windowSize = 6;
11834
- for (let start = 0;start < turns.length - 1 && examples.length < limit2; start++) {
11835
- const window2 = turns.slice(start, start + windowSize);
11836
- if (!window2[0] || window2[0].role !== "user")
11837
- continue;
11838
- const lastAssistantIdx = window2.map((t) => t.role).lastIndexOf("assistant");
11839
- if (lastAssistantIdx < 1)
11840
- continue;
11841
- const usedTurns = window2.slice(0, lastAssistantIdx + 1);
11842
- examples.push({
11843
- messages: [
11844
- { role: "system", content: SYSTEM_PROMPT4 },
11845
- ...usedTurns
11846
- ]
11847
- });
11848
- }
11849
- }
12008
+ const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
12009
+ const separator = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
12010
+ const headerLine = headers.map((h, i) => ` ${chalk.bold(h.padEnd(colWidths[i] ?? 0))} `).join("\u2502");
12011
+ const topBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C");
12012
+ const midBorder = separator;
12013
+ const bottomBorder = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534");
12014
+ console.log("\u250C" + topBorder + "\u2510");
12015
+ console.log("\u2502" + headerLine + "\u2502");
12016
+ console.log("\u251C" + midBorder + "\u2524");
12017
+ for (const row of rows) {
12018
+ const rowLine = row.map((cell, i) => ` ${(cell ?? "").padEnd(colWidths[i] ?? 0)} `).join("\u2502");
12019
+ console.log("\u2502" + rowLine + "\u2502");
11850
12020
  }
11851
- return { source: "sessions", examples, count: examples.length };
12021
+ console.log("\u2514" + bottomBorder + "\u2518");
11852
12022
  }
11853
- // src/lib/gatherers/index.ts
11854
- var ALL_SOURCES = ["todos", "mementos", "conversations", "sessions"];
11855
- async function gatherAll(sources, options = {}) {
11856
- const targets = sources.includes("all") ? [...ALL_SOURCES] : sources;
11857
- const results = await Promise.allSettled(targets.map((source) => {
11858
- switch (source) {
11859
- case "todos":
11860
- return gatherFromTodos(options);
11861
- case "mementos":
11862
- return gatherFromMementos(options);
11863
- case "conversations":
11864
- return gatherFromConversations(options);
11865
- case "sessions":
11866
- return gatherFromSessions(options);
11867
- default:
11868
- return Promise.resolve({ source, examples: [], count: 0 });
11869
- }
11870
- }));
11871
- return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
12023
+ var STATUS_COLORS = {
12024
+ succeeded: chalk.green,
12025
+ running: chalk.cyan,
12026
+ pending: chalk.yellow,
12027
+ failed: chalk.red,
12028
+ cancelled: chalk.dim,
12029
+ queued: chalk.yellow,
12030
+ validating_files: chalk.blue
12031
+ };
12032
+ function printStatus(status) {
12033
+ const colorFn = STATUS_COLORS[status] ?? chalk.white;
12034
+ return colorFn(`\u25CF ${status}`);
12035
+ }
12036
+ function printJson(obj) {
12037
+ console.log(JSON.stringify(obj, null, 2));
12038
+ }
12039
+ function printError(message) {
12040
+ console.error(chalk.red("\u2717 Error: ") + message);
12041
+ }
12042
+ function printSuccess(message) {
12043
+ console.log(chalk.green("\u2713 ") + message);
12044
+ }
12045
+ function printInfo(message) {
12046
+ console.log(chalk.dim(" " + message));
11872
12047
  }
11873
12048
 
11874
12049
  // src/cli/index.ts
11875
12050
  var program2 = new Command;
11876
12051
  program2.name("brains").description("Fine-tuned model tracker and trainer").version("0.0.1");
11877
12052
  var modelsCmd = program2.command("models").description("Manage tracked fine-tuned models");
11878
- modelsCmd.command("list").description("List all tracked fine-tuned models").action(async () => {
12053
+ modelsCmd.command("list").description("List all tracked fine-tuned models").option("--json", "Output as JSON").action(async (opts) => {
11879
12054
  try {
11880
12055
  const db = getDb();
11881
12056
  const models = await db.select().from(fineTunedModels);
12057
+ if (opts.json) {
12058
+ printJson(models);
12059
+ return;
12060
+ }
11882
12061
  if (models.length === 0) {
11883
12062
  printInfo("No models tracked yet. Use 'brains finetune start' to train one.");
11884
12063
  return;
@@ -11896,14 +12075,22 @@ modelsCmd.command("list").description("List all tracked fine-tuned models").acti
11896
12075
  process.exit(1);
11897
12076
  }
11898
12077
  });
11899
- modelsCmd.command("show <id>").description("Show details of a specific model").action(async (id) => {
12078
+ modelsCmd.command("show <id>").description("Show details of a specific model").option("--json", "Output as JSON").action(async (id, opts) => {
11900
12079
  try {
11901
12080
  const db = getDb();
11902
12081
  const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.id, id));
11903
12082
  if (!model) {
11904
- printError(`Model not found: ${id}`);
12083
+ if (opts.json) {
12084
+ printJson({ error: `Model not found: ${id}` });
12085
+ } else {
12086
+ printError(`Model not found: ${id}`);
12087
+ }
11905
12088
  process.exit(1);
11906
12089
  }
12090
+ if (opts.json) {
12091
+ printJson(model);
12092
+ return;
12093
+ }
11907
12094
  console.log();
11908
12095
  const tagsList = model.tags ? JSON.parse(model.tags).join(", ") : "(none)";
11909
12096
  console.log(` ID: ${model.id}`);
@@ -11990,29 +12177,89 @@ modelsCmd.command("collection <id> <collectionName>").description("Set the colle
11990
12177
  process.exit(1);
11991
12178
  }
11992
12179
  });
12180
+ 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) => {
12181
+ try {
12182
+ let result;
12183
+ if (opts.provider === "openai") {
12184
+ result = await getFineTuneStatus(jobId);
12185
+ } else {
12186
+ const tl = new ThinkerLabsProvider;
12187
+ result = await tl.getFineTuneStatus(jobId);
12188
+ }
12189
+ const db = getDb();
12190
+ const [existing] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
12191
+ if (existing) {
12192
+ printInfo(`Model already tracked as: ${existing.id}`);
12193
+ return;
12194
+ }
12195
+ const modelId = randomUUID();
12196
+ const now = Date.now();
12197
+ const name = opts.name ?? result.fineTunedModel ?? `imported-${jobId}`;
12198
+ await db.insert(fineTunedModels).values({
12199
+ id: modelId,
12200
+ name,
12201
+ provider: opts.provider,
12202
+ baseModel: result.baseModel ?? "unknown",
12203
+ status: result.status,
12204
+ fineTuneJobId: jobId,
12205
+ createdAt: now,
12206
+ updatedAt: now
12207
+ });
12208
+ await db.insert(trainingJobs).values({
12209
+ id: randomUUID(),
12210
+ modelId,
12211
+ provider: opts.provider,
12212
+ status: result.status,
12213
+ startedAt: now
12214
+ });
12215
+ printSuccess(`Model imported successfully.`);
12216
+ console.log();
12217
+ console.log(` Local ID: ${modelId}`);
12218
+ console.log(` Job ID: ${jobId}`);
12219
+ console.log(` Name: ${name}`);
12220
+ console.log(` Status: ${printStatus(result.status)}`);
12221
+ if (result.fineTunedModel)
12222
+ console.log(` Model: ${result.fineTunedModel}`);
12223
+ console.log();
12224
+ } catch (err) {
12225
+ printError(err instanceof Error ? err.message : String(err));
12226
+ process.exit(1);
12227
+ }
12228
+ });
11993
12229
  var finetuneCmd = program2.command("finetune").description("Manage fine-tuning jobs");
11994
- 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)").requiredOption("--dataset <path>", "Path to the JSONL training dataset").requiredOption("--name <name>", "Human-readable name for this fine-tuned model").action(async (opts) => {
12230
+ 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) => {
11995
12231
  try {
11996
12232
  if (opts.provider !== "openai" && opts.provider !== "thinker-labs") {
11997
12233
  printError(`Unknown provider: ${opts.provider}. Use 'openai' or 'thinker-labs'.`);
11998
12234
  process.exit(1);
11999
12235
  }
12000
- if (!existsSync2(opts.dataset)) {
12001
- printError(`Dataset file not found: ${opts.dataset}`);
12236
+ let datasetPath = opts.dataset;
12237
+ if (!datasetPath) {
12238
+ const db2 = getDb();
12239
+ const [latest] = await db2.select().from(trainingDatasets).orderBy(desc(trainingDatasets.createdAt)).limit(1);
12240
+ if (!latest?.filePath) {
12241
+ printError("No datasets found. Run 'brains data gather' first.");
12242
+ process.exit(1);
12243
+ }
12244
+ datasetPath = latest.filePath;
12245
+ printInfo(`Using latest dataset: ${datasetPath} (${latest.exampleCount} examples)`);
12246
+ }
12247
+ if (!existsSync4(datasetPath)) {
12248
+ printError(`Dataset file not found: ${datasetPath}`);
12002
12249
  process.exit(1);
12003
12250
  }
12004
- printInfo(`Uploading training file: ${opts.dataset} \u2026`);
12251
+ printInfo(`Uploading training file: ${datasetPath} \u2026`);
12005
12252
  let fileId;
12006
12253
  let jobId;
12007
12254
  let jobStatus;
12008
12255
  if (opts.provider === "openai") {
12009
- ({ fileId } = await uploadTrainingFile(opts.dataset));
12256
+ ({ fileId } = await uploadTrainingFile(datasetPath));
12010
12257
  printSuccess(`File uploaded. fileId = ${fileId}`);
12011
12258
  printInfo(`Creating fine-tune job on OpenAI \u2026`);
12012
12259
  ({ jobId, status: jobStatus } = await createFineTuneJob(fileId, opts.baseModel, opts.name));
12013
12260
  } else {
12014
12261
  const tl = new ThinkerLabsProvider;
12015
- ({ fileId } = await tl.uploadTrainingFile(opts.dataset));
12262
+ ({ fileId } = await tl.uploadTrainingFile(datasetPath));
12016
12263
  printSuccess(`File uploaded. fileId = ${fileId}`);
12017
12264
  printInfo(`Creating fine-tune job on Thinker Labs \u2026`);
12018
12265
  ({ jobId, status: jobStatus } = await tl.createFineTuneJob(fileId, opts.baseModel, opts.name));
@@ -12050,7 +12297,7 @@ finetuneCmd.command("start").description("Start a fine-tuning job").requiredOpti
12050
12297
  process.exit(1);
12051
12298
  }
12052
12299
  });
12053
- finetuneCmd.command("status <job-id>").description("Get the status of a fine-tuning job").option("--provider <provider>", "Provider (openai|thinker-labs)", "openai").action(async (jobId, opts) => {
12300
+ 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) => {
12054
12301
  try {
12055
12302
  let result;
12056
12303
  if (opts.provider === "openai") {
@@ -12059,16 +12306,20 @@ finetuneCmd.command("status <job-id>").description("Get the status of a fine-tun
12059
12306
  const tl = new ThinkerLabsProvider;
12060
12307
  result = await tl.getFineTuneStatus(jobId);
12061
12308
  }
12062
- console.log();
12063
- console.log(` Job ID: ${result.jobId}`);
12064
- console.log(` Status: ${printStatus(result.status)}`);
12065
- if (result.fineTunedModel) {
12066
- console.log(` Fine-tuned model: ${result.fineTunedModel}`);
12067
- }
12068
- if (result.error) {
12069
- console.log(` Error: ${result.error}`);
12309
+ if (opts.json) {
12310
+ printJson(result);
12311
+ } else {
12312
+ console.log();
12313
+ console.log(` Job ID: ${result.jobId}`);
12314
+ console.log(` Status: ${printStatus(result.status)}`);
12315
+ if (result.fineTunedModel) {
12316
+ console.log(` Fine-tuned model: ${result.fineTunedModel}`);
12317
+ }
12318
+ if (result.error) {
12319
+ console.log(` Error: ${result.error}`);
12320
+ }
12321
+ console.log();
12070
12322
  }
12071
- console.log();
12072
12323
  const db = getDb();
12073
12324
  const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
12074
12325
  if (model) {
@@ -12080,7 +12331,64 @@ finetuneCmd.command("status <job-id>").description("Get the status of a fine-tun
12080
12331
  process.exit(1);
12081
12332
  }
12082
12333
  });
12083
- finetuneCmd.command("list").description("List all fine-tuning jobs").option("--provider <provider>", "Provider to query (openai|thinker-labs)", "openai").action(async (opts) => {
12334
+ 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) => {
12335
+ const intervalMs = Math.max(5, parseInt(opts.interval, 10) || 30) * 1000;
12336
+ const terminalStates = new Set(["succeeded", "failed", "cancelled"]);
12337
+ printInfo(`Watching job ${jobId} (polling every ${intervalMs / 1000}s) \u2026`);
12338
+ console.log();
12339
+ const poll = async () => {
12340
+ try {
12341
+ let result;
12342
+ if (opts.provider === "openai") {
12343
+ result = await getFineTuneStatus(jobId);
12344
+ } else {
12345
+ const tl = new ThinkerLabsProvider;
12346
+ result = await tl.getFineTuneStatus(jobId);
12347
+ }
12348
+ const ts = new Date().toISOString().replace("T", " ").slice(0, 19);
12349
+ process.stdout.write(` [${ts}] ${printStatus(result.status)}`);
12350
+ if (result.fineTunedModel)
12351
+ process.stdout.write(` model: ${result.fineTunedModel}`);
12352
+ process.stdout.write(`
12353
+ `);
12354
+ const db = getDb();
12355
+ const [model] = await db.select().from(fineTunedModels).where(eq(fineTunedModels.fineTuneJobId, jobId));
12356
+ if (model) {
12357
+ await db.update(fineTunedModels).set({ status: result.status, updatedAt: Date.now() }).where(eq(fineTunedModels.fineTuneJobId, jobId));
12358
+ }
12359
+ if (terminalStates.has(result.status)) {
12360
+ console.log();
12361
+ if (result.status === "succeeded") {
12362
+ printSuccess(`Job completed successfully.`);
12363
+ if (result.fineTunedModel)
12364
+ printSuccess(`Fine-tuned model: ${result.fineTunedModel}`);
12365
+ } else if (result.status === "failed") {
12366
+ printError(`Job failed.${result.error ? " Error: " + result.error : ""}`);
12367
+ } else {
12368
+ printInfo(`Job ${result.status}.`);
12369
+ }
12370
+ return true;
12371
+ }
12372
+ return false;
12373
+ } catch (err) {
12374
+ printError(err instanceof Error ? err.message : String(err));
12375
+ return false;
12376
+ }
12377
+ };
12378
+ const done = await poll();
12379
+ if (!done) {
12380
+ await new Promise((resolve2) => {
12381
+ const timer = setInterval(async () => {
12382
+ const finished = await poll();
12383
+ if (finished) {
12384
+ clearInterval(timer);
12385
+ resolve2();
12386
+ }
12387
+ }, intervalMs);
12388
+ });
12389
+ }
12390
+ });
12391
+ 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) => {
12084
12392
  try {
12085
12393
  let jobs;
12086
12394
  if (opts.provider === "openai") {
@@ -12089,6 +12397,10 @@ finetuneCmd.command("list").description("List all fine-tuning jobs").option("--p
12089
12397
  const tl = new ThinkerLabsProvider;
12090
12398
  jobs = await tl.listFineTunedModels();
12091
12399
  }
12400
+ if (opts.json) {
12401
+ printJson(jobs);
12402
+ return;
12403
+ }
12092
12404
  if (jobs.length === 0) {
12093
12405
  printInfo("No fine-tuning jobs found.");
12094
12406
  return;
@@ -12104,7 +12416,7 @@ finetuneCmd.command("list").description("List all fine-tuning jobs").option("--p
12104
12416
  process.exit(1);
12105
12417
  }
12106
12418
  });
12107
- var DEFAULT_DATASETS_DIR = join6(homedir6(), ".brains", "datasets");
12419
+ var DEFAULT_DATASETS_DIR = join7(homedir7(), ".brains", "datasets");
12108
12420
  var dataCmd = program2.command("data").description("Manage training datasets");
12109
12421
  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) => {
12110
12422
  const validSources = ["todos", "mementos", "conversations", "sessions", "all"];
@@ -12118,44 +12430,59 @@ dataCmd.command("gather").description("Gather training data from agent memory so
12118
12430
  process.exit(1);
12119
12431
  }
12120
12432
  try {
12121
- mkdirSync2(opts.output, { recursive: true });
12433
+ mkdirSync3(opts.output, { recursive: true });
12122
12434
  const sources = opts.source === "all" ? ["todos", "mementos", "conversations", "sessions"] : [opts.source];
12123
12435
  const now = Date.now();
12124
12436
  const db = getDb();
12125
- const results = await gatherAll(sources, { limit: limit2 });
12437
+ const gathererMap = {
12438
+ todos: (o) => Promise.resolve().then(() => (init_todos(), exports_todos)).then((m) => m.gatherFromTodos(o)),
12439
+ mementos: (o) => Promise.resolve().then(() => (init_mementos(), exports_mementos)).then((m) => m.gatherFromMementos(o)),
12440
+ conversations: (o) => Promise.resolve().then(() => (init_conversations(), exports_conversations)).then((m) => m.gatherFromConversations(o)),
12441
+ sessions: (o) => Promise.resolve().then(() => (init_sessions(), exports_sessions2)).then((m) => m.gatherFromSessions(o))
12442
+ };
12126
12443
  let totalExamples = 0;
12127
- for (const result of results) {
12128
- const { source, examples, count } = result;
12129
- printInfo(`Gathered from ${source} \u2026`);
12130
- if (count === 0) {
12131
- printInfo(` No examples gathered from ${source}.`);
12132
- continue;
12133
- }
12134
- totalExamples += count;
12135
- const fileName = `${source}-${now}.jsonl`;
12136
- const filePath = join6(opts.output, fileName);
12137
- writeFileSync(filePath, examples.map((e) => JSON.stringify(e)).join(`
12444
+ let successfulSources = 0;
12445
+ for (const source of sources) {
12446
+ printInfo(`Gathering from ${source} \u2026`);
12447
+ try {
12448
+ const gatherer = gathererMap[source];
12449
+ if (!gatherer) {
12450
+ printError(` Unknown source: ${source}`);
12451
+ continue;
12452
+ }
12453
+ const { examples, count } = await gatherer({ limit: limit2 });
12454
+ if (count === 0) {
12455
+ printInfo(` No examples found in ${source}.`);
12456
+ continue;
12457
+ }
12458
+ const fileName = `${source}-${now}.jsonl`;
12459
+ const filePath = join7(opts.output, fileName);
12460
+ writeFileSync2(filePath, examples.map((e) => JSON.stringify(e)).join(`
12138
12461
  `) + `
12139
12462
  `, "utf8");
12140
- const datasetId = randomUUID();
12141
- await db.insert(trainingDatasets).values({
12142
- id: datasetId,
12143
- source,
12144
- filePath,
12145
- exampleCount: count,
12146
- createdAt: now
12147
- });
12148
- printSuccess(` ${count} examples \u2192 ${filePath}`);
12463
+ await db.insert(trainingDatasets).values({
12464
+ id: randomUUID(),
12465
+ source,
12466
+ filePath,
12467
+ exampleCount: count,
12468
+ createdAt: now
12469
+ });
12470
+ printSuccess(` \u2713 ${count} examples \u2192 ${filePath}`);
12471
+ totalExamples += count;
12472
+ successfulSources++;
12473
+ } catch (sourceErr) {
12474
+ printError(` \u2717 ${source}: ${sourceErr instanceof Error ? sourceErr.message : String(sourceErr)}`);
12475
+ }
12149
12476
  }
12150
12477
  console.log();
12151
- printSuccess(`Total examples gathered: ${totalExamples}`);
12478
+ printSuccess(`Total: ${totalExamples} examples from ${successfulSources} source(s)`);
12152
12479
  } catch (err) {
12153
12480
  printError(err instanceof Error ? err.message : String(err));
12154
12481
  process.exit(1);
12155
12482
  }
12156
12483
  });
12157
12484
  dataCmd.command("preview <file>").description("Preview a JSONL training file").option("-n, --count <n>", "Number of examples to show", "5").action((file, opts) => {
12158
- if (!existsSync2(file)) {
12485
+ if (!existsSync4(file)) {
12159
12486
  printError(`File not found: ${file}`);
12160
12487
  process.exit(1);
12161
12488
  }
@@ -12165,7 +12492,7 @@ dataCmd.command("preview <file>").description("Preview a JSONL training file").o
12165
12492
  process.exit(1);
12166
12493
  }
12167
12494
  try {
12168
- const content = readFileSync2(file, "utf8");
12495
+ const content = readFileSync3(file, "utf8");
12169
12496
  const lines = content.trim().split(`
12170
12497
  `).filter(Boolean);
12171
12498
  const total = lines.length;
@@ -12190,10 +12517,63 @@ dataCmd.command("preview <file>").description("Preview a JSONL training file").o
12190
12517
  process.exit(1);
12191
12518
  }
12192
12519
  });
12193
- dataCmd.command("list").description("List all gathered datasets").action(async () => {
12520
+ dataCmd.command("merge <files...>").description("Merge multiple JSONL datasets into one").option("--output <path>", "Output file path", join7(DEFAULT_DATASETS_DIR, `merged-${Date.now()}.jsonl`)).option("--no-dedupe", "Skip deduplication").action(async (files, opts) => {
12521
+ try {
12522
+ for (const f of files) {
12523
+ if (!existsSync4(f)) {
12524
+ printError(`File not found: ${f}`);
12525
+ process.exit(1);
12526
+ }
12527
+ }
12528
+ mkdirSync3(join7(opts.output, "..").replace(/\/\.\.$/, "") || DEFAULT_DATASETS_DIR, { recursive: true });
12529
+ const allExamples = [];
12530
+ for (const f of files) {
12531
+ const lines = readFileSync3(f, "utf8").split(`
12532
+ `).map((l) => l.trim()).filter(Boolean);
12533
+ allExamples.push(...lines);
12534
+ printInfo(` Read ${lines.length} examples from ${f}`);
12535
+ }
12536
+ let finalLines = allExamples;
12537
+ let dupeCount = 0;
12538
+ if (opts.dedupe) {
12539
+ const seen = new Set;
12540
+ finalLines = allExamples.filter((line) => {
12541
+ if (seen.has(line)) {
12542
+ dupeCount++;
12543
+ return false;
12544
+ }
12545
+ seen.add(line);
12546
+ return true;
12547
+ });
12548
+ }
12549
+ writeFileSync2(opts.output, finalLines.join(`
12550
+ `) + `
12551
+ `, "utf8");
12552
+ const db = getDb();
12553
+ await db.insert(trainingDatasets).values({
12554
+ id: randomUUID(),
12555
+ source: "mixed",
12556
+ filePath: opts.output,
12557
+ exampleCount: finalLines.length,
12558
+ createdAt: Date.now()
12559
+ });
12560
+ console.log();
12561
+ printSuccess(`Merged ${files.length} files \u2014 ${finalLines.length} examples \u2192 ${opts.output}`);
12562
+ if (opts.dedupe && dupeCount > 0)
12563
+ printInfo(` Removed ${dupeCount} duplicate(s)`);
12564
+ } catch (err) {
12565
+ printError(err instanceof Error ? err.message : String(err));
12566
+ process.exit(1);
12567
+ }
12568
+ });
12569
+ dataCmd.command("list").description("List all gathered datasets").option("--json", "Output as JSON").action(async (opts) => {
12194
12570
  try {
12195
12571
  const db = getDb();
12196
12572
  const datasets = await db.select().from(trainingDatasets);
12573
+ if (opts.json) {
12574
+ printJson(datasets);
12575
+ return;
12576
+ }
12197
12577
  if (datasets.length === 0) {
12198
12578
  printInfo("No datasets found. Use 'brains data gather' to create one.");
12199
12579
  return;
@@ -12211,10 +12591,10 @@ dataCmd.command("list").description("List all gathered datasets").action(async (
12211
12591
  }
12212
12592
  });
12213
12593
  var collectionsCmd = program2.command("collections").description("Manage model collections");
12214
- collectionsCmd.action(async () => {
12215
- await listCollections();
12594
+ collectionsCmd.option("--json", "Output as JSON").action(async (opts) => {
12595
+ await listCollections(opts.json);
12216
12596
  });
12217
- async function listCollections() {
12597
+ async function listCollections(json = false) {
12218
12598
  try {
12219
12599
  const db = getDb();
12220
12600
  const rows = await db.select({
@@ -12222,6 +12602,10 @@ async function listCollections() {
12222
12602
  count: sql`count(*)`.as("count"),
12223
12603
  names: sql`group_concat(coalesce(${fineTunedModels.displayName}, ${fineTunedModels.name}), ', ')`.as("names")
12224
12604
  }).from(fineTunedModels).groupBy(fineTunedModels.collection);
12605
+ if (json) {
12606
+ printJson(rows);
12607
+ return;
12608
+ }
12225
12609
  if (rows.length === 0) {
12226
12610
  printInfo("No collections found. Set a collection with 'brains models set-collection'.");
12227
12611
  return;
@@ -12236,8 +12620,8 @@ async function listCollections() {
12236
12620
  process.exit(1);
12237
12621
  }
12238
12622
  }
12239
- collectionsCmd.command("list").description("List all collections with model counts").action(async () => {
12240
- await listCollections();
12623
+ collectionsCmd.command("list").description("List all collections with model counts").option("--json", "Output as JSON").action(async (opts) => {
12624
+ await listCollections(opts.json);
12241
12625
  });
12242
12626
  collectionsCmd.command("show <name>").description("List all models in a collection").action(async (name) => {
12243
12627
  try {
@@ -12296,4 +12680,43 @@ program2.command("remove <id>").alias("rm").alias("uninstall").description("Remo
12296
12680
  process.exit(1);
12297
12681
  }
12298
12682
  });
12683
+ var configCmd = program2.command("config").description("Manage API keys and settings");
12684
+ configCmd.command("list").description("Show all config keys and their sources").action(() => {
12685
+ const entries = listConfig();
12686
+ console.log();
12687
+ for (const { key, value, source } of entries) {
12688
+ const display = source === "unset" ? "(unset)" : value.length > 8 ? value.slice(0, 4) + "****" + value.slice(-4) : "****";
12689
+ const src = source === "env" ? " [env]" : source === "file" ? " [file]" : "";
12690
+ console.log(` ${key.padEnd(28)} ${display}${src}`);
12691
+ }
12692
+ console.log();
12693
+ });
12694
+ configCmd.command("get <key>").description("Get a config value").action((key) => {
12695
+ if (!CONFIG_KEYS.includes(key)) {
12696
+ printError(`Unknown key: ${key}. Valid keys: ${CONFIG_KEYS.join(", ")}`);
12697
+ process.exit(1);
12698
+ }
12699
+ const value = getConfigValue(key);
12700
+ if (!value) {
12701
+ printInfo(`${key} is not set.`);
12702
+ } else {
12703
+ console.log(value);
12704
+ }
12705
+ });
12706
+ configCmd.command("set <key> <value>").description("Set a config value (stored in ~/.brains/config.json)").action((key, value) => {
12707
+ if (!CONFIG_KEYS.includes(key)) {
12708
+ printError(`Unknown key: ${key}. Valid keys: ${CONFIG_KEYS.join(", ")}`);
12709
+ process.exit(1);
12710
+ }
12711
+ setConfigValue(key, value);
12712
+ printSuccess(`${key} saved to ~/.brains/config.json`);
12713
+ });
12714
+ configCmd.command("unset <key>").description("Remove a config value from the config file").action((key) => {
12715
+ if (!CONFIG_KEYS.includes(key)) {
12716
+ printError(`Unknown key: ${key}. Valid keys: ${CONFIG_KEYS.join(", ")}`);
12717
+ process.exit(1);
12718
+ }
12719
+ deleteConfigValue(key);
12720
+ printSuccess(`${key} removed from config.`);
12721
+ });
12299
12722
  program2.parse();