tokenmix 1.4.17 → 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 +4 -0
- package/dist/agents/aider.js +7 -14
- package/dist/agents/claude.js +4 -22
- package/dist/agents/cline.js +7 -13
- package/dist/agents/codex.js +19 -30
- package/dist/agents/continue.js +7 -12
- package/dist/agents/goose.js +4 -10
- package/dist/agents/helpers.js +36 -0
- package/dist/agents/kilo.js +8 -14
- package/dist/agents/opencode.js +7 -27
- package/dist/agents/openhands.js +6 -11
- package/dist/agents/qwen.js +6 -23
- package/dist/agents/registry.js +0 -3
- package/dist/agents/roo.js +7 -12
- package/dist/api/client.js +8 -5
- package/dist/commands/agent-runner.js +1 -2
- package/dist/commands/balance.js +8 -5
- package/dist/commands/logout.js +2 -1
- package/dist/commands/topup.js +3 -3
- package/dist/config/store.js +8 -0
- package/dist/config/urls.js +3 -0
- package/dist/program.js +5 -20
- package/dist/utils/prompt.js +1 -10
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# TokenMix CLI
|
|
2
2
|
|
|
3
|
+
[](https://github.com/TokenMixAi/tokenmix-cli/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/tokenmix)
|
|
5
|
+
[](./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 / ...
|
package/dist/agents/aider.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { commandExists, run
|
|
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
|
-
|
|
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:
|
|
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 ??
|
|
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 = {
|
package/dist/agents/claude.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
9
|
-
|
|
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
|
package/dist/agents/cline.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { vscodeConfigOnlyCheck } from './helpers.js';
|
|
2
|
+
import { v1Url } from '../config/store.js';
|
|
2
3
|
import { t } from '../i18n/index.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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}
|
|
19
|
+
` Base URL: ${v1Url(baseUrl)}`,
|
|
26
20
|
` API Key: ${apiKey}`,
|
|
27
21
|
` Model ID: ${defaultModel}`,
|
|
28
22
|
'',
|
package/dist/agents/codex.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
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:
|
|
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',
|
|
52
|
-
|
|
53
|
-
'--config',
|
|
54
|
-
|
|
55
|
-
'--config',
|
|
56
|
-
|
|
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 ??
|
|
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
|
}
|
package/dist/agents/continue.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { vscodeConfigOnlyCheck } from './helpers.js';
|
|
2
|
+
import { v1Url } from '../config/store.js';
|
|
2
3
|
import { t } from '../i18n/index.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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}
|
|
22
|
+
` apiBase: ${v1Url(baseUrl)}`,
|
|
28
23
|
` apiKey: ${apiKey}`,
|
|
29
24
|
].join('\n');
|
|
30
25
|
return {
|
package/dist/agents/goose.js
CHANGED
|
@@ -1,25 +1,19 @@
|
|
|
1
|
-
import { commandExists, run
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/agents/kilo.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { vscodeConfigOnlyCheck } from './helpers.js';
|
|
2
|
+
import { v1Url } from '../config/store.js';
|
|
2
3
|
import { t } from '../i18n/index.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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}
|
|
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:
|
|
25
|
+
openAiBaseUrl: v1Url(baseUrl),
|
|
32
26
|
openAiApiKey: apiKey,
|
|
33
27
|
defaultModelId: defaultModel,
|
|
34
28
|
}, null, 2),
|
package/dist/agents/opencode.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
18
|
-
|
|
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:
|
|
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) {
|
package/dist/agents/openhands.js
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
|
-
import { commandExists, run
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 })],
|
package/dist/agents/qwen.js
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
6
|
-
|
|
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:
|
|
17
|
+
OPENAI_BASE_URL: v1Url(baseUrl),
|
|
35
18
|
OPENAI_MODEL: defaultModel,
|
|
36
19
|
},
|
|
37
20
|
notes: [t('qwen.noteUsing'), t('qwen.noteModel', { model: defaultModel })],
|
package/dist/agents/registry.js
CHANGED
package/dist/agents/roo.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { vscodeConfigOnlyCheck } from './helpers.js';
|
|
2
|
+
import { v1Url } from '../config/store.js';
|
|
2
3
|
import { t } from '../i18n/index.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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}
|
|
18
|
+
` Base URL: ${v1Url(baseUrl)}`,
|
|
24
19
|
` API Key: ${apiKey}`,
|
|
25
20
|
` Model ID: ${defaultModel}`,
|
|
26
21
|
'',
|
package/dist/api/client.js
CHANGED
|
@@ -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
|
|
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() {
|
package/dist/commands/balance.js
CHANGED
|
@@ -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:
|
|
34
|
+
logger.dim(t('balance.topupAt', { url: DASHBOARD_CREDITS_URL }));
|
|
36
35
|
console.log();
|
|
37
36
|
}
|
|
38
|
-
catch {
|
|
39
|
-
//
|
|
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
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -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 }) +
|
|
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
|
}
|
package/dist/commands/topup.js
CHANGED
|
@@ -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:
|
|
7
|
-
await openOrHint(
|
|
6
|
+
logger.step(t('topup.opening', { url: DASHBOARD_CREDITS_URL }));
|
|
7
|
+
await openOrHint(DASHBOARD_CREDITS_URL);
|
|
8
8
|
}
|
package/dist/config/store.js
CHANGED
|
@@ -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');
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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;
|
package/dist/utils/prompt.js
CHANGED
|
@@ -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.
|
|
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/
|
|
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": {
|