claude-statuskit 0.3.0__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.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-statuskit
3
+ Version: 0.3.0
4
+ Summary: Modular statusline for Claude Code
5
+ Project-URL: Homepage, https://github.com/NoNameItem/claude-tools/tree/master/packages/statuskit
6
+ Project-URL: Repository, https://github.com/NoNameItem/claude-tools
7
+ Author-email: Artem Vasin <nonameitem@me.com>
8
+ License: MIT
9
+ Keywords: claude,claude-code,cli,statusline
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: termcolor
20
+ Description-Content-Type: text/markdown
21
+
22
+ # statuskit
23
+
24
+ [![PyPI version](https://img.shields.io/pypi/v/claude-statuskit)](https://pypi.org/project/claude-statuskit/)
25
+ [![Python versions](https://img.shields.io/pypi/pyversions/claude-statuskit)](https://pypi.org/project/claude-statuskit/)
26
+ [![License](https://img.shields.io/pypi/l/claude-statuskit)](https://github.com/NoNameItem/claude-tools/blob/master/LICENSE)
27
+
28
+ Modular statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
29
+
30
+ Statuskit displays contextual information below Claude's responses: current model, git status, API usage limits, and more. It reads JSON from Claude Code's statusline hook and renders formatted, colored output.
31
+
32
+ ## Features
33
+
34
+ - **Modular architecture** — enable only the modules you need
35
+ - **Configurable** — customize each module's behavior via TOML config
36
+ - **Built-in modules:**
37
+ - `model` — current Claude model name
38
+ - `git` — branch, remote status, changes, last commit, project/worktree location
39
+ - `usage_limits` — API quota tracking (5h session, 7d weekly) with color-coded warnings
40
+ - **Coming soon:**
41
+ - `beads` — display active beads tasks
42
+ - External modules support — load custom modules from separate packages
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ # Using uv (recommended)
48
+ uv tool install claude-statuskit
49
+
50
+ # Using pipx
51
+ pipx install claude-statuskit
52
+
53
+ # Using pip
54
+ pip install claude-statuskit
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ Run the setup command to configure Claude Code:
60
+
61
+ ```bash
62
+ # User-level setup (recommended for personal use)
63
+ statuskit setup
64
+
65
+ # Project-level setup (shared config for team)
66
+ statuskit setup --scope project
67
+
68
+ # Local setup (personal overrides, gitignored)
69
+ statuskit setup --scope local
70
+ ```
71
+
72
+ Setup will:
73
+ 1. Add the statusline hook to Claude Code settings
74
+ 2. Create configuration file at the appropriate level
75
+ 3. Update gitignore for local configs (if applicable)
76
+
77
+ After setup, restart Claude Code to see the statusline.
78
+
79
+ ## Example Output
80
+
81
+ ```
82
+ Opus 4.5 · 47m · 120k free (60%)
83
+ claude-tools master ↑1 +2 ~1 ?3 · a1b2c3d 2h
84
+ Session: 45% (2h 30m) · Weekly: 12%
85
+ ```
86
+
87
+ This shows:
88
+ - **Line 1 (model):** Model name, session duration, context window
89
+ - **Line 2 (git):** Project, branch, remote status, changes, last commit
90
+ - **Line 3 (usage_limits):** API quota for session and weekly limits
91
+
92
+ ## Configuration
93
+
94
+ Configuration files are loaded in priority order (first found wins):
95
+
96
+ | Level | Path | Use case |
97
+ |-------|------|----------|
98
+ | Local | `.claude/statuskit.local.toml` | Personal overrides, gitignored |
99
+ | Project | `.claude/statuskit.toml` | Shared team configuration |
100
+ | User | `~/.claude/statuskit.toml` | Global personal defaults |
101
+
102
+ ### Basic Configuration
103
+
104
+ ```toml
105
+ # Modules to display (in order)
106
+ modules = ["model", "git", "usage_limits"]
107
+
108
+ # Enable debug output
109
+ debug = false
110
+ ```
111
+
112
+ ### Module Configuration
113
+
114
+ Each module can be configured in its own section:
115
+
116
+ ```toml
117
+ [git]
118
+ show_branch = true
119
+ commit_age_format = "compact"
120
+
121
+ [usage_limits]
122
+ show_session = true
123
+ show_weekly = true
124
+ multiline = false
125
+ ```
126
+
127
+ ## Module Reference
128
+
129
+ ### `model` Module
130
+
131
+ Displays model name, session duration, and context window usage.
132
+
133
+ | Parameter | Type | Default | Description |
134
+ |-----------|------|---------|-------------|
135
+ | `show_duration` | bool | `true` | Show session duration |
136
+ | `show_context` | bool | `true` | Show context window usage |
137
+ | `context_format` | string | `"free"` | Context display format (see below) |
138
+ | `context_compact` | bool | `false` | Use compact numbers (e.g., `150k` instead of `150,000`) |
139
+ | `context_threshold_green` | int | `50` | Percentage of free context to show green |
140
+ | `context_threshold_yellow` | int | `25` | Percentage of free context to show yellow (below = red) |
141
+
142
+ **`context_format` values:**
143
+
144
+ | Value | Output example |
145
+ |-------|----------------|
146
+ | `"free"` | `150,000 free (75.0%)` |
147
+ | `"used"` | `50,000 used (25.0%)` |
148
+ | `"ratio"` | `50,000/200,000 (25.0%)` |
149
+ | `"bar"` | `[███████░░░] 70%` |
150
+
151
+ ### `git` Module
152
+
153
+ Displays git branch, remote status, changes, last commit, and project/worktree location.
154
+
155
+ | Parameter | Type | Default | Description |
156
+ |-----------|------|---------|-------------|
157
+ | `show_project` | bool | `true` | Show project (repository) name |
158
+ | `show_worktree` | bool | `true` | Show worktree name with 🌲 indicator |
159
+ | `show_folder` | bool | `true` | Show current subfolder relative to repo root |
160
+ | `show_branch` | bool | `true` | Show current branch name |
161
+ | `show_remote_status` | bool | `true` | Show ahead/behind/diverged status |
162
+ | `show_changes` | bool | `true` | Show staged/modified/untracked counts |
163
+ | `show_commit` | bool | `true` | Show last commit hash and age |
164
+ | `commit_age_format` | string | `"relative"` | Commit age format (see below) |
165
+
166
+ **`commit_age_format` values:**
167
+
168
+ | Value | Output example |
169
+ |-------|----------------|
170
+ | `"relative"` | `2 hours ago` |
171
+ | `"compact"` | `2h` |
172
+
173
+ ### `usage_limits` Module
174
+
175
+ Displays API usage limits with color-coded warnings based on consumption rate.
176
+
177
+ | Parameter | Type | Default | Description |
178
+ |-----------|------|---------|-------------|
179
+ | `show_session` | bool | `true` | Show 5-hour session limit |
180
+ | `show_weekly` | bool | `true` | Show 7-day weekly limit |
181
+ | `show_sonnet` | bool | `false` | Show Sonnet-only weekly limit |
182
+ | `show_reset_time` | bool | `true` | Show time until reset |
183
+ | `multiline` | bool | `true` | Display each limit on separate line |
184
+ | `show_progress_bar` | bool | `false` | Show visual progress bar |
185
+ | `bar_width` | int | `10` | Progress bar width in characters |
186
+ | `session_time_format` | string | `"remaining"` | Time format for session limit |
187
+ | `weekly_time_format` | string | `"reset_at"` | Time format for weekly limit |
188
+ | `sonnet_time_format` | string | `"reset_at"` | Time format for Sonnet limit |
189
+ | `cache_ttl` | int | `60` | Cache lifetime in seconds |
190
+
191
+ **Time format values:**
192
+
193
+ | Value | Output example |
194
+ |-------|----------------|
195
+ | `"remaining"` | `2h 30m` |
196
+ | `"reset_at"` | `Thu 17:00` |
197
+
198
+ **Color coding:** Usage is colored based on consumption rate vs elapsed time:
199
+ - 🟢 Green — on track or under
200
+ - 🟡 Yellow — approaching the limit trajectory
201
+ - 🔴 Red — ahead of pace, may hit limit
202
+
203
+ ## License
204
+
205
+ MIT — see [LICENSE](https://github.com/NoNameItem/claude-tools/blob/master/LICENSE) for details.
@@ -0,0 +1,22 @@
1
+ statuskit/__init__.py,sha256=5YT6UVacwSqav_yy0eTZykROhp3LjbdK8m-Wkia32VY,3642
2
+ statuskit/cli.py,sha256=IRXKUCIiQX-HGjNBeo_ZiA_55L6lUQa1_zsznp2i64Q,1833
3
+ statuskit/core/__init__.py,sha256=ws2smu4R-UF5LWFM35yuGBUuKGuOiy6U68RKmEq3Aqk,37
4
+ statuskit/core/config.py,sha256=HqYhYWv-1VDMDV48R43dSYcJ9qf_ZKdjmvPk3rfjLOQ,2439
5
+ statuskit/core/loader.py,sha256=ehmwwITKZRLgc6yxyOPVqQchHQQFtVSoGn33_Sy_LK0,992
6
+ statuskit/core/models.py,sha256=-9SFMFNCUpYOERLCmta2jdoa-eKl_xGDoYWw4iIuZ0Y,3837
7
+ statuskit/modules/__init__.py,sha256=8Nq_CdDeUtNUtikAqrZnPYmPezDPU_AmHfRTmwfNf7M,163
8
+ statuskit/modules/base.py,sha256=kOuZKZgU3S8oxTENaGuHNuhiYMxkt_MVUlw1C22Hs0M,952
9
+ statuskit/modules/git.py,sha256=ipg_ibj4HKiN26tt_J9Z0IGS7D413nkLIymb6rqxFwQ,12439
10
+ statuskit/modules/model.py,sha256=iJAiFWsjSmTDaMVubabatBLVLOeQbj4TVxHc417hb78,4369
11
+ statuskit/modules/usage_limits.py,sha256=83UhiceuZf0XkxOewdQq49exaVYlibTAkyO154ONfYw,15361
12
+ statuskit/setup/__init__.py,sha256=TlrT2k5z4QUG9Op3w7Ho0lHQCu86NnJCVrJteqnC6NE,35
13
+ statuskit/setup/commands.py,sha256=7ZpsKrscQJj5TPeibY8T-zeiz64jvCGjYD7_htK8kFo,6835
14
+ statuskit/setup/config.py,sha256=IdRHxpknOM2Ho-QwTV98-PXqw1tY_lD72gk6_0xFdm8,645
15
+ statuskit/setup/gitignore.py,sha256=euRZ2lFG3KAM13jMGUt6oiIsjQMifE1zrJuiH3eNe20,1566
16
+ statuskit/setup/hooks.py,sha256=L2owfXpKZle1VAde9c8pPqaR8uXht4cC-44MkWsJMTw,1471
17
+ statuskit/setup/paths.py,sha256=Z_eogXqmsITnlEh9ifWqDMs7YZvrbtWShLT89ZLsi6I,877
18
+ statuskit/setup/ui.py,sha256=Lz9CX02tqqn3GF2EqHsvo2olDd_D7rbN6_u-T5nZ6q4,956
19
+ claude_statuskit-0.3.0.dist-info/METADATA,sha256=ylVqwDjyaR-Dm1uFgobvGK4EheWIe9624JWZ0cEzL2A,7115
20
+ claude_statuskit-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
21
+ claude_statuskit-0.3.0.dist-info/entry_points.txt,sha256=6C0Zr7X1ues0_IA2XtSqH7I64SL7qhBxXd612xEGbMo,45
22
+ claude_statuskit-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ statuskit = statuskit:main
statuskit/__init__.py ADDED
@@ -0,0 +1,120 @@
1
+ """Modular statusline kit for Claude Code."""
2
+
3
+ import json
4
+ import sys
5
+ from argparse import Namespace
6
+
7
+ from termcolor import colored
8
+
9
+ from .cli import create_parser
10
+ from .core.config import load_config
11
+ from .core.loader import load_modules
12
+ from .core.models import RenderContext, StatusInput
13
+
14
+
15
+ def _handle_setup(args: Namespace) -> None:
16
+ """Handle setup command."""
17
+ from .setup.commands import check_installation
18
+ from .setup.paths import Scope
19
+ from .setup.ui import ConsoleUI
20
+
21
+ if args.check:
22
+ print(check_installation())
23
+ return
24
+
25
+ scope = Scope(args.scope)
26
+ ui = None if args.force else ConsoleUI()
27
+
28
+ if args.remove:
29
+ _handle_remove(scope, args.force, ui)
30
+ else:
31
+ _handle_install(scope, args.force, ui)
32
+
33
+
34
+ def _handle_remove(scope, force: bool, ui) -> None:
35
+ """Handle setup --remove command."""
36
+ from .setup.commands import remove_hook
37
+
38
+ result = remove_hook(scope, force=force, ui=ui)
39
+ if result.not_installed:
40
+ print(f"statuskit is not installed at {scope.value} scope.")
41
+ elif result.success:
42
+ print(f"\u2713 Removed statusline hook from {scope.value} scope.")
43
+ else:
44
+ print(f"Error: {result.message}")
45
+ sys.exit(1)
46
+
47
+
48
+ def _handle_install(scope, force: bool, ui) -> None:
49
+ """Handle setup install command."""
50
+ from .setup.commands import install_hook
51
+
52
+ result = install_hook(scope, force=force, ui=ui)
53
+ if result.higher_scope_installed and result.higher_scope:
54
+ print(f"statuskit is already installed at {result.higher_scope.value} scope.")
55
+ print("The hook will work for this project too.")
56
+ if result.config_created:
57
+ print(f"\u2713 Created config file at {scope.value} scope.")
58
+ elif result.already_installed:
59
+ print(f"statuskit is already installed at {scope.value} scope.")
60
+ if result.config_created:
61
+ print("\u2713 Created config file.")
62
+ elif result.success:
63
+ print(f"\u2713 Added statusline hook to {scope.value} scope.")
64
+ if result.backup_created:
65
+ print("\u2713 Created backup of previous settings.")
66
+ if result.config_created:
67
+ print("\u2713 Created config file.")
68
+ if result.gitignore_updated:
69
+ print("\u2713 Added .claude/*.local.* to .gitignore")
70
+ print("\nRun `claude` to see your new statusline!")
71
+ else:
72
+ print(f"Error: {result.message}")
73
+ sys.exit(1)
74
+
75
+
76
+ def _render_statusline() -> None:
77
+ """Read from stdin and render statusline."""
78
+ config = load_config()
79
+
80
+ try:
81
+ raw_data = json.load(sys.stdin)
82
+ data = StatusInput.from_dict(raw_data)
83
+ except Exception as e:
84
+ if config.debug:
85
+ print(colored(f"[!] Failed to parse input: {e}", "red"))
86
+ return
87
+
88
+ ctx = RenderContext(debug=config.debug, data=data, cache_dir=config.cache_dir)
89
+ modules = load_modules(config, ctx)
90
+
91
+ for mod in modules:
92
+ try:
93
+ output = mod.render()
94
+ if output:
95
+ print(output)
96
+ except Exception as e:
97
+ if config.debug:
98
+ print(colored(f"[!] {mod.name}: {e}", "red"))
99
+
100
+
101
+ def main() -> None:
102
+ """Entry point for statuskit command."""
103
+ parser = create_parser()
104
+ args = parser.parse_args()
105
+
106
+ if args.command == "setup":
107
+ _handle_setup(args)
108
+ return
109
+
110
+ if sys.stdin.isatty():
111
+ print("statuskit: reads JSON from stdin")
112
+ print("Usage: echo '{...}' | statuskit")
113
+ print("\nRun 'statuskit setup' to configure Claude Code integration.")
114
+ return
115
+
116
+ _render_statusline()
117
+
118
+
119
+ if __name__ == "__main__":
120
+ main()
statuskit/cli.py ADDED
@@ -0,0 +1,68 @@
1
+ """CLI argument parsing for statuskit."""
2
+
3
+ import argparse
4
+ from importlib.metadata import version
5
+
6
+ MODULES_HELP = """
7
+ Built-in modules:
8
+ model Display current Claude model name
9
+ git Show git branch and status
10
+ beads Display active beads tasks
11
+ quota Track token usage
12
+ """
13
+
14
+
15
+ def get_version() -> str:
16
+ """Get statuskit version from package metadata."""
17
+ try:
18
+ return version("statuskit")
19
+ except Exception:
20
+ return "0.1.0" # fallback for development
21
+
22
+
23
+ def create_parser() -> argparse.ArgumentParser:
24
+ """Create argument parser for statuskit CLI."""
25
+ parser = argparse.ArgumentParser(
26
+ prog="statuskit",
27
+ description="Modular statusline for Claude Code",
28
+ epilog=MODULES_HELP,
29
+ formatter_class=argparse.RawDescriptionHelpFormatter,
30
+ )
31
+ parser.add_argument(
32
+ "-V",
33
+ "--version",
34
+ action="version",
35
+ version=f"statuskit {get_version()}",
36
+ )
37
+
38
+ subparsers = parser.add_subparsers(dest="command")
39
+
40
+ # setup subcommand
41
+ setup_parser = subparsers.add_parser(
42
+ "setup",
43
+ help="Configure Claude Code integration",
44
+ )
45
+ setup_parser.add_argument(
46
+ "-s",
47
+ "--scope",
48
+ choices=["user", "project", "local"],
49
+ default="user",
50
+ help="Installation scope (default: user)",
51
+ )
52
+ setup_parser.add_argument(
53
+ "--check",
54
+ action="store_true",
55
+ help="Check installation status (all scopes)",
56
+ )
57
+ setup_parser.add_argument(
58
+ "--remove",
59
+ action="store_true",
60
+ help="Remove integration (requires --scope)",
61
+ )
62
+ setup_parser.add_argument(
63
+ "--force",
64
+ action="store_true",
65
+ help="Skip confirmations, backup and overwrite",
66
+ )
67
+
68
+ return parser
@@ -0,0 +1 @@
1
+ """Core statuskit infrastructure."""
@@ -0,0 +1,73 @@
1
+ """Configuration loading for statuskit."""
2
+
3
+ import tomllib
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from termcolor import colored
8
+
9
+ # Kept for backward compatibility in tests
10
+ CONFIG_PATH = Path.home() / ".claude" / "statuskit.toml"
11
+ DEFAULT_CACHE_DIR = Path.home() / ".cache" / "statuskit"
12
+
13
+
14
+ def _get_config_paths() -> list[Path]:
15
+ """Get config paths in priority order (highest first)."""
16
+ return [
17
+ Path(".claude") / "statuskit.local.toml", # Local (highest)
18
+ Path(".claude") / "statuskit.toml", # Project
19
+ Path.home() / ".claude" / "statuskit.toml", # User (lowest)
20
+ ]
21
+
22
+
23
+ @dataclass
24
+ class Config:
25
+ """Statuskit configuration."""
26
+
27
+ debug: bool = False
28
+ modules: list[str] = field(default_factory=lambda: ["model", "git", "beads", "quota"])
29
+ module_configs: dict[str, dict] = field(default_factory=dict)
30
+ cache_dir: Path = field(default_factory=lambda: DEFAULT_CACHE_DIR)
31
+
32
+ def get_module_config(self, name: str) -> dict:
33
+ """Get configuration for a specific module."""
34
+ return self.module_configs.get(name, {})
35
+
36
+
37
+ def load_config() -> Config:
38
+ """Load configuration from TOML files.
39
+
40
+ Searches in priority order:
41
+ 1. .claude/statuskit.local.toml (Local)
42
+ 2. .claude/statuskit.toml (Project)
43
+ 3. ~/.claude/statuskit.toml (User)
44
+
45
+ Returns defaults if no config file exists.
46
+ Shows error and returns defaults if file is invalid.
47
+ """
48
+ for config_path in _get_config_paths():
49
+ if config_path.exists():
50
+ try:
51
+ with config_path.open("rb") as f:
52
+ data = tomllib.load(f)
53
+ except (tomllib.TOMLDecodeError, OSError) as e:
54
+ print(colored(f"[!] Config error in {config_path}: {e}", "red"))
55
+ return Config()
56
+
57
+ # Extract module configs
58
+ module_configs = {
59
+ k: v for k, v in data.items() if isinstance(v, dict) and k not in ("debug", "modules", "cache_dir")
60
+ }
61
+
62
+ # Parse cache_dir
63
+ cache_dir_str = data.get("cache_dir")
64
+ cache_dir = Path(cache_dir_str).expanduser() if cache_dir_str else DEFAULT_CACHE_DIR
65
+
66
+ return Config(
67
+ debug=data.get("debug", False),
68
+ modules=data.get("modules", Config().modules),
69
+ module_configs=module_configs,
70
+ cache_dir=cache_dir,
71
+ )
72
+
73
+ return Config()
@@ -0,0 +1,33 @@
1
+ """Module loader for statuskit."""
2
+
3
+ from statuskit.core.config import Config
4
+ from statuskit.core.models import RenderContext
5
+ from statuskit.modules import git, model, usage_limits
6
+ from statuskit.modules.base import BaseModule
7
+
8
+ BUILTIN_MODULES: dict[str, type[BaseModule]] = {
9
+ "model": model.ModelModule,
10
+ "usage_limits": usage_limits.UsageLimitsModule,
11
+ "git": git.GitModule,
12
+ # "beads": ..., # v0.3
13
+ }
14
+
15
+
16
+ def load_modules(config: Config, ctx: RenderContext) -> list[BaseModule]:
17
+ """Load modules based on configuration.
18
+
19
+ Args:
20
+ config: Statuskit configuration
21
+ ctx: Render context for modules
22
+
23
+ Returns:
24
+ List of instantiated modules
25
+ """
26
+ modules = []
27
+ for name in config.modules:
28
+ if name in BUILTIN_MODULES:
29
+ module_config = config.get_module_config(name)
30
+ modules.append(BUILTIN_MODULES[name](ctx, module_config))
31
+ elif ctx.debug:
32
+ print(f"[!] Unknown module: {name}")
33
+ return modules
@@ -0,0 +1,142 @@
1
+ """Data types for statuskit."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass
8
+ class Model:
9
+ """Model information from Claude Code."""
10
+
11
+ id: str | None
12
+ display_name: str
13
+
14
+
15
+ @dataclass
16
+ class Workspace:
17
+ """Workspace paths from Claude Code."""
18
+
19
+ current_dir: str
20
+ project_dir: str
21
+
22
+
23
+ @dataclass
24
+ class Cost:
25
+ """Cost and timing information from Claude Code."""
26
+
27
+ total_cost_usd: float | None
28
+ total_duration_ms: int | None
29
+ total_api_duration_ms: int | None
30
+ total_lines_added: int | None
31
+ total_lines_removed: int | None
32
+
33
+
34
+ @dataclass
35
+ class CurrentUsage:
36
+ """Current context window usage."""
37
+
38
+ input_tokens: int
39
+ output_tokens: int
40
+ cache_creation_input_tokens: int
41
+ cache_read_input_tokens: int
42
+
43
+
44
+ @dataclass
45
+ class ContextWindow:
46
+ """Context window information from Claude Code."""
47
+
48
+ context_window_size: int | None
49
+ total_input_tokens: int | None
50
+ total_output_tokens: int | None
51
+ current_usage: CurrentUsage | None
52
+
53
+
54
+ @dataclass
55
+ class StatusInput:
56
+ """Parsed input from Claude Code status hook."""
57
+
58
+ session_id: str | None
59
+ cwd: str | None
60
+ model: Model | None
61
+ workspace: Workspace | None
62
+ cost: Cost | None
63
+ context_window: ContextWindow | None
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: dict) -> "StatusInput":
67
+ """Parse JSON dict into StatusInput dataclass.
68
+
69
+ Missing fields become None.
70
+ """
71
+ model_data = data.get("model")
72
+ model = (
73
+ Model(
74
+ id=model_data.get("id"),
75
+ display_name=model_data.get("display_name", "Unknown"),
76
+ )
77
+ if model_data
78
+ else None
79
+ )
80
+
81
+ workspace_data = data.get("workspace")
82
+ workspace = (
83
+ Workspace(
84
+ current_dir=workspace_data.get("current_dir", ""),
85
+ project_dir=workspace_data.get("project_dir", ""),
86
+ )
87
+ if workspace_data
88
+ else None
89
+ )
90
+
91
+ cost_data = data.get("cost")
92
+ cost = (
93
+ Cost(
94
+ total_cost_usd=cost_data.get("total_cost_usd"),
95
+ total_duration_ms=cost_data.get("total_duration_ms"),
96
+ total_api_duration_ms=cost_data.get("total_api_duration_ms"),
97
+ total_lines_added=cost_data.get("total_lines_added"),
98
+ total_lines_removed=cost_data.get("total_lines_removed"),
99
+ )
100
+ if cost_data
101
+ else None
102
+ )
103
+
104
+ ctx_data = data.get("context_window")
105
+ context_window = None
106
+ if ctx_data:
107
+ usage_data = ctx_data.get("current_usage")
108
+ current_usage = (
109
+ CurrentUsage(
110
+ input_tokens=usage_data.get("input_tokens", 0),
111
+ output_tokens=usage_data.get("output_tokens", 0),
112
+ cache_creation_input_tokens=usage_data.get("cache_creation_input_tokens", 0),
113
+ cache_read_input_tokens=usage_data.get("cache_read_input_tokens", 0),
114
+ )
115
+ if usage_data
116
+ else None
117
+ )
118
+
119
+ context_window = ContextWindow(
120
+ context_window_size=ctx_data.get("context_window_size"),
121
+ total_input_tokens=ctx_data.get("total_input_tokens"),
122
+ total_output_tokens=ctx_data.get("total_output_tokens"),
123
+ current_usage=current_usage,
124
+ )
125
+
126
+ return cls(
127
+ session_id=data.get("session_id"),
128
+ cwd=data.get("cwd"),
129
+ model=model,
130
+ workspace=workspace,
131
+ cost=cost,
132
+ context_window=context_window,
133
+ )
134
+
135
+
136
+ @dataclass
137
+ class RenderContext:
138
+ """Context passed to modules for rendering."""
139
+
140
+ debug: bool
141
+ data: StatusInput
142
+ cache_dir: Path | None = None
@@ -0,0 +1,6 @@
1
+ """Statuskit modules."""
2
+
3
+ from statuskit.core.models import RenderContext
4
+ from statuskit.modules.base import BaseModule
5
+
6
+ __all__ = ["BaseModule", "RenderContext"]