claude-dev-env 8.43.5 → 8.44.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/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/cli-chain.md +9 -12
- package/_shared/advisor/reference/lifecycle.md +1 -1
- package/_shared/advisor/reference/third-party-bind.md +3 -3
- package/bin/install.test.mjs +1 -1
- package/docs/second-claude-account.md +11 -13
- package/hooks/blocking/reply_length_gate.py +43 -11
- package/hooks/blocking/test_reply_length_gate.py +55 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/reply_length_gate_constants.py +11 -0
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +29 -39
- package/scripts/claude_account_worker.py +46 -297
- package/scripts/claude_account_worker_process.py +18 -154
- package/scripts/claude_account_worker_report.py +8 -4
- package/scripts/claude_chain_usage.py +7 -271
- package/scripts/codec_forwarding_test_support.py +11 -65
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
- package/scripts/invoke_code_review.py +34 -56
- package/scripts/resolve_worker_spawn.py +40 -72
- package/scripts/test_account_broker_guard.py +0 -4
- package/scripts/test_claude_account_worker.py +105 -350
- package/scripts/test_claude_chain_usage.py +50 -510
- package/scripts/test_dispatcher_profile_import.py +5 -9
- package/scripts/test_invoke_code_review.py +7 -6
- package/scripts/test_invoke_code_review_chain.py +26 -0
- package/scripts/test_invoke_code_review_cli.py +8 -8
- package/scripts/test_invoke_code_review_codec.py +3 -11
- package/scripts/test_invoke_code_review_contract.py +5 -1
- package/scripts/test_resolve_worker_spawn.py +118 -265
- package/scripts/test_resolve_worker_spawn_codec.py +9 -21
- package/scripts/tests/test_code_review_constants.py +2 -2
- package/scripts/claude_account_choice.py +0 -422
- package/scripts/claude_chain_runner.py +0 -1289
- package/scripts/test_claude_account_choice.py +0 -355
- package/scripts/test_claude_chain_runner.py +0 -1839
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Choose a Claude account for a runner job while protecting the main account.
|
|
3
|
-
|
|
4
|
-
The main account takes a job only to spend usage that expires soon. Otherwise,
|
|
5
|
-
the picker tries extra profiles in order and waits when every one is full.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import argparse
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
from collections.abc import Sequence
|
|
14
|
-
from dataclasses import dataclass
|
|
15
|
-
from datetime import datetime
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
from claude_account_profile import default_profile_home
|
|
19
|
-
from claude_chain_usage import (
|
|
20
|
-
AccountUsageMeters,
|
|
21
|
-
WeeklyUtilizationProbeError,
|
|
22
|
-
probe_account_meters,
|
|
23
|
-
)
|
|
24
|
-
from dev_env_scripts_constants.claude_account_constants import (
|
|
25
|
-
CHOICE_MAIN,
|
|
26
|
-
CHOICE_SECOND,
|
|
27
|
-
CHOICE_WAIT,
|
|
28
|
-
CREDENTIALS_FILE_NAME,
|
|
29
|
-
FULL_PERCENT,
|
|
30
|
-
JSON_ACCOUNT_KEY,
|
|
31
|
-
JSON_CONFIG_DIRECTORY_KEY,
|
|
32
|
-
JSON_METERS_KEY,
|
|
33
|
-
JSON_REASON_KEY,
|
|
34
|
-
JSON_SESSION_RESETS_AT_KEY,
|
|
35
|
-
JSON_SESSION_USED_PERCENT_KEY,
|
|
36
|
-
JSON_WEEKLY_RESETS_AT_KEY,
|
|
37
|
-
JSON_WEEKLY_USED_PERCENT_KEY,
|
|
38
|
-
MAIN_CLAUDE_HOME_DIRECTORY_NAME,
|
|
39
|
-
MAIN_SESSION_USED_CEILING_PERCENT,
|
|
40
|
-
MAIN_SPEND_WINDOW,
|
|
41
|
-
MAIN_WEEKLY_USED_CEILING_PERCENT,
|
|
42
|
-
REASON_MAIN_EXPIRING_TEMPLATE,
|
|
43
|
-
REASON_ALL_EXTRAS_WAIT_TEMPLATE,
|
|
44
|
-
REASON_EXTRA_HAS_ROOM_TEMPLATE,
|
|
45
|
-
REASON_EXTRA_UNREAD_TEMPLATE,
|
|
46
|
-
REASON_SECOND_HAS_ROOM_TEMPLATE,
|
|
47
|
-
REASON_SECOND_UNREAD,
|
|
48
|
-
REASON_WAIT_TEMPLATE,
|
|
49
|
-
SECOND_SESSION_USED_CEILING_PERCENT,
|
|
50
|
-
SECOND_WEEKLY_USED_CEILING_PERCENT,
|
|
51
|
-
SECONDS_PER_HOUR,
|
|
52
|
-
UNKNOWN_RESET_TEXT,
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@dataclass(frozen=True)
|
|
57
|
-
class AccountDecision:
|
|
58
|
-
"""The account a job runs on, and the plain-words reason for it."""
|
|
59
|
-
|
|
60
|
-
account: str
|
|
61
|
-
reason: str
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
@dataclass(frozen=True)
|
|
65
|
-
class ExtraAccount:
|
|
66
|
-
"""One ordered extra profile and its usage reading."""
|
|
67
|
-
|
|
68
|
-
name: str
|
|
69
|
-
meters: AccountUsageMeters | None
|
|
70
|
-
config_dir: Path | None = None
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _main_spendable_reason(
|
|
74
|
-
main_meters: AccountUsageMeters | None, now: datetime
|
|
75
|
-
) -> str | None:
|
|
76
|
-
if main_meters is None:
|
|
77
|
-
return None
|
|
78
|
-
session_used = main_meters.session_utilization
|
|
79
|
-
weekly_used = main_meters.weekly_utilization
|
|
80
|
-
weekly_resets_at = main_meters.weekly_resets_at
|
|
81
|
-
if session_used is None or weekly_used is None or weekly_resets_at is None:
|
|
82
|
-
return None
|
|
83
|
-
time_until_reset = weekly_resets_at - now
|
|
84
|
-
is_expiring_soon = time_until_reset <= MAIN_SPEND_WINDOW
|
|
85
|
-
has_weekly_room = weekly_used < MAIN_WEEKLY_USED_CEILING_PERCENT
|
|
86
|
-
has_session_room = session_used < MAIN_SESSION_USED_CEILING_PERCENT
|
|
87
|
-
if not (is_expiring_soon and has_weekly_room and has_session_room):
|
|
88
|
-
return None
|
|
89
|
-
return REASON_MAIN_EXPIRING_TEMPLATE.format(
|
|
90
|
-
hours_until_reset=int(time_until_reset.total_seconds() // SECONDS_PER_HOUR),
|
|
91
|
-
remaining_percent=FULL_PERCENT - weekly_used,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _blocking_reset(meters: AccountUsageMeters) -> datetime | None:
|
|
96
|
-
all_blocking_resets: list[datetime] = []
|
|
97
|
-
weekly_used = meters.weekly_utilization
|
|
98
|
-
session_used = meters.session_utilization
|
|
99
|
-
if weekly_used is not None and weekly_used >= SECOND_WEEKLY_USED_CEILING_PERCENT:
|
|
100
|
-
if meters.weekly_resets_at is None:
|
|
101
|
-
return None
|
|
102
|
-
all_blocking_resets.append(meters.weekly_resets_at)
|
|
103
|
-
if session_used is not None and session_used >= SECOND_SESSION_USED_CEILING_PERCENT:
|
|
104
|
-
if meters.session_resets_at is None:
|
|
105
|
-
return None
|
|
106
|
-
all_blocking_resets.append(meters.session_resets_at)
|
|
107
|
-
return max(all_blocking_resets, default=None)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _wait_decision(second_meters: AccountUsageMeters) -> AccountDecision:
|
|
111
|
-
blocking_reset = _blocking_reset(second_meters)
|
|
112
|
-
return AccountDecision(
|
|
113
|
-
account=CHOICE_WAIT,
|
|
114
|
-
reason=REASON_WAIT_TEMPLATE.format(
|
|
115
|
-
weekly_used_percent=second_meters.weekly_utilization,
|
|
116
|
-
session_used_percent=second_meters.session_utilization,
|
|
117
|
-
next_reset=(
|
|
118
|
-
blocking_reset.isoformat() if blocking_reset else UNKNOWN_RESET_TEXT
|
|
119
|
-
),
|
|
120
|
-
),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _extra_account_decision(extra_account: ExtraAccount) -> AccountDecision | None:
|
|
125
|
-
meters = extra_account.meters
|
|
126
|
-
unread_reason = (
|
|
127
|
-
REASON_SECOND_UNREAD
|
|
128
|
-
if extra_account.name == CHOICE_SECOND
|
|
129
|
-
else REASON_EXTRA_UNREAD_TEMPLATE.format(account=extra_account.name)
|
|
130
|
-
)
|
|
131
|
-
if meters is None:
|
|
132
|
-
return AccountDecision(account=extra_account.name, reason=unread_reason)
|
|
133
|
-
session_used = meters.session_utilization
|
|
134
|
-
weekly_used = meters.weekly_utilization
|
|
135
|
-
if session_used is None or weekly_used is None:
|
|
136
|
-
return AccountDecision(account=extra_account.name, reason=unread_reason)
|
|
137
|
-
is_week_blocked = weekly_used >= SECOND_WEEKLY_USED_CEILING_PERCENT
|
|
138
|
-
is_session_blocked = session_used >= SECOND_SESSION_USED_CEILING_PERCENT
|
|
139
|
-
if is_week_blocked or is_session_blocked:
|
|
140
|
-
return None
|
|
141
|
-
reason_template = (
|
|
142
|
-
REASON_SECOND_HAS_ROOM_TEMPLATE
|
|
143
|
-
if extra_account.name == CHOICE_SECOND
|
|
144
|
-
else REASON_EXTRA_HAS_ROOM_TEMPLATE
|
|
145
|
-
)
|
|
146
|
-
return AccountDecision(
|
|
147
|
-
account=extra_account.name,
|
|
148
|
-
reason=reason_template.format(
|
|
149
|
-
account=extra_account.name,
|
|
150
|
-
weekly_remaining_percent=FULL_PERCENT - weekly_used,
|
|
151
|
-
session_remaining_percent=FULL_PERCENT - session_used,
|
|
152
|
-
),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def choose_account_from_extras(
|
|
157
|
-
*,
|
|
158
|
-
main_meters: AccountUsageMeters | None,
|
|
159
|
-
all_extra_accounts: Sequence[ExtraAccount],
|
|
160
|
-
now: datetime,
|
|
161
|
-
) -> AccountDecision:
|
|
162
|
-
"""Choose main or the first extra profile with room in list order.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
main_meters: The main account's meters, or None when unread.
|
|
166
|
-
all_extra_accounts: The extra accounts in the order they are tried.
|
|
167
|
-
now: The time the reset windows are measured from.
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
The chosen account and the reason, or a wait naming the soonest reset.
|
|
171
|
-
"""
|
|
172
|
-
main_reason = _main_spendable_reason(main_meters, now)
|
|
173
|
-
if main_reason is not None:
|
|
174
|
-
return AccountDecision(account=CHOICE_MAIN, reason=main_reason)
|
|
175
|
-
for each_extra_account in all_extra_accounts:
|
|
176
|
-
decision = _extra_account_decision(each_extra_account)
|
|
177
|
-
if decision is not None:
|
|
178
|
-
return decision
|
|
179
|
-
return _all_extras_wait_decision(all_extra_accounts)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def _all_extras_wait_decision(
|
|
183
|
-
all_extra_accounts: Sequence[ExtraAccount],
|
|
184
|
-
) -> AccountDecision:
|
|
185
|
-
if len(all_extra_accounts) == 1 and all_extra_accounts[0].meters is not None:
|
|
186
|
-
return _wait_decision(all_extra_accounts[0].meters)
|
|
187
|
-
all_resets = (
|
|
188
|
-
_blocking_reset(each_extra_account.meters)
|
|
189
|
-
for each_extra_account in all_extra_accounts
|
|
190
|
-
if each_extra_account.meters is not None
|
|
191
|
-
)
|
|
192
|
-
next_reset = min((reset for reset in all_resets if reset is not None), default=None)
|
|
193
|
-
return AccountDecision(
|
|
194
|
-
account=CHOICE_WAIT,
|
|
195
|
-
reason=REASON_ALL_EXTRAS_WAIT_TEMPLATE.format(
|
|
196
|
-
next_reset=next_reset.isoformat() if next_reset else UNKNOWN_RESET_TEXT
|
|
197
|
-
),
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def choose_account(
|
|
202
|
-
*,
|
|
203
|
-
main_meters: AccountUsageMeters | None,
|
|
204
|
-
second_meters: AccountUsageMeters | None,
|
|
205
|
-
now: datetime,
|
|
206
|
-
) -> AccountDecision:
|
|
207
|
-
"""Pick the account a job runs on from both accounts' meters.
|
|
208
|
-
|
|
209
|
-
::
|
|
210
|
-
|
|
211
|
-
main: week resets in 5h, 80% used, 5-hour 10% used -> main
|
|
212
|
-
main: week resets in 30h -> second, if it has room
|
|
213
|
-
main unread, second at 99% of its week -> wait
|
|
214
|
-
main unread, second unread -> second
|
|
215
|
-
|
|
216
|
-
Main runs a job only to spend leftover usage that expires soon.
|
|
217
|
-
|
|
218
|
-
Args:
|
|
219
|
-
main_meters: The main account's meters, or None when unread.
|
|
220
|
-
second_meters: The second account's meters, or None when unread.
|
|
221
|
-
now: The time the reset windows are measured from.
|
|
222
|
-
|
|
223
|
-
Returns:
|
|
224
|
-
The chosen account and the reason.
|
|
225
|
-
"""
|
|
226
|
-
return choose_account_from_extras(
|
|
227
|
-
main_meters=main_meters,
|
|
228
|
-
all_extra_accounts=(ExtraAccount(name=CHOICE_SECOND, meters=second_meters),),
|
|
229
|
-
now=now,
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def read_account_meters(credentials_path: Path) -> AccountUsageMeters | None:
|
|
234
|
-
"""Read one account's meters, or None when its credential or probe fails.
|
|
235
|
-
|
|
236
|
-
Args:
|
|
237
|
-
credentials_path: The account's CLI credential file.
|
|
238
|
-
|
|
239
|
-
Returns:
|
|
240
|
-
The meters, or None when they cannot be read.
|
|
241
|
-
"""
|
|
242
|
-
try:
|
|
243
|
-
return probe_account_meters(credentials_path)
|
|
244
|
-
except WeeklyUtilizationProbeError:
|
|
245
|
-
return None
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def read_extra_accounts(
|
|
249
|
-
all_config_directories: Sequence[Path],
|
|
250
|
-
) -> tuple[ExtraAccount, ...]:
|
|
251
|
-
"""Read ordered extra profiles and reject duplicate directories.
|
|
252
|
-
|
|
253
|
-
Args:
|
|
254
|
-
all_config_directories: The extra profile homes in the order they are tried.
|
|
255
|
-
|
|
256
|
-
Returns:
|
|
257
|
-
One extra account per directory, with its meters.
|
|
258
|
-
|
|
259
|
-
Raises:
|
|
260
|
-
ValueError: When two entries name the same directory.
|
|
261
|
-
"""
|
|
262
|
-
all_resolved_directories = [
|
|
263
|
-
str(each_directory.resolve()).casefold()
|
|
264
|
-
for each_directory in all_config_directories
|
|
265
|
-
]
|
|
266
|
-
if len(set(all_resolved_directories)) != len(all_config_directories):
|
|
267
|
-
raise ValueError("extra account config directories must be distinct")
|
|
268
|
-
return tuple(
|
|
269
|
-
ExtraAccount(
|
|
270
|
-
name=CHOICE_SECOND if each_index == 1 else f"extra_{each_index}",
|
|
271
|
-
meters=read_account_meters(each_directory / CREDENTIALS_FILE_NAME),
|
|
272
|
-
config_dir=each_directory,
|
|
273
|
-
)
|
|
274
|
-
for each_index, each_directory in enumerate(all_config_directories, start=1)
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def config_directory_for_decision(
|
|
279
|
-
decision: AccountDecision,
|
|
280
|
-
*,
|
|
281
|
-
main_config_dir: Path,
|
|
282
|
-
all_extra_accounts: Sequence[ExtraAccount],
|
|
283
|
-
) -> Path | None:
|
|
284
|
-
"""Find the selected account's config directory.
|
|
285
|
-
|
|
286
|
-
Args:
|
|
287
|
-
decision: The chosen account and reason.
|
|
288
|
-
main_config_dir: The main account's Claude home.
|
|
289
|
-
all_extra_accounts: The extra accounts the decision chose from.
|
|
290
|
-
|
|
291
|
-
Returns:
|
|
292
|
-
The chosen account's Claude home, or None on wait.
|
|
293
|
-
"""
|
|
294
|
-
if decision.account == CHOICE_MAIN:
|
|
295
|
-
return main_config_dir
|
|
296
|
-
return next(
|
|
297
|
-
(
|
|
298
|
-
each_extra_account.config_dir
|
|
299
|
-
for each_extra_account in all_extra_accounts
|
|
300
|
-
if each_extra_account.name == decision.account
|
|
301
|
-
),
|
|
302
|
-
None,
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
307
|
-
parser = argparse.ArgumentParser(
|
|
308
|
-
description="Name the Claude account a runner job uses."
|
|
309
|
-
)
|
|
310
|
-
parser.add_argument(
|
|
311
|
-
"--main-config-dir",
|
|
312
|
-
type=Path,
|
|
313
|
-
default=Path.home() / MAIN_CLAUDE_HOME_DIRECTORY_NAME,
|
|
314
|
-
)
|
|
315
|
-
parser.add_argument(
|
|
316
|
-
"--second-config-dir",
|
|
317
|
-
type=Path,
|
|
318
|
-
default=default_profile_home(),
|
|
319
|
-
)
|
|
320
|
-
parser.add_argument("--extra-config-dir", type=Path, action="append", default=[])
|
|
321
|
-
return parser
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
def decision_payload(
|
|
325
|
-
decision: AccountDecision, *, config_directory: Path | None
|
|
326
|
-
) -> dict[str, str | None]:
|
|
327
|
-
"""Shape a decision as the JSON object the runner job reads.
|
|
328
|
-
|
|
329
|
-
::
|
|
330
|
-
|
|
331
|
-
AccountDecision("second", "...") with the profile directory
|
|
332
|
-
-> {"account": "second", "config_dir": "<profile>", "reason": "..."}
|
|
333
|
-
|
|
334
|
-
Args:
|
|
335
|
-
decision: The chosen account and reason.
|
|
336
|
-
config_directory: The Claude home the account runs under, or None on wait.
|
|
337
|
-
|
|
338
|
-
Returns:
|
|
339
|
-
The account, its Claude home, and the reason.
|
|
340
|
-
"""
|
|
341
|
-
return {
|
|
342
|
-
JSON_ACCOUNT_KEY: decision.account,
|
|
343
|
-
JSON_CONFIG_DIRECTORY_KEY: str(config_directory) if config_directory else None,
|
|
344
|
-
JSON_REASON_KEY: decision.reason,
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
def _iso_or_none(moment: datetime | None) -> str | None:
|
|
349
|
-
return moment.isoformat() if moment else None
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def meters_payload(
|
|
353
|
-
account_meters: AccountUsageMeters | None,
|
|
354
|
-
) -> dict[str, float | str | None] | None:
|
|
355
|
-
"""Shape one account's meters as the JSON object a usage report reads.
|
|
356
|
-
|
|
357
|
-
::
|
|
358
|
-
|
|
359
|
-
AccountUsageMeters(12.0, <reset>, 34.0, <reset>)
|
|
360
|
-
-> {"session_used_percent": 12.0, "session_resets_at": "2026-09-22T22:00:00+00:00",
|
|
361
|
-
"weekly_used_percent": 34.0, "weekly_resets_at": "2026-09-25T21:00:00+00:00"}
|
|
362
|
-
|
|
363
|
-
Args:
|
|
364
|
-
account_meters: The account's meters, or None when unread.
|
|
365
|
-
|
|
366
|
-
Returns:
|
|
367
|
-
Each used percent and reset time, or None for an unread account.
|
|
368
|
-
"""
|
|
369
|
-
if account_meters is None:
|
|
370
|
-
return None
|
|
371
|
-
return {
|
|
372
|
-
JSON_SESSION_USED_PERCENT_KEY: account_meters.session_utilization,
|
|
373
|
-
JSON_SESSION_RESETS_AT_KEY: _iso_or_none(account_meters.session_resets_at),
|
|
374
|
-
JSON_WEEKLY_USED_PERCENT_KEY: account_meters.weekly_utilization,
|
|
375
|
-
JSON_WEEKLY_RESETS_AT_KEY: _iso_or_none(account_meters.weekly_resets_at),
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def main(all_command_arguments: list[str]) -> int:
|
|
380
|
-
"""Print the chosen account and every account's meters as JSON.
|
|
381
|
-
|
|
382
|
-
Args:
|
|
383
|
-
all_command_arguments: Command-line arguments after the program name.
|
|
384
|
-
|
|
385
|
-
Returns:
|
|
386
|
-
Zero once the choice is printed.
|
|
387
|
-
"""
|
|
388
|
-
parser = _build_argument_parser()
|
|
389
|
-
arguments = parser.parse_args(all_command_arguments)
|
|
390
|
-
try:
|
|
391
|
-
extra_accounts = read_extra_accounts(
|
|
392
|
-
(arguments.second_config_dir, *arguments.extra_config_dir)
|
|
393
|
-
)
|
|
394
|
-
except ValueError as error:
|
|
395
|
-
parser.error(str(error))
|
|
396
|
-
main_meters = read_account_meters(arguments.main_config_dir / CREDENTIALS_FILE_NAME)
|
|
397
|
-
decision = choose_account_from_extras(
|
|
398
|
-
main_meters=main_meters,
|
|
399
|
-
all_extra_accounts=extra_accounts,
|
|
400
|
-
now=datetime.now().astimezone(),
|
|
401
|
-
)
|
|
402
|
-
config_directory = config_directory_for_decision(
|
|
403
|
-
decision,
|
|
404
|
-
main_config_dir=arguments.main_config_dir,
|
|
405
|
-
all_extra_accounts=extra_accounts,
|
|
406
|
-
)
|
|
407
|
-
report = {
|
|
408
|
-
**decision_payload(decision, config_directory=config_directory),
|
|
409
|
-
JSON_METERS_KEY: {
|
|
410
|
-
CHOICE_MAIN: meters_payload(main_meters),
|
|
411
|
-
**{
|
|
412
|
-
extra_account.name: meters_payload(extra_account.meters)
|
|
413
|
-
for extra_account in extra_accounts
|
|
414
|
-
},
|
|
415
|
-
},
|
|
416
|
-
}
|
|
417
|
-
print(json.dumps(report))
|
|
418
|
-
return 0
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if __name__ == "__main__":
|
|
422
|
-
sys.exit(main(sys.argv[1:]))
|