@helixlife-ai/xiantao 0.1.16 → 0.1.18

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.
package/README.md CHANGED
@@ -37,9 +37,27 @@ This starts a menu-driven path for choosing a tool, selecting demo data or a loc
37
37
  xt tool prepare violin_flat --file ./data.xlsx --profile opencode --json
38
38
  xt tool exec violin_flat --file ./data.xlsx --profile opencode --json
39
39
  xt tool exec violin_flat --file ./data.xlsx --profile opencode --json --download ./violin.pdf
40
+
41
+ xt tool exec gsea_analysis --demo --profile opencode --json
42
+ xt tool prepare gsea_plot --profile opencode --json
43
+ xt tool exec gsea_plot --profile opencode --json --download ./gsea_plot.pdf
44
+ xt tool exec gsea_customize --file gene_set=./gene_set.gmt --file data=./rank.xlsx --profile opencode --json
40
45
  ```
41
46
 
42
- For machine callers, use `xt tool prepare` to fetch the upload-dependent final schema, then `xt tool exec` to submit. Keep `--json`, pass an explicit profile with `--profile`, and avoid interactive-only flows.
47
+ For machine callers, use `xt tool prepare` to fetch the final schema, then `xt tool exec` to submit. Keep `--json`, pass an explicit profile with `--profile`, and avoid interactive-only flows.
48
+ The examples below illustrate command shapes, not a fixed allowlist of tools. Apply the same patterns to any tool with the same input mode.
49
+
50
+ `xt tool prepare` and `xt tool exec` support three input patterns:
51
+ - upload modules: pass `--file <xlsx>`
52
+ - multi-upload modules: repeat `--file key=path`, for example `--file gene_set=./gene_set.gmt --file data=./rank.xlsx`
53
+ - demo-capable upload modules: pass `--demo`
54
+ - history-record modules such as `gsea_plot`: pass neither `--file` nor `--demo`; the CLI reuses the default `XTRecord` returned by `tool inspect` / `tool prepare`
55
+
56
+ Current operational notes for agent-style callers:
57
+ - serialize `xt tool exec` submits per profile; Xiantao may reject overlapping or too-frequent submits with `NETWORK_ERROR`
58
+ - `tool inspect` now includes the full `args_data` payload in JSON output and summarizes every detected `XTUpload` field
59
+ - for multi-upload modules, pass keyed uploads in the same field names returned by `tool inspect`
60
+ - `gsea_customize` has been validated through `tool prepare` and `tool exec` with repeated `--file gene_set=... --file data=...`
43
61
 
44
62
  For agent workflows, do not use `xt tool run`. Treat `xt tool run` as the human-first path and `xt tool exec` as the machine path.
45
63
 
@@ -75,6 +93,7 @@ For agent workflows, do not use `xt tool run`. Treat `xt tool run` as the human-
75
93
  - `xt tool resolve`
76
94
  - `xt tool menu`
77
95
  - `xt tool menus`
96
+ - `xt tool menus-raw`
78
97
  - `xt tool fetch-all`
79
98
 
80
99
  ## Notes
@@ -83,7 +102,9 @@ For the common Xiantao tool product, `tool search`, `tool inspect`, `tool run`,
83
102
 
84
103
  The CLI also loads a project-root `.env` file on startup. You can set `XIANTAO_BASE_URL=https://agent.helixlife.cn` there to switch the API base URL without prefixing each command manually.
85
104
 
86
- `xt tool search <keyword>` searches remote tool menus by tool id, title, path, and route. `xt tool inspect <tool_id>` prints resolved metadata together with the dynamic `args_main` schema used by the tool. `xt tool run <tool_id>` runs a Xiantao bioinformatics tool in the human-first path. `xt tool exec <tool_id>` is the agent-safe submit command and also supports `--download <path>` when a local artifact is needed. `xt tool resolve <tool_id>` remains available as the low-level UUID and route lookup. `--demo` reuses the demo file metadata returned by `tool fetch-all`; otherwise `tool run` uploads the local file first and therefore also needs token auth. Run `xt login` first to authorize the stored token. When a tool exposes multiple downloadable results, `--download <path>` uses the output file extension to select the matching artifact.
105
+ `xt tool search <keyword>` searches remote tool menus by tool id, title, path, and route. `xt tool inspect <tool_id>` prints resolved metadata together with the dynamic `args_main` schema, the full `args_data` payload in JSON mode, and the detected input source summary, including per-field `XTUpload` defaults and `XTRecord` defaults. `xt tool run <tool_id>` runs a Xiantao bioinformatics tool in the human-first path. `xt tool exec <tool_id>` is the agent-safe submit command and also supports `--download <path>` when a local artifact is needed. `xt tool resolve <tool_id>` remains available as the low-level UUID and route lookup. `xt tool menus` returns a concise catalog with each tool title, `module_id`, `path`, and `route`, which is useful when `tool search` finds nothing helpful or when you want to shortlist tools for recommendation. `--demo` reuses the demo file metadata returned by `tool fetch-all`; `--file` uploads a local file first; multi-upload modules accept repeated `--file key=path`; for `XTRecord` modules the CLI can submit directly from the default history record returned by `fetch-all`. Run `xt login` first to authorize the stored token. When a tool exposes multiple downloadable results, `--download <path>` uses the output file extension to select the matching artifact.
106
+
107
+ For the current GSEA tool family, `gsea_plot`, `gsea_plot2`, `gsea_bar`, `gsea_point`, `gsea_ridge`, `gsea_emapplot`, `gsea_tree`, and `gsea_path_cluster` are record-backed visual modules that reuse the default `gsea_analysis` history record. `gsea_analysis` itself is an upload/demo analysis module. `gsea_customize` is a custom gene-set upload module whose frontend expects two uploads, `gene_set` and `data`; submit it with `--file gene_set=... --file data=...`. A real CLI submit path has been verified with the sample files in [`docs/GSEA-基因集.csv`](/Users/chenyunjie/Desktop/xtz-cli/xiantao/docs/GSEA-基因集.csv) and [`docs/GSEA-分子.xlsx`](/Users/chenyunjie/Desktop/xtz-cli/xiantao/docs/GSEA-分子.xlsx).
87
108
 
88
109
  `xt tool run <tool_id> ... --interactive` prompts for dynamic `args_main` values before the final submit, prints reusable `--set` flags, and supports `<` for the previous item plus `/skip` for the current section.
89
110
 
@@ -0,0 +1,31 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { XtzCommand } from '../../base-command.js';
3
+ import { getAuthContext } from '../../lib/auth.js';
4
+ import { globalFlags } from '../../lib/flags.js';
5
+ import { resolveXiantaoToolProductUuid } from '../../lib/storage.js';
6
+ import { fetchToolMenus } from '../../lib/xiantao.js';
7
+ export default class PlotMenusRaw extends XtzCommand {
8
+ static description = 'Fetch all raw Xiantao menu metadata for a tool product';
9
+ static flags = {
10
+ ...globalFlags,
11
+ token: Flags.string({
12
+ description: 'Bearer token for agent.helixlife.cn xiantao APIs; falls back to the stored agent token',
13
+ }),
14
+ toolProductUuid: Flags.string({
15
+ description: 'tool_product_uuid used in the menu filter payload; defaults to the main Xiantao product UUID',
16
+ }),
17
+ };
18
+ async run() {
19
+ this.warnIfLegacyPlotCommand();
20
+ const { flags } = await this.parse(PlotMenusRaw);
21
+ const agent = await this.getProfile(flags.profile);
22
+ const auth = await getAuthContext(agent, flags.token);
23
+ const toolProductUuid = await resolveXiantaoToolProductUuid(flags.toolProductUuid);
24
+ const menus = await fetchToolMenus(auth, toolProductUuid);
25
+ this.print(flags, {
26
+ agent,
27
+ menus,
28
+ tool_product_uuid: toolProductUuid,
29
+ }, JSON.stringify(menus, null, 2), { profile: agent });
30
+ }
31
+ }
@@ -3,9 +3,9 @@ import { XtzCommand } from '../../base-command.js';
3
3
  import { getAuthContext } from '../../lib/auth.js';
4
4
  import { globalFlags } from '../../lib/flags.js';
5
5
  import { resolveXiantaoToolProductUuid } from '../../lib/storage.js';
6
- import { fetchToolMenus } from '../../lib/xiantao.js';
6
+ import { fetchToolMenus, listToolMenuSummaries } from '../../lib/xiantao.js';
7
7
  export default class PlotMenus extends XtzCommand {
8
- static description = 'Fetch all Xiantao menu metadata for a tool product';
8
+ static description = 'List Xiantao tools with title, tool id, path, and route for a product';
9
9
  static flags = {
10
10
  ...globalFlags,
11
11
  token: Flags.string({
@@ -21,11 +21,27 @@ export default class PlotMenus extends XtzCommand {
21
21
  const agent = await this.getProfile(flags.profile);
22
22
  const auth = await getAuthContext(agent, flags.token);
23
23
  const toolProductUuid = await resolveXiantaoToolProductUuid(flags.toolProductUuid);
24
- const menus = await fetchToolMenus(auth, toolProductUuid);
24
+ const menus = listToolMenuSummaries(await fetchToolMenus(auth, toolProductUuid));
25
25
  this.print(flags, {
26
26
  agent,
27
27
  menus,
28
28
  tool_product_uuid: toolProductUuid,
29
- }, JSON.stringify(menus, null, 2), { profile: agent });
29
+ }, this.formatMenusText(menus), { profile: agent });
30
+ }
31
+ formatMenusText(menus) {
32
+ if (menus.length === 0) {
33
+ return '未找到可用工具';
34
+ }
35
+ const lines = [`共 ${menus.length} 个工具`];
36
+ for (const menu of menus) {
37
+ lines.push('');
38
+ lines.push(`${menu.title}`);
39
+ lines.push(` ${menu.module_id}`);
40
+ if (menu.path) {
41
+ lines.push(` ${menu.path}`);
42
+ }
43
+ lines.push(` ${menu.route}`);
44
+ }
45
+ return lines.join('\n');
30
46
  }
31
47
  }
@@ -6,16 +6,26 @@ import { downloadToPath } from '../../lib/download.js';
6
6
  import { XtzError } from '../../lib/errors.js';
7
7
  import { globalFlags } from '../../lib/flags.js';
8
8
  import { fetchXiantaoHistoryDownloadButtons, getXiantaoHistoryRecordStatus, waitForLatestXiantaoHistoryRecord, waitForXiantaoHistoryRecordCompletion, } from '../../lib/history.js';
9
- import { deepMerge, isJsonObject } from '../../lib/json.js';
9
+ import { deepMerge } from '../../lib/json.js';
10
10
  import { confirmPlotArgsMain, promptPlotArgsMain, promptPlotFilePath, promptPlotInputSource, promptPlotMenuSelection, promptPlotResultSelection, } from '../../lib/plot-interactive.js';
11
11
  import { uploadFile } from '../../lib/plot.js';
12
12
  import { parseSetOverrides, plotOverrideFlags } from '../../lib/plot-options.js';
13
13
  import { buildArgsMainFromSchema } from '../../lib/plot-schema.js';
14
14
  import { loadPlotArgsMainConfig, resolveXiantaoToolProductUuid } from '../../lib/storage.js';
15
- import { resolveXiantaoDownload, submitXiantaoPlot, verifyXiantaoPlotUpload } from '../../lib/xiantao-plot.js';
15
+ import { findDefaultXiantaoRecord, listXiantaoUploadFields } from '../../lib/tool-inputs.js';
16
+ import { resolveXiantaoDownload, submitXiantaoPlot, verifyXiantaoPlotUpload, } from '../../lib/xiantao-plot.js';
16
17
  import { fetchAllPlotArgs, fetchToolMenu, fetchToolMenus, resolveToolMenuByCode } from '../../lib/xiantao.js';
17
18
  export class ToolRunCommandBase extends XtzCommand {
18
- async prepareToolRun(requestedModuleId, filePath, flags) {
19
+ resolveToolFileInputs(positionalFile, flagFiles) {
20
+ if (positionalFile && flagFiles && flagFiles.length > 0) {
21
+ throw new XtzError('FILE 与 `--file` 不能同时使用。');
22
+ }
23
+ if (positionalFile) {
24
+ return [positionalFile];
25
+ }
26
+ return flagFiles ?? [];
27
+ }
28
+ async prepareToolRun(requestedModuleId, fileInputs, flags) {
19
29
  const agent = await this.getProfile(flags.profile);
20
30
  const auth = await getAuthContext(agent, flags.token);
21
31
  const resolved = await this.resolveModule(requestedModuleId, flags, auth);
@@ -23,41 +33,45 @@ export class ToolRunCommandBase extends XtzCommand {
23
33
  const fetchAllResult = await fetchAllPlotArgs(auth, { moduleId, uuid });
24
34
  const inputMode = await this.resolveInputMode(fetchAllResult, {
25
35
  demo: flags.demo,
26
- filePath,
36
+ fileInputs,
27
37
  interactive: flags.interactive,
28
38
  });
29
39
  const configArgsMain = await loadPlotArgsMainConfig(moduleId, flags.config);
30
40
  const overrides = deepMerge(configArgsMain, parseSetOverrides(flags.set));
31
41
  let schema = fetchAllResult.args_main ?? [];
32
42
  let argsMain = deepMerge(buildArgsMainFromSchema(schema), overrides);
33
- const { duid, upload, schema: verifiedSchema } = inputMode.mode === 'demo'
43
+ const preparedInput = inputMode.mode === 'demo'
34
44
  ? this.resolveDemoInput(fetchAllResult, moduleId)
35
- : await this.uploadAndVerify(agent, inputMode.filePath, {
36
- argsMain,
37
- moduleId,
38
- uuid,
39
- }, auth);
40
- if (verifiedSchema.length) {
41
- schema = verifiedSchema;
45
+ : inputMode.mode === 'record'
46
+ ? this.resolveRecordInput(fetchAllResult, moduleId)
47
+ : await this.uploadAndVerify(agent, inputMode.fileInputs ?? [], {
48
+ argsMain,
49
+ argsData: fetchAllResult.args_data ?? [],
50
+ moduleId,
51
+ uuid,
52
+ }, auth);
53
+ if (preparedInput.schema.length) {
54
+ schema = preparedInput.schema;
42
55
  argsMain = deepMerge(buildArgsMainFromSchema(schema), overrides);
43
56
  }
44
57
  return {
45
58
  agent,
46
59
  argsMain,
47
60
  auth,
48
- duid,
61
+ duid: preparedInput.duid,
62
+ input: preparedInput.input,
49
63
  inputMode: inputMode.mode,
64
+ inputSummary: this.summarizePreparedInput(preparedInput.input),
50
65
  moduleId,
51
66
  notice: fetchAllResult.notice ?? '',
52
67
  route,
53
68
  schema,
54
69
  toolProductUuid,
55
- upload,
56
70
  uuid,
57
71
  };
58
72
  }
59
- async executeToolRun(requestedModuleId, filePath, flags) {
60
- const prepared = await this.prepareToolRun(requestedModuleId, filePath, flags);
73
+ async executeToolRun(requestedModuleId, fileInputs, flags) {
74
+ const prepared = await this.prepareToolRun(requestedModuleId, fileInputs, flags);
61
75
  let argsMain = prepared.argsMain;
62
76
  if (flags.interactive) {
63
77
  argsMain = await promptPlotArgsMain(prepared.schema ?? [], argsMain);
@@ -67,9 +81,9 @@ export class ToolRunCommandBase extends XtzCommand {
67
81
  const result = await submitXiantaoPlot(prepared.auth, {
68
82
  argsMain,
69
83
  duid: prepared.duid,
84
+ input: prepared.input,
70
85
  moduleId: prepared.moduleId,
71
86
  route: prepared.route,
72
- upload: prepared.upload,
73
87
  uuid: prepared.uuid,
74
88
  });
75
89
  if (result.asyncSubmission) {
@@ -105,11 +119,9 @@ export class ToolRunCommandBase extends XtzCommand {
105
119
  downloads: result.downloads,
106
120
  duid: prepared.duid,
107
121
  file: result.file ?? null,
108
- file_name: prepared.upload.fileName,
109
- file_oss: prepared.upload.fileOss,
110
- file_size: prepared.upload.fileSize,
111
122
  history: null,
112
123
  history_hint: null,
124
+ ...prepared.inputSummary,
113
125
  mode: prepared.inputMode,
114
126
  module_id: prepared.moduleId,
115
127
  notice: prepared.notice,
@@ -141,11 +153,9 @@ export class ToolRunCommandBase extends XtzCommand {
141
153
  downloads: [],
142
154
  duid: prepared.duid,
143
155
  file: null,
144
- file_name: prepared.upload.fileName,
145
- file_oss: prepared.upload.fileOss,
146
- file_size: prepared.upload.fileSize,
147
156
  history: recordedHistory,
148
157
  history_hint: this.buildHistoryHint(recordedHistory),
158
+ ...prepared.inputSummary,
149
159
  mode: prepared.inputMode,
150
160
  module_id: prepared.moduleId,
151
161
  notice: prepared.notice,
@@ -169,11 +179,9 @@ export class ToolRunCommandBase extends XtzCommand {
169
179
  downloads: [],
170
180
  duid: prepared.duid,
171
181
  file: null,
172
- file_name: prepared.upload.fileName,
173
- file_oss: prepared.upload.fileOss,
174
- file_size: prepared.upload.fileSize,
175
182
  history: recordedHistory,
176
183
  history_hint: this.buildHistoryHint(recordedHistory),
184
+ ...prepared.inputSummary,
177
185
  mode: prepared.inputMode,
178
186
  module_id: prepared.moduleId,
179
187
  notice: prepared.notice,
@@ -203,11 +211,9 @@ export class ToolRunCommandBase extends XtzCommand {
203
211
  downloads: [],
204
212
  duid: prepared.duid,
205
213
  file: null,
206
- file_name: prepared.upload.fileName,
207
- file_oss: prepared.upload.fileOss,
208
- file_size: prepared.upload.fileSize,
209
214
  history: finalHistory,
210
215
  history_hint: this.buildHistoryHint(finalHistory),
216
+ ...prepared.inputSummary,
211
217
  mode: prepared.inputMode,
212
218
  module_id: prepared.moduleId,
213
219
  notice: prepared.notice,
@@ -235,11 +241,9 @@ export class ToolRunCommandBase extends XtzCommand {
235
241
  downloads,
236
242
  duid: prepared.duid,
237
243
  file: null,
238
- file_name: prepared.upload.fileName,
239
- file_oss: prepared.upload.fileOss,
240
- file_size: prepared.upload.fileSize,
241
244
  history: finalHistory,
242
245
  history_hint: this.buildHistoryHint(finalHistory),
246
+ ...prepared.inputSummary,
243
247
  mode: prepared.inputMode,
244
248
  module_id: prepared.moduleId,
245
249
  notice: prepared.notice,
@@ -431,18 +435,21 @@ export class ToolRunCommandBase extends XtzCommand {
431
435
  if (input.demo) {
432
436
  return { mode: 'demo' };
433
437
  }
434
- if (input.filePath) {
438
+ if (input.fileInputs.length > 0) {
435
439
  return {
436
- filePath: input.filePath,
440
+ fileInputs: input.fileInputs,
437
441
  mode: 'upload',
438
442
  };
439
443
  }
444
+ if (this.hasRecordInput(fetchAllResult)) {
445
+ return { mode: 'record' };
446
+ }
440
447
  if (!input.interactive) {
441
448
  return { mode: 'upload' };
442
449
  }
443
450
  if (!this.hasDemoInput(fetchAllResult)) {
444
451
  return {
445
- filePath: await promptPlotFilePath(),
452
+ fileInputs: [await promptPlotFilePath()],
446
453
  mode: 'upload',
447
454
  };
448
455
  }
@@ -451,7 +458,7 @@ export class ToolRunCommandBase extends XtzCommand {
451
458
  return { mode };
452
459
  }
453
460
  return {
454
- filePath: await promptPlotFilePath(),
461
+ fileInputs: [await promptPlotFilePath()],
455
462
  mode,
456
463
  };
457
464
  }
@@ -460,22 +467,25 @@ export class ToolRunCommandBase extends XtzCommand {
460
467
  if (!duid) {
461
468
  throw new XtzError(`模块 ${moduleId} 未返回 demo duid,无法使用 \`--demo\`。`);
462
469
  }
463
- const uploadField = (fetchAllResult.args_data ?? []).find((item) => isJsonObject(item) && item.type === 'XTUpload');
464
- const defaultValue = isJsonObject(uploadField) && isJsonObject(uploadField.default_value) ? uploadField.default_value : undefined;
465
- const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
466
- const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
467
- const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
468
- if (!fileName || !fileOss || typeof fileSize !== 'number') {
470
+ const uploadFields = listXiantaoUploadFields(fetchAllResult.args_data ?? []);
471
+ if (!uploadFields.length || uploadFields.some((field) => !field.defaultUpload)) {
469
472
  throw new XtzError(`模块 ${moduleId} 未返回可用的 demo 文件信息,无法使用 \`--demo\`。`);
470
473
  }
471
474
  return {
472
475
  duid,
473
- schema: [],
474
- upload: {
475
- fileName,
476
- fileOss,
477
- fileSize,
476
+ input: {
477
+ kind: 'upload',
478
+ uploads: uploadFields.map((field) => ({
479
+ key: field.key,
480
+ label: field.label,
481
+ upload: {
482
+ fileName: field.defaultUpload.fileName,
483
+ fileOss: field.defaultUpload.fileOss,
484
+ fileSize: field.defaultUpload.fileSize,
485
+ },
486
+ })),
478
487
  },
488
+ schema: [],
479
489
  };
480
490
  }
481
491
  hasDemoInput(fetchAllResult) {
@@ -483,31 +493,180 @@ export class ToolRunCommandBase extends XtzCommand {
483
493
  if (!duid) {
484
494
  return false;
485
495
  }
486
- const uploadField = (fetchAllResult.args_data ?? []).find((item) => isJsonObject(item) && item.type === 'XTUpload');
487
- const defaultValue = isJsonObject(uploadField) && isJsonObject(uploadField.default_value) ? uploadField.default_value : undefined;
488
- const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
489
- const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
490
- const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
491
- return Boolean(fileName && fileOss && typeof fileSize === 'number');
496
+ const uploadFields = listXiantaoUploadFields(fetchAllResult.args_data ?? []);
497
+ return uploadFields.length > 0 && uploadFields.every((field) => Boolean(field.defaultUpload));
492
498
  }
493
- async uploadAndVerify(agent, filePath, input, auth) {
494
- if (!filePath) {
495
- throw new XtzError('通用模式请传入 FILE,或改用 `--demo`。');
499
+ resolveRecordInput(fetchAllResult, moduleId) {
500
+ const duid = fetchAllResult.id?.duid?.trim();
501
+ if (!duid) {
502
+ throw new XtzError(`模块 ${moduleId} 未返回 record duid,无法使用历史记录输入。`);
496
503
  }
497
- const uploadAuth = await getAuthContext(agent);
498
- const upload = await uploadFile(uploadAuth, filePath);
504
+ const record = findDefaultXiantaoRecord(fetchAllResult.args_data ?? []);
505
+ if (!record) {
506
+ throw new XtzError(`模块 ${moduleId} 未返回可用的数据记录,无法直接运行。`);
507
+ }
508
+ return {
509
+ duid,
510
+ input: {
511
+ kind: 'record',
512
+ record: {
513
+ key: record.key,
514
+ label: record.label,
515
+ moduleId: record.moduleId,
516
+ moduleName: record.moduleName,
517
+ recordId: record.recordId,
518
+ recordName: record.recordName,
519
+ time: record.time,
520
+ },
521
+ },
522
+ schema: [],
523
+ };
524
+ }
525
+ hasRecordInput(fetchAllResult) {
526
+ return Boolean(fetchAllResult.id?.duid?.trim() && findDefaultXiantaoRecord(fetchAllResult.args_data ?? []));
527
+ }
528
+ async uploadAndVerify(agent, fileInputs, input, auth) {
529
+ const uploads = await this.uploadFiles(agent, fileInputs, input.argsData ?? [], input.moduleId);
499
530
  const verified = await verifyXiantaoPlotUpload(auth, {
500
531
  argsMain: input.argsMain,
501
532
  moduleId: input.moduleId,
502
- upload,
533
+ uploads,
503
534
  uuid: input.uuid,
504
535
  });
505
536
  return {
506
537
  duid: verified.duid,
538
+ input: {
539
+ kind: 'upload',
540
+ uploads,
541
+ },
507
542
  schema: verified.schema,
508
- upload,
509
543
  };
510
544
  }
545
+ summarizePreparedInput(input) {
546
+ if (input.kind === 'record') {
547
+ return {
548
+ file_name: null,
549
+ file_oss: null,
550
+ file_size: null,
551
+ input_record: {
552
+ label: input.record.label,
553
+ module_id: input.record.moduleId,
554
+ module_name: input.record.moduleName,
555
+ record_id: input.record.recordId,
556
+ record_name: input.record.recordName,
557
+ time: input.record.time,
558
+ },
559
+ input_uploads: null,
560
+ };
561
+ }
562
+ const inputUploads = input.uploads.map((item) => ({
563
+ file_name: item.upload.fileName,
564
+ file_oss: item.upload.fileOss,
565
+ file_size: item.upload.fileSize,
566
+ key: item.key,
567
+ label: item.label,
568
+ }));
569
+ const singularUpload = inputUploads.length === 1 ? inputUploads[0] : null;
570
+ return {
571
+ file_name: singularUpload?.file_name ?? null,
572
+ file_oss: singularUpload?.file_oss ?? null,
573
+ file_size: singularUpload?.file_size ?? null,
574
+ input_record: null,
575
+ input_uploads: inputUploads,
576
+ };
577
+ }
578
+ async uploadFiles(agent, fileInputs, argsData, moduleId) {
579
+ const uploadFields = listXiantaoUploadFields(argsData ?? []);
580
+ const uploadSpecs = this.resolveUploadInputs(fileInputs, uploadFields, moduleId);
581
+ const uploadAuth = await getAuthContext(agent);
582
+ const uploads = [];
583
+ for (const item of uploadSpecs) {
584
+ const upload = await uploadFile(uploadAuth, item.filePath);
585
+ uploads.push({
586
+ key: item.key,
587
+ label: item.label,
588
+ upload,
589
+ });
590
+ }
591
+ return uploads;
592
+ }
593
+ resolveUploadInputs(fileInputs, uploadFields, moduleId) {
594
+ if (!uploadFields.length) {
595
+ throw new XtzError(`模块 ${moduleId} 未检测到可上传输入,不能使用 \`--file\`。`);
596
+ }
597
+ if (!fileInputs.length) {
598
+ if (uploadFields.length === 1) {
599
+ throw new XtzError('通用模式请传入 FILE / `--file <xlsx>`,或改用 `--demo`。');
600
+ }
601
+ throw new XtzError(`模块 ${moduleId} 需要多个上传字段,请使用 \`--file key=path\`。可用字段: ${uploadFields.map((field) => field.key).join(', ')}`);
602
+ }
603
+ const [singleUploadField] = uploadFields;
604
+ if (uploadFields.length === 1 && fileInputs.length === 1) {
605
+ const parsed = this.parseUploadFileInput(fileInputs[0]);
606
+ if (parsed.key && parsed.key !== singleUploadField.key) {
607
+ throw new XtzError(`模块 ${moduleId} 仅支持上传字段 ${singleUploadField.key},收到 ${parsed.key}。`);
608
+ }
609
+ return [{
610
+ filePath: parsed.path,
611
+ key: singleUploadField.key,
612
+ label: singleUploadField.label,
613
+ }];
614
+ }
615
+ const fieldsByKey = new Map(uploadFields.map((field) => [field.key, field]));
616
+ const assignments = new Map();
617
+ for (const rawValue of fileInputs) {
618
+ const parsed = this.parseUploadFileInput(rawValue);
619
+ if (!parsed.key) {
620
+ if (uploadFields.length === 1) {
621
+ if (assignments.has(singleUploadField.key)) {
622
+ throw new XtzError(`模块 ${moduleId} 仅需要 1 个上传文件,收到重复输入。`);
623
+ }
624
+ assignments.set(singleUploadField.key, parsed.path);
625
+ continue;
626
+ }
627
+ throw new XtzError(`模块 ${moduleId} 需要多个上传字段,请使用 \`--file key=path\`。可用字段: ${uploadFields.map((field) => field.key).join(', ')}`);
628
+ }
629
+ const field = fieldsByKey.get(parsed.key);
630
+ if (!field) {
631
+ throw new XtzError(`模块 ${moduleId} 不存在上传字段 ${parsed.key}。可用字段: ${uploadFields.map((item) => item.key).join(', ')}`);
632
+ }
633
+ if (assignments.has(field.key)) {
634
+ throw new XtzError(`上传字段 ${field.key} 重复传入。`);
635
+ }
636
+ assignments.set(field.key, parsed.path);
637
+ }
638
+ const missingFields = uploadFields.filter((field) => field.required && !assignments.has(field.key));
639
+ if (missingFields.length > 0) {
640
+ throw new XtzError(`模块 ${moduleId} 缺少必填上传字段: ${missingFields.map((field) => field.key).join(', ')}`);
641
+ }
642
+ return uploadFields.flatMap((field) => {
643
+ const filePath = assignments.get(field.key);
644
+ if (!filePath) {
645
+ return [];
646
+ }
647
+ return [{
648
+ filePath,
649
+ key: field.key,
650
+ label: field.label,
651
+ }];
652
+ });
653
+ }
654
+ parseUploadFileInput(input) {
655
+ const normalized = input.trim();
656
+ if (!normalized) {
657
+ throw new XtzError('`--file` 不能为空。');
658
+ }
659
+ const separatorIndex = normalized.indexOf('=');
660
+ if (separatorIndex <= 0) {
661
+ return { path: normalized };
662
+ }
663
+ const key = normalized.slice(0, separatorIndex).trim();
664
+ const path = normalized.slice(separatorIndex + 1).trim();
665
+ if (!key || !path) {
666
+ throw new XtzError('多上传模块请使用 `--file key=path`。');
667
+ }
668
+ return { key, path };
669
+ }
511
670
  async resolveModule(requestedModuleId, flags, auth) {
512
671
  const moduleId = requestedModuleId?.trim();
513
672
  const explicitRoute = flags.route?.trim();
@@ -605,7 +764,8 @@ export default class PlotRun extends ToolRunCommandBase {
605
764
  description: 'Use the module demo data returned by fetch-all instead of uploading a local file',
606
765
  }),
607
766
  file: Flags.string({
608
- description: 'Path to the local xlsx file; optional in --interactive mode',
767
+ description: 'Path to the local xlsx file; repeat or use KEY=PATH for named upload fields',
768
+ multiple: true,
609
769
  }),
610
770
  download: Flags.string({
611
771
  description: 'Download the generated file or preferred analysis result to this local path after submission',
@@ -636,10 +796,8 @@ export default class PlotRun extends ToolRunCommandBase {
636
796
  if (flags.interactive && flags.json) {
637
797
  throw new XtzError('`--interactive` 不能与 `--json` 同时使用。');
638
798
  }
639
- if (args.file && flags.file) {
640
- throw new XtzError('FILE `--file` 不能同时使用。');
641
- }
642
- const result = await this.executeToolRun(args.tool, args.file ?? flags.file, flags);
799
+ const fileInputs = this.resolveToolFileInputs(args.file, flags.file);
800
+ const result = await this.executeToolRun(args.tool, fileInputs, flags);
643
801
  this.print(flags, result.data, result.text, { profile: result.data.agent });
644
802
  }
645
803
  }
@@ -6,7 +6,7 @@ import { plotOverrideFlags } from '../../lib/plot-options.js';
6
6
  import { ToolRunCommandBase } from '../plot/run.js';
7
7
  export default class ToolExec extends ToolRunCommandBase {
8
8
  static description = 'Execute a Xiantao bioinformatics tool in agent mode';
9
- static usage = 'tool exec <tool> (--file <value> | --demo) --json [--download <path>]';
9
+ static usage = 'tool exec <tool> [--file <value> ... | --demo] --json [--download <path>]';
10
10
  static args = {
11
11
  tool: Args.string({
12
12
  description: 'Tool id, e.g. violin_flat',
@@ -27,7 +27,8 @@ export default class ToolExec extends ToolRunCommandBase {
27
27
  description: 'Download the generated file or preferred analysis result to this local path after submission',
28
28
  }),
29
29
  file: Flags.string({
30
- description: 'Path to the local xlsx file',
30
+ description: 'Path to the local xlsx file; repeat or use KEY=PATH for named upload fields',
31
+ multiple: true,
31
32
  }),
32
33
  interactive: Flags.boolean({
33
34
  default: false,
@@ -59,17 +60,11 @@ export default class ToolExec extends ToolRunCommandBase {
59
60
  if (!flags.json) {
60
61
  throw new XtzError('`xt tool exec` 仅支持 `--json` 输出。');
61
62
  }
62
- if (args.file && flags.file) {
63
- throw new XtzError('FILE `--file` 不能同时使用。');
64
- }
65
- if (flags.demo && flags.file) {
63
+ const fileInputs = this.resolveToolFileInputs(args.file, flags.file);
64
+ if (flags.demo && fileInputs.length > 0) {
66
65
  throw new XtzError('`--demo` 与 `--file` 不能同时使用。');
67
66
  }
68
- const filePath = args.file ?? flags.file;
69
- if (!flags.demo && !filePath) {
70
- throw new XtzError('`xt tool exec` 需要 `--file`,或显式传入 `--demo`。');
71
- }
72
- const result = await this.executeToolRun(args.tool, filePath, {
67
+ const result = await this.executeToolRun(args.tool, fileInputs, {
73
68
  ...flags,
74
69
  interactive: false,
75
70
  });
@@ -3,8 +3,8 @@ import { XtzCommand } from '../../base-command.js';
3
3
  import { getAuthContext } from '../../lib/auth.js';
4
4
  import { globalFlags } from '../../lib/flags.js';
5
5
  import { buildArgsMainFromSchema } from '../../lib/plot-schema.js';
6
- import { isJsonObject } from '../../lib/json.js';
7
6
  import { resolveXiantaoToolProductUuid } from '../../lib/storage.js';
7
+ import { findDefaultXiantaoRecord, listXiantaoUploadFields } from '../../lib/tool-inputs.js';
8
8
  import { fetchAllPlotArgs, resolveToolMenuByCode } from '../../lib/xiantao.js';
9
9
  export default class ToolInspect extends XtzCommand {
10
10
  static description = 'Inspect a Xiantao tool and its dynamic inputs';
@@ -38,6 +38,7 @@ export default class ToolInspect extends XtzCommand {
38
38
  const summary = this.summarizeSchema(schema);
39
39
  const output = {
40
40
  agent,
41
+ args_data: fetchAllResult.args_data ?? [],
41
42
  args_main: schema,
42
43
  args_main_defaults: buildArgsMainFromSchema(schema),
43
44
  duid: fetchAllResult.id?.duid ?? '',
@@ -64,8 +65,17 @@ export default class ToolInspect extends XtzCommand {
64
65
  lines.push(`notice: ${notice}`);
65
66
  }
66
67
  lines.push('input:');
67
- lines.push(` upload: ${inputs.upload_available ? 'local xlsx' : 'not detected'}`);
68
- lines.push(` demo: ${inputs.demo_available ? this.formatDemoSummary(inputs.demo_upload) : 'unavailable'}`);
68
+ lines.push(` upload: ${inputs.upload_available ? `${inputs.upload_fields.length} fields` : 'not detected'}`);
69
+ for (const field of inputs.upload_fields) {
70
+ const suffixes = [
71
+ field.label && field.label !== field.key ? field.label : undefined,
72
+ field.required ? 'required' : undefined,
73
+ field.demo_upload ? this.formatDemoSummary(field.demo_upload) : 'no demo default',
74
+ ].filter(Boolean);
75
+ lines.push(` - ${field.key}${suffixes.length > 0 ? ` - ${suffixes.join(', ')}` : ''}`);
76
+ }
77
+ lines.push(` demo: ${inputs.demo_available ? 'available' : 'unavailable'}`);
78
+ lines.push(` record: ${inputs.record_available ? this.formatRecordSummary(inputs.record_default) : 'unavailable'}`);
69
79
  if (schema.length === 0) {
70
80
  lines.push('args_main: none');
71
81
  return lines.join('\n');
@@ -90,22 +100,40 @@ export default class ToolInspect extends XtzCommand {
90
100
  return lines.join('\n');
91
101
  }
92
102
  summarizeInputs(argsData, duid) {
93
- const uploadField = argsData.find((item) => isJsonObject(item) && item.type === 'XTUpload');
94
- const defaultValue = isJsonObject(uploadField) && isJsonObject(uploadField.default_value) ? uploadField.default_value : undefined;
95
- const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
96
- const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
97
- const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
98
- const demoAvailable = Boolean(duid.trim() && fileName && fileOss && typeof fileSize === 'number');
103
+ const uploadFields = listXiantaoUploadFields(argsData);
104
+ const record = findDefaultXiantaoRecord(argsData);
105
+ const demoAvailable = Boolean(duid.trim() && uploadFields.length > 0 && uploadFields.every((field) => Boolean(field.defaultUpload)));
106
+ const recordAvailable = Boolean(duid.trim() && record);
107
+ const recordDefault = recordAvailable && record
108
+ ? {
109
+ key: record.key,
110
+ label: record.label,
111
+ module_id: record.moduleId,
112
+ module_name: record.moduleName,
113
+ record_id: record.recordId,
114
+ record_name: record.recordName,
115
+ time: record.time,
116
+ }
117
+ : null;
99
118
  return {
100
119
  demo_available: demoAvailable,
101
- demo_upload: demoAvailable
102
- ? {
103
- file_name: fileName,
104
- file_oss: fileOss,
105
- file_size: fileSize,
106
- }
107
- : null,
108
- upload_available: Boolean(uploadField),
120
+ record_available: recordAvailable,
121
+ record_default: recordDefault,
122
+ upload_available: uploadFields.length > 0,
123
+ upload_fields: uploadFields.map((field) => ({
124
+ demo_upload: field.defaultUpload
125
+ ? {
126
+ file_name: field.defaultUpload.fileName,
127
+ file_oss: field.defaultUpload.fileOss,
128
+ file_size: field.defaultUpload.fileSize,
129
+ key: field.key,
130
+ label: field.label,
131
+ }
132
+ : null,
133
+ key: field.key,
134
+ label: field.label,
135
+ required: field.required,
136
+ })),
109
137
  };
110
138
  }
111
139
  summarizeSchema(schema) {
@@ -117,10 +145,13 @@ export default class ToolInspect extends XtzCommand {
117
145
  };
118
146
  }
119
147
  formatDemoSummary(demoUpload) {
120
- if (!demoUpload) {
148
+ return `${demoUpload.file_name} (${this.formatFileSize(demoUpload.file_size)})`;
149
+ }
150
+ formatRecordSummary(record) {
151
+ if (!record) {
121
152
  return 'available';
122
153
  }
123
- return `${demoUpload.file_name} (${this.formatFileSize(demoUpload.file_size)})`;
154
+ return `${record.module_id || '-'} ${record.record_id}${record.time ? ` (${record.time})` : ''}`;
124
155
  }
125
156
  formatFileSize(fileSize) {
126
157
  if (fileSize < 1024) {
@@ -0,0 +1,3 @@
1
+ import PlotMenusRaw from '../plot/menus-raw.js';
2
+ export default class ToolMenusRaw extends PlotMenusRaw {
3
+ }
@@ -7,7 +7,7 @@ import { buildArgsMainFromSchema } from '../../lib/plot-schema.js';
7
7
  import { ToolRunCommandBase } from '../plot/run.js';
8
8
  export default class ToolPrepare extends ToolRunCommandBase {
9
9
  static description = 'Prepare a Xiantao bioinformatics tool in agent mode and return the final dynamic schema';
10
- static usage = 'tool prepare <tool> (--file <value> | --demo) --json';
10
+ static usage = 'tool prepare <tool> [--file <value> ... | --demo] --json';
11
11
  static args = {
12
12
  tool: Args.string({
13
13
  description: 'Tool id, e.g. violin_flat',
@@ -25,7 +25,8 @@ export default class ToolPrepare extends ToolRunCommandBase {
25
25
  description: 'Use the module demo data returned by fetch-all instead of uploading a local file',
26
26
  }),
27
27
  file: Flags.string({
28
- description: 'Path to the local xlsx file',
28
+ description: 'Path to the local xlsx file; repeat or use KEY=PATH for named upload fields',
29
+ multiple: true,
29
30
  }),
30
31
  interactive: Flags.boolean({
31
32
  default: false,
@@ -57,17 +58,11 @@ export default class ToolPrepare extends ToolRunCommandBase {
57
58
  if (!flags.json) {
58
59
  throw new XtzError('`xt tool prepare` 仅支持 `--json` 输出。');
59
60
  }
60
- if (args.file && flags.file) {
61
- throw new XtzError('FILE `--file` 不能同时使用。');
62
- }
63
- if (flags.demo && flags.file) {
61
+ const fileInputs = this.resolveToolFileInputs(args.file, flags.file);
62
+ if (flags.demo && fileInputs.length > 0) {
64
63
  throw new XtzError('`--demo` 与 `--file` 不能同时使用。');
65
64
  }
66
- const filePath = args.file ?? flags.file;
67
- if (!flags.demo && !filePath) {
68
- throw new XtzError('`xt tool prepare` 需要 `--file`,或显式传入 `--demo`。');
69
- }
70
- const prepared = await this.prepareToolRun(args.tool, filePath, {
65
+ const prepared = await this.prepareToolRun(args.tool, fileInputs, {
71
66
  ...flags,
72
67
  interactive: false,
73
68
  });
@@ -77,9 +72,11 @@ export default class ToolPrepare extends ToolRunCommandBase {
77
72
  args_main: prepared.schema ?? [],
78
73
  args_main_defaults: buildArgsMainFromSchema(prepared.schema),
79
74
  duid: prepared.duid,
80
- file_name: prepared.upload.fileName,
81
- file_oss: prepared.upload.fileOss,
82
- file_size: prepared.upload.fileSize,
75
+ file_name: prepared.inputSummary.file_name,
76
+ file_oss: prepared.inputSummary.file_oss,
77
+ file_size: prepared.inputSummary.file_size,
78
+ input_record: prepared.inputSummary.input_record,
79
+ input_uploads: prepared.inputSummary.input_uploads,
83
80
  mode: prepared.inputMode,
84
81
  module_id: prepared.moduleId,
85
82
  notice: prepared.notice,
@@ -0,0 +1,51 @@
1
+ import { isJsonObject } from './json.js';
2
+ export function listXiantaoUploadFields(argsData) {
3
+ return argsData.flatMap((item) => {
4
+ if (!isJsonObject(item) || item.type !== 'XTUpload') {
5
+ return [];
6
+ }
7
+ const key = typeof item.key === 'string' ? item.key.trim() : '';
8
+ if (!key) {
9
+ return [];
10
+ }
11
+ const defaultValue = isJsonObject(item.default_value) ? item.default_value : undefined;
12
+ const fileName = typeof defaultValue?.file_name === 'string' ? defaultValue.file_name.trim() : '';
13
+ const fileOss = typeof defaultValue?.file_oss === 'string' ? defaultValue.file_oss.trim() : '';
14
+ const fileSize = typeof defaultValue?.file_size === 'number' ? defaultValue.file_size : undefined;
15
+ return [{
16
+ defaultUpload: fileName && fileOss && typeof fileSize === 'number'
17
+ ? {
18
+ fileName,
19
+ fileOss,
20
+ fileSize,
21
+ }
22
+ : null,
23
+ key,
24
+ label: typeof item.label === 'string' ? item.label.trim() : '',
25
+ required: item.required === true,
26
+ }];
27
+ });
28
+ }
29
+ export function findDefaultXiantaoRecord(argsData) {
30
+ for (const item of argsData) {
31
+ if (!isJsonObject(item) || item.type !== 'XTRecord') {
32
+ continue;
33
+ }
34
+ const key = typeof item.key === 'string' ? item.key.trim() : '';
35
+ const defaultValue = isJsonObject(item.default_value) ? item.default_value : undefined;
36
+ const recordId = typeof defaultValue?.record_id === 'string' ? defaultValue.record_id.trim() : '';
37
+ if (!key || !recordId) {
38
+ continue;
39
+ }
40
+ return {
41
+ key,
42
+ label: typeof item.label === 'string' ? item.label.trim() : '',
43
+ moduleId: typeof defaultValue?.module_id === 'string' ? defaultValue.module_id.trim() : '',
44
+ moduleName: typeof defaultValue?.module_name === 'string' ? defaultValue.module_name.trim() : '',
45
+ recordId,
46
+ recordName: typeof defaultValue?.record_name === 'string' ? defaultValue.record_name.trim() : '',
47
+ time: typeof defaultValue?.time === 'string' ? defaultValue.time.trim() : '',
48
+ };
49
+ }
50
+ return null;
51
+ }
@@ -14,14 +14,7 @@ export async function verifyXiantaoPlotUpload(auth, input) {
14
14
  },
15
15
  args_data: {
16
16
  duid: '',
17
- info: {
18
- main: {
19
- file_oss: getXiantaoFileOss(input.upload),
20
- file_name: input.upload.fileName,
21
- file_size: getXiantaoFileSize(input.upload),
22
- type: 'upload',
23
- },
24
- },
17
+ info: buildUploadInfo(input.uploads),
25
18
  },
26
19
  args_main: input.argsMain,
27
20
  },
@@ -52,17 +45,7 @@ export async function submitXiantaoPlot(auth, input) {
52
45
  module_id: input.moduleId,
53
46
  uuid: input.uuid,
54
47
  },
55
- args_data: {
56
- duid: input.duid,
57
- info: {
58
- main: {
59
- file_oss: getXiantaoFileOss(input.upload),
60
- file_name: input.upload.fileName,
61
- file_size: getXiantaoFileSize(input.upload),
62
- type: 'upload',
63
- },
64
- },
65
- },
48
+ args_data: buildSubmitArgsData(input.duid, input.input),
66
49
  args_main: input.argsMain,
67
50
  },
68
51
  }),
@@ -163,6 +146,39 @@ function getXiantaoFileOss(upload) {
163
146
  function getXiantaoFileSize(upload) {
164
147
  return upload.fileSizeForXiantao ?? upload.fileSize;
165
148
  }
149
+ function buildSubmitArgsData(duid, input) {
150
+ if (input.kind === 'record') {
151
+ return {
152
+ duid,
153
+ info: {
154
+ [input.record.key]: {
155
+ label: input.record.label,
156
+ module_id: input.record.moduleId,
157
+ module_name: input.record.moduleName,
158
+ record_id: input.record.recordId,
159
+ record_name: input.record.recordName,
160
+ time: input.record.time,
161
+ type: 'record',
162
+ },
163
+ },
164
+ };
165
+ }
166
+ return {
167
+ duid,
168
+ info: buildUploadInfo(input.uploads),
169
+ };
170
+ }
171
+ function buildUploadInfo(uploads) {
172
+ return Object.fromEntries(uploads.map((item) => [
173
+ item.key,
174
+ {
175
+ file_oss: getXiantaoFileOss(item.upload),
176
+ file_name: item.upload.fileName,
177
+ file_size: getXiantaoFileSize(item.upload),
178
+ type: 'upload',
179
+ },
180
+ ]));
181
+ }
166
182
  function isAsyncSubmissionMessage(message) {
167
183
  return Boolean(message?.includes('任务提交成功'));
168
184
  }
@@ -48,6 +48,14 @@ export async function resolveToolMenuByCode(auth, toolProductUuid, code) {
48
48
  export function listResolvedToolMenus(menus) {
49
49
  return collectToolMenuMatches(menus);
50
50
  }
51
+ export function listToolMenuSummaries(menus) {
52
+ return listResolvedToolMenus(menus).map((menu) => ({
53
+ module_id: menu.code,
54
+ path: menu.path,
55
+ route: menu.route,
56
+ title: menu.title,
57
+ }));
58
+ }
51
59
  export function searchResolvedToolMenus(menus, keyword) {
52
60
  const normalizedKeyword = keyword.trim().toLowerCase();
53
61
  return listResolvedToolMenus(menus)
package/dist/xtz-help.js CHANGED
@@ -6,7 +6,7 @@ export default class XtzHelp extends Help {
6
6
  this.log(this.section('MAIN FLOWS', [
7
7
  ['First use', '$ xt login\n$ xt tool search violin\n$ xt tool inspect violin_flat\n$ xt tool run violin_flat ./data.xlsx'],
8
8
  ['Interactive tool run', '$ xt tool run --interactive'],
9
- ['Agent automation', '$ xt tool prepare violin_flat --file ./data.xlsx --profile opencode --json\n$ xt tool exec violin_flat --file ./data.xlsx --profile opencode --json'],
9
+ ['Agent automation', '$ xt tool prepare violin_flat --file ./data.xlsx --profile opencode --json\n$ xt tool exec violin_flat --file ./data.xlsx --profile opencode --json\n$ xt tool exec gsea_customize --file gene_set=./gene_set.gmt --file data=./rank.xlsx --profile opencode --json'],
10
10
  ]));
11
11
  this.log('');
12
12
  this.log(this.section('COMMON COMMANDS', this.renderCommandItems([
@@ -73,7 +73,8 @@ export default class XtzHelp extends Help {
73
73
  ['xt history --help', 'Show the history command set'],
74
74
  ['xt tool resolve', 'Resolve a tool id to UUID and route'],
75
75
  ['xt tool menu', 'Fetch one raw tool menu by UUID'],
76
- ['xt tool menus', 'Fetch all raw tool menus for a product'],
76
+ ['xt tool menus', 'List tool titles and ids for a product'],
77
+ ['xt tool menus-raw', 'Fetch all raw tool menus for a product'],
77
78
  ['xt tool fetch-all', 'Fetch raw dynamic args for a tool'],
78
79
  ['xt tool prepare', 'Resolve upload-dependent dynamic schema for agent workflows'],
79
80
  ['xt auth ...', 'Legacy auth topic kept for compatibility'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixlife-ai/xiantao",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "CLI for Xiantao bioinformatics tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",