├── index.js ├── .npmignore ├── assets └── screenshot.png ├── CLAUDE.md ├── .gitignore ├── package.json ├── docs ├── plan.md └── plan.v2.md ├── lib ├── providers.js └── utils.js ├── .github └── workflows │ ├── claude.yml │ └── claude-code-review.yml ├── Agents.md ├── README.zh.md ├── README.md └── bin └── cli.js /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | version: require('./package.json').version 3 | }; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .claude/ 2 | .github/ 3 | assets/ 4 | docs/ 5 | CLAUDE.md 6 | Agents.md 7 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaichen/kimicc/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | Follow @Agents.md 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .DS_Store 4 | .env 5 | .env.local 6 | .env.development.local 7 | .env.test.local 8 | .env.production.local 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | .kimicc.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kimicc", 3 | "version": "2.1.1", 4 | "description": "Run Claude Code with Kimi K2 API - 支持使用多种大模型驱动运行 Claude Code", 5 | "main": "index.js", 6 | "bin": { 7 | "kimicc": "./bin/cli.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"no test is fine\"" 11 | }, 12 | "keywords": [ 13 | "cli", 14 | "claude", 15 | "claude-code", 16 | "kimi", 17 | "deepseek", 18 | "qwen", 19 | "glm", 20 | "ai", 21 | "llm" 22 | ], 23 | "author": "Kai", 24 | "license": "MIT", 25 | "dependencies": { 26 | "commander": "^14.0.0" 27 | }, 28 | "engines": { 29 | "node": ">=18.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/plan.md: -------------------------------------------------------------------------------- 1 | build nodejs cli wrap claude-code cli to use Kimi K2 LLM. 2 | 3 | launch cli as `kimicc` 4 | The final setup is super simple — users just need to run `npx kimicc`. 5 | 6 | Here’s how the claude code package check and install works: When you start, it checks if claude is already in your execution path. If it is, it skips the install. If not, it runs `npm install -g @anthropic-ai/claude-code` to get it installed. 7 | 8 | For the Auth Token: On startup, it looks for KIMI_AUTH_TOKEN first. If missing, it'll prompt you interactively to enter your kimi auth token, which it then saves to ~/.kimicc.json. 9 | 10 | Next, you need to update the Claude settings by checking the ~/.claude/settings.json file. Change the value of autoUpdates to false (add this key if it’s not there), and set hasCompletedOnboarding to true (add this key if it’s missing). 11 | 12 | Before launching, it sets up environment variables: it assigns ANTHROPIC_AUTH_TOKEN (from the method above) and ANTHROPIC_BASE_URL=https://api.moonshot.ai/anthropic, then starts the claude process. 13 | 14 | Focus on launch project ASAP, keep everything simple. 15 | -------------------------------------------------------------------------------- /lib/providers.js: -------------------------------------------------------------------------------- 1 | const PROVIDERS = { 2 | kimi: { 3 | slug: 'kimi', 4 | name: 'Kimi', 5 | baseUrl: 'https://api.moonshot.cn/anthropic', 6 | defaultModel: 'kimi-k2-turbo-preview', 7 | defaultSmallFastModel: 'kimi-k2-turbo-preview' 8 | }, 9 | deepseek: { 10 | slug: 'deepseek', 11 | name: 'DeepSeek', 12 | baseUrl: 'https://api.deepseek.com/anthropic', 13 | defaultModel: 'deepseek-chat', 14 | defaultSmallFastModel: 'deepseek-chat' 15 | }, 16 | glm: { 17 | slug: 'glm', 18 | name: 'GLM', 19 | baseUrl: 'https://open.bigmodel.cn/api/anthropic', 20 | defaultModel: 'glm-4.6', 21 | defaultSmallFastModel: 'glm-4.5-air' 22 | }, 23 | qwen: { 24 | slug: 'qwen', 25 | name: 'Qwen', 26 | baseUrl: 'https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy', 27 | defaultModel: 'qwen3-coder-plus', 28 | defaultSmallFastModel: 'qwen3-coder-plus' 29 | } 30 | }; 31 | 32 | function getProviderBySlug(slug) { 33 | if (!slug) return null; 34 | return PROVIDERS[slug] || null; 35 | } 36 | 37 | function parseProviderFlag(args) { 38 | if (!Array.isArray(args)) return null; 39 | for (let i = 0; i < args.length; i++) { 40 | const arg = args[i]; 41 | if (typeof arg !== 'string') continue; 42 | if (!arg.startsWith('--')) continue; 43 | const name = arg.slice(2); 44 | if (PROVIDERS[name]) { 45 | return { slug: name, index: i }; 46 | } 47 | } 48 | return null; 49 | } 50 | 51 | module.exports = { 52 | PROVIDERS, 53 | getProviderBySlug, 54 | parseProviderFlag 55 | }; 56 | -------------------------------------------------------------------------------- /docs/plan.v2.md: -------------------------------------------------------------------------------- 1 | # KimiCC v2 Plan 2 | 3 | ## feature#1 reset subcommand 4 | 5 | 添加 reset 子命令删除 .kimicc.json 配置文件。 6 | 7 | DONE: PR#3 8 | 9 | ## feature#2 inject managed env into `.claude/settings.json` 10 | 11 | 添加 inject 子命令,实现以下能力: 12 | 13 | - 将 `ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_BASE_URL`、`ANTHROPIC_MODEL`、`ANTHROPIC_SMALL_FAST_MODEL` 写入 `~/.claude/settings.json` 的 `env` 字段,方便 `claude` CLI 直接读取。 14 | - 支持 `--profile`、`--base-url`、`--model`、`--small-fast-model` 覆盖写入值,并且优先级与运行时保持一致(环境变量 > profile > 默认值)。 15 | - 提供 `kimicc inject --reset` 从 `~/.claude/settings.json` 中移除上述受管键,不影响其他字段。 16 | 17 | ## feature#3 multi profiles 18 | 19 | 为了可以使用多个服务供应商并发多个 kimicc 实例,可以在 .kimicc.json 配置文件中添加 `{'profiles':{ $slug: { url: $url, key: $authtoken }, ... }}` 的配置,并在启动时选择不同 profile 设定。 20 | 21 | 提供子命令 22 | - profile list :列出所有 profile。 23 | - profile add --slug $slug $url $authtoken :添加新的profile,自动将 url 中的 hostname,如 123.example.com => examplecom 作为 slug,也可指定 slug。 24 | - profile del $slug :删除某个 profile,需要用户输入作为确认。 25 | - profile del -i :交互式删除,列出 profile 与序号,用户输入一个或多个数字(逗号隔开),按照选择删除。 26 | 27 | profile add 时,支持添加 --model 可选选项,添加到 profile 配置中,以 model 为 key 存储;如果 profile 设定了 model,则将 ANTHROPIC_MODEL 与 ANTHROPIC_SMALL_FAST_MODEL 环境变量设定为 profile 的 model。 28 | 29 | 30 | 启动时 kimicc --profile 选择不同的 profile,选择后读取配置中的 url 与 key 设定到 claude 启动的进程环境变量中。 31 | 32 | 向前兼容,执行 profile add 时,假如从未添加过 profile,并且设置过 authToken,则自动把默认的 ANTHROPIC_BASE_URL=https://api.moonshot.cn/anthropic 以及 authToken 迁移到 profile 形式,并设定为 default。 33 | 34 | 配置优先级,理论上应当保持这三种状态,要么有 profile 没有 authToken,要么没有 profile 有 authToken,要么都没有。 35 | -------------------------------------------------------------------------------- /.github/workflows/claude.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | actions: read # Required for Claude to read CI results on PRs 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 1 32 | 33 | - name: Run Claude Code 34 | id: claude 35 | uses: anthropics/claude-code-action@v1 36 | with: 37 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 38 | 39 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 40 | # claude_args: --model claude-opus-4-1 41 | 42 | # Optional: Customize the trigger phrase (default: @claude) 43 | trigger_phrase: "/claude" 44 | 45 | # Optional: Trigger when specific user is assigned to an issue 46 | assignee_trigger: "claude-bot" 47 | 48 | # Optional: Allow Claude to run specific commands 49 | # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)" 50 | 51 | # Optional: Add custom instructions for Claude to customize its behavior for your project 52 | # custom_instructions: | 53 | # Follow our coding standards 54 | # Ensure all new code has tests 55 | # Use TypeScript for new files 56 | 57 | # Optional: Custom environment variables for Claude 58 | # claude_env: | 59 | # NODE_ENV: test 60 | 61 | -------------------------------------------------------------------------------- /.github/workflows/claude-code-review.yml: -------------------------------------------------------------------------------- 1 | name: Claude Code Review 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | # Optional: Only run on specific file changes 7 | paths: 8 | - "bin/**/*.js" 9 | - "lib/**/*.js" 10 | 11 | jobs: 12 | claude-review: 13 | # Optional: Filter by PR author 14 | # if: | 15 | # github.event.pull_request.user.login == 'external-contributor' || 16 | # github.event.pull_request.user.login == 'new-developer' || 17 | # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' 18 | 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: read 22 | pull-requests: read 23 | issues: read 24 | id-token: write 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 1 31 | 32 | - name: Run Claude Code Review 33 | id: claude-review 34 | uses: anthropics/claude-code-action@v1 35 | with: 36 | claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} 37 | 38 | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) 39 | # claude_args: --model claude-opus-4-1 40 | 41 | # Direct prompt for automated review (no @claude mention needed) 42 | prompt: | 43 | Force on critical issues. 44 | Please review this pull request and provide feedback on: 45 | - Code quality and best practices 46 | - Potential bugs or issues 47 | - Performance considerations 48 | - Security concerns 49 | - Test coverage(if unit testing setup) 50 | Be constructive and helpful in your feedback. 51 | 52 | # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR 53 | # use_sticky_comment: true 54 | 55 | # Optional: Customize review based on file types 56 | # prompt: | 57 | # Review this PR focusing on: 58 | # - For TypeScript files: Type safety and proper interface usage 59 | # - For API endpoints: Security, input validation, and error handling 60 | # - For React components: Performance, accessibility, and best practices 61 | # - For tests: Coverage, edge cases, and test quality 62 | 63 | # Optional: Different prompts for different authors 64 | # prompt: | 65 | # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && 66 | # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || 67 | # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} 68 | 69 | # Optional: Add specific tools for running tests or linting 70 | # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" 71 | 72 | # Optional: Skip review for certain conditions 73 | # if: | 74 | # !contains(github.event.pull_request.title, '[skip-review]') && 75 | # !contains(github.event.pull_request.title, '[WIP]') 76 | 77 | -------------------------------------------------------------------------------- /Agents.md: -------------------------------------------------------------------------------- 1 | # Agents.md 2 | 3 | ## Project Overview 4 | 5 | kimicc is a CLI wrapper that allows users to run Claude Code using the Kimi K2 API. It acts as a proxy by intercepting the Claude Code CLI and redirecting API calls to Kimi's endpoint at https://api.moonshot.cn/anthropic. 6 | 7 | ## Architecture 8 | 9 | The project has a simple architecture: 10 | - `bin/cli.js` - Main entry point that handles the CLI lifecycle 11 | - `lib/utils.js` - Utility functions for checking Claude installation, managing auth tokens, and configuration 12 | 13 | ## Key Implementation Details 14 | 15 | ### Authentication 16 | The system exclusively uses `ANTHROPIC_AUTH_TOKEN` for authentication. The legacy `ANTHROPIC_API_KEY` has been removed. 17 | 18 | ### Auth Token Priority 19 | The system checks for auth tokens in this order: 20 | 1. `KIMI_AUTH_TOKEN` environment variable 21 | 2. `~/.kimicc.json` config file (profiles or legacy format) 22 | 3. Interactive prompt (saves to config file) 23 | 24 | ### Environment Setup 25 | Before spawning the Claude process, the wrapper sets: 26 | - `ANTHROPIC_AUTH_TOKEN` - The Kimi auth token 27 | - `ANTHROPIC_BASE_URL` - https://api.moonshot.cn/anthropic 28 | - `ANTHROPIC_MODEL` - (optional) From CLI/env/profile 29 | - `ANTHROPIC_SMALL_FAST_MODEL` - (optional) From CLI/env/profile; falls back to `ANTHROPIC_MODEL` if unset 30 | 31 | ### Multi-Profile Support 32 | - Profiles are stored in `~/.kimicc.json` under the `profiles` key 33 | - Each profile contains: `url`, `key` (auth token), and optionally `model`, `smallFastModel` (may be empty). If `smallFastModel` is not set, runtime falls back to `model`. 34 | - Users can switch profiles using `kimicc --profile ` 35 | - Default profile is stored in `defaultProfile` field 36 | 37 | ### Claude Installation 38 | If `claude` command is not found in PATH, the wrapper automatically runs: 39 | ```bash 40 | npm install -g @anthropic-ai/claude-code 41 | ``` 42 | 43 | ## CLI Commands 44 | 45 | ### Main Commands 46 | - `kimicc` - Start Claude Code with default settings 47 | - `kimicc --profile ` - Start with a specific profile 48 | - `kimicc --model ` - Override model for this run 49 | - `kimicc --small-fast-model ` - Override small/fast model for this run 50 | - `kimicc reset` - Delete the configuration file 51 | - `kimicc inject` - Persist managed Anthropic environment variables into `~/.claude/settings.json` (`env` section) 52 | - `kimicc inject --reset` - Remove managed Anthropic environment variables from `~/.claude/settings.json` 53 | 54 | ### Profile Management 55 | - `kimicc profile list` - List all profiles 56 | - `kimicc profile add [--slug SLUG] [--model MODEL] [--small-fast-model MODEL] [--default] URL AUTH_TOKEN` 57 | - `kimicc profile del SLUG` - Delete a specific profile 58 | - `kimicc profile del -i` - Interactive deletion mode 59 | - `kimicc profile set-default SLUG` - Set default profile 60 | 61 | ## Development Commands 62 | 63 | ```bash 64 | # Install dependencies 65 | npm install 66 | 67 | # Test the CLI locally 68 | node bin/cli.js [arguments] 69 | 70 | # Test with environment variable 71 | KIMI_AUTH_TOKEN=your-token node bin/cli.js [arguments] 72 | 73 | # Link for global testing 74 | npm link 75 | kimicc [arguments] 76 | 77 | # Unlink after testing 78 | npm unlink 79 | ``` 80 | 81 | ## Testing Considerations 82 | 83 | - The project currently targets macOS and Node.js >=18.0.0 84 | - Windows and Linux compatibility is not guaranteed 85 | - Test auth token management flow by removing ~/.kimicc.json 86 | - Test Claude installation detection by checking PATH 87 | - Test profile switching and management commands 88 | - Test `.claude/settings.json` env merge/reset behaviour with different flag combinations 89 | 90 | ## Configuration File Format 91 | 92 | ```json 93 | { 94 | "profiles": { 95 | "default": { 96 | "url": "https://api.moonshot.cn/anthropic", 97 | "key": "sk-..." 98 | }, 99 | "custom": { 100 | "url": "https://api.example.com/anthropic", 101 | "key": "sk-...", 102 | "model": "claude-3-opus-20240229", 103 | "smallFastModel": "claude-3-haiku-20240307" 104 | } 105 | }, 106 | "defaultProfile": "default" 107 | } 108 | ``` 109 | 110 | ## Precedence Rules 111 | 112 | - Auth token: `KIMI_AUTH_TOKEN` > profile/default > prompt 113 | - Base URL: CLI `--base-url`/`base_url=` > `ANTHROPIC_BASE_URL` > profile/default > Kimi default 114 | - Models: CLI `--model`/`--small-fast-model` > `ANTHROPIC_MODEL`/`ANTHROPIC_SMALL_FAST_MODEL` > profile fields > `smallFastModel` falls back to `model` 115 | 116 | ## Legacy Migration 117 | The system automatically migrates legacy configurations (with `apiKey` field) to the new profile format when adding the first profile. 118 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # KIMICC 2 | 3 | [![npm version](https://img.shields.io/npm/v/kimicc.svg)](https://www.npmjs.com/package/kimicc) 4 | 5 | [Read english readme file](README.md) 6 | 7 | One-step command `npx kimicc` to run Claude Code using Kimi K2 / GLM-4.6 / Qwen3-Coder / DeepSeek-v3.2. 8 | 9 | 一步命令 `npx kimicc` 使用 Kimi K2 / GLM-4.5 / Qwen3-Coder / DeepSeek-v3.1 运行 Claude Code。 10 | 11 | 或者 `npm install -g kimicc` 安装 KimiCC。 12 | 13 | This is a lightweight Node.js npm package that sets up environment variables at startup, allowing Claude Code to call Kimi K2 / GLM-4.6 / Qwen3-Coder / DeepSeek-v3.2 models. 14 | 15 | 这是一个轻量的 nodejs 的 npm 包,在启动时设置好环境变量,让 Claude Code 调用 Kimi K2 / GLM-4.6 / Qwen3-Coder / DeepSeek-v3.2 模型。 16 | 17 | ## 为什么使用 Kimi K2 / GLM-4.5 / Qwen3-Coder / DeepSeek-v3.1 运行 Claude Code 18 | 19 | 1. Claude 由于特殊原因难以稳定订阅,并且对一些地区进行技术封锁; 20 | 2. Claude 订阅价格相对于发展中国家的广大群众难以负担以及支付; 21 | 3. 不少大模型厂商都推出了兼容 anthropic 的接口; 22 | 4. 当前好几家国产大模型都达到较高 Agentic 特性,足以驾驭 Claude Code 这个系统; 23 | 5. API 没有网络问题,价格仅有 Claude 的 1/N,支持多种支付方式; 24 | 6. 让更多人体验最先进的开发工具,让厂商卷起来。 25 | 7. v2.0 新增:支持多个不同 API 同时运行,提升并发能力,比如可以同时让多个 Kimi 账号在本地并发。 26 | 27 | ## 这个工具包做了什么? 28 | 29 | 这个工具做一些微小的工作,帮你节省时间去处理配置的工作。底层是在启动 claude 之前设定好 Auth Token 和 Base URL 环境变量参数。 30 | 31 | 对比其他教程让你配置文件,或者在启动时加长长的参数,使用本工具会节省你宝贵的时间精力。 32 | 33 | ## 使用方式 34 | 35 | ### 获取 API Key 36 | 37 | 以下是控制台地址: 38 | - [Kimi](https://platform.moonshot.cn/console/api-keys) 39 | - [GLM/智谱](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) [支持 60 元包月](https://www.bigmodel.cn/claude-code?ic=FHME2QL1XG) 40 | - [Qwen/阿里云百炼](https://bailian.console.aliyun.com/?tab=globalset#/efm/api_key) 41 | - [DeepSeek](https://platform.deepseek.com/api_keys) 42 | 43 | ### 配置与启动 44 | 45 | - 第一步,获取 API Key 46 | - 第二步,在你本地有 Nodejs 环境的前提下,运行 `npx kimicc` 安装并启动,或者使用 `npm install -g kimicc`; 47 | - 第三步,在安装后可使用 kimicc 直接启动,并在提示下输入 Auth Token。 48 | 49 | 首次初始化会提示你输入 Auth Token,下次就无需再次设置。 50 | 51 | ⚠️注意⚠️,启动时 Claude Code 会询问 AUTH TOKEN,默认是 NO(recommended),此时要选 YES。 52 | 53 | Do you want to use this auth token? SELECT YES!!! 54 | 55 | ![screenshot](assets/screenshot.png) 56 | 57 | 如何卸载: `npm uninstall kimicc` 58 | 59 | 完全卸载 claude code:`npm uninstall @anthropic-ai/claude-code` 60 | 61 | ### 使用 Kimi 以外模型的方式 62 | 63 | 默认什么参数都不添加,那么启动使用的是 kimi 模型的配置,若使用以下参数可以启动其他模型 64 | 65 | - `--deepseek` 会设定 base_url=https://api.deepseek.com/anthropic model=deepseek-chat 66 | - `--glm` 会设定 base_url=https://open.bigmodel.cn/api/anthropic model=glm-4.5 67 | - `--qwen` 会设定 base_url=base_url=https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy model=qwen3-coder-plus 68 | 69 | ### 自定义大模型名称 70 | 71 | 启动或设定 Profile 时可以使用以下参数设定模型, 72 | 73 | - `--model` 用于指定主要的大模型型号。 74 | - `--small-fast-model` 用于指定辅助快速小模型,若没有指定则使用 `--model` 配置。 75 | 76 | 这能让用户第一时间使用上大模型发布。即在 kimi 发布 kimi-k2-0905-preview 通过 `kimicc --model=kimi-k2-0905-preview` 使用最新模型。 77 | 78 | ### 附加命令功能 79 | 80 | - `kimicc reset` 重制配置。 81 | - `kimicc inject` 将 Anthropic 环境变量写入 `~/.claude/settings.json` 的 `env` 字段,这样可以直接运行 `claude`。可搭配 `--profile`、`--base-url`、`--model`、`--small-fast-model` 覆盖写入的值。 82 | - `kimicc inject --reset` 从 `~/.claude/settings.json` 中移除受管环境变量(`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_BASE_URL`、`ANTHROPIC_MODEL`、`ANTHROPIC_SMALL_FAST_MODEL`)。 83 | - `kimicc profile` 支持配置多个不同服务提供商。 84 | 85 | ## 实现原理 86 | 87 | Claude Code 暴露出一些环境变量可以用于配置模型服务,其中分为三个等级 haiku sonnet opus。 88 | 89 | - ANTHROPIC_BASE_URL 大模型服务接口地址(这个变量已经不写在官方文档中可能随时会被移除阻止第三方模型使用) 90 | - ANTHROPIC_MODEL 假如没有以下细致配置使用的 fallback 91 | - ANTHROPIC_DEFAULT_HAIKU_MODEL 轻量任务 92 | - ANTHROPIC_DEFAULT_SONNET_MODEL 主力模型 93 | - ANTHROPIC_DEFAULT_OPUS_MODEL 最高智能模型,或用于驱动 Plan 模式 94 | 95 | 注意,其中 ANTHROPIC_SMALL_FAST_MODEL 已经被废弃。 96 | 97 | 只要 claude code 运行时设定以上环境变量即可修改其使用的模型,当然也需要服务接口与 anthropic 保持一致。 98 | 99 | 对于修改环境变量有好几种方式, 100 | 101 | 1. 直接在启动命令行前,以 KEY=value 形式放在命令行之前即可; 102 | 2. 写入到对应 shell 的配置文件,如 bashrc,zshrc; 103 | 3. 写入到 `.claude/settings.json` 配置中的 "env" 下。 104 | 105 | 参考文档 106 | - https://docs.claude.com/en/docs/claude-code/model-config#environment-variables 107 | - https://docs.claude.com/en/docs/claude-code/settings#environment-variables 108 | 109 | ## 版本记录 110 | 111 | - v1.x 基本功能实现 112 | - v2.x 113 | - v2.0.0 支持 profile 功能,可分开启动多个配置 114 | - v2.1.0 默认支持 Kimi K2 / GLM-4.5 / Qwen3-Coder / DeepSeek-v3.1 115 | - v2.1.1 更新 glm-4.6 默认配置 116 | 117 | ## 已知问题 118 | 119 | - 本项目先在 Mac 上开发并测试,不保证 Linux 以及 Windows 系统运行,欢迎反馈问题以及提交 PR。 120 | - 由于 Kimi K2 不支持多模态,无法粘贴和读取图片。 121 | - Kimi 在不同充值档位对请求频率和每日请求量有限制,可能需要至少充值 50 元达到一档才能符合基本使用需求,具体查看[官方接口限速说明](https://platform.moonshot.cn/docs/pricing/limits)。 122 | - 由于这个工具只是修改环境变量,会让 kimicc 也写入 Claude 的数据目录,会共享 session 和数据统计数据,最新版本 v2.1.x 会写入对应模型调用,统计中会将不同模型分开显示。 123 | 124 | 👏 欢迎遇到问题或想要更多功能提出 Issue。 125 | 126 | ## License 127 | 128 | MIT. Kai 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KIMICC 2 | 3 | [![npm version](https://img.shields.io/npm/v/kimicc.svg)](https://www.npmjs.com/package/kimicc) 4 | 5 | [Read Chinese Instructions](README.zh.md) 6 | 7 | One-step command `npx kimicc` to run Claude Code using Kimi K2 / GLM-4.6 / Qwen3-Coder / DeepSeek-v3.2. 8 | 9 | 一步命令 `npx kimicc` 使用 Kimi K2 / GLM-4.5 / Qwen3-Coder / DeepSeek-v3.1 运行 Claude Code。 10 | 11 | Or install KimiCC with `npm install -g kimicc`. 12 | 13 | This is a lightweight Node.js npm package that sets up environment variables at startup, allowing Claude Code to call Kimi K2 / GLM-4.6 / Qwen3-Coder / DeepSeek-v3.2 models. 14 | 15 | 这是一个轻量的 nodejs 的 npm 包,在启动时设置好环境变量,让 Claude Code 调用 Kimi K2 / GLM-4.6 / Qwen3-Coder / DeepSeek-v3.2 模型。 16 | 17 | ## Why use Kimi K2 / GLM-4.5 / Qwen3-Coder / DeepSeek-v3.1 to run Claude Code 18 | 19 | 1. Due to special reasons, Claude is difficult to subscribe to stably and has technical restrictions on certain regions; 20 | 2. Claude subscription prices are unaffordable and difficult to pay for the vast majority of people in developing countries; 21 | 3. Many large model vendors have launched Anthropic-compatible interfaces; 22 | 4. Several domestic Chinese large models have achieved high Agentic capabilities, sufficient to handle the Claude Code system; 23 | 5. API has no network issues, costs only 1/N of Claude's price, supports multiple payment methods; 24 | 6. Let more people experience the most advanced development tools and make vendors compete. 25 | 7. v2.0 added: Support for running multiple different APIs simultaneously, improving concurrent capabilities, such as allowing multiple Kimi accounts to run concurrently locally. 26 | 27 | ## What does this tool package do? 28 | 29 | This tool does some small work to save you time handling configuration tasks. At the bottom layer, it sets up Auth Token and Base URL environment variable parameters before starting claude. 30 | 31 | Compared to other tutorials that require you to configure files or add long parameters at startup, using this tool will save your valuable time and energy. 32 | 33 | ## Usage 34 | 35 | ### Get API Key 36 | 37 | Here are the console addresses: 38 | - [Kimi/Moonshot](https://platform.moonshot.cn/console/api-keys) 39 | - [GLM/Zhipu](https://bigmodel.cn/usercenter/proj-mgmt/apikeys) [Supports 60 RMB monthly subscription](https://www.bigmodel.cn/claude-code?ic=FHME2QL1XG) 40 | - [Qwen/AlibabaCloud](https://bailian.console.aliyun.com/?tab=globalset#/efm/api_key) 41 | - [DeepSeek](https://platform.deepseek.com/api_keys) 42 | 43 | ### Configuration and Startup 44 | 45 | - Step 1: Get API Key 46 | - Step 2: With Node.js environment on your local machine, run `npx kimicc` to install and start, or use `npm install -g kimicc`; 47 | - Step 3: After installation, you can use kimicc to start directly, and enter the Auth Token when prompted. 48 | 49 | First initialization will prompt you to enter Auth Token, no need to set it again next time. 50 | 51 | ⚠️Note⚠️, when starting Claude Code will ask for AUTH TOKEN, default is NO(recommended), at this time select YES. 52 | 53 | Do you want to use this auth token? SELECT YES!!! 54 | 55 | ![screenshot](assets/screenshot.png) 56 | 57 | How to uninstall: `npm uninstall kimicc` 58 | 59 | Completely uninstall claude code: `npm uninstall @anthropic-ai/claude-code` 60 | 61 | ### Using models other than Kimi 62 | 63 | By default, if no parameters are added, it starts with the kimi model configuration. Using the following parameters can start other models 64 | 65 | - `--deepseek` will set base_url=https://api.deepseek.com/anthropic model=deepseek-chat 66 | - `--glm` will set base_url=https://open.bigmodel.cn/api/anthropic model=glm-4.6 67 | - `--qwen` will set base_url=https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy model=qwen3-coder-plus 68 | 69 | ### Custom large model names 70 | 71 | When starting or setting Profile, you can use the following parameters to set models, 72 | 73 | - `--model` used to specify the main large model type. 74 | - `--small-fast-model` used to specify the auxiliary fast small model, if not specified then use `--model` configuration. 75 | 76 | This allows users to use the latest large model releases immediately. For example, when kimi releases kimi-k2-0905-preview, use the latest model via `kimicc --model=kimi-k2-0905-preview`. 77 | 78 | ### Additional command features 79 | 80 | - `kimicc reset` Reset configuration. 81 | - `kimicc inject` Persist Anthropic environment variables into `~/.claude/settings.json` (`env` block) so `claude` picks them up without running `kimicc`. Use `--profile`, `--base-url`, `--model`, or `--small-fast-model` to override values for the saved env. 82 | - `kimicc inject --reset` Remove the managed environment keys (`ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL`) from `~/.claude/settings.json`. 83 | - `kimicc profile` Support configuring multiple different service providers. 84 | 85 | ## Under the hood 86 | 87 | Claude Code exposes some environment variables that can be used to configure model services, which are divided into three levels: haiku, sonnet, and opus. 88 | 89 | - ANTHROPIC_BASE_URL Large model service interface address (this variable is no longer documented in official docs and may be removed at any time to prevent third-party model usage) 90 | - ANTHROPIC_MODEL Fallback used when no detailed configuration below is provided 91 | - ANTHROPIC_DEFAULT_HAIKU_MODEL Lightweight tasks 92 | - ANTHROPIC_DEFAULT_SONNET_MODEL Main model 93 | - ANTHROPIC_DEFAULT_OPUS_MODEL Highest intelligence model, or used to drive Plan mode 94 | 95 | Note that ANTHROPIC_SMALL_FAST_MODEL has been deprecated. 96 | 97 | As long as the above environment variables are set during claude code runtime, the models it uses can be modified, of course the service interface also needs to be consistent with anthropic. 98 | 99 | There are several ways to modify environment variables: 100 | 101 | 1. Directly before the startup command line, in the form of KEY=value placed before the command line; 102 | 2. Write to the corresponding shell configuration file, such as bashrc, zshrc; 103 | 3. Write to the "env" section in the `.claude/settings.json` configuration. 104 | 105 | Reference: 106 | - https://docs.claude.com/en/docs/claude-code/model-config#environment-variables 107 | - https://docs.claude.com/en/docs/claude-code/settings#environment-variables 108 | 109 | ## Version History 110 | 111 | - v1.x Basic functionality implementation 112 | - v2.x 113 | - v2.0.0 Support profile functionality, can start multiple configurations separately 114 | - v2.1.0 Default support for Kimi K2 / GLM-4.5 / Qwen3-Coder / DeepSeek-v3.1 115 | - v2.1.1 update glm-4.6 config 116 | 117 | ## Known Issues 118 | 119 | - This project was first developed and tested on Mac, no guarantee for Linux and Windows systems, welcome to report issues and submit PRs. 120 | - Due to Kimi K2 not supporting multimodal, cannot paste and read images. 121 | - Kimi has restrictions on request frequency and daily request volume at different recharge levels, may need to recharge at least 50 yuan to reach a level that meets basic usage needs, check [official interface rate limit description](https://platform.moonshot.cn/docs/pricing/limits). 122 | - Since this tool only modifies environment variables, it will make kimicc also write to Claude's data directory, sharing session and data statistics. The latest version v2.1.x will write corresponding model calls, and statistics will display different models separately. 123 | 124 | 👏 Welcome to raise issues or request more features. 125 | 126 | ## License 127 | 128 | MIT. Kai 129 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const os = require('os'); 4 | const { execSync } = require('child_process'); 5 | const readline = require('readline'); 6 | const { getProviderBySlug } = require('./providers'); 7 | 8 | const CONFIG_FILE = path.join(os.homedir(), '.kimicc.json'); 9 | const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude', 'settings.json'); 10 | const CLAUDE_CONFIG_BACKUP_FILE = `${CLAUDE_CONFIG_FILE}.bak`; 11 | const CLAUDE_ENV_KEYS = Object.freeze([ 12 | 'ANTHROPIC_AUTH_TOKEN', 13 | 'ANTHROPIC_BASE_URL', 14 | 'ANTHROPIC_MODEL', 15 | 'ANTHROPIC_SMALL_FAST_MODEL' 16 | ]); 17 | 18 | function checkClaudeInPath() { 19 | try { 20 | execSync('which claude', { stdio: 'ignore' }); 21 | return true; 22 | } catch { 23 | return false; 24 | } 25 | } 26 | 27 | function installClaudeCode() { 28 | console.log('Claude Code not found. Installing @anthropic-ai/claude-code globally...'); 29 | try { 30 | execSync('npm install -g @anthropic-ai/claude-code', { stdio: 'inherit' }); 31 | console.log('Claude Code installed successfully!'); 32 | return true; 33 | } catch (error) { 34 | console.error('Failed to install Claude Code:', error.message); 35 | return false; 36 | } 37 | } 38 | 39 | function readConfig() { 40 | try { 41 | if (fs.existsSync(CONFIG_FILE)) { 42 | const content = fs.readFileSync(CONFIG_FILE, 'utf8'); 43 | return JSON.parse(content); 44 | } 45 | } catch (error) { 46 | console.error('Error reading config:', error.message); 47 | } 48 | return {}; 49 | } 50 | 51 | function writeConfig(config) { 52 | try { 53 | fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); 54 | } catch (error) { 55 | console.error('Error writing config:', error.message); 56 | } 57 | } 58 | 59 | function assertObject(value, errorMessage) { 60 | if (!value || typeof value !== 'object' || Array.isArray(value)) { 61 | throw new Error(errorMessage || 'Expected configuration to be an object'); 62 | } 63 | } 64 | 65 | function readClaudeConfig() { 66 | if (!fs.existsSync(CLAUDE_CONFIG_FILE)) { 67 | return { data: {}, exists: false }; 68 | } 69 | 70 | const raw = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf8'); 71 | if (!raw.trim()) { 72 | return { data: {}, exists: true }; 73 | } 74 | 75 | try { 76 | const parsed = JSON.parse(raw); 77 | assertObject(parsed, `Invalid structure in ${CLAUDE_CONFIG_FILE}`); 78 | return { data: parsed, exists: true }; 79 | } catch (error) { 80 | throw new Error(`Failed to parse ${CLAUDE_CONFIG_FILE}: ${error.message}`); 81 | } 82 | } 83 | 84 | function writeClaudeConfigFile(config) { 85 | assertObject(config, 'Claude configuration must be an object'); 86 | 87 | const dir = path.dirname(CLAUDE_CONFIG_FILE); 88 | if (!fs.existsSync(dir)) { 89 | fs.mkdirSync(dir, { recursive: true, mode: 0o755 }); 90 | } 91 | 92 | if (fs.existsSync(CLAUDE_CONFIG_FILE) && !fs.existsSync(CLAUDE_CONFIG_BACKUP_FILE)) { 93 | fs.copyFileSync(CLAUDE_CONFIG_FILE, CLAUDE_CONFIG_BACKUP_FILE); 94 | } 95 | 96 | const payload = JSON.stringify(config, null, 2) + '\n'; 97 | const tempFile = `${CLAUDE_CONFIG_FILE}.${process.pid}.${Date.now()}.tmp`; 98 | 99 | try { 100 | fs.writeFileSync(tempFile, payload, { mode: 0o600 }); 101 | fs.renameSync(tempFile, CLAUDE_CONFIG_FILE); 102 | fs.chmodSync(CLAUDE_CONFIG_FILE, 0o600); 103 | } finally { 104 | if (fs.existsSync(tempFile)) { 105 | try { 106 | fs.unlinkSync(tempFile); 107 | } catch { 108 | // ignore cleanup error 109 | } 110 | } 111 | } 112 | } 113 | 114 | function applyClaudeEnvUpdate({ set = {}, remove = [] } = {}) { 115 | const { data: existingConfig } = readClaudeConfig(); 116 | const envSnapshot = existingConfig.env && typeof existingConfig.env === 'object' && !Array.isArray(existingConfig.env) 117 | ? { ...existingConfig.env } 118 | : {}; 119 | 120 | const setKeys = Object.keys(set); 121 | const validKeys = new Set(CLAUDE_ENV_KEYS); 122 | const appliedKeys = []; 123 | const removedKeys = []; 124 | 125 | for (const key of setKeys) { 126 | if (!validKeys.has(key)) continue; 127 | const value = set[key]; 128 | if (value === undefined || value === null || value === '') { 129 | if (Object.prototype.hasOwnProperty.call(envSnapshot, key)) { 130 | delete envSnapshot[key]; 131 | removedKeys.push(key); 132 | } 133 | } else if (envSnapshot[key] !== value) { 134 | envSnapshot[key] = value; 135 | appliedKeys.push(key); 136 | } 137 | } 138 | 139 | for (const key of remove) { 140 | if (!validKeys.has(key)) continue; 141 | if (Object.prototype.hasOwnProperty.call(envSnapshot, key)) { 142 | delete envSnapshot[key]; 143 | if (!removedKeys.includes(key)) { 144 | removedKeys.push(key); 145 | } 146 | } 147 | } 148 | 149 | const changed = appliedKeys.length > 0 || removedKeys.length > 0; 150 | if (!changed) { 151 | return { changed: false, appliedKeys, removedKeys, config: existingConfig, env: envSnapshot }; 152 | } 153 | 154 | const nextConfig = { ...existingConfig, env: envSnapshot }; 155 | writeClaudeConfigFile(nextConfig); 156 | return { changed: true, appliedKeys, removedKeys, config: nextConfig, env: envSnapshot }; 157 | } 158 | 159 | function updateClaudeEnvFile(values) { 160 | return applyClaudeEnvUpdate({ set: values }); 161 | } 162 | 163 | function resetClaudeEnvFile(keys = CLAUDE_ENV_KEYS) { 164 | return applyClaudeEnvUpdate({ remove: keys }); 165 | } 166 | 167 | async function promptForAuthToken() { 168 | const rl = readline.createInterface({ 169 | input: process.stdin, 170 | output: process.stdout 171 | }); 172 | 173 | return new Promise((resolve) => { 174 | rl.question('Please enter your API Auth Token: ', (authToken) => { 175 | rl.close(); 176 | resolve(authToken.trim()); 177 | }); 178 | }); 179 | } 180 | 181 | function getProfileConfig(config, profileName) { 182 | if (!config.profiles || !config.profiles[profileName]) { 183 | return null; 184 | } 185 | return config.profiles[profileName]; 186 | } 187 | 188 | function getDefaultProfile(config) { 189 | return config.defaultProfile || null; 190 | } 191 | 192 | async function getAuthToken(profileName = null) { 193 | // Check environment variables first (highest priority) 194 | if (process.env.KIMI_AUTH_TOKEN) { 195 | return process.env.KIMI_AUTH_TOKEN; 196 | } 197 | 198 | 199 | // Check config file 200 | const config = readConfig(); 201 | 202 | // Ensure clean state - if we have profiles, we should not use legacy authToken 203 | const hasProfiles = config.profiles && Object.keys(config.profiles).length > 0; 204 | 205 | // If profile is specified, use profile config 206 | if (profileName) { 207 | const profile = getProfileConfig(config, profileName); 208 | if (profile && profile.key) { 209 | return profile.key; 210 | } 211 | console.error(`Profile '${profileName}' not found or missing auth token.`); 212 | return null; 213 | } 214 | 215 | // Check if a default profile is set 216 | if (hasProfiles) { 217 | const defaultProfile = getDefaultProfile(config); 218 | if (defaultProfile) { 219 | const profile = getProfileConfig(config, defaultProfile); 220 | if (profile && profile.key) { 221 | return profile.key; 222 | } 223 | } 224 | console.error('No default profile found and no legacy auth token.'); 225 | return null; 226 | } 227 | 228 | // Only use legacy authToken if no profiles exist 229 | if (config.authToken) { 230 | return config.authToken; 231 | } 232 | 233 | // Prompt for auth token 234 | console.log('No auth token found in environment variables or config file.'); 235 | const authToken = await promptForAuthToken(); 236 | 237 | if (authToken) { 238 | // Use legacy format only if no profiles exist 239 | const currentConfig = readConfig(); 240 | if (!currentConfig.profiles || Object.keys(currentConfig.profiles).length === 0) { 241 | writeConfig({ ...currentConfig, authToken }); 242 | console.log('Auth token saved to ~/.kimicc.json (legacy format)'); 243 | } else { 244 | // Profiles exist, create default profile instead 245 | migrateLegacyConfig(); 246 | const p = getProviderBySlug('kimi'); 247 | const model = p && p.defaultModel ? p.defaultModel : null; 248 | const smallFastModel = p && p.defaultSmallFastModel ? p.defaultSmallFastModel : model; 249 | const { addProfile } = require('./utils'); 250 | addProfile('default', 'https://api.moonshot.cn/anthropic', authToken, true, model, smallFastModel); 251 | console.log('Auth token saved as default profile'); 252 | } 253 | } 254 | 255 | return authToken; 256 | } 257 | 258 | function getBaseUrl(profileName = null) { 259 | // Check environment variables first 260 | if (process.env.ANTHROPIC_BASE_URL) { 261 | return process.env.ANTHROPIC_BASE_URL; 262 | } 263 | 264 | // Check config file 265 | const config = readConfig(); 266 | 267 | // If profile is specified, use profile config 268 | if (profileName) { 269 | const profile = getProfileConfig(config, profileName); 270 | if (profile && profile.url) { 271 | return profile.url; 272 | } 273 | return 'https://api.moonshot.cn/anthropic'; // fallback 274 | } 275 | 276 | // Check if a default profile is set 277 | const defaultProfile = getDefaultProfile(config); 278 | if (defaultProfile) { 279 | const profile = getProfileConfig(config, defaultProfile); 280 | if (profile && profile.url) { 281 | return profile.url; 282 | } 283 | } 284 | 285 | // Default Kimi endpoint 286 | return 'https://api.moonshot.cn/anthropic'; 287 | } 288 | 289 | function getModel(profileName = null) { 290 | // Prefer new Anthropic configuration variables introduced in Claude Code v0.6+ 291 | if (process.env.ANTHROPIC_DEFAULT_SONNET_MODEL) { 292 | return process.env.ANTHROPIC_DEFAULT_SONNET_MODEL; 293 | } 294 | if (process.env.ANTHROPIC_MODEL) { 295 | return process.env.ANTHROPIC_MODEL; 296 | } 297 | 298 | const config = readConfig(); 299 | 300 | if (profileName) { 301 | const profile = getProfileConfig(config, profileName); 302 | if (!profile) return null; 303 | if (profile.sonnetModel) return profile.sonnetModel; 304 | if (profile.model) return profile.model; 305 | return null; 306 | } 307 | 308 | const defaultProfile = getDefaultProfile(config); 309 | if (defaultProfile) { 310 | const profile = getProfileConfig(config, defaultProfile); 311 | if (profile) { 312 | if (profile.sonnetModel) return profile.sonnetModel; 313 | if (profile.model) return profile.model; 314 | } 315 | } 316 | 317 | return null; 318 | } 319 | 320 | function getSmallFastModel(profileName = null, options = {}) { 321 | const { fallbackToModel = true } = options; 322 | 323 | // Priority: new env vars > legacy env vars > profile overrides > model fallback 324 | if (process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL) { 325 | return process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL; 326 | } 327 | if (process.env.ANTHROPIC_SMALL_FAST_MODEL) { 328 | return process.env.ANTHROPIC_SMALL_FAST_MODEL; 329 | } 330 | 331 | const config = readConfig(); 332 | 333 | const resolveFromProfile = (profile) => { 334 | if (!profile) return null; 335 | if (profile.haikuModel) return profile.haikuModel; 336 | if (profile.smallFastModel) return profile.smallFastModel; 337 | if (fallbackToModel && profile.model) return profile.model; 338 | return null; 339 | }; 340 | 341 | if (profileName) { 342 | return resolveFromProfile(getProfileConfig(config, profileName)); 343 | } 344 | 345 | const defaultProfile = getDefaultProfile(config); 346 | if (defaultProfile) { 347 | return resolveFromProfile(getProfileConfig(config, defaultProfile)); 348 | } 349 | 350 | return null; 351 | } 352 | 353 | function getOpusModel(profileName = null) { 354 | if (process.env.ANTHROPIC_DEFAULT_OPUS_MODEL) { 355 | return process.env.ANTHROPIC_DEFAULT_OPUS_MODEL; 356 | } 357 | 358 | const config = readConfig(); 359 | 360 | const resolveFromProfile = (profile) => { 361 | if (!profile) return null; 362 | if (profile.opusModel) return profile.opusModel; 363 | if (profile.model) return profile.model; 364 | return null; 365 | }; 366 | 367 | if (profileName) { 368 | return resolveFromProfile(getProfileConfig(config, profileName)); 369 | } 370 | 371 | const defaultProfile = getDefaultProfile(config); 372 | if (defaultProfile) { 373 | return resolveFromProfile(getProfileConfig(config, defaultProfile)); 374 | } 375 | 376 | return null; 377 | } 378 | 379 | function updateClaudeSettings() { 380 | const claudeConfigFile = CLAUDE_CONFIG_FILE; 381 | 382 | try { 383 | let claudeConfig = {}; 384 | 385 | // Read existing config if it exists 386 | if (fs.existsSync(claudeConfigFile)) { 387 | const content = fs.readFileSync(claudeConfigFile, 'utf8'); 388 | claudeConfig = JSON.parse(content); 389 | } 390 | 391 | // Update required settings 392 | claudeConfig.autoUpdates = false; 393 | claudeConfig.hasCompletedOnboarding = true; 394 | 395 | const claudeDir = path.dirname(claudeConfigFile); 396 | if (!fs.existsSync(claudeDir)) { 397 | fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 }); 398 | } 399 | 400 | // Write back the updated config 401 | fs.writeFileSync(claudeConfigFile, JSON.stringify(claudeConfig, null, 2)); 402 | console.log('Claude settings updated successfully.'); 403 | 404 | } catch (error) { 405 | console.error('Warning: Could not update Claude settings:', error.message); 406 | // Don't fail the process, just warn 407 | } 408 | } 409 | 410 | 411 | function generateSlugFromUrl(url) { 412 | try { 413 | const urlObj = new URL(url); 414 | let hostname = urlObj.hostname; 415 | 416 | // Remove common prefixes including www, api, app, etc. 417 | hostname = hostname.replace(/^(www\.|api\.|app\.|dev\.|test\.|staging\.|beta\.|alpha\.)/, ''); 418 | 419 | // Replace dots with empty string and convert to lowercase 420 | return hostname.replace(/\./g, '').toLowerCase(); 421 | } catch (error) { 422 | return null; 423 | } 424 | } 425 | 426 | function listProfiles() { 427 | const config = readConfig(); 428 | if (!config.profiles || Object.keys(config.profiles).length === 0) { 429 | return []; 430 | } 431 | 432 | return Object.keys(config.profiles).map(slug => ({ 433 | slug, 434 | ...config.profiles[slug], 435 | isDefault: slug === config.defaultProfile 436 | })); 437 | } 438 | 439 | function migrateLegacyConfig() { 440 | const config = readConfig(); 441 | 442 | // Check if we have legacy config (authToken exists but no profiles) 443 | if (config.authToken && (!config.profiles || Object.keys(config.profiles).length === 0)) { 444 | console.log('🔧 Migrating legacy configuration to profile format...'); 445 | 446 | // Create profiles object if it doesn't exist 447 | if (!config.profiles) { 448 | config.profiles = {}; 449 | } 450 | 451 | // Create default profile with legacy values 452 | const defaultSlug = 'default'; 453 | const p = getProviderBySlug('kimi'); 454 | config.profiles[defaultSlug] = { 455 | url: 'https://api.moonshot.cn/anthropic', 456 | key: config.authToken, 457 | model: p && p.defaultModel ? p.defaultModel : undefined, 458 | smallFastModel: p && p.defaultSmallFastModel ? p.defaultSmallFastModel : (p && p.defaultModel ? p.defaultModel : undefined) 459 | }; 460 | 461 | // Set as default profile 462 | config.defaultProfile = defaultSlug; 463 | 464 | // Remove legacy authToken to ensure clean state 465 | delete config.authToken; 466 | 467 | writeConfig(config); 468 | console.log(`✅ Migrated legacy configuration to profile '${defaultSlug}'`); 469 | return true; 470 | } 471 | 472 | return false; 473 | } 474 | 475 | function ensureDefaultProfileKimiDefaults() { 476 | const config = readConfig(); 477 | if (!config.profiles) return false; 478 | const defaultProfileObj = config.profiles['default']; 479 | if (!defaultProfileObj) return false; 480 | 481 | const p = getProviderBySlug('kimi'); 482 | const kimiUrl = (p && p.baseUrl) || 'https://api.moonshot.cn/anthropic'; 483 | const kimiModel = p && p.defaultModel ? p.defaultModel : null; 484 | const kimiSmallFast = p && p.defaultSmallFastModel ? p.defaultSmallFastModel : kimiModel; 485 | 486 | let changed = false; 487 | if (!defaultProfileObj.url || defaultProfileObj.url !== kimiUrl) { 488 | defaultProfileObj.url = kimiUrl; 489 | changed = true; 490 | } 491 | if (!defaultProfileObj.model && kimiModel) { 492 | defaultProfileObj.model = kimiModel; 493 | changed = true; 494 | } 495 | if (!defaultProfileObj.smallFastModel && kimiSmallFast) { 496 | defaultProfileObj.smallFastModel = kimiSmallFast; 497 | changed = true; 498 | } 499 | 500 | if (changed) { 501 | // write back 502 | writeConfig(config); 503 | } 504 | return changed; 505 | } 506 | 507 | function addProfile(slug, url, authToken, setAsDefault = false, model = null, smallFastModel = null) { 508 | let config = readConfig(); 509 | 510 | // Check for legacy migration on first profile add 511 | if (!config.profiles || Object.keys(config.profiles).length === 0) { 512 | const migrated = migrateLegacyConfig(); 513 | if (migrated) { 514 | // Migration occurred, now add the new profile alongside the migrated one 515 | // Reload config after migration 516 | config = readConfig(); 517 | } 518 | } 519 | 520 | if (!config.profiles) { 521 | config.profiles = {}; 522 | } 523 | 524 | config.profiles[slug] = { 525 | url, 526 | key: authToken 527 | }; 528 | 529 | if (model) { 530 | config.profiles[slug].model = model; 531 | } 532 | // Only set smallFastModel if explicitly provided; otherwise leave unset 533 | if (smallFastModel) { 534 | config.profiles[slug].smallFastModel = smallFastModel; 535 | } 536 | 537 | if (setAsDefault || !config.defaultProfile) { 538 | config.defaultProfile = slug; 539 | } 540 | 541 | // Ensure clean state - remove legacy authToken if it exists 542 | if (config.authToken) { 543 | delete config.authToken; 544 | } 545 | 546 | writeConfig(config); 547 | return true; 548 | } 549 | 550 | function deleteProfile(slug) { 551 | const config = readConfig(); 552 | 553 | if (!config.profiles || !config.profiles[slug]) { 554 | return false; 555 | } 556 | 557 | delete config.profiles[slug]; 558 | 559 | // If this was the default profile, clear it 560 | if (config.defaultProfile === slug) { 561 | config.defaultProfile = null; 562 | 563 | // Set another profile as default if available 564 | const remainingProfiles = Object.keys(config.profiles); 565 | if (remainingProfiles.length > 0) { 566 | config.defaultProfile = remainingProfiles[0]; 567 | } 568 | } 569 | 570 | writeConfig(config); 571 | return true; 572 | } 573 | 574 | function setDefaultProfile(slug) { 575 | const config = readConfig(); 576 | 577 | if (!config.profiles || !config.profiles[slug]) { 578 | return false; 579 | } 580 | 581 | config.defaultProfile = slug; 582 | writeConfig(config); 583 | return true; 584 | } 585 | 586 | module.exports = { 587 | checkClaudeInPath, 588 | installClaudeCode, 589 | getAuthToken, 590 | getBaseUrl, 591 | getModel, 592 | getSmallFastModel, 593 | getOpusModel, 594 | updateClaudeSettings, 595 | getProfileConfig, 596 | getDefaultProfile, 597 | generateSlugFromUrl, 598 | listProfiles, 599 | addProfile, 600 | deleteProfile, 601 | setDefaultProfile, 602 | readConfig, 603 | writeConfig, 604 | ensureDefaultProfileKimiDefaults, 605 | updateClaudeEnvFile, 606 | resetClaudeEnvFile, 607 | readClaudeConfig, 608 | CLAUDE_ENV_KEYS 609 | }; 610 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require('child_process'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const os = require('os'); 7 | const readline = require('readline'); 8 | const { checkClaudeInPath, installClaudeCode, getAuthToken, getBaseUrl, getModel, getSmallFastModel, getOpusModel, updateClaudeSettings, readConfig, getProfileConfig, addProfile, ensureDefaultProfileKimiDefaults, updateClaudeEnvFile, resetClaudeEnvFile, CLAUDE_ENV_KEYS } = require('../lib/utils'); 9 | const { parseProviderFlag, getProviderBySlug } = require('../lib/providers'); 10 | const { version } = require('../package.json'); 11 | 12 | const CONFIG_FILE = path.join(os.homedir(), '.kimicc.json'); 13 | const CLAUDE_ENV_FILE = path.join(os.homedir(), '.claude', 'settings.json'); 14 | 15 | function promptForTokenForProvider(providerName) { 16 | const rl = readline.createInterface({ 17 | input: process.stdin, 18 | output: process.stdout 19 | }); 20 | const name = providerName || 'provider'; 21 | return new Promise((resolve) => { 22 | rl.question(`Enter API Auth Token for ${name}: `, (authToken) => { 23 | rl.close(); 24 | resolve((authToken || '').trim()); 25 | }); 26 | }); 27 | } 28 | 29 | async function handleResetCommand() { 30 | console.log('🗑️ Resetting kimicc configuration...\n'); 31 | 32 | if (!fs.existsSync(CONFIG_FILE)) { 33 | console.log('No configuration file found at ~/.kimicc.json'); 34 | return; 35 | } 36 | 37 | const rl = readline.createInterface({ 38 | input: process.stdin, 39 | output: process.stdout 40 | }); 41 | 42 | return new Promise((resolve) => { 43 | rl.question('Are you sure you want to delete the configuration file? (y/N): ', (answer) => { 44 | rl.close(); 45 | 46 | if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { 47 | try { 48 | fs.unlinkSync(CONFIG_FILE); 49 | console.log('✅ Configuration file deleted successfully.'); 50 | } catch (error) { 51 | console.error('❌ Failed to delete configuration file:', error.message); 52 | } 53 | } else { 54 | console.log('Reset cancelled.'); 55 | } 56 | resolve(); 57 | }); 58 | }); 59 | } 60 | 61 | async function handleInjectCommand() { 62 | const args = process.argv.slice(2); 63 | const injectArgs = args.slice(1); 64 | 65 | let reset = false; 66 | let profileName = null; 67 | let baseUrlOverride = null; 68 | let modelOverride = null; 69 | let smallFastOverride = null; 70 | let legacyForceFlag = false; 71 | const unexpectedArgs = []; 72 | 73 | for (let i = 0; i < injectArgs.length; i++) { 74 | const token = injectArgs[i]; 75 | if (!token) continue; 76 | 77 | if (token === '--reset' || token === '-r') { 78 | reset = true; 79 | continue; 80 | } 81 | if (token === '--profile') { 82 | if (i + 1 >= injectArgs.length) { 83 | console.error('❌ Missing value for --profile'); 84 | process.exit(1); 85 | } 86 | profileName = injectArgs[++i]; 87 | continue; 88 | } 89 | if (token.startsWith('--profile=')) { 90 | profileName = token.slice('--profile='.length); 91 | continue; 92 | } 93 | if (token === '--base-url') { 94 | if (i + 1 >= injectArgs.length) { 95 | console.error('❌ Missing value for --base-url'); 96 | process.exit(1); 97 | } 98 | baseUrlOverride = injectArgs[++i]; 99 | continue; 100 | } 101 | if (token.startsWith('--base-url=')) { 102 | baseUrlOverride = token.split('=').slice(1).join('='); 103 | continue; 104 | } 105 | if (typeof token === 'string') { 106 | const baseUrlMatch = token.match(/^base_url=(.+)$/i); 107 | if (baseUrlMatch) { 108 | baseUrlOverride = baseUrlMatch[1]; 109 | continue; 110 | } 111 | } 112 | if (token === '--model') { 113 | if (i + 1 >= injectArgs.length) { 114 | console.error('❌ Missing value for --model'); 115 | process.exit(1); 116 | } 117 | modelOverride = injectArgs[++i]; 118 | continue; 119 | } 120 | if (token.startsWith('--model=')) { 121 | modelOverride = token.split('=').slice(1).join('='); 122 | continue; 123 | } 124 | if (token === '--small-fast-model') { 125 | if (i + 1 >= injectArgs.length) { 126 | console.error('❌ Missing value for --small-fast-model'); 127 | process.exit(1); 128 | } 129 | smallFastOverride = injectArgs[++i]; 130 | continue; 131 | } 132 | if (token.startsWith('--small-fast-model=')) { 133 | smallFastOverride = token.split('=').slice(1).join('='); 134 | continue; 135 | } 136 | if (token === '--force' || token === '-f') { 137 | legacyForceFlag = true; 138 | continue; 139 | } 140 | if (token === '--help' || token === '-h') { 141 | console.log('Usage: kimicc inject [--profile SLUG] [--base-url URL|base_url=URL] [--model NAME] [--small-fast-model NAME] [--reset]'); 142 | return; 143 | } 144 | unexpectedArgs.push(token); 145 | } 146 | 147 | if (unexpectedArgs.length > 0) { 148 | console.error(`❌ Unknown arguments for inject command: ${unexpectedArgs.join(', ')}`); 149 | process.exit(1); 150 | } 151 | 152 | if (legacyForceFlag) { 153 | console.log('ℹ️ --force is deprecated for inject and will be ignored.'); 154 | } 155 | 156 | if (reset) { 157 | if (profileName || baseUrlOverride || modelOverride || smallFastOverride) { 158 | console.log('ℹ️ --reset ignores profile/model/base-url overrides. Managed keys are global.'); 159 | } 160 | console.log('🧹 Removing managed Claude environment variables...\n'); 161 | console.log(`Managed keys: ${CLAUDE_ENV_KEYS.join(', ')}`); 162 | try { 163 | const result = resetClaudeEnvFile(); 164 | if (result.removedKeys.length > 0) { 165 | console.log(`✅ Removed ${result.removedKeys.join(', ')} from ${CLAUDE_ENV_FILE}.`); 166 | } else { 167 | console.log(`ℹ️ No managed keys found in ${CLAUDE_ENV_FILE}.`); 168 | } 169 | } catch (error) { 170 | console.error(`❌ Failed to reset managed keys: ${error.message}`); 171 | process.exit(1); 172 | } 173 | return; 174 | } 175 | 176 | const config = readConfig(); 177 | const selectedProfile = profileName ? getProfileConfig(config, profileName) : null; 178 | if (profileName && !selectedProfile) { 179 | console.error(`❌ Profile '${profileName}' not found.`); 180 | process.exit(1); 181 | } 182 | 183 | const authToken = await getAuthToken(profileName); 184 | if (!authToken) { 185 | console.error('❌ No auth token provided. Cannot inject environment variables.'); 186 | return; 187 | } 188 | 189 | const baseUrl = baseUrlOverride || getBaseUrl(profileName); 190 | if (profileName && baseUrlOverride && selectedProfile && selectedProfile.url && selectedProfile.url !== baseUrlOverride) { 191 | console.log(`⚠️ Using --base-url over profile "${profileName}" url "${selectedProfile.url}".`); 192 | } 193 | 194 | const model = modelOverride || getModel(profileName); 195 | const smallFastModel = smallFastOverride || getSmallFastModel(profileName, { fallbackToModel: false }); 196 | 197 | console.log('💉 Injecting managed environment variables into ~/.claude/settings.json...\n'); 198 | console.log(`Managed keys: ${CLAUDE_ENV_KEYS.join(', ')}`); 199 | 200 | const envUpdates = { 201 | ANTHROPIC_AUTH_TOKEN: authToken, 202 | ANTHROPIC_BASE_URL: baseUrl, 203 | ANTHROPIC_MODEL: model || undefined, 204 | ANTHROPIC_SMALL_FAST_MODEL: smallFastModel || undefined 205 | }; 206 | 207 | try { 208 | const result = updateClaudeEnvFile(envUpdates); 209 | if (result.changed) { 210 | const changeParts = []; 211 | if (result.appliedKeys.length > 0) { 212 | changeParts.push(`set ${result.appliedKeys.join(', ')}`); 213 | } 214 | if (result.removedKeys.length > 0) { 215 | changeParts.push(`removed ${result.removedKeys.join(', ')}`); 216 | } 217 | console.log(`✅ Updated ${CLAUDE_ENV_FILE}${changeParts.length ? ` (${changeParts.join('; ')})` : ''}.`); 218 | } else { 219 | console.log(`ℹ️ ${CLAUDE_ENV_FILE} already up to date.`); 220 | } 221 | } catch (error) { 222 | console.error(`❌ Failed to update ${CLAUDE_ENV_FILE}: ${error.message}`); 223 | process.exit(1); 224 | } 225 | } 226 | 227 | async function handleProfileCommand() { 228 | const args = process.argv.slice(2); 229 | const profileArgs = args.slice(1); // Skip the 'profile' command itself 230 | // Detect provider quick flags and base_url/--base-url override 231 | let providerFlag = parseProviderFlag(profileArgs); 232 | let providerSlug = providerFlag ? providerFlag.slug : null; 233 | if (providerFlag) { 234 | profileArgs.splice(providerFlag.index, 1); 235 | } 236 | // Parse base_url=... or --base-url URL 237 | let overrideBaseUrl = null; 238 | for (let i = 0; i < profileArgs.length; i++) { 239 | const tok = profileArgs[i]; 240 | const m = typeof tok === 'string' ? tok.match(/^base_url=(.+)$/i) : null; 241 | if (m) { 242 | overrideBaseUrl = m[1]; 243 | profileArgs.splice(i, 1); 244 | break; 245 | } 246 | } 247 | const baseUrlFlagIndex = profileArgs.findIndex(a => a === '--base-url'); 248 | if (baseUrlFlagIndex !== -1) { 249 | if (baseUrlFlagIndex + 1 < profileArgs.length) { 250 | overrideBaseUrl = profileArgs[baseUrlFlagIndex + 1]; 251 | profileArgs.splice(baseUrlFlagIndex, 2); 252 | } else { 253 | console.error('❌ Missing value for --base-url'); 254 | process.exit(1); 255 | } 256 | } 257 | 258 | if (profileArgs.length === 0 || profileArgs[0] === 'list') { 259 | // profile list 260 | const profiles = require('../lib/utils').listProfiles(); 261 | 262 | if (profiles.length === 0) { 263 | console.log('📋 No profiles found.'); 264 | console.log('💡 Use "kimicc profile add --slug example https://api.example.com YOUR_AUTH_TOKEN" to add a profile.'); 265 | return; 266 | } 267 | 268 | console.log('📋 Available profiles:\n'); 269 | profiles.forEach(profile => { 270 | const marker = profile.isDefault ? ' (default)' : ''; 271 | console.log(` ${profile.slug}${marker}`); 272 | console.log(` URL: ${profile.url}`); 273 | console.log(` Key: ${profile.key.substring(0, 8)}...`); 274 | if (profile.model) console.log(` Model: ${profile.model}`); 275 | if (profile.smallFastModel) console.log(` SmallFastModel: ${profile.smallFastModel}`); 276 | console.log(); 277 | }); 278 | return; 279 | } 280 | 281 | if (profileArgs[0] === 'add') { 282 | // profile add [--slug slug] [--model model] [--small-fast-model model] [--default] url AUTH_TOKEN 283 | let slug = null; 284 | let url = null; 285 | let authToken = null; 286 | let model = null; 287 | let smallFastModel = null; 288 | let setAsDefault = false; 289 | 290 | // Collect positional args after options 291 | const remaining = []; 292 | for (let i = 1; i < profileArgs.length; i++) { 293 | const v = profileArgs[i]; 294 | if (v === '--slug' && i + 1 < profileArgs.length) { 295 | slug = profileArgs[++i]; 296 | } else if (v === '--model' && i + 1 < profileArgs.length) { 297 | model = profileArgs[++i]; 298 | } else if (v && typeof v === 'string' && v.startsWith('--model=')) { 299 | model = v.split('=')[1]; 300 | } else if (v === '--small-fast-model' && i + 1 < profileArgs.length) { 301 | smallFastModel = profileArgs[++i]; 302 | } else if (v && typeof v === 'string' && v.startsWith('--small-fast-model=')) { 303 | smallFastModel = v.split('=')[1]; 304 | } else if (v === '--default') { 305 | setAsDefault = true; 306 | } else { 307 | remaining.push(v); 308 | } 309 | } 310 | 311 | // If provider flag present and overrideBaseUrl not set, set defaults from provider 312 | if (providerSlug) { 313 | const p = getProviderBySlug(providerSlug); 314 | if (!overrideBaseUrl && p) overrideBaseUrl = p.baseUrl; 315 | // Default slug if not provided 316 | if (!slug) slug = providerSlug; 317 | // Default model if not provided 318 | if (!model && p && p.defaultModel) model = p.defaultModel; 319 | } 320 | 321 | // Derive url/authToken from remaining 322 | if (remaining.length >= 2) { 323 | url = remaining[0]; 324 | authToken = remaining[1]; 325 | } else if (remaining.length === 1) { 326 | // If provider specified, treat single arg as auth token 327 | if (overrideBaseUrl) { 328 | url = overrideBaseUrl; 329 | authToken = remaining[0]; 330 | } else { 331 | // Ambiguous: treat as URL and prompt later (but we require token), so fail 332 | url = remaining[0]; 333 | } 334 | } 335 | 336 | // If no URL but we have overrideBaseUrl, use it 337 | if (!url && overrideBaseUrl) url = overrideBaseUrl; 338 | 339 | if (!url || !authToken) { 340 | console.error('❌ Missing required arguments: URL and auth token'); 341 | console.log('💡 Usage: kimicc profile add [--slug SLUG] [--model MODEL] [--small-fast-model MODEL] [--default] URL AUTH_TOKEN'); 342 | console.log(' Or: kimicc profile add --kimi|--deepseek|--glm|--qwen [--base-url URL|base_url=URL] AUTH_TOKEN'); 343 | process.exit(1); 344 | } 345 | 346 | // Validate URL 347 | try { 348 | new URL(url); 349 | } catch (error) { 350 | console.error('❌ Invalid URL provided'); 351 | process.exit(1); 352 | } 353 | 354 | // Generate slug if not provided 355 | if (!slug) { 356 | slug = require('../lib/utils').generateSlugFromUrl(url); 357 | if (!slug) { 358 | console.error('❌ Could not generate slug from URL. Please provide --slug manually.'); 359 | process.exit(1); 360 | } 361 | } 362 | 363 | // Check if slug already exists and prompt for confirmation 364 | const { readConfig } = require('../lib/utils'); 365 | const config = readConfig(); 366 | if (config.profiles && config.profiles[slug]) { 367 | console.log(`⚠️ Profile '${slug}' already exists.`); 368 | console.log(` Existing: URL=${config.profiles[slug].url}, Key=${config.profiles[slug].key.substring(0, 8)}...`); 369 | 370 | const rl = readline.createInterface({ 371 | input: process.stdin, 372 | output: process.stdout 373 | }); 374 | 375 | return new Promise((resolve) => { 376 | rl.question(`Do you want to overwrite profile '${slug}'? (y/N): `, async (answer) => { 377 | rl.close(); 378 | 379 | if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { 380 | const { addProfile } = require('../lib/utils'); 381 | addProfile(slug, url, authToken, setAsDefault, model, smallFastModel); 382 | 383 | console.log(`✅ Profile '${slug}' updated successfully.`); 384 | if (setAsDefault) { 385 | console.log(` Set as default profile.`); 386 | } 387 | if (model) { 388 | console.log(` Model: ${model}`); 389 | } 390 | if (smallFastModel) { 391 | console.log(` SmallFastModel: ${smallFastModel}`); 392 | } 393 | } else { 394 | console.log('Profile addition cancelled.'); 395 | } 396 | resolve(); 397 | }); 398 | }); 399 | } 400 | 401 | const { addProfile } = require('../lib/utils'); 402 | addProfile(slug, url, authToken, setAsDefault, model, smallFastModel); 403 | 404 | console.log(`✅ Profile '${slug}' added successfully.`); 405 | if (setAsDefault) { 406 | console.log(` Set as default profile.`); 407 | } 408 | if (model) { 409 | console.log(` Model: ${model}`); 410 | } 411 | if (smallFastModel) { 412 | console.log(` SmallFastModel: ${smallFastModel}`); 413 | } 414 | return; 415 | } 416 | 417 | if (profileArgs[0] === 'del' || profileArgs[0] === 'delete' || profileArgs[0] === 'remove') { 418 | // Check for -i flag 419 | const hasInteractive = profileArgs.includes('-i') || profileArgs.includes('--interactive'); 420 | 421 | if (hasInteractive) { 422 | // Interactive deletion mode 423 | const { listProfiles, deleteProfile } = require('../lib/utils'); 424 | const profiles = listProfiles(); 425 | 426 | if (profiles.length === 0) { 427 | console.log('📋 No profiles found to delete.'); 428 | return; 429 | } 430 | 431 | console.log('🗑️ Interactive Profile Deletion\n'); 432 | console.log('📋 Available profiles:\n'); 433 | profiles.forEach((profile, index) => { 434 | const marker = profile.isDefault ? ' (default)' : ''; 435 | console.log(` ${index + 1}. ${profile.slug}${marker}`); 436 | console.log(` URL: ${profile.url}`); 437 | console.log(` Key: ${profile.key.substring(0, 8)}...`); 438 | console.log(); 439 | }); 440 | 441 | const rl = readline.createInterface({ 442 | input: process.stdin, 443 | output: process.stdout 444 | }); 445 | 446 | return new Promise((resolve) => { 447 | rl.question('Enter profile numbers to delete (comma-separated, e.g., 1,3): ', async (answer) => { 448 | rl.close(); 449 | 450 | const indices = answer.split(',') 451 | .map(s => parseInt(s.trim()) - 1) 452 | .filter(i => !isNaN(i) && i >= 0 && i < profiles.length); 453 | 454 | if (indices.length === 0) { 455 | console.log('No valid profile numbers provided. Deletion cancelled.'); 456 | resolve(); 457 | return; 458 | } 459 | 460 | console.log(`\n📋 Selected profiles to delete: ${indices.map(i => profiles[i].slug).join(', ')}`); 461 | 462 | const confirmRl = readline.createInterface({ 463 | input: process.stdin, 464 | output: process.stdout 465 | }); 466 | 467 | confirmRl.question('Are you sure you want to delete these profiles? (y/N): ', async (confirmAnswer) => { 468 | confirmRl.close(); 469 | 470 | if (confirmAnswer.toLowerCase() === 'y' || confirmAnswer.toLowerCase() === 'yes') { 471 | let deletedCount = 0; 472 | 473 | // Delete profiles in reverse order to maintain indices 474 | for (let i = indices.length - 1; i >= 0; i--) { 475 | const slug = profiles[indices[i]].slug; 476 | const success = deleteProfile(slug); 477 | if (success) { 478 | console.log(`✅ Profile '${slug}' deleted successfully.`); 479 | deletedCount++; 480 | } else { 481 | console.log(`❌ Failed to delete profile '${slug}'.`); 482 | } 483 | } 484 | 485 | console.log(`\n🎉 Deleted ${deletedCount} profile(s).`); 486 | } else { 487 | console.log('Deletion cancelled.'); 488 | } 489 | resolve(); 490 | }); 491 | }); 492 | }); 493 | } else { 494 | // Original deletion mode with specific slug 495 | const slug = profileArgs[1]; 496 | 497 | if (!slug) { 498 | console.error('❌ Missing profile slug'); 499 | console.log('💡 Usage: kimicc profile del SLUG'); 500 | console.log(' kimicc profile del -i # Interactive deletion'); 501 | process.exit(1); 502 | } 503 | 504 | const { readConfig, deleteProfile } = require('../lib/utils'); 505 | const config = readConfig(); 506 | 507 | if (!config.profiles || !config.profiles[slug]) { 508 | console.error(`❌ Profile '${slug}' not found.`); 509 | process.exit(1); 510 | } 511 | 512 | // Confirmation prompt 513 | const rl = readline.createInterface({ 514 | input: process.stdin, 515 | output: process.stdout 516 | }); 517 | 518 | return new Promise((resolve) => { 519 | rl.question(`Are you sure you want to delete profile '${slug}'? (y/N): `, async (answer) => { 520 | rl.close(); 521 | 522 | if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { 523 | const success = deleteProfile(slug); 524 | if (success) { 525 | console.log(`✅ Profile '${slug}' deleted successfully.`); 526 | } else { 527 | console.log(`❌ Failed to delete profile '${slug}'.`); 528 | } 529 | } else { 530 | console.log('Deletion cancelled.'); 531 | } 532 | resolve(); 533 | }); 534 | }); 535 | } 536 | } 537 | 538 | if (profileArgs[0] === 'set-default') { 539 | // profile set-default slug 540 | const slug = profileArgs[1]; 541 | 542 | if (!slug) { 543 | console.error('❌ Missing profile slug'); 544 | console.log('💡 Usage: kimicc profile set-default SLUG'); 545 | process.exit(1); 546 | } 547 | 548 | const { readConfig, setDefaultProfile } = require('../lib/utils'); 549 | const config = readConfig(); 550 | 551 | if (!config.profiles || !config.profiles[slug]) { 552 | console.error(`❌ Profile '${slug}' not found.`); 553 | process.exit(1); 554 | } 555 | 556 | setDefaultProfile(slug); 557 | console.log(`✅ Set '${slug}' as default profile.`); 558 | return; 559 | } 560 | 561 | console.error('❌ Unknown profile command'); 562 | console.log('💡 Available profile commands:'); 563 | console.log(' kimicc profile list # List all profiles'); 564 | console.log(' kimicc profile add [--slug SLUG] [--model MODEL] [--small-fast-model MODEL] [--default] URL AUTH_TOKEN'); 565 | console.log(' kimicc profile add --kimi|--deepseek|--glm|--qwen [--base-url URL|base_url=URL] AUTH_TOKEN'); 566 | console.log(' kimicc profile del SLUG # Delete a profile'); 567 | console.log(' kimicc profile del -i # Interactive deletion'); 568 | console.log(' kimicc profile set-default SLUG # Set default profile'); 569 | } 570 | 571 | async function main() { 572 | // Get command line arguments (remove 'node' and script path) 573 | const args = process.argv.slice(2); 574 | // Ensure default profile behaves like --kimi (URL + models set) 575 | try { ensureDefaultProfileKimiDefaults(); } catch {} 576 | 577 | // Handle subcommands 578 | if (args[0] === 'reset') { 579 | await handleResetCommand(); 580 | return; 581 | } 582 | 583 | if (args[0] === 'inject') { 584 | await handleInjectCommand(); 585 | return; 586 | } 587 | 588 | if (args[0] === 'profile') { 589 | await handleProfileCommand(); 590 | return; 591 | } 592 | 593 | // Parse provider quick flags and base_url overrides 594 | let provider = parseProviderFlag(args); 595 | let selectedProviderSlug = provider ? provider.slug : null; 596 | if (provider) { 597 | // Remove provider flag from args passed through 598 | args.splice(provider.index, 1); 599 | } 600 | // Parse base_url=... or --base-url URL on root command 601 | let explicitBaseUrl = null; 602 | let explicitModel = null; 603 | let explicitSmallFastModel = null; 604 | for (let i = 0; i < args.length; i++) { 605 | const tok = args[i]; 606 | const m = typeof tok === 'string' ? tok.match(/^base_url=(.+)$/i) : null; 607 | if (m) { 608 | explicitBaseUrl = m[1]; 609 | args.splice(i, 1); 610 | break; 611 | } 612 | } 613 | const baseUrlIdx = args.findIndex(a => a === '--base-url'); 614 | if (baseUrlIdx !== -1) { 615 | if (baseUrlIdx + 1 < args.length) { 616 | explicitBaseUrl = args[baseUrlIdx + 1]; 617 | args.splice(baseUrlIdx, 2); 618 | } else { 619 | console.error('❌ Missing value for --base-url'); 620 | process.exit(1); 621 | } 622 | } 623 | 624 | // Parse model overrides on root command (support --flag value and --flag=value) 625 | // --model value 626 | let modelIdx = args.findIndex(a => a === '--model'); 627 | if (modelIdx !== -1) { 628 | if (modelIdx + 1 < args.length) { 629 | explicitModel = args[modelIdx + 1]; 630 | args.splice(modelIdx, 2); 631 | } else { 632 | console.error('❌ Missing value for --model'); 633 | process.exit(1); 634 | } 635 | } else { 636 | // --model=value 637 | const modelEqIdx = args.findIndex(a => typeof a === 'string' && a.startsWith('--model=')); 638 | if (modelEqIdx !== -1) { 639 | explicitModel = args[modelEqIdx].split('=')[1]; 640 | args.splice(modelEqIdx, 1); 641 | } 642 | } 643 | // --small-fast-model value 644 | let smallFastIdx = args.findIndex(a => a === '--small-fast-model'); 645 | if (smallFastIdx !== -1) { 646 | if (smallFastIdx + 1 < args.length) { 647 | explicitSmallFastModel = args[smallFastIdx + 1]; 648 | args.splice(smallFastIdx, 2); 649 | } else { 650 | console.error('❌ Missing value for --small-fast-model'); 651 | process.exit(1); 652 | } 653 | } else { 654 | // --small-fast-model=value 655 | const smallFastEqIdx = args.findIndex(a => typeof a === 'string' && a.startsWith('--small-fast-model=')); 656 | if (smallFastEqIdx !== -1) { 657 | explicitSmallFastModel = args[smallFastEqIdx].split('=')[1]; 658 | args.splice(smallFastEqIdx, 1); 659 | } 660 | } 661 | 662 | // Parse profile argument from main command 663 | let profileName = null; 664 | const profileIndex = args.findIndex(arg => arg === '--profile' || arg === '-p'); 665 | if (profileIndex !== -1 && profileIndex + 1 < args.length) { 666 | profileName = args[profileIndex + 1]; 667 | // Remove --profile and profile name from args to pass to claude 668 | args.splice(profileIndex, 2); 669 | } 670 | 671 | // If provider flag and profile both present, provider takes precedence 672 | if (selectedProviderSlug && profileName) { 673 | console.log(`⚠️ Both --profile and provider flag specified; using provider '--${selectedProviderSlug}'.`); 674 | profileName = null; 675 | } 676 | 677 | // If a provider flag is used, prefer existing same-name profile; otherwise initialize one 678 | if (selectedProviderSlug) { 679 | const config = readConfig(); 680 | const existing = config.profiles && config.profiles[selectedProviderSlug]; 681 | if (existing) { 682 | profileName = selectedProviderSlug; 683 | } else { 684 | const p = getProviderBySlug(selectedProviderSlug); 685 | const urlToUse = explicitBaseUrl || (p && p.baseUrl) || 'https://api.moonshot.cn/anthropic'; 686 | try { 687 | new URL(urlToUse); 688 | } catch (e) { 689 | console.error('❌ Invalid base URL. Provide a valid URL via --base-url or base_url=.'); 690 | process.exit(1); 691 | } 692 | const token = await promptForTokenForProvider(p ? p.name : selectedProviderSlug); 693 | if (!token) { 694 | console.error('❌ No auth token provided. Exiting...'); 695 | process.exit(1); 696 | } 697 | const modelToUse = p && p.defaultModel ? p.defaultModel : null; 698 | const smallFastToUse = p && p.defaultSmallFastModel ? p.defaultSmallFastModel : null; 699 | addProfile(selectedProviderSlug, urlToUse, token, false, modelToUse, smallFastToUse); 700 | console.log(`✅ Profile '${selectedProviderSlug}' created.`); 701 | profileName = selectedProviderSlug; 702 | } 703 | } 704 | 705 | console.log(`🚀 Starting kimicc v${version} - Claude Code with Kimi K2...\n`); 706 | 707 | // Check if claude is installed 708 | if (!checkClaudeInPath()) { 709 | const installed = installClaudeCode(); 710 | if (!installed) { 711 | console.error('Failed to install Claude Code. Please install it manually.'); 712 | process.exit(1); 713 | } 714 | } 715 | 716 | // Update Claude settings 717 | updateClaudeSettings(); 718 | 719 | // Get auth token, base URL, and model based on profile or provider 720 | const authToken = await getAuthToken(profileName); 721 | if (!authToken) { 722 | console.error('No auth token provided. Exiting...'); 723 | process.exit(1); 724 | } 725 | 726 | // Base URL resolution priority: 727 | // 1) explicit --base-url/base_url 728 | // 2) profile URL when a profile is selected (including provider-created) 729 | // 3) provider default URL when provider flag used but no profile selected 730 | // 4) default behavior in utils 731 | let baseUrl = null; 732 | if (explicitBaseUrl) { 733 | baseUrl = explicitBaseUrl; 734 | } else if (profileName) { 735 | baseUrl = getBaseUrl(profileName); 736 | } else if (selectedProviderSlug) { 737 | const p = getProviderBySlug(selectedProviderSlug); 738 | baseUrl = (p && p.baseUrl) ? p.baseUrl : null; 739 | } 740 | if (!baseUrl) { 741 | baseUrl = getBaseUrl(profileName); 742 | } 743 | const sonnetFromEnvOrProfile = getModel(profileName); 744 | const haikuFromEnvOrProfile = getSmallFastModel(profileName); 745 | const opusFromEnvOrProfile = getOpusModel(profileName); 746 | 747 | const env = { 748 | ...process.env, 749 | ANTHROPIC_BASE_URL: baseUrl, 750 | }; 751 | 752 | // Always use auth token mode 753 | env.ANTHROPIC_AUTH_TOKEN = authToken; 754 | 755 | // Determine models from CLI > ENV > Profile and map to new Anthropic defaults 756 | let resolvedSonnetModel = explicitModel || sonnetFromEnvOrProfile; 757 | let resolvedHaikuModel = explicitSmallFastModel || haikuFromEnvOrProfile; 758 | let resolvedOpusModel = opusFromEnvOrProfile; 759 | 760 | if (!resolvedHaikuModel && resolvedSonnetModel) { 761 | resolvedHaikuModel = resolvedSonnetModel; 762 | } 763 | 764 | if (!resolvedOpusModel) { 765 | resolvedOpusModel = resolvedSonnetModel || resolvedHaikuModel || null; 766 | } 767 | 768 | if (resolvedSonnetModel) { 769 | env.ANTHROPIC_DEFAULT_SONNET_MODEL = resolvedSonnetModel; 770 | env.ANTHROPIC_MODEL = explicitModel || env.ANTHROPIC_MODEL || resolvedSonnetModel; 771 | } 772 | 773 | if (resolvedHaikuModel) { 774 | env.ANTHROPIC_DEFAULT_HAIKU_MODEL = resolvedHaikuModel; 775 | } 776 | 777 | if (resolvedOpusModel) { 778 | env.ANTHROPIC_DEFAULT_OPUS_MODEL = resolvedOpusModel; 779 | } 780 | 781 | // Display profile info if using one 782 | if (profileName) { 783 | console.log(`📋 Using profile: ${profileName}`); 784 | } else if (selectedProviderSlug) { 785 | console.log(`📋 Using provider: ${selectedProviderSlug}`); 786 | } 787 | 788 | // Spawn claude process 789 | console.log('Launching Claude Code...\n'); 790 | const claude = spawn('claude', args, { 791 | env, 792 | stdio: 'inherit', 793 | shell: true 794 | }); 795 | 796 | // Handle process exit 797 | claude.on('close', (code) => { 798 | process.exit(code); 799 | }); 800 | 801 | claude.on('error', (err) => { 802 | console.error('Failed to start Claude Code:', err.message); 803 | process.exit(1); 804 | }); 805 | } 806 | 807 | // Run main function 808 | main().catch((error) => { 809 | console.error('Error:', error.message); 810 | process.exit(1); 811 | }); 812 | --------------------------------------------------------------------------------