claude-multiacc 2.0.37 → 2.0.39

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 (33) hide show
  1. package/README.md +84 -6
  2. package/bin/claude +122 -6
  3. package/bin/claude-accounts +146 -9
  4. package/bin/codex +116 -7
  5. package/bin/codex-accounts +137 -5
  6. package/docs/ACCOUNT_OPERATIONS.md +34 -0
  7. package/docs/CODEX.md +29 -2
  8. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  9. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  10. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  11. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  12. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  13. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  14. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  15. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  16. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  17. package/lib/claude_reset.py +70 -35
  18. package/lib/codex_reset.py +31 -4
  19. package/lib/codex_reset_telemetry.py +4 -2
  20. package/lib/common.sh +19 -0
  21. package/lib/mcp_registry.py +1372 -0
  22. package/package.json +2 -2
  23. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  24. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  25. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  26. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  27. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  28. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  29. package/tests/run-tests.sh +588 -0
  30. package/tests/test_account_seeding.py +40 -0
  31. package/tests/test_claude_reset.py +61 -11
  32. package/tests/test_codex_reset.py +43 -2
  33. package/tests/test_mcp_registry.py +597 -0
@@ -107,6 +107,46 @@ class AccountSeedingTests(unittest.TestCase):
107
107
  self.assertTrue((root / "acct-01" / name).is_symlink())
108
108
  self.assertFalse(os.path.lexists(root / "acct-99" / name))
109
109
 
110
+ def test_seeding_applies_the_pool_mcp_registry(self):
111
+ """The registry reaches a fresh account at seed time; an account's own servers survive."""
112
+ registry = {"version": 1, "retired": [], "projects": {}, "mcpServers": {
113
+ "seeded": {"type": "stdio", "command": "echo", "args": ["hi"], "env": {"K": "v"}}}}
114
+ for provider, name in (("claude", ".claude.json"), ("codex", "config.toml")):
115
+ with self.subTest(provider=provider):
116
+ root = self.home / f".{provider}-accounts"
117
+ root.mkdir(exist_ok=True)
118
+ (root / "accounts.json").write_text(json.dumps({
119
+ "version": 1, "server": "none",
120
+ "accounts": [{"id": "acct-01", "email": "fixture@example.test", "home": "mac"}]}))
121
+ (root / "mcp-servers.json").write_text(json.dumps(registry))
122
+ account = root / "acct-01"
123
+ account.mkdir()
124
+ if provider == "claude":
125
+ (account / name).write_text(json.dumps(
126
+ {"mcpServers": {"own": {"type": "stdio", "command": "true", "args": [], "env": {}}}}))
127
+ else:
128
+ (account / name).write_text('model = "gpt-5"\n\n[mcp_servers.own]\ncommand = "true"\nargs = []\n')
129
+ for _ in range(2):
130
+ self.seed(provider, account)
131
+ text = (account / name).read_text()
132
+ if provider == "claude":
133
+ servers = json.loads(text)["mcpServers"]
134
+ self.assertEqual(servers["seeded"]["command"], "echo")
135
+ self.assertIn("own", servers)
136
+ else:
137
+ self.assertIn("[mcp_servers.seeded]", text)
138
+ self.assertIn('command = "echo"', text)
139
+ self.assertIn("[mcp_servers.own]", text)
140
+ self.assertIn('model = "gpt-5"', text)
141
+ self.assertTrue((account / ".mcp-applied").exists())
142
+
143
+ def test_seeding_without_a_registry_touches_no_config(self):
144
+ for provider, _, name in PROVIDERS:
145
+ with self.subTest(provider=provider):
146
+ account = self.seed(provider)
147
+ self.assertFalse((account / ".mcp-applied").exists())
148
+ self.assertFalse((self.home / f".{provider}-accounts" / "mcp-servers.json").exists())
149
+
110
150
 
111
151
  if __name__ == "__main__":
112
152
  unittest.main()
@@ -36,7 +36,7 @@ def iso(offset: int) -> str:
36
36
  def usage_doc(*, session=0, weekly=50, fable=50, exhausted=(), resets_left=1,
37
37
  use_requires_limit=False, eligible=True, reason=None, blocking=(),
38
38
  clears=("five_hour", "seven_day", "seven_day_overage_included"),
39
- usable=True, week_resets_in=3 * 86400) -> dict:
39
+ usable=True, week_resets_in=3 * 86400, session_resets_in=3600) -> dict:
40
40
  """A usage response in the live 2026-09-22 shape, cedar_ember block included."""
41
41
  grants = [{
42
42
  "id": GRANT, "label": "Claude Opus 5.5 launch: one usage-limit reset for Pro and Max",
@@ -50,7 +50,7 @@ def usage_doc(*, session=0, weekly=50, fable=50, exhausted=(), resets_left=1,
50
50
  return {
51
51
  "limits": [
52
52
  {"kind": "session", "group": "session", "percent": session,
53
- "resets_at": iso(3600) if session else None, "scope": None},
53
+ "resets_at": iso(session_resets_in) if session else None, "scope": None},
54
54
  {"kind": "weekly_all", "group": "weekly", "percent": weekly,
55
55
  "resets_at": iso(week_resets_in), "scope": None},
56
56
  {"kind": "weekly_scoped", "group": "weekly", "percent": fable,
@@ -234,22 +234,65 @@ class ClaudeResetIntegrationTest(ClaudePoolSandbox, unittest.TestCase):
234
234
  self.run_limits()
235
235
  self.assertEqual(len(self.server.state["posts"]), 1)
236
236
 
237
- def test_does_not_redeem_above_five_percent_remaining(self) -> None:
238
- self.server.state["usage"] = usage_doc(session=94, weekly=80, fable=94)
237
+ def test_does_not_redeem_below_the_pools_own_threshold(self) -> None:
238
+ self.server.state["usage"] = usage_doc(session=89, weekly=80, fable=89)
239
239
  self.run_limits()
240
240
  self.assertEqual(self.server.state["posts"], [])
241
241
  self.assertFalse((self.account / ".usage-reset.json").exists())
242
242
  self.assertEqual(self.limits_doc()["reset_credits_available"], 1)
243
243
 
244
- def test_a_full_five_hour_window_alone_never_spends_the_week(self) -> None:
245
- # The grant refills the WEEK and exists once; a five-hour wall refills itself in
246
- # hours while the pool picks another account.
244
+ def test_a_week_parked_at_the_pools_threshold_is_reset(self) -> None:
245
+ # The live acct-15 shape (2026-09-22): weekly_all 93% parks the account for six
246
+ # days at the pool's 90% line; waiting for 95% left its reset unused.
247
+ self.server.state["usage"] = usage_doc(session=100, weekly=93, fable=64,
248
+ session_resets_in=5400)
249
+ self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
250
+ self.run_limits()
251
+ self.assertEqual(len(self.server.state["posts"]), 1)
252
+ self.assertFalse((self.account / ".limited").exists())
253
+
254
+ def park(self, bucket: str, lifts_in: int, reason: str = "client-rate-limit") -> Path:
255
+ marker = self.account / ".limited"
256
+ marked = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 600))
257
+ marker.write_text(f"{int(time.time()) + lifts_in}\nbucket={bucket} percent=100 "
258
+ f"marked_at={marked} reason={reason}\n", encoding="utf-8")
259
+ return marker
260
+
261
+ def test_a_park_the_telemetry_cannot_see_is_reset(self) -> None:
262
+ # acct-13 (a client five-hour 429 while OAuth read 0% everywhere) and acct-19 (the
263
+ # panel's weekly park while OAuth read 24%): parked is parked.
264
+ for bucket, lifts_in in (("client:five_hour", 5400),
265
+ ("weekly_all", 3 * 86400), ("panel:observed", 86400)):
266
+ with self.subTest(bucket=bucket):
267
+ self.server.state["posts"] = []
268
+ self.server.state["usage"] = usage_doc(session=0, weekly=19, fable=24)
269
+ self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
270
+ (self.account / ".usage-reset.json").unlink(missing_ok=True)
271
+ (self.account / "limits.json").unlink(missing_ok=True)
272
+ marker = self.park(bucket, lifts_in)
273
+ self.run_limits()
274
+ self.assertEqual(len(self.server.state["posts"]), 1)
275
+ self.assertFalse(marker.exists())
276
+
277
+ def test_an_error_cooldown_is_not_a_usage_park(self) -> None:
278
+ self.park("session", 600, reason="error-cooldown")
279
+ self.server.state["usage"] = usage_doc(weekly=19, fable=24)
280
+ self.run_limits()
281
+ self.assertEqual(self.server.state["posts"], [])
282
+
283
+ def test_a_five_hour_wall_that_lifts_within_the_hour_keeps_its_reset(self) -> None:
247
284
  self.server.state["usage"] = usage_doc(session=100, weekly=40, fable=40,
248
- exhausted=["five_hour"])
285
+ exhausted=["five_hour"], session_resets_in=1800)
249
286
  self.run_limits()
250
287
  self.assertEqual(self.server.state["posts"], [])
288
+ self.assertIn("limit reset held", self.log())
251
289
  self.assertTrue((self.account / ".limited").exists()) # still parked for the 5h
252
- self.assertEqual(self.limits_doc()["reset_credits_available"], 1)
290
+ # ...but one that holds the account for longer is reset.
291
+ self.server.state["usage"] = usage_doc(session=100, weekly=40, fable=40,
292
+ exhausted=["five_hour"], session_resets_in=5400)
293
+ self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
294
+ self.run_limits()
295
+ self.assertEqual(len(self.server.state["posts"]), 1)
253
296
 
254
297
  def test_a_five_hour_wall_beside_a_spent_week_is_redeemed(self) -> None:
255
298
  # The live acct-17 shape of 2026-09-22: session 100% AND Fable weekly at 95%.
@@ -260,9 +303,9 @@ class ClaudeResetIntegrationTest(ClaudePoolSandbox, unittest.TestCase):
260
303
  self.assertEqual(len(self.server.state["posts"]), 1)
261
304
  self.assertFalse((self.account / ".limited").exists())
262
305
 
263
- def test_a_week_that_refills_tomorrow_keeps_its_reset(self) -> None:
306
+ def test_a_week_that_refills_within_the_hour_keeps_its_reset(self) -> None:
264
307
  self.server.state["usage"] = usage_doc(fable=100, exhausted=["seven_day_overage_included"],
265
- week_resets_in=13 * 3600)
308
+ week_resets_in=1800)
266
309
  self.run_limits()
267
310
  self.assertEqual(self.server.state["posts"], [])
268
311
  self.assertIn("limit reset held", self.log())
@@ -272,6 +315,13 @@ class ClaudeResetIntegrationTest(ClaudePoolSandbox, unittest.TestCase):
272
315
  self.run_limits(CLAUDE_MULTIACC_RESET_MIN_HORIZON="0")
273
316
  self.assertEqual(len(self.server.state["posts"]), 1)
274
317
 
318
+ def test_a_week_that_refills_in_eleven_hours_is_reset(self) -> None:
319
+ # acct-21 (2026-09-22): Fable at 95%, week back at 09:00 — out of service 11h.
320
+ self.server.state["usage"] = usage_doc(weekly=56, fable=95, week_resets_in=11 * 3600)
321
+ self.server.state["responses"] = [(200, {"result": "reset", "resets_left": 0}, True)]
322
+ self.run_limits()
323
+ self.assertEqual(len(self.server.state["posts"]), 1)
324
+
275
325
  def test_a_grant_that_needs_the_limit_waits_for_it(self) -> None:
276
326
  self.server.state["usage"] = usage_doc(fable=97, use_requires_limit=True)
277
327
  self.run_limits()
@@ -140,18 +140,59 @@ class CodexResetIntegrationTest(CodexPoolSandbox, unittest.TestCase):
140
140
  self.run_limits()
141
141
  self.assertEqual(len(self.server.state["posts"]), 2)
142
142
 
143
- def test_does_not_redeem_above_five_percent_remaining(self) -> None:
143
+ def test_does_not_redeem_below_the_pools_threshold(self) -> None:
144
144
  now = int(time.time())
145
145
  self.server.state["usage"] = {"rate_limit": {"allowed": True,
146
- "primary_window": {"used_percent": 94, "limit_window_seconds": 18000,
146
+ "primary_window": {"used_percent": 89, "limit_window_seconds": 18000,
147
147
  "reset_at": now + 3600}},
148
148
  "rate_limit_reset_credits": {"available_count": 1}}
149
+ self.server.state["credits"] = {"available_count": 1, "credits": []}
149
150
  result = self.run_limits()
150
151
  self.assertEqual(result.returncode, 0, result.stderr)
151
152
  self.assertEqual(self.server.state["credit_gets"], 1)
152
153
  self.assertEqual(self.server.state["posts"], [])
153
154
  self.assertFalse((self.pool / "acct-01/.usage-reset.json").exists())
154
155
 
156
+ def test_an_account_parked_at_the_pools_threshold_uses_its_reset(self) -> None:
157
+ # Operator rule (2026-09-22): a PARKED account uses its reset. The pool parks at
158
+ # 90%; waiting for 95% left accounts at 90-94% idle for days with credits unused.
159
+ now = int(time.time())
160
+ self.server.state["usage"] = {"rate_limit": {"allowed": True,
161
+ "primary_window": {"used_percent": 92, "limit_window_seconds": 604800,
162
+ "reset_at": now + 4 * 86400}}}
163
+ self.server.state["credits"] = {"available_count": 1, "credits": []}
164
+ self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
165
+ self.assertEqual(self.run_limits().returncode, 0)
166
+ self.assertEqual(len(self.server.state["posts"]), 1)
167
+ self.assertFalse((self.pool / "acct-01/.limited").exists())
168
+
169
+ def test_a_client_reported_park_uses_its_reset(self) -> None:
170
+ # The rollout scan parked the account on a real 429 its telemetry does not show.
171
+ now = int(time.time())
172
+ marker = self.pool / "acct-01/.limited"
173
+ marker.write_text(f"{now + 3 * 86400}\nbucket=client:7d percent=100 "
174
+ f"marked_at=2020-01-01T00:00:00Z reason=client-rate-limit\n")
175
+ self.server.state["usage"] = {"rate_limit": {"allowed": True,
176
+ "primary_window": {"used_percent": 40, "limit_window_seconds": 604800,
177
+ "reset_at": now + 3 * 86400}}}
178
+ self.server.state["credits"] = {"available_count": 1, "credits": []}
179
+ self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
180
+ self.assertEqual(self.run_limits().returncode, 0)
181
+ self.assertEqual(len(self.server.state["posts"]), 1)
182
+ self.assertFalse(marker.exists())
183
+
184
+ def test_a_park_that_lifts_within_the_hour_keeps_its_reset(self) -> None:
185
+ now = int(time.time())
186
+ (self.pool / "acct-01/.limited").write_text(
187
+ f"{now + 1200}\nbucket=client:5h percent=100 marked_at=2020-01-01T00:00:00Z "
188
+ f"reason=client-rate-limit\n")
189
+ self.server.state["usage"] = {"rate_limit": {"allowed": True,
190
+ "primary_window": {"used_percent": 40, "limit_window_seconds": 604800,
191
+ "reset_at": now + 3 * 86400}}}
192
+ self.server.state["credits"] = {"available_count": 1, "credits": []}
193
+ self.assertEqual(self.run_limits().returncode, 0)
194
+ self.assertEqual(self.server.state["posts"], [])
195
+
155
196
  def test_retries_a_lost_response_with_the_same_idempotency_key(self) -> None:
156
197
  now = int(time.time())
157
198
  self.server.state["usage"] = {"rate_limit": {"allowed": False,