tokenmix 1.5.2 → 1.5.3

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.
@@ -29,16 +29,22 @@ async function configure(apiKey, baseUrl, defaultModel) {
29
29
  // OpenAI-compatible provider. wire_api MUST be "responses": Codex 0.135+ dropped
30
30
  // support for "chat" (openai/codex#7782), and tokenmix's gateway implements the
31
31
  // Responses API specifically for Codex clients (POST /v1/responses).
32
+ // Escape a value for a TOML double-quoted string (codex `--config key="value"`),
33
+ // so a user-set model/baseUrl containing " \ or a newline can't break the parse
34
+ // or inject extra config keys (e.g. overwrite base_url via an embedded newline).
35
+ function tomlStr(v) {
36
+ return v.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r/g, '\\r').replace(/\n/g, '\\n');
37
+ }
32
38
  export function providerOverrides(baseUrl, model) {
33
39
  return [
34
40
  '--config',
35
41
  `model_provider="${PROVIDER_ID}"`,
36
42
  '--config',
37
- `model="${model}"`,
43
+ `model="${tomlStr(model)}"`,
38
44
  '--config',
39
45
  `model_providers.${PROVIDER_ID}.name="TokenMix"`,
40
46
  '--config',
41
- `model_providers.${PROVIDER_ID}.base_url="${baseUrl}"`,
47
+ `model_providers.${PROVIDER_ID}.base_url="${tomlStr(baseUrl)}"`,
42
48
  '--config',
43
49
  `model_providers.${PROVIDER_ID}.env_key="${KEY_ENV}"`,
44
50
  '--config',
@@ -11,6 +11,9 @@ async function configure(apiKey, baseUrl, defaultModel) {
11
11
  // plus apiBase/apiKey for an OpenAI-compatible endpoint). We PRINT a ready-to-use
12
12
  // config rather than write the file, so we never clobber a user's existing
13
13
  // ~/.continue/config.yaml (and need no YAML dependency or cleanup step).
14
+ // Quote user-controlled scalars as YAML single-quoted strings so a model/baseUrl
15
+ // containing ':' '#' etc. can't break the structure when pasted into config.yaml.
16
+ const ys = (v) => `'${v.replace(/'/g, "''")}'`;
14
17
  const yaml = [
15
18
  'name: TokenMix',
16
19
  'version: 1.0.0',
@@ -18,9 +21,9 @@ async function configure(apiKey, baseUrl, defaultModel) {
18
21
  'models:',
19
22
  ' - name: TokenMix',
20
23
  ' provider: openai',
21
- ` model: ${defaultModel}`,
22
- ` apiBase: ${v1Url(baseUrl)}`,
23
- ` apiKey: ${apiKey}`,
24
+ ` model: ${ys(defaultModel)}`,
25
+ ` apiBase: ${ys(v1Url(baseUrl))}`,
26
+ ` apiKey: ${ys(apiKey)}`,
24
27
  ].join('\n');
25
28
  return {
26
29
  notes: [
@@ -23,7 +23,7 @@ async function configure(apiKey, baseUrl, defaultModel) {
23
23
  return {
24
24
  envVars: {
25
25
  LLM_API_KEY: apiKey,
26
- LLM_MODEL: `openai/${defaultModel}`,
26
+ LLM_MODEL: defaultModel.includes('/') ? defaultModel : `openai/${defaultModel}`,
27
27
  LLM_BASE_URL: v1Url(baseUrl),
28
28
  OPENHANDS_SUPPRESS_BANNER: '1',
29
29
  },
@@ -38,6 +38,7 @@ export function registerAgentCommands(program, runner = runAgent) {
38
38
  .description(t('cmd.agent', { name: agent.displayName }))
39
39
  .allowUnknownOption(true)
40
40
  .passThroughOptions(true) // forward --version / --help / --any to the underlying agent
41
+ .helpOption(false) // so `tokenmix <agent> --help` reaches the agent, not commander's wrapper help
41
42
  .action(async (args = []) => {
42
43
  await runner(agent, args);
43
44
  });
@@ -23,11 +23,10 @@ export function getLocale() {
23
23
  // Translate a key for the active locale, filling {placeholders} from params.
24
24
  // Falls back to English, then to the raw key, so a partial catalog never throws.
25
25
  export function t(key, params) {
26
- let s = catalogs[current]?.[key] ?? en[key] ?? key;
27
- if (params) {
28
- for (const [k, v] of Object.entries(params)) {
29
- s = s.split(`{${k}}`).join(String(v));
30
- }
31
- }
32
- return s;
26
+ const s = catalogs[current]?.[key] ?? en[key] ?? key;
27
+ if (!params)
28
+ return s;
29
+ // Single-pass replacement so a value that itself contains "{other}" is NOT
30
+ // re-substituted by a later param (order-independent and injection-safe).
31
+ return s.replace(/\{(\w+)\}/g, (match, name) => Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match);
33
32
  }
@@ -6,6 +6,7 @@
6
6
  // `{name}`-style placeholders are filled by t() at call time.
7
7
  export const en = {
8
8
  'common.notLoggedIn': 'Not logged in. Run `tokenmix login` first.',
9
+ 'cli.unknownCommand': 'Unknown command: {cmd}. Run `tokenmix --help` to see available commands.',
9
10
  'config.corrupt': 'Your TokenMix config file was corrupt and has been ignored — run `tokenmix login` to re-create it.',
10
11
  // welcome screen (bare `tokenmix`, no args — onboarding for first-time users)
11
12
  'welcome.tagline': 'one account, 160+ models, every open-source coding agent',
@@ -185,6 +186,7 @@ export const en = {
185
186
  };
186
187
  export const zh = {
187
188
  'common.notLoggedIn': '未登录,请先运行 `tokenmix login`。',
189
+ 'cli.unknownCommand': '未知命令:{cmd}。运行 `tokenmix --help` 查看可用命令。',
188
190
  'config.corrupt': 'TokenMix 配置文件已损坏,已忽略 —— 请运行 `tokenmix login` 重新创建。',
189
191
  'welcome.tagline': '一个账户、160+ 模型、对接所有开源编程 agent',
190
192
  'welcome.why': '你选的模型就是你用的模型 —— 不偷换、不降级,按真实用量计费。',
@@ -356,6 +358,7 @@ export const zh = {
356
358
  };
357
359
  export const ja = {
358
360
  'common.notLoggedIn': 'ログインしていません。まず `tokenmix login` を実行してください。',
361
+ 'cli.unknownCommand': '不明なコマンド: {cmd}。`tokenmix --help` で利用可能なコマンドを確認してください。',
359
362
  'config.corrupt': 'TokenMix の設定ファイルが破損していたため無視しました —— `tokenmix login` を実行して再作成してください。',
360
363
  'welcome.tagline': '1 つのアカウントで 160+ モデル、あらゆるオープンソース・コーディング agent に対応',
361
364
  'welcome.why': '選んだモデルがそのまま使われます —— すり替えなし、実使用量で課金。',
@@ -512,6 +515,7 @@ export const ja = {
512
515
  };
513
516
  export const ko = {
514
517
  'common.notLoggedIn': '로그인되어 있지 않습니다. 먼저 `tokenmix login`을 실행하세요.',
518
+ 'cli.unknownCommand': '알 수 없는 명령: {cmd}. `tokenmix --help`로 사용 가능한 명령을 확인하세요.',
515
519
  'config.corrupt': 'TokenMix 설정 파일이 손상되어 무시했습니다 —— `tokenmix login`을 실행해 다시 만드세요.',
516
520
  'welcome.tagline': '하나의 계정으로 160+ 모델, 모든 오픈소스 코딩 agent 지원',
517
521
  'welcome.why': '선택한 모델 그대로 실행됩니다 —— 몰래 바꾸지 않고 실사용량으로 과금합니다.',
@@ -668,6 +672,7 @@ export const ko = {
668
672
  };
669
673
  export const es = {
670
674
  'common.notLoggedIn': 'No has iniciado sesión. Ejecuta `tokenmix login` primero.',
675
+ 'cli.unknownCommand': 'Comando desconocido: {cmd}. Ejecuta `tokenmix --help` para ver los comandos disponibles.',
671
676
  'config.corrupt': 'Tu archivo de configuración de TokenMix estaba dañado y se ha ignorado: ejecuta `tokenmix login` para volver a crearlo.',
672
677
  'welcome.tagline': 'una cuenta, 160+ modelos, todos los agentes de programación de código abierto',
673
678
  'welcome.why': 'El modelo que eliges es el que usas: sin cambios ocultos, facturado por uso real.',
@@ -824,6 +829,7 @@ export const es = {
824
829
  };
825
830
  export const fr = {
826
831
  'common.notLoggedIn': 'Non connecté. Exécutez d’abord `tokenmix login`.',
832
+ 'cli.unknownCommand': 'Commande inconnue : {cmd}. Exécutez `tokenmix --help` pour voir les commandes disponibles.',
827
833
  'config.corrupt': 'Votre fichier de configuration TokenMix était corrompu et a été ignoré — exécutez `tokenmix login` pour le recréer.',
828
834
  'welcome.tagline': 'un seul compte, 160+ modèles, tous les agents de codage open source',
829
835
  'welcome.why': 'Le modèle que vous choisissez est celui que vous utilisez — sans substitution, facturé à l’usage réel.',
package/dist/program.js CHANGED
@@ -9,6 +9,7 @@ import { listCommand } from './commands/list.js';
9
9
  import { doctorCommand } from './commands/doctor.js';
10
10
  import { welcomeCommand } from './commands/welcome.js';
11
11
  import { registerAgentCommands } from './commands/agent-runner.js';
12
+ import { logger } from './utils/logger.js';
12
13
  import { t } from './i18n/index.js';
13
14
  // Read version from package.json so we never have to bump it in two places.
14
15
  const pkg = createRequire(import.meta.url)('../package.json');
@@ -25,7 +26,15 @@ export function buildProgram(deps = {}) {
25
26
  .description(t('cmd.program'))
26
27
  .version(pkg.version)
27
28
  // Bare `tokenmix` (no command) shows a friendly onboarding screen, not raw help.
28
- .action(welcomeCommand);
29
+ // An unrecognized command would otherwise be swallowed into this default action
30
+ // (printing welcome + exiting 0), so reject it explicitly instead.
31
+ .action((_options, command) => {
32
+ if (command.args.length > 0) {
33
+ logger.error(t('cli.unknownCommand', { cmd: command.args.join(' ') }));
34
+ process.exit(1);
35
+ }
36
+ return welcomeCommand();
37
+ });
29
38
  program
30
39
  .command('login')
31
40
  .description(t('cmd.login'))
@@ -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.trim().startsWith('sk-tm-') ? true : t('login.keyMustStart')),
9
9
  });
10
10
  const key = r.apiKey?.trim();
11
11
  return key || null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmix",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
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": {