easyai 2.0.0 → 2.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0e9e5abc64041d1ca3e0a9fb59a81aa3dcdd16edfdfa716069fa17a71df1f4b
4
- data.tar.gz: b729e5453a287e1e7fb6908fba42bd4516e48800eafff811d4b0fea80bb83653
3
+ metadata.gz: 1913c617c3cd8a792e90964f704dd92c2ca3795925aa4826065e9235ba6c732a
4
+ data.tar.gz: db3b5e247223d08364d4788b499e43cfb838866c22de215c97a3c44a864ba60f
5
5
  SHA512:
6
- metadata.gz: f5fa95bca000bf57323f11026e5392b90fa71d22f606e77ddf49e87420afe1b128ef8313e3c51e6940bec15a177df3bc1ba7b81de532613c23cd394556a1c2f4
7
- data.tar.gz: a4b24f06ec5dfa6ea7ed8a7476619eb580f4b606151e22a0921ad7a6760775e8ee5202e7832d7bfca2a8c470664e57daa0be0ac1944ca55d3b82e502a0c779de
6
+ metadata.gz: 30f3c3c3dbc3fbbc916893737ba9ee15b922679c2cab0b36ac377d5527a576e355712fc4c5e1a00981c1d04a4af970cd483dc11c13ae2960c574343d3b9d6bf7
7
+ data.tar.gz: 98c6112929ddf724f2dc1e6642d9b5055968f7ff05d6b987b6dd6cc60ae6b02ef9c636ba4c90640f232122e47ef1e391ac1c0be0245b828caf4ccdce5547c4be
data/CLAUDE.md CHANGED
@@ -272,3 +272,13 @@ CLAide::Command
272
272
  - 输入用 `IO#noecho` 不回显
273
273
  - `--list` 输出做"前 4 + 后 4 + 长度"脱敏
274
274
  - 不得把 `~/.easyai/config.json` 上传到代码仓库或日志服务
275
+
276
+ ## Ruby Rules
277
+
278
+ 编写 Ruby 代码时参考以下规范:
279
+ - 通用编码风格:`.claude/rules/ecc/common/coding-style.md`
280
+ - Ruby 专项规范:`.claude/rules/ecc/ruby/` 目录(coding-style / testing / security / patterns / hooks)
281
+
282
+ ## 提交规范
283
+
284
+ 提交代码时参考 `.claude/rules/git-flow/git-flow.md` 中的规范。
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyAI
4
+ module Base
5
+ # 极简 TOML "段" 处理工具:只按 table 头切块,不解析具体值。
6
+ #
7
+ # 适用范围:codex ~/.codex/config.toml 这类结构简单的文件
8
+ # (顶层 key/value + 若干 [table] / [a.b] 段,无多行数组 / 复杂内联表)。
9
+ #
10
+ # 提供能力:
11
+ # - split_sections 把文本切成有序的段(每段含 header + 原始行)
12
+ # - reject_projects 过滤掉所有 [projects.*] 段,返回新文本
13
+ # - merge 段级合并:override 覆盖 base 同名段,base 独有段保留
14
+ module TomlSections
15
+ module_function
16
+
17
+ PROJECT_HEADER_PREFIX = '[projects.'
18
+
19
+ Section = Struct.new(:header, :body, keyword_init: true)
20
+
21
+ # 判断一行是否是 table 头:去掉前导空白后以 '[' 开头(含 '[[' 数组表)
22
+ def header_line?(line)
23
+ line.lstrip.start_with?('[')
24
+ end
25
+
26
+ # 归一化 header(整行去空白),用于同名段比对
27
+ def header_key(line)
28
+ line.strip
29
+ end
30
+
31
+ def project_header?(header)
32
+ return false if header.nil?
33
+
34
+ header.start_with?(PROJECT_HEADER_PREFIX)
35
+ end
36
+
37
+ # 切段:返回 [Section, ...]。文件开头无 table 头的部分作为 header=nil 的前导段。
38
+ def split_sections(text)
39
+ sections = []
40
+ current = Section.new(header: nil, body: +'')
41
+
42
+ text.to_s.each_line do |line|
43
+ if header_line?(line)
44
+ sections << current unless empty_preamble?(current)
45
+ current = Section.new(header: header_key(line), body: +line.dup)
46
+ else
47
+ current.body << line
48
+ end
49
+ end
50
+ sections << current unless empty_preamble?(current)
51
+ sections
52
+ end
53
+
54
+ def reject_projects(text)
55
+ kept = split_sections(text).reject { |s| project_header?(s.header) }
56
+ render(kept)
57
+ end
58
+
59
+ # 段级合并,override 优先:
60
+ # - override 出现的 header 覆盖 base 同名段
61
+ # - base 独有 header(含 [projects.*])保留,追加在 override 具名段之后
62
+ # - 前导段(header=nil):override 非空用 override,否则用 base
63
+ def merge(base_text, override_text)
64
+ base = split_sections(base_text)
65
+ override = split_sections(override_text)
66
+ override_keys = override.map(&:header).compact
67
+
68
+ result = []
69
+
70
+ over_pre = override.find { |s| s.header.nil? }
71
+ base_pre = base.find { |s| s.header.nil? }
72
+ preamble = over_pre && !over_pre.body.strip.empty? ? over_pre : base_pre
73
+ result << preamble if preamble
74
+
75
+ override.each { |s| result << s unless s.header.nil? }
76
+ base.each do |s|
77
+ next if s.header.nil?
78
+ next if override_keys.include?(s.header)
79
+
80
+ result << s
81
+ end
82
+
83
+ render(result)
84
+ end
85
+
86
+ # ----- private -----
87
+
88
+ def render(sections)
89
+ sections.map { |s| ensure_trailing_newline(s.body) }.join
90
+ end
91
+
92
+ def ensure_trailing_newline(body)
93
+ return body if body.empty? || body.end_with?("\n")
94
+
95
+ "#{body}\n"
96
+ end
97
+
98
+ def empty_preamble?(section)
99
+ section.header.nil? && section.body.empty?
100
+ end
101
+
102
+ private_class_method :render, :ensure_trailing_newline, :empty_preamble?
103
+ end
104
+ end
105
+ end
@@ -48,11 +48,12 @@ module EasyAI
48
48
 
49
49
  def run
50
50
  cfg = resolve_config
51
+ @resolved_config = cfg # 供子类 pre_exec 读取当前生效的平台配置
51
52
  print_platform_env(cfg)
52
- env = build_environment(cfg)
53
- export_environment_variables(env)
53
+ @env = build_environment(cfg) # 存 ivar:pre_exec 可按需追加(如 codex 注入 CODEX_HOME)
54
+ export_environment_variables(@env)
54
55
  pre_exec
55
- exec(env, exec_command, *@passthrough_args)
56
+ exec(@env, exec_command, *@passthrough_args)
56
57
  rescue Config::LocalConfig::NotFoundError => e
57
58
  print_error(e.message)
58
59
  puts " 请运行: #{'easyai setup'.yellow}"
@@ -95,12 +96,19 @@ module EasyAI
95
96
  {}
96
97
  end
97
98
 
98
- # 子类可选重写:在 exec 启动子进程之前执行的钩子(如:写入子进程依赖的状态文件)。
99
+ # 子类可选重写:在 exec 启动子进程之前执行的钩子(如:写入子进程依赖的状态文件、
100
+ # 追加只对本次子进程生效的环境变量到 @env,如 codex 注入 CODEX_HOME)。
99
101
  # 默认 no-op;不应阻塞主流程,异常应降级为 warning。
100
102
  def pre_exec
101
103
  # no-op
102
104
  end
103
105
 
106
+ # 子类可选重写:向平台选择列表注入的虚拟平台 { key => data }(如 codex 的 ChatGPT Subscribe)。
107
+ # 默认空;与 config.json 真实平台合并参与交互选择。
108
+ def extra_platforms
109
+ {}
110
+ end
111
+
104
112
  private
105
113
 
106
114
  # 从 remainder 中识别第一个以 .json 结尾且文件存在的位置参数作为本地覆盖入口
@@ -119,7 +127,9 @@ module EasyAI
119
127
  print_success("使用本地覆盖配置 #{@local_config_file}(工具:#{tool_name})")
120
128
  cfg
121
129
  else
122
- Config::LocalConfig.resolve_platform(tool: tool_name, platform: @platform, verbose: @verbose) do |name|
130
+ Config::LocalConfig.resolve_platform(
131
+ tool: tool_name, platform: @platform, verbose: @verbose, extra: extra_platforms
132
+ ) do |name|
123
133
  print_success("使用平台 #{name}(工具:#{tool_name})")
124
134
  end
125
135
  end
@@ -27,20 +27,23 @@ module EasyAI
27
27
  $ easyai backup claude
28
28
  DESC
29
29
 
30
- SOURCE_PATH = File.expand_path('~/.claude.json').freeze
31
- BACKUP_DIR = File.expand_path('~/.easyai/backup').freeze
32
- BACKUP_PATH = File.join(BACKUP_DIR, '.claude.json').freeze
33
30
  EXCLUDED_KEYS = %w[projects].freeze
34
31
  KEYCHAIN_LABEL = 'Claude Code-credentials'.freeze
35
32
  KEYCHAIN_BACKUP_KEY = '_easyai_keychain'.freeze
36
33
 
34
+ # 注意:路径用方法动态 expand_path,避免常量在加载期冻结真实 HOME,
35
+ # 让 spec 切换 ENV['HOME'] 后定位失败(同 LocalConfig.config_path 的处理)
36
+ def source_path = File.expand_path('~/.claude.json')
37
+ def backup_dir = File.expand_path('~/.easyai/backup')
38
+ def backup_path = File.join(backup_dir, '.claude.json')
39
+
37
40
  def validate!
38
41
  super
39
- help! "源文件不存在: #{SOURCE_PATH}\n请先登录 Claude Code 后再备份。" unless File.exist?(SOURCE_PATH)
42
+ help! "源文件不存在: #{source_path}\n请先登录 Claude Code 后再备份。" unless File.exist?(source_path)
40
43
  end
41
44
 
42
45
  def run
43
- source = read_json(SOURCE_PATH)
46
+ source = read_json(source_path)
44
47
  EXCLUDED_KEYS.each { |key| source.delete(key) }
45
48
 
46
49
  keychain_value = read_keychain_credential(KEYCHAIN_LABEL)
@@ -48,18 +51,31 @@ module EasyAI
48
51
  source[KEYCHAIN_BACKUP_KEY] = { KEYCHAIN_LABEL => keychain_value }
49
52
  end
50
53
 
51
- target = File.exist?(BACKUP_PATH) ? read_json(BACKUP_PATH) : {}
54
+ target = File.exist?(backup_path) ? read_json(backup_path) : {}
52
55
  before_keys = target.keys
53
56
 
54
57
  merged = deep_merge(target, source)
55
58
 
56
- FileUtils.mkdir_p(BACKUP_DIR)
57
- File.write(BACKUP_PATH, JSON.pretty_generate(merged))
58
- File.chmod(0o600, BACKUP_PATH) unless windows?
59
+ FileUtils.mkdir_p(backup_dir)
60
+ File.write(backup_path, JSON.pretty_generate(merged))
61
+ File.chmod(0o600, backup_path) unless windows?
59
62
 
60
63
  report(before_keys, source.keys, merged.keys, keychain_value)
61
64
  end
62
65
 
66
+ # 供 easyai claude 运行前调用:仅当备份不存在且源文件存在时自动备份一次。
67
+ # 返回是否执行了备份;任何异常降级为 warning,绝不阻塞启动。
68
+ def backup_if_absent
69
+ return false if File.exist?(backup_path)
70
+ return false unless File.exist?(source_path)
71
+
72
+ run
73
+ true
74
+ rescue StandardError => e
75
+ warn " ⚠ 自动备份 Claude 登录信息失败(#{e.message}),继续启动"
76
+ false
77
+ end
78
+
63
79
  private
64
80
 
65
81
  def read_json(path)
@@ -100,8 +116,8 @@ module EasyAI
100
116
  kept = before_keys - source_keys
101
117
 
102
118
  puts "✓ Claude 登录信息已备份"
103
- puts " 源文件: #{SOURCE_PATH}"
104
- puts " 备份路径: #{BACKUP_PATH}"
119
+ puts " 源文件: #{source_path}"
120
+ puts " 备份路径: #{backup_path}"
105
121
  puts " 排除字段: #{EXCLUDED_KEYS.join(', ')}"
106
122
  puts " 字段更新: 新增 #{added.size},更新 #{updated.size},保留目标独有 #{kept.size}(合计 #{after_keys.size})"
107
123
  if keychain_value
@@ -0,0 +1,206 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+ require_relative '../../base/toml_sections'
4
+ require_relative '../../base/system_info'
5
+
6
+ module EasyAI
7
+ class Command
8
+ class Backup
9
+ class Codex < Backup
10
+ self.summary = '备份 Codex 登录信息'
11
+ self.description = <<-DESC
12
+ 把 ~/.codex/auth.json(登录凭证)+ ~/.codex/config.toml(去除 [projects.*] 授信段)
13
+ 合并存进单个文件 ~/.easyai/backup/.codex.json,便于跨机器迁移或灾难恢复。
14
+
15
+ 特点:
16
+
17
+ * 单文件存储:auth 与 config.toml 文本一并放进 .codex.json 的 auth / config_toml 字段
18
+
19
+ * auth 字段级深度 merge:源字段覆盖/新增到备份,备份独有字段保留
20
+
21
+ * config.toml 排除 [projects.*]:本地按项目授信的段不进备份,其余段段级 merge(源覆盖)
22
+
23
+ * 安全权限:备份文件 chmod 600(仅当前用户可读写)
24
+
25
+ 使用示例:
26
+
27
+ $ easyai backup codex
28
+ DESC
29
+
30
+ AUTH_KEY = 'auth'.freeze
31
+ CONFIG_KEY = 'config_toml'.freeze
32
+
33
+ # 注意:路径用方法动态 expand_path,避免常量在加载期冻结真实 HOME,
34
+ # 让 spec 切换 ENV['HOME'] 后定位失败(同 LocalConfig.config_path 的处理)
35
+ def auth_source = File.expand_path('~/.codex/auth.json')
36
+ def config_source = File.expand_path('~/.codex/config.toml')
37
+ def backup_dir = File.expand_path('~/.easyai/backup')
38
+ def backup_path = File.join(backup_dir, '.codex.json')
39
+
40
+ def validate!
41
+ super
42
+ ensure_file_credentials_supported!
43
+ help! "源文件不存在: #{auth_source}\n请先登录 Codex 后再备份。" unless File.exist?(auth_source)
44
+ end
45
+
46
+ def run
47
+ ensure_file_credentials_supported!
48
+ unless official_token_present?
49
+ puts '⚠ 未在 ~/.codex/auth.json 检测到 ChatGPT 官方登录 token(OAuth),跳过备份。'
50
+ puts ' 如需备份,请先运行 codex 完成 ChatGPT 订阅账户登录后重试。'
51
+ return
52
+ end
53
+
54
+ perform_backup
55
+ end
56
+
57
+ # 供 easyai setup 选择 ChatGPT Subscribe 时复用:仅当存在官方 token 才更新备份,
58
+ # 效果等同 easyai backup codex。返回是否实际执行了备份。
59
+ def backup_if_official
60
+ return false unless official_token_present?
61
+
62
+ perform_backup
63
+ true
64
+ end
65
+
66
+ # 供 easyai codex 运行前调用:仅当备份不存在、源文件存在且含官方 token 时自动备份一次。
67
+ # 返回是否执行了备份;任何异常降级为 warning,绝不阻塞启动。
68
+ def backup_if_absent
69
+ return false if File.exist?(backup_path)
70
+ return false unless File.exist?(auth_source)
71
+ # keyring 模式下凭证不在文件里,自动备份会一路触发 help! → 每次启动刷屏;此处静默跳过
72
+ return false if File.exist?(config_source) && keyring_credentials_store?(File.read(config_source))
73
+ # 只备份真正的 ChatGPT 官方登录态;EasyAI 给 longcat/deepseek 写的纯 API Key auth.json 不在此列
74
+ return false unless official_token_present?
75
+
76
+ perform_backup
77
+ true
78
+ rescue StandardError => e
79
+ warn " ⚠ 自动备份 Codex 登录信息失败(#{e.message}),继续启动"
80
+ false
81
+ end
82
+
83
+ # 备份文件 .codex.json 中是否含 ChatGPT 官方 OAuth token,
84
+ # 供 easyai codex 运行时决定是否提供 ChatGPT Subscribe 选项。
85
+ def backup_has_official_token?
86
+ return false unless File.exist?(backup_path)
87
+
88
+ oauth_token?(read_json(backup_path)[AUTH_KEY])
89
+ rescue StandardError
90
+ false
91
+ end
92
+
93
+ private
94
+
95
+ def perform_backup
96
+ FileUtils.mkdir_p(backup_dir)
97
+ existing = File.exist?(backup_path) ? read_json(backup_path) : {}
98
+
99
+ auth_summary = merge_auth(existing)
100
+ config_summary = merge_config(existing)
101
+
102
+ File.write(backup_path, JSON.pretty_generate(existing))
103
+ File.chmod(0o600, backup_path) unless windows?
104
+
105
+ report(auth_summary, config_summary)
106
+ end
107
+
108
+ # 检测 ~/.codex/auth.json 是否为 ChatGPT 官方登录(OAuth)。
109
+ def official_token_present?
110
+ return false unless File.exist?(auth_source)
111
+
112
+ oauth_token?(read_json(auth_source))
113
+ rescue StandardError
114
+ false
115
+ end
116
+
117
+ # auth hash 是否带 ChatGPT OAuth 凭证:含非空 tokens 段,或 auth_mode == "chatgpt"。
118
+ # 仅有第三方 OPENAI_API_KEY 不算。
119
+ def oauth_token?(auth)
120
+ return false unless auth.is_a?(Hash)
121
+
122
+ tokens = auth['tokens']
123
+ has_oauth = tokens.is_a?(Hash) && tokens.values.any? { |v| !v.to_s.strip.empty? }
124
+ has_oauth || auth['auth_mode'].to_s == 'chatgpt'
125
+ end
126
+
127
+ def ensure_file_credentials_supported!
128
+ return unless File.exist?(config_source)
129
+
130
+ return unless keyring_credentials_store?(File.read(config_source))
131
+
132
+ help! <<~MSG
133
+ 当前 Codex 配置 cli_auth_credentials_store = "keyring",凭证存储在系统钥匙串中。
134
+ easyai backup codex 目前只支持 file/auth.json 凭证备份,请先改用 file 存储或手动备份系统钥匙串凭证。
135
+ MSG
136
+ end
137
+
138
+ def keyring_credentials_store?(toml_text)
139
+ toml_text.to_s.each_line.any? do |line|
140
+ line.match?(/^\s*cli_auth_credentials_store\s*=\s*["']keyring["']\s*(?:#.*)?$/)
141
+ end
142
+ end
143
+
144
+ # 把 ~/.codex/auth.json 深度 merge 进 existing[AUTH_KEY],返回统计
145
+ def merge_auth(existing)
146
+ source = read_json(auth_source)
147
+ target = existing[AUTH_KEY].is_a?(Hash) ? existing[AUTH_KEY] : {}
148
+ before = target.keys
149
+ existing[AUTH_KEY] = deep_merge(target, source)
150
+
151
+ {
152
+ added: (source.keys - before).size,
153
+ updated: (source.keys & before).size,
154
+ kept: (before - source.keys).size,
155
+ total: existing[AUTH_KEY].keys.size
156
+ }
157
+ end
158
+
159
+ # 把 config.toml(去 projects)段级 merge 进 existing[CONFIG_KEY],返回统计
160
+ def merge_config(existing)
161
+ return nil unless File.exist?(config_source)
162
+
163
+ raw = File.read(config_source)
164
+ excluded = Base::TomlSections.split_sections(raw)
165
+ .count { |s| Base::TomlSections.project_header?(s.header) }
166
+ filtered = Base::TomlSections.reject_projects(raw)
167
+ base = existing[CONFIG_KEY].is_a?(String) ? existing[CONFIG_KEY] : ''
168
+ existing[CONFIG_KEY] = Base::TomlSections.merge(base, filtered)
169
+
170
+ { excluded_projects: excluded }
171
+ end
172
+
173
+ def read_json(path)
174
+ JSON.parse(File.read(path))
175
+ rescue JSON::ParserError => e
176
+ raise "解析 JSON 失败 (#{path}): #{e.message}"
177
+ end
178
+
179
+ def deep_merge(target, source)
180
+ target.merge(source) do |_key, old_val, new_val|
181
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
182
+ deep_merge(old_val, new_val)
183
+ else
184
+ new_val
185
+ end
186
+ end
187
+ end
188
+
189
+ def report(auth, config)
190
+ puts "✓ Codex 登录信息已备份"
191
+ puts " 备份文件: #{backup_path}"
192
+ puts " auth: 新增 #{auth[:added]},更新 #{auth[:updated]},保留备份独有 #{auth[:kept]}(合计 #{auth[:total]})"
193
+ if config
194
+ puts " config.toml: 已备份(排除 #{config[:excluded_projects]} 个 [projects.*] 授信段)"
195
+ else
196
+ puts " config.toml: 源文件不存在,跳过"
197
+ end
198
+ end
199
+
200
+ def windows?
201
+ Base::SystemInfo.windows?
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -8,10 +8,12 @@ module EasyAI
8
8
  可用命令:
9
9
 
10
10
  * claude - 备份 Claude Code 登录信息
11
+ * codex - 备份 Codex 登录信息
11
12
 
12
13
  使用示例:
13
14
 
14
15
  $ easyai backup claude # 备份 ~/.claude.json(去除 projects)到 ~/.easyai/backup/.claude.json
16
+ $ easyai backup codex # 备份 ~/.codex/auth.json + config.toml(去除 [projects.*])到 ~/.easyai/backup/.codex.json
15
17
  DESC
16
18
 
17
19
  self.abstract_command = true
@@ -21,3 +23,4 @@ end
21
23
 
22
24
  # 加载子命令
23
25
  require_relative 'backup/claude'
26
+ require_relative 'backup/codex'
@@ -4,6 +4,7 @@ require 'json'
4
4
  require 'fileutils'
5
5
  require 'open3'
6
6
  require_relative 'ai_tool_base'
7
+ require_relative 'backup'
7
8
 
8
9
  module EasyAI
9
10
  class Command
@@ -57,6 +58,8 @@ DESC
57
58
  # 会清掉 OAuth 状态。EasyAI 设计前提是统一走 ANTHROPIC_AUTH_TOKEN 环境变量路径,OAuth 登录
58
59
  # 态由 `easyai backup claude` / `easyai restore claude` 显式管理。
59
60
  def pre_exec
61
+ # 方案 A:清理 OAuth 残留前先兜底备份,确保首次运行可恢复(仅当备份缺失时触发)
62
+ Backup::Claude.new(CLAide::ARGV.new([])).backup_if_absent
60
63
  reset_claude_state_to_token_mode
61
64
  delete_keychain_credential(KEYCHAIN_LABEL)
62
65
  end
@@ -9,18 +9,17 @@ module EasyAI
9
9
  # 清理 AI CLI 自身在本地产生的缓存与配置目录。
10
10
  #
11
11
  # 设计要点(详见 docs/设计文档.md §2.6):
12
- # - 仅清理三家 AI CLI(claude / gemini / codex)的缓存路径
12
+ # - 仅清理两家 AI CLI(claude / codex)的缓存路径
13
13
  # - 不会触碰 ~/.easyai/config.json(重置 EasyAI 自身配置请用 easyai setup --reset)
14
14
  # - --force 跳过确认;--dry-run 仅打印
15
15
  # - 删除时遇到 Errno::EACCES 继续删余下项;最终如有失败项以退出码 1 退出
16
16
  class Clean < Command
17
17
  self.summary = '清理 AI CLI 自身缓存(不影响 EasyAI 配置)'
18
18
  self.description = <<-DESC
19
- 清理 claude / gemini / codex 各自在本地产生的缓存目录与配置文件。
19
+ 清理 claude / codex 各自在本地产生的缓存目录与配置文件。
20
20
 
21
21
  支持范围:
22
22
  * claude → ~/.claude、~/.claude.json、~/.config/claude
23
- * gemini → ~/.gemini、~/.config/gemini
24
23
  * codex → ~/.codex、~/.config/codex
25
24
  * all → 上述全部
26
25
 
@@ -37,12 +36,11 @@ module EasyAI
37
36
  $ easyai setup --reset
38
37
  DESC
39
38
 
40
- VALID_TOOLS = %w[claude gemini codex all].freeze
39
+ VALID_TOOLS = %w[claude codex all].freeze
41
40
 
42
41
  # AI CLI 自身的缓存路径表(不含 ~/.easyai/config.json)
43
42
  CACHE_PATHS = {
44
43
  'claude' => %w[~/.claude ~/.claude.json ~/.config/claude],
45
- 'gemini' => %w[~/.gemini ~/.config/gemini],
46
44
  'codex' => %w[~/.codex ~/.config/codex]
47
45
  }.freeze
48
46