@bgicli/bgicli 2.2.7 → 2.2.9

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 (117) hide show
  1. package/data/skills/anthropic-algorithmic-art/SKILL.md +405 -0
  2. package/data/skills/anthropic-canvas-design/SKILL.md +130 -0
  3. package/data/skills/anthropic-claude-api/SKILL.md +243 -0
  4. package/data/skills/anthropic-doc-coauthoring/SKILL.md +375 -0
  5. package/data/skills/anthropic-docx/SKILL.md +590 -0
  6. package/data/skills/anthropic-frontend-design/SKILL.md +42 -0
  7. package/data/skills/anthropic-internal-comms/SKILL.md +32 -0
  8. package/data/skills/anthropic-mcp-builder/SKILL.md +236 -0
  9. package/data/skills/anthropic-pdf/SKILL.md +314 -0
  10. package/data/skills/anthropic-pptx/SKILL.md +232 -0
  11. package/data/skills/anthropic-skill-creator/SKILL.md +485 -0
  12. package/data/skills/anthropic-webapp-testing/SKILL.md +96 -0
  13. package/data/skills/anthropic-xlsx/SKILL.md +292 -0
  14. package/data/skills/arxiv-database/SKILL.md +362 -0
  15. package/data/skills/astropy/SKILL.md +329 -0
  16. package/data/skills/ctx-advanced-evaluation/SKILL.md +402 -0
  17. package/data/skills/ctx-bdi-mental-states/SKILL.md +311 -0
  18. package/data/skills/ctx-context-compression/SKILL.md +272 -0
  19. package/data/skills/ctx-context-degradation/SKILL.md +206 -0
  20. package/data/skills/ctx-context-fundamentals/SKILL.md +201 -0
  21. package/data/skills/ctx-context-optimization/SKILL.md +195 -0
  22. package/data/skills/ctx-evaluation/SKILL.md +251 -0
  23. package/data/skills/ctx-filesystem-context/SKILL.md +287 -0
  24. package/data/skills/ctx-hosted-agents/SKILL.md +260 -0
  25. package/data/skills/ctx-memory-systems/SKILL.md +225 -0
  26. package/data/skills/ctx-multi-agent-patterns/SKILL.md +257 -0
  27. package/data/skills/ctx-project-development/SKILL.md +291 -0
  28. package/data/skills/ctx-tool-design/SKILL.md +271 -0
  29. package/data/skills/dhdna-profiler/SKILL.md +162 -0
  30. package/data/skills/generate-image/SKILL.md +183 -0
  31. package/data/skills/geomaster/SKILL.md +365 -0
  32. package/data/skills/get-available-resources/SKILL.md +275 -0
  33. package/data/skills/hamelsmu-build-review-interface/SKILL.md +96 -0
  34. package/data/skills/hamelsmu-error-analysis/SKILL.md +164 -0
  35. package/data/skills/hamelsmu-eval-audit/SKILL.md +183 -0
  36. package/data/skills/hamelsmu-evaluate-rag/SKILL.md +177 -0
  37. package/data/skills/hamelsmu-generate-synthetic-data/SKILL.md +131 -0
  38. package/data/skills/hamelsmu-validate-evaluator/SKILL.md +212 -0
  39. package/data/skills/hamelsmu-write-judge-prompt/SKILL.md +144 -0
  40. package/data/skills/hf-cli/SKILL.md +174 -0
  41. package/data/skills/hf-mcp/SKILL.md +178 -0
  42. package/data/skills/hugging-face-dataset-viewer/SKILL.md +121 -0
  43. package/data/skills/hugging-face-datasets/SKILL.md +542 -0
  44. package/data/skills/hugging-face-evaluation/SKILL.md +651 -0
  45. package/data/skills/hugging-face-jobs/SKILL.md +1042 -0
  46. package/data/skills/hugging-face-model-trainer/SKILL.md +717 -0
  47. package/data/skills/hugging-face-paper-pages/SKILL.md +239 -0
  48. package/data/skills/hugging-face-paper-publisher/SKILL.md +624 -0
  49. package/data/skills/hugging-face-tool-builder/SKILL.md +110 -0
  50. package/data/skills/hugging-face-trackio/SKILL.md +115 -0
  51. package/data/skills/hugging-face-vision-trainer/SKILL.md +593 -0
  52. package/data/skills/huggingface-gradio/SKILL.md +245 -0
  53. package/data/skills/matlab/SKILL.md +376 -0
  54. package/data/skills/modal/SKILL.md +381 -0
  55. package/data/skills/openai-cloudflare-deploy/SKILL.md +224 -0
  56. package/data/skills/openai-develop-web-game/SKILL.md +149 -0
  57. package/data/skills/openai-doc/SKILL.md +80 -0
  58. package/data/skills/openai-figma/SKILL.md +42 -0
  59. package/data/skills/openai-figma-implement-design/SKILL.md +264 -0
  60. package/data/skills/openai-gh-address-comments/SKILL.md +25 -0
  61. package/data/skills/openai-gh-fix-ci/SKILL.md +69 -0
  62. package/data/skills/openai-imagegen/SKILL.md +174 -0
  63. package/data/skills/openai-jupyter-notebook/SKILL.md +107 -0
  64. package/data/skills/openai-linear/SKILL.md +87 -0
  65. package/data/skills/openai-netlify-deploy/SKILL.md +247 -0
  66. package/data/skills/openai-notion-knowledge-capture/SKILL.md +56 -0
  67. package/data/skills/openai-notion-meeting-intelligence/SKILL.md +60 -0
  68. package/data/skills/openai-notion-research-documentation/SKILL.md +59 -0
  69. package/data/skills/openai-notion-spec-to-implementation/SKILL.md +58 -0
  70. package/data/skills/openai-openai-docs/SKILL.md +69 -0
  71. package/data/skills/openai-pdf/SKILL.md +67 -0
  72. package/data/skills/openai-playwright/SKILL.md +147 -0
  73. package/data/skills/openai-render-deploy/SKILL.md +479 -0
  74. package/data/skills/openai-screenshot/SKILL.md +267 -0
  75. package/data/skills/openai-security-best-practices/SKILL.md +86 -0
  76. package/data/skills/openai-security-ownership-map/SKILL.md +206 -0
  77. package/data/skills/openai-security-threat-model/SKILL.md +81 -0
  78. package/data/skills/openai-sentry/SKILL.md +123 -0
  79. package/data/skills/openai-sora/SKILL.md +178 -0
  80. package/data/skills/openai-speech/SKILL.md +144 -0
  81. package/data/skills/openai-spreadsheet/SKILL.md +145 -0
  82. package/data/skills/openai-transcribe/SKILL.md +81 -0
  83. package/data/skills/openai-vercel-deploy/SKILL.md +77 -0
  84. package/data/skills/openai-yeet/SKILL.md +28 -0
  85. package/data/skills/pennylane/SKILL.md +224 -0
  86. package/data/skills/polars-bio/SKILL.md +374 -0
  87. package/data/skills/primekg/SKILL.md +97 -0
  88. package/data/skills/pymatgen/SKILL.md +689 -0
  89. package/data/skills/qiskit/SKILL.md +273 -0
  90. package/data/skills/qutip/SKILL.md +316 -0
  91. package/data/skills/recursive-decomposition/SKILL.md +185 -0
  92. package/data/skills/rowan/SKILL.md +427 -0
  93. package/data/skills/scholar-evaluation/SKILL.md +298 -0
  94. package/data/skills/sentry-create-alert/SKILL.md +210 -0
  95. package/data/skills/sentry-fix-issues/SKILL.md +126 -0
  96. package/data/skills/sentry-pr-code-review/SKILL.md +105 -0
  97. package/data/skills/sentry-python-sdk/SKILL.md +317 -0
  98. package/data/skills/sentry-setup-ai-monitoring/SKILL.md +217 -0
  99. package/data/skills/stable-baselines3/SKILL.md +297 -0
  100. package/data/skills/sympy/SKILL.md +498 -0
  101. package/data/skills/trailofbits-ask-questions-if-underspecified/SKILL.md +85 -0
  102. package/data/skills/trailofbits-audit-context-building/SKILL.md +302 -0
  103. package/data/skills/trailofbits-differential-review/SKILL.md +220 -0
  104. package/data/skills/trailofbits-insecure-defaults/SKILL.md +117 -0
  105. package/data/skills/trailofbits-modern-python/SKILL.md +333 -0
  106. package/data/skills/trailofbits-property-based-testing/SKILL.md +123 -0
  107. package/data/skills/trailofbits-semgrep-rule-creator/SKILL.md +172 -0
  108. package/data/skills/trailofbits-sharp-edges/SKILL.md +292 -0
  109. package/data/skills/trailofbits-variant-analysis/SKILL.md +142 -0
  110. package/data/skills/transformers.js/SKILL.md +637 -0
  111. package/data/skills/writing/SKILL.md +419 -0
  112. package/data/workflows/survival-analysis-clinical/SKILL.md +348 -0
  113. package/data/workflows/survival-analysis-clinical/scripts/full_workflow.R +95 -0
  114. package/data/workflows/survival-analysis-clinical/scripts/load_example_data.R +65 -0
  115. package/data/workflows/survival-analysis-clinical/scripts/plot_forest.R +46 -0
  116. package/dist/bgi.js +1608 -233
  117. package/package.json +45 -45
package/dist/bgi.js CHANGED
@@ -6368,18 +6368,18 @@ function createFileFromPath(path, { mtimeMs, size }, filenameOrOptions, options
6368
6368
  });
6369
6369
  }
6370
6370
  function fileFromPathSync(path, filenameOrOptions, options = {}) {
6371
- const stats = (0, import_fs2.statSync)(path);
6371
+ const stats = (0, import_fs.statSync)(path);
6372
6372
  return createFileFromPath(path, stats, filenameOrOptions, options);
6373
6373
  }
6374
6374
  async function fileFromPath2(path, filenameOrOptions, options) {
6375
- const stats = await import_fs2.promises.stat(path);
6375
+ const stats = await import_fs.promises.stat(path);
6376
6376
  return createFileFromPath(path, stats, filenameOrOptions, options);
6377
6377
  }
6378
- var import_fs2, import_path2, import_node_domexception, __classPrivateFieldSet4, __classPrivateFieldGet5, _FileFromPath_path, _FileFromPath_start, MESSAGE, FileFromPath;
6378
+ var import_fs, import_path, import_node_domexception, __classPrivateFieldSet4, __classPrivateFieldGet5, _FileFromPath_path, _FileFromPath_start, MESSAGE, FileFromPath;
6379
6379
  var init_fileFromPath = __esm({
6380
6380
  "node_modules/formdata-node/lib/esm/fileFromPath.js"() {
6381
- import_fs2 = require("fs");
6382
- import_path2 = require("path");
6381
+ import_fs = require("fs");
6382
+ import_path = require("path");
6383
6383
  import_node_domexception = __toESM(require_node_domexception(), 1);
6384
6384
  init_File();
6385
6385
  init_isPlainObject();
@@ -6402,7 +6402,7 @@ var init_fileFromPath = __esm({
6402
6402
  _FileFromPath_start.set(this, void 0);
6403
6403
  __classPrivateFieldSet4(this, _FileFromPath_path, input.path, "f");
6404
6404
  __classPrivateFieldSet4(this, _FileFromPath_start, input.start || 0, "f");
6405
- this.name = (0, import_path2.basename)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6405
+ this.name = (0, import_path.basename)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6406
6406
  this.size = input.size;
6407
6407
  this.lastModified = input.lastModified;
6408
6408
  }
@@ -6415,12 +6415,12 @@ var init_fileFromPath = __esm({
6415
6415
  });
6416
6416
  }
6417
6417
  async *stream() {
6418
- const { mtimeMs } = await import_fs2.promises.stat(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6418
+ const { mtimeMs } = await import_fs.promises.stat(__classPrivateFieldGet5(this, _FileFromPath_path, "f"));
6419
6419
  if (mtimeMs > this.lastModified) {
6420
6420
  throw new import_node_domexception.default(MESSAGE, "NotReadableError");
6421
6421
  }
6422
6422
  if (this.size) {
6423
- yield* (0, import_fs2.createReadStream)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"), {
6423
+ yield* (0, import_fs.createReadStream)(__classPrivateFieldGet5(this, _FileFromPath_path, "f"), {
6424
6424
  start: __classPrivateFieldGet5(this, _FileFromPath_start, "f"),
6425
6425
  end: __classPrivateFieldGet5(this, _FileFromPath_start, "f") + this.size - 1
6426
6426
  });
@@ -6932,105 +6932,10 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
6932
6932
  var source_default = chalk;
6933
6933
 
6934
6934
  // src/index.ts
6935
- var import_fs4 = require("fs");
6936
- var import_path4 = require("path");
6935
+ var import_fs5 = require("fs");
6936
+ var import_path5 = require("path");
6937
6937
  var import_os3 = require("os");
6938
6938
 
6939
- // src/config.ts
6940
- var import_fs = require("fs");
6941
- var import_os = require("os");
6942
- var import_path = require("path");
6943
-
6944
- // src/providers.ts
6945
- var PROVIDERS = {
6946
- bailian: {
6947
- name: "\u767E\u70BC \xB7 \u963F\u91CC\u4E91 (DashScope)",
6948
- baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
6949
- envKey: "DASHSCOPE_API_KEY",
6950
- models: [
6951
- // ── Qwen3.5 (最新旗舰) ────────────────────────────────
6952
- "qwen3.5-397b-a17b",
6953
- "qwen3.5-122b-a10b",
6954
- "qwen3.5-plus",
6955
- "qwen3.5-flash",
6956
- // ── Qwen3 ─────────────────────────────────────────────
6957
- "qwen3-235b-a22b",
6958
- "qwen3-max",
6959
- "qwen3-32b",
6960
- "qwen3-14b",
6961
- "qwen3-8b",
6962
- // ── Qwen3 代码模型 ────────────────────────────────────
6963
- "qwen3-coder-plus",
6964
- "qwen3-coder-flash",
6965
- "qwen3-coder-480b-a35b-instruct",
6966
- // ── 经典 Qwen ─────────────────────────────────────────
6967
- "qwen-max",
6968
- "qwen-plus",
6969
- "qwen-turbo",
6970
- "qwen-long",
6971
- "qwen-flash",
6972
- // ── 推理模型 ──────────────────────────────────────────
6973
- "qwq-plus",
6974
- "deepseek-r1",
6975
- "deepseek-v3",
6976
- "deepseek-v3.2",
6977
- // ── 第三方 (DashScope 聚合) ───────────────────────────
6978
- "kimi-k2.5",
6979
- "kimi-k2-thinking",
6980
- "MiniMax-M2.5",
6981
- "glm-5"
6982
- ],
6983
- defaultModel: "qwen3.5-plus"
6984
- },
6985
- intranet: {
6986
- name: "\u5185\u7F51 Qwen3-235B (172.16.224.137)",
6987
- baseURL: "http://172.16.224.137:1024/v1",
6988
- models: ["Qwen3-235B-A22B"],
6989
- defaultModel: "Qwen3-235B-A22B",
6990
- envKey: ""
6991
- // no auth required
6992
- },
6993
- custom: {
6994
- name: "\u81EA\u5B9A\u4E49 (Custom URL)",
6995
- baseURL: "",
6996
- // filled from config.customUrl at runtime
6997
- models: [],
6998
- // filled from config.customModel at runtime
6999
- defaultModel: "",
7000
- envKey: "CUSTOM_API_KEY"
7001
- }
7002
- };
7003
- var DEFAULT_PROVIDER = "bailian";
7004
-
7005
- // src/config.ts
7006
- var BGI_DIR = (0, import_path.join)((0, import_os.homedir)(), ".bgicli");
7007
- var WORKFLOWS_DIR = (0, import_path.join)(BGI_DIR, "workflows");
7008
- var TOOLS_DIR = (0, import_path.join)(BGI_DIR, "tools");
7009
- var SKILLS_DIR = (0, import_path.join)(BGI_DIR, "skills");
7010
- var CONFIG_FILE = (0, import_path.join)(BGI_DIR, "config.json");
7011
- function ensureDirs() {
7012
- for (const dir of [BGI_DIR, WORKFLOWS_DIR, TOOLS_DIR, SKILLS_DIR]) {
7013
- if (!(0, import_fs.existsSync)(dir)) (0, import_fs.mkdirSync)(dir, { recursive: true });
7014
- }
7015
- }
7016
- function loadConfig() {
7017
- ensureDirs();
7018
- if (!(0, import_fs.existsSync)(CONFIG_FILE)) {
7019
- const def = {
7020
- provider: DEFAULT_PROVIDER,
7021
- model: PROVIDERS[DEFAULT_PROVIDER].defaultModel,
7022
- apiKeys: {}
7023
- };
7024
- saveConfig(def);
7025
- return def;
7026
- }
7027
- return JSON.parse((0, import_fs.readFileSync)(CONFIG_FILE, "utf8"));
7028
- }
7029
- function saveConfig(cfg) {
7030
- ensureDirs();
7031
- (0, import_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
7032
- }
7033
-
7034
6939
  // node_modules/openai/internal/qs/formats.mjs
7035
6940
  var default_format = "RFC3986";
7036
6941
  var formatters = {
@@ -13627,11 +13532,108 @@ OpenAI.Containers = Containers;
13627
13532
  OpenAI.ContainerListResponsesPage = ContainerListResponsesPage;
13628
13533
  var openai_default = OpenAI;
13629
13534
 
13535
+ // src/config.ts
13536
+ var import_fs2 = require("fs");
13537
+ var import_os = require("os");
13538
+ var import_path2 = require("path");
13539
+
13540
+ // src/providers.ts
13541
+ var PROVIDERS = {
13542
+ bailian: {
13543
+ name: "\u767E\u70BC \xB7 \u963F\u91CC\u4E91 (DashScope)",
13544
+ baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
13545
+ envKey: "DASHSCOPE_API_KEY",
13546
+ models: [
13547
+ // ── Qwen3.5 (最新旗舰) ────────────────────────────────
13548
+ "qwen3.5-397b-a17b",
13549
+ "qwen3.5-122b-a10b",
13550
+ "qwen3.5-plus",
13551
+ "qwen3.5-flash",
13552
+ // ── Qwen3 ─────────────────────────────────────────────
13553
+ "qwen3-235b-a22b",
13554
+ "qwen3-max",
13555
+ "qwen3-32b",
13556
+ "qwen3-14b",
13557
+ "qwen3-8b",
13558
+ // ── Qwen3 代码模型 ────────────────────────────────────
13559
+ "qwen3-coder-plus",
13560
+ "qwen3-coder-flash",
13561
+ "qwen3-coder-480b-a35b-instruct",
13562
+ // ── 经典 Qwen ─────────────────────────────────────────
13563
+ "qwen-max",
13564
+ "qwen-plus",
13565
+ "qwen-turbo",
13566
+ "qwen-long",
13567
+ "qwen-flash",
13568
+ // ── 推理模型 ──────────────────────────────────────────
13569
+ "qwq-plus",
13570
+ "deepseek-r1",
13571
+ "deepseek-v3",
13572
+ "deepseek-v3.2",
13573
+ // ── 第三方 (DashScope 聚合) ───────────────────────────
13574
+ "kimi-k2.5",
13575
+ "kimi-k2-thinking",
13576
+ "MiniMax-M2.5",
13577
+ "glm-5"
13578
+ ],
13579
+ defaultModel: "qwen3.5-plus"
13580
+ },
13581
+ intranet: {
13582
+ name: "\u5185\u7F51 Qwen3-235B (172.16.224.137)",
13583
+ baseURL: "http://172.16.224.137:1024/v1",
13584
+ models: ["Qwen3-235B-A22B"],
13585
+ defaultModel: "Qwen3-235B-A22B",
13586
+ envKey: ""
13587
+ // no auth required
13588
+ },
13589
+ custom: {
13590
+ name: "\u81EA\u5B9A\u4E49 (Custom URL)",
13591
+ baseURL: "",
13592
+ // filled from config.customUrl at runtime
13593
+ models: [],
13594
+ // filled from config.customModel at runtime
13595
+ defaultModel: "",
13596
+ envKey: "CUSTOM_API_KEY"
13597
+ }
13598
+ };
13599
+ var DEFAULT_PROVIDER = "bailian";
13600
+
13601
+ // src/config.ts
13602
+ var BGI_DIR = (0, import_path2.join)((0, import_os.homedir)(), ".bgicli");
13603
+ var WORKFLOWS_DIR = (0, import_path2.join)(BGI_DIR, "workflows");
13604
+ var TOOLS_DIR = (0, import_path2.join)(BGI_DIR, "tools");
13605
+ var SKILLS_DIR = (0, import_path2.join)(BGI_DIR, "skills");
13606
+ var CONFIG_FILE = (0, import_path2.join)(BGI_DIR, "config.json");
13607
+ function ensureDirs() {
13608
+ for (const dir of [BGI_DIR, WORKFLOWS_DIR, TOOLS_DIR, SKILLS_DIR]) {
13609
+ if (!(0, import_fs2.existsSync)(dir)) (0, import_fs2.mkdirSync)(dir, { recursive: true });
13610
+ }
13611
+ }
13612
+ function loadConfig() {
13613
+ ensureDirs();
13614
+ if (!(0, import_fs2.existsSync)(CONFIG_FILE)) {
13615
+ const def = {
13616
+ provider: DEFAULT_PROVIDER,
13617
+ model: PROVIDERS[DEFAULT_PROVIDER].defaultModel,
13618
+ apiKeys: {}
13619
+ };
13620
+ saveConfig(def);
13621
+ return def;
13622
+ }
13623
+ return JSON.parse((0, import_fs2.readFileSync)(CONFIG_FILE, "utf8"));
13624
+ }
13625
+ function saveConfig(cfg) {
13626
+ ensureDirs();
13627
+ (0, import_fs2.writeFileSync)(CONFIG_FILE, JSON.stringify(cfg, null, 2), "utf8");
13628
+ }
13629
+
13630
13630
  // src/tools.ts
13631
13631
  var import_child_process = require("child_process");
13632
13632
  var import_fs3 = require("fs");
13633
13633
  var import_path3 = require("path");
13634
13634
  var import_os2 = require("os");
13635
+ var import_https = require("https");
13636
+ var import_http = require("http");
13635
13637
  var TOOL_DEFINITIONS = [
13636
13638
  {
13637
13639
  type: "function",
@@ -13651,7 +13653,7 @@ var TOOL_DEFINITIONS = [
13651
13653
  },
13652
13654
  timeout_ms: {
13653
13655
  type: "number",
13654
- description: "Timeout in milliseconds (default 30000, max 300000 for long jobs)"
13656
+ description: "Timeout in milliseconds (default 300000 / 5 min, max 1800000 / 30 min for long jobs like STAR alignment)"
13655
13657
  }
13656
13658
  },
13657
13659
  required: ["command"]
@@ -13669,7 +13671,7 @@ var TOOL_DEFINITIONS = [
13669
13671
  path: { type: "string", description: "Absolute or relative file path" },
13670
13672
  max_lines: {
13671
13673
  type: "number",
13672
- description: "Maximum number of lines to return (default: 200)"
13674
+ description: "Maximum number of lines to return (default: 500)"
13673
13675
  },
13674
13676
  offset: {
13675
13677
  type: "number",
@@ -13729,21 +13731,43 @@ var TOOL_DEFINITIONS = [
13729
13731
  required: ["pattern"]
13730
13732
  }
13731
13733
  }
13734
+ },
13735
+ {
13736
+ type: "function",
13737
+ function: {
13738
+ name: "fetch_geo",
13739
+ description: "Query NCBI GEO database by accession number (GSE, GDS, GPL, GSM). Returns dataset metadata, sample info, organism, platform, and download links. Use this BEFORE asking the user to manually download data \u2014 always try fetch_geo first when a GEO accession is mentioned.",
13740
+ parameters: {
13741
+ type: "object",
13742
+ properties: {
13743
+ accession: {
13744
+ type: "string",
13745
+ description: 'GEO accession number, e.g. "GSE12345", "GDS1234", "GPL570"'
13746
+ },
13747
+ include_samples: {
13748
+ type: "boolean",
13749
+ description: "Whether to include individual sample (GSM) metadata (default: false, set true for small datasets)"
13750
+ }
13751
+ },
13752
+ required: ["accession"]
13753
+ }
13754
+ }
13732
13755
  }
13733
13756
  ];
13734
- async function executeTool(name, args) {
13757
+ async function executeTool(name, args, onStream) {
13735
13758
  try {
13736
13759
  switch (name) {
13737
13760
  case "bash":
13738
13761
  return await toolBash(
13739
13762
  args["command"],
13740
13763
  args["workdir"],
13741
- args["timeout_ms"] ?? 3e4
13764
+ args["timeout_ms"] ?? 3e5,
13765
+ onStream
13742
13766
  );
13743
13767
  case "read_file":
13744
13768
  return toolReadFile(
13745
13769
  args["path"],
13746
- args["max_lines"] ?? 200,
13770
+ args["max_lines"] ?? 500,
13747
13771
  args["offset"] ?? 0
13748
13772
  );
13749
13773
  case "write_file":
@@ -13755,6 +13779,11 @@ async function executeTool(name, args) {
13755
13779
  args["pattern"],
13756
13780
  args["path"] ?? process.cwd()
13757
13781
  );
13782
+ case "fetch_geo":
13783
+ return await toolFetchGeo(
13784
+ args["accession"],
13785
+ args["include_samples"] ?? false
13786
+ );
13758
13787
  default:
13759
13788
  return { output: "", error: `Unknown tool: ${name}` };
13760
13789
  }
@@ -13775,7 +13804,32 @@ function decodeBuffer(buf) {
13775
13804
  }
13776
13805
  }
13777
13806
  }
13778
- async function toolBash(command, workdir, timeoutMs = 3e4) {
13807
+ var DANGEROUS_PATTERNS = [
13808
+ { pattern: /rm\s+-rf\s+\/(?!\S)/, reason: "\u5220\u9664\u6839\u76EE\u5F55 (rm -rf /)" },
13809
+ { pattern: /rm\s+-rf\s+~(?!\S)/, reason: "\u5220\u9664 home \u76EE\u5F55 (rm -rf ~)" },
13810
+ { pattern: /rm\s+-rf\s+\$HOME(?!\S)/, reason: "\u5220\u9664 $HOME \u76EE\u5F55" },
13811
+ { pattern: /dd\s+if=\/dev\/(?:zero|random|urandom)\s+of=\/dev\//, reason: "\u8986\u5199\u78C1\u76D8\u8BBE\u5907 (dd)" },
13812
+ { pattern: /mkfs\b/, reason: "\u683C\u5F0F\u5316\u6587\u4EF6\u7CFB\u7EDF (mkfs)" },
13813
+ { pattern: />\s*\/dev\/sd[a-z]/, reason: "\u76F4\u63A5\u5199\u5165\u78C1\u76D8\u8BBE\u5907" },
13814
+ { pattern: /chmod\s+-R\s+777\s+\/(?!\S)/, reason: "\u9012\u5F52\u4FEE\u6539\u6839\u76EE\u5F55\u6743\u9650" },
13815
+ { pattern: /:\(\)\s*\{.*\}.*:/, reason: "Fork bomb \u68C0\u6D4B" }
13816
+ ];
13817
+ function checkDangerousCommand(command) {
13818
+ for (const { pattern, reason } of DANGEROUS_PATTERNS) {
13819
+ if (pattern.test(command)) return reason;
13820
+ }
13821
+ return null;
13822
+ }
13823
+ async function toolBash(command, workdir, timeoutMs = 3e5, onStream) {
13824
+ const danger = checkDangerousCommand(command);
13825
+ if (danger) {
13826
+ return {
13827
+ output: "",
13828
+ error: `\u26A0\uFE0F \u5B89\u5168\u62E6\u622A\uFF1A\u68C0\u6D4B\u5230\u5371\u9669\u547D\u4EE4\uFF08${danger}\uFF09\u3002
13829
+ \u547D\u4EE4\u5DF2\u88AB\u963B\u6B62\uFF0C\u8BF7\u786E\u8BA4\u4F60\u7684\u610F\u56FE\u540E\u624B\u52A8\u6267\u884C\u3002
13830
+ \u88AB\u62E6\u622A\u7684\u547D\u4EE4: ${command}`
13831
+ };
13832
+ }
13779
13833
  return new Promise((resolve3) => {
13780
13834
  const isWin = process.platform === "win32";
13781
13835
  const child = (0, import_child_process.spawn)(isWin ? "cmd" : "/bin/sh", isWin ? ["/c", command] : ["-c", command], {
@@ -13788,10 +13842,16 @@ async function toolBash(command, workdir, timeoutMs = 3e4) {
13788
13842
  const MAX = 10 * 1024 * 1024;
13789
13843
  let total = 0;
13790
13844
  child.stdout?.on("data", (c2) => {
13791
- if ((total += c2.length) <= MAX) outChunks.push(c2);
13845
+ if ((total += c2.length) <= MAX) {
13846
+ outChunks.push(c2);
13847
+ if (onStream) onStream(decodeBuffer(c2));
13848
+ }
13792
13849
  });
13793
13850
  child.stderr?.on("data", (c2) => {
13794
- if ((total += c2.length) <= MAX) errChunks.push(c2);
13851
+ if ((total += c2.length) <= MAX) {
13852
+ errChunks.push(c2);
13853
+ if (onStream) onStream(decodeBuffer(c2));
13854
+ }
13795
13855
  });
13796
13856
  let timedOut = false;
13797
13857
  const timer = setTimeout(() => {
@@ -13848,6 +13908,121 @@ async function toolSearchFiles(pattern, rootPath) {
13848
13908
  const command = isWin ? `dir /s /b "${resolved}\\${pattern}" 2>nul` : `find "${resolved}" -name ${pattern.includes("/") ? pattern : `"${pattern}"`} 2>/dev/null | head -50`;
13849
13909
  return toolBash(command, resolved, 1e4);
13850
13910
  }
13911
+ function httpFetch(url, timeoutMs = 15e3) {
13912
+ return new Promise((resolve3, reject) => {
13913
+ const getter = url.startsWith("https") ? import_https.get : import_http.get;
13914
+ const req = getter(url, { headers: { "User-Agent": "BGI-CLI/1.0" } }, (res) => {
13915
+ if ((res.statusCode === 301 || res.statusCode === 302) && res.headers.location) {
13916
+ httpFetch(res.headers.location, timeoutMs).then(resolve3).catch(reject);
13917
+ return;
13918
+ }
13919
+ if (res.statusCode && res.statusCode >= 400) {
13920
+ reject(new Error(`HTTP ${res.statusCode}`));
13921
+ return;
13922
+ }
13923
+ const chunks = [];
13924
+ res.on("data", (c2) => chunks.push(c2));
13925
+ res.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
13926
+ res.on("error", reject);
13927
+ });
13928
+ req.setTimeout(timeoutMs, () => {
13929
+ req.destroy();
13930
+ reject(new Error("Request timed out"));
13931
+ });
13932
+ req.on("error", reject);
13933
+ });
13934
+ }
13935
+ async function toolFetchGeo(accession, includeSamples) {
13936
+ const acc = accession.trim().toUpperCase();
13937
+ if (!/^(GSE|GDS|GPL|GSM)\d+$/.test(acc)) {
13938
+ return {
13939
+ output: "",
13940
+ error: `\u65E0\u6548\u7684 GEO \u7F16\u53F7: "${acc}"\u3002\u652F\u6301\u683C\u5F0F: GSE12345, GDS1234, GPL570, GSM123456`
13941
+ };
13942
+ }
13943
+ const accType = acc.slice(0, 3);
13944
+ try {
13945
+ const searchUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=gds&term=${acc}[Accession]&retmode=json&retmax=1`;
13946
+ const searchRaw = await httpFetch(searchUrl);
13947
+ const searchJson = JSON.parse(searchRaw);
13948
+ const idList = searchJson.esearchresult?.idlist ?? [];
13949
+ if (idList.length === 0) {
13950
+ return { output: "", error: `\u672A\u627E\u5230 GEO \u8BB0\u5F55: ${acc}\u3002\u8BF7\u786E\u8BA4\u7F16\u53F7\u662F\u5426\u6B63\u786E\u3002` };
13951
+ }
13952
+ const uid = idList[0];
13953
+ const summaryUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=gds&id=${uid}&retmode=json`;
13954
+ const summaryRaw = await httpFetch(summaryUrl);
13955
+ const summaryJson = JSON.parse(summaryRaw);
13956
+ const rec = summaryJson.result?.[uid];
13957
+ if (!rec) {
13958
+ return { output: "", error: `\u65E0\u6CD5\u83B7\u53D6 ${acc} \u7684\u8BE6\u7EC6\u4FE1\u606F` };
13959
+ }
13960
+ const lines = [];
13961
+ lines.push(`=== GEO \u6570\u636E\u96C6: ${acc} ===`);
13962
+ lines.push("");
13963
+ if (rec.title) lines.push(`\u6807\u9898: ${rec.title}`);
13964
+ if (rec.taxon || rec.organism)
13965
+ lines.push(`\u7269\u79CD: ${rec.taxon ?? rec.organism}`);
13966
+ if (rec.gdstype || rec.ptechtype)
13967
+ lines.push(`\u7C7B\u578B: ${rec.gdstype ?? rec.ptechtype}`);
13968
+ if (rec.gpl) lines.push(`\u5E73\u53F0: GPL${rec.gpl}`);
13969
+ if (rec.n_samples) lines.push(`\u6837\u672C\u6570: ${rec.n_samples}`);
13970
+ if (rec.summary) {
13971
+ const shortSummary = rec.summary.length > 500 ? rec.summary.slice(0, 500) + "..." : rec.summary;
13972
+ lines.push("");
13973
+ lines.push(`\u6458\u8981:
13974
+ ${shortSummary}`);
13975
+ }
13976
+ lines.push("");
13977
+ lines.push("=== \u4E0B\u8F7D\u94FE\u63A5 ===");
13978
+ lines.push(`GEO \u9875\u9762: https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=${acc}`);
13979
+ if (accType === "GSE") {
13980
+ lines.push(`\u77E9\u9635\u6587\u4EF6: https://ftp.ncbi.nlm.nih.gov/geo/series/${acc.slice(0, -3)}nnn/${acc}/matrix/`);
13981
+ lines.push(`\u539F\u59CB\u6570\u636E: https://ftp.ncbi.nlm.nih.gov/geo/series/${acc.slice(0, -3)}nnn/${acc}/suppl/`);
13982
+ lines.push("");
13983
+ lines.push("=== R \u4E0B\u8F7D\u4EE3\u7801 ===");
13984
+ lines.push("```r");
13985
+ lines.push("# \u65B9\u6CD51: GEOquery\uFF08\u63A8\u8350\uFF0C\u81EA\u52A8\u89E3\u6790\uFF09");
13986
+ lines.push('if (!require("GEOquery")) BiocManager::install("GEOquery")');
13987
+ lines.push(`gse <- getGEO("${acc}", GSEMatrix = TRUE, getGPL = FALSE)`);
13988
+ lines.push("expr_matrix <- exprs(gse[[1]]) # \u8868\u8FBE\u77E9\u9635");
13989
+ lines.push("pheno_data <- pData(gse[[1]]) # \u6837\u672C\u5143\u6570\u636E");
13990
+ lines.push("");
13991
+ lines.push("# \u65B9\u6CD52: \u76F4\u63A5\u4E0B\u8F7D\u77E9\u9635\u6587\u4EF6");
13992
+ lines.push(`url <- "https://ftp.ncbi.nlm.nih.gov/geo/series/${acc.slice(0, -3)}nnn/${acc}/matrix/${acc}_series_matrix.txt.gz"`);
13993
+ lines.push('download.file(url, destfile = "series_matrix.txt.gz")');
13994
+ lines.push("```");
13995
+ lines.push("");
13996
+ lines.push("=== Python \u4E0B\u8F7D\u4EE3\u7801 ===");
13997
+ lines.push("```python");
13998
+ lines.push("import GEOparse");
13999
+ lines.push(`gse = GEOparse.get_GEO(geo="${acc}", destdir="./data")`);
14000
+ lines.push("# gse.gsms \u2014 \u6837\u672C\u5B57\u5178");
14001
+ lines.push("# gse.gpls \u2014 \u5E73\u53F0\u4FE1\u606F");
14002
+ lines.push("```");
14003
+ }
14004
+ if (rec.suppfile) {
14005
+ lines.push("");
14006
+ lines.push(`\u8865\u5145\u6587\u4EF6: ${rec.suppfile}`);
14007
+ }
14008
+ if (includeSamples && rec.samples && rec.samples.length > 0) {
14009
+ lines.push("");
14010
+ lines.push(`=== \u6837\u672C\u5217\u8868 (${rec.samples.length} \u4E2A) ===`);
14011
+ const showSamples = rec.samples.slice(0, 20);
14012
+ showSamples.forEach((s2) => lines.push(` ${s2.accession}: ${s2.title}`));
14013
+ if (rec.samples.length > 20) {
14014
+ lines.push(` ... \u8FD8\u6709 ${rec.samples.length - 20} \u4E2A\u6837\u672C`);
14015
+ }
14016
+ }
14017
+ return { output: lines.join("\n") };
14018
+ } catch (err) {
14019
+ const msg = err instanceof Error ? err.message : String(err);
14020
+ return {
14021
+ output: `GEO \u9875\u9762: https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=${acc}`,
14022
+ error: `\u7F51\u7EDC\u8BF7\u6C42\u5931\u8D25 (${msg})\u3002\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\uFF0C\u6216\u76F4\u63A5\u8BBF\u95EE\u4E0A\u65B9\u94FE\u63A5\u3002`
14023
+ };
14024
+ }
14025
+ }
13851
14026
 
13852
14027
  // src/chat.ts
13853
14028
  async function chat(messages, config, systemPrompt) {
@@ -13872,6 +14047,7 @@ async function chat(messages, config, systemPrompt) {
13872
14047
  async function streamLoop(client, messages, model) {
13873
14048
  let finalText = "";
13874
14049
  for (let round = 0; round < 20; round++) {
14050
+ messages = deduplicateSkillInjections(trimToolOutputs(messages));
13875
14051
  const { text, toolCalls, finishReason } = await streamOnce(client, messages, model);
13876
14052
  if (text) finalText = text;
13877
14053
  if (finishReason === "tool_calls" && toolCalls.length > 0) {
@@ -13888,28 +14064,63 @@ async function streamLoop(client, messages, model) {
13888
14064
  const SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
13889
14065
  for (const tc of toolCalls) {
13890
14066
  const args = parseArgs(tc.args);
14067
+ const isBash = tc.name === "bash";
13891
14068
  const label = source_default.dim(`[\u5DE5\u5177: ${tc.name}(${summarizeArgs(args)})]`);
13892
14069
  const t0 = Date.now();
13893
- let frame = 0;
13894
14070
  process.stdout.write(`
13895
- ${label} `);
13896
- const spin = setInterval(() => {
13897
- const secs = ((Date.now() - t0) / 1e3).toFixed(1);
13898
- process.stdout.write(
13899
- `\r${label} ${source_default.cyan(SPIN_FRAMES[frame++ % SPIN_FRAMES.length])} ${source_default.dim(secs + "s")}`
13900
- );
13901
- }, 80);
13902
- const result = await executeTool(tc.name, args);
13903
- clearInterval(spin);
14071
+ ${label}
14072
+ `);
14073
+ let streamedLines = 0;
14074
+ let lastLineWasEmpty = false;
14075
+ const MAX_STREAM_LINES = 200;
14076
+ let spin = null;
14077
+ let frame = 0;
14078
+ const onStream = isBash ? (chunk) => {
14079
+ if (streamedLines >= MAX_STREAM_LINES) return;
14080
+ const lines = chunk.split("\n");
14081
+ for (let i2 = 0; i2 < lines.length; i2++) {
14082
+ const line = lines[i2];
14083
+ if (line.trim() === "") {
14084
+ if (lastLineWasEmpty) continue;
14085
+ lastLineWasEmpty = true;
14086
+ } else {
14087
+ lastLineWasEmpty = false;
14088
+ }
14089
+ if (i2 < lines.length - 1 || line.length > 0) {
14090
+ process.stdout.write(source_default.dim(" \u2502 ") + line + (i2 < lines.length - 1 ? "\n" : ""));
14091
+ streamedLines++;
14092
+ if (streamedLines >= MAX_STREAM_LINES) {
14093
+ process.stdout.write(source_default.dim("\n \u2502 ... (\u8F93\u51FA\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD)\n"));
14094
+ break;
14095
+ }
14096
+ }
14097
+ }
14098
+ } : void 0;
14099
+ if (!isBash) {
14100
+ spin = setInterval(() => {
14101
+ const secs = ((Date.now() - t0) / 1e3).toFixed(1);
14102
+ process.stdout.write(
14103
+ `\r ${source_default.cyan(SPIN_FRAMES[frame++ % SPIN_FRAMES.length])} ${source_default.dim(secs + "s")}`
14104
+ );
14105
+ }, 80);
14106
+ }
14107
+ const result = await executeTool(tc.name, args, onStream);
14108
+ if (spin) {
14109
+ clearInterval(spin);
14110
+ process.stdout.write("\r\x1B[2K");
14111
+ }
13904
14112
  const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
13905
14113
  const doneIcon = result.error ? source_default.yellow("\u2717") : source_default.green("\u2713");
13906
- process.stdout.write(`\r\x1B[2K${label} ${doneIcon} ${source_default.dim(elapsed + "s")}
14114
+ if (isBash && streamedLines > 0) {
14115
+ process.stdout.write("\n");
14116
+ }
14117
+ process.stdout.write(` ${doneIcon} ${source_default.dim("\u5B8C\u6210 " + elapsed + "s")}
13907
14118
  `);
13908
14119
  if (result.error) {
13909
14120
  process.stdout.write(source_default.yellow(` \u26A0 ${result.error}
13910
14121
  `));
13911
14122
  }
13912
- if (result.output) {
14123
+ if (!isBash && result.output) {
13913
14124
  const preview = result.output.split("\n").slice(0, 3).join("\n");
13914
14125
  const more = result.output.split("\n").length > 3;
13915
14126
  process.stdout.write(source_default.dim(` ${preview}${more ? "\n ..." : ""}
@@ -13974,6 +14185,52 @@ async function streamOnce(client, messages, model) {
13974
14185
  finishReason
13975
14186
  };
13976
14187
  }
14188
+ function estimateTokens(messages) {
14189
+ const chars = messages.reduce((n2, m2) => {
14190
+ const content = m2.content;
14191
+ if (typeof content === "string") return n2 + content.length;
14192
+ if (Array.isArray(content)) {
14193
+ return n2 + content.reduce((s2, c2) => s2 + (typeof c2 === "object" && "text" in c2 ? c2.text.length : 0), 0);
14194
+ }
14195
+ return n2;
14196
+ }, 0);
14197
+ return Math.round(chars / 3.5);
14198
+ }
14199
+ var MAX_TOOL_OUTPUT_CHARS = 8e3;
14200
+ function trimToolOutputs(messages) {
14201
+ return messages.map((m2) => {
14202
+ if (m2.role !== "tool") return m2;
14203
+ const content = typeof m2.content === "string" ? m2.content : "";
14204
+ if (content.length <= MAX_TOOL_OUTPUT_CHARS) return m2;
14205
+ const head = content.slice(0, MAX_TOOL_OUTPUT_CHARS / 2);
14206
+ const tail = content.slice(-MAX_TOOL_OUTPUT_CHARS / 2);
14207
+ const trimmed = `${head}
14208
+
14209
+ ... [\u8F93\u51FA\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD ${content.length - MAX_TOOL_OUTPUT_CHARS} \u5B57\u7B26] ...
14210
+
14211
+ ${tail}`;
14212
+ return { ...m2, content: trimmed };
14213
+ });
14214
+ }
14215
+ function deduplicateSkillInjections(messages) {
14216
+ const SKILL_MARKER = "[Skill \u5DF2\u52A0\u8F7D:";
14217
+ const seenSkills = /* @__PURE__ */ new Set();
14218
+ const result = [];
14219
+ for (let i2 = messages.length - 1; i2 >= 0; i2--) {
14220
+ const m2 = messages[i2];
14221
+ const content = typeof m2.content === "string" ? m2.content : "";
14222
+ if (m2.role === "user" && content.startsWith(SKILL_MARKER)) {
14223
+ const idMatch = content.match(/\[Skill 已加载: ([^\]]+)\]/);
14224
+ const skillId = idMatch?.[1];
14225
+ if (skillId) {
14226
+ if (seenSkills.has(skillId)) continue;
14227
+ seenSkills.add(skillId);
14228
+ }
14229
+ }
14230
+ result.unshift(m2);
14231
+ }
14232
+ return result;
14233
+ }
13977
14234
  async function compactMessages(messages, config) {
13978
14235
  const prov = PROVIDERS[config.provider];
13979
14236
  if (!prov) throw new Error(`Unknown provider: ${config.provider}`);
@@ -14040,6 +14297,7 @@ You have access to these tools:
14040
14297
  - **write_file**: Create or update files
14041
14298
  - **list_dir**: List directory contents
14042
14299
  - **search_files**: Find files by pattern (glob)
14300
+ - **fetch_geo**: Query NCBI GEO database by accession (GSE/GDS/GPL/GSM). Returns metadata, sample info, organism, platform, and ready-to-use R/Python download code. **Always call this first when the user mentions a GEO accession number \u2014 never ask them to download manually.**
14043
14301
 
14044
14302
  **MANDATORY WORKFLOW**: When the user gives you a bioinformatics task:
14045
14303
  1. Check if a matching pre-built workflow exists (see Workflow Library below)
@@ -14092,6 +14350,7 @@ cat ${WORKFLOWS_DIR}/<workflow-id>/SKILL.md
14092
14350
 
14093
14351
  | ID | Use When |
14094
14352
  |----|----------|
14353
+ | \`survival-analysis-clinical\` | \u751F\u5B58\u5206\u6790\uFF1AKM \u66F2\u7EBF\u3001log-rank \u68C0\u9A8C\u3001Cox \u56DE\u5F52\u3001\u7ADE\u4E89\u98CE\u9669\uFF08OS/PFS/DFS\uFF09 |
14095
14354
  | \`clinicaltrials-landscape\` | ClinicalTrials.gov \u6570\u636E\u5206\u6790 |
14096
14355
  | \`literature-preclinical\` | \u4E34\u5E8A\u524D\u6587\u732E\u7CFB\u7EDF\u63D0\u53D6\u4E0E\u7EFC\u5408 |
14097
14356
  | \`experimental-design-statistics\` | \u7EDF\u8BA1\u68C0\u9A8C\u9009\u62E9\u3001\u6837\u672C\u91CF\u8BA1\u7B97\u3001\u968F\u673A\u5316\u65B9\u6848 |
@@ -14175,6 +14434,62 @@ samtools --version 2>&1 | head -1
14175
14434
 
14176
14435
  ---
14177
14436
 
14437
+ ## Data Integrity Rules\uFF08\u5206\u6790\u524D\u5FC5\u987B\u6267\u884C\uFF09
14438
+
14439
+ \u{1F52C} **\u5728\u4EFB\u4F55\u7EDF\u8BA1\u5206\u6790\u5F00\u59CB\u524D\uFF0C\u5FC5\u987B\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A**
14440
+
14441
+ ### 1. \u6837\u672C ID \u4E00\u81F4\u6027
14442
+ \`\`\`r
14443
+ # R: \u68C0\u67E5 count \u77E9\u9635\u5217\u540D\u4E0E metadata \u884C\u540D\u662F\u5426\u5B8C\u5168\u4E00\u81F4
14444
+ stopifnot(all(colnames(counts) == rownames(metadata)))
14445
+ # \u5982\u679C\u4E0D\u4E00\u81F4\uFF0C\u5148\u5BF9\u9F50\u518D\u5206\u6790\uFF1A
14446
+ metadata <- metadata[colnames(counts), , drop = FALSE]
14447
+ \`\`\`
14448
+ \`\`\`python
14449
+ # Python: \u68C0\u67E5 AnnData obs_names \u4E0E metadata index \u662F\u5426\u4E00\u81F4
14450
+ assert list(adata.obs_names) == list(metadata.index), "\u6837\u672C ID \u4E0D\u5339\u914D\uFF01"
14451
+ \`\`\`
14452
+
14453
+ ### 2. \u91CD\u590D\u884C/\u5217\u540D\u68C0\u6D4B
14454
+ \`\`\`r
14455
+ # \u68C0\u67E5\u91CD\u590D\u57FA\u56E0\u540D\uFF08\u884C\u540D\uFF09
14456
+ if (any(duplicated(rownames(counts)))) {
14457
+ warning("\u53D1\u73B0\u91CD\u590D\u57FA\u56E0\u540D\uFF0C\u5C06\u805A\u5408\u91CD\u590D\u884C\uFF08\u53D6\u5747\u503C\uFF09")
14458
+ counts <- aggregate(counts, by = list(rownames(counts)), FUN = mean)
14459
+ }
14460
+ \`\`\`
14461
+
14462
+ ### 3. \u7F3A\u5931\u503C\u62A5\u544A
14463
+ \`\`\`r
14464
+ na_pct <- sum(is.na(counts)) / prod(dim(counts)) * 100
14465
+ message(sprintf("\u7F3A\u5931\u503C\u6BD4\u4F8B: %.2f%%", na_pct))
14466
+ if (na_pct > 5) warning("\u7F3A\u5931\u503C\u8D85\u8FC7 5%\uFF0C\u8BF7\u68C0\u67E5\u6570\u636E\u8D28\u91CF")
14467
+ \`\`\`
14468
+
14469
+ ### 4. \u5DEE\u5F02\u8868\u8FBE\u5206\u6790\uFF1A\u5FC5\u987B\u4F7F\u7528 padj\uFF0C\u7981\u6B62\u4F7F\u7528\u539F\u59CB pvalue
14470
+ \`\`\`r
14471
+ # \u2705 \u6B63\u786E\uFF1A\u4F7F\u7528 FDR \u6821\u6B63\u540E\u7684 p \u503C
14472
+ sig_genes <- res[!is.na(res$padj) & res$padj <= 0.05 & abs(res$log2FoldChange) >= 1, ]
14473
+
14474
+ # \u274C \u9519\u8BEF\uFF1A\u4E0D\u8981\u7528\u539F\u59CB pvalue \u7B5B\u9009 DEG
14475
+ # sig_genes <- res[res$pvalue < 0.05, ] # \u8FD9\u662F\u9519\u7684\uFF01
14476
+ \`\`\`
14477
+
14478
+ **\u6807\u51C6\u9608\u503C**\uFF08\u9664\u975E\u7528\u6237\u660E\u786E\u6307\u5B9A\u5176\u4ED6\u503C\uFF09\uFF1A
14479
+ - \u663E\u8457\u6027\uFF1A**padj \u2264 0.05**\uFF08FDR \u6821\u6B63\uFF0CBenjamini-Hochberg\uFF09
14480
+ - \u6548\u5E94\u91CF\uFF1A**|log2FoldChange| \u2265 1**\uFF08\u5373 2 \u500D\u5DEE\u5F02\uFF09
14481
+
14482
+ ### 5. \u7ED3\u679C\u9A8C\u8BC1
14483
+ \u6BCF\u4E2A\u5206\u6790\u5B8C\u6210\u540E\uFF0C\u8F93\u51FA\u5173\u952E\u7EDF\u8BA1\u6458\u8981\uFF1A
14484
+ \`\`\`r
14485
+ message(sprintf("\u603B\u57FA\u56E0\u6570: %d | \u663E\u8457 DEG: %d (\u4E0A\u8C03: %d, \u4E0B\u8C03: %d)",
14486
+ nrow(res), nrow(sig_genes),
14487
+ sum(sig_genes$log2FoldChange > 0),
14488
+ sum(sig_genes$log2FoldChange < 0)))
14489
+ \`\`\`
14490
+
14491
+ ---
14492
+
14178
14493
  ## Script Execution Rules
14179
14494
 
14180
14495
  \u{1F6A8} **\u5173\u952E\u89C4\u5219\uFF1A**
@@ -14200,7 +14515,25 @@ samtools --version 2>&1 | head -1
14200
14515
  \u4F7F\u7528 functional-enrichment-from-degs \u5DE5\u4F5C\u6D41
14201
14516
 
14202
14517
  **\u7528\u6237\u8BF4 "\u8BBE\u8BA1 CRISPR guide RNA" \u2192**
14203
- \u4F7F\u7528 molecular_biology.py \u7684 design_crispr_knockout_guides()`;
14518
+ \u4F7F\u7528 molecular_biology.py \u7684 design_crispr_knockout_guides()
14519
+
14520
+ **\u7528\u6237\u8BF4 "\u54EA\u4E9B\u57FA\u56E0\u5728\u80BF\u7624\u91CC\u8868\u8FBE\u91CF\u9AD8" / "\u627E\u4E0A\u8C03\u57FA\u56E0" \u2192**
14521
+ \u2192 \u5DEE\u5F02\u8868\u8FBE\u5206\u6790\uFF08DESeq2\uFF09\uFF0C\u4F7F\u7528 bulk-rnaseq-counts-to-de-deseq2 \u5DE5\u4F5C\u6D41
14522
+
14523
+ **\u7528\u6237\u8BF4 "\u5148\u505A\u5DEE\u5F02\u8868\u8FBE\uFF0C\u518D\u505A\u5BCC\u96C6\u5206\u6790" \u2192**
14524
+ \u2192 \u8BC6\u522B\u4E3A\u591A\u4EFB\u52A1\uFF1A\u4F9D\u6B21\u6267\u884C bulk-rnaseq-counts-to-de-deseq2 + functional-enrichment-from-degs
14525
+
14526
+ **\u7528\u6237\u8BF4 "\u8FD9\u4E9B\u57FA\u56E0\u53C2\u4E0E\u4EC0\u4E48\u901A\u8DEF" / "\u57FA\u56E0\u529F\u80FD\u662F\u4EC0\u4E48" \u2192**
14527
+ \u2192 \u529F\u80FD\u5BCC\u96C6\u5206\u6790\uFF0C\u4F7F\u7528 functional-enrichment-from-degs \u5DE5\u4F5C\u6D41
14528
+
14529
+ **\u7528\u6237\u8BF4 "\u5206\u6790\u5355\u7EC6\u80DE\u6570\u636E" / "10X\u6570\u636E\u5206\u6790" \u2192**
14530
+ \u5148\u95EE\uFF1APython \u8FD8\u662F R\uFF1F\u2192 Scanpy \u6216 Seurat
14531
+
14532
+ **\u7528\u6237\u8BF4 "\u753B\u751F\u5B58\u66F2\u7EBF" / "\u5206\u6790\u60A3\u8005\u9884\u540E" / "OS/PFS \u5206\u6790" \u2192**
14533
+ \u2192 \u751F\u5B58\u5206\u6790\uFF0C\u4F7F\u7528 survival-analysis-clinical \u5DE5\u4F5C\u6D41
14534
+
14535
+ **\u7528\u6237\u8BF4 "\u5E2E\u6211\u5206\u6790 GSE12345" / "\u4E0B\u8F7D GEO \u6570\u636E" \u2192**
14536
+ \u2192 \u7ACB\u5373\u8C03\u7528 fetch_geo("GSE12345") \u83B7\u53D6\u5143\u6570\u636E\u548C\u4E0B\u8F7D\u4EE3\u7801\uFF0C\u65E0\u9700\u8BA9\u7528\u6237\u624B\u52A8\u4E0B\u8F7D`;
14204
14537
  }
14205
14538
 
14206
14539
  // src/skillRouter.ts
@@ -14225,8 +14558,11 @@ var SKILL_ROUTES = [
14225
14558
  category: "\u8F6C\u5F55\u7EC4",
14226
14559
  tag: "workflow",
14227
14560
  keywords: [
14561
+ // exact tool names
14228
14562
  "deseq2",
14229
14563
  "edger",
14564
+ "limma-voom",
14565
+ // explicit analysis terms
14230
14566
  "rna-seq\u5DEE\u5F02",
14231
14567
  "rnaseq\u5DEE\u5F02",
14232
14568
  "\u5DEE\u5F02\u8868\u8FBE\u5206\u6790",
@@ -14234,7 +14570,21 @@ var SKILL_ROUTES = [
14234
14570
  "count\u77E9\u9635",
14235
14571
  "count matrix",
14236
14572
  "\u539F\u59CBcounts",
14237
- "raw counts"
14573
+ "raw counts",
14574
+ // natural-language synonyms (the main gap in the original)
14575
+ "\u54EA\u4E9B\u57FA\u56E0\u8868\u8FBE\u91CF\u9AD8",
14576
+ "\u54EA\u4E9B\u57FA\u56E0\u4E0A\u8C03",
14577
+ "\u54EA\u4E9B\u57FA\u56E0\u4E0B\u8C03",
14578
+ "\u57FA\u56E0\u8868\u8FBE\u5DEE\u5F02",
14579
+ "\u8F6C\u5F55\u7EC4\u5DEE\u5F02",
14580
+ "\u8868\u8FBE\u91CF\u5DEE\u5F02",
14581
+ "\u4E0A\u8C03\u57FA\u56E0",
14582
+ "\u4E0B\u8C03\u57FA\u56E0",
14583
+ "deg\u5206\u6790",
14584
+ "differentially expressed",
14585
+ "\u5DEE\u5F02\u5206\u6790",
14586
+ "\u6BD4\u8F83\u4E24\u7EC4\u57FA\u56E0\u8868\u8FBE",
14587
+ "\u80BF\u7624vs\u6B63\u5E38"
14238
14588
  ]
14239
14589
  },
14240
14590
  {
@@ -14250,7 +14600,16 @@ var SKILL_ROUTES = [
14250
14600
  "hdbscan",
14251
14601
  "\u6837\u672C\u805A\u7C7B",
14252
14602
  "\u7279\u5F81\u805A\u7C7B",
14253
- "omics clustering"
14603
+ "omics clustering",
14604
+ // natural-language synonyms
14605
+ "\u6837\u672C\u5206\u7EC4",
14606
+ "\u6837\u672C\u5206\u7C7B",
14607
+ "\u805A\u7C7B\u5206\u6790",
14608
+ "\u65E0\u76D1\u7763\u805A\u7C7B",
14609
+ "\u70ED\u56FE\u805A\u7C7B",
14610
+ "pca\u5206\u6790",
14611
+ "\u4E3B\u6210\u5206\u5206\u6790",
14612
+ "umap\u964D\u7EF4"
14254
14613
  ]
14255
14614
  },
14256
14615
  {
@@ -14266,7 +14625,16 @@ var SKILL_ROUTES = [
14266
14625
  "10x chromium",
14267
14626
  "leiden\u805A\u7C7B",
14268
14627
  "python\u5355\u7EC6\u80DE",
14269
- "anndata\u5206\u6790"
14628
+ "anndata\u5206\u6790",
14629
+ // natural-language synonyms
14630
+ "\u5355\u7EC6\u80DE\u6D4B\u5E8F",
14631
+ "\u5355\u7EC6\u80DE\u5206\u6790",
14632
+ "\u5355\u7EC6\u80DE\u6570\u636E",
14633
+ "10x\u6570\u636E",
14634
+ "\u7EC6\u80DE\u805A\u7C7B",
14635
+ "\u7EC6\u80DE\u7C7B\u578B\u9274\u5B9A",
14636
+ "scrna",
14637
+ "sc-rna"
14270
14638
  ]
14271
14639
  },
14272
14640
  {
@@ -14280,7 +14648,11 @@ var SKILL_ROUTES = [
14280
14648
  "findclusters",
14281
14649
  "findneighbors",
14282
14650
  "sctransform",
14283
- "r\u5355\u7EC6\u80DE\u5206\u6790"
14651
+ "r\u5355\u7EC6\u80DE\u5206\u6790",
14652
+ // natural-language synonyms
14653
+ "r\u505A\u5355\u7EC6\u80DE",
14654
+ "seurat\u5206\u6790",
14655
+ "\u7528r\u5206\u6790\u5355\u7EC6\u80DE"
14284
14656
  ]
14285
14657
  },
14286
14658
  {
@@ -14296,7 +14668,13 @@ var SKILL_ROUTES = [
14296
14668
  "spatial deconvolution",
14297
14669
  "\u914D\u4F53\u53D7\u4F53\u5206\u6790",
14298
14670
  "\u7A7A\u95F4\u57FA\u56E0\u8868\u8FBE",
14299
- "stereo-seq"
14671
+ "stereo-seq",
14672
+ // natural-language synonyms
14673
+ "\u7A7A\u95F4\u7EC4\u5B66",
14674
+ "\u7EC4\u7EC7\u5207\u7247\u6D4B\u5E8F",
14675
+ "\u7A7A\u95F4\u5206\u8FA8\u7387\u8F6C\u5F55\u7EC4",
14676
+ "\u7EC6\u80DE\u7A7A\u95F4\u5206\u5E03",
14677
+ "\u7A7A\u95F4\u5355\u7EC6\u80DE"
14300
14678
  ]
14301
14679
  },
14302
14680
  {
@@ -14310,7 +14688,12 @@ var SKILL_ROUTES = [
14310
14688
  "coexpression network",
14311
14689
  "\u57FA\u56E0\u5171\u8868\u8FBE\u6A21\u5757",
14312
14690
  "weighted gene coexpression",
14313
- "\u4E0E\u8868\u578B\u76F8\u5173\u7684\u57FA\u56E0\u6A21\u5757"
14691
+ "\u4E0E\u8868\u578B\u76F8\u5173\u7684\u57FA\u56E0\u6A21\u5757",
14692
+ // natural-language synonyms
14693
+ "\u57FA\u56E0\u6A21\u5757",
14694
+ "\u5171\u8868\u8FBE",
14695
+ "\u57FA\u56E0\u7F51\u7EDC",
14696
+ "\u57FA\u56E0\u76F8\u5173\u6027\u7F51\u7EDC"
14314
14697
  ]
14315
14698
  },
14316
14699
  {
@@ -14330,7 +14713,19 @@ var SKILL_ROUTES = [
14330
14713
  "functional enrichment",
14331
14714
  "\u529F\u80FD\u5BCC\u96C6",
14332
14715
  "deg\u5BCC\u96C6",
14333
- "\u5DEE\u5F02\u57FA\u56E0\u901A\u8DEF"
14716
+ "\u5DEE\u5F02\u57FA\u56E0\u901A\u8DEF",
14717
+ // natural-language synonyms
14718
+ "\u8FD9\u4E9B\u57FA\u56E0\u53C2\u4E0E\u4EC0\u4E48\u901A\u8DEF",
14719
+ "\u57FA\u56E0\u529F\u80FD\u6CE8\u91CA",
14720
+ "\u4FE1\u53F7\u901A\u8DEF",
14721
+ "\u751F\u7269\u5B66\u8FC7\u7A0B",
14722
+ "\u5206\u5B50\u529F\u80FD",
14723
+ "\u7EC6\u80DE\u7EC4\u5206",
14724
+ "go\u5BCC\u96C6",
14725
+ "\u901A\u8DEF\u5BCC\u96C6",
14726
+ "\u57FA\u56E0\u96C6\u5206\u6790",
14727
+ "ora\u5206\u6790",
14728
+ "gsea\u5206\u6790"
14334
14729
  ]
14335
14730
  },
14336
14731
  {
@@ -14345,7 +14740,12 @@ var SKILL_ROUTES = [
14345
14740
  "gene regulatory network",
14346
14741
  "\u8F6C\u5F55\u56E0\u5B50\u8C03\u63A7\u5B50",
14347
14742
  "tf regulon",
14348
- "grn\u63A8\u65AD"
14743
+ "grn\u63A8\u65AD",
14744
+ // natural-language synonyms
14745
+ "\u8F6C\u5F55\u56E0\u5B50\u5206\u6790",
14746
+ "\u8F6C\u5F55\u56E0\u5B50\u9776\u57FA\u56E0",
14747
+ "tf\u6D3B\u6027",
14748
+ "\u8C03\u63A7\u7F51\u7EDC\u63A8\u65AD"
14349
14749
  ]
14350
14750
  },
14351
14751
  // ── Genomics ──────────────────────────────────────────────────────────────────
@@ -14364,7 +14764,14 @@ var SKILL_ROUTES = [
14364
14764
  "annovar",
14365
14765
  "\u53D8\u5F02\u81F4\u75C5\u6027\u9884\u6D4B",
14366
14766
  "\u53D8\u5F02\u529F\u80FD\u9884\u6D4B",
14367
- "clinvar\u6CE8\u91CA"
14767
+ "clinvar\u6CE8\u91CA",
14768
+ // natural-language synonyms
14769
+ "\u7A81\u53D8\u6CE8\u91CA",
14770
+ "\u53D8\u5F02\u89E3\u8BFB",
14771
+ "vcf\u6587\u4EF6\u5206\u6790",
14772
+ "\u9057\u4F20\u53D8\u5F02\u89E3\u6790",
14773
+ "\u81F4\u75C5\u6027\u8BC4\u4F30",
14774
+ "\u53D8\u5F02\u5F71\u54CD\u9884\u6D4B"
14368
14775
  ]
14369
14776
  },
14370
14777
  {
@@ -14380,7 +14787,14 @@ var SKILL_ROUTES = [
14380
14787
  "\u5168\u57FA\u56E0\u7EC4\u5173\u8054\u5206\u6790",
14381
14788
  "genome-wide association",
14382
14789
  "\u56E0\u679C\u57FA\u56E0\u9274\u5B9A",
14383
- "qtl\u6574\u5408"
14790
+ "qtl\u6574\u5408",
14791
+ // natural-language synonyms
14792
+ "gwas\u7ED3\u679C\u89E3\u8BFB",
14793
+ "gwas\u529F\u80FD\u6CE8\u91CA",
14794
+ "\u5173\u8054\u4F4D\u70B9\u57FA\u56E0",
14795
+ "snp\u529F\u80FD",
14796
+ "gwas\u4FE1\u53F7",
14797
+ "\u9057\u4F20\u5173\u8054\u5206\u6790"
14384
14798
  ]
14385
14799
  },
14386
14800
  {
@@ -14396,7 +14810,12 @@ var SKILL_ROUTES = [
14396
14810
  "ivw\u65B9\u6CD5",
14397
14811
  "mr-egger",
14398
14812
  "\u53CC\u6837\u672Cmr",
14399
- "\u5DE5\u5177\u53D8\u91CFiv"
14813
+ "\u5DE5\u5177\u53D8\u91CFiv",
14814
+ // natural-language synonyms
14815
+ "\u56E0\u679C\u63A8\u65AD",
14816
+ "\u66B4\u9732\u4E0E\u7ED3\u5C40",
14817
+ "\u9057\u4F20\u5DE5\u5177\u53D8\u91CF",
14818
+ "mr\u5206\u6790"
14400
14819
  ]
14401
14820
  },
14402
14821
  {
@@ -14410,7 +14829,11 @@ var SKILL_ROUTES = [
14410
14829
  "\u591A\u57FA\u56E0\u98CE\u9669\u8BC4\u5206",
14411
14830
  "prs-cs",
14412
14831
  "\u9057\u4F20\u98CE\u9669\u9884\u6D4B",
14413
- "prs\u8BA1\u7B97"
14832
+ "prs\u8BA1\u7B97",
14833
+ // natural-language synonyms
14834
+ "\u9057\u4F20\u98CE\u9669\u8BC4\u5206",
14835
+ "\u591A\u57FA\u56E0\u8BC4\u5206",
14836
+ "\u75BE\u75C5\u9057\u4F20\u98CE\u9669"
14414
14837
  ]
14415
14838
  },
14416
14839
  {
@@ -14425,7 +14848,13 @@ var SKILL_ROUTES = [
14425
14848
  "bagel2",
14426
14849
  "sgrna\u7B5B\u9009",
14427
14850
  "pooled crispr",
14428
- "crispr hit\u8BC6\u522B"
14851
+ "crispr hit\u8BC6\u522B",
14852
+ // natural-language synonyms
14853
+ "crispr\u7B5B\u9009",
14854
+ "\u529F\u80FD\u57FA\u56E0\u7EC4\u7B5B\u9009",
14855
+ "\u5FC5\u9700\u57FA\u56E0\u7B5B\u9009",
14856
+ "crispr\u6572\u9664\u7B5B\u9009",
14857
+ "crispr\u6587\u5E93"
14429
14858
  ]
14430
14859
  },
14431
14860
  // ── Epigenomics ───────────────────────────────────────────────────────────────
@@ -14439,7 +14868,13 @@ var SKILL_ROUTES = [
14439
14868
  "chip-seq\u5CF0\u503C\u5BCC\u96C6",
14440
14869
  "peak enrichment chip",
14441
14870
  "chip atlas\u6570\u636E\u5E93",
14442
- "histone chip\u5206\u6790"
14871
+ "histone chip\u5206\u6790",
14872
+ // natural-language synonyms
14873
+ "chip-seq\u5206\u6790",
14874
+ "chip\u6570\u636E",
14875
+ "\u7EC4\u86CB\u767D\u4FEE\u9970",
14876
+ "h3k27ac",
14877
+ "h3k4me3"
14443
14878
  ]
14444
14879
  },
14445
14880
  {
@@ -14452,7 +14887,11 @@ var SKILL_ROUTES = [
14452
14887
  "differential binding",
14453
14888
  "\u5DEE\u5F02chip-seq",
14454
14889
  "differential peak",
14455
- "chip-seq\u6761\u4EF6\u6BD4\u8F83"
14890
+ "chip-seq\u6761\u4EF6\u6BD4\u8F83",
14891
+ // natural-language synonyms
14892
+ "\u5DEE\u5F02\u5CF0",
14893
+ "\u5DEE\u5F02\u7ED3\u5408\u4F4D\u70B9",
14894
+ "chip-seq\u5DEE\u5F02"
14456
14895
  ]
14457
14896
  },
14458
14897
  {
@@ -14466,7 +14905,12 @@ var SKILL_ROUTES = [
14466
14905
  "\u8F6C\u5F55\u56E0\u5B50\u9776\u57FA\u56E0chip",
14467
14906
  "tf\u9776\u57FA\u56E0",
14468
14907
  "peak annotation\u9776\u57FA\u56E0",
14469
- "chip-seq peak\u6CE8\u91CA"
14908
+ "chip-seq peak\u6CE8\u91CA",
14909
+ // natural-language synonyms
14910
+ "peak\u6CE8\u91CA",
14911
+ "\u5CF0\u503C\u6CE8\u91CA",
14912
+ "\u8F6C\u5F55\u56E0\u5B50\u7ED3\u5408\u4F4D\u70B9",
14913
+ "chip\u9776\u70B9"
14470
14914
  ]
14471
14915
  },
14472
14916
  // ── Clinical ──────────────────────────────────────────────────────────────────
@@ -14480,7 +14924,12 @@ var SKILL_ROUTES = [
14480
14924
  "clinical trial landscape",
14481
14925
  "ct.gov\u6570\u636E\u5206\u6790",
14482
14926
  "\u4E34\u5E8A\u8BD5\u9A8C\u683C\u5C40",
14483
- "\u4E34\u5E8A\u7814\u7A76\u5206\u6790"
14927
+ "\u4E34\u5E8A\u7814\u7A76\u5206\u6790",
14928
+ // natural-language synonyms
14929
+ "\u4E34\u5E8A\u8BD5\u9A8C",
14930
+ "\u5728\u7814\u836F\u7269",
14931
+ "\u4E34\u5E8A\u7BA1\u7EBF",
14932
+ "\u4E34\u5E8A\u7814\u7A76\u73B0\u72B6"
14484
14933
  ]
14485
14934
  },
14486
14935
  {
@@ -14493,7 +14942,12 @@ var SKILL_ROUTES = [
14493
14942
  "preclinical literature",
14494
14943
  "\u7CFB\u7EDF\u6587\u732E\u63D0\u53D6",
14495
14944
  "literature extraction",
14496
- "\u6587\u732E\u7CFB\u7EDF\u7EFC\u5408"
14945
+ "\u6587\u732E\u7CFB\u7EDF\u7EFC\u5408",
14946
+ // natural-language synonyms
14947
+ "\u6587\u732E\u7EFC\u8FF0",
14948
+ "\u7CFB\u7EDF\u7EFC\u8FF0",
14949
+ "\u6587\u732E\u6574\u7406",
14950
+ "\u6587\u732E\u6316\u6398"
14497
14951
  ]
14498
14952
  },
14499
14953
  {
@@ -14510,7 +14964,16 @@ var SKILL_ROUTES = [
14510
14964
  "\u5B9E\u9A8C\u8BBE\u8BA1\u7EDF\u8BA1",
14511
14965
  "\u5047\u8BBE\u68C0\u9A8C\u9009\u62E9",
14512
14966
  "t\u68C0\u9A8C\u8FD8\u662F",
14513
- "anova\u65B9\u5DEE\u5206\u6790"
14967
+ "anova\u65B9\u5DEE\u5206\u6790",
14968
+ // natural-language synonyms
14969
+ "\u7528\u4EC0\u4E48\u7EDF\u8BA1\u65B9\u6CD5",
14970
+ "\u7EDF\u8BA1\u663E\u8457\u6027",
14971
+ "\u68C0\u9A8C\u65B9\u6CD5",
14972
+ "\u9700\u8981\u591A\u5C11\u6837\u672C",
14973
+ "\u529F\u6548\u5206\u6790",
14974
+ "\u7EDF\u8BA1\u529F\u6548",
14975
+ "p\u503C",
14976
+ "\u663E\u8457\u6027\u68C0\u9A8C"
14514
14977
  ]
14515
14978
  },
14516
14979
  {
@@ -14524,7 +14987,14 @@ var SKILL_ROUTES = [
14524
14987
  "biomarker panel\u7B5B\u9009",
14525
14988
  "\u6700\u5C0F\u6807\u5FD7\u7269\u9762\u677F",
14526
14989
  "feature selection lasso",
14527
- "\u8BCA\u65AD\u6807\u5FD7\u7269\u7B5B\u9009"
14990
+ "\u8BCA\u65AD\u6807\u5FD7\u7269\u7B5B\u9009",
14991
+ // natural-language synonyms
14992
+ "\u751F\u7269\u6807\u5FD7\u7269\u7B5B\u9009",
14993
+ "\u6807\u5FD7\u7269\u7EC4\u5408",
14994
+ "\u8BCA\u65AD\u6A21\u578B",
14995
+ "\u9884\u6D4B\u6A21\u578B\u7279\u5F81\u7B5B\u9009",
14996
+ "biomarker",
14997
+ "\u7279\u5F81\u9009\u62E9"
14528
14998
  ]
14529
14999
  },
14530
15000
  {
@@ -14540,7 +15010,64 @@ var SKILL_ROUTES = [
14540
15010
  "primer3",
14541
15011
  "qrt-pcr\u8BBE\u8BA1",
14542
15012
  "\u6269\u589E\u5B50\u8BBE\u8BA1",
14543
- "\u5F15\u7269\u7279\u5F02\u6027\u9A8C\u8BC1"
15013
+ "\u5F15\u7269\u7279\u5F02\u6027\u9A8C\u8BC1",
15014
+ // natural-language synonyms
15015
+ "\u8BBE\u8BA1\u5F15\u7269",
15016
+ "\u6269\u589E\u5F15\u7269",
15017
+ "rt-pcr\u5F15\u7269",
15018
+ "\u5B9A\u91CFpcr"
15019
+ ]
15020
+ },
15021
+ // ── Survival Analysis ─────────────────────────────────────────────────────────
15022
+ {
15023
+ id: "survival-analysis-clinical",
15024
+ name: "\u4E34\u5E8A\u751F\u5B58\u5206\u6790 (KM + Cox)",
15025
+ category: "\u4E34\u5E8A",
15026
+ tag: "workflow",
15027
+ keywords: [
15028
+ // exact method names
15029
+ "kaplan-meier",
15030
+ "kaplan meier",
15031
+ "km\u66F2\u7EBF",
15032
+ "cox\u56DE\u5F52",
15033
+ "cox regression",
15034
+ "\u751F\u5B58\u5206\u6790",
15035
+ "survival analysis",
15036
+ "\u751F\u5B58\u66F2\u7EBF",
15037
+ "log-rank",
15038
+ // outcome types
15039
+ "\u603B\u751F\u5B58\u671F",
15040
+ "overall survival",
15041
+ "os\u5206\u6790",
15042
+ "\u65E0\u8FDB\u5C55\u751F\u5B58",
15043
+ "progression-free survival",
15044
+ "pfs\u5206\u6790",
15045
+ "\u65E0\u75C5\u751F\u5B58",
15046
+ "disease-free survival",
15047
+ "dfs\u5206\u6790",
15048
+ "\u590D\u53D1\u751F\u5B58",
15049
+ "relapse-free survival",
15050
+ "rfs\u5206\u6790",
15051
+ // natural-language synonyms
15052
+ "\u60A3\u8005\u9884\u540E",
15053
+ "\u9884\u540E\u5206\u6790",
15054
+ "\u751F\u5B58\u9884\u540E",
15055
+ "\u4E34\u5E8A\u9884\u540E",
15056
+ "\u5220\u5931\u6570\u636E",
15057
+ "\u53F3\u5220\u5931",
15058
+ "censored data",
15059
+ "\u98CE\u9669\u6BD4",
15060
+ "hazard ratio",
15061
+ "hr\u503C",
15062
+ "\u7ADE\u4E89\u98CE\u9669",
15063
+ "competing risk",
15064
+ "fine-gray",
15065
+ "\u4E2D\u4F4D\u751F\u5B58\u65F6\u95F4",
15066
+ "5\u5E74\u751F\u5B58\u7387",
15067
+ "3\u5E74\u751F\u5B58\u7387",
15068
+ "\u9AD8\u8868\u8FBE\u9884\u540E\u5DEE",
15069
+ "\u57FA\u56E0\u8868\u8FBE\u4E0E\u9884\u540E",
15070
+ "\u7A81\u53D8\u4E0E\u9884\u540E"
14544
15071
  ]
14545
15072
  },
14546
15073
  // ── OpenClaw Key Skills ────────────────────────────────────────────────────────
@@ -14743,36 +15270,166 @@ function routeSkill(message) {
14743
15270
  let score = 0;
14744
15271
  for (const kw of route.keywords) {
14745
15272
  if (lower.includes(kw.toLowerCase())) {
14746
- score += kw.length;
15273
+ score += 1 + kw.length * 0.1;
14747
15274
  }
14748
15275
  }
14749
15276
  if (score > 0) scores.set(route.id, { route, score });
14750
15277
  }
14751
- const sorted = Array.from(scores.values()).sort((a2, b2) => b2.score - a2.score).slice(0, 3);
15278
+ const sorted = Array.from(scores.values()).sort((a2, b2) => b2.score - a2.score).slice(0, 5);
14752
15279
  return {
14753
15280
  routes: sorted.map((v2) => v2.route),
14754
15281
  topScore: sorted[0]?.score ?? 0
14755
15282
  };
14756
15283
  }
14757
15284
 
15285
+ // src/sessions.ts
15286
+ var import_fs4 = require("fs");
15287
+ var import_path4 = require("path");
15288
+ var SESSIONS_DIR = (0, import_path4.join)(BGI_DIR, "sessions");
15289
+ var CHECKPOINTS_DIR = (0, import_path4.join)(BGI_DIR, "checkpoints");
15290
+ function ensureSessionDirs() {
15291
+ for (const d2 of [SESSIONS_DIR, CHECKPOINTS_DIR]) {
15292
+ if (!(0, import_fs4.existsSync)(d2)) (0, import_fs4.mkdirSync)(d2, { recursive: true });
15293
+ }
15294
+ }
15295
+ function sessionPath(id) {
15296
+ return (0, import_path4.join)(SESSIONS_DIR, `${id}.json`);
15297
+ }
15298
+ function newSessionId() {
15299
+ const now = /* @__PURE__ */ new Date();
15300
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
15301
+ const rand = Math.random().toString(36).slice(2, 6);
15302
+ return `${date}-${rand}`;
15303
+ }
15304
+ function saveSession(id, name, messages, skills, createdAt) {
15305
+ ensureSessionDirs();
15306
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15307
+ const firstUser = messages.find((m2) => m2.role === "user");
15308
+ const rawPreview = typeof firstUser?.content === "string" ? firstUser.content : "";
15309
+ const preview = rawPreview.replace(/\[Skill 已加载[^\]]*\][\s\S]*/, "").trim().slice(0, 80);
15310
+ const session = {
15311
+ id,
15312
+ name,
15313
+ createdAt,
15314
+ updatedAt: now,
15315
+ messageCount: messages.length,
15316
+ skills,
15317
+ preview,
15318
+ messages
15319
+ };
15320
+ (0, import_fs4.writeFileSync)(sessionPath(id), JSON.stringify(session, null, 2), "utf8");
15321
+ }
15322
+ function loadSession(id) {
15323
+ ensureSessionDirs();
15324
+ const p2 = sessionPath(id);
15325
+ if (!(0, import_fs4.existsSync)(p2)) return null;
15326
+ try {
15327
+ return JSON.parse((0, import_fs4.readFileSync)(p2, "utf8"));
15328
+ } catch {
15329
+ return null;
15330
+ }
15331
+ }
15332
+ function listSessions() {
15333
+ ensureSessionDirs();
15334
+ const files = (0, import_fs4.readdirSync)(SESSIONS_DIR).filter((f2) => f2.endsWith(".json"));
15335
+ const metas = [];
15336
+ for (const f2 of files) {
15337
+ try {
15338
+ const raw = JSON.parse((0, import_fs4.readFileSync)((0, import_path4.join)(SESSIONS_DIR, f2), "utf8"));
15339
+ metas.push({
15340
+ id: raw.id,
15341
+ name: raw.name,
15342
+ createdAt: raw.createdAt,
15343
+ updatedAt: raw.updatedAt,
15344
+ messageCount: raw.messageCount,
15345
+ skills: raw.skills ?? [],
15346
+ preview: raw.preview ?? ""
15347
+ });
15348
+ } catch {
15349
+ }
15350
+ }
15351
+ return metas.sort((a2, b2) => b2.updatedAt.localeCompare(a2.updatedAt));
15352
+ }
15353
+ function deleteSession(id) {
15354
+ const p2 = sessionPath(id);
15355
+ if (!(0, import_fs4.existsSync)(p2)) return false;
15356
+ (0, import_fs4.unlinkSync)(p2);
15357
+ return true;
15358
+ }
15359
+ function getLastSession() {
15360
+ const all = listSessions();
15361
+ return all[0] ?? null;
15362
+ }
15363
+ function checkpointPath(id) {
15364
+ return (0, import_path4.join)(CHECKPOINTS_DIR, `${id}.json`);
15365
+ }
15366
+ function saveCheckpoint(sessionId, label, messages, skills) {
15367
+ ensureSessionDirs();
15368
+ const id = `${sessionId}-cp${Date.now()}`;
15369
+ const cp = {
15370
+ id,
15371
+ sessionId,
15372
+ label,
15373
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
15374
+ messageCount: messages.length,
15375
+ messages,
15376
+ skills
15377
+ };
15378
+ (0, import_fs4.writeFileSync)(checkpointPath(id), JSON.stringify(cp, null, 2), "utf8");
15379
+ return id;
15380
+ }
15381
+ function listCheckpoints(sessionId) {
15382
+ ensureSessionDirs();
15383
+ const files = (0, import_fs4.readdirSync)(CHECKPOINTS_DIR).filter((f2) => f2.endsWith(".json"));
15384
+ const cps = [];
15385
+ for (const f2 of files) {
15386
+ try {
15387
+ const cp = JSON.parse((0, import_fs4.readFileSync)((0, import_path4.join)(CHECKPOINTS_DIR, f2), "utf8"));
15388
+ if (!sessionId || cp.sessionId === sessionId) cps.push(cp);
15389
+ } catch {
15390
+ }
15391
+ }
15392
+ return cps.sort((a2, b2) => b2.createdAt.localeCompare(a2.createdAt));
15393
+ }
15394
+ function deleteCheckpoint(id) {
15395
+ const p2 = checkpointPath(id);
15396
+ if (!(0, import_fs4.existsSync)(p2)) return false;
15397
+ (0, import_fs4.unlinkSync)(p2);
15398
+ return true;
15399
+ }
15400
+ function clearCheckpoints(sessionId) {
15401
+ const cps = listCheckpoints(sessionId);
15402
+ let count = 0;
15403
+ for (const cp of cps) {
15404
+ if (deleteCheckpoint(cp.id)) count++;
15405
+ }
15406
+ return count;
15407
+ }
15408
+
14758
15409
  // src/index.ts
14759
- var VERSION2 = "2.2.7";
15410
+ var import_fs6 = require("fs");
15411
+ var VERSION2 = "2.2.9";
15412
+ var SESSION_CTX = {
15413
+ id: "",
15414
+ createdAt: "",
15415
+ wdirSnapshot: null
15416
+ };
14760
15417
  function installBundledData() {
14761
- const bundledData = (0, import_path4.join)(__dirname, "..", "data");
14762
- if (!(0, import_fs4.existsSync)(bundledData)) return;
15418
+ const bundledData = (0, import_path5.join)(__dirname, "..", "data");
15419
+ if (!(0, import_fs5.existsSync)(bundledData)) return;
14763
15420
  ensureDirs();
14764
15421
  const targets = [
14765
- { src: (0, import_path4.join)(bundledData, "workflows"), dest: WORKFLOWS_DIR, name: "Skills (\u751F\u4FE1\u5DE5\u4F5C\u6D41)" },
14766
- { src: (0, import_path4.join)(bundledData, "skills"), dest: SKILLS_DIR, name: "Skills (\u533B\u5B66\u4E13\u79D1)" },
14767
- { src: (0, import_path4.join)(bundledData, "tools"), dest: TOOLS_DIR, name: "\u5DE5\u5177" }
15422
+ { src: (0, import_path5.join)(bundledData, "workflows"), dest: WORKFLOWS_DIR, name: "Skills (\u751F\u4FE1\u5DE5\u4F5C\u6D41)" },
15423
+ { src: (0, import_path5.join)(bundledData, "skills"), dest: SKILLS_DIR, name: "Skills (\u533B\u5B66\u4E13\u79D1)" },
15424
+ { src: (0, import_path5.join)(bundledData, "tools"), dest: TOOLS_DIR, name: "\u5DE5\u5177" }
14768
15425
  ];
14769
15426
  let installed = false;
14770
15427
  for (const { src, dest, name } of targets) {
14771
- if (!(0, import_fs4.existsSync)(src)) continue;
14772
- const isEmpty = !(0, import_fs4.existsSync)(dest) || (0, import_fs4.readdirSync)(dest).length === 0;
15428
+ if (!(0, import_fs5.existsSync)(src)) continue;
15429
+ const isEmpty = !(0, import_fs5.existsSync)(dest) || (0, import_fs5.readdirSync)(dest).length === 0;
14773
15430
  if (isEmpty) {
14774
- (0, import_fs4.mkdirSync)(dest, { recursive: true });
14775
- (0, import_fs4.cpSync)(src, dest, { recursive: true });
15431
+ (0, import_fs5.mkdirSync)(dest, { recursive: true });
15432
+ (0, import_fs5.cpSync)(src, dest, { recursive: true });
14776
15433
  if (!installed) {
14777
15434
  process.stdout.write(source_default.dim("\u6B63\u5728\u521D\u59CB\u5316\u5185\u7F6E\u6570\u636E...\n"));
14778
15435
  installed = true;
@@ -14807,22 +15464,49 @@ function printHelp() {
14807
15464
  console.log(` ${source_default.cyan("/clear")} \u6E05\u7A7A\u5BF9\u8BDD\u5386\u53F2`);
14808
15465
  console.log(` ${source_default.cyan("/history")} \u67E5\u770B\u5BF9\u8BDD\u7EDF\u8BA1\uFF08\u8F6E\u6B21 / Token \u4F30\u7B97\uFF09`);
14809
15466
  console.log(` ${source_default.cyan("/compact")} \u7ACB\u5373\u538B\u7F29\u5BF9\u8BDD\u5386\u53F2\uFF08\u8D85 60k token \u81EA\u52A8\u89E6\u53D1\uFF09`);
14810
- console.log(` ${source_default.cyan("/save")} [\u6587\u4EF6\u540D] \u4FDD\u5B58\u5BF9\u8BDD\u4E3A Markdown \u6587\u4EF6`);
15467
+ console.log(` ${source_default.cyan("/save")} [\u540D\u79F0] \u4FDD\u5B58\u5BF9\u8BDD\u4E3A Markdown \u6587\u4EF6`);
14811
15468
  console.log(` ${source_default.cyan("/think")} [on|off] \u5207\u6362\u601D\u8003\u6A21\u5F0F (Qwen3 /think \u524D\u7F00)`);
14812
15469
  console.log();
15470
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u4F1A\u8BDD\u6301\u4E45\u5316 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15471
+ console.log(` ${source_default.cyan("/sessions")} \u5217\u51FA\u5386\u53F2\u4F1A\u8BDD`);
15472
+ console.log(` ${source_default.cyan("/resume")} [id] \u6062\u590D\u4E0A\u6B21\uFF08\u6216\u6307\u5B9A\uFF09\u4F1A\u8BDD`);
15473
+ console.log(` ${source_default.cyan("/session-save")} [\u540D\u79F0] \u624B\u52A8\u547D\u540D\u4FDD\u5B58\u5F53\u524D\u4F1A\u8BDD`);
15474
+ console.log(` ${source_default.cyan("/session-del")} <id> \u5220\u9664\u6307\u5B9A\u4F1A\u8BDD`);
15475
+ console.log();
15476
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u65AD\u70B9\u7EED\u4F20 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15477
+ console.log(` ${source_default.cyan("/checkpoint")} \u4FDD\u5B58\u5F53\u524D\u5BF9\u8BDD\u65AD\u70B9`);
15478
+ console.log(` ${source_default.cyan("/checkpoint list")} \u5217\u51FA\u5F53\u524D\u4F1A\u8BDD\u6240\u6709\u65AD\u70B9`);
15479
+ console.log(` ${source_default.cyan("/checkpoint restore")} <id> \u6062\u590D\u5230\u6307\u5B9A\u65AD\u70B9`);
15480
+ console.log(` ${source_default.cyan("/checkpoint clear")} \u6E05\u9664\u5F53\u524D\u4F1A\u8BDD\u6240\u6709\u65AD\u70B9`);
15481
+ console.log();
14813
15482
  console.log(source_default.bold.cyan("\u2500\u2500\u2500 Skills \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
14814
15483
  console.log(` ${source_default.cyan("/cat")} \u6309\u9886\u57DF\u6D4F\u89C8 Skills \u5206\u7C7B\u76EE\u5F55`);
14815
15484
  console.log(` ${source_default.cyan("/sk")} \u5217\u51FA\u5168\u90E8 Skills`);
14816
15485
  console.log(` ${source_default.cyan("/sk")} <\u5173\u952E\u8BCD> \u6A21\u7CCA\u641C\u7D22\uFF0C\u5339\u914D\u5219\u6CE8\u5165\uFF0C\u5426\u5219\u5217\u51FA\u5019\u9009`);
14817
15486
  console.log(` ${source_default.cyan("/wf")} \u540C /sk\uFF0C\u522B\u540D`);
14818
- console.log(source_default.dim(" \u793A\u4F8B: /cat /sk deseq2 /sk pubmed /sk alphafold /sk crispr"));
14819
- console.log(source_default.dim(" \u63D0\u793A: \u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\uFF0CAI \u4F1A\u81EA\u52A8\u8BC6\u522B\u5E76\u6FC0\u6D3B\u5BF9\u5E94\u6280\u80FD"));
15487
+ console.log(` ${source_default.cyan("/skills")} \u67E5\u770B\u5F53\u524D\u4F1A\u8BDD\u5DF2\u52A0\u8F7D\u7684 Skills`);
15488
+ console.log(` ${source_default.cyan("/unload")} <id> \u4ECE\u5F53\u524D\u4F1A\u8BDD\u5378\u8F7D\u6307\u5B9A Skill`);
15489
+ console.log(source_default.dim(" \u793A\u4F8B: /cat /sk deseq2 /skills /unload deseq2"));
15490
+ console.log(source_default.dim(" \u63D0\u793A: \u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\uFF0CAI \u4F1A\u81EA\u52A8\u8BC6\u522B\u5E76\u6FC0\u6D3B\u5BF9\u5E94\u6280\u80FD\uFF08\u52A0\u8F7D\u524D\u4F1A\u8BE2\u95EE\u786E\u8BA4\uFF09"));
15491
+ console.log();
15492
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u76F4\u63A5\u6267\u884C \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15493
+ console.log(` ${source_default.cyan("!<\u547D\u4EE4>")} \u7ED5\u8FC7 AI \u76F4\u63A5\u6267\u884C Shell \u547D\u4EE4\uFF08\u5B9E\u65F6\u8F93\u51FA\uFF09`);
15494
+ console.log(source_default.dim(" \u793A\u4F8B: !ls -la !Rscript analysis.R !python script.py !samtools view -h a.bam"));
15495
+ console.log();
15496
+ console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u5DE5\u4F5C\u6D41\u5411\u5BFC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15497
+ console.log(` ${source_default.cyan("/run")} <skill-id> \u4EA4\u4E92\u5F0F\u53C2\u6570\u5411\u5BFC\uFF0C\u81EA\u52A8\u751F\u6210\u5E76\u6267\u884C\u5206\u6790\u811A\u672C`);
15498
+ console.log(` ${source_default.cyan("/check-env")} [id] \u68C0\u6D4B Skill \u6240\u9700 R/Python \u5305\u662F\u5426\u5DF2\u5B89\u88C5`);
15499
+ console.log(` ${source_default.cyan("/install")} <url> \u4ECE GitHub \u5B89\u88C5\u7B2C\u4E09\u65B9 Skill`);
15500
+ console.log(` ${source_default.cyan("/uninstall")} <id> \u5378\u8F7D\u5DF2\u5B89\u88C5\u7684\u7B2C\u4E09\u65B9 Skill`);
14820
15501
  console.log();
14821
15502
  console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u6587\u4EF6 & \u76EE\u5F55 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
14822
15503
  console.log(` ${source_default.cyan("/cd")} <\u8DEF\u5F84> \u66F4\u6539\u5DE5\u4F5C\u76EE\u5F55`);
14823
15504
  console.log(` ${source_default.cyan("/cwd")} \u663E\u793A\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55`);
15505
+ console.log(` ${source_default.cyan("/diff")} \u663E\u793A\u672C\u6B21\u4F1A\u8BDD\u65B0\u589E/\u4FEE\u6539\u7684\u6587\u4EF6`);
14824
15506
  console.log(` ${source_default.cyan("/tools")} \u5217\u51FA AI \u53EF\u8C03\u7528\u7684\u5DE5\u5177`);
14825
15507
  console.log(` ${source_default.cyan("@\u8DEF\u5F84")} \u6D88\u606F\u4E2D\u5185\u5D4C\u6587\u4EF6\u5185\u5BB9 (\u4F8B: @data.csv \u91CC\u6709\u4EC0\u4E48?)`);
15508
+ console.log(` ${source_default.cyan("@\u76EE\u5F55/")} \u5185\u5D4C\u76EE\u5F55\u4E0B\u6240\u6709\u6587\u4EF6\u6458\u8981 (\u4F8B: @results/)`);
15509
+ console.log(` ${source_default.cyan("@*.csv")} \u901A\u914D\u7B26\u5185\u5D4C\u591A\u4E2A\u6587\u4EF6 (\u4F8B: @*.csv @*.tsv)`);
14826
15510
  console.log();
14827
15511
  console.log(source_default.bold.cyan("\u2500\u2500\u2500 \u5176\u4ED6 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
14828
15512
  console.log(` ${source_default.cyan("/help")} \u663E\u793A\u672C\u5E2E\u52A9`);
@@ -14914,10 +15598,10 @@ async function firstRunIfNeeded(rl) {
14914
15598
  function collectAllSkills() {
14915
15599
  const entries = [];
14916
15600
  const addFrom = (dir, tag) => {
14917
- if (!(0, import_fs4.existsSync)(dir)) return;
14918
- (0, import_fs4.readdirSync)(dir).forEach((f2) => {
15601
+ if (!(0, import_fs5.existsSync)(dir)) return;
15602
+ (0, import_fs5.readdirSync)(dir).forEach((f2) => {
14919
15603
  try {
14920
- if ((0, import_fs4.statSync)((0, import_path4.join)(dir, f2)).isDirectory()) entries.push({ id: f2, dir, tag });
15604
+ if ((0, import_fs5.statSync)((0, import_path5.join)(dir, f2)).isDirectory()) entries.push({ id: f2, dir, tag });
14921
15605
  } catch {
14922
15606
  }
14923
15607
  });
@@ -14945,7 +15629,72 @@ function listSkills(keyword) {
14945
15629
  if (matched.length > 50) console.log(source_default.dim(` ... \u8FD8\u6709 ${matched.length - 50} \u4E2A\uFF0C\u8BF7\u7528\u5173\u952E\u8BCD\u7B5B\u9009`));
14946
15630
  console.log();
14947
15631
  }
14948
- function injectSkill(id, history) {
15632
+ async function llmRecommendSkills(userQuery) {
15633
+ const cfg = loadConfig();
15634
+ const prov = PROVIDERS[cfg.provider];
15635
+ if (!prov) return [];
15636
+ const apiKey = cfg.apiKeys[cfg.provider] ?? (prov.envKey ? process.env[prov.envKey] : void 0);
15637
+ const requiresKey = prov.envKey !== "";
15638
+ if (requiresKey && !apiKey) return [];
15639
+ const baseURL = cfg.provider === "custom" ? cfg.customUrl ?? prov.baseURL : prov.baseURL;
15640
+ const model = cfg.provider === "custom" ? cfg.customModel ?? cfg.model : cfg.model;
15641
+ const all = collectAllSkills();
15642
+ const catalogLines = [];
15643
+ for (const entry of all) {
15644
+ const skillPath = (0, import_path5.join)(entry.dir, entry.id, "SKILL.md");
15645
+ if (!(0, import_fs5.existsSync)(skillPath)) continue;
15646
+ const raw = (0, import_fs5.readFileSync)(skillPath, "utf8");
15647
+ const { name, shortDesc } = parseSkillMeta(raw);
15648
+ const displayName = name || entry.id;
15649
+ const desc = shortDesc ? ` \u2014 ${shortDesc}` : "";
15650
+ catalogLines.push(`${entry.id}|${displayName}${desc}`);
15651
+ }
15652
+ if (catalogLines.length === 0) return [];
15653
+ const catalog = catalogLines.join("\n");
15654
+ const systemMsg = `\u4F60\u662F\u4E00\u4E2A\u751F\u7269\u4FE1\u606F\u5B66 Skill \u63A8\u8350\u52A9\u624B\u3002
15655
+ \u7528\u6237\u4F1A\u63CF\u8FF0\u4ED6\u4EEC\u60F3\u505A\u7684\u5206\u6790\u4EFB\u52A1\uFF0C\u4F60\u9700\u8981\u4ECE\u4E0B\u9762\u7684 Skill \u76EE\u5F55\u4E2D\u63A8\u8350\u6700\u76F8\u5173\u7684 5 \u4E2A\uFF08\u6216\u66F4\u5C11\uFF09\u3002
15656
+
15657
+ Skill \u76EE\u5F55\uFF08\u683C\u5F0F: id|\u540D\u79F0 \u2014 \u7B80\u4ECB\uFF09\uFF1A
15658
+ ${catalog}
15659
+
15660
+ \u8BF7\u4E25\u683C\u6309\u7167\u4EE5\u4E0B JSON \u683C\u5F0F\u56DE\u590D\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u5176\u4ED6\u5185\u5BB9\uFF1A
15661
+ [
15662
+ {"id": "skill-id", "name": "Skill \u540D\u79F0", "reason": "\u4E00\u53E5\u8BDD\u8BF4\u660E\u4E3A\u4EC0\u4E48\u63A8\u8350"},
15663
+ ...
15664
+ ]`;
15665
+ try {
15666
+ const client = new openai_default({ apiKey: apiKey || "none", baseURL });
15667
+ const resp = await client.chat.completions.create({
15668
+ model,
15669
+ messages: [
15670
+ { role: "system", content: systemMsg },
15671
+ { role: "user", content: userQuery }
15672
+ ],
15673
+ temperature: 0.2,
15674
+ max_tokens: 800
15675
+ });
15676
+ const text = resp.choices[0]?.message?.content ?? "";
15677
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
15678
+ if (!jsonMatch) return [];
15679
+ const parsed = JSON.parse(jsonMatch[0]);
15680
+ const validIds = new Set(all.map((e2) => e2.id));
15681
+ return parsed.filter((r2) => r2.id && validIds.has(r2.id)).slice(0, 5);
15682
+ } catch {
15683
+ return [];
15684
+ }
15685
+ }
15686
+ function parseSkillMeta(content) {
15687
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/) || content.match(/<!--[\s\S]*?-->\s*([\s\S]*?)(?=\n#|\n\n)/);
15688
+ if (!fmMatch) return { name: "", shortDesc: "" };
15689
+ const fm = fmMatch[1];
15690
+ const nameMatch = fm.match(/^name:\s*['"]?(.+?)['"]?\s*$/m);
15691
+ const descMatch = fm.match(/^short-description:\s*(.+)$/m);
15692
+ return {
15693
+ name: nameMatch?.[1]?.trim() ?? "",
15694
+ shortDesc: descMatch?.[1]?.trim() ?? ""
15695
+ };
15696
+ }
15697
+ async function injectSkill(id, history, injectedSkills, rl, skipConfirm = false) {
14949
15698
  const all = collectAllSkills();
14950
15699
  const match = all.find((e2) => e2.id === id) || all.find((e2) => e2.id.startsWith(id)) || all.find((e2) => e2.id.includes(id));
14951
15700
  if (!match) {
@@ -14953,12 +15702,41 @@ function injectSkill(id, history) {
14953
15702
  console.log(source_default.dim("\u4F7F\u7528 /sk <\u5173\u952E\u8BCD> \u641C\u7D22"));
14954
15703
  return false;
14955
15704
  }
14956
- const skillPath = (0, import_path4.join)(match.dir, match.id, "SKILL.md");
14957
- if (!(0, import_fs4.existsSync)(skillPath)) {
15705
+ const skillPath = (0, import_path5.join)(match.dir, match.id, "SKILL.md");
15706
+ if (!(0, import_fs5.existsSync)(skillPath)) {
14958
15707
  console.log(source_default.red(`${match.id} \u7F3A\u5C11 SKILL.md`));
14959
15708
  return false;
14960
15709
  }
14961
- const content = (0, import_fs4.readFileSync)(skillPath, "utf8");
15710
+ const content = (0, import_fs5.readFileSync)(skillPath, "utf8");
15711
+ const { name, shortDesc } = parseSkillMeta(content);
15712
+ const displayName = name || match.id;
15713
+ if (!skipConfirm) {
15714
+ console.log();
15715
+ console.log(source_default.bold.cyan("\u250C\u2500 \u5373\u5C06\u52A0\u8F7D Skill \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15716
+ console.log(`\u2502 ${source_default.bold("ID:")} ${source_default.cyan(match.id)}`);
15717
+ console.log(`\u2502 ${source_default.bold("\u540D\u79F0:")} ${displayName}`);
15718
+ if (shortDesc) {
15719
+ const words = shortDesc.split(" ");
15720
+ let line = "\u2502 \u529F\u80FD: ";
15721
+ for (const w2 of words) {
15722
+ if (line.length + w2.length > 70) {
15723
+ console.log(source_default.dim(line));
15724
+ line = "\u2502 " + w2 + " ";
15725
+ } else {
15726
+ line += w2 + " ";
15727
+ }
15728
+ }
15729
+ if (line.trim() !== "\u2502") console.log(source_default.dim(line));
15730
+ }
15731
+ console.log(source_default.bold.cyan("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
15732
+ console.log();
15733
+ const ans = await question(rl, source_default.cyan(" \u786E\u8BA4\u52A0\u8F7D\u6B64 Skill\uFF1F[Y/n] \u203A "));
15734
+ const confirmed = ans.trim() === "" || ans.trim().toLowerCase() === "y";
15735
+ if (!confirmed) {
15736
+ console.log(source_default.dim(" \u5DF2\u53D6\u6D88"));
15737
+ return false;
15738
+ }
15739
+ }
14962
15740
  history.push({
14963
15741
  role: "user",
14964
15742
  content: `[Skill \u5DF2\u52A0\u8F7D: ${match.id}]
@@ -14971,29 +15749,127 @@ ${content}`
14971
15749
  role: "assistant",
14972
15750
  content: `\u2713 Skill **${match.id}** \u5DF2\u52A0\u8F7D\u3002\u6211\u5DF2\u9605\u8BFB\u6307\u5357\uFF0C\u968F\u65F6\u53EF\u4EE5\u5F00\u59CB\u3002\u8BF7\u544A\u8BC9\u6211\u60A8\u7684\u5177\u4F53\u6570\u636E\u548C\u9700\u6C42\u3002`
14973
15751
  });
14974
- console.log(source_default.green(`\u2713 Skill ${match.id} \u5DF2\u6CE8\u5165\u5230\u5F53\u524D\u5BF9\u8BDD\u4E0A\u4E0B\u6587`));
15752
+ injectedSkills.set(match.id, displayName);
15753
+ console.log(source_default.green(`\u2713 Skill "${match.id}" \u5DF2\u52A0\u8F7D\u5230\u5F53\u524D\u5BF9\u8BDD\u4E0A\u4E0B\u6587`));
14975
15754
  return true;
14976
15755
  }
14977
- function expandFileRefs(input) {
14978
- return input.replace(/@"([^"]+)"|@'([^']+)'|@([\w./\\~:-]+)/g, (_2, q1, q2, q3) => {
14979
- const rawPath = q1 ?? q2 ?? q3;
14980
- try {
14981
- const resolved = (0, import_path4.resolve)(rawPath.replace(/^~/, (0, import_os3.homedir)()));
14982
- if (!(0, import_fs4.existsSync)(resolved)) return _2;
14983
- const content = (0, import_fs4.readFileSync)(resolved, "utf8");
14984
- const lines = content.split("\n");
14985
- const preview = lines.length > 100 ? lines.slice(0, 100).join("\n") + `
14986
- ... (\u5171 ${lines.length} \u884C\uFF0C\u5DF2\u622A\u65AD\u663E\u793A\u524D 100 \u884C)` : content;
15756
+ var FILE_SIZE_LIMIT = 100 * 1024;
15757
+ var DIR_FILE_LIMIT = 20;
15758
+ function listDirFiles(dirPath) {
15759
+ try {
15760
+ return (0, import_fs6.readdirSync)(dirPath).map((f2) => (0, import_path5.join)(dirPath, f2)).filter((p2) => {
15761
+ try {
15762
+ return (0, import_fs6.statSync)(p2).isFile();
15763
+ } catch {
15764
+ return false;
15765
+ }
15766
+ });
15767
+ } catch {
15768
+ return [];
15769
+ }
15770
+ }
15771
+ function expandSingleFile(resolved) {
15772
+ try {
15773
+ const stat = (0, import_fs6.statSync)(resolved);
15774
+ if (!stat.isFile()) return `[${resolved}: \u4E0D\u662F\u6587\u4EF6]`;
15775
+ if (stat.size > FILE_SIZE_LIMIT) {
14987
15776
  return `
14988
15777
  \`\`\`
14989
15778
  [\u6587\u4EF6: ${resolved}]
15779
+ (\u6587\u4EF6\u8FC7\u5927 ${Math.round(stat.size / 1024)}KB\uFF0C\u5DF2\u8DF3\u8FC7)
15780
+ \`\`\`
15781
+ `;
15782
+ }
15783
+ const content = (0, import_fs5.readFileSync)(resolved, "utf8");
15784
+ const lines = content.split("\n");
15785
+ const preview = lines.length > 150 ? lines.slice(0, 150).join("\n") + `
15786
+ ... (\u5171 ${lines.length} \u884C\uFF0C\u5DF2\u622A\u65AD)` : content;
15787
+ return `
15788
+ \`\`\`
15789
+ [\u6587\u4EF6: ${resolved}]
14990
15790
  ${preview}
14991
15791
  \`\`\`
14992
15792
  `;
15793
+ } catch {
15794
+ return `[\u65E0\u6CD5\u8BFB\u53D6: ${resolved}]`;
15795
+ }
15796
+ }
15797
+ function expandFileRefs(input) {
15798
+ return input.replace(/@"([^"]+)"|@'([^']+)'|@([\/\w.*?~:-]+)/g, (match, q1, q2, q3) => {
15799
+ const rawPath = q1 ?? q2 ?? q3;
15800
+ const expanded = rawPath.replace(/^~/, (0, import_os3.homedir)());
15801
+ if (rawPath.endsWith("/") || rawPath.endsWith("\\")) {
15802
+ const dirResolved = (0, import_path5.resolve)(expanded);
15803
+ if (!(0, import_fs5.existsSync)(dirResolved)) return match;
15804
+ const files = listDirFiles(dirResolved).slice(0, DIR_FILE_LIMIT);
15805
+ if (files.length === 0) return `[\u76EE\u5F55 ${dirResolved} \u4E3A\u7A7A]`;
15806
+ const parts = [`
15807
+ [\u76EE\u5F55: ${dirResolved} \u5171 ${files.length} \u4E2A\u6587\u4EF6]`];
15808
+ for (const f2 of files) parts.push(expandSingleFile(f2));
15809
+ return parts.join("\n");
15810
+ }
15811
+ if (rawPath.includes("*") || rawPath.includes("?")) {
15812
+ const { globSync } = (() => {
15813
+ try {
15814
+ return require("glob");
15815
+ } catch {
15816
+ return { globSync: null };
15817
+ }
15818
+ })();
15819
+ let matched = [];
15820
+ if (globSync) {
15821
+ matched = globSync(expanded, { absolute: true }).slice(0, DIR_FILE_LIMIT);
15822
+ } else {
15823
+ const ext = rawPath.replace(/^.*\./, ".");
15824
+ matched = listDirFiles(process.cwd()).filter((f2) => f2.endsWith(ext)).slice(0, DIR_FILE_LIMIT);
15825
+ }
15826
+ if (matched.length === 0) return `[\u672A\u627E\u5230\u5339\u914D: ${rawPath}]`;
15827
+ const parts = [`
15828
+ [\u901A\u914D\u7B26\u5339\u914D: ${rawPath} \u5171 ${matched.length} \u4E2A\u6587\u4EF6]`];
15829
+ for (const f2 of matched) parts.push(expandSingleFile(f2));
15830
+ return parts.join("\n");
15831
+ }
15832
+ const resolved = (0, import_path5.resolve)(expanded);
15833
+ if (!(0, import_fs5.existsSync)(resolved)) return match;
15834
+ return expandSingleFile(resolved);
15835
+ });
15836
+ }
15837
+ function snapshotWorkdir(dir) {
15838
+ const snap = /* @__PURE__ */ new Map();
15839
+ function walk(d2, depth = 0) {
15840
+ if (depth > 3) return;
15841
+ try {
15842
+ for (const entry of (0, import_fs5.readdirSync)(d2)) {
15843
+ if (entry.startsWith(".") || entry === "node_modules") continue;
15844
+ const full = (0, import_path5.join)(d2, entry);
15845
+ try {
15846
+ const st2 = (0, import_fs5.statSync)(full);
15847
+ if (st2.isDirectory()) {
15848
+ walk(full, depth + 1);
15849
+ } else {
15850
+ snap.set(full, { path: full, mtime: st2.mtimeMs, size: st2.size });
15851
+ }
15852
+ } catch {
15853
+ }
15854
+ }
14993
15855
  } catch {
14994
- return _2;
14995
15856
  }
14996
- });
15857
+ }
15858
+ walk(dir);
15859
+ return snap;
15860
+ }
15861
+ function diffWorkdir(before, after) {
15862
+ const added = [];
15863
+ const modified = [];
15864
+ for (const [path, snap] of after) {
15865
+ const prev = before.get(path);
15866
+ if (!prev) {
15867
+ added.push(path);
15868
+ } else if (snap.mtime !== prev.mtime || snap.size !== prev.size) {
15869
+ modified.push(path);
15870
+ }
15871
+ }
15872
+ return { added, modified };
14997
15873
  }
14998
15874
  function saveConversation(history, filename) {
14999
15875
  if (history.length === 0) {
@@ -15002,7 +15878,7 @@ function saveConversation(history, filename) {
15002
15878
  }
15003
15879
  const now = /* @__PURE__ */ new Date();
15004
15880
  const stamp = now.toISOString().slice(0, 16).replace("T", "-").replace(":", "-");
15005
- const outPath = (0, import_path4.resolve)(filename || `bgicli-chat-${stamp}.md`);
15881
+ const outPath = (0, import_path5.resolve)(filename || `bgicli-chat-${stamp}.md`);
15006
15882
  const lines = [`# BGI CLI \u5BF9\u8BDD\u8BB0\u5F55
15007
15883
  `, `> \u5BFC\u51FA\u65F6\u95F4: ${now.toLocaleString("zh-CN")}
15008
15884
  `];
@@ -15021,24 +15897,41 @@ ${msg.content}
15021
15897
  `);
15022
15898
  }
15023
15899
  }
15024
- (0, import_fs4.writeFileSync)(outPath, lines.join("\n"), "utf8");
15900
+ (0, import_fs5.writeFileSync)(outPath, lines.join("\n"), "utf8");
15025
15901
  console.log(source_default.green(`\u2713 \u5BF9\u8BDD\u5DF2\u4FDD\u5B58: ${outPath}`));
15026
15902
  }
15027
- var COMPACT_TOKEN_THRESHOLD = 6e4;
15903
+ var COMPACT_TOKEN_THRESHOLD = 4e4;
15028
15904
  var COMPACT_KEEP_RECENT = 8;
15029
- function estimateTokens(messages) {
15030
- const chars = messages.reduce((n2, m2) => n2 + String(m2.content ?? "").length, 0);
15031
- return Math.round(chars / 3.5);
15032
- }
15905
+ var WARN_TOKEN_THRESHOLD = 3e4;
15906
+ var estimateTokens2 = estimateTokens;
15033
15907
  async function maybeCompact(history, cfg) {
15034
- const tokens = estimateTokens(history);
15035
- if (tokens < COMPACT_TOKEN_THRESHOLD) return history;
15036
- const recent = history.slice(-COMPACT_KEEP_RECENT);
15037
- const old = history.slice(0, -COMPACT_KEEP_RECENT);
15038
- if (old.length === 0) return history;
15039
- process.stdout.write(source_default.dim(`
15040
- [\u4E0A\u4E0B\u6587\u5DF2\u8FBE ~${Math.round(tokens / 1e3)}k tokens\uFF0C\u6B63\u5728\u81EA\u52A8\u538B\u7F29...]
15041
- `));
15908
+ let cleaned = deduplicateSkillInjections(trimToolOutputs(history));
15909
+ const tokensBefore = estimateTokens2(history);
15910
+ const tokensAfter = estimateTokens2(cleaned);
15911
+ if (tokensAfter < tokensBefore) {
15912
+ const saved = tokensBefore - tokensAfter;
15913
+ process.stdout.write(source_default.dim(
15914
+ `
15915
+ [\u4E0A\u4E0B\u6587\u4F18\u5316: \u53BB\u91CD/\u622A\u65AD\u8282\u7701 ~${Math.round(saved / 1e3)}k tokens]
15916
+ `
15917
+ ));
15918
+ }
15919
+ if (tokensAfter >= WARN_TOKEN_THRESHOLD && tokensAfter < COMPACT_TOKEN_THRESHOLD) {
15920
+ process.stdout.write(source_default.dim(
15921
+ `[\u63D0\u793A: \u4E0A\u4E0B\u6587\u5DF2\u8FBE ~${Math.round(tokensAfter / 1e3)}k tokens\uFF0C\u63A5\u8FD1\u538B\u7F29\u9608\u503C ${Math.round(COMPACT_TOKEN_THRESHOLD / 1e3)}k]
15922
+ `
15923
+ ));
15924
+ return cleaned;
15925
+ }
15926
+ if (tokensAfter < COMPACT_TOKEN_THRESHOLD) return cleaned;
15927
+ const recent = cleaned.slice(-COMPACT_KEEP_RECENT);
15928
+ const old = cleaned.slice(0, -COMPACT_KEEP_RECENT);
15929
+ if (old.length === 0) return cleaned;
15930
+ process.stdout.write(source_default.dim(
15931
+ `
15932
+ [\u4E0A\u4E0B\u6587\u5DF2\u8FBE ~${Math.round(tokensAfter / 1e3)}k tokens\uFF0C\u6B63\u5728\u81EA\u52A8\u538B\u7F29\u5386\u53F2...]
15933
+ `
15934
+ ));
15042
15935
  try {
15043
15936
  const summary = await compactMessages(old, cfg);
15044
15937
  const compacted = [
@@ -15054,16 +15947,31 @@ ${summary}`
15054
15947
  },
15055
15948
  ...recent
15056
15949
  ];
15057
- const saved = estimateTokens(history) - estimateTokens(compacted);
15058
- process.stdout.write(source_default.dim(`[\u538B\u7F29\u5B8C\u6210\uFF0C\u91CA\u653E\u7EA6 ~${Math.round(saved / 1e3)}k tokens]
15950
+ const finalTokens = estimateTokens2(compacted);
15951
+ const totalSaved = tokensBefore - finalTokens;
15952
+ process.stdout.write(source_default.dim(
15953
+ `[\u538B\u7F29\u5B8C\u6210: ~${Math.round(tokensBefore / 1e3)}k \u2192 ~${Math.round(finalTokens / 1e3)}k tokens\uFF0C\u8282\u7701 ~${Math.round(totalSaved / 1e3)}k]
15059
15954
 
15060
- `));
15955
+ `
15956
+ ));
15061
15957
  return compacted;
15062
15958
  } catch {
15063
- return history.slice(-COMPACT_KEEP_RECENT * 2);
15064
- }
15065
- }
15066
- async function handleCommand(input, rl, history, thinkMode) {
15959
+ process.stdout.write(source_default.yellow("[\u538B\u7F29\u5931\u8D25\uFF0C\u56DE\u9000\u5230\u622A\u65AD\u6A21\u5F0F]\n"));
15960
+ return cleaned.slice(-COMPACT_KEEP_RECENT * 2);
15961
+ }
15962
+ }
15963
+ function formatAge(isoDate) {
15964
+ const diff = Date.now() - new Date(isoDate).getTime();
15965
+ const mins = Math.floor(diff / 6e4);
15966
+ const hours = Math.floor(diff / 36e5);
15967
+ const days = Math.floor(diff / 864e5);
15968
+ if (mins < 1) return "\u521A\u521A";
15969
+ if (mins < 60) return `${mins} \u5206\u949F\u524D`;
15970
+ if (hours < 24) return `${hours} \u5C0F\u65F6\u524D`;
15971
+ if (days < 30) return `${days} \u5929\u524D`;
15972
+ return new Date(isoDate).toLocaleDateString("zh-CN");
15973
+ }
15974
+ async function handleCommand(input, rl, history, thinkMode, injectedSkills) {
15067
15975
  const [cmd, ...rest] = input.slice(1).trim().split(/\s+/);
15068
15976
  const arg = rest.join(" ");
15069
15977
  const cfg = loadConfig();
@@ -15154,20 +16062,370 @@ async function handleCommand(input, rl, history, thinkMode) {
15154
16062
  return { clearHistory: true };
15155
16063
  case "history": {
15156
16064
  const turns = Math.floor(history.length / 2);
15157
- const chars = history.reduce((n2, m2) => n2 + String(m2.content ?? "").length, 0);
15158
- const estTokens = Math.round(chars / 3.5);
16065
+ const estTokens = estimateTokens2(history);
16066
+ const pct = Math.round(estTokens / COMPACT_TOKEN_THRESHOLD * 100);
16067
+ const bar = "\u2588".repeat(Math.min(20, Math.round(pct / 5))) + "\u2591".repeat(Math.max(0, 20 - Math.round(pct / 5)));
15159
16068
  console.log(source_default.bold("\u5BF9\u8BDD\u7EDF\u8BA1:"));
15160
16069
  console.log(` \u8F6E\u6B21: ${turns}`);
15161
16070
  console.log(` \u6D88\u606F\u603B\u6570: ${history.length}`);
15162
16071
  console.log(` \u4F30\u7B97 Token: ~${estTokens.toLocaleString()}`);
16072
+ console.log(` \u4E0A\u4E0B\u6587\u7528\u91CF: [${pct >= 80 ? source_default.red(bar) : pct >= 50 ? source_default.yellow(bar) : source_default.green(bar)}] ${pct}%`);
16073
+ console.log(` \u538B\u7F29\u9608\u503C: ~${Math.round(COMPACT_TOKEN_THRESHOLD / 1e3)}k tokens`);
16074
+ if (pct >= 80) console.log(source_default.yellow(" \u26A0 \u63A5\u8FD1\u4E0A\u9650\uFF0C\u5EFA\u8BAE\u8FD0\u884C /compact"));
15163
16075
  break;
15164
16076
  }
15165
16077
  case "save": {
15166
16078
  saveConversation(history, arg || void 0);
15167
16079
  break;
15168
16080
  }
16081
+ // ── Session persistence ──────────────────────────────────────────────────
16082
+ case "sessions": {
16083
+ const sessions = listSessions();
16084
+ if (sessions.length === 0) {
16085
+ console.log(source_default.dim("\u6682\u65E0\u5386\u53F2\u4F1A\u8BDD\u3002\u5BF9\u8BDD\u7ED3\u675F\u540E\u4F1A\u81EA\u52A8\u4FDD\u5B58\u3002"));
16086
+ break;
16087
+ }
16088
+ console.log(source_default.bold(`
16089
+ \u5386\u53F2\u4F1A\u8BDD (${sessions.length} \u4E2A):
16090
+ `));
16091
+ sessions.slice(0, 20).forEach((s2, i2) => {
16092
+ const age = formatAge(s2.updatedAt);
16093
+ const skills = s2.skills.length > 0 ? source_default.dim(` [${s2.skills.join(", ")}]`) : "";
16094
+ const preview = s2.preview ? source_default.dim(` \u2014 "${s2.preview.slice(0, 40)}${s2.preview.length > 40 ? "\u2026" : ""}"`) : "";
16095
+ const marker = i2 === 0 ? source_default.green("\u25CF") : source_default.dim("\u25CB");
16096
+ console.log(` ${marker} ${source_default.cyan(s2.id)} ${source_default.dim(age)}${skills}`);
16097
+ if (preview) console.log(` ${preview}`);
16098
+ });
16099
+ console.log();
16100
+ console.log(source_default.dim("\u4F7F\u7528 /resume [id] \u6062\u590D\u4F1A\u8BDD /session-del <id> \u5220\u9664"));
16101
+ break;
16102
+ }
16103
+ case "resume": {
16104
+ let targetId = arg;
16105
+ if (!targetId) {
16106
+ const last = getLastSession();
16107
+ if (!last) {
16108
+ console.log(source_default.yellow("\u6682\u65E0\u5386\u53F2\u4F1A\u8BDD"));
16109
+ break;
16110
+ }
16111
+ targetId = last.id;
16112
+ console.log(source_default.dim(`\u6062\u590D\u6700\u8FD1\u4F1A\u8BDD: ${targetId}`));
16113
+ }
16114
+ const session = loadSession(targetId);
16115
+ if (!session) {
16116
+ const all = listSessions();
16117
+ const match = all.find((s3) => s3.id.includes(targetId));
16118
+ if (!match) {
16119
+ console.log(source_default.red(`\u672A\u627E\u5230\u4F1A\u8BDD: ${targetId}`));
16120
+ break;
16121
+ }
16122
+ const s2 = loadSession(match.id);
16123
+ if (!s2) {
16124
+ console.log(source_default.red("\u52A0\u8F7D\u5931\u8D25"));
16125
+ break;
16126
+ }
16127
+ console.log(source_default.green(`\u2713 \u5DF2\u6062\u590D\u4F1A\u8BDD ${s2.id} (${s2.messageCount} \u6761\u6D88\u606F)`));
16128
+ if (s2.skills.length > 0) console.log(source_default.dim(` \u5DF2\u52A0\u8F7D Skills: ${s2.skills.join(", ")}`));
16129
+ for (const sk of s2.skills) injectedSkills.set(sk, sk);
16130
+ return { injectHistory: s2.messages };
16131
+ }
16132
+ console.log(source_default.green(`\u2713 \u5DF2\u6062\u590D\u4F1A\u8BDD ${session.id} (${session.messageCount} \u6761\u6D88\u606F)`));
16133
+ if (session.skills.length > 0) console.log(source_default.dim(` \u5DF2\u52A0\u8F7D Skills: ${session.skills.join(", ")}`));
16134
+ for (const sk of session.skills) injectedSkills.set(sk, sk);
16135
+ return { injectHistory: session.messages };
16136
+ }
16137
+ case "session-save": {
16138
+ const sessionId = SESSION_CTX.id || void 0;
16139
+ if (!sessionId) {
16140
+ console.log(source_default.yellow("\u5F53\u524D\u4F1A\u8BDD\u5C1A\u672A\u521D\u59CB\u5316"));
16141
+ break;
16142
+ }
16143
+ const name = arg || (/* @__PURE__ */ new Date()).toLocaleString("zh-CN");
16144
+ saveSession(sessionId, name, history, Array.from(injectedSkills.keys()), SESSION_CTX.createdAt || (/* @__PURE__ */ new Date()).toISOString());
16145
+ console.log(source_default.green(`\u2713 \u4F1A\u8BDD\u5DF2\u4FDD\u5B58: ${sessionId} \u540D\u79F0: "${name}"`));
16146
+ break;
16147
+ }
16148
+ case "session-del": {
16149
+ if (!arg) {
16150
+ console.log("\u7528\u6CD5: /session-del <id>");
16151
+ break;
16152
+ }
16153
+ const ok = deleteSession(arg);
16154
+ console.log(ok ? source_default.green(`\u2713 \u5DF2\u5220\u9664\u4F1A\u8BDD: ${arg}`) : source_default.yellow(`\u672A\u627E\u5230\u4F1A\u8BDD: ${arg}`));
16155
+ break;
16156
+ }
16157
+ // ── Checkpoints ──────────────────────────────────────────────────────────
16158
+ case "checkpoint": {
16159
+ const sessionId = SESSION_CTX.id || "default";
16160
+ const sub = arg.split(/\s+/)[0]?.toLowerCase();
16161
+ const subArg = arg.split(/\s+/).slice(1).join(" ");
16162
+ if (!sub || sub === "save" || sub === "") {
16163
+ const label = subArg || `\u7B2C ${Math.floor(history.length / 2)} \u8F6E`;
16164
+ const cpId = saveCheckpoint(sessionId, label, history, Array.from(injectedSkills.keys()));
16165
+ console.log(source_default.green(`\u2713 \u65AD\u70B9\u5DF2\u4FDD\u5B58: ${cpId} \u6807\u7B7E: "${label}"`));
16166
+ } else if (sub === "list") {
16167
+ const cps = listCheckpoints(sessionId);
16168
+ if (cps.length === 0) {
16169
+ console.log(source_default.dim("\u5F53\u524D\u4F1A\u8BDD\u6682\u65E0\u65AD\u70B9"));
16170
+ } else {
16171
+ console.log(source_default.bold(`
16172
+ \u5F53\u524D\u4F1A\u8BDD\u65AD\u70B9 (${cps.length} \u4E2A):
16173
+ `));
16174
+ cps.forEach((cp) => {
16175
+ const age = formatAge(cp.createdAt);
16176
+ console.log(` ${source_default.cyan(cp.id.split("-cp")[1] ?? cp.id)} ${source_default.dim(age)} "${cp.label}" (${cp.messageCount} \u6761\u6D88\u606F)`);
16177
+ console.log(source_default.dim(` /checkpoint restore ${cp.id}`));
16178
+ });
16179
+ }
16180
+ } else if (sub === "restore") {
16181
+ if (!subArg) {
16182
+ console.log("\u7528\u6CD5: /checkpoint restore <id>");
16183
+ break;
16184
+ }
16185
+ const cps = listCheckpoints(sessionId);
16186
+ const target = cps.find((cp) => cp.id === subArg || cp.id.endsWith(subArg));
16187
+ if (!target) {
16188
+ console.log(source_default.red(`\u672A\u627E\u5230\u65AD\u70B9: ${subArg}`));
16189
+ break;
16190
+ }
16191
+ console.log(source_default.green(`\u2713 \u5DF2\u6062\u590D\u5230\u65AD\u70B9: "${target.label}" (${target.messageCount} \u6761\u6D88\u606F)`));
16192
+ injectedSkills.clear();
16193
+ for (const sk of target.skills) injectedSkills.set(sk, sk);
16194
+ return { injectHistory: target.messages };
16195
+ } else if (sub === "clear") {
16196
+ const n2 = clearCheckpoints(sessionId);
16197
+ console.log(source_default.green(`\u2713 \u5DF2\u6E05\u9664 ${n2} \u4E2A\u65AD\u70B9`));
16198
+ } else {
16199
+ console.log("\u7528\u6CD5: /checkpoint [list|restore <id>|clear]");
16200
+ }
16201
+ break;
16202
+ }
16203
+ // ── Workdir diff ─────────────────────────────────────────────────────────
16204
+ case "diff": {
16205
+ const snap = SESSION_CTX.wdirSnapshot ?? void 0;
16206
+ if (!snap) {
16207
+ console.log(source_default.dim("\u5DE5\u4F5C\u76EE\u5F55\u5FEB\u7167\u5C1A\u672A\u5EFA\u7ACB\uFF08\u4F1A\u8BDD\u5F00\u59CB\u65F6\u81EA\u52A8\u521B\u5EFA\uFF09"));
16208
+ break;
16209
+ }
16210
+ const current = snapshotWorkdir(process.cwd());
16211
+ const { added, modified } = diffWorkdir(snap, current);
16212
+ if (added.length === 0 && modified.length === 0) {
16213
+ console.log(source_default.dim("\u672C\u6B21\u4F1A\u8BDD\u672A\u4EA7\u751F\u65B0\u6587\u4EF6\u6216\u4FEE\u6539"));
16214
+ } else {
16215
+ if (added.length > 0) {
16216
+ console.log(source_default.bold.green(`
16217
+ \u65B0\u589E\u6587\u4EF6 (${added.length} \u4E2A):`));
16218
+ added.forEach((f2) => console.log(` ${source_default.green("+")} ${f2}`));
16219
+ }
16220
+ if (modified.length > 0) {
16221
+ console.log(source_default.bold.yellow(`
16222
+ \u4FEE\u6539\u6587\u4EF6 (${modified.length} \u4E2A):`));
16223
+ modified.forEach((f2) => console.log(` ${source_default.yellow("~")} ${f2}`));
16224
+ }
16225
+ console.log();
16226
+ }
16227
+ break;
16228
+ }
16229
+ // ── /run workflow wizard ─────────────────────────────────────────────────
16230
+ case "run": {
16231
+ const targetId = arg;
16232
+ if (!targetId) {
16233
+ console.log("\u7528\u6CD5: /run <skill-id>");
16234
+ console.log(source_default.dim("\u793A\u4F8B: /run deseq2-analysis /run survival-analysis-clinical"));
16235
+ break;
16236
+ }
16237
+ const allSkillsRun = collectAllSkills();
16238
+ const runMatch = allSkillsRun.find((e2) => e2.id === targetId || e2.id.startsWith(targetId) || e2.id.includes(targetId));
16239
+ if (!runMatch) {
16240
+ console.log(source_default.red(`\u672A\u627E\u5230 Skill: ${targetId}`));
16241
+ break;
16242
+ }
16243
+ const skillPath = (0, import_path5.join)(runMatch.dir, runMatch.id, "SKILL.md");
16244
+ if (!(0, import_fs5.existsSync)(skillPath)) {
16245
+ console.log(source_default.red(`${runMatch.id} \u7F3A\u5C11 SKILL.md`));
16246
+ break;
16247
+ }
16248
+ const skillContent = (0, import_fs5.readFileSync)(skillPath, "utf8");
16249
+ const { name: skillName } = parseSkillMeta(skillContent);
16250
+ const paramsMatch = skillContent.match(/##\s*(?:必要参数|Required Parameters|参数)[\s\S]*?(?=\n##|$)/i);
16251
+ const paramsSection = paramsMatch?.[0] ?? "";
16252
+ console.log(source_default.bold.cyan(`
16253
+ \u2500\u2500\u2500 /run ${runMatch.id} \u5411\u5BFC \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
16254
+ console.log(source_default.dim(` Skill: ${skillName || runMatch.id}`));
16255
+ console.log(source_default.dim(" \u8BF7\u56DE\u7B54\u4EE5\u4E0B\u95EE\u9898\uFF0CAI \u5C06\u81EA\u52A8\u751F\u6210\u5E76\u6267\u884C\u5206\u6790\u811A\u672C\n"));
16256
+ const paramLines = paramsSection.split("\n").filter((l2) => /^[-*•]|^\d+\./.test(l2.trim())).map((l2) => l2.replace(/^[-*•\d.]+\s*/, "").trim()).filter(Boolean).slice(0, 8);
16257
+ const questions = paramLines.length > 0 ? paramLines : [
16258
+ "\u6570\u636E\u6587\u4EF6\u8DEF\u5F84\uFF08\u652F\u6301\u76F8\u5BF9\u8DEF\u5F84\uFF0C\u5982 ./data/counts.csv\uFF09",
16259
+ "\u6837\u672C\u5206\u7EC4\u4FE1\u606F\uFF08\u5982 treatment,treatment,control,control\uFF09",
16260
+ "\u5BF9\u7167\u7EC4\u540D\u79F0",
16261
+ "\u5B9E\u9A8C\u7EC4\u540D\u79F0",
16262
+ "\u8F93\u51FA\u76EE\u5F55\uFF08\u9ED8\u8BA4: ./results\uFF09"
16263
+ ];
16264
+ const answers = {};
16265
+ for (const q2 of questions) {
16266
+ const ans = await question(rl, source_default.blue(` ${q2}: `));
16267
+ if (ans.trim()) answers[q2] = ans.trim();
16268
+ }
16269
+ const paramSummary = Object.entries(answers).map(([k2, v2]) => `- ${k2}: ${v2}`).join("\n");
16270
+ const runPrompt = `\u8BF7\u6839\u636E\u4EE5\u4E0B\u53C2\u6570\uFF0C\u4F7F\u7528 ${skillName || runMatch.id} \u5DE5\u4F5C\u6D41\u751F\u6210\u5B8C\u6574\u7684\u5206\u6790\u811A\u672C\u5E76\u7ACB\u5373\u6267\u884C\uFF1A
16271
+
16272
+ \u7528\u6237\u63D0\u4F9B\u7684\u53C2\u6570\uFF1A
16273
+ ${paramSummary}
16274
+
16275
+ \u8BF7\uFF1A
16276
+ 1. \u751F\u6210\u5B8C\u6574\u53EF\u8FD0\u884C\u7684\u5206\u6790\u811A\u672C\uFF08R \u6216 Python\uFF09
16277
+ 2. \u4F7F\u7528 bash \u5DE5\u5177\u6267\u884C\u811A\u672C
16278
+ 3. \u5206\u6790\u5B8C\u6210\u540E\u7ED9\u51FA\u7ED3\u679C\u6458\u8981`;
16279
+ if (!injectedSkills.has(runMatch.id)) {
16280
+ await injectSkill(runMatch.id, history, injectedSkills, rl, true);
16281
+ }
16282
+ history.push({ role: "user", content: runPrompt });
16283
+ console.log(source_default.dim("\n \u6B63\u5728\u751F\u6210\u5E76\u6267\u884C\u5206\u6790\u811A\u672C...\n"));
16284
+ try {
16285
+ const runCfg = loadConfig();
16286
+ const reply = await chat(history, runCfg, buildSystemPrompt());
16287
+ history.push({ role: "assistant", content: reply });
16288
+ } catch (err) {
16289
+ console.error(source_default.red(`\u6267\u884C\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`));
16290
+ history.pop();
16291
+ }
16292
+ break;
16293
+ }
16294
+ // ── /check-env ───────────────────────────────────────────────────────────
16295
+ case "check-env": {
16296
+ const checkId = arg;
16297
+ const skillsToCheck = checkId ? (() => {
16298
+ const all = collectAllSkills();
16299
+ const m2 = all.find((e2) => e2.id === checkId || e2.id.startsWith(checkId) || e2.id.includes(checkId));
16300
+ return m2 ? [m2] : [];
16301
+ })() : collectAllSkills().filter((e2) => injectedSkills.has(e2.id));
16302
+ if (skillsToCheck.length === 0) {
16303
+ console.log(source_default.yellow(checkId ? `\u672A\u627E\u5230 Skill: ${checkId}` : "\u5F53\u524D\u4F1A\u8BDD\u672A\u52A0\u8F7D\u4EFB\u4F55 Skill"));
16304
+ break;
16305
+ }
16306
+ for (const entry of skillsToCheck) {
16307
+ const sp = (0, import_path5.join)(entry.dir, entry.id, "SKILL.md");
16308
+ if (!(0, import_fs5.existsSync)(sp)) continue;
16309
+ const content = (0, import_fs5.readFileSync)(sp, "utf8");
16310
+ const { name } = parseSkillMeta(content);
16311
+ console.log(source_default.bold(`
16312
+ \u68C0\u6D4B ${name || entry.id} \u7684\u4F9D\u8D56\u73AF\u5883:`));
16313
+ const rPkgs = [.../* @__PURE__ */ new Set([
16314
+ ...(content.match(/library\(([\w.]+)\)/g) ?? []).map((m2) => m2.replace(/library\(|\)/g, "")),
16315
+ ...(content.match(/require\(([\w.]+)\)/g) ?? []).map((m2) => m2.replace(/require\(|\)/g, "")),
16316
+ ...(content.match(/BiocManager::install\("([^"]+)"\)/g) ?? []).map((m2) => m2.replace(/BiocManager::install\("|"\)/g, ""))
16317
+ ])];
16318
+ const pyPkgs = [.../* @__PURE__ */ new Set([
16319
+ ...(content.match(/import (\w+)/g) ?? []).map((m2) => m2.replace("import ", "")),
16320
+ ...(content.match(/from (\w+) import/g) ?? []).map((m2) => m2.replace("from ", "").replace(" import", ""))
16321
+ ])];
16322
+ if (rPkgs.length > 0) {
16323
+ console.log(source_default.dim(` R \u5305 (${rPkgs.length} \u4E2A): ${rPkgs.join(", ")}`));
16324
+ const checkScript = rPkgs.map(
16325
+ (p2) => `cat(sprintf(" %-30s %s\\n", "${p2}", if(requireNamespace("${p2}", quietly=TRUE)) "\u2713 \u5DF2\u5B89\u88C5" else "\u2717 \u672A\u5B89\u88C5"))`
16326
+ ).join("\n");
16327
+ const result = await executeTool("bash", { command: `Rscript -e '${checkScript}'` });
16328
+ if (result.error) {
16329
+ console.log(source_default.dim(" (R \u672A\u5B89\u88C5\u6216\u4E0D\u53EF\u7528)"));
16330
+ } else {
16331
+ console.log(result.output);
16332
+ const missing = rPkgs.filter((p2) => result.output.includes(`${p2}`) && result.output.includes("\u2717"));
16333
+ if (missing.length > 0) {
16334
+ console.log(source_default.yellow(` \u7F3A\u5C11 ${missing.length} \u4E2A R \u5305\uFF0C\u5B89\u88C5\u547D\u4EE4:`));
16335
+ console.log(source_default.cyan(` !Rscript -e 'install.packages(c(${missing.map((p2) => `"${p2}"`).join(", ")}))'`));
16336
+ const biocPkgs = missing.filter((p2) => ["DESeq2", "edgeR", "limma", "clusterProfiler", "Seurat", "SingleCellExperiment", "BiocGenerics"].includes(p2));
16337
+ if (biocPkgs.length > 0) {
16338
+ console.log(source_default.cyan(` !Rscript -e 'BiocManager::install(c(${biocPkgs.map((p2) => `"${p2}"`).join(", ")}))'`));
16339
+ }
16340
+ }
16341
+ }
16342
+ }
16343
+ if (pyPkgs.length > 0) {
16344
+ const commonPy = ["numpy", "pandas", "scipy", "sklearn", "matplotlib", "seaborn", "scanpy", "anndata", "torch"];
16345
+ const relevantPy = pyPkgs.filter((p2) => commonPy.includes(p2));
16346
+ if (relevantPy.length > 0) {
16347
+ console.log(source_default.dim(` Python \u5305 (${relevantPy.length} \u4E2A): ${relevantPy.join(", ")}`));
16348
+ const checkPy = relevantPy.map(
16349
+ (p2) => `python3 -c "import ${p2}; print(' %-30s \u2713 \u5DF2\u5B89\u88C5' % '${p2}')" 2>/dev/null || echo " ${p2.padEnd(30)} \u2717 \u672A\u5B89\u88C5"`
16350
+ ).join(" && ");
16351
+ const result = await executeTool("bash", { command: checkPy });
16352
+ if (!result.error) console.log(result.output);
16353
+ }
16354
+ }
16355
+ if (rPkgs.length === 0 && pyPkgs.length === 0) {
16356
+ console.log(source_default.dim(" \u672A\u68C0\u6D4B\u5230\u660E\u786E\u7684\u5305\u4F9D\u8D56\u58F0\u660E"));
16357
+ }
16358
+ }
16359
+ break;
16360
+ }
16361
+ // ── /install from GitHub ─────────────────────────────────────────────────
16362
+ case "install": {
16363
+ if (!arg) {
16364
+ console.log("\u7528\u6CD5: /install <github-url>");
16365
+ console.log(source_default.dim("\u793A\u4F8B: /install https://github.com/user/my-skill"));
16366
+ console.log(source_default.dim(" /install user/repo (GitHub \u7B80\u5199)"));
16367
+ break;
16368
+ }
16369
+ let repoUrl = arg;
16370
+ if (!repoUrl.startsWith("http")) {
16371
+ repoUrl = `https://github.com/${repoUrl}`;
16372
+ }
16373
+ const repoName = repoUrl.replace(/\.git$/, "").split("/").pop() ?? "unknown-skill";
16374
+ const installTarget = (0, import_path5.join)(SKILLS_DIR, repoName);
16375
+ if ((0, import_fs5.existsSync)(installTarget)) {
16376
+ console.log(source_default.yellow(`Skill "${repoName}" \u5DF2\u5B58\u5728\uFF0C\u5982\u9700\u66F4\u65B0\u8BF7\u5148 /uninstall ${repoName}`));
16377
+ break;
16378
+ }
16379
+ console.log(source_default.dim(`\u6B63\u5728\u4ECE GitHub \u5B89\u88C5 Skill: ${repoName}...`));
16380
+ const cloneResult = await executeTool("bash", {
16381
+ command: `git clone --depth 1 "${repoUrl}" "${installTarget}" 2>&1`
16382
+ });
16383
+ if (cloneResult.error || cloneResult.output.includes("fatal:")) {
16384
+ console.log(source_default.red(`\u5B89\u88C5\u5931\u8D25: ${cloneResult.output || cloneResult.error}`));
16385
+ break;
16386
+ }
16387
+ const skillMdPath = (0, import_path5.join)(installTarget, "SKILL.md");
16388
+ if (!(0, import_fs5.existsSync)(skillMdPath)) {
16389
+ console.log(source_default.red(`\u5B89\u88C5\u5931\u8D25: ${repoName} \u7F3A\u5C11 SKILL.md \u6587\u4EF6`));
16390
+ await executeTool("bash", { command: `rm -rf "${installTarget}"` });
16391
+ break;
16392
+ }
16393
+ const content = (0, import_fs5.readFileSync)(skillMdPath, "utf8");
16394
+ const { name, shortDesc } = parseSkillMeta(content);
16395
+ console.log(source_default.green(`\u2713 Skill \u5B89\u88C5\u6210\u529F!`));
16396
+ console.log(` ID: ${source_default.cyan(repoName)}`);
16397
+ console.log(` \u540D\u79F0: ${name || repoName}`);
16398
+ if (shortDesc) console.log(` \u529F\u80FD: ${source_default.dim(shortDesc)}`);
16399
+ console.log(source_default.dim(` \u4F7F\u7528 /sk ${repoName} \u52A0\u8F7D`));
16400
+ break;
16401
+ }
16402
+ case "uninstall": {
16403
+ if (!arg) {
16404
+ console.log("\u7528\u6CD5: /uninstall <skill-id>");
16405
+ break;
16406
+ }
16407
+ const uninstallPath = (0, import_path5.join)(SKILLS_DIR, arg);
16408
+ if (!(0, import_fs5.existsSync)(uninstallPath)) {
16409
+ console.log(source_default.red(`\u672A\u627E\u5230\u5DF2\u5B89\u88C5\u7684 Skill: ${arg}`));
16410
+ console.log(source_default.dim("\u6CE8\u610F: \u53EA\u80FD\u5378\u8F7D\u901A\u8FC7 /install \u5B89\u88C5\u7684\u7B2C\u4E09\u65B9 Skill"));
16411
+ break;
16412
+ }
16413
+ const ans = await question(rl, source_default.yellow(` \u786E\u8BA4\u5378\u8F7D Skill "${arg}"\uFF1F[y/N] \u203A `));
16414
+ if (ans.trim().toLowerCase() !== "y") {
16415
+ console.log(source_default.dim(" \u5DF2\u53D6\u6D88"));
16416
+ break;
16417
+ }
16418
+ const rmResult = await executeTool("bash", { command: `rm -rf "${uninstallPath}"` });
16419
+ if (rmResult.error) {
16420
+ console.log(source_default.red(`\u5378\u8F7D\u5931\u8D25: ${rmResult.error}`));
16421
+ } else {
16422
+ injectedSkills.delete(arg);
16423
+ console.log(source_default.green(`\u2713 Skill "${arg}" \u5DF2\u5378\u8F7D`));
16424
+ }
16425
+ break;
16426
+ }
15169
16427
  case "compact": {
15170
- const tokens = estimateTokens(history);
16428
+ const tokens = estimateTokens2(history);
15171
16429
  if (history.length < 4) {
15172
16430
  console.log(source_default.dim("\u5BF9\u8BDD\u592A\u77ED\uFF0C\u65E0\u9700\u538B\u7F29"));
15173
16431
  break;
@@ -15189,7 +16447,7 @@ ${summary}` },
15189
16447
  { role: "assistant", content: "\u2713 \u5DF2\u7406\u89E3\u4E4B\u524D\u7684\u5BF9\u8BDD\u6458\u8981\uFF0C\u8BF7\u7EE7\u7EED\u3002" },
15190
16448
  ...recent
15191
16449
  ];
15192
- const after = estimateTokens(newHistory);
16450
+ const after = estimateTokens2(newHistory);
15193
16451
  console.log(source_default.green(`\u2713 \u538B\u7F29\u5B8C\u6210: ${history.length} \u6761\u6D88\u606F \u2192 ${newHistory.length} \u6761\uFF0C~${Math.round(after / 1e3)}k tokens`));
15194
16452
  return { injectHistory: newHistory };
15195
16453
  } catch (err) {
@@ -15197,6 +16455,52 @@ ${summary}` },
15197
16455
  }
15198
16456
  break;
15199
16457
  }
16458
+ case "skills": {
16459
+ if (injectedSkills.size === 0) {
16460
+ console.log(source_default.dim("\u5F53\u524D\u4F1A\u8BDD\u672A\u52A0\u8F7D\u4EFB\u4F55 Skill"));
16461
+ console.log(source_default.dim("\u4F7F\u7528 /sk <\u5173\u952E\u8BCD> \u52A0\u8F7D\uFF0C\u6216\u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\u81EA\u52A8\u6FC0\u6D3B"));
16462
+ } else {
16463
+ console.log(source_default.bold(`
16464
+ \u5F53\u524D\u5DF2\u52A0\u8F7D\u7684 Skills (${injectedSkills.size} \u4E2A):
16465
+ `));
16466
+ for (const [id, name] of injectedSkills) {
16467
+ console.log(` ${source_default.green("\u25CF")} ${source_default.cyan(id)} ${source_default.dim("\u2014 " + name)}`);
16468
+ console.log(source_default.dim(` /unload ${id} \u53EF\u5378\u8F7D\u6B64 Skill`));
16469
+ }
16470
+ console.log();
16471
+ console.log(source_default.dim("\u63D0\u793A: /clear \u6E05\u7A7A\u5168\u90E8\u5BF9\u8BDD\u548C Skills | /unload <id> \u5378\u8F7D\u5355\u4E2A"));
16472
+ }
16473
+ break;
16474
+ }
16475
+ case "unload": {
16476
+ if (!arg) {
16477
+ console.log("\u7528\u6CD5: /unload <skill-id>");
16478
+ console.log(source_default.dim("\u4F7F\u7528 /skills \u67E5\u770B\u5F53\u524D\u5DF2\u52A0\u8F7D\u7684 Skills"));
16479
+ break;
16480
+ }
16481
+ const loadedIds = Array.from(injectedSkills.keys());
16482
+ const targetId = loadedIds.find((id) => id === arg || id.includes(arg));
16483
+ if (!targetId) {
16484
+ console.log(source_default.yellow(`\u672A\u627E\u5230\u5DF2\u52A0\u8F7D\u7684 Skill: "${arg}"`));
16485
+ console.log(source_default.dim("\u4F7F\u7528 /skills \u67E5\u770B\u5F53\u524D\u5DF2\u52A0\u8F7D\u7684 Skills"));
16486
+ break;
16487
+ }
16488
+ const SKILL_MARKER = `[Skill \u5DF2\u52A0\u8F7D: ${targetId}]`;
16489
+ let removed = 0;
16490
+ for (let i2 = history.length - 1; i2 >= 0; i2--) {
16491
+ const content = typeof history[i2].content === "string" ? history[i2].content : "";
16492
+ if (history[i2].role === "user" && content.startsWith(SKILL_MARKER)) {
16493
+ const toRemove = i2 + 1 < history.length && history[i2 + 1].role === "assistant" ? 2 : 1;
16494
+ history.splice(i2, toRemove);
16495
+ removed += toRemove;
16496
+ break;
16497
+ }
16498
+ }
16499
+ injectedSkills.delete(targetId);
16500
+ console.log(source_default.green(`\u2713 Skill "${targetId}" \u5DF2\u5378\u8F7D`));
16501
+ if (removed > 0) console.log(source_default.dim(` \u5DF2\u4ECE\u5BF9\u8BDD\u5386\u53F2\u4E2D\u79FB\u9664 ${removed} \u6761\u6CE8\u5165\u6D88\u606F`));
16502
+ break;
16503
+ }
15200
16504
  case "think": {
15201
16505
  const val = arg.toLowerCase();
15202
16506
  if (val === "on" || val === "1" || val === "true") {
@@ -15221,29 +16525,41 @@ ${summary}` },
15221
16525
  const allSkills = collectAllSkills();
15222
16526
  const idMatch = allSkills.find((e2) => e2.id === arg || e2.id.startsWith(arg) || e2.id.includes(arg));
15223
16527
  if (idMatch) {
15224
- injectSkill(arg, history);
16528
+ await injectSkill(idMatch.id, history, injectedSkills, rl);
15225
16529
  break;
15226
16530
  }
15227
- const { routes, topScore } = routeSkill(arg);
15228
- if (routes.length === 0 || topScore < 4) {
16531
+ const spinner = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
16532
+ let si = 0;
16533
+ const spinTimer = setInterval(() => {
16534
+ process.stdout.write(`\r ${source_default.cyan(spinner[si++ % spinner.length])} \u6B63\u5728\u7528 AI \u641C\u7D22\u6700\u5339\u914D\u7684 Skills...`);
16535
+ }, 80);
16536
+ const llmRecs = await llmRecommendSkills(arg);
16537
+ clearInterval(spinTimer);
16538
+ process.stdout.write("\r" + " ".repeat(50) + "\r");
16539
+ if (llmRecs.length === 0) {
16540
+ console.log(source_default.yellow(" AI \u63A8\u8350\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u663E\u793A\u5173\u952E\u8BCD\u5339\u914D\u7ED3\u679C:"));
15229
16541
  listSkills(arg);
15230
16542
  break;
15231
16543
  }
15232
- if (routes.length === 1 || topScore >= 10) {
15233
- console.log(source_default.dim(`\u6839\u636E\u63CF\u8FF0\u5339\u914D\u5230: ${routes[0].id}`));
15234
- injectSkill(routes[0].id, history);
15235
- } else {
15236
- console.log(source_default.bold(`
15237
- \u6839\u636E\u63CF\u8FF0\u63A8\u8350\u4EE5\u4E0B Skills:
16544
+ console.log(source_default.bold(`
16545
+ AI \u4E3A\u60A8\u63A8\u8350\u4EE5\u4E0B Skills (\u8F93\u5165\u5E8F\u53F7\u76F4\u63A5\u52A0\u8F7D):
15238
16546
  `));
15239
- routes.forEach((r2, i2) => {
15240
- const marker = i2 === 0 ? source_default.green("\u25B6") : source_default.dim(" ");
15241
- console.log(` ${marker} ${source_default.cyan(r2.id)} \u2014 ${r2.name}`);
15242
- if (i2 > 0) console.log(source_default.dim(` /sk ${r2.id} \u53EF\u6FC0\u6D3B\u6B64\u9879`));
15243
- });
15244
- console.log();
15245
- console.log(source_default.dim("\u5DF2\u81EA\u52A8\u6FC0\u6D3B\u7B2C\u4E00\u4E2A\uFF0C\u5982\u9700\u5207\u6362\u8BF7\u4F7F\u7528\u4E0A\u65B9\u547D\u4EE4"));
15246
- injectSkill(routes[0].id, history);
16547
+ llmRecs.forEach((r2, i2) => {
16548
+ const num = source_default.bold.cyan(`[${i2 + 1}]`);
16549
+ console.log(` ${num} ${source_default.cyan(r2.id)}`);
16550
+ console.log(` ${source_default.dim(r2.reason)}`);
16551
+ });
16552
+ console.log();
16553
+ console.log(source_default.dim(" \u8F93\u5165\u5E8F\u53F7 1-" + llmRecs.length + " \u52A0\u8F7D\uFF0C\u76F4\u63A5\u56DE\u8F66\u53D6\u6D88\uFF0C\u6216\u8F93\u5165 /sk <id> \u52A0\u8F7D\u5176\u4ED6"));
16554
+ console.log();
16555
+ const choice = await question(rl, source_default.blue(" \u9009\u62E9 \u203A "));
16556
+ const choiceNum = parseInt(choice.trim(), 10);
16557
+ if (!isNaN(choiceNum) && choiceNum >= 1 && choiceNum <= llmRecs.length) {
16558
+ await injectSkill(llmRecs[choiceNum - 1].id, history, injectedSkills, rl);
16559
+ } else if (choice.trim() === "") {
16560
+ console.log(source_default.dim(" \u5DF2\u53D6\u6D88"));
16561
+ } else {
16562
+ console.log(source_default.dim(` \u63D0\u793A: \u4F7F\u7528 /sk ${choice.trim()} \u52A0\u8F7D\u6307\u5B9A Skill`));
15247
16563
  }
15248
16564
  break;
15249
16565
  }
@@ -15279,8 +16595,8 @@ ${summary}` },
15279
16595
  console.log("\u7528\u6CD5: /cd <\u8DEF\u5F84>");
15280
16596
  break;
15281
16597
  }
15282
- const target = (0, import_path4.resolve)(arg.replace(/^~/, (0, import_os3.homedir)()));
15283
- if (!(0, import_fs4.existsSync)(target)) {
16598
+ const target = (0, import_path5.resolve)(arg.replace(/^~/, (0, import_os3.homedir)()));
16599
+ if (!(0, import_fs5.existsSync)(target)) {
15284
16600
  console.log(source_default.red(`\u8DEF\u5F84\u4E0D\u5B58\u5728: ${target}`));
15285
16601
  break;
15286
16602
  }
@@ -15293,11 +16609,12 @@ ${summary}` },
15293
16609
  break;
15294
16610
  case "tools":
15295
16611
  console.log(source_default.bold("AI \u53EF\u8C03\u7528\u7684\u5DE5\u5177:"));
15296
- console.log(` ${source_default.cyan("bash")} \u6267\u884C Shell \u547D\u4EE4 (R/Python/bash \u811A\u672C\u3001\u751F\u4FE1\u5DE5\u5177)`);
15297
- console.log(` ${source_default.cyan("read_file")} \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9 (\u652F\u6301 ~/ \u8DEF\u5F84)`);
16612
+ console.log(` ${source_default.cyan("bash")} \u6267\u884C Shell \u547D\u4EE4 (R/Python/bash \u811A\u672C\u3001\u751F\u4FE1\u5DE5\u5177) \u2014 \u5B9E\u65F6\u6D41\u5F0F\u8F93\u51FA`);
16613
+ console.log(` ${source_default.cyan("read_file")} \u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9 (\u652F\u6301 ~/ \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 500 \u884C)`);
15298
16614
  console.log(` ${source_default.cyan("write_file")} \u521B\u5EFA\u6216\u8986\u5199\u6587\u4EF6`);
15299
16615
  console.log(` ${source_default.cyan("list_dir")} \u5217\u51FA\u76EE\u5F55\u5185\u5BB9`);
15300
16616
  console.log(` ${source_default.cyan("search_files")} glob \u641C\u7D22\u6587\u4EF6 (\u5982 *.R, *.csv)`);
16617
+ console.log(` ${source_default.cyan("fetch_geo")} \u67E5\u8BE2 NCBI GEO \u6570\u636E\u5E93 (GSE/GDS/GPL/GSM \u7F16\u53F7)`);
15301
16618
  console.log();
15302
16619
  console.log(source_default.dim("\u63D0\u793A: \u76F4\u63A5\u63CF\u8FF0\u4EFB\u52A1\uFF0CAI \u4F1A\u81EA\u52A8\u51B3\u5B9A\u8C03\u7528\u54EA\u4E2A\u5DE5\u5177"));
15303
16620
  break;
@@ -15332,13 +16649,39 @@ async function main() {
15332
16649
  console.log(` ${source_default.bold("\u6A21\u578B:")} ${source_default.green(cfg.model)}`);
15333
16650
  console.log(` ${source_default.bold("Skills:")} ${totalSkills > 0 ? source_default.green(`${totalSkills} \u4E2A`) : source_default.yellow("\u672A\u5B89\u88C5")} ${source_default.dim("(/sk \u641C\u7D22 /cat \u5206\u7C7B\u76EE\u5F55)")}`);
15334
16651
  console.log(` ${source_default.bold("\u5DE5\u5177:")} bash \xB7 read_file \xB7 write_file \xB7 list_dir \xB7 search_files`);
16652
+ console.log(` ${source_default.bold("\u65B0\u529F\u80FD:")} /sessions /resume /checkpoint /run /check-env /install /diff`);
15335
16653
  console.log(source_default.bold.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
16654
+ const lastSess = getLastSession();
16655
+ if (lastSess) {
16656
+ const age = (() => {
16657
+ const diff = Date.now() - new Date(lastSess.updatedAt).getTime();
16658
+ const h2 = Math.floor(diff / 36e5);
16659
+ const d2 = Math.floor(diff / 864e5);
16660
+ return d2 > 0 ? `${d2}\u5929\u524D` : h2 > 0 ? `${h2}\u5C0F\u65F6\u524D` : "\u521A\u521A";
16661
+ })();
16662
+ console.log(source_default.dim(` \u4E0A\u6B21\u4F1A\u8BDD: ${lastSess.id} ${age} /resume \u6062\u590D`));
16663
+ }
15336
16664
  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"));
15337
16665
  console.log();
15338
16666
  const systemPrompt = buildSystemPrompt();
15339
16667
  let history = [];
15340
16668
  let thinkMode = false;
15341
- const injectedSkills = /* @__PURE__ */ new Set();
16669
+ const injectedSkills = /* @__PURE__ */ new Map();
16670
+ const sessionId = newSessionId();
16671
+ const sessionCreatedAt = (/* @__PURE__ */ new Date()).toISOString();
16672
+ SESSION_CTX.id = sessionId;
16673
+ SESSION_CTX.createdAt = sessionCreatedAt;
16674
+ const wdirSnapshot = snapshotWorkdir(process.cwd());
16675
+ SESSION_CTX.wdirSnapshot = wdirSnapshot;
16676
+ function autoSaveSession() {
16677
+ if (history.length === 0) return;
16678
+ try {
16679
+ saveSession(sessionId, sessionCreatedAt, history, Array.from(injectedSkills.keys()), sessionCreatedAt);
16680
+ } catch {
16681
+ }
16682
+ }
16683
+ let lastCheckpointMsgCount = 0;
16684
+ const CHECKPOINT_INTERVAL = 6;
15342
16685
  while (true) {
15343
16686
  let input;
15344
16687
  const thinkIndicator = thinkMode ? source_default.yellow("[\u601D\u8003]") + " " : "";
@@ -15355,7 +16698,7 @@ async function main() {
15355
16698
  break;
15356
16699
  }
15357
16700
  if (trimmed.startsWith("/")) {
15358
- const result = await handleCommand(trimmed, rl, history, thinkMode);
16701
+ const result = await handleCommand(trimmed, rl, history, thinkMode, injectedSkills);
15359
16702
  if (result.exit) break;
15360
16703
  if (result.clearHistory) {
15361
16704
  history = [];
@@ -15365,13 +16708,34 @@ async function main() {
15365
16708
  if (result.thinkMode !== void 0) thinkMode = result.thinkMode;
15366
16709
  continue;
15367
16710
  }
16711
+ if (trimmed.startsWith("!")) {
16712
+ const cmd = trimmed.slice(1).trim();
16713
+ if (!cmd) {
16714
+ console.log(source_default.yellow("\u7528\u6CD5: !<\u547D\u4EE4> \u4F8B: !ls -la !Rscript analysis.R !python script.py"));
16715
+ continue;
16716
+ }
16717
+ console.log(source_default.dim(`[\u76F4\u63A5\u6267\u884C] ${cmd}`));
16718
+ const t0 = Date.now();
16719
+ const result = await executeTool("bash", { command: cmd }, (chunk) => {
16720
+ process.stdout.write(source_default.dim(" \u2502 ") + chunk);
16721
+ });
16722
+ const elapsed = ((Date.now() - t0) / 1e3).toFixed(1);
16723
+ if (result.error) {
16724
+ console.log(source_default.yellow(`
16725
+ \u2717 ${result.error} ${source_default.dim("(" + elapsed + "s)")}`));
16726
+ } else {
16727
+ console.log(source_default.green(`
16728
+ \u2713 \u5B8C\u6210 ${source_default.dim("(" + elapsed + "s)")}`));
16729
+ }
16730
+ console.log();
16731
+ continue;
16732
+ }
15368
16733
  const { routes: suggestedRoutes, topScore } = routeSkill(trimmed);
15369
16734
  const newRoutes = suggestedRoutes.filter((r2) => !injectedSkills.has(r2.id));
15370
16735
  if (newRoutes.length === 1 && topScore >= 8) {
15371
16736
  const r2 = newRoutes[0];
15372
- const ok = injectSkill(r2.id, history);
16737
+ const ok = await injectSkill(r2.id, history, injectedSkills, rl, true);
15373
16738
  if (ok) {
15374
- injectedSkills.add(r2.id);
15375
16739
  console.log(source_default.dim(" (\u63D0\u793A: /clear \u53EF\u6E05\u9664\u4E0A\u4E0B\u6587\u540E\u5207\u6362 Skill)"));
15376
16740
  }
15377
16741
  } else if (newRoutes.length >= 2 && topScore >= 4) {
@@ -15390,6 +16754,17 @@ ${expanded}` : expanded;
15390
16754
  const reply = await chat(history, currentCfg, systemPrompt);
15391
16755
  history.push({ role: "assistant", content: reply });
15392
16756
  history = await maybeCompact(history, currentCfg);
16757
+ autoSaveSession();
16758
+ if (history.length - lastCheckpointMsgCount >= CHECKPOINT_INTERVAL) {
16759
+ const label = `\u7B2C ${Math.floor(history.length / 2)} \u8F6E`;
16760
+ saveCheckpoint(sessionId, label, history, Array.from(injectedSkills.keys()));
16761
+ lastCheckpointMsgCount = history.length;
16762
+ }
16763
+ if (injectedSkills.size > 0) {
16764
+ const ids = Array.from(injectedSkills.keys()).join(" \xB7 ");
16765
+ console.log(source_default.dim(`
16766
+ [\u6FC0\u6D3B Skill: ${ids}]`));
16767
+ }
15393
16768
  } catch (err) {
15394
16769
  const msg = err instanceof Error ? err.message : String(err);
15395
16770
  console.error(source_default.red(`