@helixlife-ai/xiantao 0.1.6 → 0.1.7

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
@@ -6,7 +6,7 @@ Install with `npm i -g @helixlife-ai/xiantao` and run it as `xt`.
6
6
 
7
7
  `tool` is the primary product topic for bioinformatics tools. `plot` is kept as a compatibility entrypoint for the current command set and prints a migration warning when executed.
8
8
 
9
- `--profile` is the primary profile selector for token lookup and config isolation, for example `default` or `openclaw`. `--agent` is kept as a compatibility alias.
9
+ `--profile` is the primary profile selector for token lookup and config isolation. If it is omitted, the CLI resolves the profile from `AGENT_NAME`, then the host runtime (`cursor`, `opencode`, `openclaw`), then `~/.config/xtz/config.json`, and finally defaults to `opencode`. `--agent` is kept as a compatibility alias.
10
10
 
11
11
  `login`, `status`, and `logout` are the primary auth entrypoints. `auth ...` is kept as a compatibility topic and prints a migration warning when executed.
12
12
 
@@ -34,7 +34,7 @@ This starts a menu-driven path for choosing a tool, selecting demo data or a loc
34
34
  ### Agent Automation
35
35
 
36
36
  ```bash
37
- xt tool exec violin_flat --file ./data.xlsx --profile openclaw --json
37
+ xt tool exec violin_flat --file ./data.xlsx --profile opencode --json
38
38
  ```
39
39
 
40
40
  For machine callers, use `xt tool exec`, keep `--json`, pass an explicit profile with `--profile`, and avoid interactive-only flows.
@@ -44,8 +44,10 @@ For machine callers, use `xt tool exec`, keep `--json`, pass an explicit profile
44
44
  ### Auth
45
45
 
46
46
  - `xt login`
47
+ - `xt bind`
47
48
  - `xt status`
48
49
  - `xt logout`
50
+ - `xt auth bind`
49
51
  - `xt tool login`
50
52
 
51
53
  ### Tool Run
@@ -73,6 +75,8 @@ For the common Xiantao tool product, `tool search`, `tool inspect`, `tool run`,
73
75
 
74
76
  When resolving tools from `--toolProductUuid`, the CLI caches the `/tool/menus?is_all=1` result locally for 12 hours per `profile + toolProductUuid` to avoid repeated full-menu fetches.
75
77
 
78
+ For remote-agent flows where the Xiantao web login callback cannot hit the machine running the CLI, use `xt bind --xiantao-token <token> --profile openclaw`. The bind command creates the upload auth session, calls the same `/user/skills/bind_user` API directly, waits for confirmation, and persists both the upload token and the Xiantao token locally. If you already created the auth session elsewhere, you can reuse it with `xt bind --auth-key <auth_key> --token-key <token_key> --xiantao-token <token>`.
79
+
76
80
  ## Development
77
81
 
78
82
  ```bash
@@ -0,0 +1,53 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { XtzCommand } from '../../base-command.js';
3
+ import { loginAuth, waitForAuthorizedToken } from '../../lib/auth.js';
4
+ import { globalFlags } from '../../lib/flags.js';
5
+ import { bindUserWithXiantaoToken } from '../../lib/xiantao-auth.js';
6
+ import { getTokenPath, getXiantaoTokenPath, loadXiantaoToken, saveToken } from '../../lib/storage.js';
7
+ export default class AuthBind extends XtzCommand {
8
+ static description = 'Bind login remotely with a Xiantao web token without the localhost callback flow';
9
+ static flags = {
10
+ ...globalFlags,
11
+ 'auth-key': Flags.string({
12
+ char: 'k',
13
+ description: 'Existing auth key returned by the upload authorization flow',
14
+ }),
15
+ 'token-key': Flags.string({
16
+ description: 'Existing upload token key paired with --auth-key',
17
+ }),
18
+ 'xiantao-token': Flags.string({
19
+ description: 'Bearer token for api.helixlife.net xiantao APIs; falls back to XIANTAO_TOKEN or the stored Xiantao web token',
20
+ }),
21
+ };
22
+ async run() {
23
+ this.warnIfLegacyAuthCommand();
24
+ const { flags } = await this.parse(AuthBind);
25
+ const agent = await this.getProfile(flags.profile);
26
+ const token = flags['xiantao-token']?.trim() || process.env.XIANTAO_TOKEN?.trim() || await loadXiantaoToken(agent);
27
+ if (!token) {
28
+ this.error(`未找到 ${agent} 的仙桃网页 token,请传 --xiantao-token、设置 XIANTAO_TOKEN,或先准备 ${getXiantaoTokenPath(agent)}`, { exit: 1 });
29
+ }
30
+ const auth = flags['auth-key']
31
+ ? {
32
+ authKey: flags['auth-key'],
33
+ token: flags['token-key']?.trim(),
34
+ tokenPath: getTokenPath(agent),
35
+ }
36
+ : await loginAuth(agent, false);
37
+ if (!auth.token) {
38
+ this.error('传 --auth-key 时必须同时传 --token-key', { exit: 1 });
39
+ }
40
+ if (flags['auth-key']) {
41
+ await saveToken(agent, auth.token);
42
+ }
43
+ const result = await bindUserWithXiantaoToken(agent, auth.authKey, token);
44
+ await waitForAuthorizedToken(auth.token);
45
+ this.print(flags, {
46
+ agent,
47
+ auth_key: auth.authKey,
48
+ bound: true,
49
+ token_path: auth.tokenPath,
50
+ xiantao_token_path: result.tokenPath,
51
+ }, `profile: ${agent}\nbound: true\nauth_key: ${auth.authKey}\ntoken: ${auth.tokenPath}\nxiantao_token: ${result.tokenPath}`);
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ import AuthBind from './auth/bind.js';
2
+ export default class Bind extends AuthBind {
3
+ }
@@ -1,4 +1,4 @@
1
- export const DEFAULT_AGENT = 'openclaw';
1
+ export const DEFAULT_AGENT = 'opencode';
2
2
  export const XTZ_CONFIG_PATH = '.config/xtz/config.json';
3
3
  export const AUTH_URL_ENDPOINT = 'https://api.helixlife.net/api/v1/user/skills/auth_url';
4
4
  export const AUTH_BIND_ENDPOINT = 'https://api.helixlife.net/api/v1/user/skills/bind_user';
@@ -135,11 +135,16 @@ export async function readXtzConfig() {
135
135
  }
136
136
  }
137
137
  export async function resolveAgent(agentFlag) {
138
- if (agentFlag) {
139
- return agentFlag;
138
+ const cliAgent = agentFlag?.trim();
139
+ if (cliAgent) {
140
+ return cliAgent;
141
+ }
142
+ const environmentAgent = resolveAgentFromEnvironment();
143
+ if (environmentAgent) {
144
+ return environmentAgent;
140
145
  }
141
146
  const config = await readXtzConfig();
142
- return config.agent ?? DEFAULT_AGENT;
147
+ return config.agent?.trim() || DEFAULT_AGENT;
143
148
  }
144
149
  export async function resolveXiantaoToolProductUuid(toolProductUuidFlag) {
145
150
  if (toolProductUuidFlag?.trim()) {
@@ -184,3 +189,32 @@ async function readJsonFile(filePath) {
184
189
  function isNodeError(error) {
185
190
  return error instanceof Error && 'code' in error;
186
191
  }
192
+ function resolveAgentFromEnvironment() {
193
+ const agentName = process.env.AGENT_NAME?.trim();
194
+ if (agentName) {
195
+ return agentName;
196
+ }
197
+ if (isCursorEnvironment()) {
198
+ return 'cursor';
199
+ }
200
+ if (isNamedEnvironment('OPENCODE')) {
201
+ return 'opencode';
202
+ }
203
+ if (isNamedEnvironment('OPENCLAW')) {
204
+ return 'openclaw';
205
+ }
206
+ return undefined;
207
+ }
208
+ function isCursorEnvironment() {
209
+ return Boolean(process.env.CURSOR_TRACE_ID?.trim()
210
+ || process.env.CURSOR_SESSION_ID?.trim()
211
+ || process.env.CURSOR_AGENT?.trim()
212
+ || process.env.VSCODE_GIT_ASKPASS_NODE?.includes('Cursor')
213
+ || process.env.TERM_PROGRAM?.toLowerCase() === 'cursor');
214
+ }
215
+ function isNamedEnvironment(name) {
216
+ return Boolean(process.env[name]?.trim()
217
+ || process.env[`${name}_AGENT`]?.trim()
218
+ || process.env[`${name}_SESSION_ID`]?.trim()
219
+ || process.env.TERM_PROGRAM?.toLowerCase() === name.toLowerCase());
220
+ }
@@ -3,7 +3,7 @@ import { AUTH_BIND_ENDPOINT, PASSPORT_JUMP_ENDPOINT } from './constants.js';
3
3
  import { openUrl } from './open-url.js';
4
4
  import { XtzError } from './errors.js';
5
5
  import { requestJson } from './http.js';
6
- import { getXiantaoTokenPath, loadXiantaoToken, requireXiantaoToken, saveXiantaoToken } from './storage.js';
6
+ import { loadXiantaoToken, requireXiantaoToken, saveXiantaoToken } from './storage.js';
7
7
  export async function getXiantaoAuthContext(agent, tokenOverride) {
8
8
  const token = tokenOverride?.trim() || process.env.XIANTAO_TOKEN?.trim() || await requireXiantaoToken(agent);
9
9
  return { agent, token };
@@ -29,24 +29,34 @@ export async function loginXiantao(agent) {
29
29
  tokenPath,
30
30
  };
31
31
  }
32
+ export async function bindUserWithXiantaoToken(agent, authKey, token) {
33
+ await requestJson(AUTH_BIND_ENDPOINT, {
34
+ body: JSON.stringify({ auth_key: authKey }),
35
+ headers: {
36
+ Authorization: `Bearer ${token}`,
37
+ 'Content-Type': 'application/json',
38
+ origin: 'https://www.helixlife.net',
39
+ platform: 'pc',
40
+ referer: 'https://www.helixlife.net/',
41
+ source: 'PC',
42
+ },
43
+ method: 'POST',
44
+ }, { agent, tokenKind: 'xiantao' });
45
+ const tokenPath = await saveXiantaoToken(agent, token);
46
+ return {
47
+ browserOpened: false,
48
+ reused: true,
49
+ token,
50
+ tokenPath,
51
+ };
52
+ }
32
53
  export async function bindUserWithStoredXiantaoToken(agent, authKey) {
33
54
  const auth = await loadXiantaoAuthContext(agent);
34
55
  if (!auth) {
35
56
  return undefined;
36
57
  }
37
58
  try {
38
- await requestJson(AUTH_BIND_ENDPOINT, {
39
- body: JSON.stringify({ auth_key: authKey }),
40
- headers: {
41
- Authorization: `Bearer ${auth.token}`,
42
- 'Content-Type': 'application/json',
43
- origin: 'https://www.helixlife.net',
44
- platform: 'pc',
45
- referer: 'https://www.helixlife.net/',
46
- source: 'PC',
47
- },
48
- method: 'POST',
49
- }, { agent, tokenKind: 'xiantao' });
59
+ return await bindUserWithXiantaoToken(agent, authKey, auth.token);
50
60
  }
51
61
  catch (error) {
52
62
  if (error instanceof XtzError && error.message.includes('仙桃网页授权已过期或无效')) {
@@ -54,12 +64,6 @@ export async function bindUserWithStoredXiantaoToken(agent, authKey) {
54
64
  }
55
65
  throw error;
56
66
  }
57
- return {
58
- browserOpened: false,
59
- reused: true,
60
- token: auth.token,
61
- tokenPath: getXiantaoTokenPath(agent),
62
- };
63
67
  }
64
68
  async function createLoginSession() {
65
69
  let resolveToken;
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 openclaw --json\n$ xt tool exec violin_flat --file ./data.xlsx --profile openclaw --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'],
10
10
  ]));
11
11
  this.log('');
12
12
  this.log(this.section('COMMON COMMANDS', this.renderCommandItems([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helixlife-ai/xiantao",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "CLI for Xiantao bioinformatics tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",