kiro-usage 0.1.0__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.
- kiro_usage-0.1.0/PKG-INFO +79 -0
- kiro_usage-0.1.0/README.md +66 -0
- kiro_usage-0.1.0/kiro_usage/__init__.py +128 -0
- kiro_usage-0.1.0/kiro_usage/archiver.py +90 -0
- kiro_usage-0.1.0/kiro_usage/service.py +164 -0
- kiro_usage-0.1.0/kiro_usage/viewer.py +336 -0
- kiro_usage-0.1.0/kiro_usage.egg-info/PKG-INFO +79 -0
- kiro_usage-0.1.0/kiro_usage.egg-info/SOURCES.txt +11 -0
- kiro_usage-0.1.0/kiro_usage.egg-info/dependency_links.txt +1 -0
- kiro_usage-0.1.0/kiro_usage.egg-info/entry_points.txt +3 -0
- kiro_usage-0.1.0/kiro_usage.egg-info/top_level.txt +1 -0
- kiro_usage-0.1.0/pyproject.toml +25 -0
- kiro_usage-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kiro-usage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Track token usage and costs across Kiro CLI sessions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Environment :: Console
|
|
7
|
+
Classifier: Topic :: Utilities
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# kiro-usage
|
|
15
|
+
|
|
16
|
+
Track token usage and costs across [Kiro CLI](https://kiro.dev) sessions.
|
|
17
|
+
|
|
18
|
+
Real-time terminal dashboard with session archiving that persists across `/clear` and restarts.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
# install uv if you don't have it
|
|
24
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
25
|
+
|
|
26
|
+
uv tool install kiro-usage && kiro-usage install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
kiro-usage # today's usage, live refresh
|
|
33
|
+
kiro-usage week # last 7 days
|
|
34
|
+
kiro-usage month # last 30 days
|
|
35
|
+
kiro-usage all # all time
|
|
36
|
+
kiro-usage --json # JSON output
|
|
37
|
+
kiro-usage --no-live # print once and exit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Service Management
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
kiro-usage install # register background archiver (launchd / systemd)
|
|
44
|
+
kiro-usage uninstall # remove background archiver
|
|
45
|
+
kiro-usage status # check archiver status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The background archiver watches the Kiro CLI database and snapshots sessions to `~/.kiro_sessions/` every 10 seconds. This ensures session data is preserved even if you `/clear` or quit Kiro CLI.
|
|
49
|
+
|
|
50
|
+
## How It Works
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
kiro-cli SQLite DB
|
|
54
|
+
│
|
|
55
|
+
│ mtime change (every 10s)
|
|
56
|
+
▼
|
|
57
|
+
kiro-usage-archiver ──► ~/.kiro_sessions/*.json
|
|
58
|
+
(launchd / systemd)
|
|
59
|
+
|
|
60
|
+
kiro-usage viewer ◄── ~/.kiro_sessions/*.json
|
|
61
|
+
(on-demand)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Metrics:**
|
|
65
|
+
- **CacheWrite** — new tokens per turn (estimated, chars/4)
|
|
66
|
+
- **CacheRead** — prior context resent to API (estimated)
|
|
67
|
+
- **Output** — tokens streamed back (from chunks, accurate)
|
|
68
|
+
- **Cost** — cache-aware pricing estimate (5-min cache write rate)
|
|
69
|
+
|
|
70
|
+
## Uninstall
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
kiro-usage uninstall
|
|
74
|
+
uv tool uninstall kiro-usage
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# kiro-usage
|
|
2
|
+
|
|
3
|
+
Track token usage and costs across [Kiro CLI](https://kiro.dev) sessions.
|
|
4
|
+
|
|
5
|
+
Real-time terminal dashboard with session archiving that persists across `/clear` and restarts.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
# install uv if you don't have it
|
|
11
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
12
|
+
|
|
13
|
+
uv tool install kiro-usage && kiro-usage install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
kiro-usage # today's usage, live refresh
|
|
20
|
+
kiro-usage week # last 7 days
|
|
21
|
+
kiro-usage month # last 30 days
|
|
22
|
+
kiro-usage all # all time
|
|
23
|
+
kiro-usage --json # JSON output
|
|
24
|
+
kiro-usage --no-live # print once and exit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Service Management
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
kiro-usage install # register background archiver (launchd / systemd)
|
|
31
|
+
kiro-usage uninstall # remove background archiver
|
|
32
|
+
kiro-usage status # check archiver status
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The background archiver watches the Kiro CLI database and snapshots sessions to `~/.kiro_sessions/` every 10 seconds. This ensures session data is preserved even if you `/clear` or quit Kiro CLI.
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
kiro-cli SQLite DB
|
|
41
|
+
│
|
|
42
|
+
│ mtime change (every 10s)
|
|
43
|
+
▼
|
|
44
|
+
kiro-usage-archiver ──► ~/.kiro_sessions/*.json
|
|
45
|
+
(launchd / systemd)
|
|
46
|
+
|
|
47
|
+
kiro-usage viewer ◄── ~/.kiro_sessions/*.json
|
|
48
|
+
(on-demand)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Metrics:**
|
|
52
|
+
- **CacheWrite** — new tokens per turn (estimated, chars/4)
|
|
53
|
+
- **CacheRead** — prior context resent to API (estimated)
|
|
54
|
+
- **Output** — tokens streamed back (from chunks, accurate)
|
|
55
|
+
- **Cost** — cache-aware pricing estimate (5-min cache write rate)
|
|
56
|
+
|
|
57
|
+
## Uninstall
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
kiro-usage uninstall
|
|
61
|
+
uv tool uninstall kiro-usage
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Kiro Usage Tracker — shared constants and utilities."""
|
|
2
|
+
|
|
3
|
+
import os, re, sqlite3, json, unicodedata, platform
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
# ── Platform paths ────────────────────────────────────────────────────────────
|
|
7
|
+
if platform.system() == "Darwin":
|
|
8
|
+
CLI_DB = Path.home() / "Library/Application Support/kiro-cli/data.sqlite3"
|
|
9
|
+
else:
|
|
10
|
+
CLI_DB = Path.home() / ".local/share/kiro-cli/data.sqlite3"
|
|
11
|
+
|
|
12
|
+
SESSIONS_DIR = Path.home() / ".kiro_sessions"
|
|
13
|
+
CHARS_PER_TOKEN = 4
|
|
14
|
+
|
|
15
|
+
# ── Pricing ($/MTok) — Anthropic 5-min cache write rate ──────────────────────
|
|
16
|
+
PRICING = {
|
|
17
|
+
"claude-opus-4.6": (6.25, 0.50, 25),
|
|
18
|
+
"claude-opus-4.5": (6.25, 0.50, 25),
|
|
19
|
+
"claude-opus-4.1": (18.75, 1.50, 75),
|
|
20
|
+
"claude-opus-4": (18.75, 1.50, 75),
|
|
21
|
+
"claude-sonnet-4.6": (3.75, 0.30, 15),
|
|
22
|
+
"claude-sonnet-4.5": (3.75, 0.30, 15),
|
|
23
|
+
"claude-sonnet-4": (3.75, 0.30, 15),
|
|
24
|
+
}
|
|
25
|
+
DEFAULT_PRICING = (6.25, 0.50, 25)
|
|
26
|
+
|
|
27
|
+
def price_for(model_id):
|
|
28
|
+
if not model_id:
|
|
29
|
+
return DEFAULT_PRICING
|
|
30
|
+
m = model_id.lower()
|
|
31
|
+
for prefix, p in PRICING.items():
|
|
32
|
+
if prefix in m:
|
|
33
|
+
return p
|
|
34
|
+
return DEFAULT_PRICING
|
|
35
|
+
|
|
36
|
+
def calc_cost(cw, cr, out, model_id=None):
|
|
37
|
+
pw, pr, po = price_for(model_id)
|
|
38
|
+
return (cw * pw + cr * pr + out * po) / 1_000_000
|
|
39
|
+
|
|
40
|
+
# ── ANSI ──────────────────────────────────────────────────────────────────────
|
|
41
|
+
C = {
|
|
42
|
+
"reset": "\033[0m", "bold": "\033[1m", "dim": "\033[2m",
|
|
43
|
+
"green": "\033[32m", "yellow": "\033[33m", "blue": "\033[34m",
|
|
44
|
+
"cyan": "\033[36m", "red": "\033[31m", "magenta": "\033[35m",
|
|
45
|
+
"white": "\033[97m", "gray": "\033[90m",
|
|
46
|
+
"bg_cyan": "\033[46m", "bg_blue": "\033[44m", "bg_magenta": "\033[45m",
|
|
47
|
+
"bg_green": "\033[42m", "bg_yellow": "\033[43m", "bg_red": "\033[41m",
|
|
48
|
+
"underline": "\033[4m",
|
|
49
|
+
}
|
|
50
|
+
NO_COLOR = os.environ.get("NO_COLOR") is not None
|
|
51
|
+
|
|
52
|
+
def c(text, *styles):
|
|
53
|
+
if NO_COLOR:
|
|
54
|
+
return str(text)
|
|
55
|
+
return "".join(C[s] for s in styles) + str(text) + C["reset"]
|
|
56
|
+
|
|
57
|
+
def fmt(n):
|
|
58
|
+
if n >= 1_000_000: return "{:.1f}M".format(n / 1_000_000)
|
|
59
|
+
if n >= 1_000: return "{:.1f}K".format(n / 1_000)
|
|
60
|
+
return str(n)
|
|
61
|
+
|
|
62
|
+
def fmt_cost(v):
|
|
63
|
+
if v >= 1: return "${:.2f}".format(v)
|
|
64
|
+
if v >= 0.01: return "${:.3f}".format(v)
|
|
65
|
+
return "${:.4f}".format(v)
|
|
66
|
+
|
|
67
|
+
def bar(pct, width=20):
|
|
68
|
+
filled = int(pct / 100 * width)
|
|
69
|
+
if pct < 40:
|
|
70
|
+
return c("━" * filled, "green") + c("╌" * (width - filled), "dim")
|
|
71
|
+
elif pct < 70:
|
|
72
|
+
return c("━" * filled, "yellow") + c("╌" * (width - filled), "dim")
|
|
73
|
+
else:
|
|
74
|
+
return c("━" * filled, "red") + c("╌" * (width - filled), "dim")
|
|
75
|
+
|
|
76
|
+
def tw():
|
|
77
|
+
try: return os.get_terminal_size().columns
|
|
78
|
+
except: return 80
|
|
79
|
+
|
|
80
|
+
# ── Box drawing helpers ───────────────────────────────────────────────────────
|
|
81
|
+
_ansi_re = re.compile(r'\033\[[0-9;]*m')
|
|
82
|
+
|
|
83
|
+
def vlen(s):
|
|
84
|
+
plain = _ansi_re.sub('', s)
|
|
85
|
+
w = 0
|
|
86
|
+
for ch in plain:
|
|
87
|
+
eaw = unicodedata.east_asian_width(ch)
|
|
88
|
+
w += 2 if eaw in ('W', 'F') else 1
|
|
89
|
+
return w
|
|
90
|
+
|
|
91
|
+
def vrpad(s, width):
|
|
92
|
+
return s + " " * max(width - vlen(s), 0)
|
|
93
|
+
|
|
94
|
+
def vlpad(s, width):
|
|
95
|
+
return " " * max(width - vlen(s), 0) + s
|
|
96
|
+
|
|
97
|
+
def vpad(s, width):
|
|
98
|
+
return vrpad(s, width)
|
|
99
|
+
|
|
100
|
+
def box_top(title, width):
|
|
101
|
+
inner = width - 2
|
|
102
|
+
label = " {} ".format(title)
|
|
103
|
+
lw = vlen(label)
|
|
104
|
+
left = (inner - lw) // 2
|
|
105
|
+
right = inner - lw - left
|
|
106
|
+
return (c("╭", "dim") + c("─" * left, "dim") +
|
|
107
|
+
c(label, "bold", "cyan") + c("─" * right, "dim") + c("╮", "dim"))
|
|
108
|
+
|
|
109
|
+
def box_bot(width):
|
|
110
|
+
return c("╰" + "─" * (width - 2) + "╯", "dim")
|
|
111
|
+
|
|
112
|
+
def box_sep(width):
|
|
113
|
+
return c("├" + "─" * (width - 2) + "┤", "dim")
|
|
114
|
+
|
|
115
|
+
def box_line(content, width):
|
|
116
|
+
inner = width - 2
|
|
117
|
+
return c("│", "dim") + vpad(" " + content, inner) + c("│", "dim")
|
|
118
|
+
|
|
119
|
+
# ── DB ────────────────────────────────────────────────────────────────────────
|
|
120
|
+
def query(db_path, sql, params=()):
|
|
121
|
+
if not db_path.exists():
|
|
122
|
+
return []
|
|
123
|
+
conn = sqlite3.connect("file:{}?mode=ro".format(db_path), uri=True)
|
|
124
|
+
conn.row_factory = sqlite3.Row
|
|
125
|
+
try:
|
|
126
|
+
return conn.execute(sql, params).fetchall()
|
|
127
|
+
finally:
|
|
128
|
+
conn.close()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Background archiver — watches Kiro CLI DB and snapshots sessions to disk.
|
|
2
|
+
|
|
3
|
+
Runs as a persistent background process (launchd on macOS, systemd on Linux).
|
|
4
|
+
Polls the DB file's mtime every 10s and archives any new/updated conversations
|
|
5
|
+
to ~/.kiro_sessions/<conversation_id>.json.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json, sys, os, time, signal
|
|
9
|
+
from . import CLI_DB, SESSIONS_DIR, query
|
|
10
|
+
|
|
11
|
+
def ensure_sessions_dir():
|
|
12
|
+
SESSIONS_DIR.mkdir(exist_ok=True)
|
|
13
|
+
|
|
14
|
+
def archive_sessions():
|
|
15
|
+
"""Snapshot all live conversations from the CLI DB into ~/.kiro_sessions/."""
|
|
16
|
+
ensure_sessions_dir()
|
|
17
|
+
rows = query(CLI_DB, """
|
|
18
|
+
SELECT conversation_id, key as cwd, created_at, updated_at, value
|
|
19
|
+
FROM conversations_v2
|
|
20
|
+
""")
|
|
21
|
+
archived = 0
|
|
22
|
+
for row in rows:
|
|
23
|
+
path = SESSIONS_DIR / "{}.json".format(row["conversation_id"])
|
|
24
|
+
if path.exists():
|
|
25
|
+
try:
|
|
26
|
+
existing = json.loads(path.read_text())
|
|
27
|
+
if existing.get("updated_at") >= row["updated_at"]:
|
|
28
|
+
continue
|
|
29
|
+
except (json.JSONDecodeError, KeyError):
|
|
30
|
+
pass
|
|
31
|
+
snapshot = {
|
|
32
|
+
"conversation_id": row["conversation_id"],
|
|
33
|
+
"cwd": row["cwd"],
|
|
34
|
+
"created_at": row["created_at"],
|
|
35
|
+
"updated_at": row["updated_at"],
|
|
36
|
+
"value": json.loads(row["value"]),
|
|
37
|
+
}
|
|
38
|
+
path.write_text(json.dumps(snapshot, separators=(",", ":")))
|
|
39
|
+
archived += 1
|
|
40
|
+
return archived
|
|
41
|
+
|
|
42
|
+
def load_archived_sessions():
|
|
43
|
+
"""Load all snapshots from ~/.kiro_sessions/."""
|
|
44
|
+
ensure_sessions_dir()
|
|
45
|
+
sessions = []
|
|
46
|
+
for path in SESSIONS_DIR.glob("*.json"):
|
|
47
|
+
try:
|
|
48
|
+
sessions.append(json.loads(path.read_text()))
|
|
49
|
+
except (json.JSONDecodeError, OSError):
|
|
50
|
+
continue
|
|
51
|
+
return sessions
|
|
52
|
+
|
|
53
|
+
def _log(msg):
|
|
54
|
+
sys.stderr.write("[kiro-archiver] {}\n".format(msg))
|
|
55
|
+
sys.stderr.flush()
|
|
56
|
+
|
|
57
|
+
def main():
|
|
58
|
+
"""Poll CLI_DB mtime every 10s, archive on change."""
|
|
59
|
+
running = True
|
|
60
|
+
def _stop(*_):
|
|
61
|
+
nonlocal running
|
|
62
|
+
running = False
|
|
63
|
+
signal.signal(signal.SIGTERM, _stop)
|
|
64
|
+
signal.signal(signal.SIGINT, _stop)
|
|
65
|
+
|
|
66
|
+
interval = int(os.environ.get("KIRO_ARCHIVE_INTERVAL", "10"))
|
|
67
|
+
_log("started — watching {} every {}s".format(CLI_DB, interval))
|
|
68
|
+
|
|
69
|
+
last_mtime = 0.0
|
|
70
|
+
while running:
|
|
71
|
+
try:
|
|
72
|
+
if CLI_DB.exists():
|
|
73
|
+
mtime = CLI_DB.stat().st_mtime
|
|
74
|
+
if mtime != last_mtime:
|
|
75
|
+
last_mtime = mtime
|
|
76
|
+
n = archive_sessions()
|
|
77
|
+
if n:
|
|
78
|
+
_log("archived {} session(s)".format(n))
|
|
79
|
+
except Exception as e:
|
|
80
|
+
_log("error: {}".format(e))
|
|
81
|
+
# Sleep in small increments so SIGTERM is responsive
|
|
82
|
+
for _ in range(interval):
|
|
83
|
+
if not running:
|
|
84
|
+
break
|
|
85
|
+
time.sleep(1)
|
|
86
|
+
|
|
87
|
+
_log("stopped")
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Service installer — register/unregister the archiver as a background service.
|
|
2
|
+
|
|
3
|
+
macOS: ~/Library/LaunchAgents/dev.kiro.usage-archiver.plist (launchctl)
|
|
4
|
+
Linux: ~/.config/systemd/user/kiro-usage-archiver.service (systemd --user)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os, platform, shutil, subprocess, textwrap
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
LABEL = "dev.kiro.usage-archiver"
|
|
11
|
+
|
|
12
|
+
def _find_archiver_bin():
|
|
13
|
+
"""Find the kiro-usage-archiver binary on PATH."""
|
|
14
|
+
p = shutil.which("kiro-usage-archiver")
|
|
15
|
+
if p:
|
|
16
|
+
return p
|
|
17
|
+
# Fallback: same directory as the current Python interpreter
|
|
18
|
+
d = Path(os.sys.executable).parent / "kiro-usage-archiver"
|
|
19
|
+
if d.exists():
|
|
20
|
+
return str(d)
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
# ── macOS (launchd) ───────────────────────────────────────────────────────────
|
|
24
|
+
def _launchd_plist_path():
|
|
25
|
+
return Path.home() / "Library/LaunchAgents/{}.plist".format(LABEL)
|
|
26
|
+
|
|
27
|
+
def _launchd_install():
|
|
28
|
+
bin_path = _find_archiver_bin()
|
|
29
|
+
if not bin_path:
|
|
30
|
+
print("Error: kiro-usage-archiver not found on PATH")
|
|
31
|
+
return False
|
|
32
|
+
plist = textwrap.dedent("""\
|
|
33
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
34
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
35
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
36
|
+
<plist version="1.0">
|
|
37
|
+
<dict>
|
|
38
|
+
<key>Label</key>
|
|
39
|
+
<string>{label}</string>
|
|
40
|
+
<key>ProgramArguments</key>
|
|
41
|
+
<array>
|
|
42
|
+
<string>{bin}</string>
|
|
43
|
+
</array>
|
|
44
|
+
<key>RunAtLoad</key>
|
|
45
|
+
<true/>
|
|
46
|
+
<key>KeepAlive</key>
|
|
47
|
+
<true/>
|
|
48
|
+
<key>StandardErrorPath</key>
|
|
49
|
+
<string>{log}</string>
|
|
50
|
+
<key>StandardOutPath</key>
|
|
51
|
+
<string>{log}</string>
|
|
52
|
+
</dict>
|
|
53
|
+
</plist>
|
|
54
|
+
""").format(
|
|
55
|
+
label=LABEL,
|
|
56
|
+
bin=bin_path,
|
|
57
|
+
log=str(Path.home() / ".kiro_sessions/archiver.log"),
|
|
58
|
+
)
|
|
59
|
+
path = _launchd_plist_path()
|
|
60
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
# Unload first if already loaded
|
|
62
|
+
if path.exists():
|
|
63
|
+
subprocess.run(["launchctl", "bootout", "gui/{}".format(os.getuid()), str(path)],
|
|
64
|
+
capture_output=True)
|
|
65
|
+
path.write_text(plist)
|
|
66
|
+
r = subprocess.run(["launchctl", "bootstrap", "gui/{}".format(os.getuid()), str(path)],
|
|
67
|
+
capture_output=True, text=True)
|
|
68
|
+
if r.returncode != 0:
|
|
69
|
+
print("Warning: launchctl bootstrap: {}".format(r.stderr.strip()))
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
def _launchd_uninstall():
|
|
73
|
+
path = _launchd_plist_path()
|
|
74
|
+
if not path.exists():
|
|
75
|
+
print("Not installed.")
|
|
76
|
+
return False
|
|
77
|
+
subprocess.run(["launchctl", "bootout", "gui/{}".format(os.getuid()), str(path)],
|
|
78
|
+
capture_output=True)
|
|
79
|
+
path.unlink()
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
# ── Linux (systemd --user) ────────────────────────────────────────────────────
|
|
83
|
+
def _systemd_unit_path():
|
|
84
|
+
return Path.home() / ".config/systemd/user/kiro-usage-archiver.service"
|
|
85
|
+
|
|
86
|
+
def _systemd_install():
|
|
87
|
+
bin_path = _find_archiver_bin()
|
|
88
|
+
if not bin_path:
|
|
89
|
+
print("Error: kiro-usage-archiver not found on PATH")
|
|
90
|
+
return False
|
|
91
|
+
unit = textwrap.dedent("""\
|
|
92
|
+
[Unit]
|
|
93
|
+
Description=Kiro Usage Session Archiver
|
|
94
|
+
|
|
95
|
+
[Service]
|
|
96
|
+
ExecStart={bin}
|
|
97
|
+
Restart=on-failure
|
|
98
|
+
RestartSec=5
|
|
99
|
+
|
|
100
|
+
[Install]
|
|
101
|
+
WantedBy=default.target
|
|
102
|
+
""").format(bin=bin_path)
|
|
103
|
+
path = _systemd_unit_path()
|
|
104
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
path.write_text(unit)
|
|
106
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
107
|
+
subprocess.run(["systemctl", "--user", "enable", "--now", "kiro-usage-archiver"],
|
|
108
|
+
capture_output=True)
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def _systemd_uninstall():
|
|
112
|
+
path = _systemd_unit_path()
|
|
113
|
+
if not path.exists():
|
|
114
|
+
print("Not installed.")
|
|
115
|
+
return False
|
|
116
|
+
subprocess.run(["systemctl", "--user", "disable", "--now", "kiro-usage-archiver"],
|
|
117
|
+
capture_output=True)
|
|
118
|
+
path.unlink()
|
|
119
|
+
subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
123
|
+
def install():
|
|
124
|
+
if platform.system() == "Darwin":
|
|
125
|
+
ok = _launchd_install()
|
|
126
|
+
else:
|
|
127
|
+
ok = _systemd_install()
|
|
128
|
+
if ok:
|
|
129
|
+
print("✅ Archiver installed and running.")
|
|
130
|
+
print(" Sessions will be archived to ~/.kiro_sessions/")
|
|
131
|
+
return ok
|
|
132
|
+
|
|
133
|
+
def uninstall():
|
|
134
|
+
if platform.system() == "Darwin":
|
|
135
|
+
ok = _launchd_uninstall()
|
|
136
|
+
else:
|
|
137
|
+
ok = _systemd_uninstall()
|
|
138
|
+
if ok:
|
|
139
|
+
print("✅ Archiver uninstalled.")
|
|
140
|
+
return ok
|
|
141
|
+
|
|
142
|
+
def status():
|
|
143
|
+
if platform.system() == "Darwin":
|
|
144
|
+
path = _launchd_plist_path()
|
|
145
|
+
if not path.exists():
|
|
146
|
+
print("Archiver: not installed")
|
|
147
|
+
return
|
|
148
|
+
r = subprocess.run(
|
|
149
|
+
["launchctl", "print", "gui/{}/{}".format(os.getuid(), LABEL)],
|
|
150
|
+
capture_output=True, text=True)
|
|
151
|
+
if r.returncode == 0:
|
|
152
|
+
# Extract state line
|
|
153
|
+
for line in r.stdout.splitlines():
|
|
154
|
+
if "state" in line.lower():
|
|
155
|
+
print("Archiver: {}".format(line.strip()))
|
|
156
|
+
return
|
|
157
|
+
print("Archiver: loaded")
|
|
158
|
+
else:
|
|
159
|
+
print("Archiver: installed but not running")
|
|
160
|
+
else:
|
|
161
|
+
r = subprocess.run(
|
|
162
|
+
["systemctl", "--user", "is-active", "kiro-usage-archiver"],
|
|
163
|
+
capture_output=True, text=True)
|
|
164
|
+
print("Archiver: {}".format(r.stdout.strip() or "not installed"))
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Kiro Usage Tracker — terminal viewer.
|
|
2
|
+
|
|
3
|
+
Reads archived sessions from ~/.kiro_sessions/ and live DB,
|
|
4
|
+
renders a TUI dashboard with token usage, costs, and session details.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json, sys, os, time, signal
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from . import (CLI_DB, SESSIONS_DIR, CHARS_PER_TOKEN, calc_cost, query,
|
|
12
|
+
c, fmt, fmt_cost, bar, tw,
|
|
13
|
+
vpad, vlpad, box_top, box_bot, box_sep, box_line)
|
|
14
|
+
from .archiver import archive_sessions, load_archived_sessions
|
|
15
|
+
|
|
16
|
+
# ── Parse a single conversation snapshot into display-ready stats ─────────────
|
|
17
|
+
def parse_conversation(conv_id, cwd, created_at_ms, updated_at_ms, data):
|
|
18
|
+
turns = data.get("history", [])
|
|
19
|
+
totals = {"cw": 0, "cr": 0, "out": 0, "cost": 0.0}
|
|
20
|
+
cumulative, prev_asst = 0, 0
|
|
21
|
+
models, tools = set(), []
|
|
22
|
+
daily = {}
|
|
23
|
+
|
|
24
|
+
for i, turn in enumerate(turns):
|
|
25
|
+
meta = turn.get("request_metadata") or {}
|
|
26
|
+
user_tok = len(json.dumps(turn.get("user", {}))) // CHARS_PER_TOKEN
|
|
27
|
+
asst_tok = len(json.dumps(turn.get("assistant", {}))) // CHARS_PER_TOKEN
|
|
28
|
+
out_tok = len(meta.get("time_between_chunks", []))
|
|
29
|
+
model = meta.get("model_id")
|
|
30
|
+
|
|
31
|
+
cr = cumulative if i > 0 else 0
|
|
32
|
+
cw = user_tok + (prev_asst if i > 0 else 0)
|
|
33
|
+
tc = calc_cost(cw, cr, out_tok, model)
|
|
34
|
+
|
|
35
|
+
totals["cw"] += cw; totals["cr"] += cr
|
|
36
|
+
totals["out"] += out_tok; totals["cost"] += tc
|
|
37
|
+
cumulative += user_tok + asst_tok
|
|
38
|
+
prev_asst = asst_tok
|
|
39
|
+
|
|
40
|
+
if model:
|
|
41
|
+
models.add(model)
|
|
42
|
+
for t in meta.get("tool_use_ids_and_names", []):
|
|
43
|
+
if len(t) > 1:
|
|
44
|
+
tools.append(t[1])
|
|
45
|
+
|
|
46
|
+
ts_ms = meta.get("request_start_timestamp_ms")
|
|
47
|
+
if ts_ms:
|
|
48
|
+
day = datetime.fromtimestamp(ts_ms / 1000).strftime("%Y-%m-%d")
|
|
49
|
+
if day not in daily:
|
|
50
|
+
daily[day] = {"cw": 0, "cr": 0, "out": 0, "cost": 0.0, "reqs": 0}
|
|
51
|
+
daily[day]["cw"] += cw; daily[day]["cr"] += cr
|
|
52
|
+
daily[day]["out"] += out_tok; daily[day]["cost"] += tc
|
|
53
|
+
daily[day]["reqs"] += 1
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"id": conv_id[:8], "full_id": conv_id, "cwd": cwd,
|
|
57
|
+
"created": datetime.fromtimestamp(created_at_ms / 1000),
|
|
58
|
+
"updated": datetime.fromtimestamp(updated_at_ms / 1000),
|
|
59
|
+
"turns": len(turns), **totals,
|
|
60
|
+
"models": models, "tools": tools, "daily": daily,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# ── Load all sessions (live DB + archive), deduplicated ───────────────────────
|
|
64
|
+
def load_all_sessions(days=None):
|
|
65
|
+
cutoff_ms = None
|
|
66
|
+
if days and days < 9000:
|
|
67
|
+
cutoff_ms = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
|
|
68
|
+
|
|
69
|
+
archive_sessions()
|
|
70
|
+
seen = {}
|
|
71
|
+
|
|
72
|
+
for snap in load_archived_sessions():
|
|
73
|
+
if cutoff_ms and snap["updated_at"] < cutoff_ms:
|
|
74
|
+
continue
|
|
75
|
+
try:
|
|
76
|
+
seen[snap["conversation_id"]] = parse_conversation(
|
|
77
|
+
snap["conversation_id"], snap["cwd"],
|
|
78
|
+
snap["created_at"], snap["updated_at"], snap["value"])
|
|
79
|
+
except Exception:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
for row in query(CLI_DB, "SELECT conversation_id, key as cwd, created_at, updated_at, value FROM conversations_v2 ORDER BY updated_at DESC"):
|
|
83
|
+
if cutoff_ms and row["updated_at"] < cutoff_ms:
|
|
84
|
+
continue
|
|
85
|
+
try:
|
|
86
|
+
data = json.loads(row["value"])
|
|
87
|
+
except Exception:
|
|
88
|
+
continue
|
|
89
|
+
seen[row["conversation_id"]] = parse_conversation(
|
|
90
|
+
row["conversation_id"], row["cwd"],
|
|
91
|
+
row["created_at"], row["updated_at"], data)
|
|
92
|
+
|
|
93
|
+
return sorted(seen.values(), key=lambda x: x["updated"], reverse=True)
|
|
94
|
+
|
|
95
|
+
# ── Render ────────────────────────────────────────────────────────────────────
|
|
96
|
+
def render(days):
|
|
97
|
+
cli_convos = load_all_sessions(days)
|
|
98
|
+
w = min(tw(), 120)
|
|
99
|
+
L = []
|
|
100
|
+
now = datetime.now().strftime("%H:%M:%S")
|
|
101
|
+
label = "all time" if days > 9000 else "last {} day{}".format(days, "s" if days != 1 else "")
|
|
102
|
+
|
|
103
|
+
L.append("")
|
|
104
|
+
L.append(" " + c("Kiro Usage Tracker", "bold", "cyan") +
|
|
105
|
+
" " + c("{} {}".format(label, now), "dim"))
|
|
106
|
+
L.append("")
|
|
107
|
+
|
|
108
|
+
L.append(box_top("Terminal", w))
|
|
109
|
+
if cli_convos:
|
|
110
|
+
t_cw = sum(cv["cw"] for cv in cli_convos)
|
|
111
|
+
t_cr = sum(cv["cr"] for cv in cli_convos)
|
|
112
|
+
t_out = sum(cv["out"] for cv in cli_convos)
|
|
113
|
+
t_cost = sum(cv["cost"] for cv in cli_convos)
|
|
114
|
+
t_reqs = sum(cv["turns"] for cv in cli_convos)
|
|
115
|
+
all_models = set()
|
|
116
|
+
for cv in cli_convos:
|
|
117
|
+
all_models.update(cv["models"])
|
|
118
|
+
|
|
119
|
+
L.append(box_line(
|
|
120
|
+
"{} reqs CWrite {} CRead {} Output {} Cost {}".format(
|
|
121
|
+
c(t_reqs, "bold", "white"), c(fmt(t_cw), "green"),
|
|
122
|
+
c(fmt(t_cr), "yellow"), c(fmt(t_out), "blue"),
|
|
123
|
+
c(fmt_cost(t_cost), "red", "bold")), w))
|
|
124
|
+
if all_models:
|
|
125
|
+
L.append(box_line(
|
|
126
|
+
"Models: " + " ".join(c(m, "magenta") for m in sorted(all_models)), w))
|
|
127
|
+
|
|
128
|
+
# Daily breakdown
|
|
129
|
+
cli_daily = {}
|
|
130
|
+
for cv in cli_convos:
|
|
131
|
+
for d, v in cv["daily"].items():
|
|
132
|
+
if d not in cli_daily:
|
|
133
|
+
cli_daily[d] = {"cw": 0, "cr": 0, "out": 0, "cost": 0.0, "reqs": 0}
|
|
134
|
+
for k in ("cw", "cr", "out", "cost", "reqs"):
|
|
135
|
+
cli_daily[d][k] += v[k]
|
|
136
|
+
|
|
137
|
+
if cli_daily:
|
|
138
|
+
L.append(box_sep(w))
|
|
139
|
+
max_r = max(v["reqs"] for v in cli_daily.values())
|
|
140
|
+
hdr = "{} {:>5} {:>8} {:>8} {:>7} {:>8} {}".format(
|
|
141
|
+
vpad("Date", 12), "Reqs", "CWrite", "CRead", "Output", "Cost", "Activity")
|
|
142
|
+
L.append(box_line(c(hdr, "dim"), w))
|
|
143
|
+
|
|
144
|
+
for d in sorted(cli_daily.keys(), reverse=True):
|
|
145
|
+
v = cli_daily[d]
|
|
146
|
+
pct = v["reqs"] / max_r * 100 if max_r else 0
|
|
147
|
+
is_today = d == datetime.now().strftime("%Y-%m-%d")
|
|
148
|
+
day_c = c(d, "bold", "white") if is_today else c(d, "dim")
|
|
149
|
+
marker = c("> ", "cyan") if is_today else " "
|
|
150
|
+
reqs_c = c(str(v["reqs"]), "bold") if is_today else str(v["reqs"])
|
|
151
|
+
cost_c = c(fmt_cost(v["cost"]), "red") if v["cost"] >= 0.1 else fmt_cost(v["cost"])
|
|
152
|
+
line = "{}{} {} {} {} {} {} {}".format(
|
|
153
|
+
marker, vpad(day_c, 10), vlpad(reqs_c, 5),
|
|
154
|
+
vlpad(fmt(v["cw"]), 8), vlpad(fmt(v["cr"]), 8),
|
|
155
|
+
vlpad(fmt(v["out"]), 7), vlpad(cost_c, 8), bar(pct))
|
|
156
|
+
L.append(box_line(line, w))
|
|
157
|
+
|
|
158
|
+
# Tool usage
|
|
159
|
+
all_tools = {}
|
|
160
|
+
for cv in cli_convos:
|
|
161
|
+
for t in cv["tools"]:
|
|
162
|
+
all_tools[t] = all_tools.get(t, 0) + 1
|
|
163
|
+
if all_tools:
|
|
164
|
+
L.append(box_sep(w))
|
|
165
|
+
top = sorted(all_tools.items(), key=lambda x: -x[1])[:6]
|
|
166
|
+
inner = w - 4
|
|
167
|
+
parts, cur = [], 7
|
|
168
|
+
for n, cnt in top:
|
|
169
|
+
part = "{}{}{}".format(c(n, "cyan"), c("x", "dim"), c(cnt, "bold"))
|
|
170
|
+
plen = len(n) + 1 + len(str(cnt))
|
|
171
|
+
if cur + plen + 2 > inner:
|
|
172
|
+
break
|
|
173
|
+
parts.append(part)
|
|
174
|
+
cur += plen + 2
|
|
175
|
+
L.append(box_line("Tools: " + " ".join(parts), w))
|
|
176
|
+
|
|
177
|
+
# Sessions
|
|
178
|
+
L.append(box_sep(w))
|
|
179
|
+
shdr = "{} {} {:>5} {:>7} {:>7} {:>6} {:>6} {}".format(
|
|
180
|
+
vpad("ID", 8), vpad("Directory", 18),
|
|
181
|
+
"Turns", "CWrite", "CRead", "Out", "Cost", "Updated")
|
|
182
|
+
L.append(box_line(c(shdr, "dim"), w))
|
|
183
|
+
|
|
184
|
+
for cv in cli_convos:
|
|
185
|
+
cwd = cv["cwd"].replace(str(Path.home()), "~")
|
|
186
|
+
if len(cwd) > 17:
|
|
187
|
+
cwd = ".." + cwd[-15:]
|
|
188
|
+
age = datetime.now() - cv["updated"]
|
|
189
|
+
if age < timedelta(hours=1):
|
|
190
|
+
dot, ts = c("*", "green"), cv["updated"].strftime("%H:%M")
|
|
191
|
+
elif age < timedelta(hours=6):
|
|
192
|
+
dot, ts = c("*", "yellow"), cv["updated"].strftime("%H:%M")
|
|
193
|
+
else:
|
|
194
|
+
dot, ts = c(".", "dim"), cv["updated"].strftime("%m-%d %H:%M")
|
|
195
|
+
updated = vpad("{} {}".format(dot, ts), 13)
|
|
196
|
+
line = "{} {} {:>5} {:>7} {:>7} {:>6} {:>6} {}".format(
|
|
197
|
+
vpad(c(cv["id"], "cyan"), 8), vpad(cwd, 18), cv["turns"],
|
|
198
|
+
fmt(cv["cw"]), fmt(cv["cr"]), fmt(cv["out"]),
|
|
199
|
+
fmt_cost(cv["cost"]), updated)
|
|
200
|
+
L.append(box_line(line, w))
|
|
201
|
+
else:
|
|
202
|
+
L.append(box_line(c("No CLI data found.", "dim"), w))
|
|
203
|
+
|
|
204
|
+
L.append(box_bot(w))
|
|
205
|
+
L.append(" " + c(
|
|
206
|
+
"📁 Sessions archived to ~/.kiro_sessions/ — history persists across /clear and restarts",
|
|
207
|
+
"dim", "green"))
|
|
208
|
+
L.append("")
|
|
209
|
+
return "\n".join(L)
|
|
210
|
+
|
|
211
|
+
# ── Modes ─────────────────────────────────────────────────────────────────────
|
|
212
|
+
def _clear():
|
|
213
|
+
sys.stdout.write("\033[2J\033[H"); sys.stdout.flush()
|
|
214
|
+
|
|
215
|
+
def live(days, interval=5):
|
|
216
|
+
signal.signal(signal.SIGINT, lambda *_: (sys.stdout.write("\033[?25h\n"), sys.exit(0)))
|
|
217
|
+
sys.stdout.write("\033[?25l")
|
|
218
|
+
try:
|
|
219
|
+
while True:
|
|
220
|
+
_clear()
|
|
221
|
+
print(render(days))
|
|
222
|
+
print(c(" ⏸ Ctrl+C to exit │ 🔄 refreshing every {}s".format(interval), "dim"))
|
|
223
|
+
time.sleep(interval)
|
|
224
|
+
finally:
|
|
225
|
+
sys.stdout.write("\033[?25h")
|
|
226
|
+
|
|
227
|
+
def view_json(days):
|
|
228
|
+
cli_convos = load_all_sessions(days)
|
|
229
|
+
out = {"cli": {"daily": {}, "sessions": []}}
|
|
230
|
+
for cv in cli_convos:
|
|
231
|
+
for d, v in cv["daily"].items():
|
|
232
|
+
if d not in out["cli"]["daily"]:
|
|
233
|
+
out["cli"]["daily"][d] = {
|
|
234
|
+
"requests": 0, "cache_write_est": 0,
|
|
235
|
+
"cache_read_est": 0, "output_tokens": 0, "cost_est_usd": 0.0,
|
|
236
|
+
}
|
|
237
|
+
for k, jk in [("reqs", "requests"), ("cw", "cache_write_est"),
|
|
238
|
+
("cr", "cache_read_est"), ("out", "output_tokens"),
|
|
239
|
+
("cost", "cost_est_usd")]:
|
|
240
|
+
out["cli"]["daily"][d][jk] += v[k]
|
|
241
|
+
out["cli"]["sessions"].append({
|
|
242
|
+
"id": cv["id"], "cwd": cv["cwd"], "turns": cv["turns"],
|
|
243
|
+
"cache_write_est": cv["cw"], "cache_read_est": cv["cr"],
|
|
244
|
+
"output_tokens": cv["out"], "cost_est_usd": round(cv["cost"], 4),
|
|
245
|
+
"models": list(cv["models"]),
|
|
246
|
+
"created": cv["created"].isoformat(),
|
|
247
|
+
"updated": cv["updated"].isoformat(),
|
|
248
|
+
})
|
|
249
|
+
for d in out["cli"]["daily"]:
|
|
250
|
+
out["cli"]["daily"][d]["cost_est_usd"] = round(
|
|
251
|
+
out["cli"]["daily"][d]["cost_est_usd"], 4)
|
|
252
|
+
print(json.dumps(out, indent=2))
|
|
253
|
+
|
|
254
|
+
# ── CLI entry point ───────────────────────────────────────────────────────────
|
|
255
|
+
PERIODS = {"today": 1, "week": 7, "month": 30, "all": 9999}
|
|
256
|
+
|
|
257
|
+
def main():
|
|
258
|
+
from . import service
|
|
259
|
+
|
|
260
|
+
args = sys.argv[1:]
|
|
261
|
+
as_json = "--json" in args
|
|
262
|
+
if as_json:
|
|
263
|
+
args.remove("--json")
|
|
264
|
+
no_live = "--no-live" in args
|
|
265
|
+
if no_live:
|
|
266
|
+
args.remove("--no-live")
|
|
267
|
+
cmd = args[0] if args else "today"
|
|
268
|
+
|
|
269
|
+
# Service management subcommands
|
|
270
|
+
if cmd == "install":
|
|
271
|
+
service.install()
|
|
272
|
+
return
|
|
273
|
+
if cmd == "uninstall":
|
|
274
|
+
service.uninstall()
|
|
275
|
+
return
|
|
276
|
+
if cmd == "status":
|
|
277
|
+
service.status()
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
if cmd in ("help", "-h", "--help"):
|
|
281
|
+
print("""
|
|
282
|
+
{} {}
|
|
283
|
+
|
|
284
|
+
{} kiro-usage [command] [flags]
|
|
285
|
+
|
|
286
|
+
{}
|
|
287
|
+
today Last 24 hours (default, live refresh)
|
|
288
|
+
week Last 7 days
|
|
289
|
+
month Last 30 days
|
|
290
|
+
all All time
|
|
291
|
+
install Register background archiver as a system service
|
|
292
|
+
uninstall Remove background archiver service
|
|
293
|
+
status Show archiver service status
|
|
294
|
+
|
|
295
|
+
{}
|
|
296
|
+
--json JSON output (no live mode)
|
|
297
|
+
--no-live Print once and exit
|
|
298
|
+
|
|
299
|
+
{} (CLI only)
|
|
300
|
+
✏️ CacheWrite = new tokens per turn (estimated, chars/4)
|
|
301
|
+
📖 CacheRead = prior context resent to API (estimated)
|
|
302
|
+
📤 Output = tokens streamed back (from chunks, accurate)
|
|
303
|
+
💰 Cost = cache-aware pricing estimate
|
|
304
|
+
|
|
305
|
+
{} (5-min cache write rate)
|
|
306
|
+
Model CWrite CRead Output
|
|
307
|
+
claude-opus-4.6 $6.25 $0.50 $25/MTok
|
|
308
|
+
claude-opus-4.5 $6.25 $0.50 $25/MTok
|
|
309
|
+
claude-opus-4.1 $18.75 $1.50 $75/MTok
|
|
310
|
+
claude-sonnet-4.x $3.75 $0.30 $15/MTok
|
|
311
|
+
|
|
312
|
+
{} 📂
|
|
313
|
+
CLI DB: {}
|
|
314
|
+
Archive: {}
|
|
315
|
+
""".format(
|
|
316
|
+
c("⚡", "yellow"), c("Kiro Usage Tracker", "bold", "cyan"),
|
|
317
|
+
c("Usage:", "bold"), c("📋 Commands:", "bold"),
|
|
318
|
+
c("🚩 Flags:", "bold"), c("📊 Metrics:", "bold"),
|
|
319
|
+
c("💲 Pricing $/MTok:", "bold"),
|
|
320
|
+
c("Data sources:", "dim"), CLI_DB, SESSIONS_DIR))
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
period = PERIODS.get(cmd)
|
|
324
|
+
if period is None:
|
|
325
|
+
print("Unknown command: " + cmd)
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
|
|
328
|
+
if as_json:
|
|
329
|
+
view_json(period)
|
|
330
|
+
elif no_live or not sys.stdout.isatty():
|
|
331
|
+
print(render(period))
|
|
332
|
+
else:
|
|
333
|
+
live(period)
|
|
334
|
+
|
|
335
|
+
if __name__ == "__main__":
|
|
336
|
+
main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kiro-usage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Track token usage and costs across Kiro CLI sessions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Environment :: Console
|
|
7
|
+
Classifier: Topic :: Utilities
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: MacOS
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# kiro-usage
|
|
15
|
+
|
|
16
|
+
Track token usage and costs across [Kiro CLI](https://kiro.dev) sessions.
|
|
17
|
+
|
|
18
|
+
Real-time terminal dashboard with session archiving that persists across `/clear` and restarts.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
# install uv if you don't have it
|
|
24
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
25
|
+
|
|
26
|
+
uv tool install kiro-usage && kiro-usage install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
kiro-usage # today's usage, live refresh
|
|
33
|
+
kiro-usage week # last 7 days
|
|
34
|
+
kiro-usage month # last 30 days
|
|
35
|
+
kiro-usage all # all time
|
|
36
|
+
kiro-usage --json # JSON output
|
|
37
|
+
kiro-usage --no-live # print once and exit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Service Management
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
kiro-usage install # register background archiver (launchd / systemd)
|
|
44
|
+
kiro-usage uninstall # remove background archiver
|
|
45
|
+
kiro-usage status # check archiver status
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The background archiver watches the Kiro CLI database and snapshots sessions to `~/.kiro_sessions/` every 10 seconds. This ensures session data is preserved even if you `/clear` or quit Kiro CLI.
|
|
49
|
+
|
|
50
|
+
## How It Works
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
kiro-cli SQLite DB
|
|
54
|
+
│
|
|
55
|
+
│ mtime change (every 10s)
|
|
56
|
+
▼
|
|
57
|
+
kiro-usage-archiver ──► ~/.kiro_sessions/*.json
|
|
58
|
+
(launchd / systemd)
|
|
59
|
+
|
|
60
|
+
kiro-usage viewer ◄── ~/.kiro_sessions/*.json
|
|
61
|
+
(on-demand)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Metrics:**
|
|
65
|
+
- **CacheWrite** — new tokens per turn (estimated, chars/4)
|
|
66
|
+
- **CacheRead** — prior context resent to API (estimated)
|
|
67
|
+
- **Output** — tokens streamed back (from chunks, accurate)
|
|
68
|
+
- **Cost** — cache-aware pricing estimate (5-min cache write rate)
|
|
69
|
+
|
|
70
|
+
## Uninstall
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
kiro-usage uninstall
|
|
74
|
+
uv tool uninstall kiro-usage
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
kiro_usage/__init__.py
|
|
4
|
+
kiro_usage/archiver.py
|
|
5
|
+
kiro_usage/service.py
|
|
6
|
+
kiro_usage/viewer.py
|
|
7
|
+
kiro_usage.egg-info/PKG-INFO
|
|
8
|
+
kiro_usage.egg-info/SOURCES.txt
|
|
9
|
+
kiro_usage.egg-info/dependency_links.txt
|
|
10
|
+
kiro_usage.egg-info/entry_points.txt
|
|
11
|
+
kiro_usage.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kiro_usage
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kiro-usage"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Track token usage and costs across Kiro CLI sessions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Topic :: Utilities",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: MacOS",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
kiro-usage = "kiro_usage.viewer:main"
|
|
22
|
+
kiro-usage-archiver = "kiro_usage.archiver:main"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.packages.find]
|
|
25
|
+
include = ["kiro_usage*"]
|