tokenmaxxing 1.57.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 +6 -6
- 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 +51 -103
- 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/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 {
|
package/src/lib/opencodego.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
6
|
import { opencodeGoAuthJsonFor, opencodeGoPaths, opencodeGoPool, opencodeGoStoreDirFor } from "./paths.ts";
|
|
7
|
-
import type {
|
|
8
|
-
import {
|
|
9
|
-
import type
|
|
7
|
+
import type { Provider } from "./provider.ts";
|
|
8
|
+
import { type Harvest } from "./state.ts";
|
|
9
|
+
import { statusOnlyProvider, type AuthEntry } from "./statusonly.ts";
|
|
10
10
|
import { c } from "../cli/render.ts";
|
|
11
11
|
|
|
12
12
|
const OpencodeAuthEntrySchema = z.looseObject({ type: z.string(), key: z.string().optional() });
|
|
@@ -20,77 +20,12 @@ function liveAuthPath(): string {
|
|
|
20
20
|
return join(dataHome(), "opencode", "auth.json");
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function parseAuthFile(path: string): { provider: string; key: string } | null {
|
|
24
|
-
let raw: unknown;
|
|
25
|
-
try {
|
|
26
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const map = OpencodeAuthFileSchema.safeParse(raw);
|
|
31
|
-
if (!map.success) return null;
|
|
32
|
-
const entry = OpencodeAuthEntrySchema.safeParse(map.data["opencode-go"]);
|
|
33
|
-
if (!entry.success || entry.data.type !== "api" || !entry.data.key) return null;
|
|
34
|
-
return { provider: "opencode-go", key: entry.data.key };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
23
|
function idOfKey(key: string): string {
|
|
38
24
|
const h = new Bun.CryptoHasher("sha256");
|
|
39
25
|
h.update(`opencode-go:${key}`);
|
|
40
26
|
return `opencode-go-${h.digest("hex")}`;
|
|
41
27
|
}
|
|
42
28
|
|
|
43
|
-
function resolveRealOpencode(): string {
|
|
44
|
-
const cfg = loadConfig();
|
|
45
|
-
if (cfg.opencodeBin) {
|
|
46
|
-
if (!existsSync(cfg.opencodeBin)) throw new Error(`configured opencodeBin does not exist: ${cfg.opencodeBin} - fix config.json`);
|
|
47
|
-
return cfg.opencodeBin;
|
|
48
|
-
}
|
|
49
|
-
for (const d of (process.env.PATH ?? "").split(":")) {
|
|
50
|
-
if (!d) continue;
|
|
51
|
-
const cand = join(d, "opencode");
|
|
52
|
-
try {
|
|
53
|
-
if (existsSync(cand)) return cand;
|
|
54
|
-
} catch {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
throw new Error("could not locate the real `opencode` binary (set opencodeBin in config.json)");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function liveId(): string | null {
|
|
62
|
-
return null;
|
|
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 --opencode-go`" });
|
|
77
|
-
}
|
|
78
|
-
return reports;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function storeUsable(a: Account): Promise<boolean> {
|
|
82
|
-
try {
|
|
83
|
-
const found = parseAuthFile(opencodeGoAuthJsonFor(a.id));
|
|
84
|
-
return found != null && idOfKey(found.key) === a.id;
|
|
85
|
-
} catch {
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function removeCredentials(a: Account): Promise<void> {
|
|
91
|
-
rmSync(opencodeGoStoreDirFor(a.id), { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
29
|
function harvestOf(key: string): Harvest {
|
|
95
30
|
const id = idOfKey(key);
|
|
96
31
|
return {
|
|
@@ -105,82 +40,38 @@ function harvestOf(key: string): Harvest {
|
|
|
105
40
|
};
|
|
106
41
|
}
|
|
107
42
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const onboardDir = opencodeGoPaths.onboardDir;
|
|
111
|
-
rmSync(onboardDir, { recursive: true, force: true });
|
|
112
|
-
mkdirSync(onboardDir, { recursive: true });
|
|
113
|
-
const p = Bun.spawn([real, "providers", "login", "-p", "opencode-go"], {
|
|
114
|
-
stdin: "inherit",
|
|
115
|
-
stdout: "inherit",
|
|
116
|
-
stderr: "inherit",
|
|
117
|
-
env: { ...process.env, XDG_DATA_HOME: onboardDir, TOKENMAXXING_PROBE: "1" },
|
|
118
|
-
});
|
|
119
|
-
await p.exited;
|
|
43
|
+
function readAuth(path: string): AuthEntry[] {
|
|
44
|
+
let raw: unknown;
|
|
120
45
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
const found = parseAuthFile(join(onboardDir, "opencode", "auth.json"));
|
|
126
|
-
if (!found) {
|
|
127
|
-
console.error(c.red("no opencode-go login landed in the isolated home - nothing added."));
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
return harvestOf(found.key);
|
|
131
|
-
} finally {
|
|
132
|
-
rmSync(onboardDir, { recursive: true, force: true });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function importLive(): Promise<Harvest | null> {
|
|
137
|
-
console.log(c.cyan("Pooling your opencode-go API key - your existing opencode auth stays as it is."));
|
|
138
|
-
console.log();
|
|
139
|
-
const found = parseAuthFile(liveAuthPath());
|
|
140
|
-
if (found) {
|
|
141
|
-
console.log(c.dim(`found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`));
|
|
142
|
-
return harvestOf(found.key);
|
|
143
|
-
}
|
|
144
|
-
return login();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function preflight(): void {
|
|
148
|
-
const real = resolveRealOpencode();
|
|
149
|
-
const p = Bun.spawnSync([real, "--version"], { stdout: "pipe", stderr: "pipe", timeout: 15_000 });
|
|
150
|
-
const out = (p.stdout?.toString() ?? "").trim();
|
|
151
|
-
if (p.exitCode !== 0 || out === "") {
|
|
152
|
-
throw new Error(`opencode binary failed verification: ${real}`);
|
|
46
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
153
49
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
50
|
+
const map = OpencodeAuthFileSchema.safeParse(raw);
|
|
51
|
+
if (!map.success) return [];
|
|
52
|
+
const entry = OpencodeAuthEntrySchema.safeParse(map.data["opencode-go"]);
|
|
53
|
+
if (!entry.success || entry.data.type !== "api" || !entry.data.key) return [];
|
|
54
|
+
return [{ id: idOfKey(entry.data.key), usable: true, harvest: harvestOf(entry.data.key) }];
|
|
159
55
|
}
|
|
160
56
|
|
|
161
|
-
export const opencodeGo: Provider = {
|
|
57
|
+
export const opencodeGo: Provider = statusOnlyProvider({
|
|
162
58
|
name: "opencode-go",
|
|
163
59
|
flag: " --opencode-go",
|
|
164
60
|
pool: opencodeGoPool,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
login,
|
|
181
|
-
importLive,
|
|
182
|
-
preflight,
|
|
183
|
-
install,
|
|
61
|
+
binName: "opencode",
|
|
62
|
+
binKey: "opencodeBin",
|
|
63
|
+
storeDirFor: opencodeGoStoreDirFor,
|
|
64
|
+
authJsonFor: opencodeGoAuthJsonFor,
|
|
65
|
+
onboardDir: opencodeGoPaths.onboardDir,
|
|
66
|
+
homeEnv: "XDG_DATA_HOME",
|
|
67
|
+
loginArgs: ["providers", "login", "-p", "opencode-go"],
|
|
68
|
+
onboardAuthRel: join("opencode", "auth.json"),
|
|
69
|
+
liveAuthPath,
|
|
70
|
+
readAuth,
|
|
71
|
+
liveId: () => null,
|
|
72
|
+
versionOk: (out) => out.trim() !== "",
|
|
73
|
+
importIntro: "Pooling your opencode-go API key - your existing opencode auth stays as it is.",
|
|
74
|
+
foundLive: () => `found an opencode-go credential in ${liveAuthPath()} - pooling it; use \`tokenmaxxing add --opencode-go\` for more keys.`,
|
|
75
|
+
installNotice: "opencode-go pool is status-only: no supervisor or hooks installed. Use `tokenmaxxing status` to view pooled keys.",
|
|
184
76
|
loginStep: () => `Paste the opencode-go API key from ${c.bold("opencode.ai/zen")} when opencode asks for it.`,
|
|
185
|
-
|
|
186
|
-
};
|
|
77
|
+
});
|
package/src/lib/paths.ts
CHANGED
|
@@ -6,8 +6,12 @@ const HOME = homedir();
|
|
|
6
6
|
|
|
7
7
|
const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
8
8
|
|
|
9
|
+
export function optionalEnv(name: string, source: Record<string, string | undefined> = process.env): string | undefined {
|
|
10
|
+
return EnvOverrideSchema.parse(source[name]);
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
export function env(name: string, fallback: string): string {
|
|
10
|
-
return
|
|
14
|
+
return optionalEnv(name) ?? fallback;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
const TM_HOME = env("TOKENMAXXING_HOME", join(HOME, ".config", "tokenmaxxing"));
|
|
@@ -42,29 +46,25 @@ export const paths = {
|
|
|
42
46
|
|
|
43
47
|
export const claudePool = {
|
|
44
48
|
accountsJson: join(TM_HOME, "accounts.json"),
|
|
45
|
-
lastSwapJson: null,
|
|
46
49
|
lockFile: join(TM_HOME, "lock"),
|
|
47
50
|
} as const;
|
|
48
51
|
|
|
49
52
|
export const codexPool = {
|
|
50
53
|
accountsJson: join(TM_HOME, "codex-accounts.json"),
|
|
51
|
-
lastSwapJson: null,
|
|
52
54
|
lockFile: join(TM_HOME, "codex-lock"),
|
|
53
55
|
} as const;
|
|
54
56
|
|
|
55
57
|
export const grokPool = {
|
|
56
58
|
accountsJson: join(TM_HOME, "grok-accounts.json"),
|
|
57
|
-
lastSwapJson: null,
|
|
58
59
|
lockFile: join(TM_HOME, "grok-lock"),
|
|
59
60
|
} as const;
|
|
60
61
|
|
|
61
62
|
export const opencodeGoPool = {
|
|
62
63
|
accountsJson: join(TM_HOME, "opencode-go-accounts.json"),
|
|
63
|
-
lastSwapJson: null,
|
|
64
64
|
lockFile: join(TM_HOME, "opencode-go-lock"),
|
|
65
65
|
} as const;
|
|
66
66
|
|
|
67
|
-
export type PoolPaths = { accountsJson: string;
|
|
67
|
+
export type PoolPaths = { accountsJson: string; lockFile: string };
|
|
68
68
|
|
|
69
69
|
const CODEX_HOME = env("TOKENMAXXING_CODEX_HOME", env("CODEX_HOME", join(HOME, ".codex")));
|
|
70
70
|
|
|
@@ -159,20 +159,4 @@ export function namespacedCredService(configDirRaw: string): string {
|
|
|
159
159
|
return `Claude Code-credentials-${h.digest("hex").slice(0, 8)}`;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export function realClaudeBinFromEnv(): string | undefined {
|
|
163
|
-
return EnvOverrideSchema.parse(process.env.TOKENMAXXING_CLAUDE_BIN);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function realCodexBinFromEnv(): string | undefined {
|
|
167
|
-
return EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_BIN);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function realGrokBinFromEnv(): string | undefined {
|
|
171
|
-
return EnvOverrideSchema.parse(process.env.TOKENMAXXING_GROK_BIN);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function realOpencodeBinFromEnv(): string | undefined {
|
|
175
|
-
return EnvOverrideSchema.parse(process.env.TOKENMAXXING_OPENCODE_BIN);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
162
|
export { HOME };
|
package/src/lib/picker.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { maxBy, minBy, sortBy } from "es-toolkit";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { familyTokens } from "./usage.ts";
|
|
4
|
-
import {
|
|
3
|
+
import type { Account, Config, Thresholds, Window } from "./types.ts";
|
|
5
4
|
|
|
6
5
|
export function thresholdBars(cfg: Config): Thresholds {
|
|
7
6
|
return {
|
|
@@ -10,14 +9,13 @@ export function thresholdBars(cfg: Config): Thresholds {
|
|
|
10
9
|
};
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
now:
|
|
15
|
-
thresholds:
|
|
16
|
-
currentId:
|
|
17
|
-
families:
|
|
18
|
-
seats:
|
|
19
|
-
}
|
|
20
|
-
export type PickCtx = z.infer<typeof PickCtxSchema>;
|
|
12
|
+
export type PickCtx = {
|
|
13
|
+
now: number;
|
|
14
|
+
thresholds: Thresholds;
|
|
15
|
+
currentId: string | null;
|
|
16
|
+
families: string[] | null;
|
|
17
|
+
seats: Map<string, number> | null;
|
|
18
|
+
};
|
|
21
19
|
|
|
22
20
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
21
|
const SESSION_WINDOW_MAX_S = 6 * 3600;
|
|
@@ -123,21 +121,12 @@ export function pickBest(accounts: Account[], ctx: PickCtx): Account | null {
|
|
|
123
121
|
return sortBy(usable, swapPreference(ctx))[0] ?? null;
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
export function currentWins(active: Account | null, accounts: Account[], ctx: PickCtx, margin = 1): boolean {
|
|
127
|
-
if (!active || active.needsReauth || isExhausted(active, ctx)) return false;
|
|
128
|
-
const best = pickBest(accounts, { ...ctx, currentId: null });
|
|
129
|
-
if (best == null || best.id === active.id) return true;
|
|
130
|
-
if (margin > 1) return pacePressure(best, ctx) <= pacePressure(active, ctx) * margin;
|
|
131
|
-
return swapPreference(ctx).every((k) => k(active) === k(best));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
124
|
export function usableAt(a: Account, ctx: PickCtx): number {
|
|
135
125
|
const blocking = blockingUntil(a, ctx).filter((t) => t > ctx.now);
|
|
136
126
|
return blocking.length ? Math.max(...blocking) : ctx.now;
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
|
|
140
|
-
export type EarliestReset = z.infer<typeof EarliestResetSchema>;
|
|
129
|
+
export type EarliestReset = { account: Account; availableAt: number };
|
|
141
130
|
|
|
142
131
|
export function pickEarliestReset(accounts: Account[], ctx: PickCtx): EarliestReset | null {
|
|
143
132
|
const mapped = accounts
|
package/src/lib/presence.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
5
5
|
import { pidExists, pidStartTime } from "./proc.ts";
|
|
6
|
+
import { ErrnoSchema } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
const PresenceSchema = z.object({
|
|
8
9
|
accountId: z.string(),
|
|
@@ -21,8 +22,7 @@ export function clearPresence(input: { dir: string; id: string }): void {
|
|
|
21
22
|
rmSync(join(input.dir, input.id), { force: true });
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
export type LivingPresence = z.infer<typeof LivingPresenceSchema>;
|
|
25
|
+
export type LivingPresence = { id: string; accountId: string };
|
|
26
26
|
|
|
27
27
|
export function livingPresences(dir: string): LivingPresence[] {
|
|
28
28
|
const living: LivingPresence[] = [];
|
|
@@ -33,8 +33,7 @@ export function livingPresences(dir: string): LivingPresence[] {
|
|
|
33
33
|
try {
|
|
34
34
|
raw = readFileSync(file, "utf8");
|
|
35
35
|
} catch (e) {
|
|
36
|
-
|
|
37
|
-
if (errno.success && errno.data.code === "ENOENT") continue;
|
|
36
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
|
|
38
37
|
throw e;
|
|
39
38
|
}
|
|
40
39
|
const parsed = PresenceSchema.safeParse((() => {
|
|
@@ -55,7 +54,7 @@ export function livingPresences(dir: string): LivingPresence[] {
|
|
|
55
54
|
rmSync(file, { force: true });
|
|
56
55
|
continue;
|
|
57
56
|
}
|
|
58
|
-
living.push(
|
|
57
|
+
living.push({ id: name, accountId: parsed.data.accountId });
|
|
59
58
|
}
|
|
60
59
|
return living;
|
|
61
60
|
}
|
package/src/lib/proc.ts
CHANGED
|
@@ -7,6 +7,28 @@ export function pidStartTime(pid: number): string | null {
|
|
|
7
7
|
return lstart === "" ? null : lstart;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export async function readStdin(): Promise<string> {
|
|
11
|
+
const chunks: Uint8Array[] = [];
|
|
12
|
+
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
|
|
13
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function* readLines(stream: ReadableStream<Uint8Array>): AsyncGenerator<string> {
|
|
17
|
+
const decoder = new TextDecoder();
|
|
18
|
+
let pending = "";
|
|
19
|
+
for await (const chunk of stream) {
|
|
20
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
21
|
+
let nl = pending.indexOf("\n");
|
|
22
|
+
while (nl !== -1) {
|
|
23
|
+
yield pending.slice(0, nl);
|
|
24
|
+
pending = pending.slice(nl + 1);
|
|
25
|
+
nl = pending.indexOf("\n");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
pending += decoder.decode();
|
|
29
|
+
if (pending.length > 0) yield pending;
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
export function pidExists(pid: number): boolean {
|
|
11
33
|
try {
|
|
12
34
|
process.kill(pid, 0);
|