tokenmaxxing 1.20.0 → 1.23.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/DESIGN.md +2 -4
- package/README.md +3 -30
- package/package.json +1 -7
- package/src/cli/check.ts +0 -3
- package/src/cli/config.ts +0 -1
- package/src/cli/status.ts +2 -12
- package/src/entries/codexstophook.ts +0 -1
- package/src/entries/stopfailurehook.ts +6 -9
- package/src/entries/stophook.ts +0 -1
- package/src/entries/supervisor.ts +1 -2
- package/src/lib/codexdecide.ts +1 -25
- package/src/lib/codexpick.ts +3 -3
- package/src/lib/codexusage.ts +0 -9
- package/src/lib/decide.ts +4 -69
- package/src/lib/paths.ts +0 -1
- package/src/lib/state.ts +2 -82
- package/src/lib/types.ts +0 -53
- package/agent-plugin/bin/tokenmaxxing-mcp +0 -7
- package/agent-plugin/mcp.json +0 -10
- package/agent-plugin/plugin.json +0 -20
- package/agent-plugin/skills/codex-pool/SKILL.md +0 -23
- package/agent-plugin/skills/codex-pool/references/codex.md +0 -5
- package/agent-plugin/skills/credentials-hygiene/SKILL.md +0 -27
- package/agent-plugin/skills/credentials-hygiene/references/credentials.md +0 -6
- package/agent-plugin/skills/doctor-diagnostics/SKILL.md +0 -26
- package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +0 -5
- package/agent-plugin/skills/pool-status/SKILL.md +0 -27
- package/agent-plugin/skills/pool-status/references/commands.md +0 -9
- package/agent-plugin/skills/safe-contribution/SKILL.md +0 -27
- package/agent-plugin/skills/safe-contribution/references/ship.md +0 -5
- package/agent-plugin/skills/sdk-pairing/SKILL.md +0 -34
- package/agent-plugin/skills/sdk-pairing/references/sdk.md +0 -6
- package/agent-plugin/skills/switching-policy/SKILL.md +0 -35
- package/agent-plugin/skills/switching-policy/references/policy.md +0 -10
- package/src/entries/mcp.ts +0 -277
- package/src/lib/bankedreset.ts +0 -234
- package/src/lib/codexreset.ts +0 -122
- package/src/sdk.ts +0 -55
package/src/lib/state.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { closeSync, existsSync, fstatSync, openSync, readFileSync, rmSync, utimesSync } from "node:fs";
|
|
2
|
-
import { isEqual
|
|
2
|
+
import { isEqual } from "es-toolkit";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { paths, realClaudeBinFromEnv, realCodexBinFromEnv } from "./paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
6
|
import {
|
|
7
7
|
AccountsIndexSchema,
|
|
8
|
-
BankedResetProvidersSchema,
|
|
9
8
|
ConfigSchema,
|
|
10
9
|
LastSwapSchema,
|
|
11
10
|
ModelUsageStateSchema,
|
|
@@ -14,11 +13,8 @@ import {
|
|
|
14
13
|
UsageStateSchema,
|
|
15
14
|
type AccountsIndex,
|
|
16
15
|
type Config,
|
|
17
|
-
type FamilyAnchor,
|
|
18
16
|
type ModelUsageState,
|
|
19
17
|
type UsageState,
|
|
20
|
-
type UsageWindow,
|
|
21
|
-
type WindowAnchor,
|
|
22
18
|
} from "./types.ts";
|
|
23
19
|
|
|
24
20
|
const DEFAULT_CONFIG: Config = {
|
|
@@ -34,7 +30,6 @@ const DEFAULT_CONFIG: Config = {
|
|
|
34
30
|
usagePollTtlMs: 90_000,
|
|
35
31
|
maxWaitMs: 3_600_000,
|
|
36
32
|
checkIntervalMs: 60_000,
|
|
37
|
-
preferToUseBankedReset: [],
|
|
38
33
|
},
|
|
39
34
|
};
|
|
40
35
|
|
|
@@ -55,7 +50,6 @@ export const ConfigFileSchema = z
|
|
|
55
50
|
usagePollTtlMs: z.number().int().positive(),
|
|
56
51
|
maxWaitMs: z.number().int().positive(),
|
|
57
52
|
checkIntervalMs: z.number().int().min(10_000),
|
|
58
|
-
preferToUseBankedReset: BankedResetProvidersSchema,
|
|
59
53
|
})
|
|
60
54
|
.partial(),
|
|
61
55
|
})
|
|
@@ -89,9 +83,6 @@ export function mergeConfigFile(p: z.infer<typeof ConfigFileSchema>): MergeOutco
|
|
|
89
83
|
if (p.policy?.switchModels) {
|
|
90
84
|
cfg.policy.switchModels = p.policy.switchModels.map((s) => s.toLowerCase());
|
|
91
85
|
}
|
|
92
|
-
if (p.policy?.preferToUseBankedReset) {
|
|
93
|
-
cfg.policy.preferToUseBankedReset = uniq(p.policy.preferToUseBankedReset);
|
|
94
|
-
}
|
|
95
86
|
const envBin = realClaudeBinFromEnv();
|
|
96
87
|
if (envBin) cfg.claudeBin = envBin;
|
|
97
88
|
const envCodexBin = realCodexBinFromEnv();
|
|
@@ -196,29 +187,6 @@ export function saveLastSwapAt(ts: number): void {
|
|
|
196
187
|
writeFileAtomic(paths.lastSwapJson, JSON.stringify(LastSwapSchema.parse({ ts })));
|
|
197
188
|
}
|
|
198
189
|
|
|
199
|
-
export function loadLastResetAt(): number | null {
|
|
200
|
-
if (!existsSync(paths.lastResetJson)) return null;
|
|
201
|
-
let json: unknown;
|
|
202
|
-
try {
|
|
203
|
-
json = JSON.parse(readFileSync(paths.lastResetJson, "utf8"));
|
|
204
|
-
} catch {
|
|
205
|
-
throw new Error(`${paths.lastResetJson} is corrupt (unparsable JSON) - refusing to treat a damaged reset clock as never-reset; repair or remove the file`);
|
|
206
|
-
}
|
|
207
|
-
return LastSwapSchema.parse(json).ts;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function saveLastResetAt(ts: number): void {
|
|
211
|
-
writeFileAtomic(paths.lastResetJson, JSON.stringify(LastSwapSchema.parse({ ts })));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function loadLastSeatChangeAt(): number | null {
|
|
215
|
-
const swapAt = loadLastSwapAt();
|
|
216
|
-
const resetAt = loadLastResetAt();
|
|
217
|
-
if (swapAt == null) return resetAt;
|
|
218
|
-
if (resetAt == null) return swapAt;
|
|
219
|
-
return Math.max(swapAt, resetAt);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
190
|
const DepletedWaitSchema = z.object({ waitUntil: z.number(), accountUuid: z.string(), ts: z.number() });
|
|
223
191
|
export type DepletedWait = z.infer<typeof DepletedWaitSchema>;
|
|
224
192
|
|
|
@@ -270,61 +238,13 @@ export function clearNextCheck(): void {
|
|
|
270
238
|
|
|
271
239
|
const USAGE_TS_REFRESH_MS = 10 * 60_000;
|
|
272
240
|
const SAMPLED_AT_REFRESH_MS = 30_000;
|
|
273
|
-
const SESSION_COST_MIN_DELTA = 25;
|
|
274
|
-
|
|
275
|
-
function anchorHolds(anchor: WindowAnchor, next: UsageState): boolean {
|
|
276
|
-
return (
|
|
277
|
-
anchor.fiveHourResetsAt === next.fiveHour.resetsAt &&
|
|
278
|
-
anchor.sevenDayResetsAt === next.sevenDay.resetsAt &&
|
|
279
|
-
next.fiveHour.usedPercentage >= anchor.session &&
|
|
280
|
-
next.sevenDay.usedPercentage >= anchor.weekly
|
|
281
|
-
);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export function measureSessionWindow(prev: UsageState | null, next: UsageState, stamp: boolean): Pick<UsageState, "anchor" | "sessionWindowWeeklyCost"> {
|
|
285
|
-
const same = prev != null && prev.account === next.account;
|
|
286
|
-
const carried = same ? prev.sessionWindowWeeklyCost : undefined;
|
|
287
|
-
if (stamp) return { ...(same && prev.anchor ? { anchor: prev.anchor } : {}), ...(carried != null ? { sessionWindowWeeklyCost: carried } : {}) };
|
|
288
|
-
const anchor: WindowAnchor =
|
|
289
|
-
same && prev.anchor && anchorHolds(prev.anchor, next)
|
|
290
|
-
? prev.anchor
|
|
291
|
-
: { fiveHourResetsAt: next.fiveHour.resetsAt, sevenDayResetsAt: next.sevenDay.resetsAt, session: next.fiveHour.usedPercentage, weekly: next.sevenDay.usedPercentage };
|
|
292
|
-
const sessionDelta = next.fiveHour.usedPercentage - anchor.session;
|
|
293
|
-
const weeklyDelta = next.sevenDay.usedPercentage - anchor.weekly;
|
|
294
|
-
const measured = sessionDelta >= SESSION_COST_MIN_DELTA && weeklyDelta > 0 ? (100 * weeklyDelta) / sessionDelta : undefined;
|
|
295
|
-
const cost = measured ?? carried;
|
|
296
|
-
return { anchor, ...(cost != null ? { sessionWindowWeeklyCost: cost } : {}) };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function familyAnchorHolds(anchor: FamilyAnchor, session: UsageWindow, caps: Record<string, number>): boolean {
|
|
300
|
-
return (
|
|
301
|
-
anchor.fiveHourResetsAt === session.resetsAt &&
|
|
302
|
-
session.usedPercentage >= anchor.session &&
|
|
303
|
-
Object.entries(caps).every(([family, used]) => anchor.caps[family] == null || used >= anchor.caps[family])
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function measureFamilyWindow(prev: ModelUsageState | null, account: string | null, session: UsageWindow | null, caps: Record<string, number>): Pick<ModelUsageState, "anchor" | "familyCosts"> {
|
|
308
|
-
const same = prev != null && prev.account === account;
|
|
309
|
-
const carried = same ? prev.familyCosts : undefined;
|
|
310
|
-
if (session == null) return { ...(same && prev.anchor ? { anchor: prev.anchor } : {}), ...(carried != null ? { familyCosts: carried } : {}) };
|
|
311
|
-
const held = same && prev.anchor && familyAnchorHolds(prev.anchor, session, caps) ? prev.anchor : null;
|
|
312
|
-
const anchor: FamilyAnchor = held ? { ...held, caps: { ...caps, ...held.caps } } : { fiveHourResetsAt: session.resetsAt, session: session.usedPercentage, caps };
|
|
313
|
-
const sessionDelta = session.usedPercentage - anchor.session;
|
|
314
|
-
const measured = Object.entries(caps).flatMap(([family, used]) => {
|
|
315
|
-
const capDelta = used - (anchor.caps[family] ?? used);
|
|
316
|
-
return sessionDelta >= SESSION_COST_MIN_DELTA && capDelta > 0 ? [[family, (100 * capDelta) / sessionDelta] as const] : [];
|
|
317
|
-
});
|
|
318
|
-
const familyCosts = { ...carried, ...Object.fromEntries(measured) };
|
|
319
|
-
return { anchor, ...(Object.keys(familyCosts).length > 0 ? { familyCosts } : {}) };
|
|
320
|
-
}
|
|
321
241
|
|
|
322
242
|
export function writeUsage(input: UsageState, opts: { stamp?: boolean } = {}): boolean {
|
|
323
243
|
const prev = loadUsage();
|
|
324
244
|
const stamp = opts.stamp === true;
|
|
325
245
|
const carriedSampleAt = prev != null && prev.account === input.account ? (prev.sampledAt ?? prev.ts) : undefined;
|
|
326
246
|
const sampledAt = stamp ? (carriedSampleAt ?? input.sampledAt) : input.ts;
|
|
327
|
-
const next: UsageState = { ...input, ...
|
|
247
|
+
const next: UsageState = { ...input, ...(sampledAt != null ? { sampledAt } : {}) };
|
|
328
248
|
if (
|
|
329
249
|
prev &&
|
|
330
250
|
isEqual({ ...prev, ts: 0, sampledAt: 0 }, { ...next, ts: 0, sampledAt: 0 }) &&
|
package/src/lib/types.ts
CHANGED
|
@@ -41,51 +41,22 @@ export type UsageWindows = z.infer<typeof UsageWindowsSchema>;
|
|
|
41
41
|
const ModelInfoSchema = z.object({ id: z.string(), display: z.string() });
|
|
42
42
|
export type ModelInfo = z.infer<typeof ModelInfoSchema>;
|
|
43
43
|
|
|
44
|
-
const WindowAnchorSchema = z.object({
|
|
45
|
-
fiveHourResetsAt: z.number().nullable(),
|
|
46
|
-
sevenDayResetsAt: z.number().nullable(),
|
|
47
|
-
session: z.number(),
|
|
48
|
-
weekly: z.number(),
|
|
49
|
-
});
|
|
50
|
-
export type WindowAnchor = z.infer<typeof WindowAnchorSchema>;
|
|
51
|
-
|
|
52
44
|
export const UsageStateSchema = UsageWindowsSchema.extend({
|
|
53
45
|
account: z.string().nullable(),
|
|
54
46
|
ts: z.number(),
|
|
55
47
|
model: ModelInfoSchema.nullable().default(null),
|
|
56
|
-
anchor: WindowAnchorSchema.optional(),
|
|
57
|
-
sessionWindowWeeklyCost: z.number().positive().optional(),
|
|
58
48
|
sampledAt: z.number().optional(),
|
|
59
49
|
});
|
|
60
50
|
export type UsageState = z.infer<typeof UsageStateSchema>;
|
|
61
51
|
|
|
62
|
-
const FamilyAnchorSchema = z.object({
|
|
63
|
-
fiveHourResetsAt: z.number().nullable(),
|
|
64
|
-
session: z.number(),
|
|
65
|
-
caps: z.record(z.string(), z.number()),
|
|
66
|
-
});
|
|
67
|
-
export type FamilyAnchor = z.infer<typeof FamilyAnchorSchema>;
|
|
68
|
-
|
|
69
52
|
export const ModelUsageStateSchema = z.object({
|
|
70
53
|
perModel: z.record(z.string(), UsageWindowSchema).default({}),
|
|
71
54
|
account: z.string().nullable(),
|
|
72
55
|
ts: z.number(),
|
|
73
56
|
sampledAt: z.number().optional(),
|
|
74
|
-
anchor: FamilyAnchorSchema.optional(),
|
|
75
|
-
familyCosts: z.record(z.string(), z.number()).optional(),
|
|
76
57
|
});
|
|
77
58
|
export type ModelUsageState = z.infer<typeof ModelUsageStateSchema>;
|
|
78
59
|
|
|
79
|
-
export const BankedResetOutcomeSchema = z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "rate_limited", "auth_error", "error"]);
|
|
80
|
-
export type BankedResetOutcome = z.infer<typeof BankedResetOutcomeSchema>;
|
|
81
|
-
|
|
82
|
-
export const BankedResetRecordSchema = z.object({
|
|
83
|
-
outcome: BankedResetOutcomeSchema,
|
|
84
|
-
at: z.number(),
|
|
85
|
-
nextAvailableAt: z.number().nullable(),
|
|
86
|
-
});
|
|
87
|
-
export type BankedResetRecord = z.infer<typeof BankedResetRecordSchema>;
|
|
88
|
-
|
|
89
60
|
export const AccountSchema = z.object({
|
|
90
61
|
accountUuid: z.string(),
|
|
91
62
|
email: z.string(),
|
|
@@ -102,8 +73,6 @@ export const AccountSchema = z.object({
|
|
|
102
73
|
needsReauth: z.boolean().optional(),
|
|
103
74
|
subscriptionType: z.string().optional(),
|
|
104
75
|
rateLimitTier: z.string().optional(),
|
|
105
|
-
sessionWindowWeeklyCost: z.number().positive().optional(),
|
|
106
|
-
bankedReset: BankedResetRecordSchema.optional(),
|
|
107
76
|
});
|
|
108
77
|
export type Account = z.infer<typeof AccountSchema>;
|
|
109
78
|
|
|
@@ -120,7 +89,6 @@ export const NextCheckSchema = z.object({ dueAt: z.number(), ts: z.number() });
|
|
|
120
89
|
|
|
121
90
|
export const EnforcedLimitSchema = z.object({
|
|
122
91
|
account: z.string(),
|
|
123
|
-
kind: z.enum(["session", "weekly", "model"]),
|
|
124
92
|
family: z.string().nullable(),
|
|
125
93
|
resetsAt: z.number().nullable(),
|
|
126
94
|
windowMs: z.number(),
|
|
@@ -145,10 +113,6 @@ export const ScreeningThresholdsSchema = z.object({
|
|
|
145
113
|
weekly: z.number().min(0).max(100),
|
|
146
114
|
});
|
|
147
115
|
|
|
148
|
-
export const BankedResetProviderSchema = z.enum(["claude", "codex"]);
|
|
149
|
-
export type BankedResetProvider = z.infer<typeof BankedResetProviderSchema>;
|
|
150
|
-
export const BankedResetProvidersSchema = z.array(BankedResetProviderSchema);
|
|
151
|
-
|
|
152
116
|
export const ConfigSchema = z
|
|
153
117
|
.object({
|
|
154
118
|
thresholds: ScreeningThresholdsSchema,
|
|
@@ -163,7 +127,6 @@ export const ConfigSchema = z
|
|
|
163
127
|
usagePollTtlMs: z.number().int().positive(),
|
|
164
128
|
maxWaitMs: z.number().int().positive(),
|
|
165
129
|
checkIntervalMs: z.number().int().min(10_000),
|
|
166
|
-
preferToUseBankedReset: BankedResetProvidersSchema,
|
|
167
130
|
}),
|
|
168
131
|
})
|
|
169
132
|
.refine((cfg) => cfg.policy.projectionMargin < Math.min(...cfg.thresholds.session, cfg.thresholds.weekly), {
|
|
@@ -181,7 +144,6 @@ export const RespawnMarkerSchema = z.object({
|
|
|
181
144
|
sessionId: z.string(),
|
|
182
145
|
prompt: z.string().optional(),
|
|
183
146
|
launchedAt: z.number().optional(),
|
|
184
|
-
reset: z.boolean().optional(),
|
|
185
147
|
});
|
|
186
148
|
|
|
187
149
|
export const RateLimitsStdinSchema = z.looseObject({
|
|
@@ -281,21 +243,9 @@ export const CodexUsageSchema = z.object({
|
|
|
281
243
|
planType: z.string().nullable(),
|
|
282
244
|
aggregate: z.array(CodexWindowSchema),
|
|
283
245
|
perLimit: z.record(z.string(), z.array(CodexWindowSchema)),
|
|
284
|
-
resetCredits: z.number().int().nullable(),
|
|
285
|
-
reachedType: z.string().nullable(),
|
|
286
246
|
});
|
|
287
247
|
export type CodexUsage = z.infer<typeof CodexUsageSchema>;
|
|
288
248
|
|
|
289
|
-
export const CodexBankedResetOutcomeSchema = z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed", "auth_error", "error"]);
|
|
290
|
-
export type CodexBankedResetOutcome = z.infer<typeof CodexBankedResetOutcomeSchema>;
|
|
291
|
-
|
|
292
|
-
export const CodexBankedResetRecordSchema = z.object({
|
|
293
|
-
outcome: CodexBankedResetOutcomeSchema,
|
|
294
|
-
at: z.number(),
|
|
295
|
-
key: z.string().optional(),
|
|
296
|
-
});
|
|
297
|
-
export type CodexBankedResetRecord = z.infer<typeof CodexBankedResetRecordSchema>;
|
|
298
|
-
|
|
299
249
|
const BareFileNameSchema = z
|
|
300
250
|
.string()
|
|
301
251
|
.refine((s) => s.length > 0 && s !== "." && s !== ".." && !s.includes("/") && !s.includes("\\"), {
|
|
@@ -314,12 +264,9 @@ export const CodexAccountSchema = z.object({
|
|
|
314
264
|
.object({
|
|
315
265
|
aggregate: z.array(CodexWindowSchema),
|
|
316
266
|
perLimit: z.record(z.string(), z.array(CodexWindowSchema)),
|
|
317
|
-
resetCredits: z.number().int().nullable().optional(),
|
|
318
|
-
reachedType: z.string().nullable().optional(),
|
|
319
267
|
})
|
|
320
268
|
.optional(),
|
|
321
269
|
lastUsageAt: z.number().optional(),
|
|
322
|
-
bankedReset: CodexBankedResetRecordSchema.optional(),
|
|
323
270
|
});
|
|
324
271
|
export type CodexAccount = z.infer<typeof CodexAccountSchema>;
|
|
325
272
|
|
package/agent-plugin/mcp.json
DELETED
package/agent-plugin/plugin.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
-
"name": "tokenmaxxing",
|
|
4
|
-
"version": "1.20.0",
|
|
5
|
-
"description": "Pool Claude Code and Codex logins, switch on pace pressure, and expose safe ops tools to agent clients.",
|
|
6
|
-
"author": {
|
|
7
|
-
"name": "anaclumos",
|
|
8
|
-
"url": "https://github.com/anaclumos"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/anaclumos/tokenmaxxing",
|
|
11
|
-
"repository": "https://github.com/anaclumos/tokenmaxxing",
|
|
12
|
-
"license": "MIT",
|
|
13
|
-
"keywords": [
|
|
14
|
-
"claude",
|
|
15
|
-
"codex",
|
|
16
|
-
"quota",
|
|
17
|
-
"account-switching",
|
|
18
|
-
"mcp"
|
|
19
|
-
]
|
|
20
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: codex-pool
|
|
3
|
-
description: Operate the Codex side of the pool (restart-is-switch, no Layer 2, never target a running sibling). Use for codex init/switch/status questions or MCP pool_switch with codex=true.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Codex pool
|
|
7
|
-
|
|
8
|
-
## Mechanics
|
|
9
|
-
|
|
10
|
-
- No hot-swap: restart IS the switch (`codex resume <sid>`). A credential write takes effect on the next codex start.
|
|
11
|
-
- Auto-switching needs trusted hooks (`/hooks`). Do not clobber the user's `notify` key in `config.toml`.
|
|
12
|
-
- Refresh-token reuse is punished; a superseded token can kill the grant family. Persist every rotation immediately.
|
|
13
|
-
- An account running in another supervised session is never a swap target and never sampler-refreshed. Parked does not mean idle.
|
|
14
|
-
- Classify windows by DURATION, never by position. Some plans have no 5h window.
|
|
15
|
-
- Layer 2 wall squeeze is Claude-only. Do not extend it to Codex.
|
|
16
|
-
|
|
17
|
-
## Agent actions
|
|
18
|
-
|
|
19
|
-
- Read: `pool_ls` / `pool_status` (Codex section included when present).
|
|
20
|
-
- Mutate: MCP `pool_switch` with `codex=true`, `confirm=true`, and `TOKENMAXXING_AGENT_MUTATIONS=1` after user approval.
|
|
21
|
-
- Interactive `init --codex` / `add --codex` / reauth stay human-driven.
|
|
22
|
-
|
|
23
|
-
See [references/codex.md](references/codex.md).
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: credentials-hygiene
|
|
3
|
-
description: Credential and identity rules for tokenmaxxing (fetchTokenIdentity account-uuid identity, flock, no blob compare, never print secrets). Use before any auth, swap, or credential-touching change.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Credentials hygiene
|
|
7
|
-
|
|
8
|
-
## Identity
|
|
9
|
-
|
|
10
|
-
- Identity is the `accountUuid` that `fetchTokenIdentity` reports for a token (`GET /api/oauth/profile`), never the organization, never a stored label, and never a blob byte compare. Every seat of a Team plan shares one `organizationUuid`, so an org-keyed lookup collapses the seats onto one account. Two rotations of one account differ byte-for-byte.
|
|
11
|
-
- Park a credential under its token's real owner. Commit the active label in the same critical section as credential writes.
|
|
12
|
-
- Every live-store write goes through `withClaudeRefreshLock` (Claude). Near-expiry sessions can rotate into the live store at any moment.
|
|
13
|
-
- A refresh rotation revokes the previous access token at once, so refreshing any copy of a credential that is live elsewhere kills those sessions' token.
|
|
14
|
-
|
|
15
|
-
## Isolation
|
|
16
|
-
|
|
17
|
-
- Refuse ambient `CLAUDE_CONFIG_DIR` / `CLAUDE_SECURESTORAGE_CONFIG_DIR` in CLI, SDK, and MCP.
|
|
18
|
-
- A missing namespaced keychain item never falls back to the live one.
|
|
19
|
-
- Same accounts pooled on several hosts race on refresh (documented limitation).
|
|
20
|
-
|
|
21
|
-
## Agent hard stops
|
|
22
|
-
|
|
23
|
-
- No tool may return keychain blobs, `.credentials.json`, `auth.json`, or OAuth access/refresh tokens.
|
|
24
|
-
- Do not bypass Claude `/usage` with direct `GET /api/oauth/usage` (owner-rejected; 429s on the active session).
|
|
25
|
-
- Prefer MCP read tools; never `cat` credential paths into the transcript.
|
|
26
|
-
|
|
27
|
-
See [references/credentials.md](references/credentials.md).
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: doctor-diagnostics
|
|
3
|
-
description: Diagnose a broken or incomplete tokenmaxxing install (supervisor, hooks, timer, credential identity). Use when doctor fails, PATH looks wrong, or identity drift is suspected. Prefer the MCP doctor tool.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Doctor and diagnostics
|
|
7
|
-
|
|
8
|
-
## Prefer MCP
|
|
9
|
-
|
|
10
|
-
Call `doctor`. It checks the supervisor wrapper, settings hooks, periodic timer, live/parked credential presence, and org identity match. Output is pass/fail plus labels.
|
|
11
|
-
|
|
12
|
-
Also useful: `config_get` (no key) for effective config sources, and `pool_ls` for needs-reauth.
|
|
13
|
-
|
|
14
|
-
## Hard stops
|
|
15
|
-
|
|
16
|
-
- Never dump keychain items, `.credentials.json`, `auth.json`, or OAuth tokens into chat.
|
|
17
|
-
- Doctor never refreshes tokens. Expired access tokens show as unverifiable identity, not as a prompt to paste secrets.
|
|
18
|
-
- Do not run `init` / `add` / `auth` from an agent session without the user present for interactive login.
|
|
19
|
-
|
|
20
|
-
## Common repairs (ask before mutating)
|
|
21
|
-
|
|
22
|
-
- Missing wrapper or hooks: user runs `tokenmaxxing init` (or `init --codex`).
|
|
23
|
-
- needs-reauth: user runs `tokenmaxxing auth <label>` (or `--all`).
|
|
24
|
-
- PATH: `binDir` must precede the real claude.
|
|
25
|
-
|
|
26
|
-
See [references/troubleshooting.md](references/troubleshooting.md).
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# Troubleshooting pointers
|
|
2
|
-
|
|
3
|
-
- `docs/content/docs/troubleshooting.mdx`
|
|
4
|
-
- `docs/content/docs/state-directory.mdx` for where state lives under `~/.config/tokenmaxxing` (or `TOKENMAXXING_HOME`)
|
|
5
|
-
- Ambient `CLAUDE_CONFIG_DIR` / `CLAUDE_SECURESTORAGE_CONFIG_DIR` must be unset for CLI, SDK, and this MCP server
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pool-status
|
|
3
|
-
description: Read the Claude and Codex account pool safely (list, usage bars, watch). Use when checking quota, which account is active, needs-reauth, or before any switch. Prefer MCP pool_ls and pool_status over raw shell.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Pool status (safe reads)
|
|
7
|
-
|
|
8
|
-
## Prefer MCP
|
|
9
|
-
|
|
10
|
-
- `pool_ls` for labels, active marker, needs-reauth
|
|
11
|
-
- `pool_status` for 5h / weekly / per-model bars (free `/usage` path)
|
|
12
|
-
|
|
13
|
-
If MCP is unavailable, run `tokenmaxxing ls --json` or `tokenmaxxing status --json` (alias `xx`) and parse the document. Never add `--ping`.
|
|
14
|
-
|
|
15
|
-
## Hard stops
|
|
16
|
-
|
|
17
|
-
- Do not run `status --ping` or bare `xx --ping`. That pings every account with a real request and opens each 5h window.
|
|
18
|
-
- Do not print credential files, keychain blobs, or OAuth tokens. Labels and status only.
|
|
19
|
-
- Do not kill sessions or supervisors to "free" an account.
|
|
20
|
-
|
|
21
|
-
## Notes
|
|
22
|
-
|
|
23
|
-
- Active Claude usage often comes from the statusLine tee; parked accounts are probed in isolation.
|
|
24
|
-
- `watch` re-renders status on an interval and never pings.
|
|
25
|
-
- Hermetic agents: set `TOKENMAXXING_HOME` to a throwaway directory. That isolates state files only: `init`, `uninstall`, and the codex hook install still write settings.json, codex `hooks.json`, the shell rc, and the timer unit under `HOME`, so never run them from an agent.
|
|
26
|
-
|
|
27
|
-
See [references/commands.md](references/commands.md).
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Status and list commands
|
|
2
|
-
|
|
3
|
-
- `tokenmaxxing` / `tokenmaxxing status`: sample and show bars; persists cached usage for the picker.
|
|
4
|
-
- `tokenmaxxing status --ping [--count N]`: DENIED for agents without explicit user approval. Meters every account, or N random ones under `--count`.
|
|
5
|
-
- `tokenmaxxing ls`: compact list.
|
|
6
|
-
- `tokenmaxxing watch [seconds]`: live re-render (default 120), never `--ping`.
|
|
7
|
-
- `--json` on any of these: one JSON document on stdout (`ok` mirrors the exit code); parse it instead of scraping the bars. `watch --json` prints one document per tick and never exits on its own.
|
|
8
|
-
|
|
9
|
-
Docs: `docs/content/docs/commands.mdx`.
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: safe-contribution
|
|
3
|
-
description: Safely change and ship tokenmaxxing (verification without a test suite, PR window, Mac vs Linux skew, public-repo hygiene). Use when editing this repo, opening PRs, or verifying decision-path changes.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Safe contribution
|
|
7
|
-
|
|
8
|
-
## Verify
|
|
9
|
-
|
|
10
|
-
- The repo carries no test code and no comments (owner ruling 2026-09-02). Never add tests, a test script, or comments.
|
|
11
|
-
- Verify with `bun run typecheck`, then hermetic CLI runs: `TOKENMAXXING_HOME=/tmp/xx-test bun run src/main.ts ...` (for decision-path changes, write accounts.json, usage.json, config.json, and a claude.json under that root and run `check`; point `TOKENMAXXING_KEYCHAIN_SERVICE` and `TOKENMAXXING_KEYCHAIN_ACCOUNT` at throwaway names and `TOKENMAXXING_CLAUDE_JSON` at that claude.json, because the CLI resolves claude.json from HOME, not from `TOKENMAXXING_HOME`).
|
|
12
|
-
- Ask before any run that meters real quota (`status --ping`, live-pool inference). Free `/usage` / plain status is fine.
|
|
13
|
-
|
|
14
|
-
## Ship
|
|
15
|
-
|
|
16
|
-
- Work on a branch; bump `package.json` and `agent-plugin/plugin.json` to the same version in the same PR; open PR; wait the full review window; handle every review; merge; `gh release create v<version>`; verify npm publish.
|
|
17
|
-
- A PR that changes no packed file skips the bump and the release, and merging it is shipping it. Check membership with `npm pack --dry-run`, not the `files` array: the tarball is `src`, `agent-plugin`, `README.md`, `DESIGN.md`, `LICENSE`, and `package.json`, the last two always packed though `files` never names them. So a `docs/`, `.memory/`, or AGENTS.md change is exempt, while a LICENSE or metadata change is not. Everything else, the full review window included, still applies.
|
|
18
|
-
- Never push directly to main. npm trusted publishing is bound to workflow filename `ci.yml`.
|
|
19
|
-
|
|
20
|
-
## Machine gotchas
|
|
21
|
-
|
|
22
|
-
- The owner's Mac may run this working tree live. Do not kill sessions or supervisors without asking.
|
|
23
|
-
- Linux boxes often run an older npm global: compare installed version before "works on Mac not Linux" debugging.
|
|
24
|
-
- Repo is PUBLIC: no Slack IDs, device info, or secrets in commits, PRs, docs, or `.memory`.
|
|
25
|
-
- Do not reintroduce banned patterns listed in `AGENTS.md` (Stop text-sniff failsafe, rejected statusline formats, old-state shims).
|
|
26
|
-
|
|
27
|
-
See [references/ship.md](references/ship.md).
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sdk-pairing
|
|
3
|
-
description: Pair tokenmaxxing with the Claude Agent SDK (ensureBestAccount, pooledOptions, stopHookCheck). Use when building or debugging Bun agents that spawn Claude through the pooled credential store.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# SDK pairing
|
|
7
|
-
|
|
8
|
-
Import from the package root (`tokenmaxxing` → `src/sdk.ts`). Bun only.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
-
import { ensureBestAccount, pooledOptions, stopHookCheck } from "tokenmaxxing";
|
|
13
|
-
|
|
14
|
-
await ensureBestAccount();
|
|
15
|
-
for await (const message of query({
|
|
16
|
-
prompt: "...",
|
|
17
|
-
options: {
|
|
18
|
-
...pooledOptions(),
|
|
19
|
-
hooks: { Stop: [{ hooks: [stopHookCheck] }] },
|
|
20
|
-
},
|
|
21
|
-
})) {
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
The loop body receives the message stream; keep the session id from the init message for `resume` across swaps.
|
|
26
|
-
|
|
27
|
-
## Rules
|
|
28
|
-
|
|
29
|
-
- Call `ensureBestAccount()` before each `query()` spawn (no mid-query hot-swap).
|
|
30
|
-
- `pooledOptions()` pins the real claude binary and a full replacement env with credential overrides scrubbed. It throws if `CLAUDE_CONFIG_DIR` or `CLAUDE_SECURESTORAGE_CONFIG_DIR` is set.
|
|
31
|
-
- `stopHookCheck` re-decides at turn boundaries; errors are swallowed so a broken check does not abort the turn.
|
|
32
|
-
- Own-accounts only (see terms docs). Offering pooled subscription logins to third parties is a ToS problem.
|
|
33
|
-
|
|
34
|
-
See [references/sdk.md](references/sdk.md).
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: switching-policy
|
|
3
|
-
description: Explain and apply tokenmaxxing switch policy (greedy vs hard path, pace pressure, Layer 2 wall squeeze Claude-only, model-family matching). Use before pool_switch, pool_check, or when changing thresholds.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Switching policy
|
|
7
|
-
|
|
8
|
-
## Vocabulary
|
|
9
|
-
|
|
10
|
-
- **Engaged**: session used >= `policy.greedySessionFloor` (default 80) or any screening bar crossed.
|
|
11
|
-
- **GREEDY path** permits lateral moves only within the incumbent organization and retains `greedySwapMargin` hysteresis.
|
|
12
|
-
- **HARD path** starts at a crossed screening bar and may cross organizations, with Layer 2 as its fallback.
|
|
13
|
-
- **Organization preference** requires measured headroom in session, aggregate weekly and gated model windows under the [policy](references/policy.md).
|
|
14
|
-
- **Verification** attempts at most two stale candidates per evaluation, with a 12-second deadline covering identity reads and the CLI for each candidate.
|
|
15
|
-
- **Probe results** are persisted and re-ranked, while screening exhaustion remains eligible for the wall squeeze and reset selection.
|
|
16
|
-
- **Unavailable verification** leaves cached figures in force, and verification never spends a parked refresh grant.
|
|
17
|
-
- **Manual switch** retains pure pace ranking with no organization preference or incumbent margin.
|
|
18
|
-
- **Pace pressure**: remaining weekly percent / time to weekly reset (highest first). Not most-remaining.
|
|
19
|
-
- **Effective bars**: `effectiveBars(cfg, pool)` = the active rung of the 5h ladder (`thresholds.session`, default `[90]`, a single rung: the lowest rung some pooled account, the current one included, still clears) and the weekly bar, each minus `policy.projectionMargin`. Trigger and screening must share these bars or swaps ping-pong. Codex reads `terminalBars(cfg)`, the top rung only. The check cadence is capped one band per rung climbed, and every band is a multiple of the tick `policy.checkIntervalMs` (default 60000).
|
|
20
|
-
- **Banked reset**: opt-in via `policy.preferToUseBankedReset` (a provider list, default `[]`), hard path only. Claude: the seat holds between the session rung and the wall while a reset is believed available and the weekly windows have room for one more measured session window (`sessionWindowWeeklyCost`; unmeasured swaps), claims the `/limit-reset` server call at the wall, and keeps the seat on `reset` (the failed turn is retriggered in place). Codex: rides to the wall while a reset credit exists, consumes one, no restart. The greedy path never resets.
|
|
21
|
-
|
|
22
|
-
## Layer 2 (Claude only)
|
|
23
|
-
|
|
24
|
-
When the hard path finds no usable target, judge against the wall (`hardThresholds` minus margin). Under-wall seat HOLDS; walled seat swaps to best under-wall sibling. Codex has no Layer 2 last-drop swap (cannot hot-adopt).
|
|
25
|
-
|
|
26
|
-
## Model matching
|
|
27
|
-
|
|
28
|
-
Match model families by exact token after splitting id/display on spaces, dots, and hyphens (`familyTokens` / `matchedFamily` in `src/lib/usage.ts`). Never exact full display strings (names drift: "Fable" / "Fable 5"). Unmeasured usage is unknown and ranks last, never 0 / first. Per-model weekly caps: Sonnet and Fable exist; only Fable gates a switch by default (`policy.switchModels`).
|
|
29
|
-
|
|
30
|
-
## Agent actions
|
|
31
|
-
|
|
32
|
-
- Explain with this skill; mutate only via MCP `pool_switch` / `pool_check` with user approval, `confirm=true`, and `TOKENMAXXING_AGENT_MUTATIONS=1`.
|
|
33
|
-
- Do not reintroduce Stop-hook text-sniffing limit failsafes.
|
|
34
|
-
|
|
35
|
-
See [references/policy.md](references/policy.md).
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# Policy sources
|
|
2
|
-
|
|
3
|
-
- `docs/content/docs/switching.mdx`
|
|
4
|
-
- `src/lib/decide.ts`, `src/lib/picker.ts`
|
|
5
|
-
- `src/lib/bankedreset.ts`, `src/lib/codexreset.ts` (banked limit resets)
|
|
6
|
-
- `.cursor/rules/switch-verification.md`
|
|
7
|
-
|
|
8
|
-
Automatic organization preference requires session usage below `greedySessionFloor` and more than `max(1, session bar - greedySessionFloor)` points below every applicable bar.
|
|
9
|
-
|
|
10
|
-
The exact policy, defaults, verification limits and manual-switch behavior live in `docs/content/docs/switching.mdx`.
|