easyai 1.7.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,75 +1,215 @@
1
1
  # EasyAI
2
2
 
3
- 简化 AI 命令行工具使用的 Ruby 包装器,支持 Claude、Gemini 和 OpenAI
3
+ Claude / Gemini / Codex 三家 AI CLI 的本地化统一入口。通过单一 `~/.easyai/config.json` 管理凭证、代理与多平台切换;运行时把环境变量仅注入子进程,不污染当前 shell
4
+
5
+ - 单机本地配置,零远程依赖
6
+ - 同一工具下保存多个平台,运行时一键切换
7
+ - 凭证只入子进程 ENV,不写入 shell rc
8
+ - 内置通用文件加解密工具 `utils encry / decry`
4
9
 
5
10
  ## 安装
6
11
 
7
12
  ```bash
8
- gem install easyai
13
+ gem install easyai # Ruby >= 3.0
14
+ easyai setup # 首次运行:交互式生成本地配置
9
15
  ```
10
16
 
11
- 或从源码安装:
17
+ 各家 AI CLI 由使用方预先安装:
18
+
19
+ | 工具 | 安装命令 |
20
+ |------|----------|
21
+ | Claude | `npm install -g @anthropic-ai/claude-code` |
22
+ | Gemini | `npm install -g @google/gemini-cli` |
23
+ | Codex | 参考 OpenAI Codex 官方文档 |
24
+
25
+ ## 快速开始
26
+
12
27
  ```bash
13
- gem build easyai.gemspec
14
- gem install easyai-*.gem
28
+ easyai setup # 1. 选择工具 → 选择平台 → 填写凭证
29
+ easyai claude # 2. 启动;多平台时交互选择
30
+ easyai gemini --platform=google_official
31
+ easyai codex
15
32
  ```
16
33
 
17
- ## 快速开始
34
+ ## 命令参考
35
+
36
+ ### 启动 AI 工具
18
37
 
19
- ### Claude
20
38
  ```bash
21
- easyai claude # 启动 Claude CLI
22
- easyai claude ./config.json # 使用本地配置文件
39
+ easyai claude # 列出 claude 平台并交互选择
40
+ easyai claude --platform=kimi # 显式指定平台
41
+ easyai claude ./adhoc.json # 一次性 JSON 覆盖(位置参数,单平台扁平 schema)
42
+ easyai claude --verbose # 详细信息
43
+
44
+ easyai gemini # 同上
45
+ easyai codex # 替代 v1.x 的 easyai gpt
46
+ ```
47
+
48
+ > 一次性 JSON 是**位置参数**(不是 `--config=` 选项);该模式下 `--platform` 被忽略,未识别的位置参数透传给目标 CLI。
49
+
50
+ 单平台扁平 schema(`adhoc.json` 用):
51
+
52
+ ```json
53
+ {
54
+ "env": { "ANTHROPIC_AUTH_TOKEN": "sk-..." },
55
+ "proxy": { "HTTP_PROXY": "http://127.0.0.1:7890" }
56
+ }
23
57
  ```
24
58
 
25
- ### Gemini
59
+ ### 配置管理
60
+
26
61
  ```bash
27
- easyai gemini # 启动 Gemini CLI
28
- easyai gemini chat # 开始聊天会话
62
+ easyai setup # 首次:全量交互;已存在:进入 upsert 选择菜单
63
+ easyai setup --tool=claude # 仅配置某个工具
64
+ easyai setup --add=kimi --tool=claude # 追加或覆盖单平台
65
+ easyai setup --remove=kimi --tool=claude # 删除单平台(platforms 删空时自动清理 tool 键)
66
+ easyai setup --list # 脱敏概览
67
+ easyai setup --edit # 用 $EDITOR / $VISUAL 直接编辑 config.json
68
+ easyai setup --reset # 删除现有 config.json 后重走全量交互
29
69
  ```
30
70
 
31
- ### OpenAI GPT
71
+ > 敏感字段输入时 `IO#noecho` 不回显;`--list` 输出对 key 名包含 `TOKEN` / `KEY` / `SECRET` / `PASSWORD` 的字段做"前 4 + 后 4 + 长度"脱敏。
72
+
73
+ ### 清理 AI CLI 缓存
74
+
32
75
  ```bash
33
- easyai gpt # 启动 OpenAI CLI
34
- easyai gpt api chat.completions # 调用 API
76
+ easyai clean # 默认清 claude
77
+ easyai clean codex # 单工具
78
+ easyai clean all # 三家一起清
79
+ easyai clean all --dry-run # 仅预览将删除的路径
80
+ easyai clean all --force # 跳过交互确认
35
81
  ```
36
82
 
37
- ## 实用工具
83
+ 清理目标(不含 `~/.easyai/config.json`,重置 EasyAI 自身配置请用 `easyai setup --reset`):
84
+
85
+ | 工具 | 路径 |
86
+ |------|------|
87
+ | claude | `~/.claude`、`~/.claude.json`、`~/.config/claude` |
88
+ | gemini | `~/.gemini`、`~/.config/gemini` |
89
+ | codex | `~/.codex`、`~/.config/codex` |
90
+
91
+ ### 自更新
38
92
 
39
- ### 文件加密解密
40
93
  ```bash
41
- easyai utils encry file.txt # 加密文件
42
- easyai utils decry file.encrypted # 解密文件
94
+ easyai update # 调用 gem update easyai
95
+ easyai update --verbose # 显示 gem 命令完整输出
43
96
  ```
44
97
 
45
- ### 配置清理
98
+ `update` 子命令本身会被启动版本检查跳过,避免"过期 → 强制更新 → 更新命令本身被阻塞"的死循环。
99
+
100
+ ### 通用加解密工具
101
+
46
102
  ```bash
47
- easyai clean # 清理 Claude 配置
48
- easyai clean all # 清理所有配置
103
+ easyai utils encry <path> # AES-128-ECB 加密单文件或目录
104
+ easyai utils decry <path> # 与 encry 配对解密
105
+ ```
106
+
107
+ 与 EasyAI 的配置体系彼此独立,可作为通用工具使用。
108
+
109
+ ## 配置 schema
110
+
111
+ `~/.easyai/config.json`(v2.0):
112
+
113
+ ```json
114
+ {
115
+ "version": "2.0.0",
116
+ "claude": {
117
+ "platforms": {
118
+ "claude_official": {
119
+ "env": {
120
+ "ANTHROPIC_AUTH_TOKEN": "sk-ant-...",
121
+ "ANTHROPIC_BASE_URL": "https://api.anthropic.com"
122
+ },
123
+ "proxy": {
124
+ "HTTP_PROXY": "http://127.0.0.1:7890",
125
+ "HTTPS_PROXY": "http://127.0.0.1:7890"
126
+ }
127
+ },
128
+ "kimi": {
129
+ "env": {
130
+ "ANTHROPIC_AUTH_TOKEN": "sk-...",
131
+ "ANTHROPIC_BASE_URL": "https://api.moonshot.cn/anthropic"
132
+ }
133
+ }
134
+ }
135
+ },
136
+ "gemini": {
137
+ "platforms": {
138
+ "google_official": { "env": { "GEMINI_API_KEY": "AIza..." } }
139
+ }
140
+ },
141
+ "codex": {
142
+ "platforms": {
143
+ "openai_official": { "env": { "OPENAI_API_KEY": "sk-..." } }
144
+ }
145
+ }
146
+ }
49
147
  ```
50
148
 
51
- ## 系统要求
149
+ 要点:
150
+
151
+ - `platforms` 字典即真相:runtime 不维护硬编码白名单,可手编 `config.json` 添加任何自定义平台名(仅 `setup` 内置 `KNOWN_PLATFORMS` 用于交互引导)
152
+ - `env` 透传不限制,可塞任意 KV(如 `ANTHROPIC_LOG=debug`)
153
+ - `proxy` 可省略;同时设大小写 `HTTP_PROXY` / `http_proxy`
154
+ - `PATH` / `HOME` / `USER` / `SHELL` 不会被 config 覆盖
155
+ - `save` 后自动 `chmod 600`(非 Windows)
156
+
157
+ 详见 [docs/设计文档.md](docs/设计文档.md) 第 3 章。
52
158
 
53
- - Ruby >= 2.7.0
54
- - 对应的 AI CLI 工具:
55
- - Claude: `npm install -g @anthropic-ai/claude-code`
56
- - Gemini: `npm install -g @google/gemini-cli`
57
- - OpenAI: `pip install openai`
159
+ ## 调试与环境变量
58
160
 
59
- ## 特性
161
+ | 变量 | 作用 |
162
+ |------|------|
163
+ | `EASYAI_DEBUG=1` | 启用调试日志 |
164
+ | `EASYAI_VERBOSE=1` | 等价于 `--verbose` |
165
+ | `EASYAI_SKIP_FORCE_UPDATE=1` | 跳过启动时的强制版本检查(网络不通时使用) |
60
166
 
61
- - 🔐 环境隔离,不影响系统配置
62
- - 🌐 支持远程配置自动下载
63
- - 🔄 透传所有参数到原生 CLI
64
- - 🛠 包含实用工具集
167
+ ## v1.x 升级
168
+
169
+ v2.0 是破坏性升级:
170
+
171
+ - `easyai gpt` 重命名为 `easyai codex`(子进程命令也是 `codex`)
172
+ - 配置位置:`~/.easyai/config.yml` → `~/.easyai/config.json`,**不会自动迁移**,需手动 `easyai setup` 重建(保留旧 yml 不会冲突)
173
+ - 删除:远程加密配置仓库、JPS 登录、`easyai utils export`、`easyai setup --branch / --list-users`、Claude 官方认证环境检查、系统钥匙串密码缓存
174
+ - 运行时依赖 `webrick` / `jpsclient` 已删除
175
+ - Ruby 最低版本:`>= 3.0.0`
176
+
177
+ 升级步骤:
178
+
179
+ ```bash
180
+ gem update easyai
181
+ easyai setup # 重新交互配置
182
+ ```
183
+
184
+ 需保留 v1.x:`gem install easyai -v 1.7.0`。
65
185
 
66
186
  ## 开发
67
187
 
68
188
  ```bash
69
- ./test_local.sh # 本地测试
70
- ./release_remote.sh # 发布到 RubyGems
189
+ bundle install # 装开发依赖
190
+ bundle exec rspec # 运行测试套件(发布前必跑)
191
+ ./script/install_local.sh # 本地构建并安装(冒烟)
71
192
  ```
72
193
 
194
+ 新增功能遵循 TDD:先在 `spec/` 下补测试 → 实现 → 全绿后提交。
195
+
196
+ 发布脚本:
197
+
198
+ | 脚本 | 用途 |
199
+ |------|------|
200
+ | `script/local_release.sh` | 本地完整发布:提交 → 合并到 master → 打 tag → `gem push` |
201
+ | `script/autoci_release.sh` | 仅本地提交 / 打 tag / 推送,由 Gitee CI 完成构建与推送 |
202
+
203
+ ## 文档索引
204
+
205
+ | 文档 | 说明 |
206
+ |------|------|
207
+ | [docs/需求文档.md](docs/需求文档.md) | 功能与非功能需求(v2.0) |
208
+ | [docs/设计文档.md](docs/设计文档.md) | 架构、模块与关键流程(v2.0) |
209
+ | [docs/plan.md](docs/plan.md) | v2.0 重构实施计划 |
210
+ | [CLAUDE.md](CLAUDE.md) | 给 Claude Code 的工程约定 |
211
+ | [AGENTS.md](AGENTS.md) | 团队协作与提交规范 |
212
+
73
213
  ## License
74
214
 
75
- MIT
215
+ MIT
data/easyai.gemspec CHANGED
@@ -6,23 +6,26 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ['Wade']
7
7
  spec.email = ['wade@example.com']
8
8
 
9
- spec.summary = 'Easy AI CLI tool wrapper'
10
- spec.description = 'A CLI tool that simplifies running AI tools like Claude, Gemini, and GPT with environment configuration'
9
+ spec.summary = 'Claude / Gemini / Codex 三家 AI CLI 的本地化统一入口'
10
+ spec.description = <<~DESC
11
+ EasyAI 是一个 Ruby CLI 包装器,为 Anthropic Claude、Google Gemini、OpenAI Codex 三家命令行工具提供统一入口。
12
+ v2.0 起聚焦单机本地化:无远程下发链路,通过单一 ~/.easyai/config.json 集中管理多家 CLI 与多平台凭证、代理;
13
+ 运行时仅向子进程注入环境变量,不污染当前 shell。
14
+ DESC
11
15
  spec.homepage = 'https://github.com/wade/easyai'
12
16
  spec.license = 'MIT'
13
-
14
- spec.required_ruby_version = '>= 2.7.0'
15
-
17
+
18
+ spec.required_ruby_version = '>= 3.0.0'
19
+
16
20
  spec.files = Dir['lib/**/*', 'bin/*', '*.md', '*.gemspec']
17
21
  spec.bindir = 'bin'
18
22
  spec.executables = ['easyai']
19
23
  spec.require_paths = ['lib']
20
-
24
+
21
25
  spec.add_dependency 'claide', '~> 1.0'
22
26
  spec.add_dependency 'colored2', '~> 3.1'
23
- spec.add_dependency 'webrick', '~> 1.9'
24
- spec.add_runtime_dependency 'jpsclient', '~> 1.4', '>= 1.4.0'
25
-
27
+ spec.add_dependency 'base64', '~> 0.2'
28
+
26
29
  spec.add_development_dependency 'bundler', '~> 2.0'
27
30
  spec.add_development_dependency 'rake', '~> 13.0'
28
31
  spec.add_development_dependency 'rspec', '~> 3.0'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyAI
4
+ module Base
5
+ # 敏感字段脱敏工具。
6
+ #
7
+ # 凡是 key 名包含 TOKEN / KEY / SECRET / PASSWORD(大小写不敏感)的环境变量,
8
+ # 在向 stdout 打印时都应通过本模块脱敏,避免 API Key 明文出现在终端历史 / 截图 / 日志。
9
+ #
10
+ # 脱敏规则:
11
+ # - 长度 ≤ 8:完全隐藏,仅保留长度
12
+ # - 长度 > 8:前 4 + "..." + 后 4 + 长度
13
+ module SecretMasker
14
+ SENSITIVE_KEY_PATTERN = /(TOKEN|KEY|SECRET|PASSWORD)/i.freeze
15
+
16
+ module_function
17
+
18
+ def sensitive_key?(key)
19
+ SENSITIVE_KEY_PATTERN.match?(key.to_s)
20
+ end
21
+
22
+ def mask(value)
23
+ s = value.to_s
24
+ len = s.length
25
+ return "*** (len=#{len})" if len <= 8
26
+
27
+ "#{s[0, 4]}...#{s[-4, 4]} (len=#{len})"
28
+ end
29
+
30
+ # 按 key 是否敏感,决定原样返回还是脱敏返回
31
+ def format_value(key, value)
32
+ return value.to_s unless value.is_a?(String)
33
+ return value unless sensitive_key?(key)
34
+
35
+ mask(value)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,11 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rbconfig'
2
4
  require 'etc'
3
5
 
4
6
  module EasyAI
5
7
  module Base
8
+ # 跨平台 OS 判定与命令检测。
9
+ #
10
+ # v2.0 起仅保留单机本地化运行所需能力:
11
+ # - 平台判定:macos? / windows? / linux?
12
+ # - PATH 检测:which_command(cmd)
13
+ #
14
+ # v1.x 中用于远程模式的方法(claude_auth_check_details / region_info /
15
+ # is_us_region? / is_english_environment? / shell_config_files 等)已删除。
6
16
  class SystemInfo
7
17
  class << self
8
- # === 平台检测 ===
18
+ # 返回平台符号::macos / :windows / :linux / :bsd / :unknown
9
19
  def platform
10
20
  @platform ||= detect_platform
11
21
  end
@@ -22,75 +32,7 @@ module EasyAI
22
32
  platform == :linux
23
33
  end
24
34
 
25
- # === 用户和环境 ===
26
- def current_user
27
- @current_user ||= Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
28
- end
29
-
30
- def current_shell
31
- @current_shell ||= ENV['SHELL']&.split('/')&.last || 'bash'
32
- end
33
-
34
- def shell_config_files
35
- @shell_config_files ||= detect_shell_config_files
36
- end
37
-
38
- # === 地区和语言检测(仅在需要时调用)===
39
- def region_info
40
- @region_info ||= detect_region_info
41
- end
42
-
43
- def is_us_region?
44
- region_info[:is_us]
45
- end
46
-
47
- # 检查是否为英语环境
48
- def is_english_environment?
49
- info = region_info
50
-
51
- case platform
52
- when :macos
53
- # macOS: 检查 locale 和首选语言
54
- locale_english = info[:locale].to_s.start_with?('en_') || info[:locale].to_s.start_with?('en.')
55
- apple_locale_english = info[:apple_locale].to_s.start_with?('en_')
56
- first_lang_english = info[:apple_languages].to_s.start_with?('en')
57
-
58
- # 任一为英语即可
59
- locale_english || apple_locale_english || first_lang_english
60
- when :windows
61
- # Windows: 检查系统和用户地区
62
- system_english = info[:system_locale].to_s.start_with?('en-')
63
- user_english = info[:user_locale].to_s.start_with?('en-')
64
- locale_english = info[:locale].to_s.start_with?('en_') || info[:locale].to_s.start_with?('en.')
65
-
66
- # 任一为英语即可
67
- system_english || user_english || locale_english
68
- else
69
- # Linux/其他: 检查 LANG 环境变量
70
- locale = info[:locale].to_s
71
- locale.start_with?('en_') || locale.start_with?('en.')
72
- end
73
- end
74
-
75
- # 检查是否满足 claude_auth 的环境要求
76
- def meets_claude_auth_requirements?
77
- !windows? && is_us_region? && is_english_environment?
78
- end
79
-
80
- # 获取 claude_auth 环境检查详情
81
- def claude_auth_check_details
82
- {
83
- os_supported: !windows?,
84
- os_name: platform.to_s.capitalize,
85
- is_us_region: is_us_region?,
86
- region: region_info[:region],
87
- is_english: is_english_environment?,
88
- locale: region_info[:locale],
89
- meets_requirements: meets_claude_auth_requirements?
90
- }
91
- end
92
-
93
- # === 命令检测 ===
35
+ # 检测命令是否存在于 PATH。返回 true / false
94
36
  def which_command(cmd)
95
37
  if windows?
96
38
  system("where #{cmd} >nul 2>&1")
@@ -113,143 +55,15 @@ module EasyAI
113
55
  when /bsd/i
114
56
  :bsd
115
57
  else
116
- # 回退到 RUBY_PLATFORM
117
58
  case RUBY_PLATFORM
118
- when /darwin/
119
- :macos
120
- when /mswin|mingw|cygwin/
121
- :windows
122
- when /linux/
123
- :linux
124
- else
125
- :unknown
126
- end
127
- end
128
- end
129
-
130
- def detect_shell_config_files
131
- files = []
132
-
133
- case current_shell
134
- when 'zsh'
135
- files << File.expand_path("~/.zshrc")
136
- when 'bash'
137
- bash_profile = File.expand_path("~/.bash_profile")
138
- bashrc = File.expand_path("~/.bashrc")
139
- files << (File.exist?(bash_profile) ? bash_profile : bashrc)
140
- when 'fish'
141
- fish_config = File.expand_path("~/.config/fish/config.fish")
142
- files << fish_config
143
-
144
- # 确保 fish 配置目录存在
145
- fish_dir = File.dirname(fish_config)
146
- unless File.exist?(fish_dir)
147
- require 'fileutils'
148
- FileUtils.mkdir_p(fish_dir)
59
+ when /darwin/ then :macos
60
+ when /mswin|mingw|cygwin/ then :windows
61
+ when /linux/ then :linux
62
+ else :unknown
149
63
  end
150
- else
151
- files << File.expand_path("~/.profile")
152
- end
153
-
154
- # 总是添加 ~/.profile 作为备份
155
- profile_file = File.expand_path("~/.profile")
156
- files << profile_file unless files.include?(profile_file)
157
-
158
- files
159
- end
160
-
161
- def detect_region_info
162
- case platform
163
- when :macos
164
- detect_macos_region
165
- when :windows
166
- detect_windows_region
167
- when :linux
168
- detect_linux_region
169
- else
170
- detect_generic_region
171
64
  end
172
65
  end
173
-
174
- def detect_macos_region
175
- locale = ENV['LANG'] || ENV['LC_ALL'] || ''
176
- apple_locale = `defaults read -g AppleLocale 2>/dev/null`.chomp
177
- apple_languages = `defaults read -g AppleLanguages 2>/dev/null`.chomp
178
-
179
- # 解析语言列表
180
- first_language = apple_languages.match(/"([^"]+)"/)&.captures&.first || ''
181
-
182
- # 判断是否为美国地区
183
- is_us = apple_locale.start_with?('en_US') ||
184
- locale.start_with?('en_US') ||
185
- first_language.start_with?('en-US')
186
-
187
- {
188
- locale: locale,
189
- apple_locale: apple_locale,
190
- apple_languages: first_language,
191
- region: apple_locale.split('_').last || 'Unknown',
192
- is_us: is_us,
193
- platform: 'macOS'
194
- }
195
- end
196
-
197
- def detect_windows_region
198
- locale = ENV['LANG'] || ''
199
-
200
- # Windows PowerShell 命令获取地区
201
- system_locale = `powershell -Command "Get-WinSystemLocale | Select-Object -ExpandProperty Name" 2>nul`.chomp
202
- user_locale = `powershell -Command "Get-Culture | Select-Object -ExpandProperty Name" 2>nul`.chomp
203
-
204
- # 判断是否为美国地区
205
- is_us = system_locale.start_with?('en-US') ||
206
- user_locale.start_with?('en-US') ||
207
- locale.start_with?('en_US')
208
-
209
- {
210
- locale: locale,
211
- system_locale: system_locale,
212
- user_locale: user_locale,
213
- region: (system_locale.split('-').last || user_locale.split('-').last || 'Unknown'),
214
- is_us: is_us,
215
- platform: 'Windows'
216
- }
217
- end
218
-
219
- def detect_linux_region
220
- locale = ENV['LANG'] || ENV['LC_ALL'] || ''
221
-
222
- # 尝试读取 /etc/locale.conf
223
- locale_conf = File.exist?('/etc/locale.conf') ? File.read('/etc/locale.conf').strip : ''
224
-
225
- # 使用 localectl 命令
226
- system_locale = `localectl status 2>/dev/null | grep "System Locale" | cut -d= -f2`.chomp
227
-
228
- # 判断是否为美国地区
229
- is_us = locale.start_with?('en_US') ||
230
- system_locale.start_with?('en_US') ||
231
- locale_conf.include?('en_US')
232
-
233
- {
234
- locale: locale,
235
- system_locale: system_locale || locale_conf,
236
- region: locale.split('_').last&.split('.').first || 'Unknown',
237
- is_us: is_us,
238
- platform: 'Linux'
239
- }
240
- end
241
-
242
- def detect_generic_region
243
- locale = ENV['LANG'] || ENV['LC_ALL'] || 'Unknown'
244
-
245
- {
246
- locale: locale,
247
- region: locale.split('_').last&.split('.').first || 'Unknown',
248
- is_us: locale.start_with?('en_US'),
249
- platform: 'Unknown'
250
- }
251
- end
252
66
  end
253
67
  end
254
68
  end
255
- end
69
+ end