coreinsight-cli 0.2.7__tar.gz → 0.2.8__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.
Files changed (30) hide show
  1. {coreinsight_cli-0.2.7/coreinsight_cli.egg-info → coreinsight_cli-0.2.8}/PKG-INFO +1 -1
  2. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/analyzer.py +106 -15
  3. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/main.py +31 -4
  4. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/memory.py +71 -0
  5. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/tui.py +281 -53
  6. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8/coreinsight_cli.egg-info}/PKG-INFO +1 -1
  7. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/pyproject.toml +1 -1
  8. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/LICENSE +0 -0
  9. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/README.md +0 -0
  10. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/Dockerfile.cpp-sandbox +0 -0
  11. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/Dockerfile.python-sandbox +0 -0
  12. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/__init__.py +0 -0
  13. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/config.py +0 -0
  14. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/__init__.py +0 -0
  15. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/bad_loop.py +0 -0
  16. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/data_processor.py +0 -0
  17. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/demo/slow.cpp +0 -0
  18. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/hardware.py +0 -0
  19. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/indexer.py +0 -0
  20. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/parser.py +0 -0
  21. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/profiler.py +0 -0
  22. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/prompts.py +0 -0
  23. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/sandbox.py +0 -0
  24. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight/scanner.py +0 -0
  25. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/SOURCES.txt +0 -0
  26. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/dependency_links.txt +0 -0
  27. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/entry_points.txt +0 -0
  28. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/requires.txt +0 -0
  29. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/coreinsight_cli.egg-info/top_level.txt +0 -0
  30. {coreinsight_cli-0.2.7 → coreinsight_cli-0.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coreinsight-cli
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA
5
5
  Author: Varun Jani
6
6
  License: GPL-3.0-or-later
@@ -14,6 +14,35 @@ from langchain_anthropic import ChatAnthropic
14
14
 
15
15
  from coreinsight.prompts import SYSTEM_PROMPT, ANALYSIS_TEMPLATE, HARNESS_ADDENDUM
16
16
 
17
+ # Phrases that appear at the start of a truncated LLM response
18
+ _TRUNCATION_HINTS = (
19
+ "context length",
20
+ "context_length_exceeded",
21
+ "maximum context",
22
+ "token limit",
23
+ "finish_reason: length",
24
+ "finish_reason\":\"length",
25
+ )
26
+
27
+ def _is_truncated(raw: str) -> bool:
28
+ """
29
+ Returns True if the raw LLM output looks like it was cut off mid-generation.
30
+ Catches both explicit error messages and structural truncation signs.
31
+ """
32
+ if not raw or len(raw.strip()) < 20:
33
+ return True
34
+ low = raw.lower()
35
+ if any(hint in low for hint in _TRUNCATION_HINTS):
36
+ return True
37
+ stripped = raw.strip()
38
+ # JSON truncation: opened but never closed
39
+ if stripped.startswith("{") and not stripped.endswith("}"):
40
+ return True
41
+ # Code truncation: opens a block but ends mid-statement
42
+ if stripped.endswith(("...", "/*", "//", "\"", "'")):
43
+ return True
44
+ return False
45
+
17
46
  logger = logging.getLogger(__name__)
18
47
 
19
48
 
@@ -163,12 +192,15 @@ class AnalyzerAgent:
163
192
  self.json_llm = self.base_llm
164
193
 
165
194
  elif provider == "local_server":
166
- base_url = api_keys.get("local_url", "http://localhost:1234/v1")
195
+ from coreinsight.prompts import ModelTier
196
+ base_url = api_keys.get("local_url", "http://localhost:1234/v1")
197
+ _max_tokens = 2048 if model_tier == ModelTier.SMALL else 4096
167
198
  self.base_llm = ChatOpenAI(
168
199
  model=model_name,
169
200
  api_key="not-needed",
170
201
  base_url=base_url,
171
202
  temperature=0.1,
203
+ max_tokens=_max_tokens,
172
204
  model_kwargs={"response_format": {"type": "json_object"}},
173
205
  )
174
206
  self.json_llm = self.base_llm
@@ -196,11 +228,20 @@ class AnalyzerAgent:
196
228
  self.json_llm = self.base_llm
197
229
 
198
230
  else: # Ollama default
231
+ from coreinsight.prompts import ModelTier
232
+ # Small models (7B) typically have 4096 native context.
233
+ # Asking for more causes silent degradation or OOM on the host.
234
+ # Medium/large local models can handle 8192 comfortably.
235
+ _ctx = 4096 if model_tier == ModelTier.SMALL else 8192
236
+ # num_predict: small models need room for JSON + code in one shot.
237
+ # Capping at 2048 for small prevents runaway generation that hits
238
+ # the limit mid-JSON and returns truncated garbage.
239
+ _predict = 2048 if model_tier == ModelTier.SMALL else 4096
199
240
  self.base_llm = ChatOllama(
200
241
  model=model_name,
201
242
  temperature=0.1,
202
- num_predict=4096,
203
- num_ctx=8192,
243
+ num_predict=_predict,
244
+ num_ctx=_ctx,
204
245
  )
205
246
  self.json_llm = self.base_llm.bind(format="json")
206
247
 
@@ -258,14 +299,31 @@ class AnalyzerAgent:
258
299
  def _invoke_code_chain(self, template: str, variables: dict, language: str) -> str:
259
300
  """Shared invocation + extraction logic for harness and fix chains."""
260
301
  chain = PromptTemplate.from_template(template) | self.base_llm
261
- result = chain.invoke(variables)
302
+ try:
303
+ result = chain.invoke(variables)
304
+ except Exception as e:
305
+ err = str(e).lower()
306
+ if any(h in err for h in _TRUNCATION_HINTS):
307
+ raise RuntimeError(
308
+ f"Model hit its context limit. Try a smaller file, fewer functions, "
309
+ f"or a model with a larger context window. Detail: {e}"
310
+ ) from e
311
+ raise
262
312
  raw = result.content if hasattr(result, "content") else str(result)
263
- # Handle Anthropic returning a list of content blocks
264
313
  if isinstance(raw, list):
265
314
  raw = "\n".join(
266
315
  item["text"] if isinstance(item, dict) and "text" in item else str(item)
267
316
  for item in raw
268
317
  )
318
+ if _is_truncated(raw):
319
+ logger.warning(
320
+ f"LLM output appears truncated (len={len(raw)}). "
321
+ f"Model likely hit its context/predict limit."
322
+ )
323
+ raise RuntimeError(
324
+ "Model output was truncated — hit context or token limit. "
325
+ "Try a model with a larger context window, or reduce the function size."
326
+ )
269
327
  return self._extract_executable_code(raw)
270
328
 
271
329
  def generate_harness(
@@ -421,12 +479,14 @@ def _build_llm(provider: str, model_name: str, api_keys: dict):
421
479
  return llm, llm
422
480
 
423
481
  if provider == "local_server":
424
- base_url = api_keys.get("local_url", "http://localhost:1234/v1")
482
+ base_url = api_keys.get("local_url", "http://localhost:1234/v1")
483
+ _max_tokens = api_keys.pop("_predict", 4096) # reuse same key as Ollama path
425
484
  llm = ChatOpenAI(
426
485
  model=model_name,
427
486
  api_key="not-needed",
428
487
  base_url=base_url,
429
488
  temperature=0.1,
489
+ max_tokens=_max_tokens,
430
490
  model_kwargs={"response_format": {"type": "json_object"}},
431
491
  )
432
492
  return llm, llm
@@ -452,16 +512,33 @@ def _build_llm(provider: str, model_name: str, api_keys: dict):
452
512
  )
453
513
  return llm, llm
454
514
 
455
- # Ollama default
515
+ # Ollama default — context and predict budget are passed in from the
516
+ # calling agent which knows its own model_tier.
517
+ # Default to medium-safe values; callers override via kwargs if needed.
518
+ _ctx = api_keys.pop("_ctx", 8192)
519
+ _predict = api_keys.pop("_predict", 4096)
456
520
  base = ChatOllama(
457
521
  model=model_name,
458
522
  temperature=0.1,
459
- num_predict=4096,
460
- num_ctx=8192,
523
+ num_predict=_predict,
524
+ num_ctx=_ctx,
461
525
  )
462
526
  return base, base.bind(format="json")
463
527
 
464
528
 
529
+ def _build_llm_tiered(provider: str, model_name: str, api_keys: dict, model_tier: str):
530
+ """Wraps _build_llm with tier-aware context settings for local providers."""
531
+ from coreinsight.prompts import ModelTier
532
+ keys = dict(api_keys or {})
533
+ if provider == "ollama":
534
+ keys["_ctx"] = 4096 if model_tier == ModelTier.SMALL else 8192
535
+ keys["_predict"] = 2048 if model_tier == ModelTier.SMALL else 4096
536
+ elif provider == "local_server":
537
+ # max_tokens controls response length — context window is server-side
538
+ keys["_predict"] = 2048 if model_tier == ModelTier.SMALL else 4096
539
+ return _build_llm(provider, model_name, keys)
540
+
541
+
465
542
  class BottleneckAgent:
466
543
  """
467
544
  Agent 1 — analysis only.
@@ -480,7 +557,7 @@ class BottleneckAgent:
480
557
  from coreinsight.prompts import BOTTLENECK_TEMPLATE, SYSTEM_PROMPT
481
558
  self.model_tier = model_tier
482
559
  self.parser = JsonOutputParser(pydantic_object=AuditResult)
483
- self._base_llm, self._json_llm = _build_llm(provider, model_name, api_keys)
560
+ self._base_llm, self._json_llm = _build_llm_tiered(provider, model_name, api_keys, model_tier)
484
561
 
485
562
  self._prompt = PromptTemplate(
486
563
  template=BOTTLENECK_TEMPLATE,
@@ -544,7 +621,7 @@ class OptimizerAgent:
544
621
  ) -> None:
545
622
  from coreinsight.prompts import OPTIMIZER_TEMPLATE
546
623
  self.model_tier = model_tier
547
- self._base_llm, _ = _build_llm(provider, model_name, api_keys)
624
+ self._base_llm, _ = _build_llm_tiered(provider, model_name, api_keys, model_tier)
548
625
  self._template = OPTIMIZER_TEMPLATE
549
626
 
550
627
  def _extract_code(self, raw: str) -> str:
@@ -620,7 +697,7 @@ class HarnessAgent:
620
697
  HARNESS_ADDENDUM_MULTI,
621
698
  )
622
699
  self.model_tier = model_tier
623
- self._base_llm, _ = _build_llm(provider, model_name, api_keys)
700
+ self._base_llm, _ = _build_llm_tiered(provider, model_name, api_keys, model_tier)
624
701
  self._harness_tmpl = HARNESS_TEMPLATE_MULTI + HARNESS_ADDENDUM_MULTI.get(model_tier, "")
625
702
  self._fix_tmpl = FIX_TEMPLATE_MULTI + HARNESS_ADDENDUM_MULTI.get(model_tier, "")
626
703
 
@@ -638,14 +715,28 @@ class HarnessAgent:
638
715
 
639
716
  def _invoke(self, template: str, variables: dict) -> str:
640
717
  chain = PromptTemplate.from_template(template) | self._base_llm
641
- result = chain.invoke(variables)
642
- raw = result.content if hasattr(result, "content") else str(result)
718
+ try:
719
+ result = chain.invoke(variables)
720
+ except Exception as e:
721
+ err = str(e).lower()
722
+ if any(h in err for h in _TRUNCATION_HINTS):
723
+ raise RuntimeError(
724
+ f"Model hit its context limit during harness generation. "
725
+ f"Detail: {e}"
726
+ ) from e
727
+ raise
728
+ raw = result.content if hasattr(result, "content") else str(result)
643
729
  if isinstance(raw, list):
644
730
  raw = "\n".join(
645
731
  item["text"] if isinstance(item, dict) and "text" in item
646
732
  else str(item)
647
733
  for item in raw
648
734
  )
735
+ if _is_truncated(raw):
736
+ raise RuntimeError(
737
+ "Harness output was truncated — model hit its token limit. "
738
+ "Switching to fix loop with truncation note."
739
+ )
649
740
  return self._extract_code(raw)
650
741
 
651
742
  def _check_speedup(self, success: bool, logs: str) -> bool:
@@ -738,7 +829,7 @@ class TestCaseAgent:
738
829
  model_tier: str,
739
830
  ) -> None:
740
831
  self.model_tier = model_tier
741
- self._base_llm, _ = _build_llm(provider, model_name, api_keys)
832
+ self._base_llm, _ = _build_llm_tiered(provider, model_name, api_keys, model_tier)
742
833
 
743
834
  def generate(
744
835
  self,
@@ -272,8 +272,15 @@ def process_function(func: dict, language: str, agent: AnalyzerAgent, sandbox: C
272
272
  return func_name, result, (success, logs, plot_data), None, verification, profiler_result, None, is_valid_optimization
273
273
 
274
274
  except Exception as e:
275
+ err_str = str(e)
276
+ if "context" in err_str.lower() and "limit" in err_str.lower():
277
+ _log(func_name, f"Context limit hit: {e}", style="bold yellow")
278
+ return func_name, None, None, (
279
+ f"⚠️ Context limit: {err_str}\n"
280
+ f"Try a model with a larger context window, or split the function."
281
+ ), None, None, None, False
275
282
  _log(func_name, f"Failed: {e}", style="bold red")
276
- return func_name, None, None, f"❌ Analysis failed: {str(e)}", None, None, None, False
283
+ return func_name, None, None, f"❌ Analysis failed: {err_str}", None, None, None, False
277
284
 
278
285
  def parse_csv_logs(logs: str):
279
286
  """Safely extracts CSV data from the sandbox logs."""
@@ -803,12 +810,23 @@ def run_demo(lang: str = "python", no_docker: bool = False):
803
810
 
804
811
  run_analysis(str(demo_dir / entry_file), no_docker=no_docker)
805
812
 
806
- def _run_memory_cmd(clear: bool):
813
+ def _run_memory_cmd(clear: bool, export_path: str = None, export_fmt: str = "csv"):
807
814
  from coreinsight.memory import OptimizationMemory, MEMORY_DIR
808
815
  import shutil
809
816
 
810
817
  mem = OptimizationMemory()
811
818
 
819
+ if export_path:
820
+ count = mem.export(export_path, fmt=export_fmt)
821
+ if count:
822
+ console.print(
823
+ f"[bold green]✅ Exported {count} optimization(s) to "
824
+ f"[cyan]{export_path}[/cyan][/bold green]"
825
+ )
826
+ else:
827
+ console.print("[yellow]Nothing to export — memory store is empty.[/yellow]")
828
+ return
829
+
812
830
  if clear:
813
831
  if MEMORY_DIR.exists():
814
832
  shutil.rmtree(MEMORY_DIR, ignore_errors=True)
@@ -930,7 +948,12 @@ def main_cli():
930
948
  index_parser.add_argument("--dir", default=".", help="Directory to index")
931
949
 
932
950
  memory_parser = subparsers.add_parser("memory", help="Inspect or clear the local optimization memory")
933
- memory_parser.add_argument("--clear", action="store_true", help="Wipe the memory store")
951
+ memory_parser.add_argument("--clear", action="store_true", help="Wipe the memory store")
952
+ memory_parser.add_argument("--export", dest="export_path", default=None,
953
+ help="Export memory to file (e.g. --export optimizations.csv)")
954
+ memory_parser.add_argument("--format", dest="export_fmt", default="csv",
955
+ choices=["csv", "md"],
956
+ help="Export format: csv (default) or md")
934
957
 
935
958
  view_parser = subparsers.add_parser("view", help="Launch the interactive TUI")
936
959
  view_parser.add_argument("--dir", default=".", help="Starting directory (default: current)")
@@ -954,7 +977,11 @@ def main_cli():
954
977
  elif args.command == "analyze":
955
978
  run_analysis(args.file, no_docker=getattr(args, "no_docker", False))
956
979
  elif args.command == "memory":
957
- _run_memory_cmd(getattr(args, "clear", False))
980
+ _run_memory_cmd(
981
+ clear=getattr(args, "clear", False),
982
+ export_path=getattr(args, "export_path", None),
983
+ export_fmt=getattr(args, "export_fmt", "csv"),
984
+ )
958
985
  elif args.command == "scan":
959
986
  scanner = ProjectScanner(args.dir)
960
987
  scanner.scan_project(max_results=args.top)
@@ -186,6 +186,77 @@ class OptimizationMemory:
186
186
  logger.debug(f"Memory lookup failed: {exc}")
187
187
  return None
188
188
 
189
+ def export(self, output_path: str, fmt: str = "csv") -> int:
190
+ """
191
+ Export all stored optimizations to CSV or Markdown.
192
+ Returns the number of records written, or 0 on failure.
193
+ """
194
+ import csv as _csv
195
+ from pathlib import Path as _Path
196
+
197
+ if not self._ensure_db():
198
+ return 0
199
+
200
+ try:
201
+ all_records = self._collection.get(include=["metadatas"])
202
+ metadatas = all_records.get("metadatas", []) or []
203
+ except Exception as e:
204
+ logger.error(f"Export failed reading store: {e}")
205
+ return 0
206
+
207
+ if not metadatas:
208
+ return 0
209
+
210
+ # Sort most recent first
211
+ metadatas = sorted(
212
+ metadatas,
213
+ key=lambda m: m.get("timestamp", ""),
214
+ reverse=True,
215
+ )
216
+
217
+ out = _Path(output_path)
218
+
219
+ if fmt == "csv":
220
+ with open(out, "w", newline="", encoding="utf-8") as f:
221
+ writer = _csv.writer(f)
222
+ writer.writerow([
223
+ "func_name", "language", "severity", "issue",
224
+ "avg_speedup", "correctness_cases",
225
+ "hardware_evidence", "verified_at", "optimized_code",
226
+ ])
227
+ for m in metadatas:
228
+ writer.writerow([
229
+ m.get("func_name", ""),
230
+ m.get("language", ""),
231
+ m.get("severity", ""),
232
+ m.get("issue", ""),
233
+ m.get("avg_speedup", ""),
234
+ m.get("correctness_cases",""),
235
+ m.get("profiler_summary", ""),
236
+ m.get("timestamp", "")[:19].replace("T", " "),
237
+ m.get("optimized_code", ""),
238
+ ])
239
+
240
+ elif fmt == "md":
241
+ with open(out, "w", encoding="utf-8") as f:
242
+ f.write("# CoreInsight Optimization Memory Export\n\n")
243
+ for i, m in enumerate(metadatas, 1):
244
+ lang = m.get("language", "")
245
+ f.write(f"## {i}. `{m.get('func_name', 'unknown')}` ({lang})\n\n")
246
+ f.write(f"- **Severity:** {m.get('severity', '')}\n")
247
+ f.write(f"- **Issue:** {m.get('issue', '')}\n")
248
+ f.write(f"- **Avg speedup:** {float(m.get('avg_speedup', 0)):.2f}x\n")
249
+ f.write(f"- **Correctness cases:** {m.get('correctness_cases', '')}\n")
250
+ f.write(f"- **Verified at:** {m.get('timestamp', '')[:19].replace('T', ' ')}\n")
251
+ if m.get("profiler_summary"):
252
+ f.write(f"- **Hardware evidence:** {m.get('profiler_summary')}\n")
253
+ code = m.get("optimized_code", "").strip()
254
+ if code:
255
+ f.write(f"\n```{lang}\n{code}\n```\n")
256
+ f.write("\n---\n\n")
257
+
258
+ return len(metadatas)
259
+
189
260
  def store(
190
261
  self,
191
262
  original_code: str,
@@ -22,8 +22,10 @@ from textual.widgets import (
22
22
  DirectoryTree,
23
23
  Footer,
24
24
  Header,
25
+ Input,
25
26
  Label,
26
27
  RichLog,
28
+ Select,
27
29
  Static,
28
30
  Switch,
29
31
  )
@@ -107,9 +109,17 @@ class MemoryModal(ModalScreen):
107
109
  MemoryModal #memory-log {
108
110
  height: 1fr;
109
111
  }
110
- MemoryModal #close-memory {
112
+ MemoryModal #memory-buttons {
111
113
  margin-top: 1;
112
- width: 100%;
114
+ align: center middle;
115
+ }
116
+ MemoryModal #memory-buttons Button {
117
+ margin: 0 1;
118
+ }
119
+ MemoryModal #export-status {
120
+ height: 1;
121
+ margin-top: 1;
122
+ color: $success;
113
123
  }
114
124
  """
115
125
 
@@ -117,7 +127,11 @@ class MemoryModal(ModalScreen):
117
127
  with Container():
118
128
  yield Label("Optimization Memory", id="memory-title")
119
129
  yield RichLog(id="memory-log", highlight=True, markup=True)
120
- yield Button("Close [Esc]", id="close-memory", variant="default")
130
+ with Horizontal(id="memory-buttons"):
131
+ yield Button("Export CSV", id="export-csv", variant="primary")
132
+ yield Button("Export MD", id="export-md", variant="primary")
133
+ yield Button("Close [Esc]",id="close-memory",variant="default")
134
+ yield Label("", id="export-status")
121
135
 
122
136
  def on_mount(self) -> None:
123
137
  log = self.query_one("#memory-log", RichLog)
@@ -194,6 +208,27 @@ class MemoryModal(ModalScreen):
194
208
  @on(Button.Pressed, "#close-memory")
195
209
  def close(self) -> None:
196
210
  self.dismiss()
211
+
212
+ @on(Button.Pressed, "#export-csv")
213
+ def export_csv(self) -> None:
214
+ self._do_export("csv")
215
+
216
+ @on(Button.Pressed, "#export-md")
217
+ def export_md(self) -> None:
218
+ self._do_export("md")
219
+
220
+ def _do_export(self, fmt: str) -> None:
221
+ from pathlib import Path
222
+ from coreinsight.memory import OptimizationMemory
223
+
224
+ out = Path.cwd() / f"coreinsight_memory_export.{fmt}"
225
+ mem = OptimizationMemory()
226
+ count = mem.export(str(out), fmt=fmt)
227
+ status = self.query_one("#export-status", Label)
228
+ if count:
229
+ status.update(f"[green]Exported {count} record(s) to {out.name}[/green]")
230
+ else:
231
+ status.update("[yellow]Nothing to export.[/yellow]")
197
232
 
198
233
 
199
234
  # ---------------------------------------------------------------------------
@@ -253,65 +288,219 @@ class ConfirmModal(ModalScreen[bool]):
253
288
  # Settings modal
254
289
  # ---------------------------------------------------------------------------
255
290
 
256
- class SettingsModal(ModalScreen):
257
- """Read-only settings viewer. Directs user to coreinsight configure for changes."""
291
+ class ConfigureModal(ModalScreen):
292
+ """Full configure screen replaces coreinsight configure for TUI users."""
258
293
 
259
- BINDINGS = [Binding("escape,q", "dismiss", "Close")]
294
+ BINDINGS = [Binding("escape", "dismiss", "Cancel")]
260
295
 
261
296
  DEFAULT_CSS = """
262
- SettingsModal {
297
+ ConfigureModal {
263
298
  align: center middle;
264
299
  }
265
- SettingsModal > Container {
266
- width: 60;
267
- height: 22;
300
+ ConfigureModal > Container {
301
+ width: 70;
302
+ height: 36;
268
303
  background: $surface;
269
304
  border: thick $primary;
270
305
  padding: 1 2;
271
306
  }
272
- SettingsModal .setting-row {
273
- height: 1;
307
+ ConfigureModal .field-label {
308
+ color: $text-muted;
309
+ margin-top: 1;
310
+ }
311
+ ConfigureModal Input {
312
+ margin-bottom: 1;
313
+ }
314
+ ConfigureModal Select {
274
315
  margin-bottom: 1;
275
316
  }
276
- SettingsModal #close-settings {
317
+ ConfigureModal #configure-buttons {
318
+ margin-top: 1;
319
+ align: center middle;
320
+ }
321
+ ConfigureModal Button {
322
+ margin: 0 1;
323
+ min-width: 16;
324
+ }
325
+ ConfigureModal #configure-status {
326
+ height: 1;
327
+ color: $success;
277
328
  margin-top: 1;
278
- width: 100%;
279
329
  }
280
330
  """
281
331
 
332
+ def __init__(self, **kwargs):
333
+ super().__init__(**kwargs)
334
+ self._config = load_config()
335
+
282
336
  def compose(self) -> ComposeResult:
283
- config = load_config()
284
- pro_user = is_pro(config)
285
- provider = config.get("provider", "ollama")
286
- model = config.get("model_name", "llama3.2")
287
- tier = "Pro" if pro_user else "Free"
288
- tier_col = "bold green" if pro_user else "bold yellow"
337
+ config = self._config
338
+ provider = config.get("provider", "ollama")
339
+ model = config.get("model_name", "llama3.2")
289
340
  agent_mode = config.get("agent_mode", "auto")
341
+ local_url = config.get("api_keys", {}).get("local_url", "http://localhost:1234/v1")
342
+ api_keys = config.get("api_keys", {})
290
343
 
291
344
  with Container():
292
- yield Label("Current Configuration", id="settings-title")
293
- yield Static(f" Tier : [{tier_col}]{tier}[/{tier_col}]", classes="setting-row")
294
- yield Static(f" Provider : [cyan]{provider}[/cyan]", classes="setting-row")
295
- yield Static(f" Model : [cyan]{model}[/cyan]", classes="setting-row")
296
- yield Static(f" Agent mode: [cyan]{agent_mode}[/cyan]", classes="setting-row")
297
- yield Static("", classes="setting-row")
298
- yield Static(
299
- " To change settings, run:\n"
300
- " [cyan]coreinsight configure[/cyan]",
301
- classes="setting-row",
345
+ yield Label("Configure CoreInsight", id="configure-title")
346
+
347
+ yield Label("Provider", classes="field-label")
348
+ yield Select(
349
+ [
350
+ ("Ollama (local, free)", "ollama"),
351
+ ("Local Server (LM Studio etc)","local_server"),
352
+ ("OpenAI", "openai"),
353
+ ("Anthropic", "anthropic"),
354
+ ("Google Gemini", "google"),
355
+ ],
356
+ value=provider,
357
+ id="provider-select",
302
358
  )
303
- if not pro_user:
304
- yield Static(
305
- f"\n [yellow]Unlock Pro (free during beta):[/yellow]\n"
306
- f" [cyan underline]{PRO_WAITLIST_URL}[/cyan underline]",
307
- classes="setting-row",
308
- )
309
- yield Button("Close [Esc]", id="close-settings", variant="default")
310
359
 
311
- @on(Button.Pressed, "#close-settings")
312
- def close(self) -> None:
313
- self.dismiss()
360
+ yield Label("Model name", classes="field-label")
361
+ yield Input(value=model, placeholder="e.g. llama3.2, gpt-4o, claude-opus-4-5", id="model-input")
362
+
363
+ yield Label("Agent mode", classes="field-label")
364
+ yield Select(
365
+ [
366
+ ("Auto (recommended)", "auto"),
367
+ ("Single agent", "single"),
368
+ ("Multi agent", "multi"),
369
+ ],
370
+ value=agent_mode,
371
+ id="agent-mode-select",
372
+ )
373
+
374
+ # Local server URL — shown only for local_server
375
+ yield Label("Local server URL", classes="field-label", id="local-url-label")
376
+ yield Input(
377
+ value=local_url,
378
+ placeholder="http://localhost:1234/v1",
379
+ id="local-url-input",
380
+ )
314
381
 
382
+ # API key fields — shown per provider
383
+ yield Label("OpenAI API key", classes="field-label", id="openai-label")
384
+ yield Input(
385
+ value=api_keys.get("openai", ""),
386
+ placeholder="sk-...",
387
+ password=True,
388
+ id="openai-input",
389
+ )
390
+
391
+ yield Label("Anthropic API key", classes="field-label", id="anthropic-label")
392
+ yield Input(
393
+ value=api_keys.get("anthropic", ""),
394
+ placeholder="sk-ant-...",
395
+ password=True,
396
+ id="anthropic-input",
397
+ )
398
+
399
+ yield Label("Google API key", classes="field-label", id="google-label")
400
+ yield Input(
401
+ value=api_keys.get("google", ""),
402
+ placeholder="AIza...",
403
+ password=True,
404
+ id="google-input",
405
+ )
406
+
407
+ yield Label("Pro key", classes="field-label")
408
+ yield Input(
409
+ value=config.get("pro_key", ""),
410
+ placeholder="Your CoreInsight Pro key",
411
+ password=True,
412
+ id="pro-key-input",
413
+ )
414
+
415
+ with Horizontal(id="configure-buttons"):
416
+ yield Button("Save", id="configure-save", variant="success")
417
+ yield Button("Cancel", id="configure-cancel", variant="default")
418
+
419
+ yield Label("", id="configure-status")
420
+
421
+ def on_mount(self) -> None:
422
+ self._refresh_visibility(self._config.get("provider", "ollama"))
423
+
424
+ @on(Select.Changed, "#provider-select")
425
+ def provider_changed(self, event: Select.Changed) -> None:
426
+ self._refresh_visibility(str(event.value))
427
+
428
+ def _refresh_visibility(self, provider: str) -> None:
429
+ """Show only the fields relevant to the selected provider."""
430
+ local_ids = ["local-url-label", "local-url-input"]
431
+ openai_ids = ["openai-label", "openai-input"]
432
+ anthropic_ids= ["anthropic-label", "anthropic-input"]
433
+ google_ids = ["google-label", "google-input"]
434
+
435
+ all_conditional = local_ids + openai_ids + anthropic_ids + google_ids
436
+
437
+ # Hide everything first
438
+ for wid in all_conditional:
439
+ try:
440
+ self.query_one(f"#{wid}").display = False
441
+ except Exception:
442
+ pass
443
+
444
+ # Show relevant ones
445
+ show = {
446
+ "local_server": local_ids,
447
+ "openai": openai_ids,
448
+ "anthropic": anthropic_ids,
449
+ "google": google_ids,
450
+ }.get(provider, [])
451
+
452
+ for wid in show:
453
+ try:
454
+ self.query_one(f"#{wid}").display = True
455
+ except Exception:
456
+ pass
457
+
458
+ @on(Button.Pressed, "#configure-save")
459
+ def save(self) -> None:
460
+ import json
461
+ from pathlib import Path
462
+
463
+ provider = str(self.query_one("#provider-select", Select).value)
464
+ model = self.query_one("#model-input", Input).value.strip()
465
+ agent_mode = str(self.query_one("#agent-mode-select", Select).value)
466
+ pro_key = self.query_one("#pro-key-input", Input).value.strip()
467
+
468
+ if not model:
469
+ self.query_one("#configure-status", Label).update(
470
+ "[red]Model name cannot be empty.[/red]"
471
+ )
472
+ return
473
+
474
+ # Build updated config
475
+ config = dict(self._config)
476
+ config["provider"] = provider
477
+ config["model_name"] = model
478
+ config["agent_mode"] = agent_mode
479
+
480
+ api_keys = dict(config.get("api_keys", {}))
481
+ api_keys["local_url"] = self.query_one("#local-url-input", Input).value.strip()
482
+ api_keys["openai"] = self.query_one("#openai-input", Input).value.strip()
483
+ api_keys["anthropic"] = self.query_one("#anthropic-input", Input).value.strip()
484
+ api_keys["google"] = self.query_one("#google-input", Input).value.strip()
485
+ config["api_keys"] = api_keys
486
+
487
+ if pro_key:
488
+ config["pro_key"] = pro_key
489
+
490
+ config_dir = Path.home() / ".coreinsight"
491
+ config_dir.mkdir(parents=True, exist_ok=True)
492
+ config_path = config_dir / "config.json"
493
+ with open(config_path, "w") as f:
494
+ json.dump(config, f, indent=2)
495
+
496
+ self.query_one("#configure-status", Label).update(
497
+ "[green]Saved. Restart coreinsight view to apply changes.[/green]"
498
+ )
499
+ self._config = config
500
+
501
+ @on(Button.Pressed, "#configure-cancel")
502
+ def cancel(self) -> None:
503
+ self.dismiss()
315
504
 
316
505
  # ---------------------------------------------------------------------------
317
506
  # Main TUI App
@@ -323,12 +512,13 @@ class CoreInsightApp(App):
323
512
  CSS_PATH = None # all CSS inline below
324
513
 
325
514
  BINDINGS = [
326
- Binding("q", "quit", "Quit"),
327
- Binding("a", "analyze", "Analyze"),
328
- Binding("i", "index", "Index"),
329
- Binding("m", "view_memory", "Memory"),
330
- Binding("s", "view_settings", "Settings"),
331
- Binding("ctrl+c","quit", "Quit", show=False),
515
+ Binding("q", "quit", "Quit"),
516
+ Binding("a", "analyze", "Analyze"),
517
+ Binding("i", "index", "Index"),
518
+ Binding("m", "view_memory", "Memory"),
519
+ Binding("c", "configure", "Configure"),
520
+ Binding("d", "demo", "Demo"),
521
+ Binding("ctrl+c", "quit", "Quit", show=False),
332
522
  ]
333
523
 
334
524
  CSS = """
@@ -456,10 +646,11 @@ class CoreInsightApp(App):
456
646
 
457
647
  with Vertical(id="action-panel"):
458
648
  yield Label("Actions", id="action-label")
459
- yield Button("Analyze [a]", id="btn-analyze", variant="success")
460
- yield Button("Index [i]", id="btn-index", variant="primary")
461
- yield Button("Memory [m]", id="btn-memory", variant="default")
462
- yield Button("Settings [s]", id="btn-settings", variant="default")
649
+ yield Button("Analyze [a]", id="btn-analyze", variant="success")
650
+ yield Button("Index [i]", id="btn-index", variant="primary")
651
+ yield Button("Demo [d]", id="btn-demo", variant="primary")
652
+ yield Button("Memory [m]", id="btn-memory", variant="default")
653
+ yield Button("Configure [c]", id="btn-configure", variant="default")
463
654
  with Horizontal(id="no-docker-row"):
464
655
  yield Switch(value=False, id="no-docker-switch")
465
656
  yield Label("Skip Docker", id="no-docker-label")
@@ -562,9 +753,46 @@ class CoreInsightApp(App):
562
753
  def action_view_memory(self) -> None:
563
754
  self.push_screen(MemoryModal())
564
755
 
565
- @on(Button.Pressed, "#btn-settings")
566
- def action_view_settings(self) -> None:
567
- self.push_screen(SettingsModal())
756
+ @on(Button.Pressed, "#btn-configure")
757
+ def action_configure(self) -> None:
758
+ self.push_screen(ConfigureModal())
759
+
760
+ @on(Button.Pressed, "#btn-demo")
761
+ def action_demo(self) -> None:
762
+ if self._busy:
763
+ self._set_status("Already running — please wait.")
764
+ return
765
+ no_docker = self.query_one("#no-docker-switch", Switch).value
766
+ self._start_demo(no_docker)
767
+
768
+ @work(thread=True)
769
+ def _start_demo(self, no_docker: bool) -> None:
770
+ from coreinsight.main import run_demo
771
+
772
+ log = self.query_one("#output-log", RichLog)
773
+ tui_console = TuiConsole(log)
774
+ self._busy = True
775
+
776
+ self.call_from_thread(self._set_status, "Running demo...")
777
+ self.call_from_thread(
778
+ log.write,
779
+ "\n[bold cyan]Running built-in Python demo...[/bold cyan]\n"
780
+ )
781
+
782
+ # Temporarily patch the demo's console output into the TUI
783
+ import coreinsight.main as _main
784
+ _prev = _main.console
785
+ _main.console = tui_console
786
+ try:
787
+ run_demo(lang="python", no_docker=no_docker)
788
+ except SystemExit:
789
+ pass
790
+ except Exception as exc:
791
+ self.call_from_thread(log.write, f"[red]Demo error: {exc}[/red]")
792
+ finally:
793
+ _main.console = _prev
794
+ self._busy = False
795
+ self.call_from_thread(self._set_status, "Demo complete.")
568
796
 
569
797
  # ── Workers ───────────────────────────────────────────────────────────
570
798
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coreinsight-cli
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA
5
5
  Author: Varun Jani
6
6
  License: GPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "coreinsight-cli"
7
- version = "0.2.7"
7
+ version = "0.2.8"
8
8
  description = "Local-first AI performance profiler that mathematically verifies optimizations for Python, C++, and CUDA"
9
9
  license = {text = "GPL-3.0-or-later"}
10
10
  authors = [
File without changes