@theowlops/boombot 1.1.1 → 1.1.3

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 (25) hide show
  1. package/dist/cli/cli/registry.js +14 -0
  2. package/dist/cli/commands/gateway.js +4 -4
  3. package/dist/cli/commands/update.js +21 -0
  4. package/dist/gateway/config.js +11 -3
  5. package/dist/gateway/handlers/system.js +54 -0
  6. package/dist/gateway/turns/session-adapter.js +4 -0
  7. package/dist/node_modules/@boombot/core/agent-core/turn-loop.js +75 -7
  8. package/dist/node_modules/@boombot/core/agent-core/types.d.ts +3 -0
  9. package/dist/node_modules/@boombot/protocol/gateway-protocol/commands.d.ts +225 -185
  10. package/dist/node_modules/@boombot/protocol/gateway-protocol/commands.js +22 -0
  11. package/dist/package.json +1 -1
  12. package/dist/web/assets/ChatView-CP16lYju.css +1 -0
  13. package/dist/web/assets/{ChatView-BZrSD-64.js → ChatView-DewkhV93.js} +15 -15
  14. package/dist/web/assets/{MemoryView-BfMq_6CD.js → MemoryView-DP25X8jn.js} +1 -1
  15. package/dist/web/assets/{SchedulesView-B22QmPR-.js → SchedulesView-DOeYo9tY.js} +1 -1
  16. package/dist/web/assets/{SessionsView-DS0-aH2a.js → SessionsView-Cg8olSle.js} +1 -1
  17. package/dist/web/assets/{TasksView-ey0FLsW8.js → TasksView-BWJym_ZX.js} +1 -1
  18. package/dist/web/assets/{Textarea-DN3Zybl1.js → Textarea-zDM3NMBL.js} +1 -1
  19. package/dist/web/assets/{Tooltip-MzV2Gqnc.js → Tooltip-Dzv4yBOV.js} +1 -1
  20. package/dist/web/assets/{browser-Bnt73-Ub.js → browser-BXTr8ura.js} +1 -1
  21. package/dist/web/assets/index-usu7RXES.js +73 -0
  22. package/dist/web/index.html +1 -1
  23. package/package.json +1 -1
  24. package/dist/web/assets/ChatView-Cahbjez6.css +0 -1
  25. package/dist/web/assets/index-CuPR73Tn.js +0 -73
@@ -10,6 +10,7 @@ import { runGateway } from "../commands/gateway.js";
10
10
  import { isSupportedMigrateSource, printMigrateReport, runMigrateFromOpenClaw, } from "../commands/migrate.js";
11
11
  import { runSetup } from "../commands/setup.js";
12
12
  import { runPluginList, runPluginInstall, runPluginUninstall } from "../commands/plugin.js";
13
+ import { runUpdate } from "../commands/update.js";
13
14
  import { CliError } from "./cli-error.js";
14
15
  async function setupHandler(_ctx) {
15
16
  await runSetup();
@@ -182,4 +183,17 @@ export const REGISTRY = [
182
183
  },
183
184
  handler: pluginUninstallHandler,
184
185
  },
186
+ {
187
+ spec: {
188
+ path: ['update'],
189
+ aliases: [['upgrade']],
190
+ summary: 'Cập nhật BoomBot lên phiên bản mới nhất từ npm',
191
+ usage: 'boombot update',
192
+ acceptsPositionals: false,
193
+ flags: [],
194
+ examples: ['boombot update', 'boombot upgrade'],
195
+ notes: ['Chạy `npm install -g @theowlops/boombot@latest` để kéo code mới.'],
196
+ },
197
+ handler: async () => runUpdate(),
198
+ },
185
199
  ];
@@ -97,11 +97,11 @@ function gatewayHostPort() {
97
97
  try {
98
98
  const config = loadConfig(resolveConfigPath());
99
99
  const host = process.env['BOOMBOT_HOST']?.trim() || config?.host || '127.0.0.1';
100
- const port = Number.parseInt(process.env['BOOMBOT_PORT']?.trim() || String(config?.port ?? 8787), 10);
101
- return { host, port: Number.isInteger(port) && port > 0 && port <= 65535 ? port : 8787 };
100
+ const port = Number.parseInt(process.env['BOOMBOT_PORT']?.trim() || String(config?.port ?? 8788), 10);
101
+ return { host, port: Number.isInteger(port) && port > 0 && port <= 65535 ? port : 8788 };
102
102
  }
103
103
  catch {
104
- return { host: '127.0.0.1', port: 8787 };
104
+ return { host: '127.0.0.1', port: 8788 };
105
105
  }
106
106
  }
107
107
  /** Public URL for the browser: 0.0.0.0/:: mean "this machine". Exported for tests. */
@@ -142,7 +142,7 @@ export function initializeGateway(configPath = resolveConfigPath()) {
142
142
  ensureMasterKey(masterKeyPath);
143
143
  }
144
144
  const { apiKeyHash } = ensureApiKey(resolveApiKeyPath(configPath));
145
- saveConfig({ host: '127.0.0.1', port: 8787, dataDir, apiKeyHash }, configPath, 'wx');
145
+ saveConfig({ host: '127.0.0.1', port: 8788, dataDir, apiKeyHash }, configPath, 'wx');
146
146
  return true;
147
147
  }
148
148
  /**
@@ -0,0 +1,21 @@
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execAsync = promisify(exec);
4
+ export async function runUpdate() {
5
+ console.log('🔄 Đang kiểm tra và cập nhật @theowlops/boombot từ npm...');
6
+ try {
7
+ const { stdout, stderr } = await execAsync('npm install -g @theowlops/boombot@latest');
8
+ if (stdout.trim()) {
9
+ console.log(stdout.trim());
10
+ }
11
+ if (stderr.trim()) {
12
+ console.error(stderr.trim());
13
+ }
14
+ console.log('✅ Cập nhật thành công! Hãy khởi động lại `boombot gateway start` để áp dụng bản mới.');
15
+ return 0;
16
+ }
17
+ catch (err) {
18
+ console.error('❌ Lỗi khi cập nhật:', err instanceof Error ? err.message : String(err));
19
+ return 1;
20
+ }
21
+ }
@@ -1,8 +1,9 @@
1
+ import { homedir } from 'node:os';
1
2
  import { join } from 'node:path';
2
3
  import { hashApiKey } from './auth.js';
3
4
  import { coerceChannelSection, defaultConfigDir, defaultConfigPath, readConfigObject, resolveConfigFilePath, } from './config-format.js';
4
5
  const DEFAULT_HOST = '127.0.0.1';
5
- const DEFAULT_PORT = 8787;
6
+ const DEFAULT_PORT = 8788;
6
7
  /**
7
8
  * Default config path (`~/.boombot/config.yml`). The real definition lives in
8
9
  * `config-format.ts`, next to the file probe order; re-exported here so old
@@ -397,8 +398,15 @@ export function loadConfig(env = process.env) {
397
398
  const fileHost = typeof file.host === 'string' && file.host.length > 0 ? file.host : undefined;
398
399
  const host = get('BOOMBOT_HOST') ?? fileHost ?? DEFAULT_HOST;
399
400
  const port = parsePort(get('BOOMBOT_PORT') ?? file.port);
400
- const fileDataDir = typeof file.dataDir === 'string' && file.dataDir.length > 0 ? file.dataDir : undefined;
401
- const dataDir = get('BOOMBOT_DATA_DIR') ?? fileDataDir ?? defaultDataDir(env);
401
+ const fileDataDir = typeof file.dataDir === 'string' && file.dataDir.length > 0
402
+ ? (file.dataDir.startsWith('~/') || file.dataDir.startsWith('~\\')
403
+ ? join(homedir(), file.dataDir.slice(2))
404
+ : file.dataDir)
405
+ : undefined;
406
+ const rawDataDir = get('BOOMBOT_DATA_DIR') ?? fileDataDir ?? defaultDataDir(env);
407
+ const dataDir = rawDataDir.startsWith('~/') || rawDataDir.startsWith('~\\')
408
+ ? join(homedir(), rawDataDir.slice(2))
409
+ : rawDataDir;
402
410
  const apiKeyHash = resolveApiKeyHash(file.apiKeyHash, get('BOOMBOT_API_KEY'));
403
411
  const channels = mergeChannelEnv(parseChannels(file.channels, configPath), get);
404
412
  const reasoningGuard = {
@@ -3,10 +3,64 @@
3
3
  *
4
4
  * Mirrors `GET /healthz` (which needs no auth): `{ ok, version, wireVersion }`.
5
5
  */
6
+ import { exec } from 'node:child_process';
7
+ import { promisify } from 'node:util';
6
8
  import { WIRE_VERSION } from '@boombot/protocol';
7
9
  import { GATEWAY_VERSION } from '../server.js';
10
+ const execAsync = promisify(exec);
8
11
  export function registerSystemHandlers(registry) {
9
12
  registry.register('gateway.health', async () => {
10
13
  return { ok: true, version: GATEWAY_VERSION, wireVersion: WIRE_VERSION };
11
14
  });
15
+ registry.register('system.version', async () => {
16
+ let latest;
17
+ try {
18
+ const res = await fetch('https://registry.npmjs.org/@theowlops/boombot/latest', {
19
+ signal: AbortSignal.timeout(4000),
20
+ });
21
+ if (res.ok) {
22
+ const data = (await res.json());
23
+ latest = data.version;
24
+ }
25
+ }
26
+ catch {
27
+ // offline / timeout
28
+ }
29
+ const hasUpdate = Boolean(latest && latest !== GATEWAY_VERSION);
30
+ return { current: GATEWAY_VERSION, latest, hasUpdate };
31
+ });
32
+ registry.register('system.update', async () => {
33
+ try {
34
+ const { stdout, stderr } = await execAsync('npm install -g @theowlops/boombot@latest', {
35
+ timeout: 120_000,
36
+ });
37
+ let latest;
38
+ try {
39
+ const res = await fetch('https://registry.npmjs.org/@theowlops/boombot/latest', {
40
+ signal: AbortSignal.timeout(4000),
41
+ });
42
+ if (res.ok) {
43
+ const data = (await res.json());
44
+ latest = data.version;
45
+ }
46
+ }
47
+ catch {
48
+ // ignore
49
+ }
50
+ return {
51
+ ok: true,
52
+ message: latest ? `Đã cập nhật lên v${latest}` : 'Đã kéo bản mới nhất từ npm thành công',
53
+ current: GATEWAY_VERSION,
54
+ latest,
55
+ output: (stdout || stderr || '').trim(),
56
+ };
57
+ }
58
+ catch (err) {
59
+ return {
60
+ ok: false,
61
+ message: err instanceof Error ? err.message : 'Lỗi cập nhật npm',
62
+ current: GATEWAY_VERSION,
63
+ };
64
+ }
65
+ });
12
66
  }
@@ -84,6 +84,7 @@ function toChatMessage(m) {
84
84
  ...(metadata?.toolCalls !== undefined ? { toolCalls: metadata.toolCalls } : {}),
85
85
  ...(metadata?.toolCallId !== undefined ? { toolCallId: metadata.toolCallId } : {}),
86
86
  ...(metadata?.toolName !== undefined ? { name: metadata.toolName } : {}),
87
+ ...(metadata?.reasoning !== undefined ? { reasoning: metadata.reasoning } : {}),
87
88
  };
88
89
  }
89
90
  /**
@@ -105,6 +106,9 @@ function toSessionMetadata(message) {
105
106
  if (message.name !== undefined && message.name !== '') {
106
107
  metadata.toolName = message.name;
107
108
  }
109
+ if (message.reasoning !== undefined && message.reasoning !== '') {
110
+ metadata.reasoning = message.reasoning;
111
+ }
108
112
  return Object.keys(metadata).length > 0 ? metadata : undefined;
109
113
  }
110
114
  function boundedContext(messages, alreadyOmitted = 0) {
@@ -174,6 +174,7 @@ async function modelRound(deps, messages, tools, emit, signal) {
174
174
  checkCancelled(signal);
175
175
  let text = '';
176
176
  let reasoning = '';
177
+ let inThinkTag = false;
177
178
  const usage = { inputTokens: 0, outputTokens: 0 };
178
179
  const accumulators = new Map();
179
180
  const order = [];
@@ -188,8 +189,43 @@ async function modelRound(deps, messages, tools, emit, signal) {
188
189
  }
189
190
  }
190
191
  if (chunk.delta) {
191
- text += chunk.delta;
192
- emit({ type: 'text.delta', text: chunk.delta });
192
+ let remaining = chunk.delta;
193
+ while (remaining.length > 0) {
194
+ if (inThinkTag) {
195
+ const closeIdx = remaining.indexOf('</think>');
196
+ if (closeIdx !== -1) {
197
+ const thinkPart = remaining.slice(0, closeIdx);
198
+ remaining = remaining.slice(closeIdx + '</think>'.length);
199
+ inThinkTag = false;
200
+ if (thinkPart) {
201
+ reasoning += thinkPart;
202
+ emit({ type: 'reasoning.delta', text: thinkPart });
203
+ }
204
+ }
205
+ else {
206
+ reasoning += remaining;
207
+ emit({ type: 'reasoning.delta', text: remaining });
208
+ remaining = '';
209
+ }
210
+ }
211
+ else {
212
+ const openIdx = remaining.indexOf('<think>');
213
+ if (openIdx !== -1) {
214
+ const textPart = remaining.slice(0, openIdx);
215
+ remaining = remaining.slice(openIdx + '<think>'.length);
216
+ inThinkTag = true;
217
+ if (textPart) {
218
+ text += textPart;
219
+ emit({ type: 'text.delta', text: textPart });
220
+ }
221
+ }
222
+ else {
223
+ text += remaining;
224
+ emit({ type: 'text.delta', text: remaining });
225
+ remaining = '';
226
+ }
227
+ }
228
+ }
193
229
  }
194
230
  if (chunk.reasoningDelta) {
195
231
  reasoning += chunk.reasoningDelta;
@@ -244,15 +280,15 @@ async function modelRound(deps, messages, tools, emit, signal) {
244
280
  arguments: parsed,
245
281
  });
246
282
  }
247
- // Fallback: If model emitted thinking/reasoning but no content tokens or tool calls,
248
- // salvage reasoning as response text instead of failing with empty response.
249
- const finalText = text.trim() ? text : (calls.length ? '' : reasoning.trim());
283
+ const finalText = text.trim();
284
+ const trimmedReasoning = reasoning.trim();
250
285
  const assistantMessage = {
251
286
  role: 'assistant',
252
287
  content: finalText,
288
+ ...(trimmedReasoning ? { reasoning: trimmedReasoning } : {}),
253
289
  ...(calls.length > 0 ? { toolCalls: calls } : {}),
254
290
  };
255
- return { text: finalText, toolCalls: calls, assistantMessage, usage };
291
+ return { text: finalText, reasoning: trimmedReasoning || undefined, toolCalls: calls, assistantMessage, usage };
256
292
  }
257
293
  async function executeToolBatch(deps, batch, emit, signal) {
258
294
  const results = [];
@@ -431,6 +467,7 @@ export async function runTurn(deps, context, onEvent, signal) {
431
467
  let emptyRecoveryPending = false;
432
468
  let emptyRecoveryUsed = false;
433
469
  let contextRetryUsed = false;
470
+ let lastRoundReasoning;
434
471
  while (rounds < maxRounds) {
435
472
  checkCancelled(signal);
436
473
  rounds++;
@@ -440,7 +477,10 @@ export async function runTurn(deps, context, onEvent, signal) {
440
477
  const requestMessages = () => {
441
478
  const prepared = [...modelMessages];
442
479
  if (recoverEmpty) {
443
- prepared.push({ role: 'user', content: `[Lưu ý hệ thống: ${EMPTY_RESPONSE_NUDGE}]` });
480
+ const nudge = lastRoundReasoning
481
+ ? 'Bạn đã hoàn thành phần suy luận nhưng chưa sinh nội dung câu trả lời cho người dùng. Hãy đưa ra câu trả lời trực tiếp và chi tiết cho người dùng ngay bây giờ bằng tiếng Việt, không lặp lại phần suy luận và không gọi công cụ.'
482
+ : EMPTY_RESPONSE_NUDGE;
483
+ prepared.push({ role: 'user', content: `[Lưu ý hệ thống: ${nudge}]` });
444
484
  }
445
485
  if (answerOnly) {
446
486
  prepared.push({
@@ -469,8 +509,36 @@ export async function runTurn(deps, context, onEvent, signal) {
469
509
  if (!emptyRecoveryUsed && rounds < maxRounds) {
470
510
  emptyRecoveryUsed = true;
471
511
  emptyRecoveryPending = true;
512
+ lastRoundReasoning = model.reasoning;
472
513
  continue;
473
514
  }
515
+ // Force one final synthesis round without tools if reasoning exists
516
+ if (model.reasoning && rounds < maxRounds) {
517
+ try {
518
+ const forceMessages = [
519
+ ...modelMessages,
520
+ {
521
+ role: 'user',
522
+ content: 'Hãy tổng hợp và đưa ra câu trả lời trực tiếp, đầy đủ cho người dùng ngay bây giờ dựa trên các suy luận ở trên bằng tiếng Việt. Không gọi thêm công cụ.',
523
+ },
524
+ ];
525
+ const finalModel = await execPhase('modelRound', () => modelRound(deps, forceMessages, [], emit, signal));
526
+ const finalText = finalModel.text.trim();
527
+ if (finalText) {
528
+ const finalMsg = {
529
+ role: 'assistant',
530
+ content: finalText,
531
+ ...(model.reasoning ? { reasoning: model.reasoning } : {}),
532
+ };
533
+ messages.push(finalMsg);
534
+ modelMessages.push(finalMsg);
535
+ break;
536
+ }
537
+ }
538
+ catch {
539
+ // fall through to error
540
+ }
541
+ }
474
542
  const result = await finish('error', 'model returned an empty response');
475
543
  emit({ type: 'turn.end', result });
476
544
  return result;
@@ -21,6 +21,8 @@ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
21
21
  export interface ChatMessage {
22
22
  readonly role: MessageRole;
23
23
  readonly content: string;
24
+ /** Optional reasoning/thinking output from reasoning models or thinking blocks. */
25
+ readonly reasoning?: string;
24
26
  /** Optional image references for multimodal user messages. */
25
27
  readonly images?: readonly {
26
28
  readonly url: string;
@@ -123,6 +125,7 @@ export type EventHandler = (event: AgentEvent) => void;
123
125
  /** Phase payloads consumed by gateway middleware after Python returns them. */
124
126
  export interface ModelRoundResult {
125
127
  readonly text: string;
128
+ readonly reasoning?: string;
126
129
  readonly toolCalls: readonly ToolCall[];
127
130
  readonly assistantMessage: ChatMessage;
128
131
  }