wikigen 1.0.0__py3-none-any.whl
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.
- wikigen/__init__.py +7 -0
- wikigen/cli.py +690 -0
- wikigen/config.py +526 -0
- wikigen/defaults.py +78 -0
- wikigen/flows/__init__.py +1 -0
- wikigen/flows/flow.py +38 -0
- wikigen/formatter/help_formatter.py +194 -0
- wikigen/formatter/init_formatter.py +56 -0
- wikigen/formatter/output_formatter.py +290 -0
- wikigen/mcp/__init__.py +12 -0
- wikigen/mcp/chunking.py +127 -0
- wikigen/mcp/embeddings.py +69 -0
- wikigen/mcp/output_resources.py +65 -0
- wikigen/mcp/search_index.py +826 -0
- wikigen/mcp/server.py +232 -0
- wikigen/mcp/vector_index.py +297 -0
- wikigen/metadata/__init__.py +35 -0
- wikigen/metadata/logo.py +28 -0
- wikigen/metadata/project.py +28 -0
- wikigen/metadata/version.py +17 -0
- wikigen/nodes/__init__.py +1 -0
- wikigen/nodes/nodes.py +1080 -0
- wikigen/utils/__init__.py +0 -0
- wikigen/utils/adjust_headings.py +72 -0
- wikigen/utils/call_llm.py +271 -0
- wikigen/utils/crawl_github_files.py +450 -0
- wikigen/utils/crawl_local_files.py +151 -0
- wikigen/utils/llm_providers.py +101 -0
- wikigen/utils/version_check.py +84 -0
- wikigen-1.0.0.dist-info/METADATA +352 -0
- wikigen-1.0.0.dist-info/RECORD +35 -0
- wikigen-1.0.0.dist-info/WHEEL +5 -0
- wikigen-1.0.0.dist-info/entry_points.txt +2 -0
- wikigen-1.0.0.dist-info/licenses/LICENSE +21 -0
- wikigen-1.0.0.dist-info/top_level.txt +1 -0
wikigen/config.py
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for WikiGen.
|
|
3
|
+
Handles loading, saving, and merging configuration with CLI arguments.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, Optional
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import keyring
|
|
15
|
+
|
|
16
|
+
KEYRING_AVAILABLE = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
KEYRING_AVAILABLE = False
|
|
19
|
+
|
|
20
|
+
from .defaults import DEFAULT_CONFIG
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_platform_config_base() -> Path:
|
|
24
|
+
"""
|
|
25
|
+
Return the OS-appropriate user config base directory.
|
|
26
|
+
|
|
27
|
+
- macOS: ~/Library/Application Support
|
|
28
|
+
- Windows: %APPDATA%
|
|
29
|
+
- Linux/other: $XDG_CONFIG_HOME or ~/.config
|
|
30
|
+
"""
|
|
31
|
+
home = Path.home()
|
|
32
|
+
if sys.platform.startswith("win"):
|
|
33
|
+
appdata = os.environ.get("APPDATA")
|
|
34
|
+
return Path(appdata) if appdata else home / "AppData" / "Roaming"
|
|
35
|
+
elif sys.platform == "darwin":
|
|
36
|
+
# Follow XDG-style on macOS per project preference
|
|
37
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
38
|
+
return Path(xdg) if xdg else home / ".config"
|
|
39
|
+
else:
|
|
40
|
+
xdg = os.environ.get("XDG_CONFIG_HOME")
|
|
41
|
+
return Path(xdg) if xdg else home / ".config"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_new_config_dir() -> Path:
|
|
45
|
+
"""Return the new config directory for wikigen under the platform base."""
|
|
46
|
+
return _get_platform_config_base() / "wikigen"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_legacy_config_dir() -> Path:
|
|
50
|
+
"""Return the previous Documents-based config directory (for migration)."""
|
|
51
|
+
return Path.home() / "Documents" / "WikiGen" / ".salt"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Configuration paths
|
|
55
|
+
CONFIG_DIR = _get_new_config_dir()
|
|
56
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
57
|
+
DEFAULT_OUTPUT_DIR = Path.home() / "Documents" / "WikiGen"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _migrate_legacy_config_if_needed() -> None:
|
|
61
|
+
"""
|
|
62
|
+
If a legacy config exists in the old Documents path and the new config
|
|
63
|
+
doesn't exist yet, migrate the file and directory.
|
|
64
|
+
"""
|
|
65
|
+
legacy_dir = _get_legacy_config_dir()
|
|
66
|
+
legacy_file = legacy_dir / "config.json"
|
|
67
|
+
if CONFIG_FILE.exists():
|
|
68
|
+
return
|
|
69
|
+
if legacy_file.exists():
|
|
70
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
try:
|
|
72
|
+
# Copy then remove legacy to be safe
|
|
73
|
+
with open(legacy_file, "r", encoding="utf-8") as f:
|
|
74
|
+
data = f.read()
|
|
75
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
76
|
+
f.write(data)
|
|
77
|
+
# best-effort cleanup of empty legacy dir
|
|
78
|
+
try:
|
|
79
|
+
legacy_file.unlink(missing_ok=True)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
try:
|
|
83
|
+
# remove legacy dir if empty
|
|
84
|
+
legacy_dir.rmdir()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
except Exception:
|
|
88
|
+
# Ignore migration errors — user can re-init
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def init_config() -> None:
|
|
93
|
+
"""Interactive setup wizard for init command."""
|
|
94
|
+
import getpass
|
|
95
|
+
|
|
96
|
+
from .formatter.init_formatter import (
|
|
97
|
+
print_init_header,
|
|
98
|
+
print_section_start,
|
|
99
|
+
print_input_prompt,
|
|
100
|
+
print_init_complete,
|
|
101
|
+
)
|
|
102
|
+
from .formatter.output_formatter import Colors, Icons, Tree
|
|
103
|
+
from .utils.llm_providers import (
|
|
104
|
+
get_provider_list,
|
|
105
|
+
get_display_name,
|
|
106
|
+
get_recommended_models,
|
|
107
|
+
get_provider_info,
|
|
108
|
+
requires_api_key,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Create directories
|
|
112
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
DEFAULT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
|
|
115
|
+
print_init_header()
|
|
116
|
+
|
|
117
|
+
# LLM Provider Selection section
|
|
118
|
+
print_section_start("LLM Provider", Icons.INFO)
|
|
119
|
+
|
|
120
|
+
# Show provider list
|
|
121
|
+
providers = get_provider_list()
|
|
122
|
+
print(
|
|
123
|
+
f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.MIDDLE} "
|
|
124
|
+
f"{Colors.MEDIUM_GRAY}Available providers:{Colors.RESET}"
|
|
125
|
+
)
|
|
126
|
+
for i, provider_id in enumerate(providers, 1):
|
|
127
|
+
display_name = get_display_name(provider_id)
|
|
128
|
+
print(
|
|
129
|
+
f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.VERTICAL} "
|
|
130
|
+
f"{Colors.MEDIUM_GRAY}{i}) {display_name}{Colors.RESET}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Provider selection
|
|
134
|
+
print_input_prompt(
|
|
135
|
+
"Select LLM provider (enter number)", Icons.ANALYZING, is_required=True
|
|
136
|
+
)
|
|
137
|
+
provider_choice = input().strip()
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
provider_index = int(provider_choice) - 1
|
|
141
|
+
if provider_index < 0 or provider_index >= len(providers):
|
|
142
|
+
print(f"✘ Invalid provider selection: {provider_choice}")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
llm_provider = providers[provider_index]
|
|
145
|
+
except ValueError:
|
|
146
|
+
print(f"✘ Invalid provider selection: {provider_choice}")
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
provider_info = get_provider_info(llm_provider)
|
|
150
|
+
provider_display = get_display_name(llm_provider)
|
|
151
|
+
|
|
152
|
+
# Model Selection
|
|
153
|
+
print_section_start("Model Selection", Icons.INFO)
|
|
154
|
+
|
|
155
|
+
# Show recommended models
|
|
156
|
+
recommended_models = get_recommended_models(llm_provider)
|
|
157
|
+
print(
|
|
158
|
+
f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.MIDDLE} "
|
|
159
|
+
f"{Colors.MEDIUM_GRAY}Recommended models for {provider_display}:{Colors.RESET}"
|
|
160
|
+
)
|
|
161
|
+
for i, model in enumerate(recommended_models, 1):
|
|
162
|
+
print(
|
|
163
|
+
f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.VERTICAL} "
|
|
164
|
+
f"{Colors.MEDIUM_GRAY}{i}) {model}{Colors.RESET}"
|
|
165
|
+
)
|
|
166
|
+
print(
|
|
167
|
+
f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.VERTICAL} "
|
|
168
|
+
f"{Colors.MEDIUM_GRAY}{len(recommended_models) + 1}) Enter custom model name{Colors.RESET}"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
print_input_prompt(
|
|
172
|
+
f"Select model for {provider_display} (enter number or custom name)",
|
|
173
|
+
Icons.ANALYZING,
|
|
174
|
+
is_required=True,
|
|
175
|
+
)
|
|
176
|
+
model_choice = input().strip()
|
|
177
|
+
|
|
178
|
+
# Parse model selection
|
|
179
|
+
try:
|
|
180
|
+
model_index = int(model_choice) - 1
|
|
181
|
+
if model_index == len(recommended_models):
|
|
182
|
+
# Custom model
|
|
183
|
+
print_input_prompt(
|
|
184
|
+
"Enter custom model name", Icons.CONFIG, is_required=True
|
|
185
|
+
)
|
|
186
|
+
llm_model = input().strip()
|
|
187
|
+
if not llm_model:
|
|
188
|
+
print("✘ Model name cannot be empty!")
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
elif 0 <= model_index < len(recommended_models):
|
|
191
|
+
llm_model = recommended_models[model_index]
|
|
192
|
+
else:
|
|
193
|
+
print(f"✘ Invalid model selection: {model_choice}")
|
|
194
|
+
sys.exit(1)
|
|
195
|
+
except ValueError:
|
|
196
|
+
# Custom model name entered directly
|
|
197
|
+
llm_model = model_choice
|
|
198
|
+
|
|
199
|
+
# API Keys section
|
|
200
|
+
print_section_start("API Keys", Icons.INFO)
|
|
201
|
+
|
|
202
|
+
# Get API key if required
|
|
203
|
+
api_key = None
|
|
204
|
+
custom_url = None
|
|
205
|
+
if requires_api_key(llm_provider):
|
|
206
|
+
env_var = provider_info.get("api_key_env")
|
|
207
|
+
key_name = env_var or f"{provider_display} API Key"
|
|
208
|
+
|
|
209
|
+
print_input_prompt(key_name, Icons.ANALYZING, is_required=True)
|
|
210
|
+
api_key = getpass.getpass().strip()
|
|
211
|
+
if not api_key:
|
|
212
|
+
print(f"✘ {key_name} is required!")
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
else:
|
|
215
|
+
# Ollama - just show base URL
|
|
216
|
+
base_url = provider_info.get("base_url", "http://localhost:11434")
|
|
217
|
+
print_input_prompt(
|
|
218
|
+
"Ollama Base URL", Icons.CONFIG, is_required=False, default_value=base_url
|
|
219
|
+
)
|
|
220
|
+
custom_url = input().strip()
|
|
221
|
+
# For Ollama, use default if empty
|
|
222
|
+
if not custom_url:
|
|
223
|
+
custom_url = base_url
|
|
224
|
+
|
|
225
|
+
# GitHub Token
|
|
226
|
+
print_input_prompt(
|
|
227
|
+
"GitHub Token", Icons.ANALYZING, is_required=False, default_value="skip"
|
|
228
|
+
)
|
|
229
|
+
github_token = input().strip()
|
|
230
|
+
|
|
231
|
+
# Store in keyring if available, otherwise save to config
|
|
232
|
+
keyring_available = KEYRING_AVAILABLE
|
|
233
|
+
if keyring_available:
|
|
234
|
+
try:
|
|
235
|
+
if api_key:
|
|
236
|
+
keyring.set_password("wikigen", provider_info["keyring_key"], api_key)
|
|
237
|
+
if github_token:
|
|
238
|
+
keyring.set_password("wikigen", "github_token", github_token)
|
|
239
|
+
except (OSError, RuntimeError, AttributeError):
|
|
240
|
+
keyring_available = False
|
|
241
|
+
|
|
242
|
+
# Preferences section
|
|
243
|
+
print_section_start("Preferences", Icons.INFO)
|
|
244
|
+
|
|
245
|
+
# Output Directory
|
|
246
|
+
print_input_prompt(
|
|
247
|
+
"Output Directory",
|
|
248
|
+
Icons.CONFIG,
|
|
249
|
+
is_required=False,
|
|
250
|
+
default_value=str(DEFAULT_OUTPUT_DIR),
|
|
251
|
+
)
|
|
252
|
+
output_dir = input().strip()
|
|
253
|
+
if not output_dir:
|
|
254
|
+
output_dir = str(DEFAULT_OUTPUT_DIR)
|
|
255
|
+
|
|
256
|
+
# Language
|
|
257
|
+
print_input_prompt(
|
|
258
|
+
"Language", Icons.CONFIG, is_required=False, default_value="english"
|
|
259
|
+
)
|
|
260
|
+
language = input().strip()
|
|
261
|
+
if not language:
|
|
262
|
+
language = "english"
|
|
263
|
+
|
|
264
|
+
# Max Abstractions
|
|
265
|
+
print_input_prompt(
|
|
266
|
+
"Max Abstractions", Icons.CONFIG, is_required=False, default_value="5"
|
|
267
|
+
)
|
|
268
|
+
max_abstractions_input = input().strip()
|
|
269
|
+
if not max_abstractions_input:
|
|
270
|
+
max_abstractions = 5
|
|
271
|
+
else:
|
|
272
|
+
try:
|
|
273
|
+
max_abstractions = int(max_abstractions_input)
|
|
274
|
+
except ValueError:
|
|
275
|
+
max_abstractions = 5
|
|
276
|
+
|
|
277
|
+
# Documentation Mode
|
|
278
|
+
print_input_prompt(
|
|
279
|
+
"Documentation Mode (minimal/comprehensive)",
|
|
280
|
+
Icons.CONFIG,
|
|
281
|
+
is_required=False,
|
|
282
|
+
default_value="minimal",
|
|
283
|
+
)
|
|
284
|
+
documentation_mode_input = input().strip().lower()
|
|
285
|
+
if not documentation_mode_input:
|
|
286
|
+
documentation_mode = "minimal"
|
|
287
|
+
elif documentation_mode_input in ["minimal", "comprehensive"]:
|
|
288
|
+
documentation_mode = documentation_mode_input
|
|
289
|
+
else:
|
|
290
|
+
print(f"⚠ Warning: Invalid mode '{documentation_mode_input}'. Using 'minimal'.")
|
|
291
|
+
documentation_mode = "minimal"
|
|
292
|
+
|
|
293
|
+
# Build configuration
|
|
294
|
+
config = {
|
|
295
|
+
"llm_provider": llm_provider,
|
|
296
|
+
"llm_model": llm_model,
|
|
297
|
+
"output_dir": output_dir,
|
|
298
|
+
"language": language,
|
|
299
|
+
"max_abstractions": max_abstractions,
|
|
300
|
+
"max_file_size": DEFAULT_CONFIG["max_file_size"],
|
|
301
|
+
"use_cache": DEFAULT_CONFIG["use_cache"],
|
|
302
|
+
"include_patterns": DEFAULT_CONFIG["include_patterns"],
|
|
303
|
+
"exclude_patterns": DEFAULT_CONFIG["exclude_patterns"],
|
|
304
|
+
"documentation_mode": documentation_mode,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Store Ollama base URL if custom
|
|
308
|
+
if llm_provider == "ollama" and custom_url and custom_url != base_url:
|
|
309
|
+
config["ollama_base_url"] = custom_url
|
|
310
|
+
|
|
311
|
+
# Add API keys to config if keyring not available
|
|
312
|
+
if not keyring_available:
|
|
313
|
+
if api_key:
|
|
314
|
+
config[provider_info["keyring_key"]] = api_key
|
|
315
|
+
if github_token:
|
|
316
|
+
config["github_token"] = github_token
|
|
317
|
+
|
|
318
|
+
# Save configuration
|
|
319
|
+
save_config(config)
|
|
320
|
+
|
|
321
|
+
# Print completion message
|
|
322
|
+
print_init_complete(CONFIG_FILE, output_dir, keyring_available)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def load_config() -> Dict[str, Any]:
|
|
326
|
+
"""Load configuration from file and keyring."""
|
|
327
|
+
config = DEFAULT_CONFIG.copy()
|
|
328
|
+
|
|
329
|
+
# Attempt migration from legacy location
|
|
330
|
+
_migrate_legacy_config_if_needed()
|
|
331
|
+
|
|
332
|
+
# Load from file if it exists
|
|
333
|
+
if CONFIG_FILE.exists():
|
|
334
|
+
try:
|
|
335
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
336
|
+
file_config = json.load(f)
|
|
337
|
+
config.update(file_config)
|
|
338
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
339
|
+
print(f"⚠ Warning: Could not load config file: {e}")
|
|
340
|
+
|
|
341
|
+
# Load API keys from keyring if available
|
|
342
|
+
# Load all provider API keys dynamically
|
|
343
|
+
if KEYRING_AVAILABLE:
|
|
344
|
+
try:
|
|
345
|
+
from .utils.llm_providers import LLM_PROVIDERS
|
|
346
|
+
|
|
347
|
+
for provider_id, provider_info in LLM_PROVIDERS.items():
|
|
348
|
+
keyring_key = provider_info.get("keyring_key")
|
|
349
|
+
if keyring_key:
|
|
350
|
+
api_key = keyring.get_password("wikigen", keyring_key)
|
|
351
|
+
if api_key:
|
|
352
|
+
config[keyring_key] = api_key
|
|
353
|
+
|
|
354
|
+
github_token = keyring.get_password("wikigen", "github_token")
|
|
355
|
+
if github_token:
|
|
356
|
+
config["github_token"] = github_token
|
|
357
|
+
except (OSError, RuntimeError, AttributeError) as e:
|
|
358
|
+
print(f"⚠ Warning: Could not load from keyring: {e}")
|
|
359
|
+
|
|
360
|
+
return config
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def save_config(config: Dict[str, Any]) -> None:
|
|
364
|
+
"""Save configuration to file."""
|
|
365
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
|
|
367
|
+
# Don't save API keys to file if keyring is available
|
|
368
|
+
config_to_save = config.copy()
|
|
369
|
+
if KEYRING_AVAILABLE:
|
|
370
|
+
# Remove all provider API keys from config file
|
|
371
|
+
from .utils.llm_providers import LLM_PROVIDERS
|
|
372
|
+
|
|
373
|
+
for provider_info in LLM_PROVIDERS.values():
|
|
374
|
+
keyring_key = provider_info.get("keyring_key")
|
|
375
|
+
if keyring_key:
|
|
376
|
+
config_to_save.pop(keyring_key, None)
|
|
377
|
+
config_to_save.pop("github_token", None)
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
381
|
+
json.dump(config_to_save, f, indent=2)
|
|
382
|
+
except IOError as e:
|
|
383
|
+
print(f"✘ Error saving config: {e}")
|
|
384
|
+
sys.exit(1)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def merge_config_with_args(config: Dict[str, Any], args) -> Dict[str, Any]:
|
|
388
|
+
"""Merge configuration with CLI arguments (CLI takes precedence)."""
|
|
389
|
+
merged = config.copy()
|
|
390
|
+
|
|
391
|
+
# Map argparse attributes to config keys
|
|
392
|
+
arg_mapping = {
|
|
393
|
+
"output": "output_dir",
|
|
394
|
+
"language": "language",
|
|
395
|
+
"max_abstractions": "max_abstractions",
|
|
396
|
+
"max_size": "max_file_size",
|
|
397
|
+
"no_cache": "use_cache", # Note: inverted logic
|
|
398
|
+
"include": "include_patterns",
|
|
399
|
+
"exclude": "exclude_patterns",
|
|
400
|
+
"token": "github_token",
|
|
401
|
+
"mode": "documentation_mode",
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for arg_name, config_key in arg_mapping.items():
|
|
405
|
+
if hasattr(args, arg_name):
|
|
406
|
+
value = getattr(args, arg_name)
|
|
407
|
+
if arg_name == "no_cache":
|
|
408
|
+
# For store_true flags, only override if explicitly provided (True)
|
|
409
|
+
# Invert the logic: no_cache=True means use_cache=False
|
|
410
|
+
if value is True:
|
|
411
|
+
merged[config_key] = False
|
|
412
|
+
elif value is not None:
|
|
413
|
+
if arg_name in ["include", "exclude"]:
|
|
414
|
+
# Convert to list if it's a set
|
|
415
|
+
if isinstance(value, set):
|
|
416
|
+
merged[config_key] = list(value)
|
|
417
|
+
else:
|
|
418
|
+
merged[config_key] = value
|
|
419
|
+
else:
|
|
420
|
+
merged[config_key] = value
|
|
421
|
+
|
|
422
|
+
return merged
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def check_config_exists() -> bool:
|
|
426
|
+
"""Check if configuration file exists."""
|
|
427
|
+
return CONFIG_FILE.exists()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def get_llm_provider() -> str:
|
|
431
|
+
"""Get LLM provider from config, defaulting to gemini."""
|
|
432
|
+
config = load_config()
|
|
433
|
+
return config.get("llm_provider", "gemini")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def get_llm_model() -> str:
|
|
437
|
+
"""Get LLM model from config, defaulting to gemini-2.5-flash."""
|
|
438
|
+
config = load_config()
|
|
439
|
+
return config.get("llm_model", "gemini-2.5-flash")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def get_api_key() -> Optional[str]:
|
|
443
|
+
"""Get API key from config or environment based on current provider."""
|
|
444
|
+
from .utils.llm_providers import get_provider_info, requires_api_key
|
|
445
|
+
|
|
446
|
+
config = load_config()
|
|
447
|
+
provider = get_llm_provider()
|
|
448
|
+
|
|
449
|
+
# Check if provider requires API key
|
|
450
|
+
if not requires_api_key(provider):
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
provider_info = get_provider_info(provider)
|
|
454
|
+
keyring_key = provider_info.get("keyring_key")
|
|
455
|
+
env_var = provider_info.get("api_key_env")
|
|
456
|
+
|
|
457
|
+
# Try keyring first, then env var
|
|
458
|
+
api_key = None
|
|
459
|
+
if keyring_key and KEYRING_AVAILABLE:
|
|
460
|
+
try:
|
|
461
|
+
api_key = keyring.get_password("wikigen", keyring_key)
|
|
462
|
+
except (OSError, RuntimeError, AttributeError):
|
|
463
|
+
pass
|
|
464
|
+
|
|
465
|
+
if not api_key:
|
|
466
|
+
# Try config file fallback
|
|
467
|
+
api_key = config.get(keyring_key or "")
|
|
468
|
+
|
|
469
|
+
if not api_key and env_var:
|
|
470
|
+
# Fallback to environment variable
|
|
471
|
+
api_key = os.getenv(env_var)
|
|
472
|
+
|
|
473
|
+
return api_key
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def get_github_token() -> Optional[str]:
|
|
477
|
+
"""Get GitHub token from config or environment."""
|
|
478
|
+
config = load_config()
|
|
479
|
+
return config.get("github_token") or os.getenv("GITHUB_TOKEN")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def should_check_for_updates() -> bool:
|
|
483
|
+
"""
|
|
484
|
+
Check if 24 hours have passed since last update check.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
True if update check should be performed, False otherwise
|
|
488
|
+
"""
|
|
489
|
+
config = load_config()
|
|
490
|
+
last_check = config.get("last_update_check")
|
|
491
|
+
|
|
492
|
+
# If never checked, return True
|
|
493
|
+
if last_check is None:
|
|
494
|
+
return True
|
|
495
|
+
|
|
496
|
+
# Check if 24 hours (86400 seconds) have passed
|
|
497
|
+
current_time = time.time()
|
|
498
|
+
time_since_last_check = current_time - last_check
|
|
499
|
+
|
|
500
|
+
return time_since_last_check >= 86400
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def update_last_check_timestamp() -> None:
|
|
504
|
+
"""Update the last update check timestamp to current time."""
|
|
505
|
+
config = load_config()
|
|
506
|
+
config["last_update_check"] = time.time()
|
|
507
|
+
save_config(config)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def get_output_dir() -> Path:
|
|
511
|
+
"""
|
|
512
|
+
Get the output directory from config or use default.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Path to the output directory
|
|
516
|
+
"""
|
|
517
|
+
try:
|
|
518
|
+
config = load_config()
|
|
519
|
+
output_dir_str = config.get("output_dir")
|
|
520
|
+
if output_dir_str:
|
|
521
|
+
return Path(output_dir_str).expanduser()
|
|
522
|
+
except Exception:
|
|
523
|
+
# If config loading fails, fallback to default
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
return DEFAULT_OUTPUT_DIR
|
wikigen/defaults.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default configuration values for WikiGen.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Default file patterns for inclusion
|
|
6
|
+
DEFAULT_INCLUDE_PATTERNS = {
|
|
7
|
+
"*.py",
|
|
8
|
+
"*.js",
|
|
9
|
+
"*.jsx",
|
|
10
|
+
"*.ts",
|
|
11
|
+
"*.tsx",
|
|
12
|
+
"*.go",
|
|
13
|
+
"*.java",
|
|
14
|
+
"*.pyi",
|
|
15
|
+
"*.pyx",
|
|
16
|
+
"*.c",
|
|
17
|
+
"*.cc",
|
|
18
|
+
"*.cpp",
|
|
19
|
+
"*.h",
|
|
20
|
+
"*.md",
|
|
21
|
+
"*.rst",
|
|
22
|
+
"*Dockerfile",
|
|
23
|
+
"*Makefile",
|
|
24
|
+
"*.yaml",
|
|
25
|
+
"*.yml",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Default file patterns for exclusion
|
|
29
|
+
DEFAULT_EXCLUDE_PATTERNS = {
|
|
30
|
+
"assets/*",
|
|
31
|
+
"data/*",
|
|
32
|
+
"images/*",
|
|
33
|
+
"public/*",
|
|
34
|
+
"static/*",
|
|
35
|
+
"temp/*",
|
|
36
|
+
"*docs/*",
|
|
37
|
+
"*venv/*",
|
|
38
|
+
"*.venv/*",
|
|
39
|
+
"*test*",
|
|
40
|
+
"*tests/*",
|
|
41
|
+
"*examples/*",
|
|
42
|
+
"v1/*",
|
|
43
|
+
"*dist/*",
|
|
44
|
+
"*build/*",
|
|
45
|
+
"*experimental/*",
|
|
46
|
+
"*deprecated/*",
|
|
47
|
+
"*misc/*",
|
|
48
|
+
"*legacy/*",
|
|
49
|
+
".git/*",
|
|
50
|
+
".github/*",
|
|
51
|
+
".next/*",
|
|
52
|
+
".vscode/*",
|
|
53
|
+
"*obj/*",
|
|
54
|
+
"*bin/*",
|
|
55
|
+
"*node_modules/*",
|
|
56
|
+
"*.log",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Default configuration values
|
|
60
|
+
DEFAULT_CONFIG = {
|
|
61
|
+
"output_dir": "~/Documents/WikiGen",
|
|
62
|
+
"language": "english",
|
|
63
|
+
"max_abstractions": 10,
|
|
64
|
+
"max_file_size": 100000,
|
|
65
|
+
"use_cache": True,
|
|
66
|
+
"include_patterns": list(DEFAULT_INCLUDE_PATTERNS),
|
|
67
|
+
"exclude_patterns": list(DEFAULT_EXCLUDE_PATTERNS),
|
|
68
|
+
"last_update_check": None, # Timestamp of last update check (None means never checked)
|
|
69
|
+
"llm_provider": "gemini",
|
|
70
|
+
"llm_model": "gemini-2.5-flash",
|
|
71
|
+
"documentation_mode": "minimal",
|
|
72
|
+
# Semantic search configuration
|
|
73
|
+
"semantic_search_enabled": True,
|
|
74
|
+
"chunk_size": 1000, # tokens (increased for better context)
|
|
75
|
+
"chunk_overlap": 200, # tokens (reduced overlap to avoid tiny fragments)
|
|
76
|
+
"embedding_model": "all-MiniLM-L6-v2", # lightweight, fast
|
|
77
|
+
"max_chunks_per_file": 5, # limit chunks returned per file
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Flows module for WikiGen."""
|
wikigen/flows/flow.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pocketflow import Flow
|
|
2
|
+
|
|
3
|
+
# Import all node classes from nodes.py
|
|
4
|
+
from wikigen.nodes.nodes import (
|
|
5
|
+
FetchRepo,
|
|
6
|
+
IdentifyAbstractions,
|
|
7
|
+
AnalyzeRelationships,
|
|
8
|
+
OrderComponents,
|
|
9
|
+
WriteComponents,
|
|
10
|
+
GenerateDocContent,
|
|
11
|
+
WriteDocFiles,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_wiki_flow():
|
|
16
|
+
"""Creates and returns the codebase wiki generation flow."""
|
|
17
|
+
|
|
18
|
+
# Instantiate nodes
|
|
19
|
+
fetch_repo = FetchRepo()
|
|
20
|
+
identify_abstractions = IdentifyAbstractions(max_retries=5, wait=20)
|
|
21
|
+
analyze_relationships = AnalyzeRelationships(max_retries=5, wait=20)
|
|
22
|
+
order_components = OrderComponents(max_retries=5, wait=20)
|
|
23
|
+
write_components = WriteComponents(max_retries=5, wait=20) # This is a BatchNode
|
|
24
|
+
generate_doc_content = GenerateDocContent()
|
|
25
|
+
write_doc_files = WriteDocFiles()
|
|
26
|
+
|
|
27
|
+
# Connect nodes in sequence based on the design
|
|
28
|
+
fetch_repo >> identify_abstractions
|
|
29
|
+
identify_abstractions >> analyze_relationships
|
|
30
|
+
analyze_relationships >> order_components
|
|
31
|
+
order_components >> write_components
|
|
32
|
+
write_components >> generate_doc_content
|
|
33
|
+
generate_doc_content >> write_doc_files
|
|
34
|
+
|
|
35
|
+
# Create the flow starting with FetchRepo
|
|
36
|
+
wiki_flow = Flow(start=fetch_repo)
|
|
37
|
+
|
|
38
|
+
return wiki_flow
|