@p-sw/brainbox 0.1.2-alpha.1 → 0.1.2-alpha.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 (2) hide show
  1. package/dist/index.js +753 -279
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,12 +4,17 @@
4
4
  import { Command } from "commander";
5
5
  import { readFileSync as readFileSync2 } from "fs";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
- import { dirname as dirname4, join as join4 } from "path";
7
+ import { dirname as dirname3, join as join6 } from "path";
8
8
 
9
9
  // src/utils/logger.ts
10
10
  import chalk from "chalk";
11
- import { existsSync, mkdirSync, createWriteStream } from "fs";
12
- import { dirname } from "path";
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ createWriteStream,
15
+ writeFileSync
16
+ } from "fs";
17
+ import { join } from "path";
13
18
  var LEVELS = {
14
19
  debug: { rank: 0, color: chalk.gray, stderr: false },
15
20
  info: { rank: 1, color: chalk.blue, stderr: false },
@@ -26,48 +31,98 @@ var ICONS = {
26
31
  error: "✖",
27
32
  fatal: "▲"
28
33
  };
34
+ function pad2(n) {
35
+ return n < 10 ? `0${n}` : `${n}`;
36
+ }
37
+ function dateKey(d = new Date) {
38
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
39
+ }
40
+
41
+ class DailyFileSink {
42
+ dir;
43
+ date;
44
+ stream;
45
+ setDir(dir) {
46
+ if (dir === this.dir)
47
+ return;
48
+ this.stream?.end();
49
+ this.stream = undefined;
50
+ this.date = undefined;
51
+ this.dir = dir;
52
+ if (dir && !existsSync(dir))
53
+ mkdirSync(dir, { recursive: true });
54
+ }
55
+ write(data) {
56
+ if (!this.dir)
57
+ return;
58
+ const today = dateKey();
59
+ if (!this.stream || this.date !== today) {
60
+ this.stream?.end();
61
+ this.date = today;
62
+ if (!existsSync(this.dir))
63
+ mkdirSync(this.dir, { recursive: true });
64
+ this.stream = createWriteStream(join(this.dir, `${today}.log`), {
65
+ flags: "a"
66
+ });
67
+ }
68
+ this.stream.write(data);
69
+ }
70
+ close() {
71
+ this.stream?.end();
72
+ this.stream = undefined;
73
+ this.date = undefined;
74
+ this.dir = undefined;
75
+ }
76
+ get enabled() {
77
+ return this.dir !== undefined;
78
+ }
79
+ }
80
+ var shared = {
81
+ level: "info",
82
+ timestamps: true,
83
+ colors: chalk.level > 0,
84
+ json: false,
85
+ silent: false
86
+ };
87
+ var mainSink = new DailyFileSink;
88
+ var llmLogDir;
89
+ function applyShared(options) {
90
+ if (options.level !== undefined)
91
+ shared.level = options.level;
92
+ if (options.timestamps !== undefined)
93
+ shared.timestamps = options.timestamps;
94
+ if (options.colors !== undefined)
95
+ shared.colors = options.colors;
96
+ if (options.silent !== undefined)
97
+ shared.silent = options.silent;
98
+ if (options.json !== undefined)
99
+ shared.json = options.json;
100
+ if ("logDir" in options)
101
+ mainSink.setDir(options.logDir);
102
+ }
29
103
 
30
104
  class Logger {
31
- level;
32
- timestamps;
33
- colors;
34
105
  tag;
35
- file;
36
- json;
37
- silent;
38
- fileStream;
39
106
  constructor(options = {}) {
40
- this.level = options.level ?? "info";
41
- this.timestamps = options.timestamps ?? true;
42
- this.colors = options.colors ?? chalk.level > 0;
43
107
  this.tag = options.tag;
44
- this.file = options.file;
45
- this.json = options.json ?? false;
46
- this.silent = options.silent ?? false;
47
- if (this.file) {
48
- const dir = dirname(this.file);
49
- if (!existsSync(dir))
50
- mkdirSync(dir, { recursive: true });
51
- this.fileStream = createWriteStream(this.file, { flags: "a" });
52
- }
108
+ applyShared(options);
53
109
  }
54
110
  shouldLog(level) {
55
- return LEVELS[level].rank >= LEVELS[this.level].rank;
111
+ return LEVELS[level].rank >= LEVELS[shared.level].rank;
56
112
  }
57
113
  formatTimestamp() {
58
114
  const now = new Date;
59
- const pad = (n) => n.toString().padStart(2, "0");
60
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
115
+ return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())} ${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
61
116
  }
62
117
  format(level, message) {
63
- const ts = this.timestamps ? `[${this.formatTimestamp()}]` : "";
118
+ const ts = shared.timestamps ? `[${this.formatTimestamp()}]` : "";
64
119
  const tag = this.tag ? `[${this.tag}]` : "";
65
120
  const icon = ICONS[level];
66
121
  const levelStr = level.toUpperCase();
67
122
  const consoleParts = [
68
123
  ts,
69
124
  tag,
70
- this.colors ? LEVELS[level].color(icon) : icon,
125
+ shared.colors ? LEVELS[level].color(icon) : icon,
71
126
  message
72
127
  ].filter(Boolean);
73
128
  const consoleLine = consoleParts.join(" ");
@@ -90,13 +145,13 @@ class Logger {
90
145
  return;
91
146
  const { console: consoleLine, file: fileLine } = this.format(level, message);
92
147
  const jsonLine = this.formatJson(level, message);
93
- if (!this.silent) {
148
+ if (!shared.silent) {
94
149
  const out = LEVELS[level].stderr ? process.stderr : process.stdout;
95
150
  out.write(consoleLine + `
96
151
  `);
97
152
  }
98
- if (this.fileStream) {
99
- this.fileStream.write(this.json ? jsonLine : fileLine);
153
+ if (mainSink.enabled) {
154
+ mainSink.write(shared.json ? jsonLine : fileLine);
100
155
  }
101
156
  }
102
157
  debug(message) {
@@ -119,57 +174,56 @@ class Logger {
119
174
  }
120
175
  child(tag) {
121
176
  const combined = this.tag ? `${this.tag}:${tag}` : tag;
122
- return new Logger({
123
- level: this.level,
124
- timestamps: this.timestamps,
125
- colors: this.colors,
126
- tag: combined,
127
- file: this.file,
128
- json: this.json,
129
- silent: this.silent
130
- });
177
+ return new Logger({ tag: combined });
131
178
  }
132
179
  configure(options) {
133
- if (options.level !== undefined)
134
- this.level = options.level;
135
- if (options.timestamps !== undefined)
136
- this.timestamps = options.timestamps;
137
- if (options.colors !== undefined)
138
- this.colors = options.colors;
139
180
  if (options.tag !== undefined)
140
181
  this.tag = options.tag;
141
- if (options.silent !== undefined)
142
- this.silent = options.silent;
143
- if (options.json !== undefined)
144
- this.json = options.json;
145
- if (options.file !== undefined && options.file !== this.file) {
146
- this.fileStream?.end();
147
- this.file = options.file;
148
- if (this.file) {
149
- const dir = dirname(this.file);
150
- if (!existsSync(dir))
151
- mkdirSync(dir, { recursive: true });
152
- this.fileStream = createWriteStream(this.file, { flags: "a" });
153
- } else {
154
- this.fileStream = undefined;
155
- }
156
- }
182
+ applyShared(options);
157
183
  }
158
184
  close() {
159
- this.fileStream?.end();
185
+ mainSink.close();
186
+ llmLogDir = undefined;
160
187
  }
161
188
  }
162
189
  var logger = new Logger;
190
+ function configureLlmLog(logDir) {
191
+ llmLogDir = logDir;
192
+ if (logDir && !existsSync(logDir))
193
+ mkdirSync(logDir, { recursive: true });
194
+ }
195
+ function isLlmLogEnabled() {
196
+ return llmLogDir !== undefined;
197
+ }
198
+ function sanitizeCaller(caller) {
199
+ const s = caller.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
200
+ return s.length > 0 ? s : "unknown";
201
+ }
202
+ function dateTimeKey(d = new Date) {
203
+ return `${dateKey(d)}-${pad2(d.getHours())}-${pad2(d.getMinutes())}-${pad2(d.getSeconds())}`;
204
+ }
205
+ function writeLlmExchange(caller, content) {
206
+ if (!llmLogDir)
207
+ return;
208
+ if (!existsSync(llmLogDir))
209
+ mkdirSync(llmLogDir, { recursive: true });
210
+ const base = `${dateTimeKey()}-${sanitizeCaller(caller)}`;
211
+ let path = join(llmLogDir, `${base}.log`);
212
+ for (let n = 2;existsSync(path); n += 1) {
213
+ path = join(llmLogDir, `${base}-${n}.log`);
214
+ }
215
+ writeFileSync(path, content, "utf8");
216
+ }
163
217
 
164
218
  // src/config/loader.ts
165
- import { dirname as dirname2, join, resolve } from "path";
219
+ import { dirname, join as join2, resolve } from "path";
166
220
  import { z, ZodError } from "zod";
167
221
  import { homedir } from "os";
168
222
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
169
- import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
170
- var brainboxRoot = process.env["BRAINBOX_ROOT_PATH"] ? resolve(process.cwd(), process.env["BRAINBOX_ROOT_PATH"]) : join(homedir(), ".brainbox");
223
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
224
+ var brainboxRoot = process.env["BRAINBOX_ROOT_PATH"] ? resolve(process.cwd(), process.env["BRAINBOX_ROOT_PATH"]) : join2(homedir(), ".brainbox");
171
225
  function configFile(file, options) {
172
- const path = () => join(brainboxRoot, file);
226
+ const path = () => join2(brainboxRoot, file);
173
227
  const read = () => {
174
228
  const p = path();
175
229
  let raw;
@@ -181,8 +235,8 @@ function configFile(file, options) {
181
235
  const defaults = defaultsFromSchema(options.schema);
182
236
  const templateStr = (options.header ? options.header + `
183
237
  ` : "") + (defaults === undefined ? "" : stringifyYaml(defaults));
184
- mkdirSync2(dirname2(p), { recursive: true });
185
- writeFileSync(p, templateStr);
238
+ mkdirSync2(dirname(p), { recursive: true });
239
+ writeFileSync2(p, templateStr);
186
240
  raw = defaults ?? {};
187
241
  }
188
242
  try {
@@ -199,8 +253,8 @@ function configFile(file, options) {
199
253
  };
200
254
  const write = (value) => {
201
255
  const p = path();
202
- mkdirSync2(dirname2(p), { recursive: true });
203
- writeFileSync(p, stringifyYaml(value));
256
+ mkdirSync2(dirname(p), { recursive: true });
257
+ writeFileSync2(p, stringifyYaml(value));
204
258
  };
205
259
  const update = (fn) => {
206
260
  const next = fn(read());
@@ -258,7 +312,6 @@ function setSupermemoryKey(key) {
258
312
  supermemory: { ...root.supermemory, apiKey: key }
259
313
  }));
260
314
  }
261
- var root_default = rootCfg.read();
262
315
 
263
316
  // src/config/file/auth.ts
264
317
  import z3 from "zod";
@@ -292,16 +345,25 @@ function removeProviderAuth(provider) {
292
345
  return next;
293
346
  });
294
347
  }
295
- var auth_default = authCfg.read();
296
348
 
297
349
  // src/config/index.ts
298
350
  var config = {
299
- debug: root_default.debug,
351
+ get debug() {
352
+ return process.env.DEBUG_MODE ? process.env.DEBUG_MODE.toLowerCase() === "true" : readRootFile().debug;
353
+ },
300
354
  brainboxRoot,
301
- supermemoryApiKey: root_default.supermemory.apiKey,
302
- conversationModel: root_default.conversationModel,
303
- identityModel: root_default.identityModel,
304
- auth: auth_default
355
+ get supermemoryApiKey() {
356
+ return readRootFile().supermemory.apiKey;
357
+ },
358
+ get conversationModel() {
359
+ return readRootFile().conversationModel;
360
+ },
361
+ get identityModel() {
362
+ return readRootFile().identityModel;
363
+ },
364
+ get auth() {
365
+ return readAuthFile();
366
+ }
305
367
  };
306
368
 
307
369
  // src/commands/index.ts
@@ -313,7 +375,7 @@ function registerCommand(program, config2) {
313
375
 
314
376
  // src/brain/manager.ts
315
377
  import { mkdir, readFile, writeFile } from "fs/promises";
316
- import { join as join2 } from "path";
378
+ import { join as join3 } from "path";
317
379
  var log = logger.child("brain-manager");
318
380
 
319
381
  class BrainDBManager {
@@ -322,7 +384,7 @@ class BrainDBManager {
322
384
  this.root = root;
323
385
  }
324
386
  dbFile() {
325
- return join2(this.root, "brains.json");
387
+ return join3(this.root, "brains.json");
326
388
  }
327
389
  async readDB() {
328
390
  try {
@@ -422,6 +484,21 @@ function defaultReasoningEffort(effort, model, identityModel) {
422
484
  return effort;
423
485
  return model === identityModel ? "medium" : "none";
424
486
  }
487
+ function stripThinkTags(content) {
488
+ return content.replace(/<think\b[^>]*>[\s\S]*?<\/think>/gi, "").trim();
489
+ }
490
+ function parseModelJson(content) {
491
+ const cleaned = stripThinkTags(content);
492
+ try {
493
+ return JSON.parse(cleaned);
494
+ } catch (first) {
495
+ const fence = cleaned.match(/```(?:json)?\s*([\s\S]*?)```/i);
496
+ if (fence?.[1]) {
497
+ return JSON.parse(fence[1].trim());
498
+ }
499
+ throw first;
500
+ }
501
+ }
425
502
  function readAuthString(auth, key, envName) {
426
503
  const fromAuth = typeof auth?.[key] === "string" ? auth[key] : undefined;
427
504
  if (fromAuth)
@@ -473,7 +550,7 @@ class LLMExecutor {
473
550
  });
474
551
  };
475
552
  if (conv.provider === id.provider) {
476
- return build(conv.provider);
553
+ return withLlmLogging(build(conv.provider));
477
554
  }
478
555
  const modelToProvider = {
479
556
  [conv.model]: conv.provider,
@@ -484,7 +561,7 @@ class LLMExecutor {
484
561
  [id.provider]: build(id.provider)
485
562
  };
486
563
  const fallback = instances[conv.provider];
487
- return new class extends LLMExecutor {
564
+ return withLlmLogging(new class extends LLMExecutor {
488
565
  providerName = "dispatch";
489
566
  models = {
490
567
  conversation: conv.model,
@@ -498,9 +575,127 @@ class LLMExecutor {
498
575
  const exec = instances[modelToProvider[model] ?? ""] ?? fallback;
499
576
  return exec.chatWithTools(model, options);
500
577
  }
501
- };
578
+ });
502
579
  }
503
580
  }
581
+ function formatPayload(value) {
582
+ if (typeof value === "string")
583
+ return value;
584
+ try {
585
+ return JSON.stringify(value, null, 2);
586
+ } catch {
587
+ return String(value);
588
+ }
589
+ }
590
+ function resolveCaller(options, fallback) {
591
+ return options.caller ?? options.jsonSchemaName ?? fallback;
592
+ }
593
+ function withLlmLogging(inner) {
594
+ return new class extends LLMExecutor {
595
+ providerName = inner.providerName;
596
+ models = inner.models;
597
+ async call(model, options) {
598
+ const jsonMode = "jsonSchemaName" in options;
599
+ const caller = resolveCaller({
600
+ caller: options.caller,
601
+ jsonSchemaName: jsonMode ? options.jsonSchemaName : undefined
602
+ }, "call");
603
+ const requestBody = [
604
+ `provider: ${inner.providerName}`,
605
+ `model: ${model}`,
606
+ `caller: ${caller}`,
607
+ `reasoningEffort: ${options.reasoningEffort ?? "-"}`,
608
+ `jsonSchemaName: ${jsonMode ? options.jsonSchemaName : "-"}`,
609
+ "",
610
+ "--- instruction ---",
611
+ options.instruction,
612
+ "",
613
+ "--- message ---",
614
+ options.message,
615
+ ...jsonMode ? ["", "--- jsonSchema ---", formatPayload(options.jsonSchema)] : []
616
+ ].join(`
617
+ `);
618
+ try {
619
+ const result = await inner.call(model, options);
620
+ if (isLlmLogEnabled()) {
621
+ writeLlmExchange(caller, [
622
+ "======== REQUEST ========",
623
+ requestBody,
624
+ "",
625
+ "======== RESPONSE ========",
626
+ formatPayload(result),
627
+ ""
628
+ ].join(`
629
+ `));
630
+ }
631
+ return result;
632
+ } catch (err) {
633
+ const reason = err instanceof Error ? err.message : String(err);
634
+ if (isLlmLogEnabled()) {
635
+ writeLlmExchange(caller, [
636
+ "======== REQUEST ========",
637
+ requestBody,
638
+ "",
639
+ "======== ERROR ========",
640
+ reason,
641
+ ""
642
+ ].join(`
643
+ `));
644
+ }
645
+ throw err;
646
+ }
647
+ }
648
+ async chatWithTools(model, options) {
649
+ const caller = resolveCaller(options, "chat");
650
+ const requestBody = [
651
+ `provider: ${inner.providerName}`,
652
+ `model: ${model}`,
653
+ `caller: ${caller}`,
654
+ `reasoningEffort: ${options.reasoningEffort ?? "-"}`,
655
+ `parallelToolCalls: ${options.parallelToolCalls ?? false}`,
656
+ "",
657
+ "--- instruction ---",
658
+ options.instruction,
659
+ "",
660
+ "--- messages ---",
661
+ formatPayload(options.messages),
662
+ "",
663
+ "--- tools ---",
664
+ formatPayload(options.tools)
665
+ ].join(`
666
+ `);
667
+ try {
668
+ const result = await inner.chatWithTools(model, options);
669
+ if (isLlmLogEnabled()) {
670
+ writeLlmExchange(caller, [
671
+ "======== REQUEST ========",
672
+ requestBody,
673
+ "",
674
+ "======== RESPONSE ========",
675
+ formatPayload(result),
676
+ ""
677
+ ].join(`
678
+ `));
679
+ }
680
+ return result;
681
+ } catch (err) {
682
+ const reason = err instanceof Error ? err.message : String(err);
683
+ if (isLlmLogEnabled()) {
684
+ writeLlmExchange(caller, [
685
+ "======== REQUEST ========",
686
+ requestBody,
687
+ "",
688
+ "======== ERROR ========",
689
+ reason,
690
+ ""
691
+ ].join(`
692
+ `));
693
+ }
694
+ throw err;
695
+ }
696
+ }
697
+ };
698
+ }
504
699
  function listProviderNames() {
505
700
  return LLMExecutor.listProviderNames();
506
701
  }
@@ -538,7 +733,7 @@ function fromOrChoice(choice) {
538
733
  }));
539
734
  return {
540
735
  message: {
541
- content: typeof msg.content === "string" ? msg.content : undefined,
736
+ content: typeof msg.content === "string" ? stripThinkTags(msg.content) : undefined,
542
737
  toolCalls
543
738
  }
544
739
  };
@@ -590,13 +785,14 @@ class OpenRouterExecutor extends LLMExecutor {
590
785
  stream: false
591
786
  }
592
787
  });
593
- const content = result.choices[0]?.message?.content;
788
+ const raw = result.choices[0]?.message?.content;
789
+ const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
594
790
  if (!content) {
595
791
  log3.debug(`call: empty content in choice 0`);
596
792
  throw new Error("Empty response from model");
597
793
  }
598
794
  log3.debug(`call: response ${content.length} chars`);
599
- return jsonMode ? JSON.parse(content) : content;
795
+ return jsonMode ? parseModelJson(content) : content;
600
796
  }
601
797
  async chatWithTools(model, options) {
602
798
  log3.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
@@ -655,7 +851,7 @@ function fromChoice(choice) {
655
851
  }));
656
852
  return {
657
853
  message: {
658
- content: typeof msg.content === "string" ? msg.content : undefined,
854
+ content: typeof msg.content === "string" ? stripThinkTags(msg.content) : undefined,
659
855
  toolCalls
660
856
  }
661
857
  };
@@ -770,13 +966,14 @@ class OpenAICompatibleExecutor extends LLMExecutor {
770
966
  reasoningEffort: reasoning
771
967
  });
772
968
  const data = await this.sendRequest(body, options.reasoningEffort);
773
- const content = data.choices?.[0]?.message?.content;
969
+ const raw = data.choices?.[0]?.message?.content;
970
+ const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
774
971
  if (!content) {
775
972
  log4.debug(`call: empty content in choice 0`);
776
973
  throw new Error("Empty response from model");
777
974
  }
778
975
  log4.debug(`call: response ${content.length} chars`);
779
- return jsonMode ? JSON.parse(content) : content;
976
+ return jsonMode ? parseModelJson(content) : content;
780
977
  }
781
978
  async chatWithTools(model, options) {
782
979
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
@@ -1090,16 +1287,35 @@ class ZenMuxExecutor extends OpenAICompatibleExecutor {
1090
1287
  }
1091
1288
  }
1092
1289
 
1093
- // src/provider/providers/MiniMax.ts
1094
- class MiniMaxExecutor extends OpenAICompatibleExecutor {
1290
+ // src/provider/providers/minimax.ts
1291
+ class MiniMaxBase extends OpenAICompatibleExecutor {
1292
+ buildBody(opts) {
1293
+ const body = super.buildBody(opts);
1294
+ delete body["reasoning_effort"];
1295
+ body["reasoning_split"] = true;
1296
+ body["thinking"] = opts.reasoningEffort && opts.reasoningEffort !== "none" ? { type: "adaptive" } : { type: "disabled" };
1297
+ return body;
1298
+ }
1299
+ }
1300
+ function minimaxOpts(providerName, baseURL, opts) {
1301
+ return {
1302
+ providerName,
1303
+ baseURL,
1304
+ apiKey: opts.apiKey,
1305
+ conversationModel: opts.conversationModel,
1306
+ identityModel: opts.identityModel
1307
+ };
1308
+ }
1309
+
1310
+ class MiniMaxExecutor extends MiniMaxBase {
1311
+ constructor(opts) {
1312
+ super(minimaxOpts("minimax", "https://api.minimax.io/v1", opts));
1313
+ }
1314
+ }
1315
+
1316
+ class MiniMaxCnExecutor extends MiniMaxBase {
1095
1317
  constructor(opts) {
1096
- super({
1097
- providerName: "MiniMax",
1098
- baseURL: "https://api.MiniMax.chat/v1",
1099
- apiKey: opts.apiKey,
1100
- conversationModel: opts.conversationModel,
1101
- identityModel: opts.identityModel
1102
- });
1318
+ super(minimaxOpts("minimax-cn", "https://api.minimaxi.com/v1", opts));
1103
1319
  }
1104
1320
  }
1105
1321
 
@@ -1252,7 +1468,17 @@ class CloudflareGatewayExecutor extends OpenAICompatibleExecutor {
1252
1468
  // src/provider/providers/cloudflare_workers.ts
1253
1469
  function toCloudflareMessage(m) {
1254
1470
  if (m.role === "assistant") {
1255
- return { role: "assistant", content: m.content ?? "" };
1471
+ return {
1472
+ role: "assistant",
1473
+ content: m.content ?? "",
1474
+ tool_calls: m.toolCalls?.map((c) => {
1475
+ let args = c.function.arguments;
1476
+ try {
1477
+ args = JSON.parse(c.function.arguments);
1478
+ } catch {}
1479
+ return { id: c.id, name: c.function.name, arguments: args };
1480
+ })
1481
+ };
1256
1482
  }
1257
1483
  if (m.role === "tool") {
1258
1484
  return { role: "tool", content: m.content, tool_call_id: m.toolCallId };
@@ -1314,11 +1540,11 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1314
1540
  if (data.errors && data.errors.length > 0) {
1315
1541
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1316
1542
  }
1317
- const content = data.result?.response ?? "";
1543
+ const content = stripThinkTags(data.result?.response ?? "");
1318
1544
  if (!content) {
1319
1545
  throw new Error("Empty response from model");
1320
1546
  }
1321
- return jsonMode ? JSON.parse(content) : content;
1547
+ return jsonMode ? parseModelJson(content) : content;
1322
1548
  }
1323
1549
  async chatWithTools(model, options) {
1324
1550
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
@@ -1343,7 +1569,7 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1343
1569
  if (data.errors && data.errors.length > 0) {
1344
1570
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1345
1571
  }
1346
- const content = data.result?.response ?? "";
1572
+ const content = stripThinkTags(data.result?.response ?? "");
1347
1573
  const toolCalls = data.result?.tool_calls?.map((c, idx) => ({
1348
1574
  id: `call_${idx}`,
1349
1575
  function: {
@@ -1381,7 +1607,8 @@ class AzureOpenAIExecutor extends OpenAICompatibleExecutor {
1381
1607
  super({
1382
1608
  providerName: "azure-openai",
1383
1609
  baseURL: `https://${resource || "__resource__"}.openai.azure.com/openai/deployments`,
1384
- apiKey: opts.apiKey,
1610
+ apiKey: "",
1611
+ defaultHeaders: { "api-key": opts.apiKey },
1385
1612
  conversationModel: opts.conversationModel,
1386
1613
  identityModel: opts.identityModel
1387
1614
  });
@@ -1401,7 +1628,8 @@ class AzureCognitiveExecutor extends OpenAICompatibleExecutor {
1401
1628
  super({
1402
1629
  providerName: "azure-cognitive",
1403
1630
  baseURL: `https://${resource || "__resource__"}.cognitiveservices.azure.com/openai/deployments`,
1404
- apiKey: opts.apiKey,
1631
+ apiKey: "",
1632
+ defaultHeaders: { "api-key": opts.apiKey },
1405
1633
  conversationModel: opts.conversationModel,
1406
1634
  identityModel: opts.identityModel
1407
1635
  });
@@ -1525,44 +1753,50 @@ class AnthropicExecutor extends LLMExecutor {
1525
1753
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1526
1754
  const log5 = logger.child("llm:anthropic");
1527
1755
  log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
1756
+ const outputCap = 4096;
1528
1757
  const body = {
1529
1758
  model,
1530
- max_tokens: 4096,
1759
+ max_tokens: outputCap,
1531
1760
  system: options.instruction,
1532
1761
  messages: [{ role: "user", content: options.message }]
1533
1762
  };
1534
1763
  if (reasoning !== "none") {
1764
+ const budget = REASONING_BUDGET[reasoning];
1765
+ body["max_tokens"] = budget + outputCap;
1535
1766
  body["thinking"] = {
1536
1767
  type: "enabled",
1537
- budget_tokens: REASONING_BUDGET[reasoning]
1768
+ budget_tokens: budget
1538
1769
  };
1539
1770
  }
1540
1771
  const data = await this.send(body);
1541
1772
  if (data.error) {
1542
1773
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1543
1774
  }
1544
- const text = (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
1775
+ const text = stripThinkTags((data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join(""));
1545
1776
  if (!text) {
1546
1777
  throw new Error("Empty response from model");
1547
1778
  }
1548
- return jsonMode ? JSON.parse(text) : text;
1779
+ return jsonMode ? parseModelJson(text) : text;
1549
1780
  }
1550
1781
  async chatWithTools(model, options) {
1551
1782
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1552
1783
  const log5 = logger.child("llm:anthropic");
1553
1784
  log5.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
1554
1785
  const { system, msgs } = toAnthropicMessages(options.messages);
1786
+ const outputCap = 4096;
1555
1787
  const body = {
1556
1788
  model,
1557
- max_tokens: 4096,
1789
+ max_tokens: outputCap,
1558
1790
  system: system ?? options.instruction,
1559
1791
  messages: msgs,
1560
1792
  tools: options.tools.map(toAnthropicTool)
1561
1793
  };
1562
1794
  if (reasoning !== "none") {
1795
+ const budget = REASONING_BUDGET[reasoning];
1796
+ body["max_tokens"] = budget + outputCap;
1563
1797
  body["thinking"] = {
1564
1798
  type: "enabled",
1565
- budget_tokens: REASONING_BUDGET[reasoning]
1799
+ budget_tokens: budget
1566
1800
  };
1567
1801
  }
1568
1802
  const data = await this.send(body);
@@ -1570,7 +1804,7 @@ class AnthropicExecutor extends LLMExecutor {
1570
1804
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1571
1805
  }
1572
1806
  const blocks = data.content ?? [];
1573
- const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1807
+ const text = stripThinkTags(blocks.filter((b) => b.type === "text").map((b) => b.text).join(""));
1574
1808
  const toolCalls = blocks.filter((b) => b.type === "tool_use").map((b) => ({
1575
1809
  id: b.id,
1576
1810
  function: {
@@ -1584,11 +1818,11 @@ class AnthropicExecutor extends LLMExecutor {
1584
1818
 
1585
1819
  // src/provider/providers/bedrock.ts
1586
1820
  import { createHmac, createHash } from "node:crypto";
1587
- var pad2 = (n) => n.toString().padStart(2, "0");
1821
+ var pad22 = (n) => n.toString().padStart(2, "0");
1588
1822
  function amzDate(now) {
1589
1823
  return {
1590
- date: `${now.getUTCFullYear()}${pad2(now.getUTCMonth() + 1)}${pad2(now.getUTCDate())}`,
1591
- datetime: `${now.getUTCFullYear()}${pad2(now.getUTCMonth() + 1)}${pad2(now.getUTCDate())}T` + `${pad2(now.getUTCHours())}${pad2(now.getUTCMinutes())}${pad2(now.getUTCSeconds())}Z`
1824
+ date: `${now.getUTCFullYear()}${pad22(now.getUTCMonth() + 1)}${pad22(now.getUTCDate())}`,
1825
+ datetime: `${now.getUTCFullYear()}${pad22(now.getUTCMonth() + 1)}${pad22(now.getUTCDate())}T` + `${pad22(now.getUTCHours())}${pad22(now.getUTCMinutes())}${pad22(now.getUTCSeconds())}Z`
1592
1826
  };
1593
1827
  }
1594
1828
  function signRequest(opts) {
@@ -1696,7 +1930,7 @@ function toAnthropicTool2(t) {
1696
1930
  }
1697
1931
  function extractAnthropicContent(data) {
1698
1932
  const content = data["content"] ?? [];
1699
- const text = content.filter((b) => b["type"] === "text").map((b) => b["text"]).join("");
1933
+ const text = stripThinkTags(content.filter((b) => b["type"] === "text").map((b) => b["text"]).join(""));
1700
1934
  const toolCalls = content.filter((b) => b["type"] === "tool_use").map((b) => ({
1701
1935
  id: b["id"],
1702
1936
  function: {
@@ -1771,7 +2005,7 @@ class BedrockExecutor extends LLMExecutor {
1771
2005
  if (!text) {
1772
2006
  throw new Error("Empty response from model");
1773
2007
  }
1774
- return jsonMode ? JSON.parse(text) : text;
2008
+ return jsonMode ? parseModelJson(text) : text;
1775
2009
  }
1776
2010
  async chatWithTools(model, options) {
1777
2011
  const log5 = logger.child("llm:bedrock");
@@ -1801,6 +2035,7 @@ var VertexAuthSchema = z5.object({
1801
2035
  }).loose();
1802
2036
  function toGeminiContents(messages) {
1803
2037
  const out = [];
2038
+ const nameByCallId = new Map;
1804
2039
  for (const m of messages) {
1805
2040
  if (m.role === "system")
1806
2041
  continue;
@@ -1814,6 +2049,7 @@ function toGeminiContents(messages) {
1814
2049
  parts.push({ text: m.content });
1815
2050
  if (m.toolCalls) {
1816
2051
  for (const c of m.toolCalls) {
2052
+ nameByCallId.set(c.id, c.function.name);
1817
2053
  let args = {};
1818
2054
  try {
1819
2055
  args = c.function.arguments ? JSON.parse(c.function.arguments) : {};
@@ -1835,21 +2071,26 @@ function toGeminiContents(messages) {
1835
2071
  }
1836
2072
  out.push({
1837
2073
  role: "user",
1838
- parts: [{ functionResponse: { name: "", response } }]
2074
+ parts: [
2075
+ {
2076
+ functionResponse: {
2077
+ name: nameByCallId.get(m.toolCallId) ?? "",
2078
+ response
2079
+ }
2080
+ }
2081
+ ]
1839
2082
  });
1840
2083
  }
1841
2084
  }
1842
2085
  return out;
1843
2086
  }
1844
- function toGeminiTool(t) {
2087
+ function toGeminiTools(tools) {
1845
2088
  return {
1846
- functionDeclarations: [
1847
- {
1848
- name: t.name,
1849
- description: t.description,
1850
- parameters: t.parameters ?? { type: "object", properties: {} }
1851
- }
1852
- ]
2089
+ functionDeclarations: tools.map((t) => ({
2090
+ name: t.name,
2091
+ description: t.description,
2092
+ parameters: t.parameters ?? { type: "object", properties: {} }
2093
+ }))
1853
2094
  };
1854
2095
  }
1855
2096
  function extractFromGemini(data) {
@@ -1870,7 +2111,10 @@ function extractFromGemini(data) {
1870
2111
  });
1871
2112
  }
1872
2113
  }
1873
- return { text, toolCalls: toolCalls.length > 0 ? toolCalls : undefined };
2114
+ return {
2115
+ text: stripThinkTags(text),
2116
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
2117
+ };
1874
2118
  }
1875
2119
 
1876
2120
  class VertexExecutor extends LLMExecutor {
@@ -1936,7 +2180,7 @@ class VertexExecutor extends LLMExecutor {
1936
2180
  if (!text) {
1937
2181
  throw new Error("Empty response from model");
1938
2182
  }
1939
- return jsonMode ? JSON.parse(text) : text;
2183
+ return jsonMode ? parseModelJson(text) : text;
1940
2184
  }
1941
2185
  async chatWithTools(model, options) {
1942
2186
  const log5 = logger.child("llm:vertex");
@@ -1945,7 +2189,7 @@ class VertexExecutor extends LLMExecutor {
1945
2189
  const body = {
1946
2190
  contents,
1947
2191
  systemInstruction: { parts: [{ text: options.instruction }] },
1948
- tools: options.tools.length > 0 ? [toGeminiTool(options.tools[0])] : undefined
2192
+ tools: options.tools.length > 0 ? [toGeminiTools(options.tools)] : undefined
1949
2193
  };
1950
2194
  const data = await this.generate(model, body);
1951
2195
  if (data.error) {
@@ -2053,11 +2297,11 @@ class GitLabDuoExecutor extends LLMExecutor {
2053
2297
  if (data.error) {
2054
2298
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2055
2299
  }
2056
- const content = data.choices?.[0]?.message?.content ?? "";
2300
+ const content = stripThinkTags(data.choices?.[0]?.message?.content ?? "");
2057
2301
  if (!content) {
2058
2302
  throw new Error("Empty response from model");
2059
2303
  }
2060
- return jsonMode ? JSON.parse(content) : content;
2304
+ return jsonMode ? parseModelJson(content) : content;
2061
2305
  }
2062
2306
  async chatWithTools(model, options) {
2063
2307
  const log5 = logger.child("llm:gitlab-duo");
@@ -2081,7 +2325,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2081
2325
  }));
2082
2326
  return {
2083
2327
  message: {
2084
- content: choice?.message?.content ?? undefined,
2328
+ content: choice?.message?.content ? stripThinkTags(choice.message.content) : undefined,
2085
2329
  toolCalls
2086
2330
  }
2087
2331
  };
@@ -2172,11 +2416,11 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2172
2416
  if (data.error) {
2173
2417
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2174
2418
  }
2175
- const content = data.choices?.[0]?.message?.content ?? data.message ?? "";
2419
+ const content = stripThinkTags(data.choices?.[0]?.message?.content ?? data.message ?? "");
2176
2420
  if (!content) {
2177
2421
  throw new Error("Empty response from model");
2178
2422
  }
2179
- return jsonMode ? JSON.parse(content) : content;
2423
+ return jsonMode ? parseModelJson(content) : content;
2180
2424
  }
2181
2425
  async chatWithTools(model, options) {
2182
2426
  const log5 = logger.child("llm:snowflake-cortex");
@@ -2194,7 +2438,7 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2194
2438
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2195
2439
  }
2196
2440
  const choice = data.choices?.[0];
2197
- const content = choice?.message?.content ?? data.message ?? "";
2441
+ const content = stripThinkTags(choice?.message?.content ?? data.message ?? "");
2198
2442
  const toolCalls = choice?.message?.tool_calls?.map((c) => ({
2199
2443
  id: c.id,
2200
2444
  function: {
@@ -2233,7 +2477,8 @@ register("stackit", StackitExecutor);
2233
2477
  register("gmi", GmiExecutor);
2234
2478
  register("zai", ZaiExecutor);
2235
2479
  register("zenmux", ZenMuxExecutor);
2236
- register("MiniMax", MiniMaxExecutor);
2480
+ register("minimax", MiniMaxExecutor);
2481
+ register("minimax-cn", MiniMaxCnExecutor);
2237
2482
  register("ionet", IoNetExecutor);
2238
2483
  register("baseten", BasetenExecutor);
2239
2484
  register("cortecs", CortecsExecutor);
@@ -2267,14 +2512,14 @@ var llm = new Proxy({}, {
2267
2512
  // src/provider/promptLoader.ts
2268
2513
  import { existsSync as existsSync2 } from "fs";
2269
2514
  import { readFile as readFile2 } from "fs/promises";
2270
- import path, { dirname as dirname3 } from "path";
2515
+ import path, { dirname as dirname2 } from "path";
2271
2516
  import { fileURLToPath } from "url";
2272
2517
  var log5 = logger.child("prompt-loader");
2273
2518
  function fileName(promptKey) {
2274
2519
  return promptKey.toLowerCase() + ".md";
2275
2520
  }
2276
2521
  var __filename2 = fileURLToPath(import.meta.url);
2277
- var __dirname2 = dirname3(__filename2);
2522
+ var __dirname2 = dirname2(__filename2);
2278
2523
  var PROMPTS_DIR = existsSync2(path.resolve(__dirname2, "prompts")) ? path.resolve(__dirname2, "prompts") : path.resolve(__dirname2, "../../prompts");
2279
2524
  async function loadPrompt(promptKey) {
2280
2525
  const filePath = path.join(PROMPTS_DIR, fileName(promptKey));
@@ -2399,14 +2644,14 @@ function translateMessageHistory(personaName, entries) {
2399
2644
  }
2400
2645
 
2401
2646
  // src/brain/schedule.ts
2402
- function pad22(n) {
2647
+ function pad23(n) {
2403
2648
  return n < 10 ? `0${n}` : `${n}`;
2404
2649
  }
2405
2650
  function formatDateKey(d) {
2406
- return `${d.getFullYear()}-${pad22(d.getMonth() + 1)}-${pad22(d.getDate())}`;
2651
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
2407
2652
  }
2408
2653
  function formatMonthKey(d) {
2409
- return `${d.getFullYear()}-${pad22(d.getMonth() + 1)}`;
2654
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}`;
2410
2655
  }
2411
2656
 
2412
2657
  // src/brain/memory.ts
@@ -2494,22 +2739,22 @@ class Brain {
2494
2739
  db;
2495
2740
  space;
2496
2741
  brainbase;
2497
- memory;
2498
2742
  availabilityCache = new Map;
2499
- constructor(db, space, brainbase, memory = new Memory(this.db, this.space)) {
2743
+ memory;
2744
+ constructor(db, space, brainbase, memory) {
2500
2745
  this.db = db;
2501
2746
  this.space = space;
2502
2747
  this.brainbase = brainbase;
2503
- this.memory = memory;
2748
+ this.memory = memory ?? new Memory(this.db, this.space);
2504
2749
  log7.debug(`Brain constructed: id=${brainbase.brainId} name=${brainbase.displayName} space=${space.name}`);
2505
2750
  }
2506
2751
  async createDailySchedule(datetime) {
2507
- const dateKey = formatDateKey(datetime);
2508
- log7.debug(`createDailySchedule: starting for ${dateKey}`);
2752
+ const dateKey2 = formatDateKey(datetime);
2753
+ log7.debug(`createDailySchedule: starting for ${dateKey2}`);
2509
2754
  try {
2510
- const existing = await this.memory.get(`daily-schedule:${dateKey}`);
2755
+ const existing = await this.memory.get(`daily-schedule:${dateKey2}`);
2511
2756
  if (existing) {
2512
- log7.debug(`createDailySchedule: cache hit for ${dateKey}`);
2757
+ log7.debug(`createDailySchedule: cache hit for ${dateKey2}`);
2513
2758
  try {
2514
2759
  return JSON.parse(existing.content);
2515
2760
  } catch (parseErr) {
@@ -2537,7 +2782,7 @@ class Brain {
2537
2782
  }
2538
2783
  const instruction = await loadPrompt("DAILY_SCHEDULE");
2539
2784
  const promptMessage = [
2540
- `Target date: ${dateKey} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
2785
+ `Target date: ${dateKey2} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
2541
2786
  `Personality: ${this.brainbase.baseSystemPrompt}`,
2542
2787
  monthlySummary ? `Monthly summary for this day: ${monthlySummary}` : "(no monthly summary available for this date)",
2543
2788
  `Recent schedule (${twoDaysAgoKey}, 2 days ago): ${twoDaysAgoSchedule ? twoDaysAgoSchedule.items.map((s) => `${s.start} ${s.activity}`).join(", ") : "(no schedule on file for 2 days ago)"}`,
@@ -2548,6 +2793,7 @@ class Brain {
2548
2793
  `);
2549
2794
  log7.debug(`createDailySchedule: calling identity model`);
2550
2795
  const schedule = await llm.call(llm.models.identity, {
2796
+ caller: "daily-schedule",
2551
2797
  instruction,
2552
2798
  message: promptMessage,
2553
2799
  jsonSchemaName: "daily-schedule",
@@ -2555,15 +2801,15 @@ class Brain {
2555
2801
  });
2556
2802
  log7.debug(`createDailySchedule: model returned ${schedule.items.length} slots`);
2557
2803
  await this.memory.add({
2558
- customId: `daily-schedule:${dateKey}`,
2804
+ customId: `daily-schedule:${dateKey2}`,
2559
2805
  content: JSON.stringify(schedule),
2560
2806
  metadata: {
2561
2807
  kind: "schedule",
2562
2808
  source: "createDailySchedule",
2563
- date: dateKey
2809
+ date: dateKey2
2564
2810
  }
2565
2811
  });
2566
- log7.debug(`createDailySchedule: persisted ${dateKey}`);
2812
+ log7.debug(`createDailySchedule: persisted ${dateKey2}`);
2567
2813
  return schedule;
2568
2814
  } catch (error) {
2569
2815
  let reason = error instanceof Error ? error.message + `(${error.name})` : String(error);
@@ -2575,17 +2821,17 @@ class Brain {
2575
2821
  }
2576
2822
  async regenerateSchedules() {
2577
2823
  const today = new Date;
2824
+ const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
2825
+ await this.createMonthlySchedule(nextMonth);
2578
2826
  const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
2579
2827
  await this.createDailySchedule(tomorrow);
2580
2828
  await this.createDailySchedule(today);
2581
- const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
2582
- await this.createMonthlySchedule(nextMonth);
2583
2829
  }
2584
2830
  async createMonthlySchedule(datetime) {
2585
2831
  const year = datetime.getMonth() === 11 ? datetime.getFullYear() + 1 : datetime.getFullYear();
2586
2832
  const month = (datetime.getMonth() + 1) % 12;
2587
2833
  const daysInMonth = new Date(year, month + 1, 0).getDate();
2588
- const monthKey = `${year}-${pad22(month + 1)}`;
2834
+ const monthKey = `${year}-${pad23(month + 1)}`;
2589
2835
  log7.debug(`createMonthlySchedule: starting for ${monthKey}`);
2590
2836
  try {
2591
2837
  const existing = await this.memory.get(`monthly-schedule:${monthKey}`);
@@ -2598,7 +2844,7 @@ class Brain {
2598
2844
  }
2599
2845
  }
2600
2846
  const twoMonthsAgo = new Date(year, month - 2, 1);
2601
- const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad22(twoMonthsAgo.getMonth() + 1)}`;
2847
+ const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad23(twoMonthsAgo.getMonth() + 1)}`;
2602
2848
  log7.debug(`createMonthlySchedule: gathering context (history, ${twoMonthsAgoKey})`);
2603
2849
  const [history, twoMonthsAgoStored] = await Promise.all([
2604
2850
  this.getHistoryFacts(),
@@ -2626,6 +2872,7 @@ class Brain {
2626
2872
  `);
2627
2873
  log7.debug(`createMonthlySchedule: calling identity model`);
2628
2874
  const schedule = await llm.call(llm.models.identity, {
2875
+ caller: "monthly-schedule",
2629
2876
  instruction,
2630
2877
  message: promptMessage,
2631
2878
  jsonSchemaName: "monthly-schedule",
@@ -2656,11 +2903,11 @@ class Brain {
2656
2903
  }
2657
2904
  log7.debug(`sleepMemory: starting, ${history.length} messages`);
2658
2905
  try {
2659
- const dateKey = formatDateKey(datetime);
2906
+ const dateKey2 = formatDateKey(datetime);
2660
2907
  const instruction = await loadPrompt("MEMOIR");
2661
2908
  const historyBlock = translateMessageHistory(this.brainbase.displayName, history);
2662
2909
  const promptMessage = [
2663
- `Date: ${dateKey}`,
2910
+ `Date: ${dateKey2}`,
2664
2911
  `Personality: ${this.brainbase.baseSystemPrompt}`,
2665
2912
  `Conversation log:`,
2666
2913
  historyBlock
@@ -2669,20 +2916,21 @@ class Brain {
2669
2916
  `);
2670
2917
  log7.debug(`sleepMemory: calling identity model`);
2671
2918
  const memoir = await llm.call(llm.models.identity, {
2919
+ caller: "sleep-memory",
2672
2920
  instruction,
2673
2921
  message: promptMessage
2674
2922
  });
2675
2923
  log7.debug(`sleepMemory: model returned ${memoir.length} chars`);
2676
2924
  await this.memory.add({
2677
- customId: `daily-journal:${dateKey}`,
2925
+ customId: `daily-journal:${dateKey2}`,
2678
2926
  content: memoir,
2679
2927
  metadata: {
2680
2928
  kind: "daily-journal",
2681
2929
  source: "sleepMemory",
2682
- date: dateKey
2930
+ date: dateKey2
2683
2931
  }
2684
2932
  });
2685
- log7.debug(`sleepMemory: journal persisted for ${dateKey}`);
2933
+ log7.debug(`sleepMemory: journal persisted for ${dateKey2}`);
2686
2934
  return memoir;
2687
2935
  } catch (error) {
2688
2936
  const reason = error instanceof Error ? error.message : String(error);
@@ -2691,22 +2939,22 @@ class Brain {
2691
2939
  }
2692
2940
  }
2693
2941
  async getTodayScheduledAvailability(datetime) {
2694
- const dateKey = formatDateKey(datetime);
2942
+ const dateKey2 = formatDateKey(datetime);
2695
2943
  try {
2696
- const cached = this.availabilityCache.get(dateKey);
2944
+ const cached = this.availabilityCache.get(dateKey2);
2697
2945
  if (cached) {
2698
- log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey}`);
2946
+ log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey2}`);
2699
2947
  return cached;
2700
2948
  }
2701
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
2949
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2702
2950
  if (!stored) {
2703
- log7.debug(`getTodayScheduledAvailability: no schedule for ${dateKey}`);
2951
+ log7.debug(`getTodayScheduledAvailability: no schedule for ${dateKey2}`);
2704
2952
  return null;
2705
2953
  }
2706
2954
  const dailySchedule = JSON.parse(stored.content);
2707
2955
  const availability = await this.deriveAvailabilityFromSchedule(dailySchedule);
2708
- this.availabilityCache.set(dateKey, availability);
2709
- log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey}`);
2956
+ this.availabilityCache.set(dateKey2, availability);
2957
+ log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey2}`);
2710
2958
  return availability;
2711
2959
  } catch (error) {
2712
2960
  const reason = error instanceof Error ? error.message : String(error);
@@ -2717,7 +2965,7 @@ class Brain {
2717
2965
  async getAvailability(datetime = new Date) {
2718
2966
  const h = datetime.getHours();
2719
2967
  const m = datetime.getMinutes();
2720
- const hhmm = `${pad22(h)}:${pad22(m)}`;
2968
+ const hhmm = `${pad23(h)}:${pad23(m)}`;
2721
2969
  const current = h * 60 + m;
2722
2970
  const toMinutes = (s) => {
2723
2971
  const [hh = 0, mm = 0] = s.split(":").map((x) => parseInt(x, 10));
@@ -2730,10 +2978,10 @@ class Brain {
2730
2978
  return result;
2731
2979
  }
2732
2980
  async getCurrentAndAdjacentSlots(now) {
2733
- const dateKey = formatDateKey(now);
2734
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
2981
+ const dateKey2 = formatDateKey(now);
2982
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2735
2983
  if (!stored) {
2736
- log7.debug(`getCurrentAndAdjacentSlots: no schedule for ${dateKey}`);
2984
+ log7.debug(`getCurrentAndAdjacentSlots: no schedule for ${dateKey2}`);
2737
2985
  return [];
2738
2986
  }
2739
2987
  let schedule;
@@ -2766,6 +3014,7 @@ class Brain {
2766
3014
  personality: this.brainbase.baseSystemPrompt
2767
3015
  });
2768
3016
  const result = await llm.call(llm.models.identity, {
3017
+ caller: "availability",
2769
3018
  instruction,
2770
3019
  message: promptMessage,
2771
3020
  jsonSchemaName: "availability",
@@ -2841,6 +3090,7 @@ class Brain {
2841
3090
  let choice;
2842
3091
  try {
2843
3092
  choice = await llm.chatWithTools(llm.models.conversation, {
3093
+ caller: initiate ? "start-conversation" : "send-message",
2844
3094
  instruction: `${this.brainbase.baseSystemPrompt}
2845
3095
 
2846
3096
  ${instruction}`,
@@ -2912,11 +3162,11 @@ ${instruction}`,
2912
3162
  ${facts || "(none indexed)"}`;
2913
3163
  }
2914
3164
  async buildScheduleBlock(now) {
2915
- const dateKey = formatDateKey(now);
3165
+ const dateKey2 = formatDateKey(now);
2916
3166
  const currentSlots = await this.getCurrentAndAdjacentSlots(now);
2917
3167
  const currentBlock = currentSlots.length > 0 ? `Currently (around ${now.toTimeString().slice(0, 5)}):
2918
3168
  ${currentSlots.map((s) => ` ${s.start}-${s.end} ${s.activity}${s.notes ? ` (${s.notes})` : ""}`).join(`
2919
- `)}` : `Currently (${dateKey} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
3169
+ `)}` : `Currently (${dateKey2} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
2920
3170
  const days = [
2921
3171
  {
2922
3172
  label: "Yesterday",
@@ -2939,9 +3189,9 @@ ${currentBlock}
2939
3189
  ${blocks.join(`
2940
3190
  `)}`;
2941
3191
  }
2942
- async getDailyScheduleSummary(dateKey) {
3192
+ async getDailyScheduleSummary(dateKey2) {
2943
3193
  try {
2944
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
3194
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2945
3195
  if (!stored)
2946
3196
  return null;
2947
3197
  const schedule = JSON.parse(stored.content);
@@ -3012,6 +3262,7 @@ ${blocks.join(`
3012
3262
  const personaInitInstruction = await loadPrompt("PERSONA_INIT");
3013
3263
  log7.debug(`Brain.create: generating description`);
3014
3264
  const description = await llm.call(llm.models.identity, {
3265
+ caller: "persona-init",
3015
3266
  instruction: personaInitInstruction,
3016
3267
  message: seed
3017
3268
  });
@@ -3019,6 +3270,7 @@ ${blocks.join(`
3019
3270
  const personaSystemInstruction = await loadPrompt("PERSONA_BASE_SYSTEM_PROMPT");
3020
3271
  log7.debug(`Brain.create: generating base system prompt + dials`);
3021
3272
  const generated = await llm.call(llm.models.identity, {
3273
+ caller: "base-system-prompt",
3022
3274
  instruction: personaSystemInstruction,
3023
3275
  message: description,
3024
3276
  jsonSchemaName: "base-system-prompt",
@@ -3056,9 +3308,12 @@ ${personaSystemFixed}`;
3056
3308
  log7.debug(`Brain.create: brainbase saved (id=${brainId})`);
3057
3309
  return { brainId, brain: new Brain(db, space, brainbase, memory) };
3058
3310
  } catch (error) {
3059
- const reason = error instanceof Error ? error.message : String(error);
3060
- logger.error(`Failed to create brain "${displayName}": ${reason}`);
3061
- return null;
3311
+ let reason = error instanceof Error ? `${error.message} (${error.name})` : String(error);
3312
+ if (error instanceof BadRequestResponseError) {
3313
+ reason = `${reason} ${JSON.stringify(error.body)}`;
3314
+ }
3315
+ log7.debug(`Brain.create failed: ${reason}`);
3316
+ return { error: reason };
3062
3317
  }
3063
3318
  }
3064
3319
  static async delete(brainId) {
@@ -3176,10 +3431,18 @@ var SLEEP_MEMORY_CRON_KEY = "__sleep-memory__";
3176
3431
  var SLEEP_MEMORY_CRON_PATTERN = "0 * * * *";
3177
3432
  var START_CONVERSATION_CRON_KEY = "__start-conversation__";
3178
3433
  var START_CONVERSATION_CRON_PATTERN = "*/10 * * * *";
3179
- var DAILY_SCHEDULE_CRON_KEY = "__daily-schedule__";
3180
- var DAILY_SCHEDULE_CRON_PATTERN = "0 0 * * *";
3181
- var DAILY_SCHEDULE_NOON_CRON_KEY = "__daily-schedule-noon__";
3182
- var DAILY_SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *";
3434
+ var SCHEDULE_CRON_KEY = "__schedule__";
3435
+ var SCHEDULE_CRON_PATTERN = "0 0 * * *";
3436
+ var SCHEDULE_NOON_CRON_KEY = "__schedule-noon__";
3437
+ var SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *";
3438
+ var DO_ACTIONS = ["generateSchedule", "sleepMemory"];
3439
+ var VIEW_THINGS = [
3440
+ "daily-schedule",
3441
+ "monthly-schedule",
3442
+ "sending-queue",
3443
+ "deferred-queue",
3444
+ "today-availability"
3445
+ ];
3183
3446
 
3184
3447
  class BaseChannel {
3185
3448
  brain;
@@ -3199,24 +3462,27 @@ class BaseChannel {
3199
3462
  static activeChannels = new Map;
3200
3463
  constructor(brain) {
3201
3464
  this.brain = brain;
3202
- this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN, async () => {
3203
- const dateKey = formatDateKey(new Date);
3465
+ this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN, () => this.runSleepMemory());
3466
+ this.registerCron(SCHEDULE_CRON_KEY, SCHEDULE_CRON_PATTERN, () => this.regenerateSchedules());
3467
+ this.registerCron(SCHEDULE_NOON_CRON_KEY, SCHEDULE_NOON_CRON_PATTERN, () => this.regenerateSchedules());
3468
+ this.registerCron(START_CONVERSATION_CRON_KEY, START_CONVERSATION_CRON_PATTERN, () => this.runStartConversation());
3469
+ }
3470
+ async runSleepMemory(force = false) {
3471
+ const dateKey2 = formatDateKey(new Date);
3472
+ if (!force) {
3204
3473
  const availability = await this.brain.getAvailability();
3205
3474
  if (availability.status !== "offline") {
3206
3475
  logger.debug(`sleepMemory cron: skip — availability=${availability.status}`);
3207
3476
  return;
3208
3477
  }
3209
- const existing = await this.brain.memory.get(`daily-journal:${dateKey}`);
3478
+ const existing = await this.brain.memory.get(`daily-journal:${dateKey2}`);
3210
3479
  if (existing) {
3211
- logger.debug(`sleepMemory cron: skip — journal for ${dateKey} exists`);
3480
+ logger.debug(`sleepMemory cron: skip — journal for ${dateKey2} exists`);
3212
3481
  return;
3213
3482
  }
3214
- const history = await this.getMessageHistoryBetween(new Date(Date.now() - 24 * 60 * 60 * 1000), new Date);
3215
- await this.brain.sleepMemory(new Date, history);
3216
- });
3217
- this.registerCron(DAILY_SCHEDULE_CRON_KEY, DAILY_SCHEDULE_CRON_PATTERN, () => this.regenerateSchedules());
3218
- this.registerCron(DAILY_SCHEDULE_NOON_CRON_KEY, DAILY_SCHEDULE_NOON_CRON_PATTERN, () => this.regenerateSchedules());
3219
- this.registerCron(START_CONVERSATION_CRON_KEY, START_CONVERSATION_CRON_PATTERN, () => this.runStartConversation());
3483
+ }
3484
+ const history = await this.getMessageHistoryBetween(new Date(Date.now() - 24 * 60 * 60 * 1000), new Date);
3485
+ await this.brain.sleepMemory(new Date, history);
3220
3486
  }
3221
3487
  async regenerateSchedules() {
3222
3488
  logger.debug(`regenerateSchedules: tick for ${this.brain.brainbase.displayName}`);
@@ -3237,8 +3503,8 @@ class BaseChannel {
3237
3503
  return;
3238
3504
  }
3239
3505
  const now = new Date;
3240
- const dateKey = formatDateKey(now);
3241
- const count = this.startConversationCounters.get(dateKey) ?? 0;
3506
+ const dateKey2 = formatDateKey(now);
3507
+ const count = this.startConversationCounters.get(dateKey2) ?? 0;
3242
3508
  const countThreshold = this.brain.brainbase.startConversationCountThreshold;
3243
3509
  if (count >= countThreshold)
3244
3510
  return;
@@ -3251,7 +3517,7 @@ class BaseChannel {
3251
3517
  });
3252
3518
  if (replies.length === 0)
3253
3519
  return;
3254
- this.startConversationCounters.set(dateKey, count + 1);
3520
+ this.startConversationCounters.set(dateKey2, count + 1);
3255
3521
  this.startConversationTimeout = true;
3256
3522
  setTimeout(() => {
3257
3523
  this.startConversationTimeout = false;
@@ -3280,7 +3546,7 @@ class BaseChannel {
3280
3546
  }, callback);
3281
3547
  }
3282
3548
  getRegisteredCrons() {
3283
- return scheduledJobs.filter((c) => c.name && c.name.startsWith(this.resolveCronPrefix())).map((c) => c.name);
3549
+ return scheduledJobs.filter((c) => c.name && c.name.startsWith(this.resolveCronPrefix()));
3284
3550
  }
3285
3551
  pauseCron(key) {
3286
3552
  const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
@@ -3371,6 +3637,13 @@ class BaseChannel {
3371
3637
  this.isChattingDebounce = null;
3372
3638
  }, IS_CHATTING_DEBOUNCE_MS);
3373
3639
  }
3640
+ async initAvailability() {
3641
+ const current = await this.brain.getAvailability();
3642
+ this.previousAvailability = current.status;
3643
+ logger.debug(`initAvailability: ${current.status}`);
3644
+ await this.setAvailability(current.status);
3645
+ this.ensureAvailabilityWatcher();
3646
+ }
3374
3647
  ensureAvailabilityWatcher() {
3375
3648
  if (this.isCronStarted(AVAILABILITY_WATCHER_KEY))
3376
3649
  return;
@@ -3380,6 +3653,9 @@ class BaseChannel {
3380
3653
  const prev = this.previousAvailability;
3381
3654
  this.previousAvailability = current.status;
3382
3655
  logger.debug(`availabilityWatcher: ${prev ?? "(initial)"} → ${current.status}`);
3656
+ if (prev !== current.status) {
3657
+ await this.setAvailability(current.status);
3658
+ }
3383
3659
  if (prev !== null && prev !== "online" && current.status === "online") {
3384
3660
  await this.flushDeferred();
3385
3661
  }
@@ -3428,6 +3704,80 @@ class BaseChannel {
3428
3704
  static all() {
3429
3705
  return Array.from(BaseChannel.activeChannels.values());
3430
3706
  }
3707
+ static async forceDo(brainId, action) {
3708
+ const channel = BaseChannel.activeChannels.get(brainId);
3709
+ if (!channel) {
3710
+ return {
3711
+ ok: false,
3712
+ error: `no active channel for brain "${brainId}" (is it activated and daemon running?)`
3713
+ };
3714
+ }
3715
+ const displayName = channel.brain.brainbase.displayName;
3716
+ logger.info(`do ${action}: forcing for "${displayName}" (${brainId})`);
3717
+ if (action === "generateSchedule") {
3718
+ await channel.regenerateSchedules();
3719
+ } else {
3720
+ await channel.runSleepMemory(true);
3721
+ }
3722
+ return { ok: true, displayName };
3723
+ }
3724
+ static async view(brainId, thing) {
3725
+ const channel = BaseChannel.activeChannels.get(brainId);
3726
+ if (!channel) {
3727
+ return {
3728
+ ok: false,
3729
+ error: `no active channel for brain "${brainId}" (is it activated and daemon running?)`
3730
+ };
3731
+ }
3732
+ const displayName = channel.brain.brainbase.displayName;
3733
+ logger.debug(`view ${thing}: "${displayName}" (${brainId})`);
3734
+ return {
3735
+ ok: true,
3736
+ displayName,
3737
+ value: await channel.readView(thing)
3738
+ };
3739
+ }
3740
+ async readView(thing) {
3741
+ const now = new Date;
3742
+ switch (thing) {
3743
+ case "daily-schedule": {
3744
+ const key = formatDateKey(now);
3745
+ const stored = await this.brain.memory.get(`daily-schedule:${key}`);
3746
+ if (!stored)
3747
+ return null;
3748
+ try {
3749
+ return { key, schedule: JSON.parse(stored.content) };
3750
+ } catch {
3751
+ return { key, raw: stored.content };
3752
+ }
3753
+ }
3754
+ case "monthly-schedule": {
3755
+ const key = formatMonthKey(now);
3756
+ const stored = await this.brain.memory.get(`monthly-schedule:${key}`);
3757
+ if (!stored)
3758
+ return null;
3759
+ try {
3760
+ return { key, schedule: JSON.parse(stored.content) };
3761
+ } catch {
3762
+ return { key, raw: stored.content };
3763
+ }
3764
+ }
3765
+ case "sending-queue":
3766
+ return this.isSendingQueue.map((m) => ({
3767
+ sender: m.sender,
3768
+ time: m.time.toISOString(),
3769
+ content: m.content
3770
+ }));
3771
+ case "deferred-queue":
3772
+ return this.deferredQueue.map((m) => ({
3773
+ sender: m.sender,
3774
+ time: m.time.toISOString(),
3775
+ content: m.content
3776
+ }));
3777
+ case "today-availability":
3778
+ return await this.brain.getTodayScheduledAvailability(now);
3779
+ }
3780
+ }
3431
3781
  static async shutdownAll() {
3432
3782
  await Promise.all(BaseChannel.all().map((c) => c.shutdown()));
3433
3783
  }
@@ -3440,8 +3790,8 @@ class BaseChannel {
3440
3790
  logger.debug(`shutdown: done`);
3441
3791
  }
3442
3792
  stopOwnCrons() {
3443
- for (const key of this.getRegisteredCrons()) {
3444
- this.removeCron(key);
3793
+ for (const cron of this.getRegisteredCrons()) {
3794
+ cron.stop();
3445
3795
  }
3446
3796
  }
3447
3797
  clearTimers() {
@@ -3551,6 +3901,7 @@ class DiscordChannel extends BaseChannel {
3551
3901
  logger.debug(`DiscordClientReady: resolving configured channel ${channelId}`);
3552
3902
  this.resolveConfiguredChannel(channelId);
3553
3903
  }
3904
+ this.initAvailability();
3554
3905
  });
3555
3906
  this.client.on(Events.MessageCreate, (msg) => {
3556
3907
  if (msg.author.bot)
@@ -3719,6 +4070,7 @@ class TelegramChannel extends BaseChannel {
3719
4070
  this.registerActive();
3720
4071
  this.bot.onStart(({ info }) => {
3721
4072
  logger.success(`Telegram ready as @${info.username}`);
4073
+ this.initAvailability();
3722
4074
  });
3723
4075
  this.bot.on("message", (ctx) => {
3724
4076
  if (ctx.from?.isBot())
@@ -3811,8 +4163,8 @@ class TelegramChannel extends BaseChannel {
3811
4163
 
3812
4164
  // src/utils/daemonClient.ts
3813
4165
  import { connect } from "node:net";
3814
- import { join as join3 } from "node:path";
3815
- var DAEMON_SOCKET_PATH = join3(config.brainboxRoot, "daemon.sock");
4166
+ import { join as join4 } from "node:path";
4167
+ var DAEMON_SOCKET_PATH = join4(config.brainboxRoot, "daemon.sock");
3816
4168
  async function sendToDaemon(payload) {
3817
4169
  logger.debug(`sendToDaemon: command="${payload.command}" args=${JSON.stringify(payload.args) ?? "null"}`);
3818
4170
  let reply;
@@ -3874,6 +4226,7 @@ function exchangeOnce(payload) {
3874
4226
  // src/commands/daemon.ts
3875
4227
  import { createServer } from "node:net";
3876
4228
  import { chmodSync, unlinkSync } from "node:fs";
4229
+ import { join as join5 } from "node:path";
3877
4230
 
3878
4231
  // src/commands/daemon/commands.ts
3879
4232
  var log8 = logger.child("daemon-cmd");
@@ -3934,6 +4287,63 @@ defineCommand({
3934
4287
  }
3935
4288
  });
3936
4289
 
4290
+ // src/commands/daemon/doCommand.ts
4291
+ defineCommand({
4292
+ name: "do",
4293
+ handler: async (args) => {
4294
+ const action = args?.action;
4295
+ const brainId = args?.brainId;
4296
+ logger.debug(`do handler: action="${action}" brainId="${brainId}"`);
4297
+ if (typeof action !== "string" || !DO_ACTIONS.includes(action)) {
4298
+ return {
4299
+ ok: false,
4300
+ error: `invalid action (expected one of: ${DO_ACTIONS.join(", ")})`
4301
+ };
4302
+ }
4303
+ if (typeof brainId !== "string" || brainId.trim().length === 0) {
4304
+ return { ok: false, error: "missing brainId" };
4305
+ }
4306
+ const result = await BaseChannel.forceDo(brainId.trim(), action);
4307
+ if (!result.ok)
4308
+ return result;
4309
+ return {
4310
+ ok: true,
4311
+ result: { action, brainId: brainId.trim(), displayName: result.displayName }
4312
+ };
4313
+ }
4314
+ });
4315
+
4316
+ // src/commands/daemon/viewCommand.ts
4317
+ defineCommand({
4318
+ name: "view",
4319
+ handler: async (args) => {
4320
+ const thing = args?.thing;
4321
+ const brainId = args?.brainId;
4322
+ logger.debug(`view handler: thing="${thing}" brainId="${brainId}"`);
4323
+ if (typeof thing !== "string" || !VIEW_THINGS.includes(thing)) {
4324
+ return {
4325
+ ok: false,
4326
+ error: `invalid thing (expected one of: ${VIEW_THINGS.join(", ")})`
4327
+ };
4328
+ }
4329
+ if (typeof brainId !== "string" || brainId.trim().length === 0) {
4330
+ return { ok: false, error: "missing brainId" };
4331
+ }
4332
+ const result = await BaseChannel.view(brainId.trim(), thing);
4333
+ if (!result.ok)
4334
+ return result;
4335
+ return {
4336
+ ok: true,
4337
+ result: {
4338
+ thing,
4339
+ brainId: brainId.trim(),
4340
+ displayName: result.displayName,
4341
+ value: result.value
4342
+ }
4343
+ };
4344
+ }
4345
+ });
4346
+
3937
4347
  // src/commands/daemon.ts
3938
4348
  async function startChannels() {
3939
4349
  const items = await brainManager.listAvailableBrain();
@@ -3968,7 +4378,10 @@ async function startChannels() {
3968
4378
  return started;
3969
4379
  }
3970
4380
  async function daemon() {
3971
- logger.debug(`daemon: boot`);
4381
+ const logDir = join5(config.brainboxRoot, "logs");
4382
+ logger.configure({ logDir });
4383
+ configureLlmLog(join5(logDir, "llm"));
4384
+ logger.debug(`daemon: boot (logDir=${logDir}, llmLogDir=${join5(logDir, "llm")})`);
3972
4385
  const started = await startChannels();
3973
4386
  if (started === 0) {
3974
4387
  logger.info("No activated brains with channels. Daemon idling.");
@@ -4108,7 +4521,8 @@ async function deactivateBrain(brainId) {
4108
4521
  async function createBrain(displayName, seed, options) {
4109
4522
  logger.debug(`createBrain: name="${displayName}" seed length=${seed.length} schedule=${options.schedule}`);
4110
4523
  const result = await Brain.create(displayName, seed);
4111
- if (!result) {
4524
+ if ("error" in result) {
4525
+ logger.error(`Failed to create brain "${displayName}": ${result.error}`);
4112
4526
  process.exitCode = 1;
4113
4527
  return;
4114
4528
  }
@@ -4134,6 +4548,33 @@ async function removeBrain(brainId) {
4134
4548
  }
4135
4549
  logger.success(`Removed brain "${brain.displayName}" (${chalk2.cyan(brainId)})`);
4136
4550
  }
4551
+ async function doAction(action, brainId) {
4552
+ if (!DO_ACTIONS.includes(action)) {
4553
+ logger.error(`Unknown action "${action}". Expected one of: ${DO_ACTIONS.join(", ")}`);
4554
+ process.exit(1);
4555
+ }
4556
+ logger.debug(`do: action=${action} brainId=${brainId}`);
4557
+ const response = await sendToDaemon({
4558
+ command: "do",
4559
+ args: { action, brainId }
4560
+ });
4561
+ const name = response.result?.displayName ?? brainId;
4562
+ logger.success(`Forced ${action} for "${name}" (${brainId}).`);
4563
+ }
4564
+ async function viewThing(thing, brainId) {
4565
+ if (!VIEW_THINGS.includes(thing)) {
4566
+ logger.error(`Unknown thing "${thing}". Expected one of: ${VIEW_THINGS.join(", ")}`);
4567
+ process.exit(1);
4568
+ }
4569
+ logger.debug(`view: thing=${thing} brainId=${brainId}`);
4570
+ const response = await sendToDaemon({
4571
+ command: "view",
4572
+ args: { thing, brainId }
4573
+ });
4574
+ const name = response.result?.displayName ?? brainId;
4575
+ logger.info(`${thing} — "${name}" (${brainId})`);
4576
+ console.log(JSON.stringify(response.result?.value ?? null, null, 2));
4577
+ }
4137
4578
  function register3(program) {
4138
4579
  const cmd = registerCommand(program, {
4139
4580
  name: "brain",
@@ -4144,6 +4585,8 @@ function register3(program) {
4144
4585
  cmd.command("remove <brainId>").description("Remove a brain and its memory").action(removeBrain);
4145
4586
  cmd.command("activate <brainId>").description("Activate a brain").action(activateBrain);
4146
4587
  cmd.command("deactivate <brainId>").description("Deactivate a brain").action(deactivateBrain);
4588
+ cmd.command("do <action> <brainId>").description(`Force-run a daemon job (${DO_ACTIONS.join(" | ")}) for a live brain`).action(doAction);
4589
+ cmd.command("view <thing> <brainId>").description(`Inspect a live brain value (${VIEW_THINGS.join(" | ")})`).action(viewThing);
4147
4590
  return cmd;
4148
4591
  }
4149
4592
 
@@ -4265,6 +4708,9 @@ function RawInput({
4265
4708
  onSubmit
4266
4709
  }) {
4267
4710
  const [value, setValue] = useState(initialValue);
4711
+ useEffect(() => {
4712
+ setValue(initialValue);
4713
+ }, [prompt, initialValue]);
4268
4714
  useInput((input, key) => {
4269
4715
  if (key.return) {
4270
4716
  onSubmit(value);
@@ -4960,6 +5406,19 @@ function Select(props) {
4960
5406
 
4961
5407
  // src/commands/onboard.tsx
4962
5408
  import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
5409
+ function ok(msg) {
5410
+ console.log(chalk3.green(`✔ ${msg}`));
5411
+ }
5412
+ function info(msg) {
5413
+ console.log(msg);
5414
+ }
5415
+ function show(active, node, status) {
5416
+ active.current.unmount();
5417
+ console.clear();
5418
+ if (status)
5419
+ ok(status);
5420
+ active.current = render3(node);
5421
+ }
4963
5422
  function ProviderApp({
4964
5423
  providers,
4965
5424
  onDone
@@ -5031,7 +5490,6 @@ function ProviderApp({
5031
5490
  const extras = PROVIDER_EXTRA_FIELDS[stage.provider] ?? [];
5032
5491
  if (extras.length === 0) {
5033
5492
  setProviderAuth(stage.provider, { apiKey });
5034
- logger.success(`Saved ${stage.provider} to auth.yaml`);
5035
5493
  onDone({ provider: stage.provider });
5036
5494
  return;
5037
5495
  }
@@ -5052,13 +5510,6 @@ function ProviderApp({
5052
5510
  }, undefined, true, undefined, this);
5053
5511
  }
5054
5512
  const nextField = stage.fields[0];
5055
- if (!nextField) {
5056
- setProviderAuth(stage.provider, stage.values);
5057
- logger.success(`Saved ${stage.provider} to auth.yaml`);
5058
- return /* @__PURE__ */ jsxDEV5(Text5, {
5059
- children: "Continuing…"
5060
- }, undefined, false, undefined, this);
5061
- }
5062
5513
  return /* @__PURE__ */ jsxDEV5(Box4, {
5063
5514
  flexDirection: "column",
5064
5515
  children: [
@@ -5084,6 +5535,11 @@ function ProviderApp({
5084
5535
  const values = { ...stage.values };
5085
5536
  if (value)
5086
5537
  values[nextField] = value;
5538
+ if (remaining.length === 0) {
5539
+ setProviderAuth(stage.provider, values);
5540
+ onDone({ provider: stage.provider });
5541
+ return;
5542
+ }
5087
5543
  setStage({
5088
5544
  kind: "extras",
5089
5545
  provider: stage.provider,
@@ -5112,7 +5568,7 @@ function ModelApp2({
5112
5568
  /* @__PURE__ */ jsxDEV5(Text5, {
5113
5569
  dimColor: true,
5114
5570
  children: [
5115
- "e.g. ",
5571
+ "model name, or ",
5116
5572
  /* @__PURE__ */ jsxDEV5(Text5, {
5117
5573
  color: "cyan",
5118
5574
  children: [
@@ -5120,7 +5576,7 @@ function ModelApp2({
5120
5576
  "/"
5121
5577
  ]
5122
5578
  }, undefined, true, undefined, this),
5123
- "model-name — fine-tune later with ",
5579
+ "model — fine-tune later with ",
5124
5580
  /* @__PURE__ */ jsxDEV5(Text5, {
5125
5581
  color: "cyan",
5126
5582
  children: "brainbox model"
@@ -5135,14 +5591,15 @@ function ModelApp2({
5135
5591
  setError("Model cannot be empty");
5136
5592
  return;
5137
5593
  }
5138
- if (!value.startsWith(`${provider}/`)) {
5139
- setError(`Must start with "${provider}/"`);
5594
+ const prefix = `${provider}/`;
5595
+ const full = value.startsWith(prefix) ? value : `${prefix}${value}`;
5596
+ if (full === prefix) {
5597
+ setError("Model cannot be empty");
5140
5598
  return;
5141
5599
  }
5142
- setModelSlot("identity", value);
5143
- setModelSlot("conversation", value);
5144
- logger.success(`Set identity + conversation model to ${value}`);
5145
- onDone();
5600
+ setModelSlot("identity", full);
5601
+ setModelSlot("conversation", full);
5602
+ onDone(full);
5146
5603
  }
5147
5604
  }, undefined, false, undefined, this),
5148
5605
  error && /* @__PURE__ */ jsxDEV5(Text5, {
@@ -5178,7 +5635,6 @@ function SuperMemoryApp({
5178
5635
  return;
5179
5636
  }
5180
5637
  setSupermemoryKey(key);
5181
- logger.success("Saved supermemory key to brainbox.yaml");
5182
5638
  onDone();
5183
5639
  }
5184
5640
  }, undefined, false, undefined, this),
@@ -5194,6 +5650,7 @@ function BrainApp({
5194
5650
  }) {
5195
5651
  const [stage, setStage] = useState5({ kind: "name" });
5196
5652
  const [error, setError] = useState5(null);
5653
+ const [busy, setBusy] = useState5(false);
5197
5654
  if (stage.kind === "name") {
5198
5655
  return /* @__PURE__ */ jsxDEV5(Box4, {
5199
5656
  flexDirection: "column",
@@ -5245,12 +5702,14 @@ function BrainApp({
5245
5702
  dimColor: true,
5246
5703
  children: "One sentence about who they are. The model will expand it."
5247
5704
  }, undefined, false, undefined, this),
5248
- /* @__PURE__ */ jsxDEV5(TextInput, {
5705
+ busy ? /* @__PURE__ */ jsxDEV5(Text5, {
5706
+ dimColor: true,
5707
+ children: "Creating brain… (This can take few minutes depending on the model's response speed)"
5708
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5(TextInput, {
5249
5709
  prompt: "seed> ",
5250
- onSubmit: async (raw) => {
5710
+ onSubmit: (raw) => {
5251
5711
  const seed = raw.trim();
5252
5712
  if (seed === "skip") {
5253
- logger.info("Skipped brain creation.");
5254
5713
  onDone({ brainId: "", displayName: stage.displayName });
5255
5714
  return;
5256
5715
  }
@@ -5258,13 +5717,19 @@ function BrainApp({
5258
5717
  setError("Seed cannot be empty (or type 'skip')");
5259
5718
  return;
5260
5719
  }
5261
- const result = await Brain.create(stage.displayName, seed);
5262
- if (!result) {
5263
- setError("Brain creation failed (check logs above, or type 'skip')");
5264
- return;
5265
- }
5266
- logger.success(`Created brain "${stage.displayName}" (${chalk3.cyan(result.brainId)})`);
5267
- onDone({ brainId: result.brainId, displayName: stage.displayName });
5720
+ setBusy(true);
5721
+ setError(null);
5722
+ Brain.create(stage.displayName, seed).then((result) => {
5723
+ setBusy(false);
5724
+ if ("error" in result) {
5725
+ setError(`Brain creation failed: ${result.error} (fix seed, or type 'skip')`);
5726
+ return;
5727
+ }
5728
+ onDone({
5729
+ brainId: result.brainId,
5730
+ displayName: stage.displayName
5731
+ });
5732
+ });
5268
5733
  }
5269
5734
  }, undefined, false, undefined, this),
5270
5735
  error && /* @__PURE__ */ jsxDEV5(Text5, {
@@ -5305,8 +5770,7 @@ function ChannelApp({
5305
5770
  items: ["discord", "telegram", "skip"],
5306
5771
  onSelect: (v) => {
5307
5772
  if (v === "skip") {
5308
- logger.info("Skipped channel setup.");
5309
- onDone();
5773
+ onDone("Skipped channel setup.");
5310
5774
  return;
5311
5775
  }
5312
5776
  setError(null);
@@ -5365,37 +5829,41 @@ function ChannelApp({
5365
5829
  }, undefined, true, undefined, this),
5366
5830
  /* @__PURE__ */ jsxDEV5(TextInput, {
5367
5831
  prompt: `${stage.kind_ === "discord" ? "channelId" : "chatId"}> `,
5368
- onSubmit: async (raw) => {
5832
+ onSubmit: (raw) => {
5369
5833
  const target = raw.trim();
5370
- const existing = await brainManager.loadBrain(brainId);
5371
- if (!existing) {
5372
- setError(`Brain ${brainId} no longer exists`);
5373
- return;
5374
- }
5375
- let updated;
5376
- if (stage.kind_ === "discord") {
5377
- updated = {
5378
- ...existing,
5379
- channel: "discord",
5380
- discord: { token: stage.token, channelId: target || undefined },
5381
- activated: true
5382
- };
5383
- } else {
5384
- const chatId = target ? Number(target) : undefined;
5385
- if (target && Number.isNaN(chatId)) {
5386
- setError("chatId must be a number");
5834
+ (async () => {
5835
+ const existing = await brainManager.loadBrain(brainId);
5836
+ if (!existing) {
5837
+ setError(`Brain ${brainId} no longer exists`);
5387
5838
  return;
5388
5839
  }
5389
- updated = {
5390
- ...existing,
5391
- channel: "telegram",
5392
- telegram: { token: stage.token, chatId },
5393
- activated: true
5394
- };
5395
- }
5396
- await brainManager.saveBrain(brainId, updated);
5397
- logger.success(`Bound ${displayName} → ${stage.kind_}${target ? ` (${target})` : " (pairing mode)"}`);
5398
- onDone();
5840
+ let updated;
5841
+ if (stage.kind_ === "discord") {
5842
+ updated = {
5843
+ ...existing,
5844
+ channel: "discord",
5845
+ discord: {
5846
+ token: stage.token,
5847
+ channelId: target || undefined
5848
+ },
5849
+ activated: true
5850
+ };
5851
+ } else {
5852
+ const chatId = target ? Number(target) : undefined;
5853
+ if (target && Number.isNaN(chatId)) {
5854
+ setError("chatId must be a number");
5855
+ return;
5856
+ }
5857
+ updated = {
5858
+ ...existing,
5859
+ channel: "telegram",
5860
+ telegram: { token: stage.token, chatId },
5861
+ activated: true
5862
+ };
5863
+ }
5864
+ await brainManager.saveBrain(brainId, updated);
5865
+ onDone(`Bound ${displayName} → ${stage.kind_}${target ? ` (${target})` : " (pairing mode)"}`);
5866
+ })();
5399
5867
  }
5400
5868
  }, undefined, false, undefined, this),
5401
5869
  error && /* @__PURE__ */ jsxDEV5(Text5, {
@@ -5406,46 +5874,52 @@ function ChannelApp({
5406
5874
  }, undefined, true, undefined, this);
5407
5875
  }
5408
5876
  async function runOnboard() {
5409
- logger.info(`Welcome — let's get ${chalk3.bold("brainbox")} ready.`);
5877
+ console.clear();
5878
+ info(`Welcome — let's get ${chalk3.bold("brainbox")} ready.`);
5410
5879
  const providers = listProviderNames().slice().sort();
5411
5880
  const { promise, resolve: resolve2 } = Promise.withResolvers();
5412
- let active = render3(/* @__PURE__ */ jsxDEV5(ProviderApp, {
5881
+ const active = { current: null };
5882
+ active.current = render3(/* @__PURE__ */ jsxDEV5(ProviderApp, {
5413
5883
  providers,
5414
5884
  onDone: (p) => {
5415
- active.unmount();
5416
- active = render3(/* @__PURE__ */ jsxDEV5(ModelApp2, {
5885
+ show(active, /* @__PURE__ */ jsxDEV5(ModelApp2, {
5417
5886
  provider: p.provider,
5418
- onDone: () => {
5419
- active.unmount();
5420
- active = render3(/* @__PURE__ */ jsxDEV5(SuperMemoryApp, {
5887
+ onDone: (model) => {
5888
+ show(active, /* @__PURE__ */ jsxDEV5(SuperMemoryApp, {
5421
5889
  onDone: () => {
5422
- active.unmount();
5423
- active = render3(/* @__PURE__ */ jsxDEV5(BrainApp, {
5890
+ show(active, /* @__PURE__ */ jsxDEV5(BrainApp, {
5424
5891
  onDone: (b) => {
5425
- active.unmount();
5426
5892
  if (!b.brainId) {
5893
+ active.current.unmount();
5894
+ console.clear();
5895
+ info("Skipped brain creation.");
5427
5896
  resolve2();
5428
5897
  return;
5429
5898
  }
5430
- active = render3(/* @__PURE__ */ jsxDEV5(ChannelApp, {
5899
+ show(active, /* @__PURE__ */ jsxDEV5(ChannelApp, {
5431
5900
  brainId: b.brainId,
5432
5901
  displayName: b.displayName,
5433
- onDone: () => {
5434
- active.unmount();
5902
+ onDone: (status) => {
5903
+ active.current.unmount();
5904
+ console.clear();
5905
+ if (status.startsWith("Skipped"))
5906
+ info(status);
5907
+ else
5908
+ ok(status);
5435
5909
  resolve2();
5436
5910
  }
5437
- }, undefined, false, undefined, this));
5911
+ }, undefined, false, undefined, this), `Created brain "${b.displayName}" (${chalk3.cyan(b.brainId)})`);
5438
5912
  }
5439
- }, undefined, false, undefined, this));
5913
+ }, undefined, false, undefined, this), "Saved supermemory key to brainbox.yaml");
5440
5914
  }
5441
- }, undefined, false, undefined, this));
5915
+ }, undefined, false, undefined, this), `Set identity + conversation model to ${model}`);
5442
5916
  }
5443
- }, undefined, false, undefined, this));
5917
+ }, undefined, false, undefined, this), `Saved ${p.provider} to auth.yaml`);
5444
5918
  }
5445
5919
  }, undefined, false, undefined, this));
5446
5920
  await promise;
5447
- logger.success("Onboarding complete.");
5448
- logger.info(`Run ${chalk3.cyan("brainbox daemon")} to bring it online.`);
5921
+ ok("Onboarding complete.");
5922
+ info(`Run ${chalk3.cyan("brainbox daemon")} to bring it online.`);
5449
5923
  }
5450
5924
  function register8(program) {
5451
5925
  return registerCommand(program, {
@@ -5457,12 +5931,12 @@ function register8(program) {
5457
5931
 
5458
5932
  // src/index.ts
5459
5933
  var __filename3 = fileURLToPath2(import.meta.url);
5460
- var __dirname3 = dirname4(__filename3);
5934
+ var __dirname3 = dirname3(__filename3);
5461
5935
  logger.configure({ level: config.debug ? "debug" : "info" });
5462
5936
  logger.debug(`brainbox starting (debug=${config.debug}, root=${config.brainboxRoot})`);
5463
5937
  function getVersion() {
5464
5938
  try {
5465
- const pkgPath = join4(__dirname3, "..", "package.json");
5939
+ const pkgPath = join6(__dirname3, "..", "package.json");
5466
5940
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5467
5941
  return pkg.version ?? "0.0.0";
5468
5942
  } catch {