leitum 0.1.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.
leitum/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """leitum — launch Claude Code against alternative LLM routers."""
2
+
3
+ __version__ = "0.1.0"
leitum/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from leitum.cli import app
2
+
3
+ app()
leitum/cli.py ADDED
@@ -0,0 +1,238 @@
1
+ """Root typer application and subcommand registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ app = typer.Typer(
10
+ name="leitum",
11
+ help="Launch Claude Code against alternative LLM routers.",
12
+ add_completion=False,
13
+ no_args_is_help=True,
14
+ )
15
+
16
+ provider_app = typer.Typer(help="Manage configured providers.", no_args_is_help=True)
17
+ app.add_typer(provider_app, name="provider")
18
+
19
+
20
+ # Shared state passed via typer Context
21
+ class _Opts:
22
+ provider: str | None = None
23
+ use_last_provider: bool = False
24
+ model: str | None = None
25
+ use_last_model: bool = False
26
+ opus: str | None = None
27
+ use_last_opus: bool = False
28
+ sonnet: str | None = None
29
+ use_last_sonnet: bool = False
30
+ haiku: str | None = None
31
+ use_last_haiku: bool = False
32
+ refresh: bool = False
33
+ no_project_config: bool = False
34
+ project_config: Path | None = None
35
+ dry_run: bool = False
36
+ verbose: bool = False
37
+
38
+
39
+ @app.callback(invoke_without_command=True)
40
+ def root(
41
+ ctx: typer.Context,
42
+ provider: str | None = typer.Option(None, "--provider", "-p", help="Set provider."),
43
+ use_last_provider: bool = typer.Option(
44
+ False, "--use-last-provider", "-P", help="Reuse last provider."
45
+ ),
46
+ model: str | None = typer.Option(None, "--model", "-m", help="Set START model."),
47
+ use_last_model: bool = typer.Option(
48
+ False, "--use-last-model", "-M", help="Reuse last START model."
49
+ ),
50
+ opus: str | None = typer.Option(None, "--opus", "-o", help="Set OPUS model."),
51
+ use_last_opus: bool = typer.Option(
52
+ False, "--use-last-opus", "-O", help="Reuse last OPUS model."
53
+ ),
54
+ sonnet: str | None = typer.Option(None, "--sonnet", "-s", help="Set SONNET model."),
55
+ use_last_sonnet: bool = typer.Option(
56
+ False, "--use-last-sonnet", "-S", help="Reuse last SONNET model."
57
+ ),
58
+ haiku: str | None = typer.Option(None, "--haiku", "-k", help="Set HAIKU model."),
59
+ use_last_haiku: bool = typer.Option(
60
+ False, "--use-last-haiku", "-K", help="Reuse last HAIKU model."
61
+ ),
62
+ refresh: bool = typer.Option(
63
+ False, "--refresh", "-r", help="Refresh model cache before selection."
64
+ ),
65
+ no_project_config: bool = typer.Option(
66
+ False, "--no-project-config", help="Ignore leitum.yaml."
67
+ ),
68
+ project_config: Path | None = typer.Option(
69
+ None, "--project-config", help="Use alternative project config."
70
+ ),
71
+ dry_run: bool = typer.Option(
72
+ False, "--dry-run", help="Print resolved env and exec line, do not launch."
73
+ ),
74
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose logging on stderr."),
75
+ version: bool = typer.Option(False, "--version", help="Show version and exit.", is_eager=True),
76
+ ) -> None:
77
+ if version:
78
+ import importlib.metadata
79
+
80
+ ver = importlib.metadata.version("leitum")
81
+ typer.echo(f"leitum {ver}")
82
+ raise typer.Exit()
83
+
84
+ ctx.ensure_object(_Opts)
85
+ obj = ctx.obj
86
+ obj.provider = provider
87
+ obj.use_last_provider = use_last_provider
88
+ obj.model = model
89
+ obj.use_last_model = use_last_model
90
+ obj.opus = opus
91
+ obj.use_last_opus = use_last_opus
92
+ obj.sonnet = sonnet
93
+ obj.use_last_sonnet = use_last_sonnet
94
+ obj.haiku = haiku
95
+ obj.use_last_haiku = use_last_haiku
96
+ obj.refresh = refresh
97
+ obj.no_project_config = no_project_config
98
+ obj.project_config = project_config
99
+ obj.dry_run = dry_run
100
+ obj.verbose = verbose
101
+
102
+ if ctx.invoked_subcommand is None:
103
+ typer.echo(ctx.get_help())
104
+ raise typer.Exit()
105
+
106
+
107
+ @app.command(
108
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
109
+ help=(
110
+ "Launch Claude Code via the configured provider.\n\n"
111
+ "All arguments after 'claude' are passed through to the claude binary unchanged.\n"
112
+ "See 'claude --help' for Claude Code's own options."
113
+ ),
114
+ )
115
+ def claude(
116
+ ctx: typer.Context,
117
+ args: list[str] = typer.Argument(default=None),
118
+ ) -> None:
119
+ ctx.ensure_object(_Opts)
120
+ opts: _Opts = ctx.obj
121
+ pass_through = list(ctx.args) + (args or [])
122
+
123
+ from leitum.commands.claude import run_claude
124
+
125
+ run_claude(
126
+ pass_through=pass_through,
127
+ provider_flag=opts.provider,
128
+ use_last_provider=opts.use_last_provider,
129
+ model_flag=opts.model,
130
+ use_last_model=opts.use_last_model,
131
+ opus_flag=opts.opus,
132
+ use_last_opus=opts.use_last_opus,
133
+ sonnet_flag=opts.sonnet,
134
+ use_last_sonnet=opts.use_last_sonnet,
135
+ haiku_flag=opts.haiku,
136
+ use_last_haiku=opts.use_last_haiku,
137
+ refresh=opts.refresh,
138
+ no_project_config=opts.no_project_config,
139
+ project_config_path=opts.project_config,
140
+ dry_run=opts.dry_run,
141
+ verbose=opts.verbose,
142
+ )
143
+
144
+
145
+ @app.command()
146
+ def init(
147
+ force: bool = typer.Option(False, "--force", help="Overwrite existing files."),
148
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts."),
149
+ ) -> None:
150
+ """Initialize leitum config directory and example providers file."""
151
+ from leitum.commands.init import run_init
152
+
153
+ run_init(force=force, yes=yes)
154
+
155
+
156
+ @app.command()
157
+ def refresh(
158
+ ctx: typer.Context,
159
+ provider_name: str | None = typer.Option(
160
+ None, "--provider", "-p", help="Refresh specific provider only."
161
+ ),
162
+ ) -> None:
163
+ """Delete model cache and re-fetch from providers."""
164
+ from leitum.commands.refresh import run_refresh
165
+
166
+ run_refresh(provider_name)
167
+
168
+
169
+ @app.command()
170
+ def doctor(
171
+ project_config: Path | None = typer.Option(
172
+ None, "--project-config", help="Path to project config."
173
+ ),
174
+ ) -> None:
175
+ """Run sanity checks on config, permissions, and environment."""
176
+ from leitum.commands.doctor import run_doctor
177
+
178
+ run_doctor(project_config)
179
+
180
+
181
+ @app.command()
182
+ def completions(
183
+ shell: str = typer.Argument(..., help="Shell: bash, zsh, or fish."),
184
+ ) -> None:
185
+ """Print shell completion script."""
186
+ import subprocess
187
+
188
+ if shell not in ("bash", "zsh", "fish"):
189
+ typer.echo(f"Unsupported shell '{shell}'. Choose from: bash, zsh, fish.", err=True)
190
+ raise typer.Exit(2)
191
+ result = subprocess.run(
192
+ ["leitum", "--show-completion", shell],
193
+ capture_output=True,
194
+ text=True,
195
+ )
196
+ if result.returncode == 0:
197
+ typer.echo(result.stdout, nl=False)
198
+ else:
199
+ typer.echo(result.stderr, err=True)
200
+ raise typer.Exit(result.returncode)
201
+
202
+
203
+ @provider_app.command("list")
204
+ def provider_list() -> None:
205
+ """List all configured providers."""
206
+ from leitum.commands.provider import run_provider_list
207
+
208
+ run_provider_list()
209
+
210
+
211
+ @provider_app.command("show")
212
+ def provider_show(
213
+ name: str = typer.Argument(..., help="Provider name."),
214
+ reveal_token: bool = typer.Option(False, "--reveal-token", help="Show plaintext token."),
215
+ ) -> None:
216
+ """Show configuration for a provider (token redacted by default)."""
217
+ from leitum.commands.provider import run_provider_show
218
+
219
+ run_provider_show(name, reveal_token=reveal_token)
220
+
221
+
222
+ @provider_app.command("add")
223
+ def provider_add() -> None:
224
+ """Interactively add a new provider."""
225
+ from leitum.commands.provider import run_provider_add
226
+
227
+ run_provider_add()
228
+
229
+
230
+ @provider_app.command("remove")
231
+ def provider_remove(
232
+ name: str = typer.Argument(..., help="Provider name to remove."),
233
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation."),
234
+ ) -> None:
235
+ """Remove a provider (with confirmation)."""
236
+ from leitum.commands.provider import run_provider_remove
237
+
238
+ run_provider_remove(name, yes=yes)
File without changes
@@ -0,0 +1,182 @@
1
+ """leitum claude subcommand — launch Claude Code via a configured provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from leitum.config.io import load_project_config, load_providers_config
9
+ from leitum.config.models import ModelSlot
10
+ from leitum.config.paths import providers_config_path
11
+ from leitum.launch import exec_claude
12
+ from leitum.providers.cache import clear_cache
13
+ from leitum.providers.discovery import discover_models
14
+ from leitum.selection.resolver import resolve_models, resolve_provider
15
+ from leitum.state import load_state, save_state
16
+
17
+
18
+ def run_claude(
19
+ pass_through: list[str],
20
+ *,
21
+ provider_flag: str | None,
22
+ use_last_provider: bool,
23
+ model_flag: str | None,
24
+ use_last_model: bool,
25
+ opus_flag: str | None,
26
+ use_last_opus: bool,
27
+ sonnet_flag: str | None,
28
+ use_last_sonnet: bool,
29
+ haiku_flag: str | None,
30
+ use_last_haiku: bool,
31
+ refresh: bool,
32
+ no_project_config: bool,
33
+ project_config_path: Path | None,
34
+ dry_run: bool,
35
+ verbose: bool,
36
+ ) -> None:
37
+ # Conflict checks
38
+ _check_conflict("--model/-m", "--use-last-model/-M", model_flag, use_last_model)
39
+ _check_conflict("--opus/-o", "--use-last-opus/-O", opus_flag, use_last_opus)
40
+ _check_conflict("--sonnet/-s", "--use-last-sonnet/-S", sonnet_flag, use_last_sonnet)
41
+ _check_conflict("--haiku/-k", "--use-last-haiku/-K", haiku_flag, use_last_haiku)
42
+ if no_project_config and project_config_path is not None:
43
+ print(
44
+ "Error: --no-project-config and --project-config are mutually exclusive.",
45
+ file=sys.stderr,
46
+ )
47
+ raise SystemExit(2)
48
+
49
+ # Load global config
50
+ cfg_path = providers_config_path()
51
+ if not cfg_path.exists():
52
+ print(
53
+ f"Error: providers config not found at {cfg_path}. Run 'leitum init' first.",
54
+ file=sys.stderr,
55
+ )
56
+ raise SystemExit(3)
57
+ try:
58
+ config = load_providers_config(cfg_path)
59
+ except Exception as exc:
60
+ print(f"Error: invalid providers config: {exc}", file=sys.stderr)
61
+ raise SystemExit(3) from exc
62
+
63
+ state = load_state()
64
+
65
+ # Load project config
66
+ project_cfg = None
67
+ if not no_project_config:
68
+ pc_path = project_config_path or Path("leitum.yaml")
69
+ if pc_path.exists():
70
+ try:
71
+ raw_pc = load_project_config(pc_path)
72
+ # Validate provider reference
73
+ if raw_pc.provider is not None:
74
+ if config.get_provider(raw_pc.provider) is None:
75
+ known = ", ".join(p.name for p in config.providers)
76
+ print(
77
+ f"Error: leitum.yaml references unknown provider '{raw_pc.provider}'. "
78
+ f"Known providers: {known}",
79
+ file=sys.stderr,
80
+ )
81
+ raise SystemExit(3)
82
+ project_cfg = raw_pc
83
+ except SystemExit:
84
+ raise
85
+ except Exception as exc:
86
+ print(f"Error: invalid leitum.yaml: {exc}", file=sys.stderr)
87
+ raise SystemExit(3) from exc
88
+
89
+ # Resolve provider
90
+ provider = resolve_provider(
91
+ flag=provider_flag,
92
+ use_last=use_last_provider,
93
+ project_provider=project_cfg.provider if project_cfg else None,
94
+ state=state,
95
+ config=config,
96
+ verbose=verbose,
97
+ )
98
+
99
+ # Refresh cache if requested
100
+ if refresh:
101
+ if provider.models:
102
+ print(
103
+ f"Warning: --refresh has no effect for provider '{provider.name}' "
104
+ "(models are pinned in YAML).",
105
+ file=sys.stderr,
106
+ )
107
+ else:
108
+ if verbose:
109
+ print(f"Refreshing model list for {provider.name}...", file=sys.stderr)
110
+ clear_cache(provider.name)
111
+
112
+ # Discover models
113
+ try:
114
+ model_infos = discover_models(provider, force=refresh and not bool(provider.models))
115
+ except RuntimeError as exc:
116
+ print(f"Error: {exc}", file=sys.stderr)
117
+ raise SystemExit(4) from exc
118
+
119
+ # Resolve models
120
+ flags: dict[ModelSlot, str | None] = {
121
+ "start": model_flag,
122
+ "opus": opus_flag,
123
+ "sonnet": sonnet_flag,
124
+ "haiku": haiku_flag,
125
+ }
126
+ use_last: dict[ModelSlot, bool] = {
127
+ "start": use_last_model,
128
+ "opus": use_last_opus,
129
+ "sonnet": use_last_sonnet,
130
+ "haiku": use_last_haiku,
131
+ }
132
+
133
+ # Warn if model not in list
134
+ for slot, flag_val in flags.items():
135
+ if flag_val and model_infos:
136
+ known_ids = {m.id for m in model_infos}
137
+ if flag_val not in known_ids:
138
+ print(
139
+ f"Warning: model '{flag_val}' for slot '{slot}' not in provider's model list.",
140
+ file=sys.stderr,
141
+ )
142
+
143
+ resolved = resolve_models(
144
+ flags=flags,
145
+ use_last=use_last,
146
+ project_models=project_cfg.models if project_cfg else None,
147
+ state=state,
148
+ provider=provider,
149
+ model_infos=model_infos,
150
+ verbose=verbose,
151
+ )
152
+
153
+ # Persist state
154
+ state.last_provider = provider.name
155
+ from leitum.selection.resolver import _SLOTS
156
+
157
+ for slot in _SLOTS:
158
+ val = resolved.get(slot)
159
+ if val:
160
+ state.set_model(provider.name, slot, val)
161
+ state.touch_provider(provider.name)
162
+ try:
163
+ save_state(state)
164
+ except Exception as exc:
165
+ print(f"Warning: could not save state: {exc}", file=sys.stderr)
166
+
167
+ project_extra = project_cfg.extra_env if project_cfg else {}
168
+
169
+ exec_claude(
170
+ provider=provider,
171
+ models=resolved,
172
+ pass_through=pass_through,
173
+ project_extra_env=project_extra,
174
+ dry_run=dry_run,
175
+ verbose=verbose,
176
+ )
177
+
178
+
179
+ def _check_conflict(flag_a: str, flag_b: str, val_a: str | None, use_last_b: bool) -> None:
180
+ if val_a is not None and use_last_b:
181
+ print(f"Error: {flag_a} and {flag_b} are mutually exclusive.", file=sys.stderr)
182
+ raise SystemExit(2)
@@ -0,0 +1,185 @@
1
+ """leitum doctor command — sanity check suite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ import stat
8
+ import subprocess
9
+ from pathlib import Path
10
+
11
+ from leitum.config.paths import get_config_dir, model_cache_path, providers_config_path, state_path
12
+
13
+ _SECRET_RE = re.compile(r"^[A-Za-z0-9+/=_\-]{24,}$")
14
+
15
+
16
+ def _ok(msg: str) -> None:
17
+ print(f"[ ok ] {msg}")
18
+
19
+
20
+ def _warn(msg: str) -> None:
21
+ print(f"[warn] {msg}")
22
+
23
+
24
+ def _fail(msg: str) -> None:
25
+ print(f"[fail] {msg}")
26
+
27
+
28
+ def run_doctor(project_config_path: Path | None = None) -> None:
29
+ failures = 0
30
+ warnings = 0
31
+
32
+ # 1. Paths & permissions
33
+ print("--- Paths & Permissions ---")
34
+ config_dir = get_config_dir()
35
+ if config_dir.exists():
36
+ mode = config_dir.stat().st_mode
37
+ if stat.S_IMODE(mode) == 0o700:
38
+ _ok(f"Config dir {config_dir} has mode 0700")
39
+ else:
40
+ _warn(f"Config dir {config_dir} has mode {oct(stat.S_IMODE(mode))} (expected 0700)")
41
+ warnings += 1
42
+ else:
43
+ _fail(f"Config dir {config_dir} does not exist. Run 'leitum init'.")
44
+ failures += 1
45
+
46
+ cfg_path = providers_config_path()
47
+ if cfg_path.exists():
48
+ mode = cfg_path.stat().st_mode
49
+ if stat.S_IMODE(mode) <= 0o600:
50
+ _ok(f"api-providers.yaml has mode {oct(stat.S_IMODE(mode))}")
51
+ else:
52
+ _warn(
53
+ f"api-providers.yaml has mode {oct(stat.S_IMODE(mode))}"
54
+ f" (recommend chmod 600 {cfg_path})"
55
+ )
56
+ warnings += 1
57
+ else:
58
+ _fail(f"api-providers.yaml not found at {cfg_path}. Run 'leitum init'.")
59
+ failures += 1
60
+
61
+ # 2. Config validation
62
+ print("\n--- Config Validation ---")
63
+ config = None
64
+ if cfg_path.exists():
65
+ try:
66
+ from leitum.config.io import load_providers_config
67
+
68
+ config = load_providers_config(cfg_path)
69
+ _ok(f"api-providers.yaml is valid ({len(config.providers)} provider(s))")
70
+ except Exception as exc:
71
+ _fail(f"api-providers.yaml parse error: {exc}")
72
+ failures += 1
73
+
74
+ pc_path = project_config_path or Path("leitum.yaml")
75
+ if pc_path.exists():
76
+ try:
77
+ from leitum.config.io import load_project_config
78
+
79
+ pc = load_project_config(pc_path)
80
+ _ok("leitum.yaml is valid")
81
+ if pc.provider and config:
82
+ if config.get_provider(pc.provider) is None:
83
+ _fail(f"leitum.yaml references unknown provider '{pc.provider}'")
84
+ failures += 1
85
+ else:
86
+ _ok(f"leitum.yaml provider '{pc.provider}' found in config")
87
+ # Heuristic secret check in extra_env
88
+ for k, v in (pc.extra_env or {}).items():
89
+ if _SECRET_RE.match(v) and "${" not in v:
90
+ _warn(
91
+ f"leitum.yaml extra_env['{k}'] looks like a secret"
92
+ f" (length {len(v)}, no interpolation)."
93
+ " Do not commit secrets to version control."
94
+ )
95
+ warnings += 1
96
+ except Exception as exc:
97
+ _fail(f"leitum.yaml parse error: {exc}")
98
+ failures += 1
99
+
100
+ # 3. State file
101
+ print("\n--- State File ---")
102
+ st_path = state_path()
103
+ if st_path.exists():
104
+ try:
105
+ from leitum.state import load_state
106
+
107
+ state = load_state()
108
+ _ok("state.yaml is valid")
109
+ if state.last_provider:
110
+ if config and config.get_provider(state.last_provider) is None:
111
+ _warn(f"last_provider '{state.last_provider}' not in current config")
112
+ warnings += 1
113
+ else:
114
+ _ok(f"last_provider '{state.last_provider}' exists")
115
+ except Exception as exc:
116
+ _warn(f"state.yaml could not be read: {exc}")
117
+ warnings += 1
118
+ else:
119
+ _warn("state.yaml not found (will be created on first run)")
120
+ warnings += 1
121
+
122
+ # 4. ENV variables
123
+ print("\n--- Environment Variables ---")
124
+ if config:
125
+ import os
126
+
127
+ from leitum.config.env import _INTERPOLATION_RE
128
+
129
+ for p in config.providers:
130
+ for match in _INTERPOLATION_RE.finditer(p.auth.token):
131
+ expr = match.group(1)
132
+ var_name = expr.split(":-")[0].strip()
133
+ if var_name in os.environ:
134
+ _ok(f"Provider '{p.name}': {var_name} is set")
135
+ else:
136
+ _fail(f"Provider '{p.name}': {var_name} is NOT set")
137
+ failures += 1
138
+
139
+ # 5. Model discovery
140
+ print("\n--- Model Discovery ---")
141
+ if config:
142
+ import httpx
143
+
144
+ for p in config.providers:
145
+ if p.models:
146
+ _ok(f"Provider '{p.name}': {len(p.models)} models from YAML")
147
+ else:
148
+ cache_p = model_cache_path(p.name)
149
+ if cache_p.exists():
150
+ _ok(f"Provider '{p.name}': cache exists at {cache_p}")
151
+ else:
152
+ # Lightweight reachability check
153
+ try:
154
+ url = p.base_url.rstrip("/")
155
+ with httpx.Client(timeout=5) as client:
156
+ client.head(url)
157
+ _ok(f"Provider '{p.name}': reachable (no cache yet; run 'leitum refresh')")
158
+ except Exception as exc:
159
+ _warn(f"Provider '{p.name}': no cache and unreachable ({exc})")
160
+ warnings += 1
161
+
162
+ # 6. Claude binary
163
+ print("\n--- Claude Binary ---")
164
+ claude_path = shutil.which("claude")
165
+ if claude_path is None:
166
+ _fail(
167
+ "'claude' not found in PATH. Install from https://docs.claude.com/en/docs/claude-code/quickstart"
168
+ )
169
+ failures += 1
170
+ else:
171
+ _ok(f"'claude' found at {claude_path}")
172
+ try:
173
+ result = subprocess.run(
174
+ ["claude", "--version"], capture_output=True, text=True, timeout=5
175
+ )
176
+ version = (result.stdout or result.stderr).strip()
177
+ _ok(f"claude version: {version}")
178
+ except Exception as exc:
179
+ _warn(f"Could not determine claude version: {exc}")
180
+ warnings += 1
181
+
182
+ # Summary
183
+ print(f"\n--- Summary: {failures} failure(s), {warnings} warning(s) ---")
184
+ if failures > 0:
185
+ raise SystemExit(1)
@@ -0,0 +1,30 @@
1
+ """leitum init command."""
2
+
3
+ from leitum.config.io import write_empty_state, write_example_providers_config
4
+ from leitum.config.paths import ensure_dirs, providers_config_path, state_path
5
+
6
+
7
+ def run_init(force: bool = False, yes: bool = False) -> None:
8
+ ensure_dirs()
9
+
10
+ cfg_path = providers_config_path()
11
+ if cfg_path.exists() and not force:
12
+ print(f"Config already exists at {cfg_path}. Use --force to overwrite.")
13
+ elif cfg_path.exists() and force:
14
+ if not yes:
15
+ answer = input(f"Overwrite {cfg_path}? [y/N] ").strip().lower()
16
+ if answer != "y":
17
+ print("Aborted.")
18
+ raise SystemExit(0)
19
+ write_example_providers_config(cfg_path)
20
+ print(f"Created {cfg_path}")
21
+ else:
22
+ write_example_providers_config(cfg_path)
23
+ print(f"Created {cfg_path}")
24
+
25
+ st_path = state_path()
26
+ if not st_path.exists():
27
+ write_empty_state(st_path)
28
+ print(f"Created {st_path}")
29
+
30
+ print("\nSet REQUESTY_API_KEY in your shell and run `leitum claude` to start.")