claude-dev-env 8.12.0 → 8.13.0
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/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""Constants for the Codex weekly-usage probe CLI.
|
|
2
|
-
|
|
3
|
-
Groups: gate threshold, probe report JSON keys, app-server JSON-RPC surface,
|
|
4
|
-
rate-limit snapshot field keys, weekly-window sizing, parse patterns for
|
|
5
|
-
text status output, source labels, and exit codes.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT = 10
|
|
11
|
-
|
|
12
|
-
USAGE_REPORT_KEY_PERCENT_LEFT = "percent_left"
|
|
13
|
-
USAGE_REPORT_KEY_WINDOW_RESET = "window_reset"
|
|
14
|
-
USAGE_REPORT_KEY_SOURCE = "source"
|
|
15
|
-
|
|
16
|
-
CODEX_BINARY_NAME = "codex"
|
|
17
|
-
ALL_APP_SERVER_COMMAND_PARTS = ("app-server", "--listen", "stdio://")
|
|
18
|
-
APP_SERVER_TIMEOUT_SECONDS = 30
|
|
19
|
-
READER_THREAD_JOIN_TIMEOUT_SECONDS = 2
|
|
20
|
-
PROCESS_TREE_WAIT_TIMEOUT_SECONDS = 5
|
|
21
|
-
WINDOWS_OS_NAME = "nt"
|
|
22
|
-
WINDOWS_COMMAND_SHELL = "cmd"
|
|
23
|
-
WINDOWS_COMMAND_SHELL_RUN_FLAG = "/c"
|
|
24
|
-
ALL_WINDOWS_SCRIPT_SUFFIXES = (".cmd", ".bat")
|
|
25
|
-
|
|
26
|
-
JSONRPC_VERSION = "2.0"
|
|
27
|
-
JSONRPC_KEY_ID = "id"
|
|
28
|
-
JSONRPC_KEY_METHOD = "method"
|
|
29
|
-
JSONRPC_KEY_PARAMS = "params"
|
|
30
|
-
JSONRPC_KEY_REPLY_BODY = "result"
|
|
31
|
-
JSONRPC_KEY_ERROR = "error"
|
|
32
|
-
JSONRPC_KEY_VERSION = "jsonrpc"
|
|
33
|
-
|
|
34
|
-
INITIALIZE_REQUEST_ID = 1
|
|
35
|
-
RATE_LIMITS_REQUEST_ID = 2
|
|
36
|
-
METHOD_INITIALIZE = "initialize"
|
|
37
|
-
METHOD_INITIALIZED = "initialized"
|
|
38
|
-
METHOD_RATE_LIMITS_READ = "account/rateLimits/read"
|
|
39
|
-
|
|
40
|
-
CLIENT_INFO_KEY = "clientInfo"
|
|
41
|
-
CLIENT_INFO_NAME_KEY = "name"
|
|
42
|
-
CLIENT_INFO_VERSION_KEY = "version"
|
|
43
|
-
CLIENT_INFO_NAME = "codex-usage-probe"
|
|
44
|
-
CLIENT_INFO_VERSION = "1.0.0"
|
|
45
|
-
CAPABILITIES_KEY = "capabilities"
|
|
46
|
-
EXPERIMENTAL_API_KEY = "experimentalApi"
|
|
47
|
-
|
|
48
|
-
RATE_LIMITS_KEY = "rateLimits"
|
|
49
|
-
PRIMARY_WINDOW_KEY = "primary"
|
|
50
|
-
SECONDARY_WINDOW_KEY = "secondary"
|
|
51
|
-
USED_PERCENT_KEY = "usedPercent"
|
|
52
|
-
RESETS_AT_KEY = "resetsAt"
|
|
53
|
-
WINDOW_DURATION_MINS_KEY = "windowDurationMins"
|
|
54
|
-
|
|
55
|
-
WEEKLY_WINDOW_DURATION_MINUTES = 10080
|
|
56
|
-
PERCENT_FULL = 100
|
|
57
|
-
PERCENT_EMPTY = 0
|
|
58
|
-
|
|
59
|
-
SOURCE_APP_SERVER_RATE_LIMITS = "codex app-server account/rateLimits/read"
|
|
60
|
-
SOURCE_TEXT_STATUS = "text-status"
|
|
61
|
-
SOURCE_NO_USAGE_SURFACE = "no-usage-surface"
|
|
62
|
-
|
|
63
|
-
TEXT_WEEKLY_PERCENT_LEFT_PATTERN = (
|
|
64
|
-
r"(?i)weekly(?:\s+(?:limit|usage|cap))?"
|
|
65
|
-
r"[^\n%]{0,40}?"
|
|
66
|
-
r"(?P<percent>\d{1,3}(?:\.\d+)?)\s*%\s*(?:left|remaining)"
|
|
67
|
-
)
|
|
68
|
-
TEXT_WEEKLY_USED_PERCENT_PATTERN = (
|
|
69
|
-
r"(?i)weekly(?:\s+(?:limit|usage|cap))?"
|
|
70
|
-
r"[^\n%]{0,40}?"
|
|
71
|
-
r"(?P<percent>\d{1,3}(?:\.\d+)?)\s*%\s*(?:used|consumed)"
|
|
72
|
-
)
|
|
73
|
-
TEXT_WINDOW_RESET_PATTERN = (
|
|
74
|
-
r"(?i)(?:weekly\s+)?(?:window\s+)?resets?(?:\s+at)?"
|
|
75
|
-
r"[:\s]+(?P<reset>[^\s)\].,;]+)"
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
EXIT_CODE_SUCCESS = 0
|
|
79
|
-
EXIT_CODE_CRASH = 1
|
|
80
|
-
|
|
81
|
-
UTF8_ENCODING = "utf-8"
|
|
82
|
-
NEWLINE = "\n"
|
|
@@ -1,573 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Report remaining weekly Codex usage as machine-readable JSON.
|
|
3
|
-
|
|
4
|
-
Discovery (codex-cli 0.144.3): no dedicated ``codex usage`` subcommand.
|
|
5
|
-
``codex login status`` prints auth only. Weekly meters come from the
|
|
6
|
-
app-server JSON-RPC method ``account/rateLimits/read`` via
|
|
7
|
-
``codex app-server --listen stdio://``. The weekly window is the rate-limit
|
|
8
|
-
window whose ``windowDurationMins`` is 10080 (seven days), preferring
|
|
9
|
-
``secondary`` when both primary and secondary carry that duration.
|
|
10
|
-
|
|
11
|
-
stdout is one JSON object ``{percent_left, window_reset, source}``.
|
|
12
|
-
``percent_left`` is 0-100, or null when the binary offers no usage data.
|
|
13
|
-
Exit 0 when the probe completed against a reachable CLI (including null
|
|
14
|
-
data). Exit non-zero when the Codex CLI is missing or broken, or when the
|
|
15
|
-
probe itself crashes — consumers must not treat that as a usage-threshold
|
|
16
|
-
skip.
|
|
17
|
-
|
|
18
|
-
Gate rule for consumers: null or unknown ``percent_left`` on exit 0 counts
|
|
19
|
-
as at-or-below ``WEEKLY_USAGE_GATE_THRESHOLD_PERCENT`` — the gate may skip
|
|
20
|
-
and never blocks on missing meters. A non-zero probe exit is CLI failure
|
|
21
|
-
(classify ``codex_down`` or fail closed), not a threshold skip. Require
|
|
22
|
-
review only when exit 0 and ``is_codex_review_required(percent_left)``.
|
|
23
|
-
|
|
24
|
-
::
|
|
25
|
-
|
|
26
|
-
python codex_usage_probe.py
|
|
27
|
-
{"percent_left": 5, "window_reset": "2026-07-19T23:49:08+00:00",
|
|
28
|
-
"source": "codex app-server account/rateLimits/read"}
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
from __future__ import annotations
|
|
32
|
-
|
|
33
|
-
import json
|
|
34
|
-
import math
|
|
35
|
-
import os
|
|
36
|
-
import re
|
|
37
|
-
import shutil
|
|
38
|
-
import subprocess
|
|
39
|
-
import sys
|
|
40
|
-
import threading
|
|
41
|
-
from collections.abc import Mapping, Sequence
|
|
42
|
-
from datetime import datetime, timezone
|
|
43
|
-
from pathlib import Path
|
|
44
|
-
from typing import IO, Protocol
|
|
45
|
-
|
|
46
|
-
_here = Path(__file__).resolve()
|
|
47
|
-
_skills_home_directory = _here.parents[3]
|
|
48
|
-
_package_or_user_home = (
|
|
49
|
-
_skills_home_directory.parent
|
|
50
|
-
if _skills_home_directory.name == ".agents"
|
|
51
|
-
else _skills_home_directory
|
|
52
|
-
)
|
|
53
|
-
_shared_under_package = _package_or_user_home / "_shared" / "process-tree" / "scripts"
|
|
54
|
-
_shared_process_tree_scripts_directory = (
|
|
55
|
-
_shared_under_package
|
|
56
|
-
if _shared_under_package.is_dir()
|
|
57
|
-
else _package_or_user_home / ".claude" / "_shared" / "process-tree" / "scripts"
|
|
58
|
-
)
|
|
59
|
-
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
60
|
-
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
61
|
-
|
|
62
|
-
from process_tree_kill import ( # noqa: E402
|
|
63
|
-
should_start_new_session,
|
|
64
|
-
terminate_process_tree,
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
68
|
-
ALL_APP_SERVER_COMMAND_PARTS,
|
|
69
|
-
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
70
|
-
APP_SERVER_TIMEOUT_SECONDS,
|
|
71
|
-
CAPABILITIES_KEY,
|
|
72
|
-
CLIENT_INFO_KEY,
|
|
73
|
-
CLIENT_INFO_NAME,
|
|
74
|
-
CLIENT_INFO_NAME_KEY,
|
|
75
|
-
CLIENT_INFO_VERSION,
|
|
76
|
-
CLIENT_INFO_VERSION_KEY,
|
|
77
|
-
CODEX_BINARY_NAME,
|
|
78
|
-
EXIT_CODE_CRASH,
|
|
79
|
-
EXIT_CODE_SUCCESS,
|
|
80
|
-
EXPERIMENTAL_API_KEY,
|
|
81
|
-
INITIALIZE_REQUEST_ID,
|
|
82
|
-
JSONRPC_KEY_ERROR,
|
|
83
|
-
JSONRPC_KEY_ID,
|
|
84
|
-
JSONRPC_KEY_METHOD,
|
|
85
|
-
JSONRPC_KEY_PARAMS,
|
|
86
|
-
JSONRPC_KEY_REPLY_BODY,
|
|
87
|
-
JSONRPC_KEY_VERSION,
|
|
88
|
-
JSONRPC_VERSION,
|
|
89
|
-
METHOD_INITIALIZE,
|
|
90
|
-
METHOD_INITIALIZED,
|
|
91
|
-
METHOD_RATE_LIMITS_READ,
|
|
92
|
-
NEWLINE,
|
|
93
|
-
PERCENT_EMPTY,
|
|
94
|
-
PERCENT_FULL,
|
|
95
|
-
PRIMARY_WINDOW_KEY,
|
|
96
|
-
PROCESS_TREE_WAIT_TIMEOUT_SECONDS,
|
|
97
|
-
RATE_LIMITS_KEY,
|
|
98
|
-
RATE_LIMITS_REQUEST_ID,
|
|
99
|
-
READER_THREAD_JOIN_TIMEOUT_SECONDS,
|
|
100
|
-
RESETS_AT_KEY,
|
|
101
|
-
SECONDARY_WINDOW_KEY,
|
|
102
|
-
SOURCE_APP_SERVER_RATE_LIMITS,
|
|
103
|
-
SOURCE_NO_USAGE_SURFACE,
|
|
104
|
-
SOURCE_TEXT_STATUS,
|
|
105
|
-
TEXT_WEEKLY_PERCENT_LEFT_PATTERN,
|
|
106
|
-
TEXT_WEEKLY_USED_PERCENT_PATTERN,
|
|
107
|
-
TEXT_WINDOW_RESET_PATTERN,
|
|
108
|
-
USAGE_REPORT_KEY_PERCENT_LEFT,
|
|
109
|
-
USAGE_REPORT_KEY_SOURCE,
|
|
110
|
-
USAGE_REPORT_KEY_WINDOW_RESET,
|
|
111
|
-
USED_PERCENT_KEY,
|
|
112
|
-
UTF8_ENCODING,
|
|
113
|
-
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
|
|
114
|
-
WEEKLY_WINDOW_DURATION_MINUTES,
|
|
115
|
-
WINDOW_DURATION_MINS_KEY,
|
|
116
|
-
WINDOWS_COMMAND_SHELL,
|
|
117
|
-
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
118
|
-
WINDOWS_OS_NAME,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
from subprocess_window_access import hidden_window_creation_flags
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
class AppServerExchange(Protocol):
|
|
125
|
-
"""Callable that sends JSON-RPC request objects and returns stdout lines."""
|
|
126
|
-
|
|
127
|
-
def __call__(
|
|
128
|
-
self, all_request_messages: list[dict[str, object]]
|
|
129
|
-
) -> list[str]:
|
|
130
|
-
"""Exchange request messages for server stdout lines.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
all_request_messages: JSON-RPC request/notification objects to send.
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
Raw stdout lines from the app-server process.
|
|
137
|
-
"""
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
UsageReport = dict[str, float | str | None]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def is_codex_review_required(percent_left: float | None) -> bool:
|
|
144
|
-
"""Return whether the weekly gate should require a Codex review pass.
|
|
145
|
-
|
|
146
|
-
::
|
|
147
|
-
|
|
148
|
-
None -> False # missing data never blocks
|
|
149
|
-
10.0 -> False # at threshold: skip
|
|
150
|
-
10.1 -> True # above threshold: require
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
percent_left: Weekly percent remaining, or None when unknown.
|
|
154
|
-
|
|
155
|
-
Returns:
|
|
156
|
-
True only when percent_left is known and strictly greater than
|
|
157
|
-
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT.
|
|
158
|
-
"""
|
|
159
|
-
if percent_left is None:
|
|
160
|
-
return False
|
|
161
|
-
return percent_left > WEEKLY_USAGE_GATE_THRESHOLD_PERCENT
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _null_usage_report() -> UsageReport:
|
|
165
|
-
return {
|
|
166
|
-
USAGE_REPORT_KEY_PERCENT_LEFT: None,
|
|
167
|
-
USAGE_REPORT_KEY_WINDOW_RESET: None,
|
|
168
|
-
USAGE_REPORT_KEY_SOURCE: SOURCE_NO_USAGE_SURFACE,
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _clamp_percent(raw_percent: float) -> float:
|
|
173
|
-
return max(float(PERCENT_EMPTY), min(float(PERCENT_FULL), raw_percent))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _format_resets_at(raw_resets_at: object) -> str | None:
|
|
177
|
-
if isinstance(raw_resets_at, bool):
|
|
178
|
-
return None
|
|
179
|
-
if isinstance(raw_resets_at, (int, float)):
|
|
180
|
-
if not math.isfinite(raw_resets_at):
|
|
181
|
-
return None
|
|
182
|
-
try:
|
|
183
|
-
return datetime.fromtimestamp(
|
|
184
|
-
float(raw_resets_at), tz=timezone.utc
|
|
185
|
-
).isoformat()
|
|
186
|
-
except (OverflowError, OSError, ValueError):
|
|
187
|
-
return None
|
|
188
|
-
if isinstance(raw_resets_at, str):
|
|
189
|
-
stripped = raw_resets_at.strip()
|
|
190
|
-
return stripped or None
|
|
191
|
-
return None
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def _window_used_percent(all_window_fields: Mapping[str, object]) -> float | None:
|
|
195
|
-
raw_used_percent = all_window_fields.get(USED_PERCENT_KEY)
|
|
196
|
-
if isinstance(raw_used_percent, bool):
|
|
197
|
-
return None
|
|
198
|
-
if isinstance(raw_used_percent, (int, float)):
|
|
199
|
-
if not math.isfinite(raw_used_percent):
|
|
200
|
-
return None
|
|
201
|
-
return float(raw_used_percent)
|
|
202
|
-
return None
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
def _window_duration_minutes(
|
|
206
|
-
all_window_fields: Mapping[str, object],
|
|
207
|
-
) -> int | None:
|
|
208
|
-
raw_duration = all_window_fields.get(WINDOW_DURATION_MINS_KEY)
|
|
209
|
-
if isinstance(raw_duration, bool):
|
|
210
|
-
return None
|
|
211
|
-
if isinstance(raw_duration, (int, float)):
|
|
212
|
-
if not math.isfinite(raw_duration):
|
|
213
|
-
return None
|
|
214
|
-
return int(raw_duration)
|
|
215
|
-
return None
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def _select_weekly_window(
|
|
219
|
-
all_rate_limit_fields: Mapping[str, object],
|
|
220
|
-
) -> Mapping[str, object] | None:
|
|
221
|
-
primary_payload = all_rate_limit_fields.get(PRIMARY_WINDOW_KEY)
|
|
222
|
-
secondary_payload = all_rate_limit_fields.get(SECONDARY_WINDOW_KEY)
|
|
223
|
-
all_candidate_windows: list[Mapping[str, object]] = []
|
|
224
|
-
if isinstance(secondary_payload, Mapping):
|
|
225
|
-
all_candidate_windows.append(secondary_payload)
|
|
226
|
-
if isinstance(primary_payload, Mapping):
|
|
227
|
-
all_candidate_windows.append(primary_payload)
|
|
228
|
-
if not all_candidate_windows:
|
|
229
|
-
return None
|
|
230
|
-
for each_window in all_candidate_windows:
|
|
231
|
-
if _window_duration_minutes(each_window) == WEEKLY_WINDOW_DURATION_MINUTES:
|
|
232
|
-
return each_window
|
|
233
|
-
return None
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _usage_report_from_window(
|
|
237
|
-
all_weekly_window_fields: Mapping[str, object],
|
|
238
|
-
source: str,
|
|
239
|
-
) -> UsageReport:
|
|
240
|
-
used_percent = _window_used_percent(all_weekly_window_fields)
|
|
241
|
-
if used_percent is None:
|
|
242
|
-
return _null_usage_report()
|
|
243
|
-
percent_left = _clamp_percent(float(PERCENT_FULL) - used_percent)
|
|
244
|
-
return {
|
|
245
|
-
USAGE_REPORT_KEY_PERCENT_LEFT: percent_left,
|
|
246
|
-
USAGE_REPORT_KEY_WINDOW_RESET: _format_resets_at(
|
|
247
|
-
all_weekly_window_fields.get(RESETS_AT_KEY)
|
|
248
|
-
),
|
|
249
|
-
USAGE_REPORT_KEY_SOURCE: source,
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def _parse_rate_limits_snapshot(
|
|
254
|
-
all_rate_limit_fields: Mapping[str, object],
|
|
255
|
-
) -> UsageReport:
|
|
256
|
-
weekly_window = _select_weekly_window(all_rate_limit_fields)
|
|
257
|
-
if weekly_window is None:
|
|
258
|
-
return _null_usage_report()
|
|
259
|
-
return _usage_report_from_window(weekly_window, SOURCE_APP_SERVER_RATE_LIMITS)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
def parse_rate_limits_message(message_text: str) -> UsageReport:
|
|
263
|
-
"""Parse one JSON-RPC reply line that may carry rateLimits.
|
|
264
|
-
|
|
265
|
-
Args:
|
|
266
|
-
message_text: A single stdout line from ``codex app-server``.
|
|
267
|
-
|
|
268
|
-
Returns:
|
|
269
|
-
The usage report, or a null report when the line has no usable meters.
|
|
270
|
-
"""
|
|
271
|
-
try:
|
|
272
|
-
message_payload = json.loads(message_text)
|
|
273
|
-
except json.JSONDecodeError:
|
|
274
|
-
return _null_usage_report()
|
|
275
|
-
if not isinstance(message_payload, dict):
|
|
276
|
-
return _null_usage_report()
|
|
277
|
-
if message_payload.get(JSONRPC_KEY_ERROR) is not None:
|
|
278
|
-
return _null_usage_report()
|
|
279
|
-
reply_body = message_payload.get(JSONRPC_KEY_REPLY_BODY)
|
|
280
|
-
if not isinstance(reply_body, dict):
|
|
281
|
-
return _null_usage_report()
|
|
282
|
-
all_rate_limit_fields = reply_body.get(RATE_LIMITS_KEY)
|
|
283
|
-
if not isinstance(all_rate_limit_fields, dict):
|
|
284
|
-
return _null_usage_report()
|
|
285
|
-
return _parse_rate_limits_snapshot(all_rate_limit_fields)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def _text_status_report(
|
|
289
|
-
percent_left: float,
|
|
290
|
-
status_text: str,
|
|
291
|
-
search_from_index: int,
|
|
292
|
-
) -> UsageReport:
|
|
293
|
-
weekly_scoped_text = status_text[search_from_index:]
|
|
294
|
-
reset_match = re.search(TEXT_WINDOW_RESET_PATTERN, weekly_scoped_text)
|
|
295
|
-
window_reset = (
|
|
296
|
-
reset_match.group("reset").strip() if reset_match is not None else None
|
|
297
|
-
)
|
|
298
|
-
return {
|
|
299
|
-
USAGE_REPORT_KEY_PERCENT_LEFT: percent_left,
|
|
300
|
-
USAGE_REPORT_KEY_WINDOW_RESET: window_reset,
|
|
301
|
-
USAGE_REPORT_KEY_SOURCE: SOURCE_TEXT_STATUS,
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def parse_text_usage_status(status_text: str) -> UsageReport:
|
|
306
|
-
"""Parse human status text for a weekly percent remaining.
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
status_text: Captured CLI or TUI status text.
|
|
310
|
-
|
|
311
|
-
Returns:
|
|
312
|
-
The usage report when a weekly percent pattern matches, else null.
|
|
313
|
-
"""
|
|
314
|
-
left_match = re.search(TEXT_WEEKLY_PERCENT_LEFT_PATTERN, status_text)
|
|
315
|
-
if left_match is not None:
|
|
316
|
-
percent_left = _clamp_percent(float(left_match.group("percent")))
|
|
317
|
-
return _text_status_report(
|
|
318
|
-
percent_left,
|
|
319
|
-
status_text,
|
|
320
|
-
search_from_index=left_match.start(),
|
|
321
|
-
)
|
|
322
|
-
used_match = re.search(TEXT_WEEKLY_USED_PERCENT_PATTERN, status_text)
|
|
323
|
-
if used_match is not None:
|
|
324
|
-
percent_left = _clamp_percent(
|
|
325
|
-
float(PERCENT_FULL) - float(used_match.group("percent"))
|
|
326
|
-
)
|
|
327
|
-
return _text_status_report(
|
|
328
|
-
percent_left,
|
|
329
|
-
status_text,
|
|
330
|
-
search_from_index=used_match.start(),
|
|
331
|
-
)
|
|
332
|
-
return _null_usage_report()
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def _build_app_server_request_messages() -> list[dict[str, object]]:
|
|
336
|
-
initialize_request: dict[str, object] = {
|
|
337
|
-
JSONRPC_KEY_VERSION: JSONRPC_VERSION,
|
|
338
|
-
JSONRPC_KEY_ID: INITIALIZE_REQUEST_ID,
|
|
339
|
-
JSONRPC_KEY_METHOD: METHOD_INITIALIZE,
|
|
340
|
-
JSONRPC_KEY_PARAMS: {
|
|
341
|
-
CLIENT_INFO_KEY: {
|
|
342
|
-
CLIENT_INFO_NAME_KEY: CLIENT_INFO_NAME,
|
|
343
|
-
CLIENT_INFO_VERSION_KEY: CLIENT_INFO_VERSION,
|
|
344
|
-
},
|
|
345
|
-
CAPABILITIES_KEY: {EXPERIMENTAL_API_KEY: True},
|
|
346
|
-
},
|
|
347
|
-
}
|
|
348
|
-
initialized_notification: dict[str, object] = {
|
|
349
|
-
JSONRPC_KEY_VERSION: JSONRPC_VERSION,
|
|
350
|
-
JSONRPC_KEY_METHOD: METHOD_INITIALIZED,
|
|
351
|
-
JSONRPC_KEY_PARAMS: {},
|
|
352
|
-
}
|
|
353
|
-
rate_limits_request: dict[str, object] = {
|
|
354
|
-
JSONRPC_KEY_VERSION: JSONRPC_VERSION,
|
|
355
|
-
JSONRPC_KEY_ID: RATE_LIMITS_REQUEST_ID,
|
|
356
|
-
JSONRPC_KEY_METHOD: METHOD_RATE_LIMITS_READ,
|
|
357
|
-
JSONRPC_KEY_PARAMS: {},
|
|
358
|
-
}
|
|
359
|
-
return [initialize_request, initialized_notification, rate_limits_request]
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def _resolve_codex_command() -> list[str]:
|
|
363
|
-
codex_path = shutil.which(CODEX_BINARY_NAME)
|
|
364
|
-
if codex_path is None:
|
|
365
|
-
raise FileNotFoundError(f"{CODEX_BINARY_NAME} binary not found on PATH")
|
|
366
|
-
lower_path = codex_path.lower()
|
|
367
|
-
if os.name == WINDOWS_OS_NAME and lower_path.endswith(
|
|
368
|
-
ALL_WINDOWS_SCRIPT_SUFFIXES
|
|
369
|
-
):
|
|
370
|
-
return [
|
|
371
|
-
WINDOWS_COMMAND_SHELL,
|
|
372
|
-
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
373
|
-
codex_path,
|
|
374
|
-
*ALL_APP_SERVER_COMMAND_PARTS,
|
|
375
|
-
]
|
|
376
|
-
return [codex_path, *ALL_APP_SERVER_COMMAND_PARTS]
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def _is_rate_limits_reply(message_text: str) -> bool:
|
|
380
|
-
"""Return whether a stdout line is the reply to our rate-limits request."""
|
|
381
|
-
try:
|
|
382
|
-
message_payload = json.loads(message_text)
|
|
383
|
-
except json.JSONDecodeError:
|
|
384
|
-
return False
|
|
385
|
-
if not isinstance(message_payload, dict):
|
|
386
|
-
return False
|
|
387
|
-
return message_payload.get(JSONRPC_KEY_ID) == RATE_LIMITS_REQUEST_ID
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
def _collect_server_lines(
|
|
391
|
-
server_stdout: IO[str],
|
|
392
|
-
all_server_lines: list[str],
|
|
393
|
-
is_exchange_over: threading.Event,
|
|
394
|
-
) -> None:
|
|
395
|
-
"""Append server stdout lines until the rate-limits reply lands or stdout ends."""
|
|
396
|
-
try:
|
|
397
|
-
for each_line in server_stdout:
|
|
398
|
-
stripped_line = each_line.strip()
|
|
399
|
-
if not stripped_line:
|
|
400
|
-
continue
|
|
401
|
-
all_server_lines.append(stripped_line)
|
|
402
|
-
if _is_rate_limits_reply(stripped_line):
|
|
403
|
-
break
|
|
404
|
-
except (OSError, ValueError):
|
|
405
|
-
pass
|
|
406
|
-
is_exchange_over.set()
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
def _terminate_process_tree(server_process: subprocess.Popen[str]) -> None:
|
|
410
|
-
"""Stop the app-server process and any children it launched, then reap it.
|
|
411
|
-
|
|
412
|
-
On Windows the probe may wrap a ``.cmd``/``.bat`` shim as ``cmd /c``, so
|
|
413
|
-
``Popen.pid`` is ``cmd.exe``. Killing only that process leaves the real
|
|
414
|
-
Codex app-server grandchild alive and holding stdout. The shared tree kill
|
|
415
|
-
ends the whole tree on either platform; the timed wait that follows reaps
|
|
416
|
-
the direct child so the enclosing ``with`` block exits without a long wait.
|
|
417
|
-
"""
|
|
418
|
-
terminate_process_tree(server_process)
|
|
419
|
-
try:
|
|
420
|
-
server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
|
|
421
|
-
except subprocess.TimeoutExpired:
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
def _teardown_app_server_exchange(
|
|
426
|
-
server_process: subprocess.Popen[str],
|
|
427
|
-
reader_thread: threading.Thread,
|
|
428
|
-
) -> None:
|
|
429
|
-
"""Terminate the process tree, then join the stdout reader before return."""
|
|
430
|
-
_terminate_process_tree(server_process)
|
|
431
|
-
reader_thread.join(timeout=READER_THREAD_JOIN_TIMEOUT_SECONDS)
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def _exchange_app_server_messages_via_subprocess(
|
|
435
|
-
all_request_messages: list[dict[str, object]],
|
|
436
|
-
) -> list[str]:
|
|
437
|
-
"""Run ``codex app-server`` and collect its replies to the probe's requests.
|
|
438
|
-
|
|
439
|
-
::
|
|
440
|
-
|
|
441
|
-
write requests -> stdin stays OPEN -> server answers -> read reply -> close
|
|
442
|
-
|
|
443
|
-
The server treats end-of-input on stdin as a shutdown signal, so it exits
|
|
444
|
-
without answering when stdin closes right after the requests are written.
|
|
445
|
-
Stdin is held open until the rate-limits reply arrives or the timeout runs
|
|
446
|
-
out, then the process tree is torn down and the stdout reader is joined.
|
|
447
|
-
|
|
448
|
-
Args:
|
|
449
|
-
all_request_messages: JSON-RPC request/notification objects to send.
|
|
450
|
-
|
|
451
|
-
Returns:
|
|
452
|
-
The non-empty stdout lines the server emitted before the exchange ended.
|
|
453
|
-
"""
|
|
454
|
-
all_command_parts = _resolve_codex_command()
|
|
455
|
-
stdin_text = NEWLINE.join(
|
|
456
|
-
json.dumps(each_message) for each_message in all_request_messages
|
|
457
|
-
) + NEWLINE
|
|
458
|
-
all_server_lines: list[str] = []
|
|
459
|
-
is_exchange_over = threading.Event()
|
|
460
|
-
with subprocess.Popen(
|
|
461
|
-
all_command_parts,
|
|
462
|
-
stdin=subprocess.PIPE,
|
|
463
|
-
stdout=subprocess.PIPE,
|
|
464
|
-
stderr=subprocess.DEVNULL,
|
|
465
|
-
text=True,
|
|
466
|
-
encoding=UTF8_ENCODING,
|
|
467
|
-
shell=False,
|
|
468
|
-
start_new_session=should_start_new_session(),
|
|
469
|
-
creationflags=hidden_window_creation_flags(),
|
|
470
|
-
) as server_process:
|
|
471
|
-
server_stdin = server_process.stdin
|
|
472
|
-
server_stdout = server_process.stdout
|
|
473
|
-
if server_stdin is None or server_stdout is None:
|
|
474
|
-
_terminate_process_tree(server_process)
|
|
475
|
-
raise OSError("codex app-server offered no stdio pipes")
|
|
476
|
-
reader_thread = threading.Thread(
|
|
477
|
-
target=_collect_server_lines,
|
|
478
|
-
args=(server_stdout, all_server_lines, is_exchange_over),
|
|
479
|
-
daemon=True,
|
|
480
|
-
)
|
|
481
|
-
reader_thread.start()
|
|
482
|
-
try:
|
|
483
|
-
server_stdin.write(stdin_text)
|
|
484
|
-
server_stdin.flush()
|
|
485
|
-
is_exchange_over.wait(timeout=APP_SERVER_TIMEOUT_SECONDS)
|
|
486
|
-
finally:
|
|
487
|
-
_teardown_app_server_exchange(
|
|
488
|
-
server_process=server_process,
|
|
489
|
-
reader_thread=reader_thread,
|
|
490
|
-
)
|
|
491
|
-
return list(all_server_lines)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
def _usage_report_from_server_lines(all_server_lines: Sequence[str]) -> UsageReport:
|
|
495
|
-
for each_line in all_server_lines:
|
|
496
|
-
usage_report = parse_rate_limits_message(each_line)
|
|
497
|
-
if usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is not None:
|
|
498
|
-
return usage_report
|
|
499
|
-
return _null_usage_report()
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
def probe_weekly_usage(
|
|
503
|
-
exchange_app_server_messages: AppServerExchange,
|
|
504
|
-
) -> UsageReport:
|
|
505
|
-
"""Probe weekly Codex usage through the app-server rate-limits surface.
|
|
506
|
-
|
|
507
|
-
Args:
|
|
508
|
-
exchange_app_server_messages: JSON-RPC exchange seam. Production passes
|
|
509
|
-
the real subprocess exchange; tests inject a fake.
|
|
510
|
-
|
|
511
|
-
Returns:
|
|
512
|
-
The usage report with percent_left, window_reset, and source.
|
|
513
|
-
"""
|
|
514
|
-
all_request_messages = _build_app_server_request_messages()
|
|
515
|
-
all_server_lines = exchange_app_server_messages(all_request_messages)
|
|
516
|
-
return _usage_report_from_server_lines(all_server_lines)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
def probe_weekly_usage_via_subprocess() -> UsageReport:
|
|
520
|
-
"""Probe weekly usage against the real ``codex app-server`` subprocess.
|
|
521
|
-
|
|
522
|
-
::
|
|
523
|
-
|
|
524
|
-
probe_weekly_usage_via_subprocess()
|
|
525
|
-
# ok: {"percent_left": 62.0, "window_reset": ..., "source": ...}
|
|
526
|
-
|
|
527
|
-
The production entry point for every caller that has no exchange seam of
|
|
528
|
-
its own to inject.
|
|
529
|
-
|
|
530
|
-
Returns:
|
|
531
|
-
The usage report with percent_left, window_reset, and source.
|
|
532
|
-
"""
|
|
533
|
-
return probe_weekly_usage(
|
|
534
|
-
exchange_app_server_messages=_exchange_app_server_messages_via_subprocess
|
|
535
|
-
)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def main() -> int:
|
|
539
|
-
"""Run the probe and print one JSON object on stdout.
|
|
540
|
-
|
|
541
|
-
::
|
|
542
|
-
|
|
543
|
-
reachable CLI, meters present -> exit 0, percent_left number
|
|
544
|
-
reachable CLI, no meters -> exit 0, percent_left null (usage skip ok)
|
|
545
|
-
missing or broken CLI -> exit non-zero (not a usage skip)
|
|
546
|
-
|
|
547
|
-
Returns:
|
|
548
|
-
EXIT_CODE_SUCCESS on a completed probe against a reachable CLI
|
|
549
|
-
(including null data), or EXIT_CODE_CRASH when the Codex CLI is
|
|
550
|
-
missing or broken or the probe itself fails.
|
|
551
|
-
"""
|
|
552
|
-
try:
|
|
553
|
-
usage_report = probe_weekly_usage(
|
|
554
|
-
exchange_app_server_messages=_exchange_app_server_messages_via_subprocess
|
|
555
|
-
)
|
|
556
|
-
except (
|
|
557
|
-
FileNotFoundError,
|
|
558
|
-
OSError,
|
|
559
|
-
subprocess.TimeoutExpired,
|
|
560
|
-
subprocess.SubprocessError,
|
|
561
|
-
UnicodeDecodeError,
|
|
562
|
-
):
|
|
563
|
-
print(json.dumps(_null_usage_report()))
|
|
564
|
-
return EXIT_CODE_CRASH
|
|
565
|
-
except (json.JSONDecodeError, ValueError, TypeError, KeyError):
|
|
566
|
-
print(json.dumps(_null_usage_report()))
|
|
567
|
-
return EXIT_CODE_CRASH
|
|
568
|
-
print(json.dumps(usage_report))
|
|
569
|
-
return EXIT_CODE_SUCCESS
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if __name__ == "__main__":
|
|
573
|
-
sys.exit(main())
|