tokenmix 1.4.16 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # TokenMix CLI
2
2
 
3
+ [![CI](https://github.com/TokenMixAi/tokenmix-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/TokenMixAi/tokenmix-cli/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/tokenmix.svg)](https://www.npmjs.com/package/tokenmix)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
+
3
7
  Zero-config CLI to use any open-source coding agent with [TokenMix](https://tokenmix.ai) as the unified LLM backend.
4
8
 
5
9
  One account, one balance, 160+ models routed automatically across Claude / GPT / Gemini / DeepSeek / Qwen / Moonshot / ...
@@ -1,4 +1,6 @@
1
- import { commandExists, run, captureRun } from '../utils/exec.js';
1
+ import { commandExists, run } from '../utils/exec.js';
2
+ import { probeVersion } from './helpers.js';
3
+ import { v1Url, DEFAULT_MODEL } from '../config/store.js';
2
4
  import { t } from '../i18n/index.js';
3
5
  const AIDER_BIN = 'aider';
4
6
  async function installCheck() {
@@ -18,13 +20,7 @@ async function installCheck() {
18
20
  hint: t('aider.hintNotInstalled', { cmd: installCmd }),
19
21
  };
20
22
  }
21
- try {
22
- const v = await captureRun(AIDER_BIN, ['--version']);
23
- return { installed: true, version: v.stdout.trim() };
24
- }
25
- catch {
26
- return { installed: true };
27
- }
23
+ return probeVersion(AIDER_BIN);
28
24
  }
29
25
  async function configure(apiKey, baseUrl, defaultModel) {
30
26
  // Aider reads OPENAI_API_KEY and OPENAI_API_BASE. We pass via env at launch
@@ -32,13 +28,10 @@ async function configure(apiKey, baseUrl, defaultModel) {
32
28
  return {
33
29
  envVars: {
34
30
  OPENAI_API_KEY: apiKey,
35
- OPENAI_API_BASE: `${baseUrl}/v1`,
31
+ OPENAI_API_BASE: v1Url(baseUrl),
36
32
  TOKENMIX_DEFAULT_MODEL: defaultModel,
37
33
  },
38
- notes: [
39
- t('aider.noteUsing'),
40
- t('aider.noteModel', { model: defaultModel }),
41
- ],
34
+ notes: [t('aider.noteUsing'), t('aider.noteModel', { model: defaultModel })],
42
35
  };
43
36
  }
44
37
  // Aider lets you pick a model with --model OR one of its built-in alias flags
@@ -67,7 +60,7 @@ async function launch(args, env) {
67
60
  // Inject our default --model only if the user didn't already pick a model.
68
61
  const finalArgs = userSelectedModel(args)
69
62
  ? args
70
- : ['--model', `openai/${env.TOKENMIX_DEFAULT_MODEL ?? 'claude-sonnet-4.6'}`, ...args];
63
+ : ['--model', `openai/${env.TOKENMIX_DEFAULT_MODEL ?? DEFAULT_MODEL}`, ...args];
71
64
  await run(AIDER_BIN, finalArgs, { env });
72
65
  }
73
66
  export const AiderAgent = {
@@ -1,31 +1,13 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
3
  import fs from 'fs-extra';
4
- import { commandExists, run, captureRun } from '../utils/exec.js';
4
+ import { run } from '../utils/exec.js';
5
+ import { npmInstallCheck, npmInstallGlobal } from './helpers.js';
5
6
  import { t } from '../i18n/index.js';
6
7
  const CLAUDE_BIN = 'claude';
7
8
  const CLAUDE_NPM_PACKAGE = '@anthropic-ai/claude-code';
8
- async function installCheck() {
9
- const bin = await commandExists(CLAUDE_BIN);
10
- if (!bin) {
11
- const cmd = `npm install -g ${CLAUDE_NPM_PACKAGE}`;
12
- return {
13
- installed: false,
14
- hint: t('install.willInstallVia', { cmd }),
15
- installCmd: cmd,
16
- };
17
- }
18
- try {
19
- const v = await captureRun(CLAUDE_BIN, ['--version']);
20
- return { installed: true, version: v.stdout.trim() };
21
- }
22
- catch {
23
- return { installed: true };
24
- }
25
- }
26
- async function install() {
27
- await run('npm', ['install', '-g', CLAUDE_NPM_PACKAGE]);
28
- }
9
+ const installCheck = () => npmInstallCheck(CLAUDE_BIN, CLAUDE_NPM_PACKAGE);
10
+ const install = () => npmInstallGlobal(CLAUDE_NPM_PACKAGE);
29
11
  async function configure(apiKey, baseUrl, _defaultModel) {
30
12
  // Claude Code reads:
31
13
  // - env: ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY
@@ -1,16 +1,10 @@
1
- import { commandExists } from '../utils/exec.js';
1
+ import { vscodeConfigOnlyCheck } from './helpers.js';
2
+ import { v1Url } from '../config/store.js';
2
3
  import { t } from '../i18n/index.js';
3
- async function installCheck() {
4
- // Cline is a config-only agent (VSCode extension). The CLI cannot install the
5
- // extension on the user's behalf, so we always proceed to `configure()` and
6
- // print the settings even if VSCode isn't installed locally yet, the user
7
- // may be copying the config for another machine.
8
- const code = await commandExists('code');
9
- return {
10
- installed: true,
11
- hint: code ? t('cline.hintMarketplace') : t('cline.hintNoVscode'),
12
- };
13
- }
4
+ // Cline is a config-only agent (VSCode extension): the CLI can't install the
5
+ // extension, so installCheck always reports installed and configure() prints the
6
+ // settings to paste (the user may even be copying the config for another machine).
7
+ const installCheck = () => vscodeConfigOnlyCheck('cline.hintMarketplace', 'cline.hintNoVscode');
14
8
  async function configure(apiKey, baseUrl, defaultModel) {
15
9
  // Cline is a VSCode extension configured through its settings panel
16
10
  // (API Provider → "OpenAI Compatible"). Unlike Kilo it exposes no documented
@@ -22,7 +16,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
22
16
  t('cline.noteConfigWith'),
23
17
  '',
24
18
  ` Provider: OpenAI Compatible`,
25
- ` Base URL: ${baseUrl}/v1`,
19
+ ` Base URL: ${v1Url(baseUrl)}`,
26
20
  ` API Key: ${apiKey}`,
27
21
  ` Model ID: ${defaultModel}`,
28
22
  '',
@@ -1,4 +1,6 @@
1
- import { commandExists, run, captureRun } from '../utils/exec.js';
1
+ import { run } from '../utils/exec.js';
2
+ import { npmInstallCheck, npmInstallGlobal } from './helpers.js';
3
+ import { v1Url, DEFAULT_MODEL } from '../config/store.js';
2
4
  import { t } from '../i18n/index.js';
3
5
  const CODEX_BIN = 'codex';
4
6
  const CODEX_NPM_PACKAGE = '@openai/codex';
@@ -8,27 +10,8 @@ const PROVIDER_ID = 'tokenmix';
8
10
  // Env var Codex reads for the bearer token (the provider's `env_key`). We set it
9
11
  // at launch from the user's TokenMix key — nothing is written to ~/.codex.
10
12
  const KEY_ENV = 'TOKENMIX_API_KEY';
11
- async function installCheck() {
12
- const bin = await commandExists(CODEX_BIN);
13
- if (!bin) {
14
- const cmd = `npm install -g ${CODEX_NPM_PACKAGE}`;
15
- return {
16
- installed: false,
17
- hint: t('install.willInstallVia', { cmd }),
18
- installCmd: cmd,
19
- };
20
- }
21
- try {
22
- const v = await captureRun(CODEX_BIN, ['--version']);
23
- return { installed: true, version: v.stdout.trim() };
24
- }
25
- catch {
26
- return { installed: true };
27
- }
28
- }
29
- async function install() {
30
- await run('npm', ['install', '-g', CODEX_NPM_PACKAGE]);
31
- }
13
+ const installCheck = () => npmInstallCheck(CODEX_BIN, CODEX_NPM_PACKAGE);
14
+ const install = () => npmInstallGlobal(CODEX_NPM_PACKAGE);
32
15
  async function configure(apiKey, baseUrl, defaultModel) {
33
16
  // We do NOT write ~/.codex/config.toml. Codex accepts a whole custom provider
34
17
  // via `--config` overrides at launch, and reads the key from the env var named
@@ -37,7 +20,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
37
20
  return {
38
21
  envVars: {
39
22
  [KEY_ENV]: apiKey,
40
- TOKENMIX_BASE_URL: `${baseUrl}/v1`,
23
+ TOKENMIX_BASE_URL: v1Url(baseUrl),
41
24
  },
42
25
  notes: [t('codex.noteUsing'), t('codex.noteModel', { model: defaultModel })],
43
26
  };
@@ -48,12 +31,18 @@ async function configure(apiKey, baseUrl, defaultModel) {
48
31
  // Responses API specifically for Codex clients (POST /v1/responses).
49
32
  export function providerOverrides(baseUrl, model) {
50
33
  return [
51
- '--config', `model_provider="${PROVIDER_ID}"`,
52
- '--config', `model="${model}"`,
53
- '--config', `model_providers.${PROVIDER_ID}.name="TokenMix"`,
54
- '--config', `model_providers.${PROVIDER_ID}.base_url="${baseUrl}"`,
55
- '--config', `model_providers.${PROVIDER_ID}.env_key="${KEY_ENV}"`,
56
- '--config', `model_providers.${PROVIDER_ID}.wire_api="responses"`,
34
+ '--config',
35
+ `model_provider="${PROVIDER_ID}"`,
36
+ '--config',
37
+ `model="${model}"`,
38
+ '--config',
39
+ `model_providers.${PROVIDER_ID}.name="TokenMix"`,
40
+ '--config',
41
+ `model_providers.${PROVIDER_ID}.base_url="${baseUrl}"`,
42
+ '--config',
43
+ `model_providers.${PROVIDER_ID}.env_key="${KEY_ENV}"`,
44
+ '--config',
45
+ `model_providers.${PROVIDER_ID}.wire_api="responses"`,
57
46
  ];
58
47
  }
59
48
  async function launch(args, env) {
@@ -64,7 +53,7 @@ async function launch(args, env) {
64
53
  await run(CODEX_BIN, args, { env });
65
54
  return;
66
55
  }
67
- const model = env.TOKENMIX_DEFAULT_MODEL ?? 'claude-sonnet-4.6';
56
+ const model = env.TOKENMIX_DEFAULT_MODEL ?? DEFAULT_MODEL;
68
57
  // Our overrides go first so user-supplied args (e.g. `--config model=...`) win.
69
58
  await run(CODEX_BIN, [...providerOverrides(baseUrl, model), ...args], { env });
70
59
  }
@@ -1,15 +1,10 @@
1
- import { commandExists } from '../utils/exec.js';
1
+ import { vscodeConfigOnlyCheck } from './helpers.js';
2
+ import { v1Url } from '../config/store.js';
2
3
  import { t } from '../i18n/index.js';
3
- async function installCheck() {
4
- // Continue is a config-only agent (VSCode/JetBrains extension). The CLI cannot
5
- // install the extension, so we always proceed to configure() and print the
6
- // config the user pastes into ~/.continue/config.yaml.
7
- const code = await commandExists('code');
8
- return {
9
- installed: true,
10
- hint: code ? t('continue.hintMarketplace') : t('continue.hintNoVscode'),
11
- };
12
- }
4
+ // Continue is a config-only agent (VSCode/JetBrains extension): the CLI can't
5
+ // install the extension, so installCheck always reports installed and configure()
6
+ // prints the config to paste into ~/.continue/config.yaml.
7
+ const installCheck = () => vscodeConfigOnlyCheck('continue.hintMarketplace', 'continue.hintNoVscode');
13
8
  async function configure(apiKey, baseUrl, defaultModel) {
14
9
  // Continue is driven by ~/.continue/config.yaml (verified schema: top-level
15
10
  // name/version/schema + a `models:` list; each model needs name/provider/model,
@@ -24,7 +19,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
24
19
  ' - name: TokenMix',
25
20
  ' provider: openai',
26
21
  ` model: ${defaultModel}`,
27
- ` apiBase: ${baseUrl}/v1`,
22
+ ` apiBase: ${v1Url(baseUrl)}`,
28
23
  ` apiKey: ${apiKey}`,
29
24
  ].join('\n');
30
25
  return {
@@ -1,25 +1,19 @@
1
- import { commandExists, run, captureRun } from '../utils/exec.js';
1
+ import { commandExists, run } from '../utils/exec.js';
2
+ import { probeVersion } from './helpers.js';
2
3
  import { t } from '../i18n/index.js';
3
4
  const GOOSE_BIN = 'goose';
4
5
  // Goose ships as a Rust binary via an official install script. We do NOT auto-run
5
6
  // a `curl | bash`; we print it so the user installs it deliberately.
6
7
  const GOOSE_INSTALL = 'curl -fsSL https://github.com/block/goose/releases/download/stable/download_cli.sh | bash';
7
8
  async function installCheck() {
8
- const bin = await commandExists(GOOSE_BIN);
9
- if (!bin) {
9
+ if (!(await commandExists(GOOSE_BIN))) {
10
10
  return {
11
11
  installed: false,
12
12
  hint: t('goose.hintInstall', { cmd: GOOSE_INSTALL }),
13
13
  installCmd: GOOSE_INSTALL,
14
14
  };
15
15
  }
16
- try {
17
- const v = await captureRun(GOOSE_BIN, ['--version']);
18
- return { installed: true, version: v.stdout.trim() };
19
- }
20
- catch {
21
- return { installed: true };
22
- }
16
+ return probeVersion(GOOSE_BIN);
23
17
  }
24
18
  async function configure(apiKey, baseUrl, defaultModel) {
25
19
  // Goose reads GOOSE_PROVIDER/GOOSE_MODEL + OPENAI_HOST/OPENAI_API_KEY. NOTE:
@@ -0,0 +1,36 @@
1
+ import { commandExists, run, captureRun } from '../utils/exec.js';
2
+ import { t } from '../i18n/index.js';
3
+ // Shared install-check / install helpers, so each agent descriptor stays a thin
4
+ // declaration instead of repeating the same boilerplate.
5
+ // Best-effort version probe for an installed agent binary. `<bin> --version` is
6
+ // advisory, so a missing or unparseable version still reports installed.
7
+ export async function probeVersion(bin) {
8
+ try {
9
+ const v = await captureRun(bin, ['--version']);
10
+ return { installed: true, version: v.stdout.trim() };
11
+ }
12
+ catch {
13
+ return { installed: true };
14
+ }
15
+ }
16
+ // Install-check for an agent shipped as a global npm package: present → probe
17
+ // version; absent → offer the `npm install -g <pkg>` command.
18
+ export async function npmInstallCheck(bin, npmPackage) {
19
+ if (!(await commandExists(bin))) {
20
+ const cmd = `npm install -g ${npmPackage}`;
21
+ return { installed: false, hint: t('install.willInstallVia', { cmd }), installCmd: cmd };
22
+ }
23
+ return probeVersion(bin);
24
+ }
25
+ // Install a global npm package (the auto-install path for npm-based agents).
26
+ export async function npmInstallGlobal(npmPackage) {
27
+ await run('npm', ['install', '-g', npmPackage]);
28
+ }
29
+ // Install-check for config-only VSCode-extension agents (Kilo / Cline / Roo /
30
+ // Continue): the CLI can't install the extension, so it's always "installed";
31
+ // the hint nudges toward installing VSCode + the extension when `code` isn't on
32
+ // PATH (vs. the marketplace hint when it is).
33
+ export async function vscodeConfigOnlyCheck(marketplaceHint, noVscodeHint) {
34
+ const code = await commandExists('code');
35
+ return { installed: true, hint: code ? t(marketplaceHint) : t(noVscodeHint) };
36
+ }
@@ -1,16 +1,10 @@
1
- import { commandExists } from '../utils/exec.js';
1
+ import { vscodeConfigOnlyCheck } from './helpers.js';
2
+ import { v1Url } from '../config/store.js';
2
3
  import { t } from '../i18n/index.js';
3
- async function installCheck() {
4
- // Kilo Code is a config-only agent (VSCode extension). The CLI cannot install
5
- // the extension on the user's behalf, so we always proceed to `configure()` and
6
- // print the snippet even if VSCode isn't installed locally yet, the user
7
- // may be copying the config for another machine.
8
- const code = await commandExists('code');
9
- return {
10
- installed: true,
11
- hint: code ? t('kilo.hintMarketplace') : t('kilo.hintNoVscode'),
12
- };
13
- }
4
+ // Kilo Code is a config-only agent (VSCode extension): the CLI can't install the
5
+ // extension, so installCheck always reports installed and configure() prints the
6
+ // snippet (the user may even be copying the config for another machine).
7
+ const installCheck = () => vscodeConfigOnlyCheck('kilo.hintMarketplace', 'kilo.hintNoVscode');
14
8
  async function configure(apiKey, baseUrl, defaultModel) {
15
9
  // Kilo Code is a VSCode extension; there is no CLI launcher.
16
10
  // We print the configuration values for the user to paste into Kilo settings.
@@ -20,7 +14,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
20
14
  t('kilo.noteConfigWith'),
21
15
  '',
22
16
  ` Provider: OpenAI Compatible`,
23
- ` Base URL: ${baseUrl}/v1`,
17
+ ` Base URL: ${v1Url(baseUrl)}`,
24
18
  ` API Key: ${apiKey}`,
25
19
  ` Default Model: ${defaultModel}`,
26
20
  '',
@@ -28,7 +22,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
28
22
  '',
29
23
  JSON.stringify({
30
24
  provider: 'openai-compatible',
31
- openAiBaseUrl: `${baseUrl}/v1`,
25
+ openAiBaseUrl: v1Url(baseUrl),
32
26
  openAiApiKey: apiKey,
33
27
  defaultModelId: defaultModel,
34
28
  }, null, 2),
@@ -1,7 +1,9 @@
1
1
  import os from 'os';
2
2
  import path from 'path';
3
3
  import fs from 'fs-extra';
4
- import { commandExists, run, captureRun } from '../utils/exec.js';
4
+ import { run } from '../utils/exec.js';
5
+ import { npmInstallCheck, npmInstallGlobal } from './helpers.js';
6
+ import { v1Url } from '../config/store.js';
5
7
  import { t } from '../i18n/index.js';
6
8
  const OPENCODE_BIN = 'opencode';
7
9
  const OPENCODE_NPM_PACKAGE = 'opencode-ai';
@@ -14,27 +16,8 @@ function configPath() {
14
16
  const xdgHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
15
17
  return path.join(xdgHome, 'opencode', 'opencode.json');
16
18
  }
17
- async function installCheck() {
18
- const bin = await commandExists(OPENCODE_BIN);
19
- if (!bin) {
20
- const cmd = `npm install -g ${OPENCODE_NPM_PACKAGE}`;
21
- return {
22
- installed: false,
23
- hint: t('install.willInstallVia', { cmd }),
24
- installCmd: cmd,
25
- };
26
- }
27
- try {
28
- const v = await captureRun(OPENCODE_BIN, ['--version']);
29
- return { installed: true, version: v.stdout.trim() };
30
- }
31
- catch {
32
- return { installed: true };
33
- }
34
- }
35
- async function install() {
36
- await run('npm', ['install', '-g', OPENCODE_NPM_PACKAGE]);
37
- }
19
+ const installCheck = () => npmInstallCheck(OPENCODE_BIN, OPENCODE_NPM_PACKAGE);
20
+ const install = () => npmInstallGlobal(OPENCODE_NPM_PACKAGE);
38
21
  async function configure(apiKey, baseUrl, defaultModel) {
39
22
  const filePath = configPath();
40
23
  let existing = {};
@@ -51,7 +34,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
51
34
  npm: '@ai-sdk/openai-compatible',
52
35
  name: 'TokenMix',
53
36
  options: {
54
- baseURL: `${baseUrl}/v1`,
37
+ baseURL: v1Url(baseUrl),
55
38
  apiKey,
56
39
  },
57
40
  // Listed models populate /connect picker; users can still type any tokenmix short_id.
@@ -72,10 +55,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
72
55
  await fs.writeFile(filePath, JSON.stringify(next, null, 2));
73
56
  return {
74
57
  configPath: filePath,
75
- notes: [
76
- t('opencode.noteModel', { model: defaultModel }),
77
- t('opencode.noteSwitch'),
78
- ],
58
+ notes: [t('opencode.noteModel', { model: defaultModel }), t('opencode.noteSwitch')],
79
59
  };
80
60
  }
81
61
  async function launch(args) {
@@ -1,25 +1,20 @@
1
- import { commandExists, run, captureRun } from '../utils/exec.js';
1
+ import { commandExists, run } from '../utils/exec.js';
2
+ import { probeVersion } from './helpers.js';
3
+ import { v1Url } from '../config/store.js';
2
4
  import { t } from '../i18n/index.js';
3
5
  const OPENHANDS_BIN = 'openhands';
4
6
  // OpenHands needs Python 3.12+; `uv` pulls a matching Python automatically. We
5
7
  // print this rather than auto-running it (it installs a toolchain + Python).
6
8
  const OPENHANDS_INSTALL = 'uv tool install openhands --python 3.12';
7
9
  async function installCheck() {
8
- const bin = await commandExists(OPENHANDS_BIN);
9
- if (!bin) {
10
+ if (!(await commandExists(OPENHANDS_BIN))) {
10
11
  return {
11
12
  installed: false,
12
13
  hint: t('openhands.hintInstall', { cmd: OPENHANDS_INSTALL }),
13
14
  installCmd: OPENHANDS_INSTALL,
14
15
  };
15
16
  }
16
- try {
17
- const v = await captureRun(OPENHANDS_BIN, ['--version']);
18
- return { installed: true, version: v.stdout.trim() };
19
- }
20
- catch {
21
- return { installed: true };
22
- }
17
+ return probeVersion(OPENHANDS_BIN);
23
18
  }
24
19
  async function configure(apiKey, baseUrl, defaultModel) {
25
20
  // OpenHands reads LLM_API_KEY/LLM_MODEL/LLM_BASE_URL but ONLY when launched with
@@ -29,7 +24,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
29
24
  envVars: {
30
25
  LLM_API_KEY: apiKey,
31
26
  LLM_MODEL: `openai/${defaultModel}`,
32
- LLM_BASE_URL: `${baseUrl}/v1`,
27
+ LLM_BASE_URL: v1Url(baseUrl),
33
28
  OPENHANDS_SUPPRESS_BANNER: '1',
34
29
  },
35
30
  notes: [t('openhands.noteUsing'), t('openhands.noteModel', { model: defaultModel })],
@@ -1,28 +1,11 @@
1
- import { commandExists, run, captureRun } from '../utils/exec.js';
1
+ import { run } from '../utils/exec.js';
2
+ import { npmInstallCheck, npmInstallGlobal } from './helpers.js';
3
+ import { v1Url } from '../config/store.js';
2
4
  import { t } from '../i18n/index.js';
3
5
  const QWEN_BIN = 'qwen';
4
6
  const QWEN_NPM_PACKAGE = '@qwen-code/qwen-code';
5
- async function installCheck() {
6
- const bin = await commandExists(QWEN_BIN);
7
- if (!bin) {
8
- const cmd = `npm install -g ${QWEN_NPM_PACKAGE}`;
9
- return {
10
- installed: false,
11
- hint: t('install.willInstallVia', { cmd }),
12
- installCmd: cmd,
13
- };
14
- }
15
- try {
16
- const v = await captureRun(QWEN_BIN, ['--version']);
17
- return { installed: true, version: v.stdout.trim() };
18
- }
19
- catch {
20
- return { installed: true };
21
- }
22
- }
23
- async function install() {
24
- await run('npm', ['install', '-g', `${QWEN_NPM_PACKAGE}@latest`]);
25
- }
7
+ const installCheck = () => npmInstallCheck(QWEN_BIN, QWEN_NPM_PACKAGE);
8
+ const install = () => npmInstallGlobal(`${QWEN_NPM_PACKAGE}@latest`);
26
9
  async function configure(apiKey, baseUrl, defaultModel) {
27
10
  // Qwen Code (a Gemini CLI fork) reads OPENAI_API_KEY / OPENAI_BASE_URL /
28
11
  // OPENAI_MODEL when launched with `--auth-type openai` — VERIFIED end-to-end
@@ -31,7 +14,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
31
14
  return {
32
15
  envVars: {
33
16
  OPENAI_API_KEY: apiKey,
34
- OPENAI_BASE_URL: `${baseUrl}/v1`,
17
+ OPENAI_BASE_URL: v1Url(baseUrl),
35
18
  OPENAI_MODEL: defaultModel,
36
19
  },
37
20
  notes: [t('qwen.noteUsing'), t('qwen.noteModel', { model: defaultModel })],
@@ -23,6 +23,3 @@ export const AGENTS = [
23
23
  GooseAgent,
24
24
  OpenHandsAgent,
25
25
  ];
26
- export function findAgent(id) {
27
- return AGENTS.find((a) => a.id === id);
28
- }
@@ -1,15 +1,10 @@
1
- import { commandExists } from '../utils/exec.js';
1
+ import { vscodeConfigOnlyCheck } from './helpers.js';
2
+ import { v1Url } from '../config/store.js';
2
3
  import { t } from '../i18n/index.js';
3
- async function installCheck() {
4
- // Roo Code is a config-only agent (VSCode extension, a Cline fork). The CLI
5
- // cannot install the extension, so we always proceed to configure() and print
6
- // the settings the user may also be copying the config for another machine.
7
- const code = await commandExists('code');
8
- return {
9
- installed: true,
10
- hint: code ? t('roo.hintMarketplace') : t('roo.hintNoVscode'),
11
- };
12
- }
4
+ // Roo Code is a config-only agent (VSCode extension, a Cline fork): the CLI can't
5
+ // install the extension, so installCheck always reports installed and configure()
6
+ // prints the settings (the user may also be copying the config for another machine).
7
+ const installCheck = () => vscodeConfigOnlyCheck('roo.hintMarketplace', 'roo.hintNoVscode');
13
8
  async function configure(apiKey, baseUrl, defaultModel) {
14
9
  // Roo Code is configured through its settings panel (API Provider →
15
10
  // "OpenAI Compatible"), exactly like Cline. No documented settings.json
@@ -20,7 +15,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
20
15
  t('roo.noteConfigWith'),
21
16
  '',
22
17
  ` Provider: OpenAI Compatible`,
23
- ` Base URL: ${baseUrl}/v1`,
18
+ ` Base URL: ${v1Url(baseUrl)}`,
24
19
  ` API Key: ${apiKey}`,
25
20
  ` Model ID: ${defaultModel}`,
26
21
  '',
@@ -12,14 +12,17 @@ export function unwrap(resp) {
12
12
  if (body && typeof body.code === 'number' && body.code !== 0) {
13
13
  throw new ApiError(0, body.message || 'API error');
14
14
  }
15
- return body?.data ?? body;
15
+ // Standard success envelope → return the inner `data`. A body with no envelope
16
+ // (no `data` field — e.g. a raw array) is passed straight through: a deliberate
17
+ // fallback for endpoints that don't wrap their payload (locked by a unit test).
18
+ if (body && body.data != null)
19
+ return body.data;
20
+ return body;
16
21
  }
17
22
  function handleAxios(err) {
18
23
  const e = err;
19
24
  if (e.response) {
20
- const msg = e.response.data?.message ||
21
- e.response.data?.error?.message ||
22
- e.message;
25
+ const msg = e.response.data?.message || e.response.data?.error?.message || e.message;
23
26
  throw new ApiError(e.response.status, msg);
24
27
  }
25
28
  throw new ApiError(0, `Could not reach the TokenMix API (${e.message || 'network error'}). Check your internet connection or proxy.`);
@@ -56,7 +59,7 @@ export async function listPublicModels(cfg) {
56
59
  timeout: REQUEST_TIMEOUT_MS,
57
60
  }));
58
61
  const list = unwrap(r);
59
- return Array.isArray(list) ? list : list?.list ?? [];
62
+ return Array.isArray(list) ? list : (list?.list ?? []);
60
63
  }
61
64
  catch (err) {
62
65
  handleAxios(err);
@@ -1,9 +1,8 @@
1
1
  import { logger } from '../utils/logger.js';
2
- import { readConfig, apiBaseUrl } from '../config/store.js';
2
+ import { readConfig, apiBaseUrl, DEFAULT_MODEL } from '../config/store.js';
3
3
  import { confirm } from '../utils/prompt.js';
4
4
  import { AGENTS } from '../agents/registry.js';
5
5
  import { t } from '../i18n/index.js';
6
- const DEFAULT_MODEL = 'claude-sonnet-4.6';
7
6
  // Major version of the running Node (e.g. 22 from "v22.9.0"). Gates agents whose
8
7
  // binary needs a newer Node than this process (Codex / Qwen Code require 22).
9
8
  export function nodeMajor() {
@@ -3,9 +3,8 @@ import { logger } from '../utils/logger.js';
3
3
  import { openOrHint } from '../utils/browser.js';
4
4
  import { readConfig, apiBaseUrl } from '../config/store.js';
5
5
  import { fetchWallet } from '../api/client.js';
6
+ import { DASHBOARD_URL, DASHBOARD_CREDITS_URL } from '../config/urls.js';
6
7
  import { t } from '../i18n/index.js';
7
- const DASHBOARD_URL = 'https://tokenmix.ai/dashboard';
8
- const TOPUP_URL = 'https://tokenmix.ai/dashboard/credits';
9
8
  // micro-USD → display string. Mirrors the platform / plugin: 2 decimals for
10
9
  // amounts >= $1 (trailing zeros trimmed), more precision for sub-dollar amounts.
11
10
  export function formatUSD(microUsd) {
@@ -32,12 +31,16 @@ export async function balanceCommand() {
32
31
  console.log(` ${t('balance.reservedLabel')}: $${formatUSD(w.frozen)}`);
33
32
  }
34
33
  console.log();
35
- logger.dim(t('balance.topupAt', { url: TOPUP_URL }));
34
+ logger.dim(t('balance.topupAt', { url: DASHBOARD_CREDITS_URL }));
36
35
  console.log();
37
36
  }
38
- catch {
39
- // Network / auth hiccup fall back to the dashboard so the user isn't stuck.
37
+ catch (err) {
38
+ // Couldn't fetch the wallet (network failure, or an invalid/expired key).
39
+ // Surface the reason — consistent with doctor/login — then fall back to the
40
+ // dashboard so the user isn't stuck.
40
41
  logger.warn(t('balance.fetchFailed'));
42
+ if (err instanceof Error && err.message)
43
+ logger.dim(err.message);
41
44
  logger.step(t('balance.opening', { url: DASHBOARD_URL }));
42
45
  await openOrHint(DASHBOARD_URL);
43
46
  }
@@ -40,6 +40,15 @@ export async function doctorCommand() {
40
40
  console.log(chalk.bold(t('doctor.agentStatus')));
41
41
  for (const a of AGENTS) {
42
42
  const r = await a.installCheck();
43
+ // Config-only agents (VSCode extensions) have no binary of their own to install,
44
+ // so installCheck always reports installed:true — labeling them "installed" is
45
+ // misleading. Show their install mode instead, matching `tokenmix list`.
46
+ if (a.installMode === 'manual-vscode') {
47
+ logger.success(`${a.displayName.padEnd(14)} ${chalk.dim(t('list.modeManualVscode'))}`);
48
+ if (r.hint)
49
+ console.log(` ${chalk.dim(r.hint)}`);
50
+ continue;
51
+ }
43
52
  if (r.installed) {
44
53
  logger.success(`${a.displayName.padEnd(14)} ${t('doctor.installed')}${r.version ? ` (${chalk.dim(r.version)})` : ''}`);
45
54
  if (r.hint)
@@ -14,7 +14,8 @@ export async function logoutCommand() {
14
14
  try {
15
15
  const r = await agent.cleanup();
16
16
  if (r.reverted) {
17
- logger.success(t('logout.reverted', { name: agent.displayName }) + (r.configPath ? ` (${r.configPath})` : ''));
17
+ logger.success(t('logout.reverted', { name: agent.displayName }) +
18
+ (r.configPath ? ` (${r.configPath})` : ''));
18
19
  if (r.note)
19
20
  logger.dim(` ${r.note}`);
20
21
  }
@@ -1,8 +1,8 @@
1
1
  import { logger } from '../utils/logger.js';
2
2
  import { openOrHint } from '../utils/browser.js';
3
+ import { DASHBOARD_CREDITS_URL } from '../config/urls.js';
3
4
  import { t } from '../i18n/index.js';
4
- const TOPUP_URL = 'https://tokenmix.ai/dashboard/credits';
5
5
  export async function topupCommand() {
6
- logger.step(t('topup.opening', { url: TOPUP_URL }));
7
- await openOrHint(TOPUP_URL);
6
+ logger.step(t('topup.opening', { url: DASHBOARD_CREDITS_URL }));
7
+ await openOrHint(DASHBOARD_CREDITS_URL);
8
8
  }
@@ -1,6 +1,14 @@
1
1
  import fs from 'fs-extra';
2
2
  import { configDir, configFile } from './paths.js';
3
3
  export const DEFAULT_API_BASE = 'https://api.tokenmix.ai';
4
+ // The model agents default to when the user hasn't chosen one (overridable via
5
+ // the TOKENMIX_DEFAULT_MODEL env var or stored config). Single source of truth.
6
+ export const DEFAULT_MODEL = 'claude-sonnet-4.6';
7
+ // Append the OpenAI-compatible `/v1` suffix to a base URL, tolerating a trailing
8
+ // slash so `https://host/` doesn't yield `https://host//v1`.
9
+ export function v1Url(baseUrl) {
10
+ return `${baseUrl.replace(/\/+$/, '')}/v1`;
11
+ }
4
12
  export async function readConfig() {
5
13
  try {
6
14
  const raw = await fs.readFile(configFile(), 'utf-8');
@@ -0,0 +1,3 @@
1
+ // User-facing TokenMix dashboard links the CLI opens in a browser.
2
+ export const DASHBOARD_URL = 'https://tokenmix.ai/dashboard';
3
+ export const DASHBOARD_CREDITS_URL = 'https://tokenmix.ai/dashboard/credits';
@@ -102,7 +102,7 @@ export const en = {
102
102
  // install hints
103
103
  'install.willInstallVia': 'Will install via: {cmd}',
104
104
  'aider.hintNeedPython': 'Aider requires Python 3. Install Python 3 from https://python.org/downloads, then come back and run `tokenmix aider` again.',
105
- 'aider.hintNotInstalled': 'Aider is not installed. Run this in another terminal:\n {cmd}\nThen come back and run `tokenmix aider` again — your TokenMix login is already saved, so it will pick up automatically.',
105
+ 'aider.hintNotInstalled': 'Aider is not installed. Run this in another terminal:\n {cmd}',
106
106
  // opencode configure notes
107
107
  'opencode.noteModel': 'Default model set to tokenmix/{model}',
108
108
  'opencode.noteSwitch': 'To switch models, run `tokenmix models` or use `/connect` inside OpenCode.',
@@ -279,7 +279,7 @@ export const zh = {
279
279
  // install hints
280
280
  'install.willInstallVia': '将自动安装:{cmd}',
281
281
  'aider.hintNeedPython': 'Aider 需要 Python 3。请从 https://python.org/downloads 安装 Python 3,然后重新运行 `tokenmix aider`。',
282
- 'aider.hintNotInstalled': 'Aider 尚未安装。请在另一个终端运行:\n {cmd}\n然后重新运行 `tokenmix aider` —— 你的 TokenMix 登录已保存,会自动生效。',
282
+ 'aider.hintNotInstalled': 'Aider 尚未安装。请在另一个终端运行:\n {cmd}',
283
283
  // opencode configure notes
284
284
  'opencode.noteModel': '默认模型已设为 tokenmix/{model}',
285
285
  'opencode.noteSwitch': '切换模型:运行 `tokenmix models`,或在 OpenCode 内使用 `/connect`。',
@@ -439,7 +439,7 @@ export const ja = {
439
439
  'desc.openhands': 'All-Hands-AI/OpenHands — 自律型コーディング agent(OpenAI 互換)',
440
440
  'install.willInstallVia': '次の方法でインストールします:{cmd}',
441
441
  'aider.hintNeedPython': 'Aider には Python 3 が必要です。https://python.org/downloads から Python 3 をインストールし、再度 `tokenmix aider` を実行してください。',
442
- 'aider.hintNotInstalled': 'Aider はインストールされていません。別のターミナルで次を実行してください:\n {cmd}\nその後 `tokenmix aider` を再実行してください — TokenMix のログインは保存済みなので自動的に反映されます。',
442
+ 'aider.hintNotInstalled': 'Aider はインストールされていません。別のターミナルで次を実行してください:\n {cmd}',
443
443
  'opencode.noteModel': 'デフォルトモデルを tokenmix/{model} に設定しました',
444
444
  'opencode.noteSwitch': 'モデルを切り替えるには `tokenmix models` を実行するか、OpenCode 内で `/connect` を使用してください。',
445
445
  'claude.noteModels': 'tokenmix 経由で利用できる Claude モデル:claude-opus-4.7、claude-sonnet-4.6、claude-haiku-4.5',
@@ -594,7 +594,7 @@ export const ko = {
594
594
  'desc.openhands': 'All-Hands-AI/OpenHands — 자율 코딩 agent (OpenAI 호환)',
595
595
  'install.willInstallVia': '다음 방법으로 설치합니다: {cmd}',
596
596
  'aider.hintNeedPython': 'Aider에는 Python 3가 필요합니다. https://python.org/downloads 에서 Python 3를 설치한 뒤 다시 `tokenmix aider`를 실행하세요.',
597
- 'aider.hintNotInstalled': 'Aider가 설치되어 있지 않습니다. 다른 터미널에서 다음을 실행하세요:\n {cmd}\n그런 다음 다시 `tokenmix aider`를 실행하세요 — TokenMix 로그인이 이미 저장되어 있어 자동으로 적용됩니다.',
597
+ 'aider.hintNotInstalled': 'Aider가 설치되어 있지 않습니다. 다른 터미널에서 다음을 실행하세요:\n {cmd}',
598
598
  'opencode.noteModel': '기본 모델을 tokenmix/{model}(으)로 설정했습니다',
599
599
  'opencode.noteSwitch': '모델을 바꾸려면 `tokenmix models`를 실행하거나 OpenCode 안에서 `/connect`를 사용하세요.',
600
600
  'claude.noteModels': 'tokenmix를 통해 사용할 수 있는 Claude 모델: claude-opus-4.7, claude-sonnet-4.6, claude-haiku-4.5',
@@ -749,7 +749,7 @@ export const es = {
749
749
  'desc.openhands': 'All-Hands-AI/OpenHands — agente de programación autónomo (compatible con OpenAI)',
750
750
  'install.willInstallVia': 'Se instalará mediante: {cmd}',
751
751
  'aider.hintNeedPython': 'Aider requiere Python 3. Instala Python 3 desde https://python.org/downloads y vuelve a ejecutar `tokenmix aider`.',
752
- 'aider.hintNotInstalled': 'Aider no está instalado. Ejecuta esto en otra terminal:\n {cmd}\nLuego vuelve a ejecutar `tokenmix aider`: tu sesión de TokenMix ya está guardada, así que se detectará automáticamente.',
752
+ 'aider.hintNotInstalled': 'Aider no está instalado. Ejecuta esto en otra terminal:\n {cmd}',
753
753
  'opencode.noteModel': 'Modelo predeterminado establecido en tokenmix/{model}',
754
754
  'opencode.noteSwitch': 'Para cambiar de modelo, ejecuta `tokenmix models` o usa `/connect` dentro de OpenCode.',
755
755
  'claude.noteModels': 'Modelos de Claude disponibles vía tokenmix: claude-opus-4.7, claude-sonnet-4.6, claude-haiku-4.5',
@@ -904,7 +904,7 @@ export const fr = {
904
904
  'desc.openhands': 'All-Hands-AI/OpenHands — agent de codage autonome (compatible OpenAI)',
905
905
  'install.willInstallVia': 'Sera installé via : {cmd}',
906
906
  'aider.hintNeedPython': 'Aider nécessite Python 3. Installez Python 3 depuis https://python.org/downloads, puis relancez `tokenmix aider`.',
907
- 'aider.hintNotInstalled': 'Aider n’est pas installé. Exécutez ceci dans un autre terminal :\n {cmd}\nPuis relancez `tokenmix aider` — votre connexion TokenMix est déjà enregistrée, elle sera donc prise en compte automatiquement.',
907
+ 'aider.hintNotInstalled': 'Aider n’est pas installé. Exécutez ceci dans un autre terminal :\n {cmd}',
908
908
  'opencode.noteModel': 'Modèle par défaut défini sur tokenmix/{model}',
909
909
  'opencode.noteSwitch': 'Pour changer de modèle, exécutez `tokenmix models` ou utilisez `/connect` dans OpenCode.',
910
910
  'claude.noteModels': 'Modèles Claude disponibles via tokenmix : claude-opus-4.7, claude-sonnet-4.6, claude-haiku-4.5',
package/dist/program.js CHANGED
@@ -33,32 +33,17 @@ export function buildProgram(deps = {}) {
33
33
  .option('-p, --paste', t('cmd.loginPaste'))
34
34
  .option('-u, --url <baseUrl>', t('cmd.loginUrl'))
35
35
  .action(loginCommand);
36
- program
37
- .command('logout')
38
- .description(t('cmd.logout'))
39
- .action(logoutCommand);
40
- program
41
- .command('balance')
42
- .description(t('cmd.balance'))
43
- .action(balanceCommand);
44
- program
45
- .command('topup')
46
- .description(t('cmd.topup'))
47
- .action(topupCommand);
36
+ program.command('logout').description(t('cmd.logout')).action(logoutCommand);
37
+ program.command('balance').description(t('cmd.balance')).action(balanceCommand);
38
+ program.command('topup').description(t('cmd.topup')).action(topupCommand);
48
39
  program
49
40
  .command('models')
50
41
  .description(t('cmd.models'))
51
42
  .option('-t, --type <type>', t('cmd.modelsType'))
52
43
  .option('-s, --search <keyword>', t('cmd.modelsSearch'))
53
44
  .action(modelsCommand);
54
- program
55
- .command('list')
56
- .description(t('cmd.list'))
57
- .action(listCommand);
58
- program
59
- .command('doctor')
60
- .description(t('cmd.doctor'))
61
- .action(doctorCommand);
45
+ program.command('list').description(t('cmd.list')).action(listCommand);
46
+ program.command('doctor').description(t('cmd.doctor')).action(doctorCommand);
62
47
  // Register one subcommand per supported agent (opencode, claude, aider, kilo, ...).
63
48
  registerAgentCommands(program, deps.runAgent);
64
49
  return program;
@@ -5,7 +5,7 @@ export async function promptApiKey() {
5
5
  type: 'password',
6
6
  name: 'apiKey',
7
7
  message: t('prompt.pasteKey'),
8
- validate: (v) => v && v.startsWith('sk-tm-') ? true : t('login.keyMustStart'),
8
+ validate: (v) => (v && v.startsWith('sk-tm-') ? true : t('login.keyMustStart')),
9
9
  });
10
10
  const key = r.apiKey?.trim();
11
11
  return key || null;
@@ -25,12 +25,3 @@ export async function confirm(message, initial = true) {
25
25
  });
26
26
  return Boolean(r.ok);
27
27
  }
28
- export async function select(message, choices) {
29
- const r = await prompts({
30
- type: 'select',
31
- name: 'value',
32
- message,
33
- choices,
34
- });
35
- return r.value ?? null;
36
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmix",
3
- "version": "1.4.16",
3
+ "version": "1.5.0",
4
4
  "description": "Zero-config CLI to use any open-source coding agent with TokenMix as the unified LLM backend.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,10 @@
11
11
  "dev": "tsx src/cli.ts",
12
12
  "start": "node bin/tokenmix.js",
13
13
  "typecheck": "tsc --noEmit",
14
+ "lint": "eslint .",
15
+ "lint:fix": "eslint . --fix",
16
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
17
+ "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
14
18
  "test": "vitest run",
15
19
  "test:watch": "vitest",
16
20
  "prepublishOnly": "pnpm build"
@@ -36,11 +40,15 @@
36
40
  "openhands",
37
41
  "coding-agent"
38
42
  ],
43
+ "author": "TokenMix",
39
44
  "license": "MIT",
40
45
  "homepage": "https://tokenmix.ai",
41
46
  "repository": {
42
47
  "type": "git",
43
- "url": "https://github.com/tokenmix/tokenmix-cli.git"
48
+ "url": "https://github.com/TokenMixAi/tokenmix-cli.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/TokenMixAi/tokenmix-cli/issues"
44
52
  },
45
53
  "files": [
46
54
  "bin",
@@ -59,12 +67,18 @@
59
67
  "which": "^5.0.0"
60
68
  },
61
69
  "devDependencies": {
70
+ "@eslint/js": "^10.0.1",
62
71
  "@types/fs-extra": "^11.0.4",
63
72
  "@types/node": "^22.9.0",
64
73
  "@types/prompts": "^2.4.9",
65
74
  "@types/which": "^3.0.4",
75
+ "eslint": "^10.4.1",
76
+ "eslint-config-prettier": "^10.1.8",
77
+ "globals": "^17.6.0",
78
+ "prettier": "^3.8.3",
66
79
  "tsx": "^4.19.2",
67
80
  "typescript": "^5.6.3",
81
+ "typescript-eslint": "^8.60.0",
68
82
  "vitest": "^4.1.7"
69
83
  },
70
84
  "pnpm": {