tokenmaxxing 1.8.0 → 1.9.1

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 (85) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +0 -40
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +1 -33
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -73
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -34
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +4 -29
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +6 -127
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -48
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -40
  67. package/src/sdk.ts +0 -59
  68. package/agent-plugin/agents/tokenmaxxing-claude.md +0 -43
  69. package/agent-plugin/agents/tokenmaxxing-codex.md +0 -40
  70. package/agent-plugin/hooks/cursor-relay.json +0 -14
  71. package/agent-plugin/skills/relay-session/SKILL.md +0 -118
  72. package/agent-plugin/skills/relay-session/references/ipc.md +0 -23
  73. package/src/cli/relay.ts +0 -323
  74. package/src/entries/relaypermission.ts +0 -105
  75. package/src/lib/relay/config.ts +0 -84
  76. package/src/lib/relay/decide.ts +0 -75
  77. package/src/lib/relay/gc.ts +0 -80
  78. package/src/lib/relay/install.ts +0 -143
  79. package/src/lib/relay/markers.ts +0 -148
  80. package/src/lib/relay/modes.ts +0 -82
  81. package/src/lib/relay/protocol.ts +0 -61
  82. package/src/lib/relay/registry.ts +0 -175
  83. package/src/lib/relay/tmux.ts +0 -109
  84. package/src/lib/relay/turn.ts +0 -137
  85. package/src/lib/relay/worker.ts +0 -141
package/src/lib/types.ts CHANGED
@@ -1,12 +1,5 @@
1
- // Shared data model - Zod schemas are the single source of truth; TS types are
2
- // inferred from them. Everything that crosses an external boundary (keychain
3
- // blob, ~/.claude.json, hook/statusLine stdin, OAuth response, our own state
4
- // files) is validated through these instead of hand-checked.
5
-
6
1
  import { z } from "zod";
7
2
 
8
- /** OAuth object inside the keychain blob (`claudeAiOauth`). Loose: preserve any
9
- * extra fields claude may add so a harvest→install round-trip is lossless. */
10
3
  const OAuthCredsSchema = z.looseObject({
11
4
  accessToken: z.string(),
12
5
  refreshToken: z.string(),
@@ -18,15 +11,8 @@ const OAuthCredsSchema = z.looseObject({
18
11
  });
19
12
  export type OAuthCreds = z.infer<typeof OAuthCredsSchema>;
20
13
 
21
- /** The `Claude Code-credentials` keychain item. Loose: the live item also holds
22
- * sibling state (e.g. per-MCP-server OAuth tokens), which we must preserve when
23
- * swapping - we only ever replace `claudeAiOauth`. */
24
14
  export const CredentialBlobSchema = z.looseObject({ claudeAiOauth: OAuthCredsSchema });
25
15
 
26
- /** The `oauthAccount` identity object in ~/.claude.json. Loose: preserve every
27
- * key so we can reinstall it verbatim on activation. Only the three ids are
28
- * required; real blobs carry many more fields (some null), so descriptive ones
29
- * are `.nullish()` (string | null | undefined) to tolerate them. */
30
16
  export const OAuthAccountSchema = z.looseObject({
31
17
  accountUuid: z.string(),
32
18
  emailAddress: z.string(),
@@ -35,8 +21,6 @@ export const OAuthAccountSchema = z.looseObject({
35
21
  seatTier: z.string().nullish(),
36
22
  billingType: z.string().nullish(),
37
23
  displayName: z.string().nullish(),
38
- /** rate-limit tier id ("default_claude_max_20x"), same value the credential
39
- * blob carries; claude fills it in on profile fetch (live-verified 2.1.211). */
40
24
  organizationRateLimitTier: z.string().nullish(),
41
25
  });
42
26
  export type OAuthAccount = z.infer<typeof OAuthAccountSchema>;
@@ -56,9 +40,6 @@ export type UsageWindows = z.infer<typeof UsageWindowsSchema>;
56
40
  const ModelInfoSchema = z.object({ id: z.string(), display: z.string() });
57
41
  export type ModelInfo = z.infer<typeof ModelInfoSchema>;
58
42
 
59
- /** usage.json - written by the statusLine shim, read by the Stop hook. Carries
60
- * the two AGGREGATE windows (session=fiveHour, week-all-models=sevenDay) plus
61
- * the active model. Per-model caps live in ModelUsageState (from `/usage`). */
62
43
  export const UsageStateSchema = UsageWindowsSchema.extend({
63
44
  org: z.string().nullable(),
64
45
  ts: z.number(),
@@ -66,22 +47,14 @@ export const UsageStateSchema = UsageWindowsSchema.extend({
66
47
  });
67
48
  export type UsageState = z.infer<typeof UsageStateSchema>;
68
49
 
69
- /** model-usage.json - per-model weekly caps parsed from `claude -p '/usage'`,
70
- * TTL-cached so we don't poll every turn. Keyed by model display name ("Fable"). */
71
50
  export const ModelUsageStateSchema = z.object({
72
51
  perModel: z.record(z.string(), UsageWindowSchema).default({}),
73
52
  org: z.string().nullable(),
74
53
  ts: z.number(),
75
- /** when the rows were actually MEASURED. `ts` is the write time and drives
76
- * the probe-TTL anti-storm, so a failed probe re-stamps it while carrying
77
- * the OLD rows forward - dating those rows by ts rolled the null-reset
78
- * self-bound forward on every failed probe (closing-review catch). Absent
79
- * on records predating this field: readers fall back to ts. */
80
54
  sampledAt: z.number().optional(),
81
55
  });
82
56
  export type ModelUsageState = z.infer<typeof ModelUsageStateSchema>;
83
57
 
84
- /** A parked account in the pool (accounts.json - NON-secret). */
85
58
  export const AccountSchema = z.object({
86
59
  accountUuid: z.string(),
87
60
  email: z.string(),
@@ -92,23 +65,11 @@ export const AccountSchema = z.object({
92
65
  addedAt: z.string(),
93
66
  lastUsage: UsageWindowsSchema.optional(),
94
67
  lastPerModel: z.record(z.string(), UsageWindowSchema).optional(),
95
- /** epoch ms of the sample behind lastPerModel SPECIFICALLY: the aggregate
96
- * stamp (lastUsageAt) advances on every engaged evaluation while the
97
- * per-model rows refresh only when a gated family is measured, so dating
98
- * the rows by lastUsageAt inflated the null-reset self-bound by days
99
- * (closing-review catch). Absent on records predating this field: readers
100
- * fall back to lastUsageAt, the previous (over-)approximation. */
101
68
  lastPerModelAt: z.number().optional(),
102
- /** epoch ms of the sample behind the AGGREGATE lastUsage windows (per-model
103
- * rows date by lastPerModelAt above). resetsAt values are absolute epochs
104
- * (UTC-anchored), so even an old snapshot still resolves to correct
105
- * resets - display it as a dated cache, never discard. */
106
69
  lastUsageAt: z.number().optional(),
70
+ enforcedUntil: z.number().optional(),
107
71
  needsReauth: z.boolean().optional(),
108
72
  subscriptionType: z.string().optional(),
109
- /** the blob's rate-limit tier id (e.g. "default_claude_max_20x"): the only
110
- * field that distinguishes max 5x from max 20x (subscriptionType is just
111
- * "max" for both). Refreshed on every verified sample. */
112
73
  rateLimitTier: z.string().optional(),
113
74
  });
114
75
  export type Account = z.infer<typeof AccountSchema>;
@@ -119,22 +80,21 @@ export const AccountsIndexSchema = z.object({
119
80
  accounts: z.array(AccountSchema).default([]),
120
81
  });
121
82
 
122
- /** lastswap.json - epoch ms of the last credential swap. Its own tiny file (not
123
- * accounts.json) so the statusLine shim reads a few bytes per tick and no other
124
- * index writer can clobber it. Absent = no swap has ever run. */
125
83
  export const LastSwapSchema = z.object({ ts: z.number() });
126
84
  export type AccountsIndex = z.infer<typeof AccountsIndexSchema>;
127
85
 
128
- /** Per-window screening bars (used %): an account with a window at/over its bar
129
- * is no switch candidate until that window resets. The session bar is lower
130
- * than the weekly one: a session reset is at most 5h away, so burning a little
131
- * headroom there is cheap, while weekly quota is use-it-or-lose-it and worth
132
- * draining closer to the wall. Screening is these bars' ONLY job - the switch
133
- * trigger is the greedy pace-pressure convergence (policy.greedySessionFloor). */
86
+ export const NextCheckSchema = z.object({ dueAt: z.number(), ts: z.number() });
87
+
88
+ export const EnforcedLimitSchema = z.object({
89
+ org: z.string(),
90
+ family: z.string().nullable(),
91
+ resetsAt: z.number().nullable(),
92
+ windowMs: z.number(),
93
+ });
94
+ export type EnforcedLimit = z.infer<typeof EnforcedLimitSchema>;
95
+
134
96
  export const ThresholdsSchema = z.object({
135
- /** 5h session window. */
136
97
  session: z.number().min(0).max(100),
137
- /** 7-day aggregate AND per-model weekly caps. */
138
98
  weekly: z.number().min(0).max(100),
139
99
  });
140
100
  export type Thresholds = z.infer<typeof ThresholdsSchema>;
@@ -142,74 +102,34 @@ export type Thresholds = z.infer<typeof ThresholdsSchema>;
142
102
  export const ConfigSchema = z
143
103
  .object({
144
104
  thresholds: ThresholdsSchema,
145
- /** LAYER 2 - the wall bars. `thresholds` (Layer 1) screen normal
146
- * account-to-account switching and deliberately leave headroom; these are
147
- * the true-wall bars the decision falls back to ONLY once every account is
148
- * exhausted at Layer 1. Below the wall a session holds its seat and pumps
149
- * the last drops; a window at/over the wall (default 100 = the server's own
150
- * limit, the same figure /rate-limit-options reads) is genuinely spent and
151
- * the pool moves to the next account, parking only when all are walled.
152
- * Set equal to `thresholds` to disable Layer 2 (hardBars subtracts the same
153
- * projectionMargin as effectiveBars, so equal thresholds collapse to one
154
- * effective bar and Layer 2 has no band to act in). At the default margin 0
155
- * the wall is the literal 100. */
156
105
  hardThresholds: ThresholdsSchema,
157
106
  claudeBin: z.string(),
158
- /** the real codex binary (empty = resolve from PATH); pinned by `init --codex`. */
159
107
  codexBin: z.string(),
160
108
  policy: z.object({
161
- /** percent margin subtracted from every bar (effectiveBars); above 100 the
162
- * effective bars go negative and everything reads exhausted, so bounded. */
163
109
  projectionMargin: z.number().min(0).max(100),
164
- /** session-used % at which the greedy convergence engages: from here on,
165
- * every evaluation swaps to the usable account furthest behind its weekly
166
- * pace whenever that beats the current one (idempotent; current keeps its
167
- * seat on ties). Below the floor a fresh session rides its account. */
168
110
  greedySessionFloor: z.number().min(0).max(100),
169
- /** models whose PER-MODEL weekly cap should trigger a switch (display names, lowercased). */
170
111
  switchModels: z.array(z.string()),
171
- /** how long a `/usage` per-model poll stays fresh before we re-poll (ms). */
172
112
  usagePollTtlMs: z.number().int().positive(),
173
- /** when every account is depleted, auto-wait for a reset only if it is within this window (ms). */
174
113
  maxWaitMs: z.number().int().positive(),
175
114
  }),
176
115
  })
177
- // Cross-field (review catch, PR #31): effectiveBars subtracts the margin
178
- // from each threshold, and a bar at or below zero makes EVERY nonnegative
179
- // usage percentage read as exhausted - the whole pool looks depleted and the
180
- // switch path churns. Per-field bounds alone cannot see this.
181
116
  .refine((cfg) => cfg.policy.projectionMargin < Math.min(cfg.thresholds.session, cfg.thresholds.weekly), {
182
117
  message: "policy.projectionMargin must be strictly below both thresholds (effectiveBars would hit zero and every account would read as exhausted)",
183
118
  })
184
- // The wall must sit at or above each screening bar. A wall BELOW its
185
- // screening bar would make Layer 2 "usable" a stricter test than Layer 1
186
- // screening - the pool could reach the wall fallback and find every account
187
- // already over the (lower) wall, parking earlier than Layer 1 alone would.
188
- // Equal is allowed and simply disables Layer 2 for that window.
189
119
  .refine((cfg) => cfg.hardThresholds.session >= cfg.thresholds.session && cfg.hardThresholds.weekly >= cfg.thresholds.weekly, {
190
120
  message: "hardThresholds (the Layer 2 wall) must be at or above thresholds (the Layer 1 screening bars) for both windows",
191
121
  });
192
122
  export type Config = z.infer<typeof ConfigSchema>;
193
123
 
194
- /** The hook -> supervisor respawn marker. Written only for a depleted-pool
195
- * wait (plain swaps adopt in place, no respawn). The marker FILE is keyed by
196
- * the supervisor's PINNED session id (the path it watches - stable for the
197
- * process's whole life), while `sessionId` carries the CURRENT transcript to
198
- * resume: after /clear claude mints a new session id, and keying the file by
199
- * the stdin sid orphaned every marker while the anticipatory pre-park still
200
- * fired (closing-review HIGH catch; the codex marker was immune by exactly
201
- * this construction). */
202
124
  export const RespawnMarkerSchema = z.object({
203
125
  account: z.string(),
204
126
  ts: z.number(),
205
- /** the supervisor waits until this epoch ms before relaunching. */
206
127
  waitUntil: z.number(),
207
- /** the transcript to `--resume`: the hook-stdin session id, which drifts
208
- * from the pinned id after /clear. */
209
128
  sessionId: z.string(),
129
+ prompt: z.string().optional(),
130
+ launchedAt: z.number().optional(),
210
131
  });
211
132
 
212
- /** rate_limits + model as they appear in statusLine stdin (epoch-seconds resets). */
213
133
  export const RateLimitsStdinSchema = z.looseObject({
214
134
  rate_limits: z
215
135
  .looseObject({
@@ -221,11 +141,6 @@ export const RateLimitsStdinSchema = z.looseObject({
221
141
  organizationUuid: z.string().optional(),
222
142
  });
223
143
 
224
- /** The statusLine stdin fields the native renderer consumes, on top of the
225
- * rate-limit tee's needs. Loose + optional throughout: fields are null before
226
- * the first API response and claude adds new ones freely. Each sub-object also
227
- * `.catch(undefined)`es so a field that drifts to a wrong shape degrades to
228
- * absent instead of failing the whole parse and erasing the info block. */
229
144
  export const StatusLineStdinSchema = RateLimitsStdinSchema.extend({
230
145
  workspace: z
231
146
  .looseObject({
@@ -247,10 +162,6 @@ export const StatusLineStdinSchema = RateLimitsStdinSchema.extend({
247
162
  effort: z.looseObject({ level: z.string().optional() }).nullable().optional().catch(undefined),
248
163
  });
249
164
 
250
- /** subagentStatusLine stdin: base session fields plus one entry per active
251
- * subagent task (verified against the 2.1.214 bundle + docs 2026-07-18;
252
- * tasks[].model/contextWindowSize need claude >= 2.1.205, effort >= 2.1.214).
253
- * Same loose + catch degradation contract as StatusLineStdinSchema. */
254
165
  export const SubagentStatusLineStdinSchema = z.looseObject({
255
166
  tasks: z
256
167
  .array(
@@ -269,7 +180,6 @@ export const SubagentStatusLineStdinSchema = z.looseObject({
269
180
  .catch(undefined),
270
181
  });
271
182
 
272
- /** Success body of the OAuth refresh grant. */
273
183
  export const RefreshResponseSchema = z.looseObject({
274
184
  access_token: z.string(),
275
185
  refresh_token: z.string().optional(),
@@ -279,19 +189,12 @@ export const RefreshResponseSchema = z.looseObject({
279
189
  token_type: z.string().optional(),
280
190
  });
281
191
 
282
- /** Success body of GET /api/oauth/claude_cli/roles - the org a token ACTUALLY
283
- * belongs to, independent of any stored label. */
284
192
  export const RolesResponseSchema = z.looseObject({
285
193
  organization_uuid: z.string(),
286
194
  organization_name: z.string(),
287
195
  });
288
196
  export type RolesResponse = z.infer<typeof RolesResponseSchema>;
289
197
 
290
- // ---- Codex pool ------------------------------------------------------------
291
-
292
- /** `tokens` inside $CODEX_HOME/auth.json (verified against a live 0.144.4
293
- * auth.json). Loose: preserve unknown siblings so a harvest and reinstall
294
- * round-trip is lossless. */
295
198
  const CodexTokensSchema = z.looseObject({
296
199
  id_token: z.string(),
297
200
  access_token: z.string(),
@@ -299,19 +202,12 @@ const CodexTokensSchema = z.looseObject({
299
202
  account_id: z.string().optional(),
300
203
  });
301
204
 
302
- /** The whole auth.json. Loose: auth_mode, OPENAI_API_KEY (may be null), and
303
- * future siblings ride along verbatim. */
304
205
  export const CodexAuthJsonSchema = z.looseObject({
305
206
  tokens: CodexTokensSchema,
306
207
  last_refresh: z.string().optional(),
307
208
  });
308
209
  export type CodexAuthJson = z.infer<typeof CodexAuthJsonSchema>;
309
210
 
310
- /** One rate-limit window as tokenmaxxing stores it: percent used, absolute
311
- * epoch ms reset, and the server-declared duration. Codex windows are
312
- * duration-driven (the weekly window is PRIMARY on plans whose 5h window was
313
- * removed in July 2026), so classification must go by windowSeconds, never
314
- * by primary/secondary position. */
315
211
  const CodexWindowSchema = z.object({
316
212
  usedPercentage: z.number(),
317
213
  resetsAt: z.number().nullable(),
@@ -319,9 +215,6 @@ const CodexWindowSchema = z.object({
319
215
  });
320
216
  export type CodexWindow = z.infer<typeof CodexWindowSchema>;
321
217
 
322
- /** Everything one free usage read yields: the token's OWN identity (the codex
323
- * analog of the claude roles endpoint: labels drift, the token cannot lie)
324
- * plus every rate-limit window, aggregate and per-limit-family. */
325
218
  export const CodexUsageSchema = z.object({
326
219
  accountId: z.string(),
327
220
  email: z.string().nullable(),
@@ -331,19 +224,12 @@ export const CodexUsageSchema = z.object({
331
224
  });
332
225
  export type CodexUsage = z.infer<typeof CodexUsageSchema>;
333
226
 
334
- /** A parked-credential file NAME, never a path. These are machine-written
335
- * (`tokenmaxxing-codex-<id8>`), so a separator here means the index is
336
- * corrupted - and `join(credsDir, credFile)` would normalize `../` right out
337
- * of codex-creds, letting `rm --codex` unlink another file (review catch).
338
- * Refusing at parse time matches the codex loaders' throw-on-unparsable
339
- * contract and covers the read/write paths with the delete. */
340
227
  const BareFileNameSchema = z
341
228
  .string()
342
229
  .refine((s) => s.length > 0 && s !== "." && s !== ".." && !s.includes("/") && !s.includes("\\"), {
343
230
  message: "credFile must be a bare file name, not a path",
344
231
  });
345
232
 
346
- /** A pooled codex account (codex-accounts.json - NON-secret). */
347
233
  export const CodexAccountSchema = z.object({
348
234
  accountId: z.string(),
349
235
  email: z.string().nullable(),
@@ -369,30 +255,17 @@ export const CodexAccountsIndexSchema = z.object({
369
255
  });
370
256
  export type CodexAccountsIndex = z.infer<typeof CodexAccountsIndexSchema>;
371
257
 
372
- /** Codex Stop-hook stdin (verified against the 0.144.4 binary wire schema and
373
- * the official hooks reference): only what the swap trigger consumes. */
374
258
  export const CodexStopStdinSchema = z.looseObject({
375
259
  session_id: z.string().optional(),
376
260
  hook_event_name: z.string().optional(),
377
261
  });
378
262
 
379
- /** The codex supervisor's respawn marker payload: which session to resume and
380
- * under which account label (for the switch banner). */
381
263
  export const CodexRespawnMarkerSchema = z.object({
382
264
  account: z.string(),
383
265
  sessionId: z.string().nullable(),
384
266
  ts: z.number(),
385
267
  });
386
268
 
387
- /** A cross-session reconcile signal (owner decisions 2026-07-20): the
388
- * deciding actor saw the addressed supervisor's session running on this
389
- * pooled NON-LIVE account - healthy or not (a non-live session cannot
390
- * refresh cross-account and wedges at token expiry) - while the live seat
391
- * is usable. The session's OWN Stop hook consumes it at its next turn
392
- * boundary - the only safe respawn point - promoting it into a respawn
393
- * marker with the session id its stdin alone carries. `accountId` doubles
394
- * as the staleness guard: a session that already moved accounts drops the
395
- * signal instead of respawning. */
396
269
  export const CodexReconcileMarkerSchema = z.object({
397
270
  accountId: z.string(),
398
271
  ts: z.number(),