Skip to content

Commit 36c93c2

Browse files
bdqnghiclaude
andcommitted
Fix issues #44, #34, #17, #43: OpenAI compat, Anthropic validation, keyring fallback, verbose logging
- #44: Use max_completion_tokens for newer OpenAI models (o1, o3, gpt-4o) that reject the deprecated max_tokens parameter - #34: Detect Anthropic API URLs and use the anthropic SDK for connectivity tests instead of forcing OpenAI client on all providers - #17: Add file-based fallback (credentials.json) when system keyring is unavailable (headless containers, RHEL). Support CODEWIKI_NO_KEYRING=1 env var to force file-based storage - #43: Add file-level and module-level verbose logging during dependency analysis, clustering, and doc generation phases Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c46a64b commit 36c93c2

4 files changed

Lines changed: 180 additions & 56 deletions

File tree

codewiki/cli/adapters/doc_generator.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,14 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
186186
components, leaf_nodes = doc_generator.graph_builder.build_dependency_graph()
187187
self.job.statistics.total_files_analyzed = len(components)
188188
self.job.statistics.leaf_nodes = len(leaf_nodes)
189-
189+
190190
if self.verbose:
191-
self.progress_tracker.update_stage(1.0, f"Found {len(leaf_nodes)} leaf nodes")
191+
self.progress_tracker.update_stage(0.8, f"Analyzed {len(components)} files, found {len(leaf_nodes)} leaf nodes")
192+
# Log individual files analyzed
193+
for comp_name in sorted(components.keys())[:20]:
194+
self.progress_tracker.update_stage(0.9, f" File: {comp_name}")
195+
if len(components) > 20:
196+
self.progress_tracker.update_stage(0.9, f" ... and {len(components) - 20} more files")
192197
except Exception as e:
193198
raise APIError(f"Dependency analysis failed: {e}")
194199

@@ -212,15 +217,22 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
212217
try:
213218
if os.path.exists(first_module_tree_path):
214219
module_tree = file_manager.load_json(first_module_tree_path)
220+
if self.verbose:
221+
self.progress_tracker.update_stage(0.5, "Loaded cached module tree")
215222
else:
223+
if self.verbose:
224+
self.progress_tracker.update_stage(0.3, f"Clustering {len(leaf_nodes)} leaf nodes with LLM...")
216225
module_tree = cluster_modules(leaf_nodes, components, backend_config)
217226
file_manager.save_json(module_tree, first_module_tree_path)
218-
227+
219228
file_manager.save_json(module_tree, module_tree_path)
220229
self.job.module_count = len(module_tree)
221-
230+
222231
if self.verbose:
223232
self.progress_tracker.update_stage(1.0, f"Created {len(module_tree)} modules")
233+
for mod_name in sorted(module_tree.keys()):
234+
file_count = len(module_tree[mod_name]) if isinstance(module_tree[mod_name], list) else "?"
235+
self.progress_tracker.update_stage(1.0, f" Module: {mod_name} ({file_count} files)")
224236
except Exception as e:
225237
raise APIError(f"Module clustering failed: {e}")
226238

@@ -232,9 +244,12 @@ async def _run_backend_generation(self, backend_config: BackendConfig):
232244
self.progress_tracker.update_stage(0.1, "Generating module documentation...")
233245

234246
try:
247+
if self.verbose:
248+
self.progress_tracker.update_stage(0.2, f"Generating documentation for {self.job.module_count} modules...")
249+
235250
# Run the actual documentation generation
236251
await doc_generator.generate_module_documentation(components, leaf_nodes)
237-
252+
238253
if self.verbose:
239254
self.progress_tracker.update_stage(0.9, "Creating repository overview...")
240255

codewiki/cli/commands/config.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,13 +490,32 @@ def config_validate(quick: bool, verbose: bool):
490490

491491
# Step 5: API connectivity test (unless --quick)
492492
if not quick:
493+
if verbose:
494+
click.echo()
495+
click.echo("[5/5] Testing API connectivity...")
496+
click.echo(f" URL: {config.base_url}")
497+
493498
try:
494-
from openai import OpenAI
495-
client = OpenAI(api_key=api_key, base_url=config.base_url)
496-
response = client.models.list()
497-
click.secho("✓ API connectivity test successful", fg="green")
499+
base_url_lower = (config.base_url or "").lower()
500+
if "api.anthropic.com" in base_url_lower:
501+
# Use Anthropic SDK for native Anthropic endpoints
502+
import anthropic
503+
client = anthropic.Anthropic(api_key=api_key)
504+
client.models.list(limit=1)
505+
else:
506+
# Use OpenAI SDK for OpenAI-compatible endpoints
507+
from openai import OpenAI
508+
client = OpenAI(api_key=api_key, base_url=config.base_url)
509+
client.models.list()
510+
511+
if verbose:
512+
click.secho(" ✓ API responded successfully", fg="green")
513+
else:
514+
click.secho("✓ API connectivity test successful", fg="green")
498515
except Exception as e:
499516
click.secho("✗ API connectivity test failed", fg="red")
517+
if verbose:
518+
click.echo(f" Error: {e}")
500519
sys.exit(EXIT_CONFIG_ERROR)
501520

502521
# Success

codewiki/cli/config_manager.py

Lines changed: 88 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
"""
22
Configuration manager with keyring integration for secure credential storage.
3+
4+
Supports fallback to file-based storage when system keyring is unavailable
5+
(e.g. headless containers, RHEL without Secret Service). Set the environment
6+
variable CODEWIKI_NO_KEYRING=1 to force file-based storage.
37
"""
48

59
import json
10+
import os
11+
import logging
612
from pathlib import Path
713
from typing import Optional
814
import keyring
@@ -12,6 +18,7 @@
1218
from codewiki.cli.utils.errors import ConfigurationError, FileSystemError
1319
from codewiki.cli.utils.fs import ensure_directory, safe_write, safe_read
1420

21+
logger = logging.getLogger(__name__)
1522

1623
# Keyring configuration
1724
KEYRING_SERVICE = "codewiki"
@@ -20,33 +27,63 @@
2027
# Configuration file location
2128
CONFIG_DIR = Path.home() / ".codewiki"
2229
CONFIG_FILE = CONFIG_DIR / "config.json"
30+
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
2331
CONFIG_VERSION = "1.0"
2432

2533

2634
class ConfigManager:
2735
"""
2836
Manages CodeWiki configuration with secure keyring storage for API keys.
29-
37+
3038
Storage:
31-
- API key: System keychain via keyring (macOS Keychain, Windows Credential Manager,
39+
- API key: System keychain via keyring (macOS Keychain, Windows Credential Manager,
3240
Linux Secret Service)
41+
- Fallback: ~/.codewiki/credentials.json when keyring is unavailable
3342
- Other settings: ~/.codewiki/config.json
43+
44+
Set CODEWIKI_NO_KEYRING=1 to skip keyring and use file-based storage.
3445
"""
35-
46+
3647
def __init__(self):
3748
"""Initialize the configuration manager."""
3849
self._api_key: Optional[str] = None
3950
self._config: Optional[Configuration] = None
51+
self._force_no_keyring = os.environ.get("CODEWIKI_NO_KEYRING", "").strip() in ("1", "true", "yes")
4052
self._keyring_available = self._check_keyring_available()
41-
53+
4254
def _check_keyring_available(self) -> bool:
4355
"""Check if system keyring is available."""
56+
if self._force_no_keyring:
57+
logger.debug("Keyring disabled via CODEWIKI_NO_KEYRING")
58+
return False
4459
try:
4560
# Try to get/set a test value
4661
keyring.get_password(KEYRING_SERVICE, "__test__")
4762
return True
48-
except KeyringError:
63+
except (KeyringError, Exception):
4964
return False
65+
66+
def _load_api_key_from_file(self) -> Optional[str]:
67+
"""Load API key from fallback credentials file."""
68+
if not CREDENTIALS_FILE.exists():
69+
return None
70+
try:
71+
content = safe_read(CREDENTIALS_FILE)
72+
data = json.loads(content)
73+
return data.get("api_key")
74+
except (json.JSONDecodeError, FileSystemError):
75+
return None
76+
77+
def _save_api_key_to_file(self, api_key: str):
78+
"""Save API key to fallback credentials file (plaintext)."""
79+
ensure_directory(CONFIG_DIR)
80+
data = {"api_key": api_key}
81+
safe_write(CREDENTIALS_FILE, json.dumps(data, indent=2))
82+
# Restrict file permissions (owner read/write only)
83+
try:
84+
CREDENTIALS_FILE.chmod(0o600)
85+
except OSError:
86+
pass
5087

5188
def load(self) -> bool:
5289
"""
@@ -70,12 +107,14 @@ def load(self) -> bool:
70107

71108
self._config = Configuration.from_dict(data)
72109

73-
# Load API key from keyring
74-
try:
75-
self._api_key = keyring.get_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
76-
except KeyringError:
77-
# Keyring unavailable, API key will be None
78-
pass
110+
# Load API key from keyring, falling back to file
111+
if self._keyring_available:
112+
try:
113+
self._api_key = keyring.get_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
114+
except (KeyringError, Exception):
115+
pass
116+
if self._api_key is None:
117+
self._api_key = self._load_api_key_from_file()
79118

80119
return True
81120
except (json.JSONDecodeError, FileSystemError) as e:
@@ -154,17 +193,23 @@ def save(
154193
if self._config.base_url and self._config.main_model and self._config.cluster_model:
155194
self._config.validate()
156195

157-
# Save API key to keyring
196+
# Save API key to keyring, falling back to file
158197
if api_key is not None:
159198
self._api_key = api_key
160-
try:
161-
keyring.set_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT, api_key)
162-
except KeyringError as e:
163-
# Fallback: warn about keyring unavailability
164-
raise ConfigurationError(
165-
f"System keychain unavailable: {e}\n"
166-
f"Please ensure your system keychain is properly configured."
167-
)
199+
if self._keyring_available:
200+
try:
201+
keyring.set_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT, api_key)
202+
except (KeyringError, Exception):
203+
# Keyring failed at runtime — fall back to file
204+
self._keyring_available = False
205+
self._save_api_key_to_file(api_key)
206+
logger.warning(
207+
"System keychain unavailable. API key stored in %s "
208+
"(plaintext). Set CODEWIKI_NO_KEYRING=1 to suppress this warning.",
209+
CREDENTIALS_FILE
210+
)
211+
else:
212+
self._save_api_key_to_file(api_key)
168213

169214
# Save non-sensitive config to JSON
170215
config_data = {
@@ -179,17 +224,20 @@ def save(
179224

180225
def get_api_key(self) -> Optional[str]:
181226
"""
182-
Get API key from keyring.
183-
227+
Get API key from keyring or fallback file.
228+
184229
Returns:
185230
API key or None if not set
186231
"""
187232
if self._api_key is None:
188-
try:
189-
self._api_key = keyring.get_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
190-
except KeyringError:
191-
pass
192-
233+
if self._keyring_available:
234+
try:
235+
self._api_key = keyring.get_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
236+
except (KeyringError, Exception):
237+
pass
238+
if self._api_key is None:
239+
self._api_key = self._load_api_key_from_file()
240+
193241
return self._api_key
194242

195243
def get_config(self) -> Optional[Configuration]:
@@ -219,12 +267,19 @@ def is_configured(self) -> bool:
219267
return self._config.is_complete()
220268

221269
def delete_api_key(self):
222-
"""Delete API key from keyring."""
223-
try:
224-
keyring.delete_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
225-
self._api_key = None
226-
except KeyringError:
227-
pass
270+
"""Delete API key from keyring and fallback file."""
271+
if self._keyring_available:
272+
try:
273+
keyring.delete_password(KEYRING_SERVICE, KEYRING_API_KEY_ACCOUNT)
274+
except (KeyringError, Exception):
275+
pass
276+
# Also remove fallback credentials file
277+
if CREDENTIALS_FILE.exists():
278+
try:
279+
CREDENTIALS_FILE.unlink()
280+
except OSError:
281+
pass
282+
self._api_key = None
228283

229284
def clear(self):
230285
"""Clear all configuration (file and keyring)."""

0 commit comments

Comments
 (0)