@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.
- package/dist/cli/index.js +1055 -632
- package/dist/db/index.d.ts.map +1 -1
- package/dist/index.js +4325 -127
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/gatherers/assistants.d.ts +3 -0
- package/dist/lib/gatherers/assistants.d.ts.map +1 -0
- package/dist/lib/gatherers/economy.d.ts +3 -0
- package/dist/lib/gatherers/economy.d.ts.map +1 -0
- package/dist/lib/gatherers/index.d.ts +3 -0
- package/dist/lib/gatherers/index.d.ts.map +1 -1
- package/dist/lib/gatherers/protocol.d.ts +29 -0
- package/dist/lib/gatherers/protocol.d.ts.map +1 -0
- package/dist/lib/gatherers/recordings.d.ts +3 -0
- package/dist/lib/gatherers/recordings.d.ts.map +1 -0
- package/dist/lib/gatherers/registry.d.ts +7 -0
- package/dist/lib/gatherers/registry.d.ts.map +1 -0
- package/dist/lib/gatherers/researcher.d.ts +3 -0
- package/dist/lib/gatherers/researcher.d.ts.map +1 -0
- package/dist/lib/gatherers/styles.d.ts +3 -0
- package/dist/lib/gatherers/styles.d.ts.map +1 -0
- package/dist/lib/gatherers/tickets.d.ts +3 -0
- package/dist/lib/gatherers/tickets.d.ts.map +1 -0
- package/dist/lib/index.d.ts +3 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/providers/openai.d.ts +2 -0
- package/dist/lib/providers/openai.d.ts.map +1 -1
- package/dist/lib/providers/thinker-labs.d.ts +1 -0
- package/dist/lib/providers/thinker-labs.d.ts.map +1 -1
- package/dist/lib/retry.d.ts +7 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/lib/schemas.d.ts +87 -0
- package/dist/lib/schemas.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +4619 -199
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4648 -9
- 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
|
|
3458
|
-
import { join as
|
|
3459
|
-
import { homedir as
|
|
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
|
-
|
|
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
|
-
|
|
5842
|
-
|
|
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((
|
|
6934
|
-
|
|
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((
|
|
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((
|
|
7811
|
-
__classPrivateFieldSet3(this, _EventStream_resolveConnectedPromise,
|
|
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((
|
|
7815
|
-
__classPrivateFieldSet3(this, _EventStream_resolveEndPromise,
|
|
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((
|
|
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,
|
|
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((
|
|
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((
|
|
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((
|
|
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
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
const
|
|
11340
|
-
const
|
|
11341
|
-
|
|
11342
|
-
|
|
11343
|
-
|
|
11344
|
-
|
|
11345
|
-
|
|
11346
|
-
|
|
11347
|
-
|
|
11348
|
-
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
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
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11767
|
+
var CONFIG_PATH = resolveConfigPath();
|
|
11768
|
+
function readConfigFile() {
|
|
11769
|
+
if (!existsSync2(CONFIG_PATH))
|
|
11770
|
+
return {};
|
|
11584
11771
|
try {
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
|
|
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
|
-
|
|
11620
|
-
|
|
11621
|
-
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
}
|
|
11638
|
-
|
|
11639
|
-
|
|
11640
|
-
|
|
11641
|
-
|
|
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
|
|
11655
|
-
const
|
|
11656
|
-
|
|
11657
|
-
|
|
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
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
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
|
-
|
|
11680
|
-
if (
|
|
11681
|
-
|
|
11682
|
-
|
|
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
|
|
11685
|
-
|
|
11686
|
-
|
|
11687
|
-
|
|
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
|
-
|
|
11691
|
-
|
|
11692
|
-
|
|
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/
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
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
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
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
|
|
11736
|
-
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
const
|
|
11748
|
-
|
|
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
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
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
|
-
}
|
|
11773
|
-
|
|
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
|
-
|
|
11778
|
-
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
return
|
|
11786
|
-
|
|
11787
|
-
|
|
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
|
-
|
|
11790
|
-
|
|
11791
|
-
|
|
11792
|
-
|
|
11793
|
-
if (
|
|
11794
|
-
|
|
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
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
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
|
-
|
|
12021
|
+
console.log("\u2514" + bottomBorder + "\u2518");
|
|
11852
12022
|
}
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
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
|
-
|
|
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)").
|
|
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
|
-
|
|
12001
|
-
|
|
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: ${
|
|
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(
|
|
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(
|
|
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
|
-
|
|
12063
|
-
|
|
12064
|
-
|
|
12065
|
-
|
|
12066
|
-
console.log(`
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
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("
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
12128
|
-
|
|
12129
|
-
printInfo(`
|
|
12130
|
-
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
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
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
|
|
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
|
|
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 (!
|
|
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 =
|
|
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("
|
|
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();
|