easyai 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,453 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'fileutils'
4
+ require 'etc'
5
+ require 'open3'
6
+
7
+ module EasyAI
8
+ class Command
9
+ class Clean < Command
10
+ self.summary = '清理AI工具配置信息'
11
+ self.description = <<-DESC
12
+ 清理指定AI工具的所有配置信息。
13
+
14
+ 支持清理:
15
+
16
+ * Claude 相关配置
17
+
18
+ * Gemini 相关配置
19
+
20
+ * OpenAI 相关配置
21
+
22
+ 使用示例:
23
+
24
+ $ easyai clean # 清理Claude配置(默认)
25
+
26
+ $ easyai clean gemini # 清理Gemini配置
27
+
28
+ $ easyai clean openai # 清理OpenAI配置
29
+
30
+ $ easyai clean all # 清理所有配置
31
+ DESC
32
+
33
+ def self.options
34
+ [
35
+ ['--force', '强制清理,不询问确认'],
36
+ ['--dry-run', '预览将要清理的项目,不执行实际清理'],
37
+ ].concat(super)
38
+ end
39
+
40
+ def initialize(argv)
41
+ @tool_name = argv.shift_argument || 'claude'
42
+ @force = argv.flag?('force')
43
+ @dry_run = argv.flag?('dry-run')
44
+ super
45
+ end
46
+
47
+ def validate!
48
+ super
49
+ valid_tools = %w[claude gemini openai all]
50
+ unless valid_tools.include?(@tool_name.downcase)
51
+ help! "不支持的工具名称: #{@tool_name}。支持的工具: #{valid_tools.join(', ')}"
52
+ end
53
+ @tool_name = @tool_name.downcase
54
+ end
55
+
56
+ def run
57
+ puts "🧹 准备清理 #{@tool_name == 'all' ? '所有AI工具' : @tool_name.upcase} 的配置信息"
58
+ puts
59
+
60
+ # 预览模式或确认
61
+ if @dry_run
62
+ puts "📋 预览模式 - 以下项目将被清理:"
63
+ preview_cleanup
64
+ return
65
+ end
66
+
67
+ unless @force
68
+ puts "⚠️ 警告:此操作将删除以下配置信息:"
69
+ preview_cleanup
70
+ puts
71
+ print "确认继续清理?(y/N): "
72
+ confirmation = STDIN.gets.chomp.downcase
73
+ unless confirmation == 'y' || confirmation == 'yes'
74
+ puts "❌ 清理操作已取消"
75
+ return
76
+ end
77
+ end
78
+
79
+ puts "🧹 开始清理配置..."
80
+ perform_cleanup
81
+ puts "✅ 清理完成"
82
+ end
83
+
84
+ private
85
+
86
+ def preview_cleanup
87
+ case @tool_name
88
+ when 'claude'
89
+ preview_claude_cleanup
90
+ when 'gemini'
91
+ preview_gemini_cleanup
92
+ when 'openai'
93
+ preview_openai_cleanup
94
+ when 'all'
95
+ preview_claude_cleanup
96
+ preview_gemini_cleanup
97
+ preview_openai_cleanup
98
+ end
99
+ end
100
+
101
+ def perform_cleanup
102
+ case @tool_name
103
+ when 'claude'
104
+ clean_claude
105
+ when 'gemini'
106
+ clean_gemini
107
+ when 'openai'
108
+ clean_openai
109
+ when 'all'
110
+ clean_claude
111
+ clean_gemini
112
+ clean_openai
113
+ end
114
+ end
115
+
116
+ def preview_claude_cleanup
117
+ puts "📌 Claude 配置(由 easyai claude 命令设置的项目):"
118
+
119
+ # 本地配置文件 - easyai 自己的配置,不是 authclaude.rb 设置的
120
+ config_file = File.expand_path('~/.easyai/config.yml')
121
+ if File.exist?(config_file)
122
+ config = YAML.load_file(config_file) rescue {}
123
+ if config && config['claude_token']
124
+ puts " • 本地配置文件: #{config_file} (claude_token)"
125
+ end
126
+ end
127
+
128
+ # Claude JSON 配置文件 - authclaude.rb 中 update_claude_json 设置的
129
+ claude_file = File.expand_path('~/.claude.json')
130
+ if File.exist?(claude_file) && has_claude_json_config?(claude_file)
131
+ puts " • Claude配置文件: #{claude_file} (由远程配置合并的内容)"
132
+ end
133
+
134
+ # Shell 配置文件中的代理别名 - authclaude.rb 中 configure_proxy 设置的
135
+ shell_files = get_shell_files
136
+ shell_files.each do |shell_file|
137
+ if File.exist?(shell_file) && has_claude_proxy_config?(shell_file)
138
+ puts " • Shell代理配置: #{shell_file} (claude_proxy, unclaude_proxy 别名)"
139
+ end
140
+ end
141
+
142
+ # Keychain 认证信息 - authclaude.rb 中 configure_keychain 设置的
143
+ if RUBY_PLATFORM.include?('darwin')
144
+ keychain_entries = get_claude_keychain_entries
145
+ keychain_entries.each do |service_name|
146
+ puts " • macOS Keychain: #{service_name} (Claude认证信息)"
147
+ end
148
+ end
149
+
150
+ puts
151
+ end
152
+
153
+ def preview_gemini_cleanup
154
+ puts "📌 Gemini 配置:"
155
+
156
+ # 本地配置文件
157
+ config_file = File.expand_path('~/.easyai/config.yml')
158
+ if File.exist?(config_file)
159
+ puts " • 本地配置文件: #{config_file} (gemini_token)"
160
+ end
161
+
162
+ puts
163
+ end
164
+
165
+ def preview_openai_cleanup
166
+ puts "📌 OpenAI 配置:"
167
+
168
+ # 本地配置文件
169
+ config_file = File.expand_path('~/.easyai/config.yml')
170
+ if File.exist?(config_file)
171
+ puts " • 本地配置文件: #{config_file} (openai_token)"
172
+ end
173
+
174
+ puts
175
+ end
176
+
177
+ def clean_claude
178
+ puts "🧹 清理Claude配置..."
179
+
180
+ # 清理本地配置文件中的Claude令牌
181
+ clean_local_config('claude_token')
182
+
183
+ # 清理 ~/.claude.json
184
+ clean_claude_json
185
+
186
+ # 清理Shell配置文件
187
+ clean_shell_config_claude
188
+
189
+ # 清理Keychain
190
+ clean_keychain_claude
191
+ end
192
+
193
+ def clean_gemini
194
+ puts "🧹 清理Gemini配置..."
195
+ clean_local_config('gemini_token')
196
+ end
197
+
198
+ def clean_openai
199
+ puts "🧹 清理OpenAI配置..."
200
+ clean_local_config('openai_token')
201
+ end
202
+
203
+ def clean_local_config(key)
204
+ config_file = File.expand_path('~/.easyai/config.yml')
205
+ return unless File.exist?(config_file)
206
+
207
+ begin
208
+ config = YAML.load_file(config_file) || {}
209
+ if config.key?(key)
210
+ config.delete(key)
211
+ File.write(config_file, config.to_yaml)
212
+ puts " ✓ 已清理本地配置文件中的 #{key}"
213
+
214
+ # 如果配置文件为空,删除整个配置目录
215
+ if config.empty?
216
+ config_dir = File.dirname(config_file)
217
+ FileUtils.rm_rf(config_dir)
218
+ puts " ✓ 已删除空配置目录: #{config_dir}"
219
+ end
220
+ end
221
+ rescue => e
222
+ puts " ✗ 清理本地配置失败: #{e.message}"
223
+ end
224
+ end
225
+
226
+ def clean_claude_json
227
+ claude_file = File.expand_path('~/.claude.json')
228
+ return unless File.exist?(claude_file)
229
+
230
+ begin
231
+ # 不是完全删除文件,而是恢复到只包含默认字段的状态
232
+ # 因为 Claude Code 会自动重新创建基本配置
233
+ content = JSON.parse(File.read(claude_file))
234
+
235
+ # 保留默认字段,删除由 authclaude.rb update_claude_json 方法添加的配置
236
+ default_fields = %w[installMethod autoUpdates userID fallbackAvailableWarningThreshold projects tipsHistory]
237
+ cleaned_content = content.select { |key, _| default_fields.include?(key) }
238
+
239
+ if cleaned_content != content
240
+ File.write(claude_file, JSON.pretty_generate(cleaned_content))
241
+ puts " ✓ 已清理Claude配置文件中的远程配置内容: #{claude_file}"
242
+ end
243
+ rescue JSON::ParserError => e
244
+ puts " ✗ 解析Claude配置文件失败: #{e.message}"
245
+ rescue => e
246
+ puts " ✗ 清理Claude配置文件失败: #{e.message}"
247
+ end
248
+ end
249
+
250
+ def clean_shell_config_claude
251
+ shell_files = get_shell_files
252
+
253
+ shell_files.each do |shell_file|
254
+ next unless File.exist?(shell_file)
255
+
256
+ begin
257
+ content = File.read(shell_file)
258
+ original_content = content.dup
259
+ lines = content.split("\n", -1)
260
+ lines.pop if lines.last == ""
261
+
262
+ # 只清理由 authclaude.rb 添加的代理配置(claude_proxy 和 unclaude_proxy)
263
+ # 不清理 CLAUDE_CODE_OAUTH_TOKEN,因为那不是 authclaude.rb 设置的
264
+ lines = remove_claude_proxy_config(lines, shell_file)
265
+
266
+ if lines.join("\n") + "\n" != original_content
267
+ File.write(shell_file, lines.join("\n") + "\n")
268
+ puts " ✓ 已清理Shell代理配置: #{shell_file}"
269
+ end
270
+ rescue => e
271
+ puts " ✗ 清理Shell配置失败 #{shell_file}: #{e.message}"
272
+ end
273
+ end
274
+ end
275
+
276
+ def clean_keychain_claude
277
+ return unless RUBY_PLATFORM.include?('darwin')
278
+
279
+ account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
280
+ keychain_entries = get_claude_keychain_entries
281
+
282
+ if keychain_entries.empty?
283
+ # 检查默认的 Claude Code-credentials
284
+ service_name = "Claude Code-credentials"
285
+ cmd = ["security", "delete-generic-password", "-a", account_name, "-s", service_name]
286
+ stdout, stderr, status = Open3.capture3(*cmd)
287
+
288
+ if status.success?
289
+ puts " ✓ 已清理Keychain: #{service_name}"
290
+ elsif !stderr.include?("could not be found")
291
+ puts " ✗ 清理Keychain失败: #{stderr}"
292
+ end
293
+ else
294
+ # 清理所有找到的 Claude 相关条目
295
+ keychain_entries.each do |service_name|
296
+ cmd = ["security", "delete-generic-password", "-a", account_name, "-s", service_name]
297
+ stdout, stderr, status = Open3.capture3(*cmd)
298
+
299
+ if status.success?
300
+ puts " ✓ 已清理Keychain: #{service_name}"
301
+ elsif !stderr.include?("could not be found")
302
+ puts " ✗ 清理Keychain失败 #{service_name}: #{stderr}"
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ def has_claude_json_config?(claude_file)
309
+ return false unless File.exist?(claude_file)
310
+
311
+ begin
312
+ content = JSON.parse(File.read(claude_file))
313
+ # 检查是否有非默认的配置项(排除 Claude Code 自动生成的默认字段)
314
+ default_fields = %w[installMethod autoUpdates userID fallbackAvailableWarningThreshold projects tipsHistory]
315
+ has_custom_config = content.keys.any? { |key| !default_fields.include?(key) }
316
+ return has_custom_config
317
+ rescue JSON::ParserError, StandardError
318
+ return false
319
+ end
320
+ end
321
+
322
+ def has_claude_proxy_config?(shell_file)
323
+ return false unless File.exist?(shell_file)
324
+
325
+ content = File.read(shell_file)
326
+ content.include?('claude_proxy') || content.include?('unclaude_proxy')
327
+ end
328
+
329
+ def get_claude_keychain_entries
330
+ return [] unless RUBY_PLATFORM.include?('darwin')
331
+
332
+ entries = []
333
+
334
+ # 检查各种可能的 Claude 相关 keychain 条目
335
+ possible_services = [
336
+ "Claude Code-credentials",
337
+ # 从 authclaude.rb 中的 configure_keychain 方法可以看到,service_name 来自配置
338
+ # 但我们不知道具体的服务名,所以需要通过搜索来查找
339
+ ]
340
+
341
+ account_name = Etc.getlogin || ENV['USER'] || ENV['LOGNAME']
342
+
343
+ # 搜索所有包含 "claude" 的 keychain 条目
344
+ search_cmd = ["security", "dump-keychain", "-d"]
345
+
346
+ begin
347
+ stdout, stderr, status = Open3.capture3(*search_cmd)
348
+ if status.success?
349
+ # 解析输出,查找 Claude 相关条目
350
+ stdout.lines.each do |line|
351
+ if line.include?("claude") && line.include?("svce")
352
+ # 提取服务名称
353
+ match = line.match(/"([^"]*claude[^"]*)"/)
354
+ entries << match[1] if match
355
+ end
356
+ end
357
+ end
358
+ rescue => e
359
+ # 如果搜索失败,回退到检查已知的服务名
360
+ possible_services.each do |service_name|
361
+ cmd = ["security", "find-generic-password", "-a", account_name, "-s", service_name]
362
+ if system(*cmd, out: File::NULL, err: File::NULL)
363
+ entries << service_name
364
+ end
365
+ end
366
+ end
367
+
368
+ entries.uniq
369
+ end
370
+
371
+ def get_shell_files
372
+ shell_files = []
373
+ current_shell = ENV['SHELL']&.split('/')&.last || 'bash'
374
+
375
+ case current_shell
376
+ when 'zsh'
377
+ shell_files << File.expand_path("~/.zshrc")
378
+ when 'bash'
379
+ bash_profile = File.expand_path("~/.bash_profile")
380
+ bashrc = File.expand_path("~/.bashrc")
381
+ shell_files << (File.exist?(bash_profile) ? bash_profile : bashrc)
382
+ when 'fish'
383
+ fish_config = File.expand_path("~/.config/fish/config.fish")
384
+ shell_files << fish_config
385
+ else
386
+ shell_files << File.expand_path("~/.profile")
387
+ end
388
+
389
+ # 总是添加 ~/.profile 作为备份
390
+ profile_file = File.expand_path("~/.profile")
391
+ shell_files << profile_file unless shell_files.include?(profile_file)
392
+
393
+ shell_files
394
+ end
395
+
396
+ def remove_claude_proxy_config(lines, shell_file)
397
+ # 只清理 claude_proxy 和 unclaude_proxy 相关配置
398
+ # 这是 authclaude.rb 中 configure_proxy 方法添加的内容
399
+ if shell_file.include?("config.fish")
400
+ # Fish shell - 移除代理相关的别名/函数
401
+ lines.reject! { |line|
402
+ line.match(/^alias\s+claude_proxy=/) ||
403
+ line.match(/^alias\s+unclaude_proxy=/) ||
404
+ line.match(/^function\s+(claude_proxy|unclaude_proxy)/)
405
+ }
406
+
407
+ # 移除函数块
408
+ in_function = false
409
+ lines.select do |line|
410
+ if line.match(/^function\s+(claude_proxy|unclaude_proxy)/)
411
+ in_function = true
412
+ false
413
+ elsif in_function && line.strip == "end"
414
+ in_function = false
415
+ false
416
+ elsif in_function
417
+ false
418
+ else
419
+ true
420
+ end
421
+ end
422
+ else
423
+ # Bash/zsh - 移除代理相关的别名/函数
424
+ lines.reject! { |line|
425
+ line.match(/^alias\s+(claude_proxy|unclaude_proxy)=/) ||
426
+ line.match(/^(claude_proxy|unclaude_proxy)\(\)/)
427
+ }
428
+
429
+ # 移除函数块
430
+ in_function = false
431
+ function_depth = 0
432
+ lines.select do |line|
433
+ if line.match(/^(claude_proxy|unclaude_proxy)\(\)/)
434
+ in_function = true
435
+ function_depth = 0
436
+ false
437
+ elsif in_function
438
+ function_depth += 1 if line.include?("{")
439
+ if line.include?("}")
440
+ function_depth -= 1
441
+ in_function = false if function_depth <= 0
442
+ end
443
+ false
444
+ else
445
+ true
446
+ end
447
+ end
448
+ end
449
+ end
450
+
451
+ end
452
+ end
453
+ end
@@ -0,0 +1,58 @@
1
+ module EasyAI
2
+ class Command
3
+ class Gemini < Command
4
+ self.summary = '运行 Gemini CLI'
5
+ self.description = <<-DESC
6
+ 启动 Google Gemini CLI 工具。
7
+
8
+ 主要功能:
9
+
10
+ * 自动配置 API 密钥
11
+
12
+ * 支持交互式对话
13
+
14
+ * 透传所有参数
15
+
16
+ 使用示例:
17
+
18
+ $ easyai gemini # 启动交互式 Gemini
19
+
20
+ $ easyai gemini chat # 开始聊天会话
21
+
22
+ $ easyai gemini --help # 查看 Gemini 帮助
23
+ DESC
24
+
25
+ def initialize(argv)
26
+ super
27
+ @gemini_args = @argv.remainder!
28
+ end
29
+
30
+ def validate!
31
+ super
32
+ help! '未找到 Gemini CLI。请安装:npm install -g @google/gemini-cli' unless gemini_available?
33
+ end
34
+
35
+ def run
36
+ # 直接使用环境变量,不再依赖配置文件
37
+ env = ENV.to_h
38
+
39
+ # 如果环境变量中已经设置了 API KEY,直接使用
40
+ # 用户可以通过 export GOOGLE_API_KEY=xxx 来设置
41
+
42
+ puts "正在运行: gemini #{@gemini_args.join(' ')}".blue if verbose?
43
+ exec(env, 'gemini', *@gemini_args)
44
+ end
45
+
46
+ private
47
+
48
+ def gemini_available?
49
+ # 跨平台命令检测
50
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
51
+ system('where gemini >nul 2>&1')
52
+ else
53
+ system('which gemini > /dev/null 2>&1')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ module EasyAI
2
+ class Command
3
+ class GPT < Command
4
+ self.summary = '运行 OpenAI CLI'
5
+ self.description = <<-DESC
6
+ 启动 OpenAI GPT CLI 工具。
7
+
8
+ 主要功能:
9
+
10
+ * 自动配置 API 密钥
11
+
12
+ * 支持 API 调用
13
+
14
+ * 透传所有参数
15
+
16
+ 使用示例:
17
+
18
+ $ easyai gpt # 启动 OpenAI CLI
19
+
20
+ $ easyai gpt api chat.completions # 调用 Chat API
21
+
22
+ $ easyai gpt --help # 查看 OpenAI 帮助
23
+ DESC
24
+
25
+ def initialize(argv)
26
+ super
27
+ @gpt_args = @argv.remainder!
28
+ end
29
+
30
+ def validate!
31
+ super
32
+ help! '未找到 OpenAI CLI。请安装:pip install openai' unless gpt_available?
33
+ end
34
+
35
+ def run
36
+ # 直接使用环境变量,不再依赖配置文件
37
+ env = ENV.to_h
38
+
39
+ # 如果环境变量中已经设置了 API KEY,直接使用
40
+ # 用户可以通过 export OPENAI_API_KEY=xxx 来设置
41
+
42
+ puts "正在运行: openai #{@gpt_args.join(' ')}".blue if verbose?
43
+ exec(env, 'openai', *@gpt_args)
44
+ end
45
+
46
+ private
47
+
48
+ def gpt_available?
49
+ # 跨平台命令检测
50
+ if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
51
+ system('where openai >nul 2>&1')
52
+ else
53
+ system('which openai > /dev/null 2>&1')
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end