claude-dev-env 8.48.4 → 8.48.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.48.4",
3
+ "version": "8.48.5",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -28,11 +28,31 @@ class WeeklyUtilizationProbeError(Exception):
28
28
  """Raised when an account's usage meters cannot be measured."""
29
29
 
30
30
 
31
- def _usage_pause_scripts_directory() -> Path:
32
- package_root = Path(__file__).resolve().parent.parent
33
- return (
31
+ def _usage_pause_scripts_directory(module_file: Path) -> Path:
32
+ """Return the usage-pause scripts directory beside this scripts tree.
33
+
34
+ ::
35
+
36
+ packages/claude-dev-env/scripts/x.py -> packages/claude-dev-env/.agents/skills/usage-pause/scripts
37
+ ~/.claude/scripts/x.py (link) -> ~/.agents/skills/usage-pause/scripts
38
+
39
+ The installer links ~/.claude/scripts to ~/.agents/scripts, so the
40
+ resolved scripts tree already sits inside the agents home.
41
+
42
+ Args:
43
+ module_file: Path of a module inside the scripts tree.
44
+
45
+ Returns:
46
+ The directory that holds the usage-pause resolver.
47
+ """
48
+ package_root = module_file.resolve().parent.parent
49
+ agents_home = (
34
50
  package_root
35
- / USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME
51
+ if package_root.name.endswith(USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME)
52
+ else package_root / USAGE_PAUSE_AGENTS_HOME_DIRECTORY_NAME
53
+ )
54
+ return (
55
+ agents_home
36
56
  / USAGE_PAUSE_SKILL_DIRECTORY_NAME
37
57
  / USAGE_PAUSE_SKILL_NAME
38
58
  / USAGE_PAUSE_SCRIPTS_DIRECTORY_NAME
@@ -48,7 +68,7 @@ def _load_resolve_usage_window_module() -> ModuleType:
48
68
  already_loaded = sys.modules.get(RESOLVE_USAGE_WINDOW_MODULE_NAME)
49
69
  if already_loaded is not None:
50
70
  return already_loaded
51
- scripts_directory = _usage_pause_scripts_directory()
71
+ scripts_directory = _usage_pause_scripts_directory(Path(__file__))
52
72
  module_path = scripts_directory / RESOLVE_USAGE_WINDOW_FILENAME
53
73
  if not module_path.is_file():
54
74
  raise WeeklyUtilizationProbeError(
@@ -90,12 +110,43 @@ class AccountUsageMeters:
90
110
  weekly_resets_at: datetime | None
91
111
 
92
112
 
113
+ def _read_access_token(
114
+ usage_window_resolver: ModuleType, credentials_path: Path, now: datetime
115
+ ) -> str | None:
116
+ """Return the bearer token that reads one account's meters.
117
+
118
+ ::
119
+
120
+ credential file holds a token -> that token
121
+ no token, the session's own credential path -> the session ingress token
122
+ no token, another account's credential path -> None
123
+
124
+ A cloud session has no credential file, only the ingress token, and that
125
+ token belongs to the account the session runs on.
126
+
127
+ Args:
128
+ usage_window_resolver: The loaded usage-pause resolver module.
129
+ credentials_path: The account's CLI credential file.
130
+ now: The current time, for the token expiry check.
131
+
132
+ Returns:
133
+ A bearer token, or None when the account has none.
134
+ """
135
+ file_token = usage_window_resolver.read_oauth_access_token(credentials_path, now)
136
+ if file_token is not None:
137
+ return file_token
138
+ session_credentials_path = usage_window_resolver.default_credentials_path()
139
+ if credentials_path.resolve() != session_credentials_path.resolve():
140
+ return None
141
+ return usage_window_resolver.read_session_ingress_token()
142
+
143
+
93
144
  def _fetch_account_usage_payload(
94
145
  usage_window_resolver: ModuleType, credentials_path: Path
95
146
  ) -> dict[str, object]:
96
147
  now = datetime.now().astimezone()
97
148
  try:
98
- access_token = usage_window_resolver.read_oauth_access_token(credentials_path, now)
149
+ access_token = _read_access_token(usage_window_resolver, credentials_path, now)
99
150
  if access_token is None:
100
151
  raise WeeklyUtilizationProbeError(
101
152
  NO_ACCESS_TOKEN_ERROR_TEMPLATE.format(credentials_path=credentials_path)
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import importlib
6
+ import os
5
7
  import subprocess
6
8
  import sys
7
9
  import urllib.error
@@ -44,18 +46,95 @@ def test_probe_reads_both_meters_and_resets(monkeypatch: pytest.MonkeyPatch) ->
44
46
  )
45
47
 
46
48
 
47
- def test_probe_rejects_missing_access_token(monkeypatch: pytest.MonkeyPatch) -> None:
49
+ def test_probe_reads_the_session_token_for_the_session_account(
50
+ monkeypatch: pytest.MonkeyPatch,
51
+ ) -> None:
52
+ session_credentials = Path("home") / ".claude" / ".credentials.json"
53
+ all_fetched_tokens: list[str] = []
54
+
55
+ class Windows:
56
+ session_utilization = 15.0
57
+ session_resets_at = None
58
+ weekly_utilization = 70.0
59
+ weekly_resets_at = None
60
+
48
61
  class Resolver:
49
62
  def read_oauth_access_token(self, credentials_path: Path, now: datetime) -> None:
50
63
  return None
51
64
 
65
+ def default_credentials_path(self) -> Path:
66
+ return session_credentials
67
+
68
+ def read_session_ingress_token(self) -> str:
69
+ return "session-token"
70
+
71
+ def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
72
+ all_fetched_tokens.append(access_token)
73
+ return {"usage": 70.0}
74
+
75
+ def extract_usage_windows(self, payload: dict[str, object]) -> Windows:
76
+ return Windows()
77
+
78
+ monkeypatch.setattr(usage, "_load_resolve_usage_window_module", lambda: Resolver())
79
+
80
+ assert usage.probe_account_meters(session_credentials) == usage.AccountUsageMeters(
81
+ 15.0, None, 70.0, None
82
+ )
83
+ assert all_fetched_tokens == ["session-token"]
84
+
85
+
86
+ def test_probe_rejects_missing_access_token_for_another_account(
87
+ monkeypatch: pytest.MonkeyPatch,
88
+ ) -> None:
89
+ class Resolver:
90
+ def read_oauth_access_token(self, credentials_path: Path, now: datetime) -> None:
91
+ return None
92
+
93
+ def default_credentials_path(self) -> Path:
94
+ return Path("home") / ".claude" / ".credentials.json"
95
+
96
+ def read_session_ingress_token(self) -> str:
97
+ return "session-token"
98
+
52
99
  def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
53
100
  pytest.fail("meter request must not run")
54
101
 
55
102
  monkeypatch.setattr(usage, "_load_resolve_usage_window_module", lambda: Resolver())
56
103
 
57
104
  with pytest.raises(usage.WeeklyUtilizationProbeError, match="bearer token"):
58
- usage.probe_account_meters(Path("account.json"))
105
+ usage.probe_account_meters(Path("home") / ".claude-profiles" / "ev" / ".credentials.json")
106
+
107
+
108
+ def _create_directory_link(*, from_link: Path, to_target: Path) -> None:
109
+ if sys.platform.startswith("win32"):
110
+ importlib.import_module("_winapi").CreateJunction(str(to_target), str(from_link))
111
+ return
112
+ os.symlink(to_target, from_link, target_is_directory=True)
113
+
114
+
115
+ def test_scripts_directory_follows_the_installed_scripts_link(tmp_path: Path) -> None:
116
+ home = tmp_path.resolve()
117
+ (home / ".agents" / "scripts").mkdir(parents=True)
118
+ (home / ".claude").mkdir()
119
+ _create_directory_link(
120
+ from_link=home / ".claude" / "scripts", to_target=home / ".agents" / "scripts"
121
+ )
122
+
123
+ scripts_directory = usage._usage_pause_scripts_directory(
124
+ home / ".claude" / "scripts" / "claude_chain_usage.py"
125
+ )
126
+
127
+ assert scripts_directory == home / ".agents" / "skills" / "usage-pause" / "scripts"
128
+
129
+
130
+ def test_scripts_directory_in_the_package_tree(tmp_path: Path) -> None:
131
+ package_root = tmp_path.resolve() / "claude-dev-env"
132
+
133
+ scripts_directory = usage._usage_pause_scripts_directory(
134
+ package_root / "scripts" / "claude_chain_usage.py"
135
+ )
136
+
137
+ assert scripts_directory == package_root / ".agents" / "skills" / "usage-pause" / "scripts"
59
138
 
60
139
 
61
140
  def test_probe_wraps_network_failure(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -79,7 +158,7 @@ def test_failed_module_load_does_not_poison_cache(
79
158
  (tmp_path / "resolve_usage_window.py").write_text(
80
159
  "raise ImportError('intentional load failure')\n", encoding="utf-8"
81
160
  )
82
- monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda: tmp_path)
161
+ monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda _module_file: tmp_path)
83
162
 
84
163
  with pytest.raises(ImportError, match="intentional load failure"):
85
164
  usage._load_resolve_usage_window_module()