tokensaver-cli 0.3.2__tar.gz → 0.3.3__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.
Files changed (45) hide show
  1. {tokensaver_cli-0.3.2/src/tokensaver_cli.egg-info → tokensaver_cli-0.3.3}/PKG-INFO +1 -1
  2. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/__init__.py +1 -1
  3. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/agents.py +31 -2
  4. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/profiles.py +9 -1
  5. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3/src/tokensaver_cli.egg-info}/PKG-INFO +1 -1
  6. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/tests/test_route_cli.py +34 -0
  7. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/LICENSE +0 -0
  8. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/README.md +0 -0
  9. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/pyproject.toml +0 -0
  10. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/setup.cfg +0 -0
  11. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/api_client.py +0 -0
  12. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/approve_cmd.py +0 -0
  13. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/auth_cmd.py +0 -0
  14. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/banner.py +0 -0
  15. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/.claude-plugin/marketplace.json +0 -0
  16. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/.claude-plugin/plugin.json +0 -0
  17. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/.mcp.json +0 -0
  18. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/commands/approve.md +0 -0
  19. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/commands/model.md +0 -0
  20. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/commands/pipeline.md +0 -0
  21. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/commands/setup.md +0 -0
  22. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/commands/status.md +0 -0
  23. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/commands/unroute.md +0 -0
  24. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/claude_plugin/hooks/hooks.json +0 -0
  25. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/config.py +0 -0
  26. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/credentials.py +0 -0
  27. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/doctor.py +0 -0
  28. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/json_merge.py +0 -0
  29. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/keys_cmd.py +0 -0
  30. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/logo.py +0 -0
  31. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/main.py +0 -0
  32. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/plugin_install.py +0 -0
  33. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/route_frame.py +0 -0
  34. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/state.py +0 -0
  35. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli/status.py +0 -0
  36. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli.egg-info/SOURCES.txt +0 -0
  37. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli.egg-info/dependency_links.txt +0 -0
  38. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli.egg-info/entry_points.txt +0 -0
  39. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli.egg-info/requires.txt +0 -0
  40. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/src/tokensaver_cli.egg-info/top_level.txt +0 -0
  41. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/tests/test_approve_cli.py +0 -0
  42. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/tests/test_auth_cli.py +0 -0
  43. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/tests/test_banner.py +0 -0
  44. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/tests/test_deploy_mode.py +0 -0
  45. {tokensaver_cli-0.3.2 → tokensaver_cli-0.3.3}/tests/test_main_args.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokensaver-cli
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: TokenSaver CLI — route AI agents (Claude Code, Cursor, Codex) through the control plane
5
5
  Author: TokenSaver
6
6
  License-Expression: MIT
@@ -5,4 +5,4 @@ Published on PyPI as ``tokensaver-cli``; installs the ``tokensaver`` console com
5
5
  No server-side pipeline code — configures local agents to call api.tokensaver.fr.
6
6
  """
7
7
 
8
- __version__ = "0.3.2"
8
+ __version__ = "0.3.3"
@@ -255,6 +255,18 @@ def _restore_routed_files(files: list[RoutedFile], *, strip_if_no_backup: bool =
255
255
  path.unlink(missing_ok=True)
256
256
 
257
257
 
258
+ def _sticky_claude_model() -> str | None:
259
+ """Last model chosen via ``--model`` / ``--profile`` (survives bare ``route --launch``)."""
260
+ rec = load_state().get("claude")
261
+ if not rec or not isinstance(rec.meta, dict):
262
+ return None
263
+ for key in ("sticky_model", "model"):
264
+ value = rec.meta.get(key)
265
+ if isinstance(value, str) and value.strip():
266
+ return value.strip()
267
+ return None
268
+
269
+
258
270
  def route_claude(
259
271
  cfg: RouteConfig,
260
272
  *,
@@ -267,12 +279,25 @@ def route_claude(
267
279
  install_plugin: bool = True,
268
280
  ) -> int:
269
281
  plan_slug = resolve_plan_slug()
282
+ explicit_model = bool(model and model.strip())
283
+ explicit_profile = bool(profile and profile.strip())
284
+ sticky = None if (explicit_model or explicit_profile) else _sticky_claude_model()
270
285
  try:
271
- model_id, profile_name = resolve_model(model=model, profile=profile, plan_slug=plan_slug)
286
+ model_id, profile_name = resolve_model(
287
+ model=model,
288
+ profile=profile,
289
+ plan_slug=plan_slug,
290
+ sticky_model=sticky,
291
+ )
272
292
  except ValueError as exc:
273
293
  print(str(exc), file=sys.stderr)
274
294
  return 2
275
- if is_free_plan(plan_slug) and not (model and model.strip()):
295
+ if sticky and model_id == sticky and not explicit_model and not explicit_profile:
296
+ print(
297
+ f"Keeping previous model {model_id} (set with --model / --profile to change)",
298
+ file=sys.stderr,
299
+ )
300
+ elif is_free_plan(plan_slug) and not explicit_model and not sticky:
276
301
  print(
277
302
  f"Free plan → default model {model_id} (override with --model / --profile)",
278
303
  file=sys.stderr,
@@ -357,15 +382,19 @@ def route_claude(
357
382
  file=sys.stderr,
358
383
  )
359
384
 
385
+ prev_sticky = _sticky_claude_model()
386
+ sticky_model = model_id if (explicit_model or explicit_profile) else (prev_sticky or model_id)
360
387
  meta = {
361
388
  "scope": scope,
362
389
  "model": model_id,
390
+ "sticky_model": sticky_model,
363
391
  "profile": profile_name,
364
392
  "with_fs": fs_ok,
365
393
  "console_url": cfg.console_url,
366
394
  "flows_url": cfg.flows_url(),
367
395
  "plugin_dir": str(plugin_dir) if plugin_dir else None,
368
396
  "deploy_mode": cfg.deploy_mode,
397
+ "plan_slug": plan_slug,
369
398
  }
370
399
  record_route("claude", routed, meta=meta)
371
400
 
@@ -64,8 +64,12 @@ def resolve_model(
64
64
  model: str | None,
65
65
  profile: str | None,
66
66
  plan_slug: str | None = None,
67
+ sticky_model: str | None = None,
67
68
  ) -> tuple[str | None, str | None]:
68
- """Return ``(model_id, profile_name)``. Explicit ``--model`` wins over ``--profile``."""
69
+ """Return ``(model_id, profile_name)``.
70
+
71
+ Precedence: ``--model`` > ``--profile`` > sticky (last explicit choice) > plan default.
72
+ """
69
73
  profiles = load_profiles(plan_slug=plan_slug)
70
74
  if model and model.strip():
71
75
  mid = model.strip()
@@ -77,6 +81,10 @@ def resolve_model(
77
81
  known = ", ".join(sorted(profiles))
78
82
  raise ValueError(f"Unknown profile {profile!r}. Known: {known}")
79
83
  return profiles[name], name
84
+ if sticky_model and sticky_model.strip():
85
+ mid = sticky_model.strip()
86
+ matched = next((name for name, value in profiles.items() if value == mid), None)
87
+ return mid, matched
80
88
  return profiles.get("default"), "default"
81
89
 
82
90
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tokensaver-cli
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: TokenSaver CLI — route AI agents (Claude Code, Cursor, Codex) through the control plane
5
5
  Author: TokenSaver
6
6
  License-Expression: MIT
@@ -129,6 +129,40 @@ def test_route_claude_free_plan_default_model(
129
129
  assert settings["env"]["ANTHROPIC_MODEL"] == FREE_PROFILES["default"]
130
130
 
131
131
 
132
+ def test_route_claude_keeps_sticky_model_on_relaunch(
133
+ route_home: Path,
134
+ cfg: RouteConfig,
135
+ tmp_path: Path,
136
+ monkeypatch: pytest.MonkeyPatch,
137
+ ) -> None:
138
+ """Bare ``route --launch`` must not reset an explicit --model to plan default."""
139
+ monkeypatch.delenv("TOKENSAVER_PLAN", raising=False)
140
+ project = tmp_path / "project"
141
+ project.mkdir()
142
+ glm = "openrouter/z-ai/glm-4.7-flash"
143
+ old_cwd = os.getcwd()
144
+ try:
145
+ os.chdir(project)
146
+ assert (
147
+ route_claude(
148
+ cfg,
149
+ launch=False,
150
+ extra_args=[],
151
+ model=glm,
152
+ install_plugin=False,
153
+ )
154
+ == 0
155
+ )
156
+ assert (
157
+ route_claude(cfg, launch=False, extra_args=[], install_plugin=False) == 0
158
+ )
159
+ finally:
160
+ os.chdir(old_cwd)
161
+ settings = json.loads((route_home / ".claude" / "settings.json").read_text())
162
+ assert settings["env"]["ANTHROPIC_MODEL"] == glm
163
+ assert load_state()["claude"].meta.get("sticky_model") == glm
164
+
165
+
132
166
  def test_route_claude_project_scope(route_home: Path, cfg: RouteConfig, tmp_path: Path) -> None:
133
167
  project = tmp_path / "project"
134
168
  project.mkdir()
File without changes
File without changes
File without changes