cortexcode 0.8.1__py3-none-any.whl → 0.9.1__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.
@@ -60,7 +60,7 @@ class AIDocGenerator:
60
60
  messages = prompts.generate_project_overview_prompt(index_data)
61
61
  output.overview = self._generate(messages, "overview")
62
62
  if output.overview:
63
- (output_dir / "AI_OVERVIEW.md").write_text(output.overview)
63
+ (output_dir / "AI_OVERVIEW.md").write_text(output.overview, encoding="utf-8")
64
64
  print(" -> AI_OVERVIEW.md")
65
65
 
66
66
  if "api" in docs:
@@ -68,7 +68,7 @@ class AIDocGenerator:
68
68
  messages = prompts.generate_api_docs_prompt(index_data)
69
69
  output.api_docs = self._generate(messages, "api")
70
70
  if output.api_docs:
71
- (output_dir / "AI_API.md").write_text(output.api_docs)
71
+ (output_dir / "AI_API.md").write_text(output.api_docs, encoding="utf-8")
72
72
  print(" -> AI_API.md")
73
73
 
74
74
  if "architecture" in docs:
@@ -76,7 +76,7 @@ class AIDocGenerator:
76
76
  messages = prompts.generate_architecture_prompt(index_data)
77
77
  output.architecture = self._generate(messages, "architecture")
78
78
  if output.architecture:
79
- (output_dir / "AI_ARCHITECTURE.md").write_text(output.architecture)
79
+ (output_dir / "AI_ARCHITECTURE.md").write_text(output.architecture, encoding="utf-8")
80
80
  print(" -> AI_ARCHITECTURE.md")
81
81
 
82
82
  if "flows" in docs:
@@ -84,7 +84,7 @@ class AIDocGenerator:
84
84
  messages = prompts.generate_flow_docs_prompt(index_data)
85
85
  output.flows = self._generate(messages, "flows")
86
86
  if output.flows:
87
- (output_dir / "AI_FLOWS.md").write_text(output.flows)
87
+ (output_dir / "AI_FLOWS.md").write_text(output.flows, encoding="utf-8")
88
88
  print(" -> AI_FLOWS.md")
89
89
 
90
90
  return output
@@ -107,7 +107,7 @@ class AIDocGenerator:
107
107
  result = self._generate(messages, f"module_{module_name}")
108
108
 
109
109
  if output_path and result:
110
- Path(output_path).write_text(result)
110
+ Path(output_path).write_text(result, encoding="utf-8")
111
111
 
112
112
  return result
113
113
 
@@ -1,6 +1,9 @@
1
+ import json
1
2
  import time
3
+ import sys
2
4
  from pathlib import Path
3
5
 
6
+ import click
4
7
  from rich import box
5
8
  from rich.console import Console
6
9
  from rich.panel import Panel
@@ -8,6 +11,497 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn
8
11
  from rich.table import Table
9
12
 
10
13
 
14
+ def _load_questionary():
15
+ try:
16
+ import questionary
17
+ except ImportError:
18
+ return None
19
+ return questionary
20
+
21
+
22
+ def _load_user_config() -> dict:
23
+ config_path = Path.home() / ".cortexcode" / "config.json"
24
+ if not config_path.exists():
25
+ return {}
26
+ try:
27
+ return json.loads(config_path.read_text(encoding="utf-8"))
28
+ except Exception:
29
+ return {}
30
+
31
+
32
+ def _save_user_config(config: dict) -> None:
33
+ config_path = Path.home() / ".cortexcode" / "config.json"
34
+ config_path.parent.mkdir(parents=True, exist_ok=True)
35
+ config_path.write_text(json.dumps(config, indent=2), encoding="utf-8")
36
+
37
+
38
+ def _load_post_index_preferences() -> dict:
39
+ config = _load_user_config()
40
+ prefs = config.get("post_index_wizard", {})
41
+ if isinstance(prefs, dict):
42
+ return prefs
43
+ return {}
44
+
45
+
46
+ def _save_post_index_preferences(preferences: dict) -> None:
47
+ config = _load_user_config()
48
+ config["post_index_wizard"] = preferences
49
+ _save_user_config(config)
50
+
51
+
52
+ def _should_show_post_index_wizard(watch: bool, disabled: bool, force: bool) -> bool:
53
+ if force:
54
+ return True
55
+ preferences = _load_post_index_preferences()
56
+ return not disabled and preferences.get("enabled", True) and not watch and sys.stdin.isatty() and sys.stdout.isatty()
57
+
58
+
59
+ def _prompt_generate_now() -> bool:
60
+ questionary = _load_questionary()
61
+ if questionary:
62
+ answer = questionary.confirm("Generate docs, diagrams, reports, or setup now?", default=False).ask()
63
+ return bool(answer)
64
+ return click.confirm("Generate docs, diagrams, reports, or setup now?", default=False)
65
+
66
+
67
+ def _get_recommended_targets(index_data: dict, preferences: dict) -> list[str]:
68
+ saved_targets = preferences.get("targets")
69
+ if isinstance(saved_targets, list) and saved_targets:
70
+ return saved_targets
71
+
72
+ recommended = ["docs", "diagrams", "report"]
73
+ profile = index_data.get("project_profile", {}) if isinstance(index_data, dict) else {}
74
+ route_count = profile.get("route_count", 0)
75
+ entity_count = profile.get("entity_count", 0)
76
+ files = index_data.get("files", {}) if isinstance(index_data, dict) else {}
77
+ symbol_count = sum(
78
+ len(file_data.get("symbols", [])) if isinstance(file_data, dict) else len(file_data)
79
+ for file_data in files.values()
80
+ )
81
+
82
+ if route_count or entity_count:
83
+ recommended.append("dashboard")
84
+ if 0 < symbol_count <= 500:
85
+ recommended.append("viz")
86
+
87
+ seen = []
88
+ for item in recommended:
89
+ if item not in seen:
90
+ seen.append(item)
91
+ return seen
92
+
93
+
94
+ def _prompt_generation_targets(console: Console, defaults: list[str] | None = None, recommended: list[str] | None = None) -> list[str]:
95
+ core_options = [
96
+ ("docs", "Project docs"),
97
+ ("diagrams", "Mermaid diagrams"),
98
+ ("report", "Interactive terminal report"),
99
+ ("viz", "Interactive graph visualization"),
100
+ ("dashboard", "Live dashboard"),
101
+ ]
102
+ ai_options = [
103
+ ("ai_docs", "AI docs"),
104
+ ("wiki", "CodeWiki"),
105
+ ]
106
+ setup_options = [
107
+ ("mcp_setup", "Configure MCP for your editor"),
108
+ ]
109
+ options = core_options + ai_options + setup_options
110
+ defaults = defaults or []
111
+ recommended = recommended or []
112
+ questionary = _load_questionary()
113
+ if questionary:
114
+ choices = [questionary.Separator("══ Core outputs ══")]
115
+ choices.extend(
116
+ questionary.Choice(
117
+ title=f"{label}{' [recommended]' if value in recommended else ''}",
118
+ value=value,
119
+ checked=value in defaults,
120
+ )
121
+ for value, label in core_options
122
+ )
123
+ choices.append(questionary.Separator("══ AI-powered ══"))
124
+ choices.extend(
125
+ questionary.Choice(
126
+ title=f"{label}{' [recommended]' if value in recommended else ''}",
127
+ value=value,
128
+ checked=value in defaults,
129
+ )
130
+ for value, label in ai_options
131
+ )
132
+ choices.append(questionary.Separator("══ Setup ══"))
133
+ choices.extend(
134
+ questionary.Choice(
135
+ title=f"{label}{' [recommended]' if value in recommended else ''}",
136
+ value=value,
137
+ checked=value in defaults,
138
+ )
139
+ for value, label in setup_options
140
+ )
141
+ selections = questionary.checkbox(
142
+ "Select what to generate",
143
+ choices=choices,
144
+ validate=lambda items: True if items else "Select at least one option.",
145
+ ).ask()
146
+ return selections or []
147
+
148
+ console.print("\n[bold]Select what to generate:[/bold]")
149
+ for idx, (_, label) in enumerate(options, 1):
150
+ suffix = " [recommended]" if options[idx - 1][0] in recommended else ""
151
+ console.print(f" {idx}. {label}{suffix}")
152
+ default_indexes = [str(idx) for idx, (value, _) in enumerate(options, 1) if value in defaults]
153
+ raw_value = click.prompt(
154
+ "Enter choices as comma-separated numbers",
155
+ default=",".join(default_indexes) if default_indexes else "1,2",
156
+ show_default=True,
157
+ )
158
+ selected = []
159
+ for part in raw_value.split(","):
160
+ part = part.strip()
161
+ if not part.isdigit():
162
+ continue
163
+ index = int(part) - 1
164
+ if 0 <= index < len(options):
165
+ selected.append(options[index][0])
166
+ seen = []
167
+ for item in selected:
168
+ if item not in seen:
169
+ seen.append(item)
170
+ return seen
171
+
172
+
173
+ def _prompt_provider(default: str | None = None) -> str:
174
+ from cortexcode.ai_docs.config import get_config
175
+
176
+ provider = default or get_config().provider or "google"
177
+ choices = ["google", "openai", "anthropic", "ollama"]
178
+ questionary = _load_questionary()
179
+ if questionary:
180
+ answer = questionary.select("Choose provider", choices=choices, default=provider).ask()
181
+ return answer or provider
182
+ return click.prompt("Choose provider", type=click.Choice(choices), default=provider, show_choices=True)
183
+
184
+
185
+ def _ensure_provider_access(console: Console, provider: str) -> bool:
186
+ from cortexcode.ai_docs.config import AIConfig, get_api_key, get_config, set_api_key, set_config
187
+
188
+ config = get_config()
189
+ set_config(AIConfig(provider=provider, model=config.model, temperature=config.temperature, max_tokens=config.max_tokens))
190
+
191
+ if provider == "ollama":
192
+ return True
193
+
194
+ if get_api_key(provider):
195
+ return True
196
+
197
+ provider_labels = {
198
+ "google": ("Gemini", "https://aistudio.google.com/apikey"),
199
+ "openai": ("OpenAI", "https://platform.openai.com/api-keys"),
200
+ "anthropic": ("Anthropic", "https://console.anthropic.com/settings/keys"),
201
+ }
202
+ label, url = provider_labels.get(provider, (provider, ""))
203
+ console.print(
204
+ f"\n[yellow]No API key found for {label}.[/yellow]\n"
205
+ f"[dim]Get your key at:[/dim] [cyan]{url}[/cyan]\n"
206
+ )
207
+
208
+ questionary = _load_questionary()
209
+ if questionary:
210
+ api_key = questionary.password(f"Enter your {label} API key (leave blank to skip)").ask() or ""
211
+ else:
212
+ api_key = click.prompt(
213
+ f"Enter your {label} API key (leave blank to skip)",
214
+ default="",
215
+ show_default=False,
216
+ hide_input=True,
217
+ ).strip()
218
+
219
+ if not api_key:
220
+ console.print(f"[dim]Skipped. You can set it later with:[/dim] [cyan]cortexcode config set {provider}_api_key YOUR_KEY[/cyan]\n")
221
+ return False
222
+
223
+ set_api_key(provider, api_key)
224
+ console.print(f"[green]✓[/green] API key saved for {label}.\n")
225
+ return True
226
+
227
+
228
+ def _prompt_report_type(available_reports: list[str], default: str | None = None) -> str:
229
+ questionary = _load_questionary()
230
+ selected_default = default if default in available_reports else (available_reports[0] if available_reports else "overview")
231
+ if questionary:
232
+ answer = questionary.select("Choose report type", choices=available_reports, default=selected_default).ask()
233
+ return answer or selected_default
234
+ return click.prompt(
235
+ "Choose report type",
236
+ type=click.Choice(available_reports),
237
+ default=selected_default,
238
+ show_choices=True,
239
+ )
240
+
241
+
242
+ def _prompt_dashboard_port(default: int) -> int:
243
+ questionary = _load_questionary()
244
+ if questionary:
245
+ answer = questionary.text("Dashboard port", default=str(default), validate=lambda value: value.isdigit() or "Enter a valid port").ask()
246
+ return int(answer or default)
247
+ return int(click.prompt("Dashboard port", default=default, type=int, show_default=True))
248
+
249
+
250
+ def _prompt_diagram_types(defaults: list[str]) -> list[str]:
251
+ diagram_types = ["call_graph", "class", "sequence", "architecture", "imports", "dependencies", "entities", "file_tree"]
252
+ questionary = _load_questionary()
253
+ if questionary:
254
+ selections = questionary.checkbox(
255
+ "Choose diagram types",
256
+ choices=[questionary.Choice(title=item, value=item, checked=item in defaults) for item in diagram_types],
257
+ validate=lambda items: True if items else "Select at least one diagram type.",
258
+ ).ask()
259
+ return selections or defaults
260
+
261
+ raw_value = click.prompt(
262
+ "Choose diagram types (comma-separated)",
263
+ default=",".join(defaults),
264
+ show_default=True,
265
+ )
266
+ values = [item.strip() for item in raw_value.split(",") if item.strip() in diagram_types]
267
+ return values or defaults
268
+
269
+
270
+ def _prompt_ai_doc_types(defaults: list[str]) -> list[str]:
271
+ doc_types = ["overview", "api", "architecture", "flows"]
272
+ questionary = _load_questionary()
273
+ if questionary:
274
+ selections = questionary.checkbox(
275
+ "Choose AI doc types",
276
+ choices=[questionary.Choice(title=item, value=item, checked=item in defaults) for item in doc_types],
277
+ validate=lambda items: True if items else "Select at least one AI doc type.",
278
+ ).ask()
279
+ return selections or defaults
280
+
281
+ raw_value = click.prompt(
282
+ "Choose AI doc types (comma-separated)",
283
+ default=",".join(defaults),
284
+ show_default=True,
285
+ )
286
+ values = [item.strip() for item in raw_value.split(",") if item.strip() in doc_types]
287
+ return values or defaults
288
+
289
+
290
+ def _prompt_confirm(message: str, default: bool) -> bool:
291
+ questionary = _load_questionary()
292
+ if questionary:
293
+ answer = questionary.confirm(message, default=default).ask()
294
+ return bool(answer)
295
+ return click.confirm(message, default=default)
296
+
297
+
298
+ def _prompt_max_modules(default: int) -> int:
299
+ questionary = _load_questionary()
300
+ if questionary:
301
+ answer = questionary.text("Max module pages", default=str(default), validate=lambda value: value.isdigit() or "Enter a valid number").ask()
302
+ return int(answer or default)
303
+ return int(click.prompt("Max module pages", default=default, type=int, show_default=True))
304
+
305
+
306
+ def _prompt_save_preferences() -> bool:
307
+ questionary = _load_questionary()
308
+ if questionary:
309
+ answer = questionary.confirm("Save these choices as defaults for future index runs?", default=True).ask()
310
+ return bool(answer)
311
+ return click.confirm("Save these choices as defaults for future index runs?", default=True)
312
+
313
+
314
+ def _prompt_show_wizard_next_time(default: bool) -> bool:
315
+ questionary = _load_questionary()
316
+ if questionary:
317
+ answer = questionary.confirm("Show this wizard automatically after future index runs?", default=default).ask()
318
+ return bool(answer)
319
+ return click.confirm("Show this wizard automatically after future index runs?", default=default)
320
+
321
+
322
+ def _run_post_index_wizard(console: Console, path: Path, index_data: dict) -> None:
323
+ import webbrowser
324
+
325
+ from cortexcode import indexer
326
+ from cortexcode.cli import handle_ai_docs_command, handle_dashboard_command, handle_diagrams_command, handle_docs_command, handle_wiki_command
327
+ from cortexcode.cli.cli_servers import handle_mcp_setup
328
+ from cortexcode.cli.cli_support import require_ai_doc_generator, require_index_path
329
+ from cortexcode.docs import generate_all_docs
330
+ from cortexcode.docs.diagrams import save_diagrams
331
+ from cortexcode.dashboard import DashboardServer
332
+ from cortexcode.reports.site.viz import generate_viz_html
333
+ from cortexcode.terminal.completion import print_ai_docs_complete, print_diagrams_complete, print_docs_complete
334
+ from cortexcode.terminal.headers import print_ai_docs_header, print_diagrams_header, print_docs_header
335
+ from cortexcode.terminal.reports import get_available_reports, print_terminal_report
336
+
337
+ preferences = _load_post_index_preferences()
338
+ if not _prompt_generate_now():
339
+ return
340
+
341
+ recommended_targets = _get_recommended_targets(index_data, preferences)
342
+ default_targets = preferences.get("targets") or recommended_targets
343
+ targets = _prompt_generation_targets(console, default_targets, recommended_targets)
344
+ if not targets:
345
+ return
346
+
347
+ provider = preferences.get("provider")
348
+ if any(target in {"ai_docs", "wiki"} for target in targets):
349
+ provider = _prompt_provider(provider)
350
+ if not _ensure_provider_access(console, provider):
351
+ targets = [target for target in targets if target not in {"ai_docs", "wiki"}]
352
+ if not targets:
353
+ return
354
+
355
+ report_type = preferences.get("report_type")
356
+ if "report" in targets:
357
+ available_reports = get_available_reports(index_data, ["overview", "tech", "hotspots", "routes", "entities", "frontend", "cli"])
358
+ report_type = _prompt_report_type(available_reports, report_type)
359
+
360
+ dashboard_port = int(preferences.get("dashboard_port", 8787))
361
+ if "dashboard" in targets:
362
+ dashboard_port = _prompt_dashboard_port(dashboard_port)
363
+
364
+ docs_open_browser = bool(preferences.get("open_docs_browser", False))
365
+ if "docs" in targets:
366
+ docs_open_browser = _prompt_confirm("Open generated docs in browser?", docs_open_browser)
367
+
368
+ recommended_diagrams = index_data.get("project_profile", {}).get("recommendations", {}).get("diagrams", [])
369
+ diagram_defaults = preferences.get("diagram_types") or [item for item in recommended_diagrams if item in ["call_graph", "class", "sequence", "architecture", "imports", "dependencies", "entities", "file_tree"]] or ["architecture", "call_graph"]
370
+ selected_diagram_types = diagram_defaults
371
+ if "diagrams" in targets:
372
+ selected_diagram_types = _prompt_diagram_types(diagram_defaults)
373
+
374
+ ai_doc_defaults = preferences.get("ai_doc_types") or ["overview", "api", "architecture", "flows"]
375
+ selected_ai_doc_types = ai_doc_defaults
376
+ if "ai_docs" in targets:
377
+ selected_ai_doc_types = _prompt_ai_doc_types(ai_doc_defaults)
378
+
379
+ wiki_open_browser = bool(preferences.get("open_wiki_browser", False))
380
+ wiki_include_modules = bool(preferences.get("wiki_include_modules", True))
381
+ wiki_max_modules = int(preferences.get("wiki_max_modules", 15))
382
+ if "wiki" in targets:
383
+ wiki_open_browser = _prompt_confirm("Open generated CodeWiki in browser?", wiki_open_browser)
384
+ wiki_include_modules = _prompt_confirm("Include per-module wiki pages?", wiki_include_modules)
385
+ if wiki_include_modules:
386
+ wiki_max_modules = _prompt_max_modules(wiki_max_modules)
387
+
388
+ viz_open_browser = bool(preferences.get("open_viz_browser", True))
389
+ if "viz" in targets:
390
+ viz_open_browser = _prompt_confirm("Open graph visualization in browser?", viz_open_browser)
391
+
392
+ wizard_enabled = _prompt_show_wizard_next_time(bool(preferences.get("enabled", True)))
393
+
394
+ if _prompt_save_preferences():
395
+ _save_post_index_preferences(
396
+ {
397
+ "targets": targets,
398
+ "provider": provider,
399
+ "report_type": report_type,
400
+ "dashboard_port": dashboard_port,
401
+ "diagram_types": selected_diagram_types,
402
+ "ai_doc_types": selected_ai_doc_types,
403
+ "open_docs_browser": docs_open_browser,
404
+ "open_wiki_browser": wiki_open_browser,
405
+ "wiki_include_modules": wiki_include_modules,
406
+ "wiki_max_modules": wiki_max_modules,
407
+ "open_viz_browser": viz_open_browser,
408
+ "enabled": wizard_enabled,
409
+ }
410
+ )
411
+
412
+ if "docs" in targets:
413
+ handle_docs_command(
414
+ console,
415
+ path,
416
+ path / ".cortexcode" / "docs",
417
+ docs_open_browser,
418
+ require_index_path,
419
+ print_docs_header,
420
+ generate_all_docs,
421
+ print_docs_complete,
422
+ )
423
+
424
+ if "diagrams" in targets:
425
+ output_dir = path / ".cortexcode" / "diagrams"
426
+ diagram_types = ["call_graph", "class", "sequence", "architecture", "imports", "dependencies", "entities", "file_tree"]
427
+ if len(selected_diagram_types) == 1:
428
+ handle_diagrams_command(
429
+ console,
430
+ path,
431
+ output_dir,
432
+ selected_diagram_types[0],
433
+ require_index_path,
434
+ print_diagrams_header,
435
+ save_diagrams,
436
+ diagram_types,
437
+ print_diagrams_complete,
438
+ )
439
+ elif len(selected_diagram_types) == len(diagram_types):
440
+ handle_diagrams_command(
441
+ console,
442
+ path,
443
+ output_dir,
444
+ None,
445
+ require_index_path,
446
+ print_diagrams_header,
447
+ save_diagrams,
448
+ diagram_types,
449
+ print_diagrams_complete,
450
+ )
451
+ else:
452
+ _, index_path = require_index_path(console, path)
453
+ print_diagrams_header(console, path)
454
+ for diagram_type in selected_diagram_types:
455
+ save_diagrams(index_path, output_dir, diagram_type=diagram_type)
456
+ generated_files = ["DIAGRAMS.md", *[f"{item}.mmd" for item in selected_diagram_types]]
457
+ print_diagrams_complete(console, output_dir, generated_files)
458
+
459
+ if "ai_docs" in targets:
460
+ handle_ai_docs_command(
461
+ console,
462
+ path,
463
+ path / ".cortexcode" / "ai-docs",
464
+ provider,
465
+ None,
466
+ tuple(selected_ai_doc_types),
467
+ require_ai_doc_generator,
468
+ require_index_path,
469
+ print_ai_docs_header,
470
+ print_ai_docs_complete,
471
+ )
472
+
473
+ if "wiki" in targets:
474
+ handle_wiki_command(
475
+ console,
476
+ path,
477
+ path / ".cortexcode" / "wiki",
478
+ provider,
479
+ None,
480
+ None,
481
+ not wiki_include_modules,
482
+ wiki_max_modules,
483
+ wiki_open_browser,
484
+ )
485
+
486
+ if "report" in targets:
487
+ print_terminal_report(console, report_type or "overview", index_data, path)
488
+
489
+ if "viz" in targets:
490
+ viz_path = path / ".cortexcode" / "graph.html"
491
+ generate_viz_html(index_data, viz_path)
492
+ if viz_open_browser:
493
+ console.print(f"[cyan]Opening visualization:[/cyan] {viz_path}")
494
+ webbrowser.open(viz_path.as_uri())
495
+ else:
496
+ console.print(f"[green]✓[/green] Visualization generated: {viz_path}")
497
+
498
+ if "mcp_setup" in targets:
499
+ handle_mcp_setup(console)
500
+
501
+ if "dashboard" in targets:
502
+ handle_dashboard_command(console, path, dashboard_port, indexer, DashboardServer)
503
+
504
+
11
505
  def handle_index_command(
12
506
  console: Console,
13
507
  path: str | Path,
@@ -20,6 +514,8 @@ def handle_index_command(
20
514
  include,
21
515
  root,
22
516
  dry_run: bool,
517
+ no_post_index_wizard: bool,
518
+ force_wizard: bool,
23
519
  indexer_module,
24
520
  print_index_header,
25
521
  print_project_profile_summary,
@@ -104,6 +600,9 @@ def handle_index_command(
104
600
  if verbose:
105
601
  show_index_summary(console, index_data)
106
602
 
603
+ if _should_show_post_index_wizard(watch, no_post_index_wizard, force_wizard):
604
+ _run_post_index_wizard(console, path, index_data)
605
+
107
606
  if watch:
108
607
  console.print("\n[yellow]Starting watcher...[/yellow]")
109
608
  console.print("[dim]Press Ctrl+C to stop[/dim]")
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import os
3
3
  import shutil
4
+ import platform
4
5
  from pathlib import Path
5
6
 
6
7
  from rich.console import Console
@@ -10,68 +11,103 @@ from rich.prompt import Prompt
10
11
  IDE_CONFIGS = {
11
12
  "vscode": {
12
13
  "name": "VS Code",
13
- "file": ".vscode/settings.json",
14
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
14
+ "file": ".vscode/mcp.json",
15
+ "global_file": "~/.vscode/mcp.json",
16
+ "config": {"servers": {"cortexcode": {"command": "cortexcode", "args": ["mcp", "start"]}}},
15
17
  },
16
18
  "cursor": {
17
19
  "name": "Cursor",
18
- "file": ".cursor/settings.json",
19
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
20
+ "file": ".cursor/mcp.json",
21
+ "global_file": "~/.cursor/mcp.json",
22
+ "config": {"servers": {"cortexcode": {"command": "cortexcode", "args": ["mcp", "start"]}}},
20
23
  },
21
24
  "windsurf": {
22
25
  "name": "Windsurf",
23
- "file": ".windsurf/config.json",
24
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
26
+ "file": ".codeium/windsurf/mcp_config.json",
27
+ "global_file": "~/.codeium/windsurf/mcp_config.json",
28
+ "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp", "start"]}}},
25
29
  },
26
30
  "claude": {
27
- "name": "Claude Desktop",
28
- "file": "claude_desktop_config.json",
29
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
31
+ "name": "Claude Code",
32
+ "file": None,
33
+ "global_file": None,
34
+ "config": None,
35
+ "cli_command": "claude mcp add cortexcode stdio -- cortexcode mcp start",
30
36
  },
31
37
  "cline": {
32
38
  "name": "Cline",
33
- "file": ".cline/settings.json",
34
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
39
+ "file": ".cline/mcp.json",
40
+ "global_file": "~/.cline/mcp.json",
41
+ "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp", "start"]}}},
35
42
  },
36
43
  "roocode": {
37
44
  "name": "RooCode",
38
- "file": ".roocode/settings.json",
39
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
45
+ "file": ".roocode/mcp.json",
46
+ "global_file": "~/.roocode/mcp.json",
47
+ "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp", "start"]}}},
40
48
  },
41
- "gemini": {
42
- "name": "Gemini CLI",
43
- "file": ".gemini/settings.json",
44
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
49
+ "opencode": {
50
+ "name": "OpenCode",
51
+ "file": "opencode.json",
52
+ "global_file": "~/.opencode/opencode.json",
53
+ "config": {"mcp": {"cortexcode": {"type": "local", "command": ["cortexcode", "mcp", "start"], "enabled": True}}},
45
54
  },
46
- "amazonq": {
47
- "name": "Amazon Q Developer",
48
- "file": ".aws/amazonq/settings.json",
49
- "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp"], "disabled": False}}},
55
+ "antigravity": {
56
+ "name": "Antigravity",
57
+ "file": ".antigravity/mcp.json",
58
+ "global_file": "~/.antigravity/mcp.json",
59
+ "config": {"mcpServers": {"cortexcode": {"command": "cortexcode", "args": ["mcp", "start"]}}},
50
60
  },
51
61
  }
52
62
 
53
63
 
64
+ def get_claude_config_path() -> Path:
65
+ """Get Claude Desktop config path based on OS."""
66
+ home = Path.home()
67
+ system = platform.system()
68
+
69
+ if system == "Windows":
70
+ return home / "AppData" / "Roaming" / "Claude" / "claude_desktop_config.json"
71
+ elif system == "Darwin": # macOS
72
+ return home / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
73
+ else: # Linux
74
+ return home / ".config" / "Claude" / "claude_desktop_config.json"
75
+
76
+
54
77
  def detect_ide() -> list[str]:
55
78
  """Detect which IDEs are available in the current environment."""
56
79
  detected = []
57
80
  home = Path.home()
58
81
 
59
- if (home / ".vscode/settings.json").exists():
82
+ # VS Code / Cursor mcp.json
83
+ if (home / ".vscode/mcp.json").exists() or (Path.cwd() / ".vscode/mcp.json").exists():
60
84
  detected.append("vscode")
61
- if (home / ".cursor/settings.json").exists():
85
+ if (home / ".cursor/mcp.json").exists():
62
86
  detected.append("cursor")
63
- if (home / ".windsurf/config.json").exists():
87
+
88
+ # Windsurf
89
+ if (home / ".codeium/windsurf/mcp_config.json").exists():
64
90
  detected.append("windsurf")
65
- if (home / "AppData/Roaming/Claude/settings.json").exists() or (home / ".claude/settings.json").exists():
91
+
92
+ # Claude Desktop
93
+ if get_claude_config_path().exists():
66
94
  detected.append("claude")
67
- if (home / ".cline/settings.json").exists():
95
+
96
+ # Cline
97
+ if (home / ".cline/mcp.json").exists():
68
98
  detected.append("cline")
69
- if (home / ".roocode/settings.json").exists():
99
+
100
+ # RooCode
101
+ if (home / ".roocode/mcp.json").exists():
70
102
  detected.append("roocode")
71
- if (home / ".gemini/settings.json").exists():
72
- detected.append("gemini")
73
- if (home / ".aws/amazonq/settings.json").exists():
74
- detected.append("amazonq")
103
+
104
+ # OpenCode
105
+ if (Path.cwd() / "opencode.json").exists() or (home / ".opencode/opencode.json").exists():
106
+ detected.append("opencode")
107
+
108
+ # Antigravity
109
+ if (home / ".antigravity/mcp.json").exists():
110
+ detected.append("antigravity")
75
111
 
76
112
  return detected
77
113
 
@@ -112,12 +148,36 @@ def handle_mcp_setup(console: Console) -> None:
112
148
  return
113
149
 
114
150
  for target in targets:
151
+ ide = IDE_CONFIGS.get(target)
152
+
153
+ if target == "claude":
154
+ console.print(f"\n[yellow]Claude Code:[/yellow] Run this command manually:")
155
+ console.print(f" [cyan]{ide.get('cli_command')}[/cyan]")
156
+ console.print("[dim]Then restart Claude Code[/dim]")
157
+ continue
158
+
159
+ if target == "opencode":
160
+ config_path = Path.cwd() / ide["file"]
161
+ config_path.parent.mkdir(parents=True, exist_ok=True)
162
+
163
+ existing = {}
164
+ if config_path.exists():
165
+ try:
166
+ existing = json.loads(config_path.read_text())
167
+ except:
168
+ pass
169
+
170
+ if "mcp" not in existing:
171
+ existing["mcp"] = {}
172
+ existing["mcp"]["cortexcode"] = ide["config"]["mcp"]["cortexcode"]
173
+
174
+ config_path.write_text(json.dumps(existing, indent=2))
175
+ console.print(f"[green]✓[/green] Updated {config_path}")
176
+ continue
177
+
115
178
  if target.startswith("custom:"):
116
179
  config_path = Path(target.split(":", 1)[1])
117
180
  else:
118
- ide = IDE_CONFIGS.get(target)
119
- if not ide:
120
- continue
121
181
  config_path = Path.cwd() / ide["file"]
122
182
 
123
183
  config_path.parent.mkdir(parents=True, exist_ok=True)
@@ -129,15 +189,18 @@ def handle_mcp_setup(console: Console) -> None:
129
189
  except:
130
190
  pass
131
191
 
132
- mcp_key = "mcpServers"
192
+ # VS Code/Cursor use "servers", others use "mcpServers"
193
+ if target in ("vscode", "cursor"):
194
+ mcp_key = "servers"
195
+ else:
196
+ mcp_key = "mcpServers"
197
+
133
198
  if mcp_key not in existing:
134
199
  existing[mcp_key] = {}
135
200
 
136
201
  existing[mcp_key]["cortexcode"] = {
137
202
  "command": "cortexcode",
138
203
  "args": ["mcp"],
139
- "disabled": False,
140
- "alwaysAllow": [],
141
204
  }
142
205
 
143
206
  config_path.write_text(json.dumps(existing, indent=2))
@@ -15,16 +15,17 @@ def get_context(index_path: Path, query: str | None = None, num_results: int = 5
15
15
  num_results: Number of results to return
16
16
 
17
17
  Returns:
18
- Dictionary with relevant symbols and their relationships
18
+ Dictionary with relevant symbols and their code snippets
19
19
  """
20
20
  index = json.loads(index_path.read_text(encoding="utf-8"))
21
21
 
22
22
  files = index.get("files", {})
23
23
  call_graph = index.get("call_graph", {})
24
24
  file_deps = index.get("file_dependencies", {})
25
+ source_code = index.get("source_code", {})
25
26
 
26
27
  if not query:
27
- return _get_all_symbols(files, call_graph, num_results)
28
+ return _get_all_symbols(files, call_graph, source_code, num_results)
28
29
 
29
30
  file_filter = None
30
31
  query_lower = query.lower()
@@ -44,11 +45,11 @@ def get_context(index_path: Path, query: str | None = None, num_results: int = 5
44
45
 
45
46
  for sym in symbols:
46
47
  if file_filter and not query_lower:
47
- results.append(_build_symbol_result(sym, rel_path, call_graph))
48
+ results.append(_build_symbol_result(sym, rel_path, call_graph, source_code))
48
49
  continue
49
50
 
50
51
  if _matches_query(sym, query_lower):
51
- results.append(_build_symbol_result(sym, rel_path, call_graph))
52
+ results.append(_build_symbol_result(sym, rel_path, call_graph, source_code))
52
53
 
53
54
  if query_lower:
54
55
  for imp in imports:
@@ -79,8 +80,8 @@ def get_context(index_path: Path, query: str | None = None, num_results: int = 5
79
80
  return response
80
81
 
81
82
 
82
- def _build_symbol_result(sym: dict, rel_path: str, call_graph: dict) -> dict:
83
- """Build a context result dict for a symbol."""
83
+ def _build_symbol_result(sym: dict, rel_path: str, call_graph: dict, source_code: dict) -> dict:
84
+ """Build a context result dict for a symbol with actual code."""
84
85
  result = {
85
86
  "name": sym.get("name"),
86
87
  "type": sym.get("type"),
@@ -92,6 +93,11 @@ def _build_symbol_result(sym: dict, rel_path: str, call_graph: dict) -> dict:
92
93
  "framework": sym.get("framework"),
93
94
  }
94
95
 
96
+ # Add actual code snippet from source_code - look up by symbol name + type
97
+ code = _get_symbol_code(sym.get("name"), sym.get("type"), rel_path, source_code)
98
+ if code:
99
+ result["code"] = code
100
+
95
101
  if sym.get("return_type"):
96
102
  result["return_type"] = sym["return_type"]
97
103
 
@@ -105,7 +111,29 @@ def _build_symbol_result(sym: dict, rel_path: str, call_graph: dict) -> dict:
105
111
  return result
106
112
 
107
113
 
108
- def _get_all_symbols(files: dict, call_graph: dict, limit: int) -> dict[str, Any]:
114
+ def _get_symbol_code(sym_name: str, sym_type: str, rel_path: str, source_code: dict) -> str | None:
115
+ """Get the actual code snippet for a symbol from source_code storage."""
116
+ if not source_code or not sym_name:
117
+ return None
118
+
119
+ # Try to find by symbol name + type key first
120
+ key = f"{sym_name}:{sym_type}"
121
+ if key in source_code:
122
+ file_data = source_code[key].get(rel_path)
123
+ if file_data:
124
+ return file_data.get("body")
125
+
126
+ # Fallback: search through all keys for this symbol name
127
+ for code_key, files in source_code.items():
128
+ if code_key.startswith(f"{sym_name}:"):
129
+ file_data = files.get(rel_path)
130
+ if file_data:
131
+ return file_data.get("body")
132
+
133
+ return None
134
+
135
+
136
+ def _get_all_symbols(files: dict, call_graph: dict, source_code: dict, limit: int) -> dict[str, Any]:
109
137
  """Get all symbols, limited."""
110
138
  all_symbols = []
111
139
 
cortexcode/indexer.py CHANGED
@@ -45,6 +45,7 @@ class CodeIndexer(IndexerExtractorMixin):
45
45
  self.symbols: list[dict[str, Any]] = []
46
46
  self.call_graph: dict[str, list[str]] = {}
47
47
  self.file_symbols: dict[str, list[dict[str, Any]]] = {}
48
+ self.source_code: dict[str, dict] = {} # Store source code lines for context
48
49
  self.gitignore_patterns: list[tuple[str, bool]] = []
49
50
 
50
51
  # Filter options
@@ -85,6 +86,7 @@ class CodeIndexer(IndexerExtractorMixin):
85
86
  self.symbols = session["symbols"]
86
87
  self.call_graph = session["call_graph"]
87
88
  self.file_symbols = session["file_symbols"]
89
+ self.source_code = session.get("source_code", {})
88
90
  self.parsers = session["parsers"]
89
91
 
90
92
  self._load_gitignore(root_path)
@@ -153,6 +155,10 @@ class CodeIndexer(IndexerExtractorMixin):
153
155
 
154
156
  def _index_file(self, file_path: Path, root: Path) -> None:
155
157
  """Index a single file."""
158
+ rel_path = file_path.relative_to(root)
159
+ rel_str = str(rel_path)
160
+
161
+ # First, run the actual indexing to get symbol positions
156
162
  result = index_file(
157
163
  file_path,
158
164
  root,
@@ -171,6 +177,41 @@ class CodeIndexer(IndexerExtractorMixin):
171
177
  return
172
178
 
173
179
  rel_path, file_data, symbols = result
180
+
181
+ # Load full file content to extract complete function bodies
182
+ try:
183
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
184
+ lines = content.split("\n")
185
+
186
+ # Store full function bodies for each symbol
187
+ for sym in symbols:
188
+ sym_line = sym.get("line", 1)
189
+ sym_name = sym.get("name", "")
190
+ sym_type = sym.get("type", "")
191
+
192
+ # Find the end of this symbol (next symbol or end of file)
193
+ end_line = len(lines)
194
+ for other_sym in symbols:
195
+ other_line = other_sym.get("line", 1)
196
+ if other_line > sym_line and (end_line == len(lines) or other_line < end_line):
197
+ end_line = other_line - 1
198
+
199
+ # Extract full body (with some context before)
200
+ start_line = max(1, sym_line - 2)
201
+ body_lines = lines[start_line-1:end_line]
202
+ body = "\n".join(body_lines)
203
+
204
+ # Store with key: "symbol_name:type" for quick lookup
205
+ key = f"{sym_name}:{sym_type}"
206
+ if key not in self.source_code:
207
+ self.source_code[key] = {}
208
+ self.source_code[key][rel_str] = {
209
+ "line": sym_line,
210
+ "body": body
211
+ }
212
+ except Exception:
213
+ pass
214
+
174
215
  merge_indexed_file(
175
216
  rel_path,
176
217
  file_data,
@@ -182,7 +223,7 @@ class CodeIndexer(IndexerExtractorMixin):
182
223
 
183
224
  def _build_index(self, root: Path) -> dict[str, Any]:
184
225
  """Build the final index structure."""
185
- return build_index_result(
226
+ result = build_index_result(
186
227
  root=root,
187
228
  file_symbols=self.file_symbols,
188
229
  call_graph=self.call_graph,
@@ -195,6 +236,9 @@ class CodeIndexer(IndexerExtractorMixin):
195
236
  regex_languages=REGEX_LANGUAGES,
196
237
  plugin_registry=plugin_registry,
197
238
  )
239
+ # Add source code for context retrieval
240
+ result["source_code"] = self.source_code
241
+ return result
198
242
 
199
243
 
200
244
  def index_directory(path: str | Path, incremental: bool = False, filter_opts: dict[str, Any] | None = None) -> dict[str, Any]:
cortexcode/main.py CHANGED
@@ -494,7 +494,9 @@ def serve_lsp():
494
494
  @click.option("-I", "--include", multiple=True, help="Patterns to include, e.g., 'apps/*', 'packages/*'")
495
495
  @click.option("-r", "--root", default=None, help="Monorepo root (for nx-style projects)")
496
496
  @click.option("-n", "--dry-run", is_flag=True, help="Preview what would be indexed without indexing")
497
- def index(path, output, verbose, watch, incremental, include_tests, exclude, include, root, dry_run):
497
+ @click.option("--no-post-index-wizard", is_flag=True, help="Skip the interactive post-index generation wizard")
498
+ @click.option("--force-wizard", is_flag=True, help="Force showing the post-index wizard even in non-TTY mode")
499
+ def index(path, output, verbose, watch, incremental, include_tests, exclude, include, root, dry_run, no_post_index_wizard, force_wizard):
498
500
  """Index a directory and save the code graph."""
499
501
  handle_index_command(
500
502
  console,
@@ -508,6 +510,8 @@ def index(path, output, verbose, watch, incremental, include_tests, exclude, inc
508
510
  include,
509
511
  root,
510
512
  dry_run,
513
+ no_post_index_wizard,
514
+ force_wizard,
511
515
  indexer,
512
516
  print_index_header_renderer,
513
517
  print_project_profile_summary_renderer,
@@ -17,7 +17,7 @@ TOOL_DEFINITIONS = [
17
17
  },
18
18
  {
19
19
  "name": "cortexcode_context",
20
- "description": "USE THIS when you need to understand how a function/class works, see its implementation, or see what it calls/who calls it. Also use when user asks 'how does X work' or 'show me the code for X'.",
20
+ "description": "USE THIS when you need to understand how a function/class works, see its implementation, or see what it calls/who calls it. Also use when user asks 'how does X work' or 'show me the code for X'. Returns ACTUAL CODE SNIPPETS from the index - no need to read files separately. This saves tokens and time.",
21
21
  "inputSchema": {
22
22
  "type": "object",
23
23
  "properties": {
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortexcode
3
- Version: 0.8.1
4
- Summary: Lightweight code indexing for AI assistants — save 90%+ tokens with structured context
3
+ Version: 0.9.1
4
+ Summary: Lightweight code indexing for AI assistants — build a searchable codegraph and grounded context
5
5
  Author-email: Naveen <naveen_joshi07@outlook.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/naveen-joshi/cortexcode
@@ -32,6 +32,7 @@ Requires-Dist: tree-sitter-rust>=0.23.0
32
32
  Requires-Dist: tree-sitter-java>=0.23.0
33
33
  Requires-Dist: tree-sitter-c-sharp>=0.23.0
34
34
  Requires-Dist: click>=8.1.0
35
+ Requires-Dist: questionary>=2.0.1
35
36
  Requires-Dist: watchdog>=4.0.0
36
37
  Requires-Dist: rich>=13.0.0
37
38
  Requires-Dist: pyyaml>=6.0.0
@@ -54,44 +55,36 @@ Dynamic: license-file
54
55
  <h1 align="center">CortexCode</h1>
55
56
  <p align="center">
56
57
  <strong>Lightweight code indexing for AI assistants</strong><br>
57
- Save 90%+ tokens by giving AI agents structured context instead of raw source files.
58
+ Build a searchable codegraph, generate documentation, and give AI assistants grounded context.
58
59
  </p>
59
60
  </p>
60
61
 
61
- <p align="center">
62
- <a href="https://pypi.org/project/cortexcode/"><img src="https://img.shields.io/pypi/v/cortexcode?style=flat-square&color=blue" alt="PyPI"></a>
63
- <a href="https://pypi.org/project/cortexcode/"><img src="https://img.shields.io/pypi/pyversions/cortexcode?style=flat-square" alt="Python"></a>
64
- <a href="https://github.com/naveen-joshi/cortexcode/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"></a>
65
- <a href="https://github.com/naveen-joshi/cortexcode"><img src="https://img.shields.io/github/stars/naveen-joshi/cortexcode?style=flat-square" alt="Stars"></a>
66
- <a href="https://marketplace.visualstudio.com/items?itemName=cortexcode.cortexcode-vscode"><img src="https://img.shields.io/visual-studio-marketplace/v/cortexcode.cortexcode-vscode?style=flat-square" alt="VS Code"></a>
67
- </p>
68
-
69
62
  ---
70
63
 
71
64
  ## The Problem
72
65
 
73
66
  AI coding assistants (Copilot, Cursor, Windsurf, etc.) need to understand your codebase. The current approach: **dump entire source files into the context window**. This is:
74
67
 
75
- - **Expensive** — A 150-file project can cost 200K+ tokens per query
76
- - **Slow** — More tokens = slower responses
77
- - **Wasteful** — Most of those tokens are irrelevant to the question
68
+ - **Noisy** — Important signals are buried inside large files
69
+ - **Fragile** — Assistants miss architecture, relationships, and runtime surface area
70
+ - **Hard to reuse** — Raw file dumps do not become a durable project map
78
71
 
79
72
  ## The Solution
80
73
 
81
- CortexCode indexes your codebase using **AST parsing** (tree-sitter) and provides a structured, searchable index. Instead of feeding 200K tokens of raw code, you feed **~500 tokens of relevant context**.
74
+ CortexCode indexes your codebase using **AST parsing** (tree-sitter) and builds a structured, searchable codegraph. You can explore symbols, trace flows, generate docs and diagrams, produce CodeWiki pages, and connect AI tools through MCP.
82
75
 
83
76
  ```
84
77
  ┌─────────────────────────────────────────────────┐
85
78
  │ Without CortexCode With CortexCode │
86
79
  │ │
87
- 200,000 tokens500 tokens
88
- $0.006/query $0.00002/query
89
- All files dumped Only relevant syms
90
- │ No structure Call graph + types
80
+ Raw files onlySearchable codegraph
81
+ Manual digging Linked symbols
82
+ Ad-hoc prompts Reusable context
83
+ │ No project map Docs + diagrams
91
84
  └─────────────────────────────────────────────────┘
92
85
  ```
93
86
 
94
- Run `cortexcode stats` on your project to see your actual savings.
87
+ Run `cortexcode index` on your project to generate the codegraph and start exploring.
95
88
 
96
89
  ## Quick Start
97
90
 
@@ -107,9 +100,6 @@ cd cortexcode && pip install -e .
107
100
  cd your-project
108
101
  cortexcode index
109
102
 
110
- # See token savings
111
- cortexcode stats
112
-
113
103
  # Get context for AI
114
104
  cortexcode context "handleAuth"
115
105
 
@@ -117,6 +107,22 @@ cortexcode context "handleAuth"
117
107
  cortexcode docs --open
118
108
  ```
119
109
 
110
+ ### Post-Index Wizard
111
+
112
+ After running `cortexcode index`, an interactive wizard can guide you through generating outputs:
113
+
114
+ ```bash
115
+ cortexcode index # Run index, then wizard prompts
116
+ cortexcode index --force-wizard # Force wizard in non-TTY mode
117
+ cortexcode index --no-post-index-wizard # Skip wizard
118
+ ```
119
+
120
+ **Wizard features:**
121
+ - Multi-select checkbox for docs, diagrams, AI docs, CodeWiki, reports, visualization, dashboard, MCP setup
122
+ - Smart recommendations based on detected project shape
123
+ - Follow-up prompts for diagram types, AI doc types, wiki options
124
+ - Preference persistence — saves your choices for future runs
125
+
120
126
  ## Features
121
127
 
122
128
  ### Multi-Language AST Indexing
@@ -142,24 +148,14 @@ Parses source code into structured symbols using tree-sitter grammars.
142
148
  - **Entities** — Database models and ORM definitions
143
149
  - **Framework Detection** — React components, Angular services, etc.
144
150
 
145
- ### Token Savings
151
+ ### Project Exploration & Context
146
152
 
147
- CortexCode dramatically reduces the tokens needed to give AI assistants useful context:
153
+ Use CortexCode to inspect structure and answer practical questions about a codebase:
148
154
 
149
- ```bash
150
- $ cortexcode stats
151
-
152
- ╭──────── Token Savings Analysis ────────╮
153
- │ Source files 154 files │
154
- │ Raw project tokens 203,847 │
155
- │ Full index tokens 45,291 │
156
- │ Context query tokens 487 │
157
- │ │
158
- │ Tokens saved 203,360 │
159
- │ Savings 99.8% │
160
- │ Compression ratio 418.6x │
161
- ╰─────────────────────────────────────────╯
162
- ```
155
+ - **Context lookup** — Pull relevant symbols and files for a query
156
+ - **Call graph tracing** — Follow how behavior moves across modules
157
+ - **Architecture visibility** — Inspect routes, entities, dependencies, and layers
158
+ - **Reusable index** Keep a structured project map that tools can build on
163
159
 
164
160
  ### Interactive HTML Documentation
165
161
 
@@ -194,7 +190,6 @@ cortexcode wiki --no-modules # Skip per-module pages (faster)
194
190
  - **Mermaid diagrams** — Auto-generated flow diagrams
195
191
  - **Concept mapping** — Maps technical concepts to symbols and files
196
192
  - **Concept search** — Ask "how does authentication work?" and get grounded answers
197
- - **Token tracking** — See exactly how many tokens each page used
198
193
 
199
194
  **Output:** `.cortexcode/wiki/index.html` — Open directly or serve locally.
200
195
 
@@ -245,12 +240,12 @@ CortexCode supports both a short form (`cc`) and full form (`cortexcode`). Comma
245
240
  | Command | Description |
246
241
  |---------|-------------|
247
242
  | `cc analyze context [query]` | Get relevant context for AI |
248
- | `cc analyze context [query] --tokens` | Show token savings for query |
243
+ | `cc analyze context [query] --tokens` | Show context size estimates for a query |
249
244
  | `cc analyze search [query]` | Grep-like symbol search with type/file filters |
250
245
  | `cc analyze find [query]` | Semantic search by meaning ("auth handler") |
251
246
  | `cc analyze diff` | Show changed symbols since last commit |
252
247
  | `cc analyze diff --ref HEAD~3` | Compare against any git ref |
253
- | `cc analyze stats` | Show project stats and token savings |
248
+ | `cc analyze stats` | Show project index and analysis stats |
254
249
  | `cc analyze scan` | Scan dependencies for security warnings |
255
250
  | `cc analyze trace <symbol>` | Trace code flow through call graph |
256
251
  | `cc analyze flow <concept>` | Analyze code flow grouped by file |
@@ -355,7 +350,7 @@ Add to `~/.windsurf/config.json`:
355
350
 
356
351
  ```bash
357
352
  # Paste this output into your AI chat
358
- cortexcode context "useAuth" --tokens
353
+ cortexcode context "useAuth"
359
354
  ```
360
355
 
361
356
  ### 2. JSON Index (programmatic)
@@ -552,7 +547,6 @@ CortexCode respects `.gitignore` files (including nested ones) and has built-in
552
547
  ## Roadmap
553
548
 
554
549
  - [x] MCP server for direct AI agent integration
555
- - [x] Tiktoken-based accurate token counting
556
550
  - [x] Semantic search over symbols (TF-IDF + synonym expansion)
557
551
  - [x] Cross-file type inference
558
552
  - [x] Git diff-aware context (show only changed symbols)
@@ -6,9 +6,9 @@ cortexcode/context.py,sha256=CEX__dc21fdSLNg-Ec48ROTBILzEDufKPKANah_0HKk,342
6
6
  cortexcode/dashboard.py,sha256=NNK4EhgBo3gBuwwntT5wIi7ZlX1ThHVlbxn6q0c-HdE,5361
7
7
  cortexcode/docs.py,sha256=GT6GxWK7r-Zq-9VqxaLFAo9Yzh11kgwUgdaz46H4NmI,912
8
8
  cortexcode/git_diff.py,sha256=YEEZM2yKYztE-T7rI5iBf2NluY4G1WIjlp-DprZ-6Rs,5742
9
- cortexcode/indexer.py,sha256=uDSpGXOX-3q3dBgX3uUTakGKPsknfhmgod-kXCXOivo,8575
9
+ cortexcode/indexer.py,sha256=lcw6iGF1rPpBpvyX9XnAKBnjl6Yatv9RpB78VceQFyo,10477
10
10
  cortexcode/lsp_server.py,sha256=mAPk78OVWQodBjBI3WhqEjrwjXrb0MF630IYDZ8_MLM,11102
11
- cortexcode/main.py,sha256=tDd0mPBhBp5x1eWsdcouzREPJUZ2npphfpJ7UA5Z884,43069
11
+ cortexcode/main.py,sha256=G7iiyXkE9c5Oe6cI4HnH6g4yXOCRih2O_Hihx9an82M,43380
12
12
  cortexcode/performance.py,sha256=tkr7RMjIuutQqcNO8wgLBNzNMFXti7y9CkiSetPam_A,706
13
13
  cortexcode/plugins.py,sha256=6WsmnvRT7D_coxD6YgwB-Dpu_iKOS89FNnT-gkgK4-8,6298
14
14
  cortexcode/semantic_search.py,sha256=PO1FjbQhKf6EGfwtwHcdqVXL5W_xMr4e7GxuctdrFzM,8745
@@ -26,7 +26,7 @@ cortexcode/advanced_analysis/advanced_analysis_security.py,sha256=ZkXbzZHim7KnHs
26
26
  cortexcode/ai_docs/__init__.py,sha256=W68LGb82g2wVOsvm3DfMd9esntVfgZH4cwDzcAxRpGg,472
27
27
  cortexcode/ai_docs/config.py,sha256=7qwDS2t7q6t-eQepBi51PsV9uTggrtzsD_JI7jH8_6k,3388
28
28
  cortexcode/ai_docs/doc_cache.py,sha256=Im4nhro2sT0uytq0rajRntd3v6KUCmspS4VDmQWeHl8,1220
29
- cortexcode/ai_docs/doc_generator.py,sha256=O8Yy_okZf5Kcc2sLK6LN87JyWAZCS83H-l0dwdMbu1o,7018
29
+ cortexcode/ai_docs/doc_generator.py,sha256=8zelx0xH6tCrEeAPyYuzWb5a7rejPT5AjfmPu2znZ64,7108
30
30
  cortexcode/ai_docs/doc_lookup.py,sha256=L_q4o_tYb_UK3l7CT-ZQBdKZPzCL7oH30LkaCxYoVdI,1067
31
31
  cortexcode/ai_docs/doc_models.py,sha256=jNGOposSsYrcrzC7UJ_81PP2yP9lzZtJ3dsxfbV7cio,339
32
32
  cortexcode/ai_docs/explainer.py,sha256=rMPab2N9UkfeY6NrmiaM2OZNElRUKqv7ZXVuzUnq6bo,9557
@@ -53,13 +53,13 @@ cortexcode/cli/cli_explain.py,sha256=q_Gh1m3XXzcJXa9px1OOMPlLx3-CqJ0x_VK3TgciltE
53
53
  cortexcode/cli/cli_find.py,sha256=EueM1_fYWgDIRD5mUJ798lN5QW3UOyz7gW0jeEMytlM,1314
54
54
  cortexcode/cli/cli_githook.py,sha256=TJoVrTlRDSeL9SzQQzKDWy8lVAR8utHxldUlawCVE-o,3315
55
55
  cortexcode/cli/cli_impact.py,sha256=KXAt8asPE9HoQmixGaBGPX7iRBGVjN7jalFnM8DDNXY,2080
56
- cortexcode/cli/cli_index.py,sha256=52xDZiIQsS3Zk2QKZ0TRkNverJfGbAQ4TR3pQyTsvuw,3925
56
+ cortexcode/cli/cli_index.py,sha256=LR79rBBPQpjt1c70eTyjVUKX3EIs-SYo3vk0rRE4Pj0,23524
57
57
  cortexcode/cli/cli_jobs.py,sha256=Zy_p2xS5KNWSlZ0dEgYbJbUNMV4CMuN22s_PwHUYptc,4820
58
58
  cortexcode/cli/cli_package.py,sha256=YQ3BI2-BUTvy7zaLHulFRxpFQzwt5pBwPi55XFs83CI,7688
59
59
  cortexcode/cli/cli_report.py,sha256=4scGu4Tv_mq7jRMu1KJ8YxBWrdMovOJmfZEfwu5olnU,562
60
60
  cortexcode/cli/cli_scan.py,sha256=Br3-8DLnnOspnLCHFi5Jg8_XBViRaQjHiBcazkQGadA,1224
61
61
  cortexcode/cli/cli_search.py,sha256=FKBjdXAgj3wu4DM_n175HMTPHjTIuZDcEQ5nYnEjOz8,1925
62
- cortexcode/cli/cli_servers.py,sha256=wkfjVn0kc9-8PwFYrMhCLxPUp9p9vUXZfb4gwrAr3A8,5718
62
+ cortexcode/cli/cli_servers.py,sha256=xnK4xL5qrr2FS-dw0z1EEOgMKMR7LSRmxPew91Wc51g,7717
63
63
  cortexcode/cli/cli_shell.py,sha256=J6ji1zhM4g-JqbUrzD43lWqsqvSkAGYvTiiyWg9OZso,5173
64
64
  cortexcode/cli/cli_stats.py,sha256=P7RrX4CcqslGWQ1ZDUjOfPojDZ6hYVXwhRUbNvL3Iq4,601
65
65
  cortexcode/cli/cli_support.py,sha256=Y05HqRtMeNsGPJruJZj8g0PLflEHRNByoFBWyf_1po4,954
@@ -69,7 +69,7 @@ cortexcode/cli/cli_wiki.py,sha256=VLKxFkqWsWa9Dp4b4k5aV8B8d607pDcnwElqTXHIf2c,58
69
69
  cortexcode/cli/cli_workspace.py,sha256=_l3aGwHfPvI-UfriM8FfztAwINN5dnlV2UFQ5AtWjiQ,3805
70
70
  cortexcode/context/__init__.py,sha256=vHyIXWJ7LRvtgh1PVywTUZ3JkFLRgJT8OD_xj9WW2YU,430
71
71
  cortexcode/context/context_format.py,sha256=4cH-MEDZbdDTYunUxDqgB0Wvm7CP9b_lVkyVkkC9FxM,623
72
- cortexcode/context/context_query.py,sha256=JDWG9-LTxGTrmnKfAetb0mR-CqBBJKtx5phAs-ECimE,5934
72
+ cortexcode/context/context_query.py,sha256=iu4nUdRS5-gQ2CKJBB5EcjeNsSsAZTDhpAD3DGfIogA,7047
73
73
  cortexcode/context/context_tokens.py,sha256=xN69ADHUn7rUlKmDJN6q7HY_I4fX9QzJyoz0Hkv6WEw,2217
74
74
  cortexcode/diagrams/__init__.py,sha256=xKmWh67Kt8ngSnNuOQ1ff2uwzZJ9boxepQsPNu6vm7I,1143
75
75
  cortexcode/diagrams/architecture.py,sha256=xx5yekiAF-VF0LxC9dbXsKjKskOlXQRuS6jlefoRPY8,2482
@@ -135,7 +135,7 @@ cortexcode/knowledge/snippets.py,sha256=8cpetnmY-KdFj4aWSt55mmO6JJ502CU6r01Jpjpx
135
135
  cortexcode/knowledge/usage.py,sha256=U6_wuXMqsNBSyZtOeerWtexoAu6ma1Eb8yds7HpTgCs,3549
136
136
  cortexcode/mcp/__init__.py,sha256=ibMesuUdMZtpjx294SNPZi5oNitZmj3DJYCOntw9Aw4,699
137
137
  cortexcode/mcp/mcp_protocol.py,sha256=grKhPjzbYSi7GI93WifnZ4NHYW8K-NdYeGUZtqrN4Zc,379
138
- cortexcode/mcp/mcp_registry.py,sha256=nE9_6AscXnblfDGtGS1uXGx85spw1bjqN1Db5Uu-5Yk,9438
138
+ cortexcode/mcp/mcp_registry.py,sha256=9pRGfkHxnQB1pT6MBto9RwOo9AdEXoop8cX_-qnkjDw,9546
139
139
  cortexcode/mcp/mcp_server.py,sha256=RM7PGxRqpijgxc_GS3r-8ax42myEl-uDnKiXgRkpPx4,2835
140
140
  cortexcode/mcp/mcp_tool_handlers.py,sha256=QOA1-dWMl_iE65y1y7Gq79fICbSq4H1n0v5HqLsbdWw,9304
141
141
  cortexcode/mcp/mcp_transport.py,sha256=-ep5iL4z6lP0gahFq_3fb_fruh8kNrQMEBpK1HoPCpo,1864
@@ -165,9 +165,9 @@ cortexcode/terminal/headers.py,sha256=Yk9IGniMCiZD45V_yc1mjM3WjHimxoe9vL3CE5NAWv
165
165
  cortexcode/terminal/prompts.py,sha256=KRwjLipbO5Ihv3ycp1T2nGQtoGmKPP1ZNXb3BUz4oF4,359
166
166
  cortexcode/terminal/reports.py,sha256=4nLu0fBqDWKuQowX78VqX1nBaJl2zrTt9vEYCVP--O0,10135
167
167
  cortexcode/terminal/stats.py,sha256=PooaYP-OqaewhwypnUHESOrdwwICvggb3n2HdqEQxrc,1365
168
- cortexcode-0.8.1.dist-info/licenses/LICENSE,sha256=DWE8vkHgP2ChQTJHFWtrutLbuHQUBsU_InyfesP4neo,1067
169
- cortexcode-0.8.1.dist-info/METADATA,sha256=UsS0GRN1LsxE2UpgtZZwnMmf5lcLmr8r_SLDuMWsEGw,21188
170
- cortexcode-0.8.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
171
- cortexcode-0.8.1.dist-info/entry_points.txt,sha256=3TfwRp96e7ICJDUoJpcAu3NXVGT6HcyvHVU3pQ_vftg,78
172
- cortexcode-0.8.1.dist-info/top_level.txt,sha256=r8FxzjLfKhRXXcORnECtGo7i2zKYXlV7v1XnIJ0SOc0,11
173
- cortexcode-0.8.1.dist-info/RECORD,,
168
+ cortexcode-0.9.1.dist-info/licenses/LICENSE,sha256=DWE8vkHgP2ChQTJHFWtrutLbuHQUBsU_InyfesP4neo,1067
169
+ cortexcode-0.9.1.dist-info/METADATA,sha256=D4Xoobbt4QrM5hxIQiU-ZTz8SRyE2gcGZ3Ud1uxsB5s,20648
170
+ cortexcode-0.9.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
171
+ cortexcode-0.9.1.dist-info/entry_points.txt,sha256=3TfwRp96e7ICJDUoJpcAu3NXVGT6HcyvHVU3pQ_vftg,78
172
+ cortexcode-0.9.1.dist-info/top_level.txt,sha256=r8FxzjLfKhRXXcORnECtGo7i2zKYXlV7v1XnIJ0SOc0,11
173
+ cortexcode-0.9.1.dist-info/RECORD,,