claude-code-model-switch 2.2.1 → 2.2.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.
@@ -0,0 +1,96 @@
1
+ name: Release npm package
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to publish, for example 2.2.4"
8
+ required: true
9
+ type: string
10
+
11
+ permissions:
12
+ contents: write
13
+ id-token: write
14
+
15
+ concurrency:
16
+ group: npm-release
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ release:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - name: Check out source
24
+ uses: actions/checkout@v6
25
+ with:
26
+ fetch-depth: 0
27
+
28
+ - name: Set up Node.js
29
+ uses: actions/setup-node@v7
30
+ with:
31
+ node-version: 24
32
+ registry-url: https://registry.npmjs.org
33
+ package-manager-cache: false
34
+
35
+ - name: Validate release version
36
+ env:
37
+ RELEASE_VERSION: ${{ inputs.version }}
38
+ run: |
39
+ if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
40
+ echo "Invalid semantic version: $RELEASE_VERSION"
41
+ exit 1
42
+ fi
43
+
44
+ PACKAGE_NAME="$(node -p "require('./package.json').name")"
45
+ if git ls-remote --exit-code --tags origin "refs/tags/v$RELEASE_VERSION" >/dev/null 2>&1; then
46
+ echo "Release tag v$RELEASE_VERSION already exists"
47
+ exit 1
48
+ fi
49
+
50
+ if npm view "$PACKAGE_NAME@$RELEASE_VERSION" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
51
+ echo "npm version $RELEASE_VERSION is already published"
52
+ exit 1
53
+ fi
54
+
55
+ CURRENT_VERSION="$(node -p "require('./package.json').version")"
56
+ if [[ "$CURRENT_VERSION" != "$RELEASE_VERSION" ]]; then
57
+ npm version "$RELEASE_VERSION" --no-git-tag-version --dry-run
58
+ else
59
+ echo "Package is already at version $RELEASE_VERSION"
60
+ fi
61
+
62
+ - name: Install dependencies
63
+ run: npm ci
64
+
65
+ - name: Run tests
66
+ run: npm test
67
+
68
+ - name: Update package version
69
+ env:
70
+ RELEASE_VERSION: ${{ inputs.version }}
71
+ run: |
72
+ CURRENT_VERSION="$(node -p "require('./package.json').version")"
73
+ if [[ "$CURRENT_VERSION" != "$RELEASE_VERSION" ]]; then
74
+ npm version "$RELEASE_VERSION" --no-git-tag-version
75
+ else
76
+ echo "Package is already at version $RELEASE_VERSION"
77
+ fi
78
+
79
+ - name: Publish package
80
+ run: npm publish --access public
81
+
82
+ - name: Commit and push release
83
+ env:
84
+ RELEASE_VERSION: ${{ inputs.version }}
85
+ BRANCH_NAME: ${{ github.ref_name }}
86
+ run: |
87
+ git config user.name "github-actions[bot]"
88
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
89
+ if ! git diff --quiet -- package.json package-lock.json; then
90
+ git add package.json package-lock.json
91
+ git commit -m "chore(release): $RELEASE_VERSION"
92
+ else
93
+ echo "Package version files are already up to date"
94
+ fi
95
+ git tag "v$RELEASE_VERSION"
96
+ git push origin "HEAD:$BRANCH_NAME" "v$RELEASE_VERSION"
package/README.md CHANGED
@@ -95,7 +95,8 @@ Edit the configuration file `~/.claude-code-model-switch/settings.json`:
95
95
  | `ANTHROPIC_DEFAULT_SONNET_MODEL` | ❌ | Same as `ANTHROPIC_MODEL` | Default Sonnet model |
96
96
  | `ANTHROPIC_DEFAULT_OPUS_MODEL` | ❌ | Same as `ANTHROPIC_MODEL` | Default Opus model |
97
97
  | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | ❌ | `8192` | Maximum output tokens |
98
- | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | ❌ | `1` | Disable non-essential traffic |
98
+ | `CLAUDE_CODE_ATTRIBUTION_HEADER` | ❌ | `0` | Attribution header setting |
99
+ | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | ❌ | Not set | Disable non-essential traffic |
99
100
 
100
101
  ### Special Rules
101
102
 
@@ -104,7 +105,8 @@ Edit the configuration file `~/.claude-code-model-switch/settings.json`:
104
105
  3. **shared env vars**: variables in `shared` apply to every model; model-specific values override shared ones
105
106
  4. **Default Values**:
106
107
  - `CLAUDE_CODE_MAX_OUTPUT_TOKENS` defaults to `8192`
107
- - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` defaults to `1`
108
+ - `CLAUDE_CODE_ATTRIBUTION_HEADER` defaults to `0`
109
+ - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` is only set when explicitly configured
108
110
  - Unset model fields automatically use the value of `ANTHROPIC_MODEL`
109
111
  4. **Configuration File**: Empty configuration file `{"shared": {}, "models": {}}` is automatically created on first run
110
112
  5. **Unset Command**: `ccms unset` removes only model-specific variables from `settings.json` and restores official models
@@ -138,4 +140,4 @@ Issues and Pull Requests are welcome!
138
140
 
139
141
  ## 🔗 Repository
140
142
 
141
- https://github.com/musnows/claude-code-model-switch
143
+ https://github.com/musnows/claude-code-model-switch
package/README_CN.md CHANGED
@@ -95,7 +95,8 @@ ccms shared unset API_TIMEOUT_MS
95
95
  | `ANTHROPIC_DEFAULT_SONNET_MODEL` | ❌ | 同 `ANTHROPIC_MODEL` | 默认 Sonnet 模型 |
96
96
  | `ANTHROPIC_DEFAULT_OPUS_MODEL` | ❌ | 同 `ANTHROPIC_MODEL` | 默认 Opus 模型 |
97
97
  | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | ❌ | `8192` | 最大输出令牌数 |
98
- | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | ❌ | `1` | 禁用非必要流量 |
98
+ | `CLAUDE_CODE_ATTRIBUTION_HEADER` | ❌ | `0` | 归属标识头设置 |
99
+ | `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` | ❌ | 不设置 | 禁用非必要流量 |
99
100
 
100
101
  ### 特殊规则
101
102
 
@@ -104,7 +105,8 @@ ccms shared unset API_TIMEOUT_MS
104
105
  3. **shared 公共变量**:`shared` 中的环境变量会应用到所有模型;单个模型里同名变量会覆盖 shared
105
106
  4. **默认值处理**:
106
107
  - `CLAUDE_CODE_MAX_OUTPUT_TOKENS` 默认为 `8192`
107
- - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` 默认为 `1`
108
+ - `CLAUDE_CODE_ATTRIBUTION_HEADER` 默认为 `0`
109
+ - `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC` 仅在显式配置时设置
108
110
  - 未设置的模型字段会自动使用 `ANTHROPIC_MODEL` 的值
109
111
  4. **配置文件**:首次运行时会自动创建空的配置文件 `{"shared": {}, "models": {}}`
110
112
  5. **Unset 命令**:`ccms unset` 只删除 `settings.json` 中的模型相关变量,恢复使用官方模型
@@ -138,4 +140,4 @@ MIT License
138
140
 
139
141
  ## 🔗 开源地址
140
142
 
141
- https://github.com/musnows/claude-code-model-switch
143
+ https://github.com/musnows/claude-code-model-switch
package/argv.js ADDED
@@ -0,0 +1,33 @@
1
+ function findStartModelIndex(args) {
2
+ for (let index = 0; index < args.length - 1; index += 1) {
3
+ if (args[index] === 'start' && args[index + 1] && !args[index + 1].startsWith('-')) {
4
+ return index;
5
+ }
6
+ }
7
+
8
+ return -1;
9
+ }
10
+
11
+ function normalizeStartCommandArgs(args) {
12
+ if (args[0] === 'start') {
13
+ return args;
14
+ }
15
+
16
+ const startIndex = findStartModelIndex(args);
17
+ if (startIndex === -1) {
18
+ return args;
19
+ }
20
+
21
+ const modelName = args[startIndex + 1];
22
+ return [
23
+ 'start',
24
+ modelName,
25
+ ...args.slice(0, startIndex),
26
+ ...args.slice(startIndex + 2),
27
+ ];
28
+ }
29
+
30
+ module.exports = {
31
+ findStartModelIndex,
32
+ normalizeStartCommandArgs,
33
+ };
package/index.js CHANGED
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawn } = require('child_process');
6
6
  const { program } = require('commander');
7
+ const { normalizeStartCommandArgs } = require('./argv');
7
8
 
8
9
  const CLAUDE_SETTINGS_PATH = path.join(process.env.HOME, '.claude', 'settings.json');
9
10
  const MODEL_CONFIG_PATH = path.join(process.env.HOME, '.claude-code-model-switch', 'settings.json');
@@ -11,7 +12,9 @@ const CLAUDE_BIN = process.env.CLAUDE_BIN || 'claude';
11
12
  const YOLO_ARGS = ['--dangerously-skip-permissions'];
12
13
 
13
14
  const DEFAULT_MAX_OUTPUT_TOKENS = '8192';
14
- const DEFAULT_DISABLE_NONESSENTIAL_TRAFFIC = '1';
15
+ const DEFAULT_ATTRIBUTION_HEADER = '0';
16
+ const DEFAULT_MAX_CONTEXT_TOKENS = '200000';
17
+ const DEFAULT_AUTOCOMPACT_PCT_OVERRIDE = '80';
15
18
 
16
19
  const MODEL_ENV_VARS = [
17
20
  'ANTHROPIC_AUTH_TOKEN',
@@ -22,6 +25,9 @@ const MODEL_ENV_VARS = [
22
25
  'ANTHROPIC_DEFAULT_OPUS_MODEL',
23
26
  'ANTHROPIC_DEFAULT_HAIKU_MODEL',
24
27
  'CLAUDE_CODE_MAX_OUTPUT_TOKENS',
28
+ 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
29
+ 'CLAUDE_AUTOCOMPACT_PCT_OVERRIDE',
30
+ 'CLAUDE_CODE_ATTRIBUTION_HEADER',
25
31
  'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
26
32
  'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
27
33
  'API_TIMEOUT_MS'
@@ -34,7 +40,14 @@ const UNSET_ENV_VARS = [
34
40
  'ANTHROPIC_SMALL_FAST_MODEL',
35
41
  'ANTHROPIC_DEFAULT_SONNET_MODEL',
36
42
  'ANTHROPIC_DEFAULT_OPUS_MODEL',
37
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL'
43
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
44
+ 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
45
+ 'CLAUDE_AUTOCOMPACT_PCT_OVERRIDE'
46
+ ];
47
+
48
+ const CONTEXT_ENV_VARS = [
49
+ 'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
50
+ 'CLAUDE_AUTOCOMPACT_PCT_OVERRIDE'
38
51
  ];
39
52
 
40
53
  const DERIVED_MODEL_FIELDS = [
@@ -160,6 +173,10 @@ function mergeModelConfig(modelName) {
160
173
  return { ...shared, ...modelConfig };
161
174
  }
162
175
 
176
+ function hasConfiguredValue(value) {
177
+ return value !== undefined && value !== null && value !== '';
178
+ }
179
+
163
180
  function normalizeModelConfig(modelConfig) {
164
181
  const normalized = { ...modelConfig };
165
182
 
@@ -171,6 +188,19 @@ function normalizeModelConfig(modelConfig) {
171
188
  }
172
189
  }
173
190
 
191
+ normalized.CLAUDE_CODE_ATTRIBUTION_HEADER = DEFAULT_ATTRIBUTION_HEADER;
192
+
193
+ const hasOneMillionContextSuffix = String(normalized.ANTHROPIC_MODEL || '').endsWith('[1m]');
194
+ const hasConfiguredContextTokens = hasConfiguredValue(normalized.CLAUDE_CODE_MAX_CONTEXT_TOKENS);
195
+
196
+ if (!hasOneMillionContextSuffix && !hasConfiguredContextTokens) {
197
+ normalized.CLAUDE_CODE_MAX_CONTEXT_TOKENS = DEFAULT_MAX_CONTEXT_TOKENS;
198
+
199
+ if (!hasConfiguredValue(normalized.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE)) {
200
+ normalized.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE = DEFAULT_AUTOCOMPACT_PCT_OVERRIDE;
201
+ }
202
+ }
203
+
174
204
  return normalized;
175
205
  }
176
206
 
@@ -195,10 +225,6 @@ function buildModelEnv(modelConfig) {
195
225
  env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = DEFAULT_MAX_OUTPUT_TOKENS;
196
226
  }
197
227
 
198
- if (!env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) {
199
- env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = DEFAULT_DISABLE_NONESSENTIAL_TRAFFIC;
200
- }
201
-
202
228
  return env;
203
229
  }
204
230
 
@@ -274,6 +300,12 @@ function applyModelConfig(modelConfig) {
274
300
  settings.env = {};
275
301
  }
276
302
 
303
+ for (const envVar of CONTEXT_ENV_VARS) {
304
+ if (!Object.prototype.hasOwnProperty.call(normalized, envVar)) {
305
+ delete settings.env[envVar];
306
+ }
307
+ }
308
+
277
309
  for (const [key, value] of Object.entries(normalized)) {
278
310
  if (value !== undefined && value !== null) {
279
311
  settings.env[key] = String(value);
@@ -291,10 +323,6 @@ function applyModelConfig(modelConfig) {
291
323
  settings.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = DEFAULT_MAX_OUTPUT_TOKENS;
292
324
  }
293
325
 
294
- if (!settings.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC) {
295
- settings.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = DEFAULT_DISABLE_NONESSENTIAL_TRAFFIC;
296
- }
297
-
298
326
  saveClaudeSettings(settings);
299
327
  console.log(`Switched to model: ${settings.env.ANTHROPIC_MODEL}`);
300
328
  }
@@ -352,6 +380,8 @@ function unsetModelConfig() {
352
380
  settings.env = {};
353
381
  }
354
382
 
383
+ settings.env.CLAUDE_CODE_ATTRIBUTION_HEADER = DEFAULT_ATTRIBUTION_HEADER;
384
+
355
385
  let removedCount = 0;
356
386
  const removedVars = [];
357
387
 
@@ -390,7 +420,7 @@ function listModels() {
390
420
  program
391
421
  .name('ccms')
392
422
  .description('Claude Code Model Switch - Manage and launch Claude Code with configured models')
393
- .version('2.2.0');
423
+ .version(require('./package.json').version);
394
424
 
395
425
  program
396
426
  .command('start <model> [claude-args...]')
@@ -465,6 +495,14 @@ program
465
495
  process.exit(1);
466
496
  });
467
497
 
498
+ // Multica may place Claude protocol args before the ccms subcommand. Normalize
499
+ // only when a concrete start/model pair exists; otherwise leave argv untouched
500
+ // so Commander keeps its normal unsupported-argument error.
501
+ process.argv = [
502
+ ...process.argv.slice(0, 2),
503
+ ...normalizeStartCommandArgs(process.argv.slice(2)),
504
+ ];
505
+
468
506
  program.parse();
469
507
 
470
508
  if (!process.argv.slice(2).length) {
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "claude-code-model-switch",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Launch Claude Code with preconfigured model environments",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/musnows/claude-code-model-switch"
8
+ },
5
9
  "main": "index.js",
6
10
  "bin": {
7
11
  "ccms": "index.js"
8
12
  },
9
13
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
14
+ "test": "node --test"
11
15
  },
12
16
  "keywords": [
13
17
  "claude",
@@ -0,0 +1,34 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+
4
+ const { findStartModelIndex, normalizeStartCommandArgs } = require('../argv');
5
+
6
+ test('finds start and model after protocol arguments', () => {
7
+ const args = ['-p', '--output-format', 'stream-json', 'start', 'dsf'];
8
+
9
+ assert.equal(findStartModelIndex(args), 3);
10
+ });
11
+
12
+ test('moves start and model before protocol arguments', () => {
13
+ const args = ['-p', '--output-format', 'stream-json', 'start', 'dsf'];
14
+
15
+ assert.deepEqual(normalizeStartCommandArgs(args), [
16
+ 'start',
17
+ 'dsf',
18
+ '-p',
19
+ '--output-format',
20
+ 'stream-json',
21
+ ]);
22
+ });
23
+
24
+ test('preserves the standard start command order', () => {
25
+ const args = ['start', 'glm53flash', '-p', 'hello'];
26
+
27
+ assert.deepEqual(normalizeStartCommandArgs(args), args);
28
+ });
29
+
30
+ test('leaves arguments unchanged when no start model pair exists', () => {
31
+ const args = ['-p', 'hello'];
32
+
33
+ assert.deepEqual(normalizeStartCommandArgs(args), args);
34
+ });
@@ -0,0 +1,12 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ const source = fs.readFileSync(path.join(__dirname, '..', 'index.js'), 'utf8');
7
+
8
+ test('does not assign a default non-essential traffic setting', () => {
9
+ assert.doesNotMatch(source, /DEFAULT_DISABLE_NONESSENTIAL_TRAFFIC/);
10
+ assert.doesNotMatch(source, /env\.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\s*=/);
11
+ assert.doesNotMatch(source, /settings\.env\.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC\s*=/);
12
+ });