claude-dev-cli 0.3.1__tar.gz → 0.5.0__tar.gz

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.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

Files changed (35) hide show
  1. {claude_dev_cli-0.3.1/src/claude_dev_cli.egg-info → claude_dev_cli-0.5.0}/PKG-INFO +28 -6
  2. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/README.md +25 -5
  3. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/pyproject.toml +3 -1
  4. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/cli.py +228 -1
  5. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/config.py +65 -5
  6. claude_dev_cli-0.5.0/src/claude_dev_cli/history.py +189 -0
  7. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/viewer.py +86 -3
  8. claude_dev_cli-0.5.0/src/claude_dev_cli/secure_storage.py +219 -0
  9. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0/src/claude_dev_cli.egg-info}/PKG-INFO +28 -6
  10. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/SOURCES.txt +4 -0
  11. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/requires.txt +2 -0
  12. claude_dev_cli-0.5.0/tests/test_diff_editor.py +272 -0
  13. claude_dev_cli-0.5.0/tests/test_secure_storage.py +224 -0
  14. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/LICENSE +0 -0
  15. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/MANIFEST.in +0 -0
  16. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/setup.cfg +0 -0
  17. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/__init__.py +0 -0
  18. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/commands.py +0 -0
  19. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/core.py +0 -0
  20. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/__init__.py +0 -0
  21. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/base.py +0 -0
  22. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/__init__.py +0 -0
  23. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/plugins/diff_editor/plugin.py +0 -0
  24. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/templates.py +0 -0
  25. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/toon_utils.py +0 -0
  26. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli/usage.py +0 -0
  27. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/dependency_links.txt +0 -0
  28. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/entry_points.txt +0 -0
  29. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/src/claude_dev_cli.egg-info/top_level.txt +0 -0
  30. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/tests/test_cli.py +0 -0
  31. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/tests/test_commands.py +0 -0
  32. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/tests/test_config.py +0 -0
  33. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/tests/test_core.py +0 -0
  34. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/tests/test_toon_utils.py +0 -0
  35. {claude_dev_cli-0.3.1 → claude_dev_cli-0.5.0}/tests/test_usage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-dev-cli
3
- Version: 0.3.1
3
+ Version: 0.5.0
4
4
  Summary: A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking
5
5
  Author-email: Julio <thinmanj@users.noreply.github.com>
6
6
  License: MIT
@@ -26,6 +26,8 @@ Requires-Dist: anthropic>=0.18.0
26
26
  Requires-Dist: click>=8.1.0
27
27
  Requires-Dist: rich>=13.0.0
28
28
  Requires-Dist: pydantic>=2.0.0
29
+ Requires-Dist: keyring>=24.0.0
30
+ Requires-Dist: cryptography>=41.0.0
29
31
  Provides-Extra: toon
30
32
  Requires-Dist: toon-format>=0.9.0; extra == "toon"
31
33
  Provides-Extra: plugins
@@ -46,9 +48,10 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
46
48
  ## Features
47
49
 
48
50
  ### 🔑 Multi-API Key Management
51
+ - **Secure Storage**: API keys stored in system keyring (macOS Keychain, Linux Secret Service, Windows Credential Locker)
49
52
  - Route tasks to different Claude API keys (personal, client, enterprise)
50
53
  - Automatic API selection based on project configuration
51
- - Environment variable support for secure key management
54
+ - Automatic migration from plaintext to secure storage
52
55
 
53
56
  ### 🧪 Developer Tools
54
57
  - **Test Generation**: Automatic pytest test generation for Python code
@@ -98,13 +101,17 @@ pip install claude-dev-cli[toon]
98
101
  # Add your personal API key
99
102
  export PERSONAL_ANTHROPIC_API_KEY="sk-ant-..."
100
103
  cdc config add personal --default --description "My personal API key"
104
+ # 🔐 Stored securely in system keyring
101
105
 
102
106
  # Add client's API key
103
107
  export CLIENT_ANTHROPIC_API_KEY="sk-ant-..."
104
108
  cdc config add client --description "Client's Enterprise API"
105
109
 
106
- # List configured APIs
110
+ # List configured APIs (shows storage method)
107
111
  cdc config list
112
+
113
+ # Manually migrate existing keys (automatic on first run)
114
+ cdc config migrate-keys
108
115
  ```
109
116
 
110
117
  ### 2. Basic Usage
@@ -186,22 +193,37 @@ cat large_data.json | cdc toon encode | cdc ask "analyze this data"
186
193
 
187
194
  ## Configuration
188
195
 
196
+ ### Secure API Key Storage
197
+
198
+ **🔐 Your API keys are stored securely and never in plain text.**
199
+
200
+ - **macOS**: Keychain
201
+ - **Linux**: Secret Service API (GNOME Keyring, KWallet)
202
+ - **Windows**: Windows Credential Locker
203
+ - **Fallback**: Encrypted file (if keyring unavailable)
204
+
205
+ Keys are automatically migrated from plaintext on first run. You can also manually migrate:
206
+
207
+ ```bash
208
+ cdc config migrate-keys
209
+ ```
210
+
189
211
  ### Global Configuration
190
212
 
191
- Configuration is stored in `~/.claude-dev-cli/config.json`:
213
+ Configuration metadata is stored in `~/.claude-dev-cli/config.json` (API keys are NOT in this file):
192
214
 
193
215
  ```json
194
216
  {
195
217
  "api_configs": [
196
218
  {
197
219
  "name": "personal",
198
- "api_key": "sk-ant-...",
220
+ "api_key": "", // Empty - actual key in secure storage
199
221
  "description": "My personal API key",
200
222
  "default": true
201
223
  },
202
224
  {
203
225
  "name": "client",
204
- "api_key": "sk-ant-...",
226
+ "api_key": "", // Empty - actual key in secure storage
205
227
  "description": "Client's Enterprise API",
206
228
  "default": false
207
229
  }
@@ -5,9 +5,10 @@ A powerful command-line tool for developers using Claude AI with multi-API routi
5
5
  ## Features
6
6
 
7
7
  ### 🔑 Multi-API Key Management
8
+ - **Secure Storage**: API keys stored in system keyring (macOS Keychain, Linux Secret Service, Windows Credential Locker)
8
9
  - Route tasks to different Claude API keys (personal, client, enterprise)
9
10
  - Automatic API selection based on project configuration
10
- - Environment variable support for secure key management
11
+ - Automatic migration from plaintext to secure storage
11
12
 
12
13
  ### 🧪 Developer Tools
13
14
  - **Test Generation**: Automatic pytest test generation for Python code
@@ -57,13 +58,17 @@ pip install claude-dev-cli[toon]
57
58
  # Add your personal API key
58
59
  export PERSONAL_ANTHROPIC_API_KEY="sk-ant-..."
59
60
  cdc config add personal --default --description "My personal API key"
61
+ # 🔐 Stored securely in system keyring
60
62
 
61
63
  # Add client's API key
62
64
  export CLIENT_ANTHROPIC_API_KEY="sk-ant-..."
63
65
  cdc config add client --description "Client's Enterprise API"
64
66
 
65
- # List configured APIs
67
+ # List configured APIs (shows storage method)
66
68
  cdc config list
69
+
70
+ # Manually migrate existing keys (automatic on first run)
71
+ cdc config migrate-keys
67
72
  ```
68
73
 
69
74
  ### 2. Basic Usage
@@ -145,22 +150,37 @@ cat large_data.json | cdc toon encode | cdc ask "analyze this data"
145
150
 
146
151
  ## Configuration
147
152
 
153
+ ### Secure API Key Storage
154
+
155
+ **🔐 Your API keys are stored securely and never in plain text.**
156
+
157
+ - **macOS**: Keychain
158
+ - **Linux**: Secret Service API (GNOME Keyring, KWallet)
159
+ - **Windows**: Windows Credential Locker
160
+ - **Fallback**: Encrypted file (if keyring unavailable)
161
+
162
+ Keys are automatically migrated from plaintext on first run. You can also manually migrate:
163
+
164
+ ```bash
165
+ cdc config migrate-keys
166
+ ```
167
+
148
168
  ### Global Configuration
149
169
 
150
- Configuration is stored in `~/.claude-dev-cli/config.json`:
170
+ Configuration metadata is stored in `~/.claude-dev-cli/config.json` (API keys are NOT in this file):
151
171
 
152
172
  ```json
153
173
  {
154
174
  "api_configs": [
155
175
  {
156
176
  "name": "personal",
157
- "api_key": "sk-ant-...",
177
+ "api_key": "", // Empty - actual key in secure storage
158
178
  "description": "My personal API key",
159
179
  "default": true
160
180
  },
161
181
  {
162
182
  "name": "client",
163
- "api_key": "sk-ant-...",
183
+ "api_key": "", // Empty - actual key in secure storage
164
184
  "description": "Client's Enterprise API",
165
185
  "default": false
166
186
  }
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "claude-dev-cli"
7
- version = "0.3.1"
7
+ version = "0.5.0"
8
8
  description = "A powerful CLI tool for developers using Claude AI with multi-API routing, test generation, code review, and usage tracking"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -32,6 +32,8 @@ dependencies = [
32
32
  "click>=8.1.0",
33
33
  "rich>=13.0.0",
34
34
  "pydantic>=2.0.0",
35
+ "keyring>=24.0.0",
36
+ "cryptography>=41.0.0",
35
37
  ]
36
38
 
37
39
  [project.optional-dependencies]
@@ -1,5 +1,6 @@
1
1
  """Command-line interface for Claude Dev CLI."""
2
2
 
3
+ import os
3
4
  import sys
4
5
  from pathlib import Path
5
6
  from typing import Optional
@@ -23,6 +24,7 @@ from claude_dev_cli.commands import (
23
24
  from claude_dev_cli.usage import UsageTracker
24
25
  from claude_dev_cli import toon_utils
25
26
  from claude_dev_cli.plugins import load_plugins
27
+ from claude_dev_cli.history import ConversationHistory, Conversation
26
28
 
27
29
  console = Console()
28
30
 
@@ -112,11 +114,36 @@ def ask(
112
114
 
113
115
  @main.command()
114
116
  @click.option('-a', '--api', help='API config to use')
117
+ @click.option('--continue', 'continue_conversation', is_flag=True,
118
+ help='Continue the last conversation')
119
+ @click.option('--save/--no-save', default=True, help='Save conversation history')
115
120
  @click.pass_context
116
- def interactive(ctx: click.Context, api: Optional[str]) -> None:
121
+ def interactive(
122
+ ctx: click.Context,
123
+ api: Optional[str],
124
+ continue_conversation: bool,
125
+ save: bool
126
+ ) -> None:
117
127
  """Start interactive chat mode."""
118
128
  console = ctx.obj['console']
119
129
 
130
+ # Setup conversation history
131
+ config = Config()
132
+ history_dir = config.config_dir / "history"
133
+ conv_history = ConversationHistory(history_dir)
134
+
135
+ # Load or create conversation
136
+ if continue_conversation:
137
+ conversation = conv_history.get_latest_conversation()
138
+ if conversation:
139
+ console.print(f"[green]↶ Continuing conversation from {conversation.updated_at.strftime('%Y-%m-%d %H:%M')}[/green]")
140
+ console.print(f"[dim]Messages: {len(conversation.messages)}[/dim]\n")
141
+ else:
142
+ console.print("[yellow]No previous conversation found, starting new one[/yellow]\n")
143
+ conversation = Conversation()
144
+ else:
145
+ conversation = Conversation()
146
+
120
147
  console.print(Panel.fit(
121
148
  "Claude Dev CLI - Interactive Mode\n"
122
149
  "Type 'exit' or 'quit' to end\n"
@@ -127,30 +154,179 @@ def interactive(ctx: click.Context, api: Optional[str]) -> None:
127
154
 
128
155
  try:
129
156
  client = ClaudeClient(api_config_name=api)
157
+ response_buffer = []
130
158
 
131
159
  while True:
132
160
  try:
133
161
  user_input = console.input("\n[bold cyan]You:[/bold cyan] ").strip()
134
162
 
135
163
  if user_input.lower() in ['exit', 'quit']:
164
+ if save and conversation.messages:
165
+ conv_history.save_conversation(conversation)
166
+ console.print(f"\n[dim]💾 Saved conversation: {conversation.conversation_id}[/dim]")
136
167
  break
168
+
169
+ if user_input.lower() == 'clear':
170
+ conversation = Conversation()
171
+ console.print("[yellow]Conversation cleared[/yellow]")
172
+ continue
173
+
137
174
  if not user_input:
138
175
  continue
139
176
 
177
+ # Add user message to history
178
+ conversation.add_message("user", user_input)
179
+
180
+ # Get response
140
181
  console.print("\n[bold green]Claude:[/bold green] ", end='')
182
+ response_buffer = []
141
183
  for chunk in client.call_streaming(user_input):
142
184
  console.print(chunk, end='')
185
+ response_buffer.append(chunk)
143
186
  console.print()
144
187
 
188
+ # Add assistant response to history
189
+ full_response = ''.join(response_buffer)
190
+ conversation.add_message("assistant", full_response)
191
+
192
+ # Auto-save periodically
193
+ if save and len(conversation.messages) % 10 == 0:
194
+ conv_history.save_conversation(conversation)
195
+
145
196
  except KeyboardInterrupt:
146
197
  console.print("\n\n[yellow]Interrupted. Type 'exit' to quit.[/yellow]")
147
198
  continue
148
199
 
149
200
  except Exception as e:
150
201
  console.print(f"[red]Error: {e}[/red]")
202
+ if save and conversation.messages:
203
+ conv_history.save_conversation(conversation)
151
204
  sys.exit(1)
152
205
 
153
206
 
207
+ @main.group()
208
+ def completion() -> None:
209
+ """Shell completion installation."""
210
+ pass
211
+
212
+
213
+ @completion.command('install')
214
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish', 'auto']), default='auto',
215
+ help='Shell type (auto-detects if not specified)')
216
+ @click.pass_context
217
+ def completion_install(ctx: click.Context, shell: str) -> None:
218
+ """Install shell completion for cdc command."""
219
+ console = ctx.obj['console']
220
+
221
+ # Auto-detect shell if needed
222
+ if shell == 'auto':
223
+ shell_path = os.environ.get('SHELL', '')
224
+ if 'zsh' in shell_path:
225
+ shell = 'zsh'
226
+ elif 'bash' in shell_path:
227
+ shell = 'bash'
228
+ elif 'fish' in shell_path:
229
+ shell = 'fish'
230
+ else:
231
+ console.print("[red]Could not auto-detect shell[/red]")
232
+ console.print("Please specify: --shell bash|zsh|fish")
233
+ sys.exit(1)
234
+
235
+ console.print(f"[cyan]Installing completion for {shell}...[/cyan]\n")
236
+
237
+ if shell == 'zsh':
238
+ console.print("Add this to your ~/.zshrc:\n")
239
+ console.print("[yellow]eval \"$(_CDC_COMPLETE=zsh_source cdc)\"[/yellow]\n")
240
+ console.print("Then run: [cyan]source ~/.zshrc[/cyan]")
241
+ elif shell == 'bash':
242
+ console.print("Add this to your ~/.bashrc:\n")
243
+ console.print("[yellow]eval \"$(_CDC_COMPLETE=bash_source cdc)\"[/yellow]\n")
244
+ console.print("Then run: [cyan]source ~/.bashrc[/cyan]")
245
+ elif shell == 'fish':
246
+ console.print("Add this to ~/.config/fish/completions/cdc.fish:\n")
247
+ console.print("[yellow]_CDC_COMPLETE=fish_source cdc | source[/yellow]\n")
248
+ console.print("Then reload: [cyan]exec fish[/cyan]")
249
+
250
+ console.print("\n[green]✓[/green] Instructions displayed above")
251
+ console.print("[dim]Completion will provide command and option suggestions[/dim]")
252
+
253
+
254
+ @completion.command('generate')
255
+ @click.option('--shell', type=click.Choice(['bash', 'zsh', 'fish']), required=True,
256
+ help='Shell type')
257
+ @click.pass_context
258
+ def completion_generate(ctx: click.Context, shell: str) -> None:
259
+ """Generate completion script for shell."""
260
+ import subprocess
261
+ import sys
262
+
263
+ env_var = f"_CDC_COMPLETE={shell}_source"
264
+ result = subprocess.run(
265
+ [sys.executable, '-m', 'claude_dev_cli.cli'],
266
+ env={**os.environ, '_CDC_COMPLETE': f'{shell}_source'},
267
+ capture_output=True,
268
+ text=True
269
+ )
270
+
271
+ if result.returncode == 0:
272
+ click.echo(result.stdout)
273
+ else:
274
+ ctx.obj['console'].print(f"[red]Error generating completion: {result.stderr}[/red]")
275
+ sys.exit(1)
276
+
277
+
278
+ @main.group()
279
+ def history() -> None:
280
+ """Manage conversation history."""
281
+ pass
282
+
283
+
284
+ @history.command('list')
285
+ @click.option('-n', '--limit', type=int, default=10, help='Number of conversations to show')
286
+ @click.option('-s', '--search', help='Search conversations')
287
+ @click.pass_context
288
+ def history_list(ctx: click.Context, limit: int, search: Optional[str]) -> None:
289
+ """List conversation history."""
290
+ console = ctx.obj['console']
291
+ config = Config()
292
+ conv_history = ConversationHistory(config.config_dir / "history")
293
+
294
+ conversations = conv_history.list_conversations(limit=limit, search_query=search)
295
+
296
+ if not conversations:
297
+ console.print("[yellow]No conversations found[/yellow]")
298
+ return
299
+
300
+ for conv in conversations:
301
+ summary = conv.get_summary(80)
302
+ console.print(f"\n[cyan]{conv.conversation_id}[/cyan]")
303
+ console.print(f"[dim]{conv.updated_at.strftime('%Y-%m-%d %H:%M')} | {len(conv.messages)} messages[/dim]")
304
+ console.print(f" {summary}")
305
+
306
+
307
+ @history.command('export')
308
+ @click.argument('conversation_id')
309
+ @click.option('--format', type=click.Choice(['markdown', 'json']), default='markdown')
310
+ @click.option('-o', '--output', type=click.Path(), help='Output file')
311
+ @click.pass_context
312
+ def history_export(ctx: click.Context, conversation_id: str, format: str, output: Optional[str]) -> None:
313
+ """Export a conversation."""
314
+ console = ctx.obj['console']
315
+ config = Config()
316
+ conv_history = ConversationHistory(config.config_dir / "history")
317
+
318
+ content = conv_history.export_conversation(conversation_id, format)
319
+ if not content:
320
+ console.print(f"[red]Conversation {conversation_id} not found[/red]")
321
+ sys.exit(1)
322
+
323
+ if output:
324
+ Path(output).write_text(content)
325
+ console.print(f"[green]✓[/green] Exported to {output}")
326
+ else:
327
+ click.echo(content)
328
+
329
+
154
330
  @main.group()
155
331
  def config() -> None:
156
332
  """Manage configuration."""
@@ -182,6 +358,49 @@ def config_add(
182
358
  make_default=default
183
359
  )
184
360
  console.print(f"[green]✓[/green] Added API config: {name}")
361
+
362
+ # Show storage method
363
+ storage_method = config.secure_storage.get_storage_method()
364
+ if storage_method == "keyring":
365
+ console.print("[dim]🔐 Stored securely in system keyring[/dim]")
366
+ else:
367
+ console.print("[dim]🔒 Stored in encrypted file (keyring unavailable)[/dim]")
368
+ except Exception as e:
369
+ console.print(f"[red]Error: {e}[/red]")
370
+ sys.exit(1)
371
+
372
+
373
+ @config.command('migrate-keys')
374
+ @click.pass_context
375
+ def config_migrate_keys(ctx: click.Context) -> None:
376
+ """Manually migrate API keys to secure storage."""
377
+ console = ctx.obj['console']
378
+
379
+ try:
380
+ config = Config()
381
+
382
+ # Check if any keys need migration
383
+ api_configs = config._data.get("api_configs", [])
384
+ plaintext_keys = {c["name"]: c.get("api_key", "")
385
+ for c in api_configs
386
+ if c.get("api_key")}
387
+
388
+ if not plaintext_keys:
389
+ console.print("[green]✓[/green] All keys are already in secure storage.")
390
+ storage_method = config.secure_storage.get_storage_method()
391
+ console.print(f"[dim]Using: {storage_method}[/dim]")
392
+ return
393
+
394
+ console.print(f"[yellow]Found {len(plaintext_keys)} plaintext key(s)[/yellow]")
395
+ console.print("Migrating to secure storage...\n")
396
+
397
+ # Trigger migration
398
+ config._auto_migrate_keys()
399
+
400
+ console.print(f"[green]✓[/green] Migrated {len(plaintext_keys)} key(s) to secure storage.")
401
+ storage_method = config.secure_storage.get_storage_method()
402
+ console.print(f"[dim]Using: {storage_method}[/dim]")
403
+
185
404
  except Exception as e:
186
405
  console.print(f"[red]Error: {e}[/red]")
187
406
  sys.exit(1)
@@ -201,6 +420,14 @@ def config_list(ctx: click.Context) -> None:
201
420
  console.print("Run 'cdc config add' to add one.")
202
421
  return
203
422
 
423
+ # Show storage method
424
+ storage_method = config.secure_storage.get_storage_method()
425
+ storage_display = {
426
+ "keyring": "🔐 System Keyring (Secure)",
427
+ "encrypted_file": "🔒 Encrypted File (Fallback)"
428
+ }.get(storage_method, storage_method)
429
+ console.print(f"\n[dim]Storage: {storage_display}[/dim]\n")
430
+
204
431
  for cfg in api_configs:
205
432
  default_marker = " [bold green](default)[/bold green]" if cfg.default else ""
206
433
  console.print(f"• {cfg.name}{default_marker}")
@@ -6,6 +6,8 @@ from pathlib import Path
6
6
  from typing import Dict, Optional, List
7
7
  from pydantic import BaseModel, Field
8
8
 
9
+ from claude_dev_cli.secure_storage import SecureStorage
10
+
9
11
 
10
12
  class APIConfig(BaseModel):
11
13
  """Configuration for a Claude API key."""
@@ -39,6 +41,12 @@ class Config:
39
41
 
40
42
  self._ensure_config_dir()
41
43
  self._data: Dict = self._load_config()
44
+
45
+ # Initialize secure storage
46
+ self.secure_storage = SecureStorage(self.config_dir)
47
+
48
+ # Auto-migrate if plaintext keys exist
49
+ self._auto_migrate_keys()
42
50
 
43
51
  def _ensure_config_dir(self) -> None:
44
52
  """Ensure configuration directory exists."""
@@ -68,6 +76,22 @@ class Config:
68
76
  with open(self.config_file, 'w') as f:
69
77
  json.dump(data, f, indent=2)
70
78
 
79
+ def _auto_migrate_keys(self) -> None:
80
+ """Automatically migrate plaintext API keys to secure storage."""
81
+ api_configs = self._data.get("api_configs", [])
82
+ migrated = False
83
+
84
+ for config in api_configs:
85
+ if "api_key" in config and config["api_key"]:
86
+ # Migrate this key to secure storage
87
+ self.secure_storage.store_key(config["name"], config["api_key"])
88
+ # Remove from plaintext config
89
+ config["api_key"] = "" # Empty string indicates key is in secure storage
90
+ migrated = True
91
+
92
+ if migrated:
93
+ self._save_config()
94
+
71
95
  def add_api_config(
72
96
  self,
73
97
  name: str,
@@ -91,14 +115,18 @@ class Config:
91
115
  if config["name"] == name:
92
116
  raise ValueError(f"API config with name '{name}' already exists")
93
117
 
118
+ # Store API key in secure storage
119
+ self.secure_storage.store_key(name, api_key)
120
+
94
121
  # If this is the first config or make_default is True, set as default
95
122
  if make_default or not api_configs:
96
123
  for config in api_configs:
97
124
  config["default"] = False
98
125
 
126
+ # Store metadata without the actual key (empty string indicates secure storage)
99
127
  api_config = APIConfig(
100
128
  name=name,
101
- api_key=api_key,
129
+ api_key="", # Empty string indicates key is in secure storage
102
130
  description=description,
103
131
  default=make_default or not api_configs
104
132
  )
@@ -111,21 +139,53 @@ class Config:
111
139
  """Get API configuration by name or default."""
112
140
  api_configs = self._data.get("api_configs", [])
113
141
 
142
+ config_data = None
114
143
  if name:
115
144
  for config in api_configs:
116
145
  if config["name"] == name:
117
- return APIConfig(**config)
146
+ config_data = config
147
+ break
118
148
  else:
119
149
  # Return default
120
150
  for config in api_configs:
121
151
  if config.get("default", False):
122
- return APIConfig(**config)
152
+ config_data = config
153
+ break
123
154
 
124
- return None
155
+ if not config_data:
156
+ return None
157
+
158
+ # Retrieve actual API key from secure storage
159
+ api_key = self.secure_storage.get_key(config_data["name"])
160
+ if not api_key:
161
+ # Fallback to plaintext if not in secure storage (shouldn't happen after migration)
162
+ api_key = config_data.get("api_key", "")
163
+
164
+ # Return config with actual key
165
+ return APIConfig(
166
+ name=config_data["name"],
167
+ api_key=api_key,
168
+ description=config_data.get("description"),
169
+ default=config_data.get("default", False)
170
+ )
125
171
 
126
172
  def list_api_configs(self) -> List[APIConfig]:
127
173
  """List all API configurations."""
128
- return [APIConfig(**c) for c in self._data.get("api_configs", [])]
174
+ configs = []
175
+ for c in self._data.get("api_configs", []):
176
+ # Retrieve actual API key from secure storage
177
+ api_key = self.secure_storage.get_key(c["name"])
178
+ if not api_key:
179
+ # Fallback to plaintext
180
+ api_key = c.get("api_key", "")
181
+
182
+ configs.append(APIConfig(
183
+ name=c["name"],
184
+ api_key=api_key,
185
+ description=c.get("description"),
186
+ default=c.get("default", False)
187
+ ))
188
+ return configs
129
189
 
130
190
  def add_project_profile(
131
191
  self,