nullabot 1.0.1__py3-none-any.whl → 1.0.2__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.
@@ -300,12 +300,17 @@ IMPORTANT:
300
300
  async def _run_claude_async(
301
301
  self,
302
302
  prompt: str,
303
- ) -> tuple[str, bool]:
304
- """Run Claude Code CLI asynchronously."""
303
+ ) -> tuple[str, bool, int, int]:
304
+ """
305
+ Run Claude Code CLI asynchronously.
306
+
307
+ Returns:
308
+ tuple: (response_text, success, input_tokens, output_tokens)
309
+ """
305
310
  cmd = [
306
311
  "claude",
307
312
  "-p",
308
- "--output-format", "text",
313
+ "--output-format", "json",
309
314
  "--model", self.model,
310
315
  "--max-turns", "50",
311
316
  "--dangerously-skip-permissions",
@@ -332,15 +337,34 @@ IMPORTANT:
332
337
  output = stdout.decode().strip()
333
338
  if process.returncode != 0:
334
339
  error = stderr.decode() or "Unknown error"
335
- return f"Error: {error}", False
340
+ return f"Error: {error}", False, 0, 0
341
+
342
+ # Parse JSON response to extract tokens
343
+ try:
344
+ data = json.loads(output)
345
+ result_text = data.get("result", output)
346
+
347
+ # Extract token usage from response
348
+ input_tokens = data.get("input_tokens", 0)
349
+ output_tokens = data.get("output_tokens", 0)
336
350
 
337
- return output, True
351
+ # Also check nested usage object
352
+ usage = data.get("usage", {})
353
+ if not input_tokens:
354
+ input_tokens = usage.get("input_tokens", 0)
355
+ if not output_tokens:
356
+ output_tokens = usage.get("output_tokens", 0)
357
+
358
+ return result_text, True, input_tokens, output_tokens
359
+ except json.JSONDecodeError:
360
+ # Fallback if not valid JSON
361
+ return output, True, 0, 0
338
362
 
339
363
  except asyncio.TimeoutError:
340
364
  timeout_min = self.timeout // 60
341
- return f"Error: Claude timed out after {timeout_min} minutes", False
365
+ return f"Error: Claude timed out after {timeout_min} minutes", False, 0, 0
342
366
  except Exception as e:
343
- return f"Error: {str(e)}", False
367
+ return f"Error: {str(e)}", False, 0, 0
344
368
 
345
369
  def _get_previous_work_hint(self) -> str:
346
370
  """Get hint about where to find previous agents' work."""
@@ -500,16 +524,16 @@ IMPORTANT:
500
524
  )
501
525
 
502
526
  # Run Claude
503
- response, success = await self._run_claude_async(current_prompt)
527
+ response, success, input_tokens, output_tokens = await self._run_claude_async(current_prompt)
504
528
 
505
529
  cycle_duration = (datetime.now() - cycle_start).total_seconds()
506
530
 
507
- # Track usage
531
+ # Track usage with actual token counts
508
532
  usage_info = self.usage.record_cycle(
509
533
  model=self.model,
510
534
  agent_type=self.agent_type,
511
- input_tokens=len(current_prompt) // 4, # Rough estimate
512
- output_tokens=len(response) // 4 if success else 0,
535
+ input_tokens=input_tokens,
536
+ output_tokens=output_tokens,
513
537
  duration_seconds=cycle_duration,
514
538
  )
515
539
 
@@ -636,14 +660,25 @@ IMPORTANT:
636
660
  state["total_time_seconds"] = (datetime.now() - start_time).total_seconds()
637
661
  self._save_state(state)
638
662
 
639
- # Send cycle complete notification with window usage
663
+ # Send cycle complete notification with window usage and tokens
640
664
  window_pct = usage_info.get('window_usage_pct', 0)
641
665
  window_bar = self._make_progress_bar(window_pct)
666
+ cycle_tokens = usage_info.get('cycle_tokens', 0)
667
+ total_tokens = usage_info.get('total_tokens', 0)
668
+
669
+ # Format token counts (K for thousands, M for millions)
670
+ def fmt_tokens(n):
671
+ if n >= 1_000_000:
672
+ return f"{n/1_000_000:.1f}M"
673
+ elif n >= 1_000:
674
+ return f"{n/1_000:.1f}K"
675
+ return str(n)
642
676
 
643
677
  await self._notify(
644
678
  "cycle_complete",
645
679
  f"✅ *Cycle {cycle}* complete ({cycle_duration:.0f}s)\n\n"
646
680
  f"📁 *Files:* {file_count}\n"
681
+ f"🔤 *Tokens:* {fmt_tokens(cycle_tokens)} (Total: {fmt_tokens(total_tokens)})\n"
647
682
  f"⏱ *5hr window:* {window_bar} {window_pct:.0f}%\n"
648
683
  f"💰 *Est. cost:* ${usage_info['cycle_cost']:.2f} (Total: ${usage_info['total_cost']:.2f})\n"
649
684
  f"📊 {status_line}\n\n"
@@ -741,6 +776,17 @@ Remember to include STATUS and SUMMARY lines at the end."""
741
776
  window_pct = usage_summary.get('window_usage_pct', 0)
742
777
  window_hours = usage_summary.get('window_hours', 0)
743
778
  window_bar = self._make_progress_bar(window_pct)
779
+ total_tokens = usage_summary.get('total_tokens', 0)
780
+ input_tokens = usage_summary.get('total_input_tokens', 0)
781
+ output_tokens = usage_summary.get('total_output_tokens', 0)
782
+
783
+ # Format token counts
784
+ def fmt_tokens(n):
785
+ if n >= 1_000_000:
786
+ return f"{n/1_000_000:.1f}M"
787
+ elif n >= 1_000:
788
+ return f"{n/1_000:.1f}K"
789
+ return str(n)
744
790
 
745
791
  await self._notify(
746
792
  "complete",
@@ -749,6 +795,7 @@ Remember to include STATUS and SUMMARY lines at the end."""
749
795
  f"🔄 *Cycles:* {cycle}\n"
750
796
  f"⏱ *Duration:* {time_str}\n"
751
797
  f"📂 *Files created:* {file_count}\n"
798
+ f"🔤 *Tokens:* {fmt_tokens(total_tokens)} ({fmt_tokens(input_tokens)} in / {fmt_tokens(output_tokens)} out)\n"
752
799
  f"⏱ *5hr window:* {window_bar} {window_pct:.0f}% ({window_hours:.1f}h used)\n"
753
800
  f"💰 *Est. cost:* ${usage_summary['total_cost_usd']:.2f}\n\n"
754
801
  f"*Files:*\n" + "\n".join(f"• `{f}`" for f in file_list[:10]),
nullabot/bot/telegram.py CHANGED
@@ -1097,6 +1097,14 @@ class TelegramBot:
1097
1097
 
1098
1098
  base_dir = self.base_projects_dir.parent
1099
1099
 
1100
+ # Format token counts
1101
+ def fmt_tokens(n):
1102
+ if n >= 1_000_000:
1103
+ return f"{n/1_000_000:.1f}M"
1104
+ elif n >= 1_000:
1105
+ return f"{n/1_000:.1f}K"
1106
+ return str(n)
1107
+
1100
1108
  lines = [
1101
1109
  "📈 *Nullabot Usage*\n",
1102
1110
  ]
@@ -1105,6 +1113,7 @@ class TelegramBot:
1105
1113
  total_cost = 0
1106
1114
  total_cycles = 0
1107
1115
  total_hours = 0
1116
+ total_tokens = 0
1108
1117
 
1109
1118
  for item in self.base_projects_dir.iterdir():
1110
1119
  if item.is_dir() and (item / ".nullabot").exists():
@@ -1112,16 +1121,18 @@ class TelegramBot:
1112
1121
  tracker = UsageTracker(item, base_dir)
1113
1122
  summary = tracker.get_summary()
1114
1123
  if summary["total_cycles"] > 0:
1124
+ tokens = summary.get("total_tokens", 0)
1115
1125
  lines.append(f"📁 *{item.name}*")
1116
- lines.append(f" {summary['total_cycles']} cycles · {summary['total_hours']:.1f}hrs · ${summary['total_cost_usd']:.2f}")
1126
+ lines.append(f" {summary['total_cycles']} cycles · {fmt_tokens(tokens)} tokens · ${summary['total_cost_usd']:.2f}")
1117
1127
  total_cost += summary["total_cost_usd"]
1118
1128
  total_cycles += summary["total_cycles"]
1119
1129
  total_hours += summary["total_hours"]
1130
+ total_tokens += tokens
1120
1131
  except:
1121
1132
  pass
1122
1133
 
1123
1134
  if total_cycles > 0:
1124
- lines.append(f"\n💰 *Total:* {total_cycles} cycles · {total_hours:.1f}hrs · ${total_cost:.2f}")
1135
+ lines.append(f"\n💰 *Total:* {total_cycles} cycles · {fmt_tokens(total_tokens)} tokens · ${total_cost:.2f}")
1125
1136
  else:
1126
1137
  lines.append("_No usage yet_")
1127
1138
 
@@ -1595,6 +1606,14 @@ class TelegramBot:
1595
1606
 
1596
1607
  base_dir = self.base_projects_dir.parent
1597
1608
 
1609
+ # Format token counts
1610
+ def fmt_tokens(n):
1611
+ if n >= 1_000_000:
1612
+ return f"{n/1_000_000:.1f}M"
1613
+ elif n >= 1_000:
1614
+ return f"{n/1_000:.1f}K"
1615
+ return str(n)
1616
+
1598
1617
  lines = [
1599
1618
  "📈 *Nullabot Usage*\n",
1600
1619
  ]
@@ -1603,6 +1622,7 @@ class TelegramBot:
1603
1622
  total_cost = 0
1604
1623
  total_cycles = 0
1605
1624
  total_hours = 0
1625
+ total_tokens = 0
1606
1626
 
1607
1627
  for item in self.base_projects_dir.iterdir():
1608
1628
  if item.is_dir() and (item / ".nullabot").exists():
@@ -1610,16 +1630,18 @@ class TelegramBot:
1610
1630
  tracker = UsageTracker(item, base_dir)
1611
1631
  summary = tracker.get_summary()
1612
1632
  if summary["total_cycles"] > 0:
1633
+ tokens = summary.get("total_tokens", 0)
1613
1634
  lines.append(f"📁 *{item.name}*")
1614
- lines.append(f" {summary['total_cycles']} cycles · {summary['total_hours']:.1f}hrs · ${summary['total_cost_usd']:.2f}")
1635
+ lines.append(f" {summary['total_cycles']} cycles · {fmt_tokens(tokens)} tokens · ${summary['total_cost_usd']:.2f}")
1615
1636
  total_cost += summary["total_cost_usd"]
1616
1637
  total_cycles += summary["total_cycles"]
1617
1638
  total_hours += summary["total_hours"]
1639
+ total_tokens += tokens
1618
1640
  except:
1619
1641
  pass
1620
1642
 
1621
1643
  if total_cycles > 0:
1622
- lines.append(f"\n💰 *Total:* {total_cycles} cycles · {total_hours:.1f}hrs · ${total_cost:.2f}")
1644
+ lines.append(f"\n💰 *Total:* {total_cycles} cycles · {fmt_tokens(total_tokens)} tokens · ${total_cost:.2f}")
1623
1645
  else:
1624
1646
  lines.append("_No usage yet_")
1625
1647
 
nullabot/core/memory.py CHANGED
@@ -776,33 +776,50 @@ class UsageTracker:
776
776
  self,
777
777
  model: str,
778
778
  agent_type: str,
779
- input_tokens: int = 0, # Kept for compatibility but not used
780
- output_tokens: int = 0, # Kept for compatibility but not used
779
+ input_tokens: int = 0,
780
+ output_tokens: int = 0,
781
781
  duration_seconds: float = 0,
782
782
  ) -> dict:
783
- """Record a cycle's usage based on actual duration."""
783
+ """Record a cycle's usage based on actual duration and tokens."""
784
784
  # Calculate cost based on time (more accurate for subscriptions)
785
785
  hourly_rate = self.COST_PER_HOUR.get(model, self.COST_PER_HOUR["opus"])
786
786
  cost = (duration_seconds / 3600) * hourly_rate
787
787
 
788
+ # Initialize token tracking if not present
789
+ if "total_input_tokens" not in self._usage:
790
+ self._usage["total_input_tokens"] = 0
791
+ self._usage["total_output_tokens"] = 0
792
+
788
793
  # Update project totals
789
794
  self._usage["total_cycles"] += 1
790
795
  self._usage["total_duration_seconds"] += duration_seconds
791
796
  self._usage["total_cost_usd"] += cost
797
+ self._usage["total_input_tokens"] += input_tokens
798
+ self._usage["total_output_tokens"] += output_tokens
792
799
 
793
800
  # By model
794
801
  if model not in self._usage["by_model"]:
795
- self._usage["by_model"][model] = {"cycles": 0, "duration": 0.0, "cost": 0.0}
802
+ self._usage["by_model"][model] = {
803
+ "cycles": 0, "duration": 0.0, "cost": 0.0,
804
+ "input_tokens": 0, "output_tokens": 0
805
+ }
796
806
  self._usage["by_model"][model]["cycles"] += 1
797
807
  self._usage["by_model"][model]["duration"] += duration_seconds
798
808
  self._usage["by_model"][model]["cost"] += cost
809
+ self._usage["by_model"][model]["input_tokens"] += input_tokens
810
+ self._usage["by_model"][model]["output_tokens"] += output_tokens
799
811
 
800
812
  # By agent
801
813
  if agent_type not in self._usage["by_agent"]:
802
- self._usage["by_agent"][agent_type] = {"cycles": 0, "duration": 0.0, "cost": 0.0}
814
+ self._usage["by_agent"][agent_type] = {
815
+ "cycles": 0, "duration": 0.0, "cost": 0.0,
816
+ "input_tokens": 0, "output_tokens": 0
817
+ }
803
818
  self._usage["by_agent"][agent_type]["cycles"] += 1
804
819
  self._usage["by_agent"][agent_type]["duration"] += duration_seconds
805
820
  self._usage["by_agent"][agent_type]["cost"] += cost
821
+ self._usage["by_agent"][agent_type]["input_tokens"] += input_tokens
822
+ self._usage["by_agent"][agent_type]["output_tokens"] += output_tokens
806
823
 
807
824
  # Session log
808
825
  self._usage["sessions"].append({
@@ -811,6 +828,8 @@ class UsageTracker:
811
828
  "agent": agent_type,
812
829
  "duration": round(duration_seconds, 1),
813
830
  "cost": round(cost, 4),
831
+ "input_tokens": input_tokens,
832
+ "output_tokens": output_tokens,
814
833
  })
815
834
  # Keep last 500 sessions
816
835
  self._usage["sessions"] = self._usage["sessions"][-500:]
@@ -820,12 +839,20 @@ class UsageTracker:
820
839
  # Update GLOBAL 5-hour window (shared across all projects)
821
840
  window_status = self._global_window.record_usage(duration_seconds)
822
841
 
842
+ total_tokens = self._usage["total_input_tokens"] + self._usage["total_output_tokens"]
843
+
823
844
  return {
824
845
  "cycle_cost": round(cost, 2),
825
846
  "total_cost": round(self._usage["total_cost_usd"], 2),
826
847
  "total_cycles": self._usage["total_cycles"],
827
848
  "cycle_duration": round(duration_seconds, 1),
828
849
  "total_duration": round(self._usage["total_duration_seconds"], 1),
850
+ "cycle_input_tokens": input_tokens,
851
+ "cycle_output_tokens": output_tokens,
852
+ "cycle_tokens": input_tokens + output_tokens,
853
+ "total_input_tokens": self._usage["total_input_tokens"],
854
+ "total_output_tokens": self._usage["total_output_tokens"],
855
+ "total_tokens": total_tokens,
829
856
  "window_usage_pct": window_status["usage_pct"],
830
857
  "window_duration": window_status["used_seconds"],
831
858
  "window_hours": window_status["used_hours"],
@@ -839,10 +866,17 @@ class UsageTracker:
839
866
  # Get global window status
840
867
  window_status = self._global_window.get_status()
841
868
 
869
+ # Token totals (with backwards compatibility)
870
+ total_input = self._usage.get("total_input_tokens", 0)
871
+ total_output = self._usage.get("total_output_tokens", 0)
872
+
842
873
  return {
843
874
  "total_cycles": self._usage["total_cycles"],
844
875
  "total_cost_usd": round(self._usage["total_cost_usd"], 2),
845
876
  "total_hours": round(total_hours, 2),
877
+ "total_input_tokens": total_input,
878
+ "total_output_tokens": total_output,
879
+ "total_tokens": total_input + total_output,
846
880
  "window_hours": window_status["used_hours"],
847
881
  "window_usage_pct": window_status["usage_pct"],
848
882
  "window_remaining_hours": window_status["remaining_hours"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nullabot
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: 24/7 AI agents that think, design, and code - controlled via Telegram
5
5
  Project-URL: Homepage, https://github.com/ebokoo/nullabot
6
6
  Project-URL: Repository, https://github.com/ebokoo/nullabot
@@ -1,19 +1,19 @@
1
1
  nullabot/__init__.py,sha256=96923LhB3uU4TE33GdjyG8M2fRRcqeWvRWlqxt48OV4,58
2
2
  nullabot/cli.py,sha256=zattwGYh4feT_sRjCxSOoG6WSToxc_F1Zp00T_zrAIU,25800
3
3
  nullabot/agents/__init__.py,sha256=6mE-zjqFa99WfnE-khjDpCSKzaNlukKVP7tB5Q4o7Vk,133
4
- nullabot/agents/claude_agent.py,sha256=P67S7UlIjKPInYcZcIGOp7o5shA3VLiv96auupvfPs8,30483
4
+ nullabot/agents/claude_agent.py,sha256=B-P43PhZ_3wfUYpfsCNci3oEpm3s065FEWTDkneWoG8,32587
5
5
  nullabot/bot/__init__.py,sha256=9axCMJfCE09CxMGsxtx81QZcBEXfz6wruHbYEtQQAeY,102
6
- nullabot/bot/telegram.py,sha256=5YNpm2OGzy5A8jHEyvuVFvq9MTYeQ9qB8qFjiYQ_cYw,68354
6
+ nullabot/bot/telegram.py,sha256=ttBv693dpkt327iCjdpvJEcEsAroaHZ1e8QWQZ--SJk,69104
7
7
  nullabot/core/__init__.py,sha256=Yn9zEFXF_2-aXiiU_iftYuaXWrX1qC7ZaEd6i3sarIE,314
8
8
  nullabot/core/claude_code.py,sha256=sIKINJzjolBBpd2B_XhM6Egwmxs8E_VvDV3AOcuJbkw,9306
9
- nullabot/core/memory.py,sha256=0Qh9WRTZIkdOfGVLsAUbHIAuoFZ9-CoZ9wgP5DiPR7E,30674
9
+ nullabot/core/memory.py,sha256=OVGCwa7z0NZtwtB0r-XttBQw66qw-z4vXFgnBA2zt1o,32252
10
10
  nullabot/core/project.py,sha256=FZofBZzIhMQUCXxCI4jjJZjKRr5u4iJUbor70tvLh7Q,6222
11
11
  nullabot/core/rate_limiter.py,sha256=41CF4cX_n5_PFc6Uk1vp6m4MERxXl25p1wyw2FVdgsM,16693
12
12
  nullabot/core/reliability.py,sha256=nfMN29dCQqM_T_MSLe0z9Tk0N5CWYFtmNXVe9Lqmiyw,13985
13
13
  nullabot/core/sandbox.py,sha256=fWgjhWglGzXsJ10qxiKwJEkQQv6tLVa-DsshO7qcQvQ,4562
14
14
  nullabot/core/state.py,sha256=yeFF4r3h0nRd5LENiXE7SSqfWPuljGy7X0FgE_GuI5o,6307
15
- nullabot-1.0.1.dist-info/METADATA,sha256=38vnwVRA7YqMgmjyLWVV6zVl95IkepRlUK1nD10nX8A,3360
16
- nullabot-1.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- nullabot-1.0.1.dist-info/entry_points.txt,sha256=lHaTGUU4ku7Q6ZRZnlpp5LrHRyLxhaRB9HzXzsJwYY8,47
18
- nullabot-1.0.1.dist-info/licenses/LICENSE,sha256=IppsK7DD3SKtJlMoquzr1-bGKs88x85956yPxCn8H-U,1063
19
- nullabot-1.0.1.dist-info/RECORD,,
15
+ nullabot-1.0.2.dist-info/METADATA,sha256=i_73BKTIn0-YsgJF-kOxmEQjSM8pnB6KaQ56XmXe0Fw,3360
16
+ nullabot-1.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
+ nullabot-1.0.2.dist-info/entry_points.txt,sha256=lHaTGUU4ku7Q6ZRZnlpp5LrHRyLxhaRB9HzXzsJwYY8,47
18
+ nullabot-1.0.2.dist-info/licenses/LICENSE,sha256=IppsK7DD3SKtJlMoquzr1-bGKs88x85956yPxCn8H-U,1063
19
+ nullabot-1.0.2.dist-info/RECORD,,