@bgicli/bgicli 2.2.15 → 2.3.0

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/bgi.js +161 -29
  2. package/package.json +1 -1
package/dist/bgi.js CHANGED
@@ -14218,7 +14218,7 @@ ${shortSummary}`);
14218
14218
  }
14219
14219
 
14220
14220
  // src/chat.ts
14221
- async function chat(messages, config, systemPrompt2) {
14221
+ async function chat(messages, config, systemPrompt2, stats, signal) {
14222
14222
  const prov = PROVIDERS[config.provider];
14223
14223
  if (!prov) throw new Error(`Unknown provider: ${config.provider}`);
14224
14224
  if (config.provider === "custom") {
@@ -14235,13 +14235,28 @@ async function chat(messages, config, systemPrompt2) {
14235
14235
  { role: "system", content: systemPrompt2 },
14236
14236
  ...messages
14237
14237
  ];
14238
- return await streamLoop(client, fullMessages, config.model);
14238
+ return await streamLoop(client, fullMessages, config.model, stats, signal);
14239
14239
  }
14240
- async function streamLoop(client, messages, model) {
14240
+ async function streamLoop(client, messages, model, stats, signal) {
14241
14241
  let finalText = "";
14242
14242
  for (let round = 0; round < 20; round++) {
14243
+ if (signal?.aborted) break;
14243
14244
  messages = deduplicateSkillInjections(trimToolOutputs(messages));
14244
- const { text, toolCalls, finishReason } = await streamOnce(client, messages, model);
14245
+ let streamResult;
14246
+ try {
14247
+ streamResult = await streamOnce(client, messages, model, signal);
14248
+ } catch (err) {
14249
+ if (signal?.aborted || err instanceof Error && (err.name === "AbortError" || err.message.includes("abort"))) {
14250
+ process.stdout.write(source_default.yellow("\n [\u4EFB\u52A1\u5DF2\u4E2D\u65AD]\n"));
14251
+ break;
14252
+ }
14253
+ throw err;
14254
+ }
14255
+ const { text, toolCalls, finishReason, inputTokens, outputTokens } = streamResult;
14256
+ if (stats) {
14257
+ stats.inputTokens += inputTokens;
14258
+ stats.outputTokens += outputTokens;
14259
+ }
14245
14260
  if (text) finalText = text;
14246
14261
  if (finishReason === "tool_calls" && toolCalls.length > 0) {
14247
14262
  messages.push({
@@ -14261,15 +14276,32 @@ async function streamLoop(client, messages, model) {
14261
14276
  const label = source_default.dim(`[\u5DE5\u5177: ${tc.name}(${summarizeArgs(args)})]`);
14262
14277
  const t0 = Date.now();
14263
14278
  process.stdout.write(`
14264
- ${label}
14265
- `);
14279
+ ${label} `);
14266
14280
  let streamedLines = 0;
14267
14281
  let lastLineWasEmpty = false;
14282
+ let spinnerCleared = false;
14268
14283
  const MAX_STREAM_LINES = 200;
14269
- let spin = null;
14270
14284
  let frame = 0;
14285
+ const spin = setInterval(() => {
14286
+ if (spinnerCleared) return;
14287
+ const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14288
+ process.stdout.write(
14289
+ `\r${label} ${source_default.cyan(SPIN_FRAMES[frame++ % SPIN_FRAMES.length])} ${source_default.dim(secs + "s")}`
14290
+ );
14291
+ }, 80);
14292
+ const clearSpinner = () => {
14293
+ if (spinnerCleared) return;
14294
+ spinnerCleared = true;
14295
+ clearInterval(spin);
14296
+ process.stdout.write("\r\x1B[2K");
14297
+ };
14271
14298
  const onStream = isBash ? (chunk) => {
14272
14299
  if (streamedLines >= MAX_STREAM_LINES) return;
14300
+ if (streamedLines === 0) {
14301
+ clearSpinner();
14302
+ process.stdout.write(`${label}
14303
+ `);
14304
+ }
14273
14305
  const lines = chunk.split("\n");
14274
14306
  for (let i2 = 0; i2 < lines.length; i2++) {
14275
14307
  const line = lines[i2];
@@ -14289,19 +14321,12 @@ ${label}
14289
14321
  }
14290
14322
  }
14291
14323
  } : void 0;
14292
- if (!isBash) {
14293
- spin = setInterval(() => {
14294
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14295
- process.stdout.write(
14296
- `\r ${source_default.cyan(SPIN_FRAMES[frame++ % SPIN_FRAMES.length])} ${source_default.dim(secs + "s")}`
14297
- );
14298
- }, 80);
14299
- }
14300
14324
  const result = await executeTool(tc.name, args, onStream);
14301
- if (spin) {
14302
- clearInterval(spin);
14303
- process.stdout.write("\r\x1B[2K");
14325
+ if (stats) {
14326
+ if (result.error) stats.failCmds++;
14327
+ else stats.successCmds++;
14304
14328
  }
14329
+ clearSpinner();
14305
14330
  const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
14306
14331
  const doneIcon = result.error ? source_default.yellow("\u2717") : source_default.green("\u2713");
14307
14332
  if (isBash && streamedLines > 0) {
@@ -14333,18 +14358,25 @@ OUTPUT: ${result.output}` : result.output
14333
14358
  }
14334
14359
  return finalText;
14335
14360
  }
14336
- async function streamOnce(client, messages, model) {
14361
+ async function streamOnce(client, messages, model, signal) {
14337
14362
  const stream = await client.chat.completions.create({
14338
14363
  model,
14339
14364
  messages,
14340
14365
  tools: TOOL_DEFINITIONS,
14341
- stream: true
14342
- });
14366
+ stream: true,
14367
+ stream_options: { include_usage: true }
14368
+ }, { signal });
14343
14369
  let text = "";
14344
14370
  const toolCallMap = {};
14345
14371
  let finishReason = null;
14372
+ let inputTokens = 0;
14373
+ let outputTokens = 0;
14346
14374
  process.stdout.write(source_default.green("BGI \u203A "));
14347
14375
  for await (const chunk of stream) {
14376
+ if (chunk.usage) {
14377
+ inputTokens = chunk.usage.prompt_tokens ?? 0;
14378
+ outputTokens = chunk.usage.completion_tokens ?? 0;
14379
+ }
14348
14380
  const choice = chunk.choices[0];
14349
14381
  if (!choice) continue;
14350
14382
  const delta = choice.delta;
@@ -14375,7 +14407,9 @@ async function streamOnce(client, messages, model) {
14375
14407
  return {
14376
14408
  text,
14377
14409
  toolCalls: Object.values(toolCallMap),
14378
- finishReason
14410
+ finishReason,
14411
+ inputTokens,
14412
+ outputTokens
14379
14413
  };
14380
14414
  }
14381
14415
  function estimateTokens(messages) {
@@ -15876,7 +15910,7 @@ function clearCheckpoints(sessionId) {
15876
15910
 
15877
15911
  // src/index.ts
15878
15912
  var import_fs7 = require("fs");
15879
- var VERSION2 = "2.2.15";
15913
+ var VERSION2 = "2.3.0";
15880
15914
  var SKILLHUB_HUBS = {
15881
15915
  bgi: { label: "BGI\u5185\u7F51", apiBase: "https://clawhub.ai", backend: "clawhub" },
15882
15916
  clawhub: { label: "clawhub.ai", apiBase: "https://clawhub.ai", backend: "clawhub" },
@@ -17520,10 +17554,8 @@ async function main() {
17520
17554
  historySize: 100
17521
17555
  });
17522
17556
  rl.on("close", () => {
17523
- console.log(source_default.dim("\n\u518D\u89C1\uFF01"));
17524
17557
  process.exit(0);
17525
17558
  });
17526
- process.on("SIGINT", () => rl.close());
17527
17559
  await firstRunIfNeeded(rl);
17528
17560
  const cfg = loadConfig();
17529
17561
  const prov = PROVIDERS[cfg.provider];
@@ -17548,6 +17580,20 @@ async function main() {
17548
17580
  console.log(source_default.dim(" \u8F93\u5165\u95EE\u9898\u5F00\u59CB\u5BF9\u8BDD /help \u67E5\u770B\u547D\u4EE4 /cat \u6280\u80FD\u5206\u7C7B @\u6587\u4EF6\u8DEF\u5F84 \u5185\u5D4C\u6587\u4EF6"));
17549
17581
  console.log();
17550
17582
  let dbRegistry2 = loadDbRegistry();
17583
+ if (Object.keys(dbRegistry2.databases).length === 0) {
17584
+ process.stdout.write(source_default.dim(" \u6B63\u5728\u81EA\u52A8\u626B\u63CF\u53C2\u8003\u6570\u636E\u5E93...\n"));
17585
+ const report = scanForDatabases([]);
17586
+ if (report.found.length > 0) {
17587
+ for (const entry of report.found) dbRegistry2.databases[entry.id] = entry;
17588
+ dbRegistry2.lastScan = (/* @__PURE__ */ new Date()).toISOString();
17589
+ saveDbRegistry(dbRegistry2);
17590
+ process.stdout.write(source_default.green(` \u2713 \u53D1\u73B0 ${report.found.length} \u4E2A\u6570\u636E\u5E93\uFF0C\u5DF2\u81EA\u52A8\u6CE8\u518C (/db list \u67E5\u770B)
17591
+ `));
17592
+ } else {
17593
+ process.stdout.write(source_default.dim(" \u672A\u53D1\u73B0\u5DF2\u77E5\u6570\u636E\u5E93\uFF08\u53EF\u7528 /db add <\u8DEF\u5F84> \u624B\u52A8\u6DFB\u52A0\uFF09\n"));
17594
+ }
17595
+ console.log();
17596
+ }
17551
17597
  let systemPrompt2 = buildSystemPrompt(buildDbPromptSection(dbRegistry2));
17552
17598
  let history = [];
17553
17599
  let thinkMode = false;
@@ -17567,6 +17613,79 @@ async function main() {
17567
17613
  }
17568
17614
  let lastCheckpointMsgCount = 0;
17569
17615
  const CHECKPOINT_INTERVAL = 6;
17616
+ const sessionStartTime = Date.now();
17617
+ const sessionStats = { inputTokens: 0, outputTokens: 0, successCmds: 0, failCmds: 0 };
17618
+ let lastSigintTime = 0;
17619
+ let currentAbortController = null;
17620
+ let exiting = false;
17621
+ function formatDuration(ms) {
17622
+ const s2 = Math.floor(ms / 1e3);
17623
+ const h2 = Math.floor(s2 / 3600);
17624
+ const m2 = Math.floor(s2 % 3600 / 60);
17625
+ const sec = s2 % 60;
17626
+ if (h2 > 0) return `${h2}h ${m2}m ${sec}s`;
17627
+ if (m2 > 0) return `${m2}m ${sec}s`;
17628
+ return `${sec}s`;
17629
+ }
17630
+ function saveSessionMemory() {
17631
+ const memDir = (0, import_path6.join)(BGI_DIR, "memory");
17632
+ if (!(0, import_fs6.existsSync)(memDir)) (0, import_fs6.mkdirSync)(memDir, { recursive: true });
17633
+ const dateStr = new Date(sessionStartTime).toISOString().slice(0, 10);
17634
+ const filename = (0, import_path6.join)(memDir, `${dateStr}-${sessionId}.md`);
17635
+ const lines = [
17636
+ `# \u4F1A\u8BDD\u8BB0\u5FC6 ${new Date(sessionStartTime).toLocaleString("zh-CN")}`,
17637
+ ``,
17638
+ `- \u6A21\u578B: ${loadConfig().model}`,
17639
+ `- Token: \u8F93\u5165 ${sessionStats.inputTokens} / \u8F93\u51FA ${sessionStats.outputTokens}`,
17640
+ `- \u547D\u4EE4: ${sessionStats.successCmds} \u6210\u529F / ${sessionStats.failCmds} \u5931\u8D25`,
17641
+ ``
17642
+ ];
17643
+ let turnCount = 0;
17644
+ for (let i2 = 0; i2 < history.length; i2++) {
17645
+ const msg = history[i2];
17646
+ if (msg.role === "user" && typeof msg.content === "string") {
17647
+ turnCount++;
17648
+ const content = msg.content.slice(0, 500) + (msg.content.length > 500 ? "..." : "");
17649
+ lines.push(`## \u5BF9\u8BDD ${turnCount}`);
17650
+ lines.push(`**\u7528\u6237**: ${content}`);
17651
+ } else if (msg.role === "assistant" && typeof msg.content === "string") {
17652
+ const content = msg.content.slice(0, 500) + (msg.content.length > 500 ? "..." : "");
17653
+ lines.push(`**BGI**: ${content}`);
17654
+ lines.push(``);
17655
+ }
17656
+ }
17657
+ (0, import_fs6.writeFileSync)(filename, lines.join("\n"), "utf8");
17658
+ console.log(source_default.green(` \u2713 \u8BB0\u5FC6\u5DF2\u4FDD\u5B58: ${filename}`));
17659
+ }
17660
+ async function exitWithReport() {
17661
+ if (exiting) return;
17662
+ exiting = true;
17663
+ const runtime = formatDuration(Date.now() - sessionStartTime);
17664
+ console.log("\n" + source_default.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 \u4F1A\u8BDD\u62A5\u544A \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
17665
+ console.log(` \u8FD0\u884C\u65F6\u95F4: ${source_default.white(runtime)}`);
17666
+ console.log(` \u6D88\u8017 Token: ${source_default.yellow("\u8F93\u5165")} ${source_default.bold(String(sessionStats.inputTokens))} | ${source_default.green("\u8F93\u51FA")} ${source_default.bold(String(sessionStats.outputTokens))}`);
17667
+ console.log(` \u6267\u884C\u547D\u4EE4: ${source_default.green("\u2713 " + sessionStats.successCmds + " \u6210\u529F")} ${sessionStats.failCmds > 0 ? source_default.yellow("\u2717 " + sessionStats.failCmds + " \u5931\u8D25") : source_default.dim("\u2717 0 \u5931\u8D25")}`);
17668
+ console.log(source_default.bold.cyan("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
17669
+ try {
17670
+ const ans = await question(rl, source_default.cyan("\n \u662F\u5426\u4FDD\u5B58\u672C\u6B21\u4F1A\u8BDD\u8BB0\u5FC6\uFF1F[y/N] "));
17671
+ if (ans.trim().toLowerCase() === "y") saveSessionMemory();
17672
+ } catch {
17673
+ }
17674
+ console.log(source_default.dim("\n\u518D\u89C1\uFF01"));
17675
+ process.exit(0);
17676
+ }
17677
+ process.on("SIGINT", () => {
17678
+ const now = Date.now();
17679
+ if (now - lastSigintTime < 2e3) {
17680
+ exitWithReport().catch(() => process.exit(0));
17681
+ } else {
17682
+ lastSigintTime = now;
17683
+ if (currentAbortController) {
17684
+ currentAbortController.abort();
17685
+ }
17686
+ process.stdout.write(source_default.yellow("\n\n [\u4EFB\u52A1\u5DF2\u4E2D\u65AD] \u518D\u6309\u4E00\u6B21 Ctrl+C \u9000\u51FA\n\n"));
17687
+ }
17688
+ });
17570
17689
  while (true) {
17571
17690
  let input;
17572
17691
  const thinkIndicator = thinkMode ? source_default.yellow("[\u601D\u8003]") + " " : "";
@@ -17578,9 +17697,8 @@ async function main() {
17578
17697
  const trimmed = input.trim();
17579
17698
  if (!trimmed) continue;
17580
17699
  if (["exit", "quit", "q", "/exit", "/quit"].includes(trimmed.toLowerCase())) {
17581
- console.log(source_default.dim("\u518D\u89C1\uFF01"));
17582
- rl.close();
17583
- break;
17700
+ await exitWithReport();
17701
+ return;
17584
17702
  }
17585
17703
  if (trimmed.startsWith("/")) {
17586
17704
  const result = await handleCommand(trimmed, rl, history, thinkMode, injectedSkills);
@@ -17636,7 +17754,14 @@ ${expanded}` : expanded;
17636
17754
  history.push({ role: "user", content: userContent });
17637
17755
  try {
17638
17756
  const currentCfg = loadConfig();
17639
- const reply = await chat(history, currentCfg, systemPrompt2);
17757
+ currentAbortController = new AbortController();
17758
+ const reply = await chat(history, currentCfg, systemPrompt2, sessionStats, currentAbortController.signal);
17759
+ currentAbortController = null;
17760
+ if (!reply && history[history.length - 1]?.role === "user") {
17761
+ history.pop();
17762
+ console.log();
17763
+ continue;
17764
+ }
17640
17765
  history.push({ role: "assistant", content: reply });
17641
17766
  history = await maybeCompact(history, currentCfg);
17642
17767
  autoSaveSession();
@@ -17651,7 +17776,13 @@ ${expanded}` : expanded;
17651
17776
  [\u6FC0\u6D3B Skill: ${ids}]`));
17652
17777
  }
17653
17778
  } catch (err) {
17779
+ currentAbortController = null;
17654
17780
  const msg = err instanceof Error ? err.message : String(err);
17781
+ if (err instanceof Error && (err.name === "AbortError" || msg.toLowerCase().includes("abort"))) {
17782
+ history.pop();
17783
+ console.log();
17784
+ continue;
17785
+ }
17655
17786
  console.error(source_default.red(`
17656
17787
  \u9519\u8BEF: ${msg}
17657
17788
  `));
@@ -17659,6 +17790,7 @@ ${expanded}` : expanded;
17659
17790
  }
17660
17791
  console.log();
17661
17792
  }
17793
+ await exitWithReport();
17662
17794
  }
17663
17795
  function question(rl, prompt) {
17664
17796
  return new Promise((resolve4, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bgicli/bgicli",
3
- "version": "2.2.15",
3
+ "version": "2.3.0",
4
4
  "description": "BGI CLI — Bioinformatics AI terminal for Chinese researchers (百炼/DeepSeek/Kimi/Qwen)",
5
5
  "bin": {
6
6
  "bgi": "dist/bgi.js"