kader 0.1.1__py3-none-any.whl → 0.1.3__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.
cli/app.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  import asyncio
4
4
  import threading
5
+ from importlib.metadata import version as get_version
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
@@ -32,9 +33,19 @@ from .utils import (
32
33
  )
33
34
  from .widgets import ConversationView, InlineSelector, LoadingSpinner, ModelSelector
34
35
 
35
- WELCOME_MESSAGE = """# Welcome to Kader CLI! 🚀
36
+ WELCOME_MESSAGE = """
37
+ <div align="center">
36
38
 
37
- Your **modern AI-powered coding assistant**.
39
+ ```
40
+ ██╗ ██╗ ██╗ █████╗ ██████╗ ███████╗██████╗
41
+ ██╔╝ ██║ ██╔╝██╔══██╗██╔══██╗██╔════╝██╔══██╗
42
+ ██╔╝ █████╔╝ ███████║██║ ██║█████╗ ██████╔╝
43
+ ██╔╝ ██╔═██╗ ██╔══██║██║ ██║██╔══╝ ██╔══██╗
44
+ ██╔╝ ██║ ██╗██║ ██║██████╔╝███████╗██║ ██║
45
+ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝
46
+ ```
47
+
48
+ </div>
38
49
 
39
50
  Type a message below to start chatting, or use one of the commands:
40
51
 
@@ -45,10 +56,16 @@ Type a message below to start chatting, or use one of the commands:
45
56
  - `/save` - Save current session
46
57
  - `/load` - Load a saved session
47
58
  - `/sessions` - List saved sessions
59
+ - `/cost` - Show the cost of the conversation
48
60
  - `/exit` - Exit the application
49
61
  """
50
62
 
51
63
 
64
+ # Minimum terminal size to prevent UI breakage
65
+ MIN_WIDTH = 89
66
+ MIN_HEIGHT = 29
67
+
68
+
52
69
  class KaderApp(App):
53
70
  """Main Kader CLI application."""
54
71
 
@@ -81,6 +98,7 @@ class KaderApp(App):
81
98
  self._confirmation_result: tuple[bool, Optional[str]] = (True, None)
82
99
  self._inline_selector: Optional[InlineSelector] = None
83
100
  self._model_selector: Optional[ModelSelector] = None
101
+ self._update_info: Optional[str] = None # Latest version if update available
84
102
 
85
103
  self._agent = self._create_agent(self._current_model)
86
104
 
@@ -291,6 +309,71 @@ class KaderApp(App):
291
309
  # Focus the input
292
310
  self.query_one("#prompt-input", Input).focus()
293
311
 
312
+ # Check initial size
313
+ self._check_terminal_size()
314
+
315
+ # Start background update check
316
+ threading.Thread(target=self._check_for_updates, daemon=True).start()
317
+
318
+ def _check_for_updates(self) -> None:
319
+ """Check for package updates in background thread."""
320
+ try:
321
+ from outdated import check_outdated
322
+
323
+ current_version = get_version("kader")
324
+ is_outdated, latest_version = check_outdated("kader", current_version)
325
+
326
+ if is_outdated:
327
+ self._update_info = latest_version
328
+ # Schedule UI update on main thread
329
+ self.call_from_thread(self._show_update_notification)
330
+ except Exception:
331
+ # Silently ignore update check failures
332
+ pass
333
+
334
+ def _show_update_notification(self) -> None:
335
+ """Show update notification as a toast."""
336
+ if not self._update_info:
337
+ return
338
+
339
+ try:
340
+ current = get_version("kader")
341
+ message = (
342
+ f">> Update available! v{current} → v{self._update_info} "
343
+ f"Run: uv tool upgrade kader"
344
+ )
345
+ self.notify(message, severity="information", timeout=10)
346
+ except Exception:
347
+ pass
348
+
349
+ def on_resize(self) -> None:
350
+ """Handle terminal resize events."""
351
+ self._check_terminal_size()
352
+
353
+ def _check_terminal_size(self) -> None:
354
+ """Check if terminal is large enough and show warning if not."""
355
+ width = self.console.size.width
356
+ height = self.console.size.height
357
+
358
+ # Check if we need to show/hide the size warning
359
+ too_small = width < MIN_WIDTH or height < MIN_HEIGHT
360
+
361
+ try:
362
+ warning = self.query_one("#size-warning", Static)
363
+ if not too_small:
364
+ warning.remove()
365
+ except Exception:
366
+ if too_small:
367
+ # Show warning overlay
368
+ warning_text = f"""⚠️ Terminal Too Small
369
+
370
+ Current: {width}x{height}
371
+ Minimum: {MIN_WIDTH}x{MIN_HEIGHT}
372
+
373
+ Please resize your terminal."""
374
+ warning = Static(warning_text, id="size-warning")
375
+ self.mount(warning)
376
+
294
377
  async def on_input_submitted(self, event: Input.Submitted) -> None:
295
378
  """Handle user input submission."""
296
379
  user_input = event.value.strip()
@@ -324,6 +407,7 @@ class KaderApp(App):
324
407
  elif cmd == "/clear":
325
408
  conversation.clear_messages()
326
409
  self._agent.memory.clear()
410
+ self._agent.provider.reset_tracking() # Reset usage/cost tracking
327
411
  self._current_session_id = None
328
412
  self.notify("Conversation cleared!", severity="information")
329
413
  elif cmd == "/save":
@@ -342,6 +426,8 @@ class KaderApp(App):
342
426
  elif cmd == "/refresh":
343
427
  self._refresh_directory_tree()
344
428
  self.notify("Directory tree refreshed!", severity="information")
429
+ elif cmd == "/cost":
430
+ self._handle_cost(conversation)
345
431
  elif cmd == "/exit":
346
432
  self.exit()
347
433
  else:
@@ -536,6 +622,42 @@ class KaderApp(App):
536
622
  conversation.add_message(f"❌ Error listing sessions: {e}", "assistant")
537
623
  self.notify(f"Error: {e}", severity="error")
538
624
 
625
+ def _handle_cost(self, conversation: ConversationView) -> None:
626
+ """Display LLM usage costs."""
627
+ try:
628
+ # Get cost and usage from the provider
629
+ cost = self._agent.provider.total_cost
630
+ usage = self._agent.provider.total_usage
631
+ model = self._agent.provider.model
632
+
633
+ lines = [
634
+ "## Usage Costs 💰\n",
635
+ f"**Model:** `{model}`\n",
636
+ "### Cost Breakdown",
637
+ "| Type | Amount |",
638
+ "|------|--------|",
639
+ f"| Input Cost | ${cost.input_cost:.6f} |",
640
+ f"| Output Cost | ${cost.output_cost:.6f} |",
641
+ f"| **Total Cost** | **${cost.total_cost:.6f}** |",
642
+ "",
643
+ "### Token Usage",
644
+ "| Type | Tokens |",
645
+ "|------|--------|",
646
+ f"| Prompt Tokens | {usage.prompt_tokens:,} |",
647
+ f"| Completion Tokens | {usage.completion_tokens:,} |",
648
+ f"| **Total Tokens** | **{usage.total_tokens:,}** |",
649
+ ]
650
+
651
+ if cost.total_cost == 0.0:
652
+ lines.append(
653
+ "\n> 💡 *Note: Ollama runs locally, so there are no API costs.*"
654
+ )
655
+
656
+ conversation.add_message("\n".join(lines), "assistant")
657
+ except Exception as e:
658
+ conversation.add_message(f"❌ Error getting costs: {e}", "assistant")
659
+ self.notify(f"Error: {e}", severity="error")
660
+
539
661
 
540
662
  def main() -> None:
541
663
  """Run the Kader CLI application."""
cli/app.tcss CHANGED
@@ -17,6 +17,23 @@ $text-muted: #6c7086;
17
17
 
18
18
  Screen {
19
19
  background: $background;
20
+ min-width: 89;
21
+ min-height: 29;
22
+ }
23
+
24
+ /* ===== Size Warning Overlay ===== */
25
+
26
+ #size-warning {
27
+ dock: top;
28
+ width: 100%;
29
+ height: 100%;
30
+ background: $background 95%;
31
+ color: $warning;
32
+ text-align: center;
33
+ content-align: center middle;
34
+ text-style: bold;
35
+ padding: 2;
36
+ layer: overlay;
20
37
  }
21
38
 
22
39
  /* ===== Header ===== */
cli/utils.py CHANGED
@@ -19,6 +19,7 @@ HELP_TEXT = """## Kader CLI Commands 📖
19
19
  | `/save` | Save current session |
20
20
  | `/load <id>` | Load a saved session |
21
21
  | `/sessions` | List saved sessions |
22
+ | `/cost` | Show usage costs |
22
23
  | `/refresh` | Refresh file tree |
23
24
  | `/exit` | Exit the CLI |
24
25
 
kader/agent/base.py CHANGED
@@ -559,12 +559,13 @@ class BaseAgent:
559
559
  token_usage["total_tokens"],
560
560
  )
561
561
 
562
+ # estimate the cost...
563
+ estimated_cost = self.provider.estimate_cost(token_usage)
564
+
562
565
  # Calculate and log cost
563
566
  agent_logger.calculate_cost(
564
567
  self.logger_id,
565
- token_usage["prompt_tokens"],
566
- token_usage["completion_tokens"],
567
- getattr(self.provider, "model", ""),
568
+ estimated_cost.total_cost,
568
569
  )
569
570
 
570
571
  # Save session update
@@ -726,12 +727,13 @@ class BaseAgent:
726
727
  token_usage["total_tokens"],
727
728
  )
728
729
 
730
+ # estimate the cost...
731
+ estimated_cost = self.provider.estimate_cost(token_usage)
732
+
729
733
  # Calculate and log cost
730
734
  agent_logger.calculate_cost(
731
735
  self.logger_id,
732
- token_usage["prompt_tokens"],
733
- token_usage["completion_tokens"],
734
- getattr(self.provider, "model", ""),
736
+ estimated_cost.total_cost,
735
737
  )
736
738
 
737
739
  # Save session update
kader/agent/logger.py CHANGED
@@ -99,27 +99,9 @@ class AgentLogger:
99
99
  def calculate_cost(
100
100
  self,
101
101
  logger_id: str,
102
- prompt_tokens: int,
103
- completion_tokens: int,
104
- model_name: str = "",
105
- pricing_data: Optional[Dict[str, float]] = None,
102
+ total_cost: float,
106
103
  ):
107
104
  """Calculate and log cost based on token usage."""
108
- # Use provided pricing data or defaults
109
- if pricing_data is None:
110
- # Default pricing (these would come from a model registry in practice)
111
- pricing_data = {
112
- "input_cost_per_million": 0.5, # $0.50 per million input tokens
113
- "output_cost_per_million": 1.5, # $1.50 per million output tokens
114
- }
115
-
116
- input_cost = (prompt_tokens / 1_000_000) * pricing_data.get(
117
- "input_cost_per_million", 0.5
118
- )
119
- output_cost = (completion_tokens / 1_000_000) * pricing_data.get(
120
- "output_cost_per_million", 1.5
121
- )
122
- total_cost = input_cost + output_cost
123
105
 
124
106
  self.log_cost(logger_id, total_cost)
125
107
  return total_cost
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kader
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: kader coding agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: faiss-cpu>=1.9.0
7
7
  Requires-Dist: jinja2>=3.1.6
8
8
  Requires-Dist: loguru>=0.7.3
9
9
  Requires-Dist: ollama>=0.6.1
10
+ Requires-Dist: outdated>=0.2.2
10
11
  Requires-Dist: pyyaml>=6.0.3
11
12
  Requires-Dist: tenacity>=9.1.2
12
13
  Requires-Dist: textual[syntax]>=6.8.0
@@ -1,9 +1,9 @@
1
1
  cli/README.md,sha256=DY3X7w6LPka1GzhtTrGwhpkFmx0YyRpcTCHjFmti3Yg,4654
2
2
  cli/__init__.py,sha256=OAi_KSwcuYXR0sRxKuw1DYQrz1jbu8p7vn41_99f36I,107
3
3
  cli/__main__.py,sha256=xO2JVjCsh691b-cjSBAEKocJeUeI3P0gfUqM-f1Mp1A,95
4
- cli/app.py,sha256=eEWrfq_XobsjAeSc2lRMNBJ1cs2nlZ_qeoamuqSnubg,19981
5
- cli/app.tcss,sha256=eJX5akeqHmoZVDjibvBPIBot5UK7eiUxgojNu9IjMBs,10098
6
- cli/utils.py,sha256=jt5wvSMIjPP-ZbvXEK2fd00-oRj32sj95HinJd4c73Q,1815
4
+ cli/app.py,sha256=PwYDnhdCJjSHpDqPjwlhHQoHtirZUNv1WHkG9Hr0Ius,24805
5
+ cli/app.tcss,sha256=fImM5JxlTCWvD4i3OmpJpBOEE8HGo87Q9E8rKbOKEGU,10416
6
+ cli/utils.py,sha256=GkyVTkIjNsnEEYCVD2l5snQybReXknfg5RihKXWuK4w,1846
7
7
  cli/widgets/__init__.py,sha256=1vj31CrJyxZROLthkKr79i_GbNyj8g3q60ZQPbJHK5k,300
8
8
  cli/widgets/confirmation.py,sha256=s6h3ZY98xHeeF3G-OtHYYw-xPlAP6K7i73jo_ftOMQc,9400
9
9
  cli/widgets/conversation.py,sha256=vqI3eq7qQrn4UD8fB8rBr33kIesrLpNVhNIp3MdGeOM,1519
@@ -12,8 +12,8 @@ kader/__init__.py,sha256=QXb0aQvySAiNmhP2kNMNtuuzG_eSUgRVcTRSafko1r0,602
12
12
  kader/config.py,sha256=B1s1PNgZ5SrFEJU5TtJG-ZAc7Umff4cwyHR7mfQgeLA,4261
13
13
  kader/agent/__init__.py,sha256=UJzUw9NIzggCrhIBHC6nJnfzkhCjCZnIzmD6uUn2SNA,159
14
14
  kader/agent/agents.py,sha256=lo41hlkPm3CStI9VAwSOR6IYhCBkqQJ48Ego5XN1XU8,4299
15
- kader/agent/base.py,sha256=qWT53Fuw3I61X9wvZFDNJ8YnYSxBJKdnOH1122cWP2s,36452
16
- kader/agent/logger.py,sha256=Y8Ig6Jab_jEDUCStAnv-zmBf8u7Ubpe5ry-zaPmj0Kk,6582
15
+ kader/agent/base.py,sha256=3SzXH93JW5LGI30c3EL_onol3UFWd0lCqi5gWOaXq8o,36452
16
+ kader/agent/logger.py,sha256=3vFwz_yycSBU-5mcdalfZ3KBVT9P_20Q-WT5Al8yIXo,5796
17
17
  kader/memory/__init__.py,sha256=VUzzhGOWvO_2aYB6uuavmtNI8l94K7H3uPn4_1MVUUs,1473
18
18
  kader/memory/conversation.py,sha256=h6Bamd8_rYnk0Bwt4MJWZRfv2wxCcg6eUxPvzP-tIyA,11810
19
19
  kader/memory/session.py,sha256=VhRPEO474SlwM5UOuFt-b_28sJs3j4SNrobCZvoP5Os,10811
@@ -39,7 +39,7 @@ kader/tools/rag.py,sha256=37Nd49D5R_DlWmMyhSdSvst_XFOnoxpaFNtlbLEt6qM,15653
39
39
  kader/tools/todo.py,sha256=omirxoG7_KVHAmMoD12DGmn7scqRaewbI1cqWm0ShUo,7735
40
40
  kader/tools/utils.py,sha256=bfq7b1vpA1qBaL_QZYBFTqOM35-omu_UeDjo6v0rxEg,14176
41
41
  kader/tools/web.py,sha256=2Aqy0nO7ZwNLAqE9IWjCg5y8qnhbt-AIZOHy02XgbxA,8464
42
- kader-0.1.1.dist-info/METADATA,sha256=EEFa_a5JeTZV9CZ8wKchfbYv1NXyZhUuHIu7zburgnU,9490
43
- kader-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
44
- kader-0.1.1.dist-info/entry_points.txt,sha256=TK0VOtrfDFqZ8JQfxpuAHHvDLHyoiafUjS-VOixl02c,39
45
- kader-0.1.1.dist-info/RECORD,,
42
+ kader-0.1.3.dist-info/METADATA,sha256=UbSQYvgdw-4AWNgfGv22VafMPPkY54nFQp5PgKq-QMA,9521
43
+ kader-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
44
+ kader-0.1.3.dist-info/entry_points.txt,sha256=TK0VOtrfDFqZ8JQfxpuAHHvDLHyoiafUjS-VOixl02c,39
45
+ kader-0.1.3.dist-info/RECORD,,
File without changes