@p-sw/brainbox 0.1.2-alpha.1 → 0.1.2-alpha.11

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 +776 -363
  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)
@@ -501,6 +578,19 @@ class LLMExecutor {
501
578
  };
502
579
  }
503
580
  }
581
+ function resolveLlmCaller(options, fallback = "llm") {
582
+ return options.caller ?? options.jsonSchemaName ?? fallback;
583
+ }
584
+ function logLlmWire(caller, request, response) {
585
+ if (!isLlmLogEnabled())
586
+ return;
587
+ writeLlmExchange(caller, `======== REQUEST ========
588
+ ${request}
589
+
590
+ ======== RESPONSE ========
591
+ ${response}
592
+ `);
593
+ }
504
594
  function listProviderNames() {
505
595
  return LLMExecutor.listProviderNames();
506
596
  }
@@ -538,7 +628,7 @@ function fromOrChoice(choice) {
538
628
  }));
539
629
  return {
540
630
  message: {
541
- content: typeof msg.content === "string" ? msg.content : undefined,
631
+ content: typeof msg.content === "string" ? stripThinkTags(msg.content) : undefined,
542
632
  toolCalls
543
633
  }
544
634
  };
@@ -569,53 +659,54 @@ class OpenRouterExecutor extends LLMExecutor {
569
659
  async call(model, options) {
570
660
  const jsonMode = "jsonSchemaName" in options;
571
661
  log3.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
572
- const result = await this.client.chat.send({
573
- chatRequest: {
574
- model,
575
- messages: [
576
- { role: "system", content: options.instruction },
577
- { role: "user", content: options.message }
578
- ],
579
- reasoning: {
580
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
581
- },
582
- responseFormat: jsonMode ? {
583
- type: "json_schema",
584
- jsonSchema: {
585
- name: options.jsonSchemaName,
586
- schema: options.jsonSchema,
587
- strict: true
588
- }
589
- } : { type: "text" },
590
- stream: false
591
- }
592
- });
593
- const content = result.choices[0]?.message?.content;
662
+ const chatRequest = {
663
+ model,
664
+ messages: [
665
+ { role: "system", content: options.instruction },
666
+ { role: "user", content: options.message }
667
+ ],
668
+ reasoning: {
669
+ effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
670
+ },
671
+ responseFormat: jsonMode ? {
672
+ type: "json_schema",
673
+ jsonSchema: {
674
+ name: options.jsonSchemaName,
675
+ schema: options.jsonSchema,
676
+ strict: true
677
+ }
678
+ } : { type: "text" },
679
+ stream: false
680
+ };
681
+ const result = await this.client.chat.send({ chatRequest });
682
+ logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
683
+ const raw = result.choices[0]?.message?.content;
684
+ const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
594
685
  if (!content) {
595
686
  log3.debug(`call: empty content in choice 0`);
596
687
  throw new Error("Empty response from model");
597
688
  }
598
689
  log3.debug(`call: response ${content.length} chars`);
599
- return jsonMode ? JSON.parse(content) : content;
690
+ return jsonMode ? parseModelJson(content) : content;
600
691
  }
601
692
  async chatWithTools(model, options) {
602
693
  log3.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
603
- const result = await this.client.chat.send({
604
- chatRequest: {
605
- model,
606
- messages: [
607
- { role: "system", content: options.instruction },
608
- ...options.messages.map(toOrMessage)
609
- ],
610
- reasoning: {
611
- effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
612
- },
613
- responseFormat: { type: "text" },
614
- tools: options.tools.map(toOrTool),
615
- parallelToolCalls: options.parallelToolCalls ?? false,
616
- stream: false
617
- }
618
- });
694
+ const chatRequest = {
695
+ model,
696
+ messages: [
697
+ { role: "system", content: options.instruction },
698
+ ...options.messages.map(toOrMessage)
699
+ ],
700
+ reasoning: {
701
+ effort: options.reasoningEffort ?? (model === this.models.identity ? REASONING_EFFORT_MAP.medium : REASONING_EFFORT_MAP.none)
702
+ },
703
+ responseFormat: { type: "text" },
704
+ tools: options.tools.map(toOrTool),
705
+ parallelToolCalls: options.parallelToolCalls ?? false,
706
+ stream: false
707
+ };
708
+ const result = await this.client.chat.send({ chatRequest });
709
+ logLlmWire(resolveLlmCaller(options), JSON.stringify(chatRequest), JSON.stringify(result));
619
710
  const choice = result.choices[0];
620
711
  if (!choice) {
621
712
  log3.debug(`chatWithTools: no choice in response`);
@@ -655,7 +746,7 @@ function fromChoice(choice) {
655
746
  }));
656
747
  return {
657
748
  message: {
658
- content: typeof msg.content === "string" ? msg.content : undefined,
749
+ content: typeof msg.content === "string" ? stripThinkTags(msg.content) : undefined,
659
750
  toolCalls
660
751
  }
661
752
  };
@@ -729,7 +820,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
729
820
  }
730
821
  return url.toString();
731
822
  }
732
- async sendRequest(body, reasoningEffort) {
823
+ async sendRequest(body, reasoningEffort, caller) {
733
824
  const modelName = body["model"];
734
825
  const modelStr = typeof modelName === "string" ? modelName : "";
735
826
  const url = this.buildRequestUrl(modelStr, reasoningEffort);
@@ -739,21 +830,34 @@ class OpenAICompatibleExecutor extends LLMExecutor {
739
830
  ...authHeader,
740
831
  ...this.defaultHeaders
741
832
  };
833
+ const requestRaw = JSON.stringify(body);
742
834
  const res = await fetch(url, {
743
835
  method: "POST",
744
836
  headers,
745
- body: JSON.stringify(body)
837
+ body: requestRaw
746
838
  });
839
+ const responseRaw = await res.text().catch(() => "");
840
+ logLlmWire(caller, requestRaw, responseRaw);
747
841
  if (!res.ok) {
748
- const text = await res.text().catch(() => "");
749
- log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
842
+ log4.error(`${this.providerName}: HTTP ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
750
843
  throw new Error(`${this.providerName} request failed: ${res.status} ${res.statusText}`);
751
844
  }
752
- const data = await res.json();
845
+ let data;
846
+ try {
847
+ data = JSON.parse(responseRaw);
848
+ } catch {
849
+ throw new Error(`${this.providerName}: invalid JSON response`);
850
+ }
753
851
  if (data.error) {
754
852
  log4.error(`${this.providerName}: API error ${data.error.type ?? ""} ${data.error.message ?? ""}`);
755
853
  throw new Error(`${this.providerName} API error: ${data.error.message ?? "unknown"}`);
756
854
  }
855
+ const baseCode = data.base_resp?.status_code;
856
+ if (typeof baseCode === "number" && baseCode !== 0) {
857
+ const msg = data.base_resp?.status_msg?.trim() || `status_code ${baseCode}`;
858
+ log4.error(`${this.providerName}: base_resp ${baseCode} ${msg}`);
859
+ throw new Error(`${this.providerName} API error: ${msg}`);
860
+ }
757
861
  return data;
758
862
  }
759
863
  async call(model, options) {
@@ -769,14 +873,18 @@ class OpenAICompatibleExecutor extends LLMExecutor {
769
873
  responseFormat: jsonMode ? buildResponseFormat(options.jsonSchemaName, options.jsonSchema) : undefined,
770
874
  reasoningEffort: reasoning
771
875
  });
772
- const data = await this.sendRequest(body, options.reasoningEffort);
773
- const content = data.choices?.[0]?.message?.content;
876
+ const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
877
+ const choice = data.choices?.[0];
878
+ const raw = choice?.message?.content;
879
+ const content = typeof raw === "string" ? stripThinkTags(raw) : raw;
774
880
  if (!content) {
775
- log4.debug(`call: empty content in choice 0`);
776
- throw new Error("Empty response from model");
881
+ const finish = choice?.finish_reason ?? "no-choice";
882
+ const reasoningLen = typeof choice?.message?.reasoning_content === "string" ? choice.message.reasoning_content.length : 0;
883
+ log4.debug(`call: empty content in choice 0 finish_reason=${finish} reasoning_len=${reasoningLen} rawType=${raw === null ? "null" : typeof raw}`);
884
+ throw new Error(reasoningLen > 0 ? `Empty response from model (finish_reason=${finish}; reasoning present but no content)` : "Empty response from model");
777
885
  }
778
886
  log4.debug(`call: response ${content.length} chars`);
779
- return jsonMode ? JSON.parse(content) : content;
887
+ return jsonMode ? parseModelJson(content) : content;
780
888
  }
781
889
  async chatWithTools(model, options) {
782
890
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
@@ -791,7 +899,7 @@ class OpenAICompatibleExecutor extends LLMExecutor {
791
899
  parallelToolCalls: options.parallelToolCalls ?? false,
792
900
  reasoningEffort: reasoning
793
901
  });
794
- const data = await this.sendRequest(body, options.reasoningEffort);
902
+ const data = await this.sendRequest(body, options.reasoningEffort, resolveLlmCaller(options));
795
903
  const choice = data.choices?.[0];
796
904
  if (!choice) {
797
905
  log4.debug(`chatWithTools: no choice in response`);
@@ -1090,16 +1198,40 @@ class ZenMuxExecutor extends OpenAICompatibleExecutor {
1090
1198
  }
1091
1199
  }
1092
1200
 
1093
- // src/provider/providers/MiniMax.ts
1094
- class MiniMaxExecutor extends OpenAICompatibleExecutor {
1201
+ // src/provider/providers/minimax.ts
1202
+ class MiniMaxBase extends OpenAICompatibleExecutor {
1203
+ buildBody(opts) {
1204
+ const jsonMode = opts.responseFormat !== undefined;
1205
+ const body = super.buildBody(opts);
1206
+ delete body["reasoning_effort"];
1207
+ delete body["response_format"];
1208
+ delete body["parallel_tool_calls"];
1209
+ body["reasoning_split"] = true;
1210
+ body["max_completion_tokens"] = 16384;
1211
+ const wantThink = !jsonMode && opts.reasoningEffort !== undefined && opts.reasoningEffort !== "none";
1212
+ body["thinking"] = wantThink ? { type: "adaptive" } : { type: "disabled" };
1213
+ return body;
1214
+ }
1215
+ }
1216
+ function minimaxOpts(providerName, baseURL, opts) {
1217
+ return {
1218
+ providerName,
1219
+ baseURL,
1220
+ apiKey: opts.apiKey,
1221
+ conversationModel: opts.conversationModel,
1222
+ identityModel: opts.identityModel
1223
+ };
1224
+ }
1225
+
1226
+ class MiniMaxExecutor extends MiniMaxBase {
1095
1227
  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
- });
1228
+ super(minimaxOpts("minimax", "https://api.minimax.io/v1", opts));
1229
+ }
1230
+ }
1231
+
1232
+ class MiniMaxCnExecutor extends MiniMaxBase {
1233
+ constructor(opts) {
1234
+ super(minimaxOpts("minimax-cn", "https://api.minimaxi.com/v1", opts));
1103
1235
  }
1104
1236
  }
1105
1237
 
@@ -1252,7 +1384,17 @@ class CloudflareGatewayExecutor extends OpenAICompatibleExecutor {
1252
1384
  // src/provider/providers/cloudflare_workers.ts
1253
1385
  function toCloudflareMessage(m) {
1254
1386
  if (m.role === "assistant") {
1255
- return { role: "assistant", content: m.content ?? "" };
1387
+ return {
1388
+ role: "assistant",
1389
+ content: m.content ?? "",
1390
+ tool_calls: m.toolCalls?.map((c) => {
1391
+ let args = c.function.arguments;
1392
+ try {
1393
+ args = JSON.parse(c.function.arguments);
1394
+ } catch {}
1395
+ return { id: c.id, name: c.function.name, arguments: args };
1396
+ })
1397
+ };
1256
1398
  }
1257
1399
  if (m.role === "tool") {
1258
1400
  return { role: "tool", content: m.content, tool_call_id: m.toolCallId };
@@ -1275,21 +1417,23 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1275
1417
  const accountId = readAuthString(opts.auth, "accountId", "CLOUDFLARE_ACCOUNT_ID");
1276
1418
  this.baseURL = accountId ? `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run` : "https://api.cloudflare.com/client/v4/accounts/__cf_account_id__/ai/run";
1277
1419
  }
1278
- async run(model, body) {
1420
+ async run(model, body, caller) {
1279
1421
  const url = `${this.baseURL}/${encodeURIComponent(model)}`;
1422
+ const requestRaw = JSON.stringify(body);
1280
1423
  const res = await fetch(url, {
1281
1424
  method: "POST",
1282
1425
  headers: {
1283
1426
  Authorization: `Bearer ${this.apiKey}`,
1284
1427
  "Content-Type": "application/json"
1285
1428
  },
1286
- body: JSON.stringify(body)
1429
+ body: requestRaw
1287
1430
  });
1431
+ const responseRaw = await res.text().catch(() => "");
1432
+ logLlmWire(caller, requestRaw, responseRaw);
1288
1433
  if (!res.ok) {
1289
- const text = await res.text().catch(() => "");
1290
- throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1434
+ throw new Error(`cloudflare-workers request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1291
1435
  }
1292
- return await res.json();
1436
+ return JSON.parse(responseRaw);
1293
1437
  }
1294
1438
  async call(model, options) {
1295
1439
  const jsonMode = "jsonSchemaName" in options;
@@ -1310,15 +1454,15 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1310
1454
  if (reasoning !== "none") {
1311
1455
  body["reasoning_effort"] = reasoning;
1312
1456
  }
1313
- const data = await this.run(model, body);
1457
+ const data = await this.run(model, body, resolveLlmCaller(options));
1314
1458
  if (data.errors && data.errors.length > 0) {
1315
1459
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1316
1460
  }
1317
- const content = data.result?.response ?? "";
1461
+ const content = stripThinkTags(data.result?.response ?? "");
1318
1462
  if (!content) {
1319
1463
  throw new Error("Empty response from model");
1320
1464
  }
1321
- return jsonMode ? JSON.parse(content) : content;
1465
+ return jsonMode ? parseModelJson(content) : content;
1322
1466
  }
1323
1467
  async chatWithTools(model, options) {
1324
1468
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
@@ -1339,11 +1483,11 @@ class CloudflareWorkersExecutor extends LLMExecutor {
1339
1483
  if (reasoning !== "none") {
1340
1484
  body["reasoning_effort"] = reasoning;
1341
1485
  }
1342
- const data = await this.run(model, body);
1486
+ const data = await this.run(model, body, resolveLlmCaller(options));
1343
1487
  if (data.errors && data.errors.length > 0) {
1344
1488
  throw new Error(`cloudflare-workers API error: ${data.errors.map((e) => e.message).join("; ")}`);
1345
1489
  }
1346
- const content = data.result?.response ?? "";
1490
+ const content = stripThinkTags(data.result?.response ?? "");
1347
1491
  const toolCalls = data.result?.tool_calls?.map((c, idx) => ({
1348
1492
  id: `call_${idx}`,
1349
1493
  function: {
@@ -1381,7 +1525,8 @@ class AzureOpenAIExecutor extends OpenAICompatibleExecutor {
1381
1525
  super({
1382
1526
  providerName: "azure-openai",
1383
1527
  baseURL: `https://${resource || "__resource__"}.openai.azure.com/openai/deployments`,
1384
- apiKey: opts.apiKey,
1528
+ apiKey: "",
1529
+ defaultHeaders: { "api-key": opts.apiKey },
1385
1530
  conversationModel: opts.conversationModel,
1386
1531
  identityModel: opts.identityModel
1387
1532
  });
@@ -1401,7 +1546,8 @@ class AzureCognitiveExecutor extends OpenAICompatibleExecutor {
1401
1546
  super({
1402
1547
  providerName: "azure-cognitive",
1403
1548
  baseURL: `https://${resource || "__resource__"}.cognitiveservices.azure.com/openai/deployments`,
1404
- apiKey: opts.apiKey,
1549
+ apiKey: "",
1550
+ defaultHeaders: { "api-key": opts.apiKey },
1405
1551
  conversationModel: opts.conversationModel,
1406
1552
  identityModel: opts.identityModel
1407
1553
  });
@@ -1504,7 +1650,8 @@ class AnthropicExecutor extends LLMExecutor {
1504
1650
  this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com").replace(/\/v1\/?$/, "");
1505
1651
  this.apiVersion = extra.apiVersion ?? "2023-06-01";
1506
1652
  }
1507
- async send(body) {
1653
+ async send(body, caller) {
1654
+ const requestRaw = JSON.stringify(body);
1508
1655
  const res = await fetch(`${this.baseURL}/v1/messages`, {
1509
1656
  method: "POST",
1510
1657
  headers: {
@@ -1512,65 +1659,72 @@ class AnthropicExecutor extends LLMExecutor {
1512
1659
  "anthropic-version": this.apiVersion,
1513
1660
  "Content-Type": "application/json"
1514
1661
  },
1515
- body: JSON.stringify(body)
1662
+ body: requestRaw
1516
1663
  });
1664
+ const responseRaw = await res.text().catch(() => "");
1665
+ logLlmWire(caller, requestRaw, responseRaw);
1517
1666
  if (!res.ok) {
1518
- const text = await res.text().catch(() => "");
1519
- throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1667
+ throw new Error(`anthropic request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1520
1668
  }
1521
- return await res.json();
1669
+ return JSON.parse(responseRaw);
1522
1670
  }
1523
1671
  async call(model, options) {
1524
1672
  const jsonMode = "jsonSchemaName" in options;
1525
1673
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1526
1674
  const log5 = logger.child("llm:anthropic");
1527
1675
  log5.debug(`call: model=${model} jsonSchema=${jsonMode ? options.jsonSchemaName : "-"} msgLen=${options.message.length}`);
1676
+ const outputCap = 4096;
1528
1677
  const body = {
1529
1678
  model,
1530
- max_tokens: 4096,
1679
+ max_tokens: outputCap,
1531
1680
  system: options.instruction,
1532
1681
  messages: [{ role: "user", content: options.message }]
1533
1682
  };
1534
1683
  if (reasoning !== "none") {
1684
+ const budget = REASONING_BUDGET[reasoning];
1685
+ body["max_tokens"] = budget + outputCap;
1535
1686
  body["thinking"] = {
1536
1687
  type: "enabled",
1537
- budget_tokens: REASONING_BUDGET[reasoning]
1688
+ budget_tokens: budget
1538
1689
  };
1539
1690
  }
1540
- const data = await this.send(body);
1691
+ const data = await this.send(body, resolveLlmCaller(options));
1541
1692
  if (data.error) {
1542
1693
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1543
1694
  }
1544
- const text = (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
1695
+ const text = stripThinkTags((data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join(""));
1545
1696
  if (!text) {
1546
1697
  throw new Error("Empty response from model");
1547
1698
  }
1548
- return jsonMode ? JSON.parse(text) : text;
1699
+ return jsonMode ? parseModelJson(text) : text;
1549
1700
  }
1550
1701
  async chatWithTools(model, options) {
1551
1702
  const reasoning = defaultReasoningEffort(options.reasoningEffort, model, this.models.identity);
1552
1703
  const log5 = logger.child("llm:anthropic");
1553
1704
  log5.debug(`chatWithTools: model=${model} msgs=${options.messages.length} tools=${options.tools.length}`);
1554
1705
  const { system, msgs } = toAnthropicMessages(options.messages);
1706
+ const outputCap = 4096;
1555
1707
  const body = {
1556
1708
  model,
1557
- max_tokens: 4096,
1709
+ max_tokens: outputCap,
1558
1710
  system: system ?? options.instruction,
1559
1711
  messages: msgs,
1560
1712
  tools: options.tools.map(toAnthropicTool)
1561
1713
  };
1562
1714
  if (reasoning !== "none") {
1715
+ const budget = REASONING_BUDGET[reasoning];
1716
+ body["max_tokens"] = budget + outputCap;
1563
1717
  body["thinking"] = {
1564
1718
  type: "enabled",
1565
- budget_tokens: REASONING_BUDGET[reasoning]
1719
+ budget_tokens: budget
1566
1720
  };
1567
1721
  }
1568
- const data = await this.send(body);
1722
+ const data = await this.send(body, resolveLlmCaller(options));
1569
1723
  if (data.error) {
1570
1724
  throw new Error(`anthropic API error: ${data.error.message ?? "unknown"}`);
1571
1725
  }
1572
1726
  const blocks = data.content ?? [];
1573
- const text = blocks.filter((b) => b.type === "text").map((b) => b.text).join("");
1727
+ const text = stripThinkTags(blocks.filter((b) => b.type === "text").map((b) => b.text).join(""));
1574
1728
  const toolCalls = blocks.filter((b) => b.type === "tool_use").map((b) => ({
1575
1729
  id: b.id,
1576
1730
  function: {
@@ -1584,11 +1738,11 @@ class AnthropicExecutor extends LLMExecutor {
1584
1738
 
1585
1739
  // src/provider/providers/bedrock.ts
1586
1740
  import { createHmac, createHash } from "node:crypto";
1587
- var pad2 = (n) => n.toString().padStart(2, "0");
1741
+ var pad22 = (n) => n.toString().padStart(2, "0");
1588
1742
  function amzDate(now) {
1589
1743
  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`
1744
+ date: `${now.getUTCFullYear()}${pad22(now.getUTCMonth() + 1)}${pad22(now.getUTCDate())}`,
1745
+ datetime: `${now.getUTCFullYear()}${pad22(now.getUTCMonth() + 1)}${pad22(now.getUTCDate())}T` + `${pad22(now.getUTCHours())}${pad22(now.getUTCMinutes())}${pad22(now.getUTCSeconds())}Z`
1592
1746
  };
1593
1747
  }
1594
1748
  function signRequest(opts) {
@@ -1696,7 +1850,7 @@ function toAnthropicTool2(t) {
1696
1850
  }
1697
1851
  function extractAnthropicContent(data) {
1698
1852
  const content = data["content"] ?? [];
1699
- const text = content.filter((b) => b["type"] === "text").map((b) => b["text"]).join("");
1853
+ const text = stripThinkTags(content.filter((b) => b["type"] === "text").map((b) => b["text"]).join(""));
1700
1854
  const toolCalls = content.filter((b) => b["type"] === "tool_use").map((b) => ({
1701
1855
  id: b["id"],
1702
1856
  function: {
@@ -1728,7 +1882,7 @@ class BedrockExecutor extends LLMExecutor {
1728
1882
  this.sessionToken = readAuthString(opts.auth, "sessionToken", "AWS_SESSION_TOKEN");
1729
1883
  this.baseURL = `https://bedrock-runtime.${this.region}.amazonaws.com`;
1730
1884
  }
1731
- async invoke(model, body) {
1885
+ async invoke(model, body, caller) {
1732
1886
  const bodyStr = JSON.stringify(body);
1733
1887
  const path = `/model/${encodeURIComponent(model)}/invoke`;
1734
1888
  const headers = signRequest({
@@ -1747,11 +1901,12 @@ class BedrockExecutor extends LLMExecutor {
1747
1901
  headers,
1748
1902
  body: bodyStr
1749
1903
  });
1904
+ const responseRaw = await res.text().catch(() => "");
1905
+ logLlmWire(caller, bodyStr, responseRaw);
1750
1906
  if (!res.ok) {
1751
- const text = await res.text().catch(() => "");
1752
- throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
1907
+ throw new Error(`bedrock request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1753
1908
  }
1754
- return await res.json();
1909
+ return JSON.parse(responseRaw);
1755
1910
  }
1756
1911
  async call(model, options) {
1757
1912
  const jsonMode = "jsonSchemaName" in options;
@@ -1766,12 +1921,12 @@ class BedrockExecutor extends LLMExecutor {
1766
1921
  system: options.instruction,
1767
1922
  messages: [{ role: "user", content: options.message }]
1768
1923
  };
1769
- const data = await this.invoke(model, body);
1924
+ const data = await this.invoke(model, body, resolveLlmCaller(options));
1770
1925
  const { text } = extractAnthropicContent(data);
1771
1926
  if (!text) {
1772
1927
  throw new Error("Empty response from model");
1773
1928
  }
1774
- return jsonMode ? JSON.parse(text) : text;
1929
+ return jsonMode ? parseModelJson(text) : text;
1775
1930
  }
1776
1931
  async chatWithTools(model, options) {
1777
1932
  const log5 = logger.child("llm:bedrock");
@@ -1787,7 +1942,7 @@ class BedrockExecutor extends LLMExecutor {
1787
1942
  messages: msgs,
1788
1943
  tools: options.tools.map(toAnthropicTool2)
1789
1944
  };
1790
- const data = await this.invoke(model, body);
1945
+ const data = await this.invoke(model, body, resolveLlmCaller(options));
1791
1946
  const { text, toolCalls } = extractAnthropicContent(data);
1792
1947
  return { message: { content: text || undefined, toolCalls } };
1793
1948
  }
@@ -1801,6 +1956,7 @@ var VertexAuthSchema = z5.object({
1801
1956
  }).loose();
1802
1957
  function toGeminiContents(messages) {
1803
1958
  const out = [];
1959
+ const nameByCallId = new Map;
1804
1960
  for (const m of messages) {
1805
1961
  if (m.role === "system")
1806
1962
  continue;
@@ -1814,6 +1970,7 @@ function toGeminiContents(messages) {
1814
1970
  parts.push({ text: m.content });
1815
1971
  if (m.toolCalls) {
1816
1972
  for (const c of m.toolCalls) {
1973
+ nameByCallId.set(c.id, c.function.name);
1817
1974
  let args = {};
1818
1975
  try {
1819
1976
  args = c.function.arguments ? JSON.parse(c.function.arguments) : {};
@@ -1835,21 +1992,26 @@ function toGeminiContents(messages) {
1835
1992
  }
1836
1993
  out.push({
1837
1994
  role: "user",
1838
- parts: [{ functionResponse: { name: "", response } }]
1995
+ parts: [
1996
+ {
1997
+ functionResponse: {
1998
+ name: nameByCallId.get(m.toolCallId) ?? "",
1999
+ response
2000
+ }
2001
+ }
2002
+ ]
1839
2003
  });
1840
2004
  }
1841
2005
  }
1842
2006
  return out;
1843
2007
  }
1844
- function toGeminiTool(t) {
2008
+ function toGeminiTools(tools) {
1845
2009
  return {
1846
- functionDeclarations: [
1847
- {
1848
- name: t.name,
1849
- description: t.description,
1850
- parameters: t.parameters ?? { type: "object", properties: {} }
1851
- }
1852
- ]
2010
+ functionDeclarations: tools.map((t) => ({
2011
+ name: t.name,
2012
+ description: t.description,
2013
+ parameters: t.parameters ?? { type: "object", properties: {} }
2014
+ }))
1853
2015
  };
1854
2016
  }
1855
2017
  function extractFromGemini(data) {
@@ -1870,7 +2032,10 @@ function extractFromGemini(data) {
1870
2032
  });
1871
2033
  }
1872
2034
  }
1873
- return { text, toolCalls: toolCalls.length > 0 ? toolCalls : undefined };
2035
+ return {
2036
+ text: stripThinkTags(text),
2037
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
2038
+ };
1874
2039
  }
1875
2040
 
1876
2041
  class VertexExecutor extends LLMExecutor {
@@ -1893,21 +2058,23 @@ class VertexExecutor extends LLMExecutor {
1893
2058
  this.region = extra.region ?? readAuthString(opts.auth, "region", "GOOGLE_CLOUD_REGION") ?? "us-central1";
1894
2059
  this.baseURL = this.project ? `https://${this.region}-aiplatform.googleapis.com/v1/projects/${this.project}/locations/${this.region}/publishers/google/models` : "https://us-central1-aiplatform.googleapis.com/v1";
1895
2060
  }
1896
- async generate(model, body) {
2061
+ async generate(model, body, caller) {
1897
2062
  const url = `${this.baseURL}/${encodeURIComponent(model)}:generateContent`;
2063
+ const requestRaw = JSON.stringify(body);
1898
2064
  const res = await fetch(url, {
1899
2065
  method: "POST",
1900
2066
  headers: {
1901
2067
  Authorization: `Bearer ${this.apiKey}`,
1902
2068
  "Content-Type": "application/json"
1903
2069
  },
1904
- body: JSON.stringify(body)
2070
+ body: requestRaw
1905
2071
  });
2072
+ const responseRaw = await res.text().catch(() => "");
2073
+ logLlmWire(caller, requestRaw, responseRaw);
1906
2074
  if (!res.ok) {
1907
- const text = await res.text().catch(() => "");
1908
- throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2075
+ throw new Error(`vertex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
1909
2076
  }
1910
- return await res.json();
2077
+ return JSON.parse(responseRaw);
1911
2078
  }
1912
2079
  async call(model, options) {
1913
2080
  const jsonMode = "jsonSchemaName" in options;
@@ -1928,7 +2095,7 @@ class VertexExecutor extends LLMExecutor {
1928
2095
  responseSchema: options.jsonSchema
1929
2096
  };
1930
2097
  }
1931
- const data = await this.generate(model, body);
2098
+ const data = await this.generate(model, body, resolveLlmCaller(options));
1932
2099
  if (data.error) {
1933
2100
  throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
1934
2101
  }
@@ -1936,7 +2103,7 @@ class VertexExecutor extends LLMExecutor {
1936
2103
  if (!text) {
1937
2104
  throw new Error("Empty response from model");
1938
2105
  }
1939
- return jsonMode ? JSON.parse(text) : text;
2106
+ return jsonMode ? parseModelJson(text) : text;
1940
2107
  }
1941
2108
  async chatWithTools(model, options) {
1942
2109
  const log5 = logger.child("llm:vertex");
@@ -1945,9 +2112,9 @@ class VertexExecutor extends LLMExecutor {
1945
2112
  const body = {
1946
2113
  contents,
1947
2114
  systemInstruction: { parts: [{ text: options.instruction }] },
1948
- tools: options.tools.length > 0 ? [toGeminiTool(options.tools[0])] : undefined
2115
+ tools: options.tools.length > 0 ? [toGeminiTools(options.tools)] : undefined
1949
2116
  };
1950
- const data = await this.generate(model, body);
2117
+ const data = await this.generate(model, body, resolveLlmCaller(options));
1951
2118
  if (data.error) {
1952
2119
  throw new Error(`vertex API error: ${data.error.message ?? "unknown"}`);
1953
2120
  }
@@ -2023,20 +2190,22 @@ class GitLabDuoExecutor extends LLMExecutor {
2023
2190
  const extra = parsed.success ? parsed.data : {};
2024
2191
  this.baseURL = (extra.baseURL ?? readAuthString(opts.auth, "baseURL", "GITLAB_BASE_URL") ?? "https://gitlab.com/api/v4/ai/llm/proxy").replace(/\/+$/, "");
2025
2192
  }
2026
- async send(body) {
2193
+ async send(body, caller) {
2194
+ const requestRaw = JSON.stringify(body);
2027
2195
  const res = await fetch(this.baseURL, {
2028
2196
  method: "POST",
2029
2197
  headers: {
2030
2198
  Authorization: `Bearer ${this.apiKey}`,
2031
2199
  "Content-Type": "application/json"
2032
2200
  },
2033
- body: JSON.stringify(body)
2201
+ body: requestRaw
2034
2202
  });
2203
+ const responseRaw = await res.text().catch(() => "");
2204
+ logLlmWire(caller, requestRaw, responseRaw);
2035
2205
  if (!res.ok) {
2036
- const text = await res.text().catch(() => "");
2037
- throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2206
+ throw new Error(`gitlab-duo request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2038
2207
  }
2039
- return await res.json();
2208
+ return JSON.parse(responseRaw);
2040
2209
  }
2041
2210
  async call(model, options) {
2042
2211
  const jsonMode = "jsonSchemaName" in options;
@@ -2049,15 +2218,15 @@ class GitLabDuoExecutor extends LLMExecutor {
2049
2218
  { role: "user", content: options.message }
2050
2219
  ]
2051
2220
  };
2052
- const data = await this.send(body);
2221
+ const data = await this.send(body, resolveLlmCaller(options));
2053
2222
  if (data.error) {
2054
2223
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2055
2224
  }
2056
- const content = data.choices?.[0]?.message?.content ?? "";
2225
+ const content = stripThinkTags(data.choices?.[0]?.message?.content ?? "");
2057
2226
  if (!content) {
2058
2227
  throw new Error("Empty response from model");
2059
2228
  }
2060
- return jsonMode ? JSON.parse(content) : content;
2229
+ return jsonMode ? parseModelJson(content) : content;
2061
2230
  }
2062
2231
  async chatWithTools(model, options) {
2063
2232
  const log5 = logger.child("llm:gitlab-duo");
@@ -2070,7 +2239,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2070
2239
  ],
2071
2240
  tools: options.tools.map(toTool)
2072
2241
  };
2073
- const data = await this.send(body);
2242
+ const data = await this.send(body, resolveLlmCaller(options));
2074
2243
  if (data.error) {
2075
2244
  throw new Error(`gitlab-duo API error: ${data.error.message ?? "unknown"}`);
2076
2245
  }
@@ -2081,7 +2250,7 @@ class GitLabDuoExecutor extends LLMExecutor {
2081
2250
  }));
2082
2251
  return {
2083
2252
  message: {
2084
- content: choice?.message?.content ?? undefined,
2253
+ content: choice?.message?.content ? stripThinkTags(choice.message.content) : undefined,
2085
2254
  toolCalls
2086
2255
  }
2087
2256
  };
@@ -2138,20 +2307,22 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2138
2307
  const account = extra.account ?? readAuthString(opts.auth, "account", "SNOWFLAKE_ACCOUNT") ?? "";
2139
2308
  this.baseURL = account ? `https://${account}.snowflakecomputing.com/api/v2/cortex/inference:complete` : "https://__account__.snowflakecomputing.com/api/v2/cortex/inference:complete";
2140
2309
  }
2141
- async send(body) {
2310
+ async send(body, caller) {
2311
+ const requestRaw = JSON.stringify(body);
2142
2312
  const res = await fetch(this.baseURL, {
2143
2313
  method: "POST",
2144
2314
  headers: {
2145
2315
  Authorization: `Bearer ${this.apiKey}`,
2146
2316
  "Content-Type": "application/json"
2147
2317
  },
2148
- body: JSON.stringify(body)
2318
+ body: requestRaw
2149
2319
  });
2320
+ const responseRaw = await res.text().catch(() => "");
2321
+ logLlmWire(caller, requestRaw, responseRaw);
2150
2322
  if (!res.ok) {
2151
- const text = await res.text().catch(() => "");
2152
- throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${text.slice(0, 500)}`);
2323
+ throw new Error(`snowflake-cortex request failed: ${res.status} ${res.statusText} body=${responseRaw.slice(0, 500)}`);
2153
2324
  }
2154
- return await res.json();
2325
+ return JSON.parse(responseRaw);
2155
2326
  }
2156
2327
  async call(model, options) {
2157
2328
  const jsonMode = "jsonSchemaName" in options;
@@ -2168,15 +2339,15 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2168
2339
  if (reasoning !== "none") {
2169
2340
  body["reasoning_effort"] = reasoning;
2170
2341
  }
2171
- const data = await this.send(body);
2342
+ const data = await this.send(body, resolveLlmCaller(options));
2172
2343
  if (data.error) {
2173
2344
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2174
2345
  }
2175
- const content = data.choices?.[0]?.message?.content ?? data.message ?? "";
2346
+ const content = stripThinkTags(data.choices?.[0]?.message?.content ?? data.message ?? "");
2176
2347
  if (!content) {
2177
2348
  throw new Error("Empty response from model");
2178
2349
  }
2179
- return jsonMode ? JSON.parse(content) : content;
2350
+ return jsonMode ? parseModelJson(content) : content;
2180
2351
  }
2181
2352
  async chatWithTools(model, options) {
2182
2353
  const log5 = logger.child("llm:snowflake-cortex");
@@ -2189,12 +2360,12 @@ class SnowflakeCortexExecutor extends LLMExecutor {
2189
2360
  ],
2190
2361
  tools: options.tools.map(toCortexTool)
2191
2362
  };
2192
- const data = await this.send(body);
2363
+ const data = await this.send(body, resolveLlmCaller(options));
2193
2364
  if (data.error) {
2194
2365
  throw new Error(`snowflake-cortex API error: ${data.error.message ?? "unknown"}`);
2195
2366
  }
2196
2367
  const choice = data.choices?.[0];
2197
- const content = choice?.message?.content ?? data.message ?? "";
2368
+ const content = stripThinkTags(choice?.message?.content ?? data.message ?? "");
2198
2369
  const toolCalls = choice?.message?.tool_calls?.map((c) => ({
2199
2370
  id: c.id,
2200
2371
  function: {
@@ -2233,7 +2404,8 @@ register("stackit", StackitExecutor);
2233
2404
  register("gmi", GmiExecutor);
2234
2405
  register("zai", ZaiExecutor);
2235
2406
  register("zenmux", ZenMuxExecutor);
2236
- register("MiniMax", MiniMaxExecutor);
2407
+ register("minimax", MiniMaxExecutor);
2408
+ register("minimax-cn", MiniMaxCnExecutor);
2237
2409
  register("ionet", IoNetExecutor);
2238
2410
  register("baseten", BasetenExecutor);
2239
2411
  register("cortecs", CortecsExecutor);
@@ -2267,14 +2439,14 @@ var llm = new Proxy({}, {
2267
2439
  // src/provider/promptLoader.ts
2268
2440
  import { existsSync as existsSync2 } from "fs";
2269
2441
  import { readFile as readFile2 } from "fs/promises";
2270
- import path, { dirname as dirname3 } from "path";
2442
+ import path, { dirname as dirname2 } from "path";
2271
2443
  import { fileURLToPath } from "url";
2272
2444
  var log5 = logger.child("prompt-loader");
2273
2445
  function fileName(promptKey) {
2274
2446
  return promptKey.toLowerCase() + ".md";
2275
2447
  }
2276
2448
  var __filename2 = fileURLToPath(import.meta.url);
2277
- var __dirname2 = dirname3(__filename2);
2449
+ var __dirname2 = dirname2(__filename2);
2278
2450
  var PROMPTS_DIR = existsSync2(path.resolve(__dirname2, "prompts")) ? path.resolve(__dirname2, "prompts") : path.resolve(__dirname2, "../../prompts");
2279
2451
  async function loadPrompt(promptKey) {
2280
2452
  const filePath = path.join(PROMPTS_DIR, fileName(promptKey));
@@ -2399,14 +2571,14 @@ function translateMessageHistory(personaName, entries) {
2399
2571
  }
2400
2572
 
2401
2573
  // src/brain/schedule.ts
2402
- function pad22(n) {
2574
+ function pad23(n) {
2403
2575
  return n < 10 ? `0${n}` : `${n}`;
2404
2576
  }
2405
2577
  function formatDateKey(d) {
2406
- return `${d.getFullYear()}-${pad22(d.getMonth() + 1)}-${pad22(d.getDate())}`;
2578
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}-${pad23(d.getDate())}`;
2407
2579
  }
2408
2580
  function formatMonthKey(d) {
2409
- return `${d.getFullYear()}-${pad22(d.getMonth() + 1)}`;
2581
+ return `${d.getFullYear()}-${pad23(d.getMonth() + 1)}`;
2410
2582
  }
2411
2583
 
2412
2584
  // src/brain/memory.ts
@@ -2494,22 +2666,22 @@ class Brain {
2494
2666
  db;
2495
2667
  space;
2496
2668
  brainbase;
2497
- memory;
2498
2669
  availabilityCache = new Map;
2499
- constructor(db, space, brainbase, memory = new Memory(this.db, this.space)) {
2670
+ memory;
2671
+ constructor(db, space, brainbase, memory) {
2500
2672
  this.db = db;
2501
2673
  this.space = space;
2502
2674
  this.brainbase = brainbase;
2503
- this.memory = memory;
2675
+ this.memory = memory ?? new Memory(this.db, this.space);
2504
2676
  log7.debug(`Brain constructed: id=${brainbase.brainId} name=${brainbase.displayName} space=${space.name}`);
2505
2677
  }
2506
2678
  async createDailySchedule(datetime) {
2507
- const dateKey = formatDateKey(datetime);
2508
- log7.debug(`createDailySchedule: starting for ${dateKey}`);
2679
+ const dateKey2 = formatDateKey(datetime);
2680
+ log7.debug(`createDailySchedule: starting for ${dateKey2}`);
2509
2681
  try {
2510
- const existing = await this.memory.get(`daily-schedule:${dateKey}`);
2682
+ const existing = await this.memory.get(`daily-schedule:${dateKey2}`);
2511
2683
  if (existing) {
2512
- log7.debug(`createDailySchedule: cache hit for ${dateKey}`);
2684
+ log7.debug(`createDailySchedule: cache hit for ${dateKey2}`);
2513
2685
  try {
2514
2686
  return JSON.parse(existing.content);
2515
2687
  } catch (parseErr) {
@@ -2537,7 +2709,7 @@ class Brain {
2537
2709
  }
2538
2710
  const instruction = await loadPrompt("DAILY_SCHEDULE");
2539
2711
  const promptMessage = [
2540
- `Target date: ${dateKey} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
2712
+ `Target date: ${dateKey2} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
2541
2713
  `Personality: ${this.brainbase.baseSystemPrompt}`,
2542
2714
  monthlySummary ? `Monthly summary for this day: ${monthlySummary}` : "(no monthly summary available for this date)",
2543
2715
  `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 +2720,7 @@ class Brain {
2548
2720
  `);
2549
2721
  log7.debug(`createDailySchedule: calling identity model`);
2550
2722
  const schedule = await llm.call(llm.models.identity, {
2723
+ caller: "daily-schedule",
2551
2724
  instruction,
2552
2725
  message: promptMessage,
2553
2726
  jsonSchemaName: "daily-schedule",
@@ -2555,15 +2728,15 @@ class Brain {
2555
2728
  });
2556
2729
  log7.debug(`createDailySchedule: model returned ${schedule.items.length} slots`);
2557
2730
  await this.memory.add({
2558
- customId: `daily-schedule:${dateKey}`,
2731
+ customId: `daily-schedule:${dateKey2}`,
2559
2732
  content: JSON.stringify(schedule),
2560
2733
  metadata: {
2561
2734
  kind: "schedule",
2562
2735
  source: "createDailySchedule",
2563
- date: dateKey
2736
+ date: dateKey2
2564
2737
  }
2565
2738
  });
2566
- log7.debug(`createDailySchedule: persisted ${dateKey}`);
2739
+ log7.debug(`createDailySchedule: persisted ${dateKey2}`);
2567
2740
  return schedule;
2568
2741
  } catch (error) {
2569
2742
  let reason = error instanceof Error ? error.message + `(${error.name})` : String(error);
@@ -2575,17 +2748,21 @@ class Brain {
2575
2748
  }
2576
2749
  async regenerateSchedules() {
2577
2750
  const today = new Date;
2751
+ const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
2752
+ const monthly = await this.createMonthlySchedule(nextMonth);
2753
+ if (!monthly) {
2754
+ log7.debug(`regenerateSchedules: skip daily — monthly schedule generation failed`);
2755
+ return;
2756
+ }
2578
2757
  const tomorrow = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1);
2579
2758
  await this.createDailySchedule(tomorrow);
2580
2759
  await this.createDailySchedule(today);
2581
- const nextMonth = new Date(today.getFullYear(), today.getMonth() + 1, today.getDate());
2582
- await this.createMonthlySchedule(nextMonth);
2583
2760
  }
2584
2761
  async createMonthlySchedule(datetime) {
2585
2762
  const year = datetime.getMonth() === 11 ? datetime.getFullYear() + 1 : datetime.getFullYear();
2586
2763
  const month = (datetime.getMonth() + 1) % 12;
2587
2764
  const daysInMonth = new Date(year, month + 1, 0).getDate();
2588
- const monthKey = `${year}-${pad22(month + 1)}`;
2765
+ const monthKey = `${year}-${pad23(month + 1)}`;
2589
2766
  log7.debug(`createMonthlySchedule: starting for ${monthKey}`);
2590
2767
  try {
2591
2768
  const existing = await this.memory.get(`monthly-schedule:${monthKey}`);
@@ -2598,7 +2775,7 @@ class Brain {
2598
2775
  }
2599
2776
  }
2600
2777
  const twoMonthsAgo = new Date(year, month - 2, 1);
2601
- const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad22(twoMonthsAgo.getMonth() + 1)}`;
2778
+ const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad23(twoMonthsAgo.getMonth() + 1)}`;
2602
2779
  log7.debug(`createMonthlySchedule: gathering context (history, ${twoMonthsAgoKey})`);
2603
2780
  const [history, twoMonthsAgoStored] = await Promise.all([
2604
2781
  this.getHistoryFacts(),
@@ -2626,6 +2803,7 @@ class Brain {
2626
2803
  `);
2627
2804
  log7.debug(`createMonthlySchedule: calling identity model`);
2628
2805
  const schedule = await llm.call(llm.models.identity, {
2806
+ caller: "monthly-schedule",
2629
2807
  instruction,
2630
2808
  message: promptMessage,
2631
2809
  jsonSchemaName: "monthly-schedule",
@@ -2656,11 +2834,11 @@ class Brain {
2656
2834
  }
2657
2835
  log7.debug(`sleepMemory: starting, ${history.length} messages`);
2658
2836
  try {
2659
- const dateKey = formatDateKey(datetime);
2837
+ const dateKey2 = formatDateKey(datetime);
2660
2838
  const instruction = await loadPrompt("MEMOIR");
2661
2839
  const historyBlock = translateMessageHistory(this.brainbase.displayName, history);
2662
2840
  const promptMessage = [
2663
- `Date: ${dateKey}`,
2841
+ `Date: ${dateKey2}`,
2664
2842
  `Personality: ${this.brainbase.baseSystemPrompt}`,
2665
2843
  `Conversation log:`,
2666
2844
  historyBlock
@@ -2669,20 +2847,21 @@ class Brain {
2669
2847
  `);
2670
2848
  log7.debug(`sleepMemory: calling identity model`);
2671
2849
  const memoir = await llm.call(llm.models.identity, {
2850
+ caller: "sleep-memory",
2672
2851
  instruction,
2673
2852
  message: promptMessage
2674
2853
  });
2675
2854
  log7.debug(`sleepMemory: model returned ${memoir.length} chars`);
2676
2855
  await this.memory.add({
2677
- customId: `daily-journal:${dateKey}`,
2856
+ customId: `daily-journal:${dateKey2}`,
2678
2857
  content: memoir,
2679
2858
  metadata: {
2680
2859
  kind: "daily-journal",
2681
2860
  source: "sleepMemory",
2682
- date: dateKey
2861
+ date: dateKey2
2683
2862
  }
2684
2863
  });
2685
- log7.debug(`sleepMemory: journal persisted for ${dateKey}`);
2864
+ log7.debug(`sleepMemory: journal persisted for ${dateKey2}`);
2686
2865
  return memoir;
2687
2866
  } catch (error) {
2688
2867
  const reason = error instanceof Error ? error.message : String(error);
@@ -2691,22 +2870,22 @@ class Brain {
2691
2870
  }
2692
2871
  }
2693
2872
  async getTodayScheduledAvailability(datetime) {
2694
- const dateKey = formatDateKey(datetime);
2873
+ const dateKey2 = formatDateKey(datetime);
2695
2874
  try {
2696
- const cached = this.availabilityCache.get(dateKey);
2875
+ const cached = this.availabilityCache.get(dateKey2);
2697
2876
  if (cached) {
2698
- log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey}`);
2877
+ log7.debug(`getTodayScheduledAvailability: cache hit for ${dateKey2}`);
2699
2878
  return cached;
2700
2879
  }
2701
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
2880
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2702
2881
  if (!stored) {
2703
- log7.debug(`getTodayScheduledAvailability: no schedule for ${dateKey}`);
2882
+ log7.debug(`getTodayScheduledAvailability: no schedule for ${dateKey2}`);
2704
2883
  return null;
2705
2884
  }
2706
2885
  const dailySchedule = JSON.parse(stored.content);
2707
2886
  const availability = await this.deriveAvailabilityFromSchedule(dailySchedule);
2708
- this.availabilityCache.set(dateKey, availability);
2709
- log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey}`);
2887
+ this.availabilityCache.set(dateKey2, availability);
2888
+ log7.debug(`getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey2}`);
2710
2889
  return availability;
2711
2890
  } catch (error) {
2712
2891
  const reason = error instanceof Error ? error.message : String(error);
@@ -2717,7 +2896,7 @@ class Brain {
2717
2896
  async getAvailability(datetime = new Date) {
2718
2897
  const h = datetime.getHours();
2719
2898
  const m = datetime.getMinutes();
2720
- const hhmm = `${pad22(h)}:${pad22(m)}`;
2899
+ const hhmm = `${pad23(h)}:${pad23(m)}`;
2721
2900
  const current = h * 60 + m;
2722
2901
  const toMinutes = (s) => {
2723
2902
  const [hh = 0, mm = 0] = s.split(":").map((x) => parseInt(x, 10));
@@ -2730,10 +2909,10 @@ class Brain {
2730
2909
  return result;
2731
2910
  }
2732
2911
  async getCurrentAndAdjacentSlots(now) {
2733
- const dateKey = formatDateKey(now);
2734
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
2912
+ const dateKey2 = formatDateKey(now);
2913
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2735
2914
  if (!stored) {
2736
- log7.debug(`getCurrentAndAdjacentSlots: no schedule for ${dateKey}`);
2915
+ log7.debug(`getCurrentAndAdjacentSlots: no schedule for ${dateKey2}`);
2737
2916
  return [];
2738
2917
  }
2739
2918
  let schedule;
@@ -2766,6 +2945,7 @@ class Brain {
2766
2945
  personality: this.brainbase.baseSystemPrompt
2767
2946
  });
2768
2947
  const result = await llm.call(llm.models.identity, {
2948
+ caller: "availability",
2769
2949
  instruction,
2770
2950
  message: promptMessage,
2771
2951
  jsonSchemaName: "availability",
@@ -2841,6 +3021,7 @@ class Brain {
2841
3021
  let choice;
2842
3022
  try {
2843
3023
  choice = await llm.chatWithTools(llm.models.conversation, {
3024
+ caller: initiate ? "start-conversation" : "send-message",
2844
3025
  instruction: `${this.brainbase.baseSystemPrompt}
2845
3026
 
2846
3027
  ${instruction}`,
@@ -2912,11 +3093,11 @@ ${instruction}`,
2912
3093
  ${facts || "(none indexed)"}`;
2913
3094
  }
2914
3095
  async buildScheduleBlock(now) {
2915
- const dateKey = formatDateKey(now);
3096
+ const dateKey2 = formatDateKey(now);
2916
3097
  const currentSlots = await this.getCurrentAndAdjacentSlots(now);
2917
3098
  const currentBlock = currentSlots.length > 0 ? `Currently (around ${now.toTimeString().slice(0, 5)}):
2918
3099
  ${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)`;
3100
+ `)}` : `Currently (${dateKey2} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
2920
3101
  const days = [
2921
3102
  {
2922
3103
  label: "Yesterday",
@@ -2939,9 +3120,9 @@ ${currentBlock}
2939
3120
  ${blocks.join(`
2940
3121
  `)}`;
2941
3122
  }
2942
- async getDailyScheduleSummary(dateKey) {
3123
+ async getDailyScheduleSummary(dateKey2) {
2943
3124
  try {
2944
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
3125
+ const stored = await this.memory.get(`daily-schedule:${dateKey2}`);
2945
3126
  if (!stored)
2946
3127
  return null;
2947
3128
  const schedule = JSON.parse(stored.content);
@@ -3012,6 +3193,7 @@ ${blocks.join(`
3012
3193
  const personaInitInstruction = await loadPrompt("PERSONA_INIT");
3013
3194
  log7.debug(`Brain.create: generating description`);
3014
3195
  const description = await llm.call(llm.models.identity, {
3196
+ caller: "persona-init",
3015
3197
  instruction: personaInitInstruction,
3016
3198
  message: seed
3017
3199
  });
@@ -3019,6 +3201,7 @@ ${blocks.join(`
3019
3201
  const personaSystemInstruction = await loadPrompt("PERSONA_BASE_SYSTEM_PROMPT");
3020
3202
  log7.debug(`Brain.create: generating base system prompt + dials`);
3021
3203
  const generated = await llm.call(llm.models.identity, {
3204
+ caller: "base-system-prompt",
3022
3205
  instruction: personaSystemInstruction,
3023
3206
  message: description,
3024
3207
  jsonSchemaName: "base-system-prompt",
@@ -3056,9 +3239,12 @@ ${personaSystemFixed}`;
3056
3239
  log7.debug(`Brain.create: brainbase saved (id=${brainId})`);
3057
3240
  return { brainId, brain: new Brain(db, space, brainbase, memory) };
3058
3241
  } catch (error) {
3059
- const reason = error instanceof Error ? error.message : String(error);
3060
- logger.error(`Failed to create brain "${displayName}": ${reason}`);
3061
- return null;
3242
+ let reason = error instanceof Error ? `${error.message} (${error.name})` : String(error);
3243
+ if (error instanceof BadRequestResponseError) {
3244
+ reason = `${reason} ${JSON.stringify(error.body)}`;
3245
+ }
3246
+ log7.debug(`Brain.create failed: ${reason}`);
3247
+ return { error: reason };
3062
3248
  }
3063
3249
  }
3064
3250
  static async delete(brainId) {
@@ -3176,10 +3362,18 @@ var SLEEP_MEMORY_CRON_KEY = "__sleep-memory__";
3176
3362
  var SLEEP_MEMORY_CRON_PATTERN = "0 * * * *";
3177
3363
  var START_CONVERSATION_CRON_KEY = "__start-conversation__";
3178
3364
  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 * * *";
3365
+ var SCHEDULE_CRON_KEY = "__schedule__";
3366
+ var SCHEDULE_CRON_PATTERN = "0 0 * * *";
3367
+ var SCHEDULE_NOON_CRON_KEY = "__schedule-noon__";
3368
+ var SCHEDULE_NOON_CRON_PATTERN = "0 12 * * *";
3369
+ var DO_ACTIONS = ["generateSchedule", "sleepMemory"];
3370
+ var VIEW_THINGS = [
3371
+ "daily-schedule",
3372
+ "monthly-schedule",
3373
+ "sending-queue",
3374
+ "deferred-queue",
3375
+ "today-availability"
3376
+ ];
3183
3377
 
3184
3378
  class BaseChannel {
3185
3379
  brain;
@@ -3199,24 +3393,27 @@ class BaseChannel {
3199
3393
  static activeChannels = new Map;
3200
3394
  constructor(brain) {
3201
3395
  this.brain = brain;
3202
- this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN, async () => {
3203
- const dateKey = formatDateKey(new Date);
3396
+ this.registerCron(SLEEP_MEMORY_CRON_KEY, SLEEP_MEMORY_CRON_PATTERN, () => this.runSleepMemory());
3397
+ this.registerCron(SCHEDULE_CRON_KEY, SCHEDULE_CRON_PATTERN, () => this.regenerateSchedules());
3398
+ this.registerCron(SCHEDULE_NOON_CRON_KEY, SCHEDULE_NOON_CRON_PATTERN, () => this.regenerateSchedules());
3399
+ this.registerCron(START_CONVERSATION_CRON_KEY, START_CONVERSATION_CRON_PATTERN, () => this.runStartConversation());
3400
+ }
3401
+ async runSleepMemory(force = false) {
3402
+ const dateKey2 = formatDateKey(new Date);
3403
+ if (!force) {
3204
3404
  const availability = await this.brain.getAvailability();
3205
3405
  if (availability.status !== "offline") {
3206
3406
  logger.debug(`sleepMemory cron: skip — availability=${availability.status}`);
3207
3407
  return;
3208
3408
  }
3209
- const existing = await this.brain.memory.get(`daily-journal:${dateKey}`);
3409
+ const existing = await this.brain.memory.get(`daily-journal:${dateKey2}`);
3210
3410
  if (existing) {
3211
- logger.debug(`sleepMemory cron: skip — journal for ${dateKey} exists`);
3411
+ logger.debug(`sleepMemory cron: skip — journal for ${dateKey2} exists`);
3212
3412
  return;
3213
3413
  }
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());
3414
+ }
3415
+ const history = await this.getMessageHistoryBetween(new Date(Date.now() - 24 * 60 * 60 * 1000), new Date);
3416
+ await this.brain.sleepMemory(new Date, history);
3220
3417
  }
3221
3418
  async regenerateSchedules() {
3222
3419
  logger.debug(`regenerateSchedules: tick for ${this.brain.brainbase.displayName}`);
@@ -3237,8 +3434,8 @@ class BaseChannel {
3237
3434
  return;
3238
3435
  }
3239
3436
  const now = new Date;
3240
- const dateKey = formatDateKey(now);
3241
- const count = this.startConversationCounters.get(dateKey) ?? 0;
3437
+ const dateKey2 = formatDateKey(now);
3438
+ const count = this.startConversationCounters.get(dateKey2) ?? 0;
3242
3439
  const countThreshold = this.brain.brainbase.startConversationCountThreshold;
3243
3440
  if (count >= countThreshold)
3244
3441
  return;
@@ -3251,7 +3448,7 @@ class BaseChannel {
3251
3448
  });
3252
3449
  if (replies.length === 0)
3253
3450
  return;
3254
- this.startConversationCounters.set(dateKey, count + 1);
3451
+ this.startConversationCounters.set(dateKey2, count + 1);
3255
3452
  this.startConversationTimeout = true;
3256
3453
  setTimeout(() => {
3257
3454
  this.startConversationTimeout = false;
@@ -3280,7 +3477,7 @@ class BaseChannel {
3280
3477
  }, callback);
3281
3478
  }
3282
3479
  getRegisteredCrons() {
3283
- return scheduledJobs.filter((c) => c.name && c.name.startsWith(this.resolveCronPrefix())).map((c) => c.name);
3480
+ return scheduledJobs.filter((c) => c.name && c.name.startsWith(this.resolveCronPrefix()));
3284
3481
  }
3285
3482
  pauseCron(key) {
3286
3483
  const job = scheduledJobs.find((c) => c.name === this.resolveCronName(key));
@@ -3371,6 +3568,13 @@ class BaseChannel {
3371
3568
  this.isChattingDebounce = null;
3372
3569
  }, IS_CHATTING_DEBOUNCE_MS);
3373
3570
  }
3571
+ async initAvailability() {
3572
+ const current = await this.brain.getAvailability();
3573
+ this.previousAvailability = current.status;
3574
+ logger.debug(`initAvailability: ${current.status}`);
3575
+ await this.setAvailability(current.status);
3576
+ this.ensureAvailabilityWatcher();
3577
+ }
3374
3578
  ensureAvailabilityWatcher() {
3375
3579
  if (this.isCronStarted(AVAILABILITY_WATCHER_KEY))
3376
3580
  return;
@@ -3380,6 +3584,9 @@ class BaseChannel {
3380
3584
  const prev = this.previousAvailability;
3381
3585
  this.previousAvailability = current.status;
3382
3586
  logger.debug(`availabilityWatcher: ${prev ?? "(initial)"} → ${current.status}`);
3587
+ if (prev !== current.status) {
3588
+ await this.setAvailability(current.status);
3589
+ }
3383
3590
  if (prev !== null && prev !== "online" && current.status === "online") {
3384
3591
  await this.flushDeferred();
3385
3592
  }
@@ -3428,6 +3635,88 @@ class BaseChannel {
3428
3635
  static all() {
3429
3636
  return Array.from(BaseChannel.activeChannels.values());
3430
3637
  }
3638
+ static forceDo(brainId, action) {
3639
+ const channel = BaseChannel.activeChannels.get(brainId);
3640
+ if (!channel) {
3641
+ return {
3642
+ ok: false,
3643
+ error: `no active channel for brain "${brainId}" (is it activated and daemon running?)`
3644
+ };
3645
+ }
3646
+ const displayName = channel.brain.brainbase.displayName;
3647
+ logger.info(`do ${action}: queued for "${displayName}" (${brainId})`);
3648
+ (async () => {
3649
+ try {
3650
+ if (action === "generateSchedule") {
3651
+ await channel.regenerateSchedules();
3652
+ } else {
3653
+ await channel.runSleepMemory(true);
3654
+ }
3655
+ logger.success(`do ${action}: done for "${displayName}" (${brainId})`);
3656
+ } catch (error) {
3657
+ const reason = error instanceof Error ? error.message : String(error);
3658
+ logger.error(`do ${action}: failed for "${displayName}" (${brainId}): ${reason}`);
3659
+ }
3660
+ })();
3661
+ return { ok: true, displayName };
3662
+ }
3663
+ static async view(brainId, thing) {
3664
+ const channel = BaseChannel.activeChannels.get(brainId);
3665
+ if (!channel) {
3666
+ return {
3667
+ ok: false,
3668
+ error: `no active channel for brain "${brainId}" (is it activated and daemon running?)`
3669
+ };
3670
+ }
3671
+ const displayName = channel.brain.brainbase.displayName;
3672
+ logger.debug(`view ${thing}: "${displayName}" (${brainId})`);
3673
+ return {
3674
+ ok: true,
3675
+ displayName,
3676
+ value: await channel.readView(thing)
3677
+ };
3678
+ }
3679
+ async readView(thing) {
3680
+ const now = new Date;
3681
+ switch (thing) {
3682
+ case "daily-schedule": {
3683
+ const key = formatDateKey(now);
3684
+ const stored = await this.brain.memory.get(`daily-schedule:${key}`);
3685
+ if (!stored)
3686
+ return null;
3687
+ try {
3688
+ return { key, schedule: JSON.parse(stored.content) };
3689
+ } catch {
3690
+ return { key, raw: stored.content };
3691
+ }
3692
+ }
3693
+ case "monthly-schedule": {
3694
+ const key = formatMonthKey(now);
3695
+ const stored = await this.brain.memory.get(`monthly-schedule:${key}`);
3696
+ if (!stored)
3697
+ return null;
3698
+ try {
3699
+ return { key, schedule: JSON.parse(stored.content) };
3700
+ } catch {
3701
+ return { key, raw: stored.content };
3702
+ }
3703
+ }
3704
+ case "sending-queue":
3705
+ return this.isSendingQueue.map((m) => ({
3706
+ sender: m.sender,
3707
+ time: m.time.toISOString(),
3708
+ content: m.content
3709
+ }));
3710
+ case "deferred-queue":
3711
+ return this.deferredQueue.map((m) => ({
3712
+ sender: m.sender,
3713
+ time: m.time.toISOString(),
3714
+ content: m.content
3715
+ }));
3716
+ case "today-availability":
3717
+ return await this.brain.getTodayScheduledAvailability(now);
3718
+ }
3719
+ }
3431
3720
  static async shutdownAll() {
3432
3721
  await Promise.all(BaseChannel.all().map((c) => c.shutdown()));
3433
3722
  }
@@ -3440,8 +3729,8 @@ class BaseChannel {
3440
3729
  logger.debug(`shutdown: done`);
3441
3730
  }
3442
3731
  stopOwnCrons() {
3443
- for (const key of this.getRegisteredCrons()) {
3444
- this.removeCron(key);
3732
+ for (const cron of this.getRegisteredCrons()) {
3733
+ cron.stop();
3445
3734
  }
3446
3735
  }
3447
3736
  clearTimers() {
@@ -3551,6 +3840,7 @@ class DiscordChannel extends BaseChannel {
3551
3840
  logger.debug(`DiscordClientReady: resolving configured channel ${channelId}`);
3552
3841
  this.resolveConfiguredChannel(channelId);
3553
3842
  }
3843
+ this.initAvailability();
3554
3844
  });
3555
3845
  this.client.on(Events.MessageCreate, (msg) => {
3556
3846
  if (msg.author.bot)
@@ -3719,6 +4009,7 @@ class TelegramChannel extends BaseChannel {
3719
4009
  this.registerActive();
3720
4010
  this.bot.onStart(({ info }) => {
3721
4011
  logger.success(`Telegram ready as @${info.username}`);
4012
+ this.initAvailability();
3722
4013
  });
3723
4014
  this.bot.on("message", (ctx) => {
3724
4015
  if (ctx.from?.isBot())
@@ -3811,8 +4102,8 @@ class TelegramChannel extends BaseChannel {
3811
4102
 
3812
4103
  // src/utils/daemonClient.ts
3813
4104
  import { connect } from "node:net";
3814
- import { join as join3 } from "node:path";
3815
- var DAEMON_SOCKET_PATH = join3(config.brainboxRoot, "daemon.sock");
4105
+ import { join as join4 } from "node:path";
4106
+ var DAEMON_SOCKET_PATH = join4(config.brainboxRoot, "daemon.sock");
3816
4107
  async function sendToDaemon(payload) {
3817
4108
  logger.debug(`sendToDaemon: command="${payload.command}" args=${JSON.stringify(payload.args) ?? "null"}`);
3818
4109
  let reply;
@@ -3874,6 +4165,7 @@ function exchangeOnce(payload) {
3874
4165
  // src/commands/daemon.ts
3875
4166
  import { createServer } from "node:net";
3876
4167
  import { chmodSync, unlinkSync } from "node:fs";
4168
+ import { join as join5 } from "node:path";
3877
4169
 
3878
4170
  // src/commands/daemon/commands.ts
3879
4171
  var log8 = logger.child("daemon-cmd");
@@ -3934,6 +4226,63 @@ defineCommand({
3934
4226
  }
3935
4227
  });
3936
4228
 
4229
+ // src/commands/daemon/doCommand.ts
4230
+ defineCommand({
4231
+ name: "do",
4232
+ handler: async (args) => {
4233
+ const action = args?.action;
4234
+ const brainId = args?.brainId;
4235
+ logger.debug(`do handler: action="${action}" brainId="${brainId}"`);
4236
+ if (typeof action !== "string" || !DO_ACTIONS.includes(action)) {
4237
+ return {
4238
+ ok: false,
4239
+ error: `invalid action (expected one of: ${DO_ACTIONS.join(", ")})`
4240
+ };
4241
+ }
4242
+ if (typeof brainId !== "string" || brainId.trim().length === 0) {
4243
+ return { ok: false, error: "missing brainId" };
4244
+ }
4245
+ const result = BaseChannel.forceDo(brainId.trim(), action);
4246
+ if (!result.ok)
4247
+ return result;
4248
+ return {
4249
+ ok: true,
4250
+ result: { action, brainId: brainId.trim(), displayName: result.displayName }
4251
+ };
4252
+ }
4253
+ });
4254
+
4255
+ // src/commands/daemon/viewCommand.ts
4256
+ defineCommand({
4257
+ name: "view",
4258
+ handler: async (args) => {
4259
+ const thing = args?.thing;
4260
+ const brainId = args?.brainId;
4261
+ logger.debug(`view handler: thing="${thing}" brainId="${brainId}"`);
4262
+ if (typeof thing !== "string" || !VIEW_THINGS.includes(thing)) {
4263
+ return {
4264
+ ok: false,
4265
+ error: `invalid thing (expected one of: ${VIEW_THINGS.join(", ")})`
4266
+ };
4267
+ }
4268
+ if (typeof brainId !== "string" || brainId.trim().length === 0) {
4269
+ return { ok: false, error: "missing brainId" };
4270
+ }
4271
+ const result = await BaseChannel.view(brainId.trim(), thing);
4272
+ if (!result.ok)
4273
+ return result;
4274
+ return {
4275
+ ok: true,
4276
+ result: {
4277
+ thing,
4278
+ brainId: brainId.trim(),
4279
+ displayName: result.displayName,
4280
+ value: result.value
4281
+ }
4282
+ };
4283
+ }
4284
+ });
4285
+
3937
4286
  // src/commands/daemon.ts
3938
4287
  async function startChannels() {
3939
4288
  const items = await brainManager.listAvailableBrain();
@@ -3968,7 +4317,10 @@ async function startChannels() {
3968
4317
  return started;
3969
4318
  }
3970
4319
  async function daemon() {
3971
- logger.debug(`daemon: boot`);
4320
+ const logDir = join5(config.brainboxRoot, "logs");
4321
+ logger.configure({ logDir });
4322
+ configureLlmLog(config.debug ? join5(logDir, "llm") : undefined);
4323
+ logger.debug(`daemon: boot (debug=${config.debug}, logDir=${logDir}, llmLog=${config.debug})`);
3972
4324
  const started = await startChannels();
3973
4325
  if (started === 0) {
3974
4326
  logger.info("No activated brains with channels. Daemon idling.");
@@ -4108,7 +4460,8 @@ async function deactivateBrain(brainId) {
4108
4460
  async function createBrain(displayName, seed, options) {
4109
4461
  logger.debug(`createBrain: name="${displayName}" seed length=${seed.length} schedule=${options.schedule}`);
4110
4462
  const result = await Brain.create(displayName, seed);
4111
- if (!result) {
4463
+ if ("error" in result) {
4464
+ logger.error(`Failed to create brain "${displayName}": ${result.error}`);
4112
4465
  process.exitCode = 1;
4113
4466
  return;
4114
4467
  }
@@ -4134,6 +4487,33 @@ async function removeBrain(brainId) {
4134
4487
  }
4135
4488
  logger.success(`Removed brain "${brain.displayName}" (${chalk2.cyan(brainId)})`);
4136
4489
  }
4490
+ async function doAction(action, brainId) {
4491
+ if (!DO_ACTIONS.includes(action)) {
4492
+ logger.error(`Unknown action "${action}". Expected one of: ${DO_ACTIONS.join(", ")}`);
4493
+ process.exit(1);
4494
+ }
4495
+ logger.debug(`do: action=${action} brainId=${brainId}`);
4496
+ const response = await sendToDaemon({
4497
+ command: "do",
4498
+ args: { action, brainId }
4499
+ });
4500
+ const name = response.result?.displayName ?? brainId;
4501
+ logger.success(`Successfully sent ${action} for "${name}" (${brainId}).`);
4502
+ }
4503
+ async function viewThing(thing, brainId) {
4504
+ if (!VIEW_THINGS.includes(thing)) {
4505
+ logger.error(`Unknown thing "${thing}". Expected one of: ${VIEW_THINGS.join(", ")}`);
4506
+ process.exit(1);
4507
+ }
4508
+ logger.debug(`view: thing=${thing} brainId=${brainId}`);
4509
+ const response = await sendToDaemon({
4510
+ command: "view",
4511
+ args: { thing, brainId }
4512
+ });
4513
+ const name = response.result?.displayName ?? brainId;
4514
+ logger.info(`${thing} — "${name}" (${brainId})`);
4515
+ console.log(JSON.stringify(response.result?.value ?? null, null, 2));
4516
+ }
4137
4517
  function register3(program) {
4138
4518
  const cmd = registerCommand(program, {
4139
4519
  name: "brain",
@@ -4144,6 +4524,8 @@ function register3(program) {
4144
4524
  cmd.command("remove <brainId>").description("Remove a brain and its memory").action(removeBrain);
4145
4525
  cmd.command("activate <brainId>").description("Activate a brain").action(activateBrain);
4146
4526
  cmd.command("deactivate <brainId>").description("Deactivate a brain").action(deactivateBrain);
4527
+ cmd.command("do <action> <brainId>").description(`Force-run a daemon job (${DO_ACTIONS.join(" | ")}) for a live brain`).action(doAction);
4528
+ cmd.command("view <thing> <brainId>").description(`Inspect a live brain value (${VIEW_THINGS.join(" | ")})`).action(viewThing);
4147
4529
  return cmd;
4148
4530
  }
4149
4531
 
@@ -4265,6 +4647,9 @@ function RawInput({
4265
4647
  onSubmit
4266
4648
  }) {
4267
4649
  const [value, setValue] = useState(initialValue);
4650
+ useEffect(() => {
4651
+ setValue(initialValue);
4652
+ }, [prompt, initialValue]);
4268
4653
  useInput((input, key) => {
4269
4654
  if (key.return) {
4270
4655
  onSubmit(value);
@@ -4960,6 +5345,19 @@ function Select(props) {
4960
5345
 
4961
5346
  // src/commands/onboard.tsx
4962
5347
  import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime";
5348
+ function ok(msg) {
5349
+ console.log(chalk3.green(`✔ ${msg}`));
5350
+ }
5351
+ function info(msg) {
5352
+ console.log(msg);
5353
+ }
5354
+ function show(active, node, status) {
5355
+ active.current.unmount();
5356
+ console.clear();
5357
+ if (status)
5358
+ ok(status);
5359
+ active.current = render3(node);
5360
+ }
4963
5361
  function ProviderApp({
4964
5362
  providers,
4965
5363
  onDone
@@ -5031,7 +5429,6 @@ function ProviderApp({
5031
5429
  const extras = PROVIDER_EXTRA_FIELDS[stage.provider] ?? [];
5032
5430
  if (extras.length === 0) {
5033
5431
  setProviderAuth(stage.provider, { apiKey });
5034
- logger.success(`Saved ${stage.provider} to auth.yaml`);
5035
5432
  onDone({ provider: stage.provider });
5036
5433
  return;
5037
5434
  }
@@ -5052,13 +5449,6 @@ function ProviderApp({
5052
5449
  }, undefined, true, undefined, this);
5053
5450
  }
5054
5451
  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
5452
  return /* @__PURE__ */ jsxDEV5(Box4, {
5063
5453
  flexDirection: "column",
5064
5454
  children: [
@@ -5084,6 +5474,11 @@ function ProviderApp({
5084
5474
  const values = { ...stage.values };
5085
5475
  if (value)
5086
5476
  values[nextField] = value;
5477
+ if (remaining.length === 0) {
5478
+ setProviderAuth(stage.provider, values);
5479
+ onDone({ provider: stage.provider });
5480
+ return;
5481
+ }
5087
5482
  setStage({
5088
5483
  kind: "extras",
5089
5484
  provider: stage.provider,
@@ -5112,7 +5507,7 @@ function ModelApp2({
5112
5507
  /* @__PURE__ */ jsxDEV5(Text5, {
5113
5508
  dimColor: true,
5114
5509
  children: [
5115
- "e.g. ",
5510
+ "model name, or ",
5116
5511
  /* @__PURE__ */ jsxDEV5(Text5, {
5117
5512
  color: "cyan",
5118
5513
  children: [
@@ -5120,7 +5515,7 @@ function ModelApp2({
5120
5515
  "/"
5121
5516
  ]
5122
5517
  }, undefined, true, undefined, this),
5123
- "model-name — fine-tune later with ",
5518
+ "model — fine-tune later with ",
5124
5519
  /* @__PURE__ */ jsxDEV5(Text5, {
5125
5520
  color: "cyan",
5126
5521
  children: "brainbox model"
@@ -5135,14 +5530,15 @@ function ModelApp2({
5135
5530
  setError("Model cannot be empty");
5136
5531
  return;
5137
5532
  }
5138
- if (!value.startsWith(`${provider}/`)) {
5139
- setError(`Must start with "${provider}/"`);
5533
+ const prefix = `${provider}/`;
5534
+ const full = value.startsWith(prefix) ? value : `${prefix}${value}`;
5535
+ if (full === prefix) {
5536
+ setError("Model cannot be empty");
5140
5537
  return;
5141
5538
  }
5142
- setModelSlot("identity", value);
5143
- setModelSlot("conversation", value);
5144
- logger.success(`Set identity + conversation model to ${value}`);
5145
- onDone();
5539
+ setModelSlot("identity", full);
5540
+ setModelSlot("conversation", full);
5541
+ onDone(full);
5146
5542
  }
5147
5543
  }, undefined, false, undefined, this),
5148
5544
  error && /* @__PURE__ */ jsxDEV5(Text5, {
@@ -5178,7 +5574,6 @@ function SuperMemoryApp({
5178
5574
  return;
5179
5575
  }
5180
5576
  setSupermemoryKey(key);
5181
- logger.success("Saved supermemory key to brainbox.yaml");
5182
5577
  onDone();
5183
5578
  }
5184
5579
  }, undefined, false, undefined, this),
@@ -5194,6 +5589,7 @@ function BrainApp({
5194
5589
  }) {
5195
5590
  const [stage, setStage] = useState5({ kind: "name" });
5196
5591
  const [error, setError] = useState5(null);
5592
+ const [busy, setBusy] = useState5(false);
5197
5593
  if (stage.kind === "name") {
5198
5594
  return /* @__PURE__ */ jsxDEV5(Box4, {
5199
5595
  flexDirection: "column",
@@ -5245,12 +5641,14 @@ function BrainApp({
5245
5641
  dimColor: true,
5246
5642
  children: "One sentence about who they are. The model will expand it."
5247
5643
  }, undefined, false, undefined, this),
5248
- /* @__PURE__ */ jsxDEV5(TextInput, {
5644
+ busy ? /* @__PURE__ */ jsxDEV5(Text5, {
5645
+ dimColor: true,
5646
+ children: "Creating brain… (This can take few minutes depending on the model's response speed)"
5647
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV5(TextInput, {
5249
5648
  prompt: "seed> ",
5250
- onSubmit: async (raw) => {
5649
+ onSubmit: (raw) => {
5251
5650
  const seed = raw.trim();
5252
5651
  if (seed === "skip") {
5253
- logger.info("Skipped brain creation.");
5254
5652
  onDone({ brainId: "", displayName: stage.displayName });
5255
5653
  return;
5256
5654
  }
@@ -5258,13 +5656,19 @@ function BrainApp({
5258
5656
  setError("Seed cannot be empty (or type 'skip')");
5259
5657
  return;
5260
5658
  }
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 });
5659
+ setBusy(true);
5660
+ setError(null);
5661
+ Brain.create(stage.displayName, seed).then((result) => {
5662
+ setBusy(false);
5663
+ if ("error" in result) {
5664
+ setError(`Brain creation failed: ${result.error} (fix seed, or type 'skip')`);
5665
+ return;
5666
+ }
5667
+ onDone({
5668
+ brainId: result.brainId,
5669
+ displayName: stage.displayName
5670
+ });
5671
+ });
5268
5672
  }
5269
5673
  }, undefined, false, undefined, this),
5270
5674
  error && /* @__PURE__ */ jsxDEV5(Text5, {
@@ -5305,8 +5709,7 @@ function ChannelApp({
5305
5709
  items: ["discord", "telegram", "skip"],
5306
5710
  onSelect: (v) => {
5307
5711
  if (v === "skip") {
5308
- logger.info("Skipped channel setup.");
5309
- onDone();
5712
+ onDone("Skipped channel setup.");
5310
5713
  return;
5311
5714
  }
5312
5715
  setError(null);
@@ -5365,37 +5768,41 @@ function ChannelApp({
5365
5768
  }, undefined, true, undefined, this),
5366
5769
  /* @__PURE__ */ jsxDEV5(TextInput, {
5367
5770
  prompt: `${stage.kind_ === "discord" ? "channelId" : "chatId"}> `,
5368
- onSubmit: async (raw) => {
5771
+ onSubmit: (raw) => {
5369
5772
  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");
5773
+ (async () => {
5774
+ const existing = await brainManager.loadBrain(brainId);
5775
+ if (!existing) {
5776
+ setError(`Brain ${brainId} no longer exists`);
5387
5777
  return;
5388
5778
  }
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();
5779
+ let updated;
5780
+ if (stage.kind_ === "discord") {
5781
+ updated = {
5782
+ ...existing,
5783
+ channel: "discord",
5784
+ discord: {
5785
+ token: stage.token,
5786
+ channelId: target || undefined
5787
+ },
5788
+ activated: true
5789
+ };
5790
+ } else {
5791
+ const chatId = target ? Number(target) : undefined;
5792
+ if (target && Number.isNaN(chatId)) {
5793
+ setError("chatId must be a number");
5794
+ return;
5795
+ }
5796
+ updated = {
5797
+ ...existing,
5798
+ channel: "telegram",
5799
+ telegram: { token: stage.token, chatId },
5800
+ activated: true
5801
+ };
5802
+ }
5803
+ await brainManager.saveBrain(brainId, updated);
5804
+ onDone(`Bound ${displayName} → ${stage.kind_}${target ? ` (${target})` : " (pairing mode)"}`);
5805
+ })();
5399
5806
  }
5400
5807
  }, undefined, false, undefined, this),
5401
5808
  error && /* @__PURE__ */ jsxDEV5(Text5, {
@@ -5406,46 +5813,52 @@ function ChannelApp({
5406
5813
  }, undefined, true, undefined, this);
5407
5814
  }
5408
5815
  async function runOnboard() {
5409
- logger.info(`Welcome — let's get ${chalk3.bold("brainbox")} ready.`);
5816
+ console.clear();
5817
+ info(`Welcome — let's get ${chalk3.bold("brainbox")} ready.`);
5410
5818
  const providers = listProviderNames().slice().sort();
5411
5819
  const { promise, resolve: resolve2 } = Promise.withResolvers();
5412
- let active = render3(/* @__PURE__ */ jsxDEV5(ProviderApp, {
5820
+ const active = { current: null };
5821
+ active.current = render3(/* @__PURE__ */ jsxDEV5(ProviderApp, {
5413
5822
  providers,
5414
5823
  onDone: (p) => {
5415
- active.unmount();
5416
- active = render3(/* @__PURE__ */ jsxDEV5(ModelApp2, {
5824
+ show(active, /* @__PURE__ */ jsxDEV5(ModelApp2, {
5417
5825
  provider: p.provider,
5418
- onDone: () => {
5419
- active.unmount();
5420
- active = render3(/* @__PURE__ */ jsxDEV5(SuperMemoryApp, {
5826
+ onDone: (model) => {
5827
+ show(active, /* @__PURE__ */ jsxDEV5(SuperMemoryApp, {
5421
5828
  onDone: () => {
5422
- active.unmount();
5423
- active = render3(/* @__PURE__ */ jsxDEV5(BrainApp, {
5829
+ show(active, /* @__PURE__ */ jsxDEV5(BrainApp, {
5424
5830
  onDone: (b) => {
5425
- active.unmount();
5426
5831
  if (!b.brainId) {
5832
+ active.current.unmount();
5833
+ console.clear();
5834
+ info("Skipped brain creation.");
5427
5835
  resolve2();
5428
5836
  return;
5429
5837
  }
5430
- active = render3(/* @__PURE__ */ jsxDEV5(ChannelApp, {
5838
+ show(active, /* @__PURE__ */ jsxDEV5(ChannelApp, {
5431
5839
  brainId: b.brainId,
5432
5840
  displayName: b.displayName,
5433
- onDone: () => {
5434
- active.unmount();
5841
+ onDone: (status) => {
5842
+ active.current.unmount();
5843
+ console.clear();
5844
+ if (status.startsWith("Skipped"))
5845
+ info(status);
5846
+ else
5847
+ ok(status);
5435
5848
  resolve2();
5436
5849
  }
5437
- }, undefined, false, undefined, this));
5850
+ }, undefined, false, undefined, this), `Created brain "${b.displayName}" (${chalk3.cyan(b.brainId)})`);
5438
5851
  }
5439
- }, undefined, false, undefined, this));
5852
+ }, undefined, false, undefined, this), "Saved supermemory key to brainbox.yaml");
5440
5853
  }
5441
- }, undefined, false, undefined, this));
5854
+ }, undefined, false, undefined, this), `Set identity + conversation model to ${model}`);
5442
5855
  }
5443
- }, undefined, false, undefined, this));
5856
+ }, undefined, false, undefined, this), `Saved ${p.provider} to auth.yaml`);
5444
5857
  }
5445
5858
  }, undefined, false, undefined, this));
5446
5859
  await promise;
5447
- logger.success("Onboarding complete.");
5448
- logger.info(`Run ${chalk3.cyan("brainbox daemon")} to bring it online.`);
5860
+ ok("Onboarding complete.");
5861
+ info(`Run ${chalk3.cyan("brainbox daemon")} to bring it online.`);
5449
5862
  }
5450
5863
  function register8(program) {
5451
5864
  return registerCommand(program, {
@@ -5457,12 +5870,12 @@ function register8(program) {
5457
5870
 
5458
5871
  // src/index.ts
5459
5872
  var __filename3 = fileURLToPath2(import.meta.url);
5460
- var __dirname3 = dirname4(__filename3);
5873
+ var __dirname3 = dirname3(__filename3);
5461
5874
  logger.configure({ level: config.debug ? "debug" : "info" });
5462
5875
  logger.debug(`brainbox starting (debug=${config.debug}, root=${config.brainboxRoot})`);
5463
5876
  function getVersion() {
5464
5877
  try {
5465
- const pkgPath = join4(__dirname3, "..", "package.json");
5878
+ const pkgPath = join6(__dirname3, "..", "package.json");
5466
5879
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
5467
5880
  return pkg.version ?? "0.0.0";
5468
5881
  } catch {