tokenmaxxing 1.56.0 → 1.58.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 +7 -7
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli/auth.ts +6 -17
- package/src/cli/check.ts +2 -2
- package/src/cli/cloudrun.ts +12 -19
- package/src/cli/config.ts +2 -1
- package/src/cli/doctor.ts +5 -4
- package/src/cli/init.ts +0 -1
- package/src/cli/render.ts +24 -9
- package/src/cli/rm.ts +0 -4
- package/src/cli/status.ts +34 -57
- package/src/entries/codexstophook.ts +10 -23
- package/src/entries/codexsupervisor.ts +30 -72
- package/src/entries/statusline.ts +17 -26
- package/src/entries/stopfailurehook.ts +4 -5
- package/src/entries/stophook.ts +12 -10
- package/src/entries/subagentstatusline.ts +4 -10
- package/src/entries/supervisor.ts +91 -116
- package/src/lib/claude.ts +11 -12
- package/src/lib/claudebin.ts +3 -2
- package/src/lib/claudelock.ts +2 -2
- package/src/lib/cloud.ts +4 -8
- package/src/lib/codex.ts +3 -3
- package/src/lib/codexauth.ts +2 -11
- package/src/lib/codexbin.ts +2 -1
- package/src/lib/codexoauth.ts +3 -8
- package/src/lib/codexusage.ts +5 -5
- package/src/lib/compact.ts +5 -18
- package/src/lib/credstore.ts +1 -6
- package/src/lib/decide.ts +6 -24
- package/src/lib/grok.ts +38 -152
- package/src/lib/http.ts +1 -2
- package/src/lib/install.ts +20 -38
- package/src/lib/keychain.ts +1 -4
- package/src/lib/log.ts +5 -5
- package/src/lib/oauth.ts +6 -5
- package/src/lib/opencodego.ts +31 -140
- package/src/lib/paths.ts +6 -22
- package/src/lib/picker.ts +9 -20
- package/src/lib/presence.ts +4 -5
- package/src/lib/proc.ts +22 -0
- package/src/lib/sample.ts +25 -24
- package/src/lib/sessions.ts +5 -6
- package/src/lib/settings.ts +1 -8
- package/src/lib/state.ts +6 -29
- package/src/lib/statusonly.ts +143 -0
- package/src/lib/supervise.ts +111 -0
- package/src/lib/types.ts +27 -36
- package/src/lib/update.ts +6 -7
- package/src/lib/usage.ts +40 -191
- package/src/main.ts +5 -5
- package/src/entries/sessionstart.ts +0 -33
package/src/lib/codexusage.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { http, safeErrorDetail } from "./http.ts";
|
|
3
|
+
import { errorMessage } from "./log.ts";
|
|
3
4
|
import { env } from "./paths.ts";
|
|
4
|
-
import {
|
|
5
|
+
import { JsonTextSchema, type CodexAuthJson, type CodexUsage, type Window } from "./types.ts";
|
|
5
6
|
import { codexIdentityOf } from "./codexauth.ts";
|
|
6
7
|
import { familyTokens } from "./usage.ts";
|
|
7
8
|
|
|
@@ -63,8 +64,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
63
64
|
},
|
|
64
65
|
});
|
|
65
66
|
} catch (e) {
|
|
66
|
-
|
|
67
|
-
throw new CodexUsageReadError(`endpoint unreachable: ${message}`);
|
|
67
|
+
throw new CodexUsageReadError(`endpoint unreachable: ${errorMessage(e)}`);
|
|
68
68
|
}
|
|
69
69
|
const text = await res.text();
|
|
70
70
|
if (!res.ok) {
|
|
@@ -76,7 +76,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
76
76
|
}
|
|
77
77
|
const wire = parsed.data;
|
|
78
78
|
|
|
79
|
-
return
|
|
79
|
+
return {
|
|
80
80
|
accountId: wire.account_id,
|
|
81
81
|
email: wire.email ?? null,
|
|
82
82
|
planType: wire.plan_type ?? null,
|
|
@@ -84,7 +84,7 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson; at: number }
|
|
|
84
84
|
...toWindows(wire.rate_limit, null, at),
|
|
85
85
|
...(wire.additional_rate_limits ?? []).flatMap((row) => toWindows(row.rate_limit, row.limit_name, at)),
|
|
86
86
|
],
|
|
87
|
-
}
|
|
87
|
+
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
const LIMIT_LABEL_ABBREVIATIONS = new Map([["reserve", "rsrv"]]);
|
package/src/lib/compact.ts
CHANGED
|
@@ -2,14 +2,11 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { delay } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { log } from "./log.ts";
|
|
5
|
+
import { errorMessage, log } from "./log.ts";
|
|
6
|
+
import { readLines } from "./proc.ts";
|
|
6
7
|
import { JsonTextSchema } from "./types.ts";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
z.object({ ok: z.literal(true) }),
|
|
10
|
-
z.object({ ok: z.literal(false), reason: z.string() }),
|
|
11
|
-
]);
|
|
12
|
-
export type CompactOutcome = z.infer<typeof CompactOutcomeSchema>;
|
|
9
|
+
export type CompactOutcome = { ok: true } | { ok: false; reason: string };
|
|
13
10
|
|
|
14
11
|
export const CLAUDE_COMPACT_KILL_MS = 300_000;
|
|
15
12
|
const PIPE_GRACE_MS = 2_000;
|
|
@@ -108,20 +105,10 @@ export async function compactCodexThread(input: { real: string; threadId: string
|
|
|
108
105
|
}
|
|
109
106
|
};
|
|
110
107
|
(async () => {
|
|
111
|
-
|
|
112
|
-
for await (const chunk of p.stdout) {
|
|
113
|
-
buffered += Buffer.from(chunk).toString("utf8");
|
|
114
|
-
let nl = buffered.indexOf("\n");
|
|
115
|
-
while (nl >= 0) {
|
|
116
|
-
onLine(buffered.slice(0, nl));
|
|
117
|
-
buffered = buffered.slice(nl + 1);
|
|
118
|
-
nl = buffered.indexOf("\n");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (buffered.trim() !== "") onLine(buffered);
|
|
108
|
+
for await (const line of readLines(p.stdout)) onLine(line);
|
|
122
109
|
const err = (await stderrText).trim().slice(0, 200);
|
|
123
110
|
finish({ ok: false, reason: p.exitCode === null ? `app-server killed after ${CODEX_COMPACT_KILL_MS / 1000}s` : `app-server exited ${p.exitCode}: ${err}` });
|
|
124
|
-
})().catch((e) => finish({ ok: false, reason:
|
|
111
|
+
})().catch((e) => finish({ ok: false, reason: errorMessage(e) }));
|
|
125
112
|
send({ id: INIT_ID, method: "initialize", params: { clientInfo: { name: "tokenmaxxing", version: packageVersion() } } });
|
|
126
113
|
});
|
|
127
114
|
|
package/src/lib/credstore.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
4
|
import * as kc from "./keychain.ts";
|
|
6
5
|
import { keychain as kcNames, namespacedCredService, storeDirFor } from "./paths.ts";
|
|
7
6
|
import { CredentialBlobSchema, ErrnoSchema, type OAuthCreds } from "./types.ts";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
z.object({ kind: z.literal("keychain"), service: z.string(), account: z.string() }),
|
|
11
|
-
z.object({ kind: z.literal("file"), path: z.string() }),
|
|
12
|
-
]);
|
|
13
|
-
export type CredTarget = z.infer<typeof CredTargetSchema>;
|
|
8
|
+
export type CredTarget = ({ kind: "keychain" } & kc.KeychainTarget) | { kind: "file"; path: string };
|
|
14
9
|
|
|
15
10
|
const darwin = process.platform === "darwin";
|
|
16
11
|
|
package/src/lib/decide.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { withLock } from "./lock.ts";
|
|
3
|
-
import {
|
|
2
|
+
import { loadAccounts, loadConfig, saveAccounts } from "./state.ts";
|
|
4
3
|
import { isExhausted, limitWindows, nextWeeklyReset, pickBest, pickEarliestReset, sessionWindow, thresholdBars, usableAt, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
4
|
import { familyTokens } from "./usage.ts";
|
|
6
|
-
import { log } from "./log.ts";
|
|
5
|
+
import { errorMessage, log } from "./log.ts";
|
|
7
6
|
import type { Observation, Provider } from "./provider.ts";
|
|
8
|
-
import {
|
|
7
|
+
import type { Account, EnforcedLimit } from "./types.ts";
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
swapped: z.boolean(),
|
|
12
|
-
account: AccountSchema.nullable(),
|
|
13
|
-
reason: z.string(),
|
|
14
|
-
waitUntil: z.number().optional(),
|
|
15
|
-
});
|
|
16
|
-
export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
|
|
9
|
+
export type SwapDecision = { swapped: boolean; account: Account | null; reason: string; waitUntil?: number };
|
|
17
10
|
|
|
18
11
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19
12
|
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
@@ -45,12 +38,6 @@ function enforcedWall(limit: EnforcedLimit, account: Account, now: number): numb
|
|
|
45
38
|
|
|
46
39
|
export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRespawn = false, enforced: EnforcedLimit | null = null, seatId: string | null = p.liveId()): Promise<SwapDecision> {
|
|
47
40
|
const activeId = seatId;
|
|
48
|
-
|
|
49
|
-
const lastSwapAt = loadLastSwapAt(p.pool);
|
|
50
|
-
if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
51
|
-
return { swapped: false, account: null, reason: "post-swap-cooldown" };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
41
|
const cfg = loadConfig();
|
|
55
42
|
const bars = thresholdBars(cfg);
|
|
56
43
|
const stored0 = loadAccounts(p.pool).accounts.find((a) => a.id === activeId);
|
|
@@ -63,10 +50,6 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
63
50
|
}
|
|
64
51
|
|
|
65
52
|
return withLock(p.pool.lockFile, async () => {
|
|
66
|
-
const lastSwapAt2 = loadLastSwapAt(p.pool);
|
|
67
|
-
if (!enforced && lastSwapAt2 != null && now - lastSwapAt2 < POST_SWAP_COOLDOWN_MS) {
|
|
68
|
-
return { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
69
|
-
}
|
|
70
53
|
const idx = loadAccounts(p.pool);
|
|
71
54
|
const id2 = seatId;
|
|
72
55
|
const active = id2 ? idx.accounts.find((a) => a.id === id2) : undefined;
|
|
@@ -113,15 +96,14 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
113
96
|
return { swapped: false, account: null, reason: "needs-respawn" };
|
|
114
97
|
}
|
|
115
98
|
|
|
116
|
-
const seatOf = (cur: {
|
|
117
|
-
cur.accounts.find((a) => a.id === id2) ?? cur.accounts.find((a) => a.id === cur.activeId) ?? null;
|
|
99
|
+
const seatOf = (cur: { accounts: Account[] }): Account | null => cur.accounts.find((a) => a.id === id2) ?? null;
|
|
118
100
|
|
|
119
101
|
const rejected = new Set<string>();
|
|
120
102
|
const usable = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (p.seats === "shared" || a.id === id2 || !present.has(a.id)));
|
|
121
103
|
const skipOrThrow = (e: unknown, candidate: Account): void => {
|
|
122
104
|
if (p.classifySwapError(e) === "fatal") throw e;
|
|
123
105
|
rejected.add(candidate.id);
|
|
124
|
-
log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error:
|
|
106
|
+
log("decide.candidate_rejected", { account: candidate.id.slice(0, 8), error: errorMessage(e) });
|
|
125
107
|
};
|
|
126
108
|
while (true) {
|
|
127
109
|
const cur = loadAccounts(p.pool);
|
package/src/lib/grok.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import { grokAuthJsonFor, grokPaths, grokPool, grokSeatFromEnv, grokStoreDirFor } from "./paths.ts";
|
|
6
|
-
import type {
|
|
7
|
-
import { loadAccounts,
|
|
8
|
-
import type
|
|
6
|
+
import type { Provider } from "./provider.ts";
|
|
7
|
+
import { loadAccounts, type Harvest } from "./state.ts";
|
|
8
|
+
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
9
9
|
import { c } from "../cli/render.ts";
|
|
10
10
|
|
|
11
11
|
const GrokAuthEntrySchema = z.looseObject({
|
|
@@ -18,85 +18,10 @@ const GrokAuthEntrySchema = z.looseObject({
|
|
|
18
18
|
});
|
|
19
19
|
type GrokAuthEntry = z.infer<typeof GrokAuthEntrySchema>;
|
|
20
20
|
|
|
21
|
-
function
|
|
22
|
-
let raw: unknown;
|
|
23
|
-
try {
|
|
24
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
25
|
-
} catch {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
const map = z.record(z.string(), z.unknown()).safeParse(raw);
|
|
29
|
-
if (!map.success) return [];
|
|
30
|
-
const out: { key: string; entry: GrokAuthEntry }[] = [];
|
|
31
|
-
for (const [key, value] of Object.entries(map.data)) {
|
|
32
|
-
const parsed = GrokAuthEntrySchema.safeParse(value);
|
|
33
|
-
if (parsed.success) out.push({ key, entry: parsed.data });
|
|
34
|
-
}
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function identityOf(key: string, entry: GrokAuthEntry): { id: string; email: string | null } {
|
|
39
|
-
const id = entry.user_id ?? entry.principal_id ?? key;
|
|
40
|
-
return { id, email: entry.email ?? null };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveRealGrok(): string {
|
|
44
|
-
const cfg = loadConfig();
|
|
45
|
-
if (cfg.grokBin) {
|
|
46
|
-
if (!existsSync(cfg.grokBin)) throw new Error(`configured grokBin does not exist: ${cfg.grokBin} - fix config.json`);
|
|
47
|
-
return cfg.grokBin;
|
|
48
|
-
}
|
|
49
|
-
for (const d of (process.env.PATH ?? "").split(":")) {
|
|
50
|
-
if (!d) continue;
|
|
51
|
-
const cand = join(d, "grok");
|
|
52
|
-
try {
|
|
53
|
-
if (existsSync(cand)) return cand;
|
|
54
|
-
} catch {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
throw new Error("could not locate the real `grok` binary (set grokBin in config.json)");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function liveId(): string | null {
|
|
62
|
-
return grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function presence(): Map<string, number> {
|
|
66
|
-
return new Map();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function observeLive(account: Account): Promise<Observation | null> {
|
|
70
|
-
return account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
|
|
74
|
-
const reports = new Map<string, SampleReport>();
|
|
75
|
-
for (const a of accounts) {
|
|
76
|
-
reports.set(a.id, (await storeUsable(a)) ? { ok: true, source: "probe" } : { ok: false, reason: "no usable credential in this account's store - run `tokenmaxxing auth --grok`" });
|
|
77
|
-
}
|
|
78
|
-
return reports;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function storeUsable(a: Account): Promise<boolean> {
|
|
82
|
-
try {
|
|
83
|
-
const entries = parseGrokAuthFile(grokAuthJsonFor(a.id));
|
|
84
|
-
return entries.some((e) => identityOf(e.key, e.entry).id === a.id && (e.entry.refresh_token ?? "") !== "");
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function removeCredentials(a: Account): Promise<void> {
|
|
91
|
-
rmSync(grokStoreDirFor(a.id), { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
|
|
95
|
-
const { id, email } = identityOf(key, entry);
|
|
96
|
-
if (!id) return null;
|
|
21
|
+
function harvestOf(id: string, key: string, entry: GrokAuthEntry): Harvest {
|
|
97
22
|
return {
|
|
98
23
|
id,
|
|
99
|
-
email,
|
|
24
|
+
email: entry.email ?? null,
|
|
100
25
|
tier: null,
|
|
101
26
|
sample: null,
|
|
102
27
|
park: async () => {
|
|
@@ -106,83 +31,44 @@ function harvestOf(key: string, entry: GrokAuthEntry): Harvest | null {
|
|
|
106
31
|
};
|
|
107
32
|
}
|
|
108
33
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const onboardDir = grokPaths.onboardDir;
|
|
112
|
-
rmSync(onboardDir, { recursive: true, force: true });
|
|
113
|
-
mkdirSync(onboardDir, { recursive: true });
|
|
114
|
-
const p = Bun.spawn([real, "login"], {
|
|
115
|
-
stdin: "inherit",
|
|
116
|
-
stdout: "inherit",
|
|
117
|
-
stderr: "inherit",
|
|
118
|
-
env: { ...process.env, GROK_HOME: onboardDir, TOKENMAXXING_PROBE: "1" },
|
|
119
|
-
});
|
|
120
|
-
await p.exited;
|
|
34
|
+
function readAuth(path: string): AuthEntry[] {
|
|
35
|
+
let raw: unknown;
|
|
121
36
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
const entries = parseGrokAuthFile(join(onboardDir, "auth.json"));
|
|
127
|
-
if (entries.length === 0) {
|
|
128
|
-
console.error(c.red("no grok login landed in the isolated home - nothing added."));
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
return harvestOf(entries[0]!.key, entries[0]!.entry);
|
|
132
|
-
} finally {
|
|
133
|
-
rmSync(onboardDir, { recursive: true, force: true });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function importLive(): Promise<Harvest | null> {
|
|
138
|
-
console.log(c.cyan("Opening an isolated grok login for your first pooled account - the login you already have stays as it is."));
|
|
139
|
-
console.log();
|
|
140
|
-
const live = parseGrokAuthFile(join(grokPaths.home, "auth.json"));
|
|
141
|
-
if (live.length > 0) {
|
|
142
|
-
console.log(c.dim(`found ${live.length} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`));
|
|
143
|
-
const h = harvestOf(live[0]!.key, live[0]!.entry);
|
|
144
|
-
if (h) return h;
|
|
37
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
145
40
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
41
|
+
const map = z.record(z.string(), z.unknown()).safeParse(raw);
|
|
42
|
+
if (!map.success) return [];
|
|
43
|
+
const out: AuthEntry[] = [];
|
|
44
|
+
for (const [key, value] of Object.entries(map.data)) {
|
|
45
|
+
const parsed = GrokAuthEntrySchema.safeParse(value);
|
|
46
|
+
if (!parsed.success) continue;
|
|
47
|
+
const entry = parsed.data;
|
|
48
|
+
const id = entry.user_id ?? entry.principal_id ?? key;
|
|
49
|
+
out.push({ id, usable: (entry.refresh_token ?? "") !== "", harvest: id === "" ? null : harvestOf(id, key, entry) });
|
|
155
50
|
}
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function install(): void {
|
|
160
|
-
console.log(c.dim("grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts."));
|
|
51
|
+
return out;
|
|
161
52
|
}
|
|
162
53
|
|
|
163
|
-
export const grok: Provider = {
|
|
54
|
+
export const grok: Provider = statusOnlyProvider({
|
|
164
55
|
name: "grok",
|
|
165
56
|
flag: " --grok",
|
|
166
57
|
pool: grokPool,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
login,
|
|
183
|
-
importLive,
|
|
184
|
-
preflight,
|
|
185
|
-
install,
|
|
58
|
+
binName: "grok",
|
|
59
|
+
binKey: "grokBin",
|
|
60
|
+
storeDirFor: grokStoreDirFor,
|
|
61
|
+
authJsonFor: grokAuthJsonFor,
|
|
62
|
+
onboardDir: grokPaths.onboardDir,
|
|
63
|
+
homeEnv: "GROK_HOME",
|
|
64
|
+
loginArgs: ["login"],
|
|
65
|
+
onboardAuthRel: "auth.json",
|
|
66
|
+
liveAuthPath: () => join(grokPaths.home, "auth.json"),
|
|
67
|
+
readAuth,
|
|
68
|
+
liveId: () => grokSeatFromEnv(loadAccounts(grokPool).accounts.map((a) => a.id)),
|
|
69
|
+
versionOk: (out) => out.trim().toLowerCase().includes("grok"),
|
|
70
|
+
importIntro: "Opening an isolated grok login for your first pooled account - the login you already have stays as it is.",
|
|
71
|
+
foundLive: (count) => `found ${count} grok login(s) in ${grokPaths.home} - pooling the first; use \`tokenmaxxing add --grok\` for the rest.`,
|
|
72
|
+
installNotice: "grok pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled grok accounts.",
|
|
186
73
|
loginStep: () => `Sign in in the browser session that opens (or run ${c.bold("grok login --device-auth")} on headless hosts first).`,
|
|
187
|
-
|
|
188
|
-
};
|
|
74
|
+
});
|
package/src/lib/http.ts
CHANGED
|
@@ -17,8 +17,7 @@ const PlainErrorBodySchema = z.looseObject({
|
|
|
17
17
|
message: z.string().optional(),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
type ErrorDetail = z.infer<typeof ErrorDetailSchema>;
|
|
20
|
+
type ErrorDetail = { code: string | null; fields: string[] };
|
|
22
21
|
|
|
23
22
|
function parseErrorBody(text: string): ErrorDetail | null {
|
|
24
23
|
const json = JsonTextSchema.safeParse(text).data;
|
package/src/lib/install.ts
CHANGED
|
@@ -2,21 +2,14 @@ import { accessSync, appendFileSync, constants, existsSync, mkdirSync, readFileS
|
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { escape } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { codexPaths, codexStoreDirFor, HOME, paths } from "./paths.ts";
|
|
5
|
+
import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.ts";
|
|
6
6
|
import { writeFileAtomic } from "./atomic.ts";
|
|
7
7
|
import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
|
|
8
8
|
import { resolveRealClaude } from "./claudebin.ts";
|
|
9
9
|
import { loadConfig } from "./state.ts";
|
|
10
10
|
import { ErrnoSchema } from "./types.ts";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
claudeWrapper: z.string(),
|
|
14
|
-
installedBin: z.string(),
|
|
15
|
-
pathAhead: z.boolean(),
|
|
16
|
-
timerLoaded: z.boolean(),
|
|
17
|
-
checkIntervalS: z.number().int().positive(),
|
|
18
|
-
});
|
|
19
|
-
export type InstallOutcome = z.infer<typeof InstallOutcomeSchema>;
|
|
12
|
+
export type InstallOutcome = { claudeWrapper: string; installedBin: string; pathAhead: boolean; timerLoaded: boolean; checkIntervalS: number };
|
|
20
13
|
|
|
21
14
|
export function isBinDirAhead(): boolean {
|
|
22
15
|
const dirs = (process.env.PATH ?? "").split(":");
|
|
@@ -31,10 +24,10 @@ export function isBinDirAhead(): boolean {
|
|
|
31
24
|
}
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
const
|
|
27
|
+
const envFlag = (name: string): boolean => z.stringbool().optional().parse(optionalEnv(name)) === true;
|
|
35
28
|
|
|
36
29
|
export function isNixPackaged(): boolean {
|
|
37
|
-
if (
|
|
30
|
+
if (envFlag("TOKENMAXXING_NIX")) return true;
|
|
38
31
|
try {
|
|
39
32
|
return realpathSync(Bun.main).startsWith("/nix/store/");
|
|
40
33
|
} catch {
|
|
@@ -43,7 +36,7 @@ export function isNixPackaged(): boolean {
|
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
export function skipImperativeTimer(): boolean {
|
|
46
|
-
return
|
|
39
|
+
return envFlag("TOKENMAXXING_SKIP_TIMER");
|
|
47
40
|
}
|
|
48
41
|
|
|
49
42
|
function isNixStorePath(path: string): boolean {
|
|
@@ -51,7 +44,7 @@ function isNixStorePath(path: string): boolean {
|
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
function cannotWriteRcTarget(target: string): boolean {
|
|
54
|
-
if (
|
|
47
|
+
if (envFlag("TOKENMAXXING_SKIP_SHELL_RC")) return true;
|
|
55
48
|
if (isNixStorePath(target)) return true;
|
|
56
49
|
if (!existsSync(target)) return false;
|
|
57
50
|
try {
|
|
@@ -177,26 +170,21 @@ export function codexStopHookGroupIndex(): number | null {
|
|
|
177
170
|
return idx >= 0 ? idx : null;
|
|
178
171
|
}
|
|
179
172
|
|
|
173
|
+
const CodexHookStateSchema = z.looseObject({
|
|
174
|
+
hooks: z.looseObject({ state: z.record(z.string(), z.looseObject({ trusted_hash: z.unknown().optional() })).optional() }).optional(),
|
|
175
|
+
});
|
|
176
|
+
|
|
180
177
|
export function codexStoreHookTrust(accountId: string): "trusted" | "untrusted" | "unknown" {
|
|
181
178
|
const group = codexStopHookGroupIndex();
|
|
182
179
|
if (group == null) return "unknown";
|
|
183
180
|
const key = `${join(codexStoreDirFor(accountId), "hooks.json")}:stop:${group}:0`;
|
|
184
|
-
let
|
|
181
|
+
let config: z.infer<typeof CodexHookStateSchema>;
|
|
185
182
|
try {
|
|
186
|
-
|
|
183
|
+
config = CodexHookStateSchema.parse(Bun.TOML.parse(readFileSync(join(codexPaths.home, "config.toml"), "utf8")));
|
|
187
184
|
} catch {
|
|
188
185
|
return "unknown";
|
|
189
186
|
}
|
|
190
|
-
|
|
191
|
-
for (const line of text.split("\n")) {
|
|
192
|
-
const trimmed = line.trim();
|
|
193
|
-
if (trimmed.startsWith("[")) {
|
|
194
|
-
inBlock = trimmed.startsWith("[hooks.state.") && trimmed.includes(key);
|
|
195
|
-
} else if (inBlock && trimmed.startsWith("trusted_hash")) {
|
|
196
|
-
return "trusted";
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return "untrusted";
|
|
187
|
+
return config.hooks?.state?.[key]?.trusted_hash !== undefined ? "trusted" : "untrusted";
|
|
200
188
|
}
|
|
201
189
|
|
|
202
190
|
export function codexSupervisorLink(): string {
|
|
@@ -375,8 +363,8 @@ function uninstallCheckTimer(live: boolean): TimerOutcome {
|
|
|
375
363
|
}
|
|
376
364
|
|
|
377
365
|
export function shellRcPath(): string | null {
|
|
378
|
-
const override =
|
|
379
|
-
if (override
|
|
366
|
+
const override = optionalEnv("TOKENMAXXING_SHELL_RC");
|
|
367
|
+
if (override != null) return override;
|
|
380
368
|
const shell = basename(process.env.SHELL ?? "");
|
|
381
369
|
if (shell === "zsh") return join(process.env.ZDOTDIR || HOME, ".zshrc");
|
|
382
370
|
if (shell === "bash") return join(HOME, ".bashrc");
|
|
@@ -417,12 +405,7 @@ export function ensurePathInRc(rc: string): "added" | "present" | "skipped" {
|
|
|
417
405
|
return "added";
|
|
418
406
|
}
|
|
419
407
|
|
|
420
|
-
|
|
421
|
-
kind: z.enum(["shadow", "bypass"]),
|
|
422
|
-
name: z.string(),
|
|
423
|
-
line: z.string(),
|
|
424
|
-
});
|
|
425
|
-
export type ShellShadower = z.infer<typeof ShellShadowerSchema>;
|
|
408
|
+
export type ShellShadower = { kind: "shadow" | "bypass"; name: string; line: string };
|
|
426
409
|
|
|
427
410
|
export function findClaudeShadowers(rcText: string): ShellShadower[] {
|
|
428
411
|
const out: ShellShadower[] = [];
|
|
@@ -433,14 +416,14 @@ export function findClaudeShadowers(rcText: string): ShellShadower[] {
|
|
|
433
416
|
const alias = line.match(/^alias\s+([A-Za-z0-9_-]+)=(.*)$/);
|
|
434
417
|
if (alias) {
|
|
435
418
|
if (alias[1] === "claude") {
|
|
436
|
-
out.push(
|
|
419
|
+
out.push({ kind: "shadow", name: "claude", line });
|
|
437
420
|
} else if (absClaude.test(alias[2]!)) {
|
|
438
|
-
out.push(
|
|
421
|
+
out.push({ kind: "bypass", name: alias[1]!, line });
|
|
439
422
|
}
|
|
440
423
|
continue;
|
|
441
424
|
}
|
|
442
425
|
if (/^(?:function\s+)?claude\s*\(\)/.test(line)) {
|
|
443
|
-
out.push(
|
|
426
|
+
out.push({ kind: "shadow", name: "claude", line });
|
|
444
427
|
}
|
|
445
428
|
}
|
|
446
429
|
return out;
|
|
@@ -462,8 +445,7 @@ export function removePathFromRc(rc: string): boolean {
|
|
|
462
445
|
return true;
|
|
463
446
|
}
|
|
464
447
|
|
|
465
|
-
|
|
466
|
-
export type UninstallOutcome = z.infer<typeof UninstallOutcomeSchema>;
|
|
448
|
+
export type UninstallOutcome = { timer: "removed" | "skipped" | "still-loaded"; pathLineRemoved: boolean };
|
|
467
449
|
|
|
468
450
|
export function loginHome(): string {
|
|
469
451
|
const cmd = process.platform === "darwin" ? ["id", "-P"] : ["getent", "passwd", String(process.getuid?.() ?? "")];
|
package/src/lib/keychain.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const KeychainTargetSchema = z.object({ service: z.string(), account: z.string() });
|
|
4
|
-
export type KeychainTarget = z.infer<typeof KeychainTargetSchema>;
|
|
1
|
+
export type KeychainTarget = { service: string; account: string };
|
|
5
2
|
|
|
6
3
|
const SECURITY = "/usr/bin/security";
|
|
7
4
|
const INTERACTIVE_MAX_LINE = 4000;
|
package/src/lib/log.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, renameSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { z } from "zod";
|
|
4
3
|
import { paths } from "./paths.ts";
|
|
5
4
|
|
|
6
5
|
const LOG_MAX_BYTES = 5_000_000;
|
|
7
6
|
|
|
7
|
+
export function errorMessage(e: unknown): string {
|
|
8
|
+
return e instanceof Error ? e.message : String(e);
|
|
9
|
+
}
|
|
10
|
+
|
|
8
11
|
function redact(s: string): string {
|
|
9
12
|
return s
|
|
10
13
|
.replace(/\b(sk-ant-[A-Za-z0-9._-]{6,})/g, "sk-ant-***")
|
|
@@ -22,10 +25,7 @@ export function log(event: string, fields: Record<string, unknown> = {}): void {
|
|
|
22
25
|
try {
|
|
23
26
|
line = Object.entries(fields)
|
|
24
27
|
.filter(([, v]) => v !== undefined)
|
|
25
|
-
.map(([k, v]) => {
|
|
26
|
-
const str = z.string().safeParse(v);
|
|
27
|
-
return `${k}=${redact(str.success ? str.data : JSON.stringify(v))}`;
|
|
28
|
-
})
|
|
28
|
+
.map(([k, v]) => `${k}=${redact(typeof v === "string" ? v : JSON.stringify(v))}`)
|
|
29
29
|
.join(" ");
|
|
30
30
|
mkdirSync(dirname(paths.logFile), { recursive: true });
|
|
31
31
|
if (existsSync(paths.logFile) && statSync(paths.logFile).size > LOG_MAX_BYTES) {
|
package/src/lib/oauth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { http, safeErrorDetail } from "./http.ts";
|
|
2
2
|
import { env } from "./paths.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { errorMessage } from "./log.ts";
|
|
4
|
+
import { JsonTextSchema, ProfileResponseSchema, type OAuthCreds, type TokenIdentity } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
const PROFILE_URL = env("TOKENMAXXING_OAUTH_PROFILE_URL", "https://api.anthropic.com/api/oauth/profile");
|
|
6
7
|
|
|
@@ -41,23 +42,23 @@ export async function fetchTokenIdentity(accessToken: string, signal?: AbortSign
|
|
|
41
42
|
signal,
|
|
42
43
|
});
|
|
43
44
|
} catch (e) {
|
|
44
|
-
throw new IdentityUnavailableError(null,
|
|
45
|
+
throw new IdentityUnavailableError(null, errorMessage(e));
|
|
45
46
|
}
|
|
46
47
|
let text: string;
|
|
47
48
|
try {
|
|
48
49
|
text = await res.text();
|
|
49
50
|
} catch (e) {
|
|
50
|
-
throw new IdentityUnavailableError(res.status, `profile response body unreadable: ${
|
|
51
|
+
throw new IdentityUnavailableError(res.status, `profile response body unreadable: ${errorMessage(e)}`);
|
|
51
52
|
}
|
|
52
53
|
if (!res.ok) throw new IdentityUnavailableError(res.status, safeErrorDetail({ text }));
|
|
53
54
|
const parsed = ProfileResponseSchema.safeParse(JsonTextSchema.safeParse(text).data);
|
|
54
55
|
if (!parsed.success) throw new IdentityUnavailableError(res.status, `profile endpoint returned an unrecognized body (${text.length} bytes, withheld)`);
|
|
55
|
-
return
|
|
56
|
+
return {
|
|
56
57
|
accountUuid: parsed.data.account.uuid,
|
|
57
58
|
email: parsed.data.account.email ?? null,
|
|
58
59
|
organizationUuid: parsed.data.organization.uuid,
|
|
59
60
|
organizationName: parsed.data.organization.name ?? null,
|
|
60
|
-
}
|
|
61
|
+
};
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function describeIdentity(id: TokenIdentity): string {
|