tokenmaxxing 1.78.0 → 1.80.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/package.json +1 -1
- package/src/cli/doctor.ts +4 -4
- package/src/cli/serve.ts +2 -2
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/supervisor.ts +3 -3
- package/src/lib/claude.ts +5 -5
- package/src/lib/codexauth.ts +8 -7
- package/src/lib/install.ts +116 -187
- package/src/lib/presence.ts +4 -11
- package/src/lib/sessions.ts +3 -2
- package/src/lib/settings.ts +5 -9
- package/src/lib/state.ts +14 -38
- package/src/lib/update.ts +4 -9
- package/src/main.ts +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.80.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/cli/doctor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { CLAUDE_BIN, verifyRealBin } from "../lib/claudebin.ts";
|
|
3
3
|
import { checkSettings, installedBin } from "../lib/settings.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { activationHint, CHECK_JOB, codexStoreHookTrust, findClaudeShadowers, HUB_JOB, isBinDirAhead, jobHealthy, shellRcPath } from "../lib/install.ts";
|
|
5
5
|
import { errorMessage } from "../lib/log.ts";
|
|
6
6
|
import { claudePool, codexPool, paths } from "../lib/paths.ts";
|
|
7
7
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
@@ -29,10 +29,10 @@ export async function cmdDoctor(): Promise<number> {
|
|
|
29
29
|
check(s.stopOk, "Stop hook installed in settings.json", "run `tokenmaxxing init`");
|
|
30
30
|
check(s.stopFailureOk, "StopFailure hook installed in settings.json", "run `tokenmaxxing init`");
|
|
31
31
|
check(s.sessionStartOk, "SessionStart hook installed in settings.json", "run `tokenmaxxing init`");
|
|
32
|
-
check(
|
|
32
|
+
check(jobHealthy(CHECK_JOB), "periodic check timer active", activationHint(CHECK_JOB));
|
|
33
33
|
if (existsSync(paths.supervisorLink)) {
|
|
34
|
-
if (
|
|
35
|
-
else check(
|
|
34
|
+
if (HUB_JOB.skip()) note("usage hub service is owned outside init (TOKENMAXXING_SKIP_HUB set) - not verified here");
|
|
35
|
+
else check(jobHealthy(HUB_JOB), "usage hub service active", "run `tokenmaxxing init` (it writes the unit and activates it)");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const idx = loadAccounts(claudePool);
|
package/src/cli/serve.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { claude } from "../lib/claude.ts";
|
|
|
7
7
|
import { codex, observeCodex } from "../lib/codex.ts";
|
|
8
8
|
import { CODEX_USAGE_URL } from "../lib/codexusage.ts";
|
|
9
9
|
import { errorMessage, log } from "../lib/log.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { deactivationHint, HUB_JOB } from "../lib/install.ts";
|
|
11
11
|
import { paths } from "../lib/paths.ts";
|
|
12
12
|
import { isExhausted, thresholdBars } from "../lib/picker.ts";
|
|
13
13
|
import type { Provider } from "../lib/provider.ts";
|
|
@@ -215,7 +215,7 @@ export async function cmdServe(args: string[]): Promise<number> {
|
|
|
215
215
|
},
|
|
216
216
|
});
|
|
217
217
|
} catch (e) {
|
|
218
|
-
emitError({ message: `cannot bind localhost:${cfg.hub.port}: ${errorMessage(e)} (another \`tokenmaxxing serve\` or the usage hub service may already hold the port - stop it first: ${
|
|
218
|
+
emitError({ message: `cannot bind localhost:${cfg.hub.port}: ${errorMessage(e)} (another \`tokenmaxxing serve\` or the usage hub service may already hold the port - stop it first: ${deactivationHint(HUB_JOB)})` });
|
|
219
219
|
return 1;
|
|
220
220
|
}
|
|
221
221
|
console.log(`${c.green("✓")} serving the CLIProxyAPI-compatible usage API at http://localhost:${server.port}/v0/management`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
@@ -10,7 +10,7 @@ import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
|
10
10
|
import { isExhausted } from "../lib/picker.ts";
|
|
11
11
|
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
12
12
|
import { saveTermios } from "../lib/tty.ts";
|
|
13
|
-
import { loadAccounts } from "../lib/state.ts";
|
|
13
|
+
import { loadAccounts, readJsonFile } from "../lib/state.ts";
|
|
14
14
|
import { CodexRespawnMarkerSchema, type Account } from "../lib/types.ts";
|
|
15
15
|
import { errorMessage, log } from "../lib/log.ts";
|
|
16
16
|
|
|
@@ -26,7 +26,7 @@ const PASSTHROUGH_FLAGS = new Set(["--version", "-V", "--help", "-h"]);
|
|
|
26
26
|
|
|
27
27
|
function readCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchema> {
|
|
28
28
|
try {
|
|
29
|
-
return
|
|
29
|
+
return readJsonFile(marker, CodexRespawnMarkerSchema);
|
|
30
30
|
} catch (e) {
|
|
31
31
|
log("codexsupervisor.marker_invalid", { err: errorMessage(e) });
|
|
32
32
|
throw new Error(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import type { FileSink, Subprocess } from "bun";
|
|
4
4
|
import { maxBy } from "es-toolkit";
|
|
@@ -16,7 +16,7 @@ import { teeObservation } from "../lib/sample.ts";
|
|
|
16
16
|
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
17
17
|
import { saveTermios } from "../lib/tty.ts";
|
|
18
18
|
import { liveSessionId, loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
19
|
-
import { loadAccounts, loadConfig, releaseWaitClaim } from "../lib/state.ts";
|
|
19
|
+
import { loadAccounts, loadConfig, readJsonFile, releaseWaitClaim } from "../lib/state.ts";
|
|
20
20
|
import { gatedFamilies, modelFromFlag } from "../lib/usage.ts";
|
|
21
21
|
import { RespawnMarkerSchema, type Account, type Config, type ModelInfo } from "../lib/types.ts";
|
|
22
22
|
import { errorMessage, log } from "../lib/log.ts";
|
|
@@ -183,7 +183,7 @@ type MarkerGate = {
|
|
|
183
183
|
function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
|
|
184
184
|
let m: z.infer<typeof RespawnMarkerSchema>;
|
|
185
185
|
try {
|
|
186
|
-
m =
|
|
186
|
+
m = readJsonFile(marker, RespawnMarkerSchema);
|
|
187
187
|
} catch (e) {
|
|
188
188
|
log("supervisor.marker_invalid", { err: errorMessage(e) });
|
|
189
189
|
throw new Error(
|
package/src/lib/claude.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
|
|
5
5
|
import { CLAUDE_BIN, resolveRealBin, resolveVerifiedClaude } from "./claudebin.ts";
|
|
6
|
-
import { ensurePathInRc,
|
|
6
|
+
import { activationHint, CHECK_JOB, ensurePathInRc, HUB_JOB, installSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
|
|
7
7
|
import { withLock } from "./lock.ts";
|
|
8
8
|
import { errorMessage, log } from "./log.ts";
|
|
9
9
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
@@ -12,7 +12,7 @@ import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./pick
|
|
|
12
12
|
import { seatCounts } from "./presence.ts";
|
|
13
13
|
import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
|
|
14
14
|
import { foldTee, sampleAccountUsage, teeObservation, usageBlockedUntil } from "./sample.ts";
|
|
15
|
-
import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, saveAccounts, type Harvest } from "./state.ts";
|
|
15
|
+
import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
|
|
16
16
|
import { saveTermios, restoreTermios } from "./tty.ts";
|
|
17
17
|
import { fetchUsageDirect, gatedFamilies, mergeWindows, modelFromFlag, scrubCredentialEnv, windowsOf } from "./usage.ts";
|
|
18
18
|
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config, type ModelInfo } from "./types.ts";
|
|
@@ -185,7 +185,7 @@ async function login(): Promise<Harvest | null> {
|
|
|
185
185
|
let blob, oauthAccount;
|
|
186
186
|
try {
|
|
187
187
|
blob = CredentialBlobSchema.parse(JSON.parse(blobRaw));
|
|
188
|
-
oauthAccount =
|
|
188
|
+
oauthAccount = readJsonFile(cjPath, z.object({ oauthAccount: OAuthAccountSchema })).oauthAccount;
|
|
189
189
|
} catch {
|
|
190
190
|
console.error(c.red("could not parse the onboarded account's credential/identity."));
|
|
191
191
|
return null;
|
|
@@ -256,9 +256,9 @@ function install(): void {
|
|
|
256
256
|
const out = installSupervisor();
|
|
257
257
|
console.log(`${c.green("✓")} installed ${c.bold("claude")} supervisor + statusLine/Stop/StopFailure/SessionStart hooks`);
|
|
258
258
|
if (out.timerLoaded) console.log(`${c.green("✓")} periodic check timer active (every ${out.checkIntervalS}s)`);
|
|
259
|
-
else console.log(c.yellow(`⚠ check timer written but not activated - run: ${
|
|
259
|
+
else console.log(c.yellow(`⚠ check timer written but not activated - run: ${activationHint(CHECK_JOB)}`));
|
|
260
260
|
if (out.hubLoaded) console.log(`${c.green("✓")} usage hub service active`);
|
|
261
|
-
else console.log(c.yellow(`⚠ usage hub service written but not activated - run: ${
|
|
261
|
+
else console.log(c.yellow(`⚠ usage hub service written but not activated - run: ${activationHint(HUB_JOB)}`));
|
|
262
262
|
if (!out.pathAhead) {
|
|
263
263
|
console.log();
|
|
264
264
|
ensurePathAhead();
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } 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 { codexAuthJsonFor, codexPaths, codexStoreDirFor } from "./paths.ts";
|
|
6
|
+
import { readJsonFile } from "./state.ts";
|
|
6
7
|
import { CodexAuthJsonSchema, ErrnoSchema, type CodexAuthJson } from "./types.ts";
|
|
7
8
|
|
|
9
|
+
const CodexAuthFileSchema = z
|
|
10
|
+
.looseObject({ tokens: z.unknown().optional() })
|
|
11
|
+
.transform((auth) => (auth.tokens === undefined || auth.tokens === null ? null : auth))
|
|
12
|
+
.pipe(CodexAuthJsonSchema.nullable());
|
|
13
|
+
|
|
8
14
|
export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
|
|
9
|
-
let raw: string;
|
|
10
15
|
try {
|
|
11
|
-
|
|
16
|
+
return readJsonFile(input.path, CodexAuthFileSchema);
|
|
12
17
|
} catch (e) {
|
|
13
18
|
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
14
19
|
throw e;
|
|
15
20
|
}
|
|
16
|
-
const parsed = JSON.parse(raw);
|
|
17
|
-
const probe = z.looseObject({ tokens: z.unknown().optional() }).parse(parsed);
|
|
18
|
-
if (probe.tokens === undefined || probe.tokens === null) return null;
|
|
19
|
-
return CodexAuthJsonSchema.parse(parsed);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export function readCodexStoreAuth(accountId: string): CodexAuthJson | null {
|
package/src/lib/install.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { codexPaths, codexStoreDirFor, HOME, optionalEnv, paths } from "./paths.
|
|
|
7
7
|
import { writeFileAtomic } from "./atomic.ts";
|
|
8
8
|
import { installedBin, installSettings, isOurHookCommand, uninstallSettings } from "./settings.ts";
|
|
9
9
|
import { CLAUDE_BIN, resolveRealBin } from "./claudebin.ts";
|
|
10
|
-
import { loadConfig } from "./state.ts";
|
|
10
|
+
import { loadConfig, readJsonFile } from "./state.ts";
|
|
11
11
|
import { ErrnoSchema } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
export type InstallOutcome = { claudeWrapper: string; installedBin: string; pathAhead: boolean; timerLoaded: boolean; hubLoaded: boolean; checkIntervalS: number };
|
|
@@ -36,11 +36,11 @@ export function isNixPackaged(): boolean {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
function skipImperativeTimer(): boolean {
|
|
40
40
|
return envFlag("TOKENMAXXING_SKIP_TIMER");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
function skipImperativeHub(): boolean {
|
|
44
44
|
return envFlag("TOKENMAXXING_SKIP_HUB");
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -106,8 +106,8 @@ export function installSupervisor(): InstallOutcome {
|
|
|
106
106
|
claudeWrapper: paths.supervisorLink,
|
|
107
107
|
installedBin: target,
|
|
108
108
|
pathAhead: isBinDirAhead(),
|
|
109
|
-
timerLoaded:
|
|
110
|
-
hubLoaded:
|
|
109
|
+
timerLoaded: installJob(CHECK_JOB, checkIntervalS),
|
|
110
|
+
hubLoaded: installJob(HUB_JOB, checkIntervalS),
|
|
111
111
|
checkIntervalS,
|
|
112
112
|
};
|
|
113
113
|
}
|
|
@@ -134,7 +134,7 @@ function withoutOurCodexStopHooks(groups: { hooks: { type?: string; command?: st
|
|
|
134
134
|
|
|
135
135
|
export function installCodexStopHook(): void {
|
|
136
136
|
const current = existsSync(codexPaths.hooksJson)
|
|
137
|
-
?
|
|
137
|
+
? readJsonFile(codexPaths.hooksJson, CodexHooksFileSchema)
|
|
138
138
|
: CodexHooksFileSchema.parse({});
|
|
139
139
|
const next = {
|
|
140
140
|
...current,
|
|
@@ -151,7 +151,7 @@ export function installCodexStopHook(): void {
|
|
|
151
151
|
|
|
152
152
|
export function uninstallCodexStopHook(): void {
|
|
153
153
|
if (!existsSync(codexPaths.hooksJson)) return;
|
|
154
|
-
const current =
|
|
154
|
+
const current = readJsonFile(codexPaths.hooksJson, CodexHooksFileSchema);
|
|
155
155
|
const next = {
|
|
156
156
|
...current,
|
|
157
157
|
hooks: {
|
|
@@ -166,7 +166,7 @@ export function codexStopHookGroupIndex(): number | null {
|
|
|
166
166
|
if (!existsSync(codexPaths.hooksJson)) return null;
|
|
167
167
|
let parsed: z.infer<typeof CodexHooksFileSchema>;
|
|
168
168
|
try {
|
|
169
|
-
parsed =
|
|
169
|
+
parsed = readJsonFile(codexPaths.hooksJson, CodexHooksFileSchema);
|
|
170
170
|
} catch {
|
|
171
171
|
return null;
|
|
172
172
|
}
|
|
@@ -205,9 +205,67 @@ export function uninstallCodexSupervisor(): void {
|
|
|
205
205
|
if (existsSync(codexSupervisorLink())) rmSync(codexSupervisorLink(), { force: true });
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
export type UnitJob = {
|
|
209
|
+
label: string;
|
|
210
|
+
arg: string;
|
|
211
|
+
log: string;
|
|
212
|
+
skip: () => boolean;
|
|
213
|
+
unit: string;
|
|
214
|
+
launchd: (intervalS: number) => string;
|
|
215
|
+
systemd: Record<string, (exec: string, intervalS: number) => string>;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const CHECK_JOB: UnitJob = {
|
|
219
|
+
label: "com.tokenmaxxing.check",
|
|
220
|
+
arg: "check",
|
|
221
|
+
log: "check.stderr.log",
|
|
222
|
+
skip: skipImperativeTimer,
|
|
223
|
+
unit: "tokenmaxxing-check.timer",
|
|
224
|
+
launchd: (intervalS) => ` <key>StartInterval</key><integer>${intervalS}</integer>\n`,
|
|
225
|
+
systemd: {
|
|
226
|
+
"tokenmaxxing-check.service": (exec) => `[Unit]
|
|
227
|
+
Description=tokenmaxxing account-switch check
|
|
228
|
+
|
|
229
|
+
[Service]
|
|
230
|
+
Type=oneshot
|
|
231
|
+
ExecStart=${exec}
|
|
232
|
+
${systemdEnv()}`,
|
|
233
|
+
"tokenmaxxing-check.timer": (_exec, intervalS) => `[Unit]
|
|
234
|
+
Description=tokenmaxxing periodic account-switch check
|
|
235
|
+
|
|
236
|
+
[Timer]
|
|
237
|
+
OnBootSec=${intervalS}
|
|
238
|
+
OnUnitActiveSec=${intervalS}
|
|
239
|
+
AccuracySec=${Math.max(1, Math.floor(intervalS / 12))}
|
|
240
|
+
|
|
241
|
+
[Install]
|
|
242
|
+
WantedBy=timers.target
|
|
243
|
+
`,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const HUB_JOB: UnitJob = {
|
|
248
|
+
label: "com.tokenmaxxing.hub",
|
|
249
|
+
arg: "serve",
|
|
250
|
+
log: "hub.stderr.log",
|
|
251
|
+
skip: skipImperativeHub,
|
|
252
|
+
unit: "tokenmaxxing-hub.service",
|
|
253
|
+
launchd: () => ` <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>\n <key>RunAtLoad</key><true/>\n`,
|
|
254
|
+
systemd: {
|
|
255
|
+
"tokenmaxxing-hub.service": (exec) => `[Unit]
|
|
256
|
+
Description=tokenmaxxing usage hub
|
|
257
|
+
StartLimitIntervalSec=0
|
|
258
|
+
|
|
259
|
+
[Service]
|
|
260
|
+
ExecStart=${exec}
|
|
261
|
+
${systemdEnv()}Restart=on-failure
|
|
262
|
+
RestartSec=5
|
|
263
|
+
|
|
264
|
+
[Install]
|
|
265
|
+
WantedBy=default.target
|
|
266
|
+
`,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
211
269
|
|
|
212
270
|
function launchdPlist(label: string): string {
|
|
213
271
|
return join(paths.launchdAgentsDir, `${label}.plist`);
|
|
@@ -233,110 +291,56 @@ function launchdEnv(): string {
|
|
|
233
291
|
return ` <key>EnvironmentVariables</key><dict><key>TOKENMAXXING_HOME</key><string>${escape(paths.home)}</string></dict>\n`;
|
|
234
292
|
}
|
|
235
293
|
|
|
236
|
-
function
|
|
237
|
-
if (
|
|
294
|
+
function installJob(job: UnitJob, intervalS: number): boolean {
|
|
295
|
+
if (job.skip()) return true;
|
|
238
296
|
|
|
239
297
|
if (process.platform === "darwin") {
|
|
240
|
-
const plist = launchdPlist(
|
|
298
|
+
const plist = launchdPlist(job.label);
|
|
241
299
|
writeFileAtomic(
|
|
242
300
|
plist,
|
|
243
301
|
`<?xml version="1.0" encoding="UTF-8"?>
|
|
244
302
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
245
303
|
<plist version="1.0">
|
|
246
304
|
<dict>
|
|
247
|
-
<key>Label</key><string>${
|
|
248
|
-
<key>ProgramArguments</key><array><string>${escape(installedBin())}</string><string
|
|
249
|
-
<key>
|
|
250
|
-
|
|
251
|
-
<key>StandardErrorPath</key><string>${escape(join(paths.home, "check.stderr.log"))}</string>
|
|
305
|
+
<key>Label</key><string>${job.label}</string>
|
|
306
|
+
<key>ProgramArguments</key><array><string>${escape(installedBin())}</string><string>${job.arg}</string></array>
|
|
307
|
+
${job.launchd(intervalS)}${launchdEnv()} <key>StandardOutPath</key><string>/dev/null</string>
|
|
308
|
+
<key>StandardErrorPath</key><string>${escape(join(paths.home, job.log))}</string>
|
|
252
309
|
</dict>
|
|
253
310
|
</plist>
|
|
254
311
|
`,
|
|
255
312
|
0o644,
|
|
256
313
|
);
|
|
257
314
|
const domain = launchdDomain();
|
|
258
|
-
run(["launchctl", "bootout", `${domain}/${
|
|
259
|
-
return run(["launchctl", "bootstrap", domain, plist]) ||
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const exec = `"${installedBin().replaceAll("%", "%%")}" check`;
|
|
263
|
-
writeFileAtomic(
|
|
264
|
-
join(paths.systemdUserDir, "tokenmaxxing-check.service"),
|
|
265
|
-
`[Unit]
|
|
266
|
-
Description=tokenmaxxing account-switch check
|
|
267
|
-
|
|
268
|
-
[Service]
|
|
269
|
-
Type=oneshot
|
|
270
|
-
ExecStart=${exec}
|
|
271
|
-
${systemdEnv()}`,
|
|
272
|
-
0o644,
|
|
273
|
-
);
|
|
274
|
-
writeFileAtomic(
|
|
275
|
-
join(paths.systemdUserDir, "tokenmaxxing-check.timer"),
|
|
276
|
-
`[Unit]
|
|
277
|
-
Description=tokenmaxxing periodic account-switch check
|
|
278
|
-
|
|
279
|
-
[Timer]
|
|
280
|
-
OnBootSec=${intervalS}
|
|
281
|
-
OnUnitActiveSec=${intervalS}
|
|
282
|
-
AccuracySec=${Math.max(1, Math.floor(intervalS / 12))}
|
|
283
|
-
|
|
284
|
-
[Install]
|
|
285
|
-
WantedBy=timers.target
|
|
286
|
-
`,
|
|
287
|
-
0o644,
|
|
288
|
-
);
|
|
289
|
-
return (
|
|
290
|
-
run(["systemctl", "--user", "daemon-reload"]) &&
|
|
291
|
-
run(["systemctl", "--user", "enable", "--now", "tokenmaxxing-check.timer"])
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
export function timerActivationHint(): string {
|
|
296
|
-
if (process.platform === "darwin") {
|
|
297
|
-
return `launchctl bootstrap gui/$(id -u) ${launchdPlist(LAUNCHD_LABEL)}`;
|
|
298
|
-
}
|
|
299
|
-
return "systemctl --user daemon-reload && systemctl --user enable --now tokenmaxxing-check.timer";
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function hubActivationHint(): string {
|
|
303
|
-
if (process.platform === "darwin") {
|
|
304
|
-
return `launchctl bootstrap gui/$(id -u) ${launchdPlist(LAUNCHD_HUB_LABEL)}`;
|
|
315
|
+
run(["launchctl", "bootout", `${domain}/${job.label}`]);
|
|
316
|
+
return run(["launchctl", "bootstrap", domain, plist]) || jobHealthy(job);
|
|
305
317
|
}
|
|
306
|
-
return `systemctl --user daemon-reload && systemctl --user enable --now ${HUB_UNIT}`;
|
|
307
|
-
}
|
|
308
318
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return existsSync(launchdPlist(LAUNCHD_LABEL)) && run(["launchctl", "print", `${launchdDomain()}/${LAUNCHD_LABEL}`]);
|
|
313
|
-
}
|
|
314
|
-
return (
|
|
315
|
-
existsSync(join(paths.systemdUserDir, "tokenmaxxing-check.timer")) &&
|
|
316
|
-
run(["systemctl", "--user", "is-active", "--quiet", "tokenmaxxing-check.timer"])
|
|
317
|
-
);
|
|
319
|
+
const exec = `"${installedBin().replaceAll("%", "%%")}" ${job.arg}`;
|
|
320
|
+
for (const [file, text] of Object.entries(job.systemd)) writeFileAtomic(join(paths.systemdUserDir, file), text(exec, intervalS), 0o644);
|
|
321
|
+
return run(["systemctl", "--user", "daemon-reload"]) && run(["systemctl", "--user", "enable", "--now", job.unit]);
|
|
318
322
|
}
|
|
319
323
|
|
|
320
|
-
export function
|
|
321
|
-
if (skipImperativeHub()) return true;
|
|
324
|
+
export function activationHint(job: UnitJob): string {
|
|
322
325
|
if (process.platform === "darwin") {
|
|
323
|
-
return
|
|
326
|
+
return `launchctl bootstrap gui/$(id -u) ${launchdPlist(job.label)}`;
|
|
324
327
|
}
|
|
325
|
-
return
|
|
328
|
+
return `systemctl --user daemon-reload && systemctl --user enable --now ${job.unit}`;
|
|
326
329
|
}
|
|
327
330
|
|
|
328
|
-
export function
|
|
331
|
+
export function jobHealthy(job: UnitJob): boolean {
|
|
332
|
+
if (job.skip()) return true;
|
|
329
333
|
if (process.platform === "darwin") {
|
|
330
|
-
return
|
|
334
|
+
return existsSync(launchdPlist(job.label)) && run(["launchctl", "print", `${launchdDomain()}/${job.label}`]);
|
|
331
335
|
}
|
|
332
|
-
return "systemctl --user
|
|
336
|
+
return existsSync(join(paths.systemdUserDir, job.unit)) && run(["systemctl", "--user", "is-active", "--quiet", job.unit]);
|
|
333
337
|
}
|
|
334
338
|
|
|
335
|
-
export function
|
|
339
|
+
export function deactivationHint(job: UnitJob): string {
|
|
336
340
|
if (process.platform === "darwin") {
|
|
337
|
-
return `launchctl bootout gui/$(id -u)/${
|
|
341
|
+
return `launchctl bootout gui/$(id -u)/${job.label}`;
|
|
338
342
|
}
|
|
339
|
-
return `systemctl --user disable --now ${
|
|
343
|
+
return `systemctl --user disable --now ${job.unit}`;
|
|
340
344
|
}
|
|
341
345
|
|
|
342
346
|
function launchdJobLoaded(label: string): "loaded" | "not-loaded" | "unavailable" {
|
|
@@ -361,108 +365,42 @@ function systemdUnitActive(unit: string): "active" | "not-active" | "unavailable
|
|
|
361
365
|
}
|
|
362
366
|
}
|
|
363
367
|
|
|
364
|
-
function
|
|
365
|
-
if (process.platform === "darwin")
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
368
|
+
function jobFiles(job: UnitJob): string[] {
|
|
369
|
+
if (process.platform === "darwin") return [launchdPlist(job.label)];
|
|
370
|
+
return Object.keys(job.systemd).map((file) => join(paths.systemdUserDir, file));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function removeJobFiles(job: UnitJob): void {
|
|
374
|
+
for (const file of jobFiles(job)) rmSync(file, { force: true });
|
|
371
375
|
}
|
|
372
376
|
|
|
373
377
|
type UnitOutcome = "removed" | "skipped" | "still-loaded";
|
|
374
378
|
|
|
375
|
-
function
|
|
376
|
-
if (
|
|
379
|
+
function uninstallJob(job: UnitJob, live: boolean): UnitOutcome {
|
|
380
|
+
if (job.skip()) return "skipped";
|
|
377
381
|
if (!live) {
|
|
378
|
-
|
|
382
|
+
removeJobFiles(job);
|
|
379
383
|
return "removed";
|
|
380
384
|
}
|
|
381
385
|
if (process.platform === "darwin") {
|
|
382
|
-
const loaded = launchdJobLoaded(
|
|
383
|
-
const deactivated = loaded === "loaded" ? run(["launchctl", "bootout", `${launchdDomain()}/${
|
|
384
|
-
|
|
386
|
+
const loaded = launchdJobLoaded(job.label);
|
|
387
|
+
const deactivated = loaded === "loaded" ? run(["launchctl", "bootout", `${launchdDomain()}/${job.label}`]) : loaded === "not-loaded";
|
|
388
|
+
removeJobFiles(job);
|
|
385
389
|
return deactivated ? "removed" : "still-loaded";
|
|
386
390
|
}
|
|
387
|
-
const active = systemdUnitActive(
|
|
388
|
-
const deactivated = active !== "unavailable" && (run(["systemctl", "--user", "disable", "--now",
|
|
389
|
-
|
|
391
|
+
const active = systemdUnitActive(job.unit);
|
|
392
|
+
const deactivated = active !== "unavailable" && (run(["systemctl", "--user", "disable", "--now", job.unit]) || active === "not-active");
|
|
393
|
+
removeJobFiles(job);
|
|
390
394
|
run(["systemctl", "--user", "daemon-reload"]);
|
|
391
395
|
return deactivated ? "removed" : "still-loaded";
|
|
392
396
|
}
|
|
393
397
|
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
plist,
|
|
401
|
-
`<?xml version="1.0" encoding="UTF-8"?>
|
|
402
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
403
|
-
<plist version="1.0">
|
|
404
|
-
<dict>
|
|
405
|
-
<key>Label</key><string>${LAUNCHD_HUB_LABEL}</string>
|
|
406
|
-
<key>ProgramArguments</key><array><string>${escape(installedBin())}</string><string>serve</string></array>
|
|
407
|
-
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
|
|
408
|
-
<key>RunAtLoad</key><true/>
|
|
409
|
-
${launchdEnv()} <key>StandardOutPath</key><string>/dev/null</string>
|
|
410
|
-
<key>StandardErrorPath</key><string>${escape(join(paths.home, "hub.stderr.log"))}</string>
|
|
411
|
-
</dict>
|
|
412
|
-
</plist>
|
|
413
|
-
`,
|
|
414
|
-
0o644,
|
|
415
|
-
);
|
|
416
|
-
const domain = launchdDomain();
|
|
417
|
-
run(["launchctl", "bootout", `${domain}/${LAUNCHD_HUB_LABEL}`]);
|
|
418
|
-
return run(["launchctl", "bootstrap", domain, plist]) || hubServiceHealthy();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const exec = `"${installedBin().replaceAll("%", "%%")}" serve`;
|
|
422
|
-
writeFileAtomic(
|
|
423
|
-
join(paths.systemdUserDir, HUB_UNIT),
|
|
424
|
-
`[Unit]
|
|
425
|
-
Description=tokenmaxxing usage hub
|
|
426
|
-
StartLimitIntervalSec=0
|
|
427
|
-
|
|
428
|
-
[Service]
|
|
429
|
-
ExecStart=${exec}
|
|
430
|
-
${systemdEnv()}Restart=on-failure
|
|
431
|
-
RestartSec=5
|
|
432
|
-
|
|
433
|
-
[Install]
|
|
434
|
-
WantedBy=default.target
|
|
435
|
-
`,
|
|
436
|
-
0o644,
|
|
437
|
-
);
|
|
438
|
-
return run(["systemctl", "--user", "daemon-reload"]) && run(["systemctl", "--user", "enable", "--now", HUB_UNIT]);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function removeHubUnits(): void {
|
|
442
|
-
if (process.platform === "darwin") {
|
|
443
|
-
rmSync(launchdPlist(LAUNCHD_HUB_LABEL), { force: true });
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
rmSync(join(paths.systemdUserDir, HUB_UNIT), { force: true });
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function uninstallHubService(live: boolean): UnitOutcome {
|
|
450
|
-
if (skipImperativeHub()) return "skipped";
|
|
451
|
-
if (!live) {
|
|
452
|
-
removeHubUnits();
|
|
453
|
-
return "removed";
|
|
454
|
-
}
|
|
455
|
-
if (process.platform === "darwin") {
|
|
456
|
-
const loaded = launchdJobLoaded(LAUNCHD_HUB_LABEL);
|
|
457
|
-
const deactivated = loaded === "loaded" ? run(["launchctl", "bootout", `${launchdDomain()}/${LAUNCHD_HUB_LABEL}`]) : loaded === "not-loaded";
|
|
458
|
-
removeHubUnits();
|
|
459
|
-
return deactivated ? "removed" : "still-loaded";
|
|
460
|
-
}
|
|
461
|
-
const active = systemdUnitActive(HUB_UNIT);
|
|
462
|
-
const deactivated = active !== "unavailable" && (run(["systemctl", "--user", "disable", "--now", HUB_UNIT]) || active === "not-active");
|
|
463
|
-
removeHubUnits();
|
|
464
|
-
run(["systemctl", "--user", "daemon-reload"]);
|
|
465
|
-
return deactivated ? "removed" : "still-loaded";
|
|
398
|
+
function uninstallTarget(job: UnitJob, live: boolean): string {
|
|
399
|
+
const files = jobFiles(job).join(" and ");
|
|
400
|
+
if (!live) return files;
|
|
401
|
+
return process.platform === "darwin"
|
|
402
|
+
? `${files} (and launchctl bootout ${launchdDomain()}/${job.label})`
|
|
403
|
+
: `${files} (and systemctl --user disable --now ${job.unit})`;
|
|
466
404
|
}
|
|
467
405
|
|
|
468
406
|
export function shellRcPath(): string | null {
|
|
@@ -563,20 +501,11 @@ export function onLoginHome(): boolean {
|
|
|
563
501
|
}
|
|
564
502
|
|
|
565
503
|
export function uninstallTargets(live: boolean): string[] {
|
|
566
|
-
const timer =
|
|
567
|
-
process.platform === "darwin"
|
|
568
|
-
? `${launchdPlist(LAUNCHD_LABEL)}${live ? ` (and launchctl bootout ${launchdDomain()}/${LAUNCHD_LABEL})` : ""}`
|
|
569
|
-
: `${join(paths.systemdUserDir, "tokenmaxxing-check.timer")} and .service${live ? " (and systemctl --user disable --now tokenmaxxing-check.timer)" : ""}`;
|
|
570
|
-
const hub =
|
|
571
|
-
process.platform === "darwin"
|
|
572
|
-
? `${launchdPlist(LAUNCHD_HUB_LABEL)}${live ? ` (and launchctl bootout ${launchdDomain()}/${LAUNCHD_HUB_LABEL})` : ""}`
|
|
573
|
-
: `${join(paths.systemdUserDir, HUB_UNIT)}${live ? ` (and systemctl --user disable --now ${HUB_UNIT})` : ""}`;
|
|
574
504
|
const rc = shellRcPath();
|
|
575
505
|
return [
|
|
576
506
|
`${paths.claudeSettings}: the hook and statusline entries`,
|
|
577
507
|
`${codexPaths.hooksJson}: the codex Stop hook entry`,
|
|
578
|
-
...(
|
|
579
|
-
...(skipImperativeHub() ? [] : [hub]),
|
|
508
|
+
...[CHECK_JOB, HUB_JOB].filter((job) => !job.skip()).map((job) => uninstallTarget(job, live)),
|
|
580
509
|
paths.supervisorLink,
|
|
581
510
|
codexSupervisorLink(),
|
|
582
511
|
join(paths.binDir, "xx"),
|
|
@@ -587,8 +516,8 @@ export function uninstallTargets(live: boolean): string[] {
|
|
|
587
516
|
|
|
588
517
|
export function uninstallSupervisor(input: { live: boolean }): UninstallOutcome {
|
|
589
518
|
uninstallSettings();
|
|
590
|
-
const timer =
|
|
591
|
-
const hub =
|
|
519
|
+
const timer = uninstallJob(CHECK_JOB, input.live);
|
|
520
|
+
const hub = uninstallJob(HUB_JOB, input.live);
|
|
592
521
|
uninstallCodexSupervisor();
|
|
593
522
|
for (const f of [paths.supervisorLink, join(paths.binDir, "xx"), installedBin()]) {
|
|
594
523
|
if (existsSync(f)) rmSync(f, { force: true });
|
package/src/lib/presence.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { countBy } from "es-toolkit";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
6
|
import { pidExists, pidStartTime } from "./proc.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { readJsonFile } from "./state.ts";
|
|
8
|
+
import { ErrnoSchema, JsonTextSchema } from "./types.ts";
|
|
8
9
|
|
|
9
10
|
const PresenceSchema = z.object({
|
|
10
11
|
accountId: z.string(),
|
|
@@ -19,14 +20,12 @@ export function writePresence(input: { dir: string; id: string; accountId: strin
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function presencePid(input: { dir: string; id: string }): number | null {
|
|
22
|
-
let raw: string;
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
return readJsonFile(join(input.dir, input.id), PresenceSchema).pid;
|
|
25
25
|
} catch (e) {
|
|
26
26
|
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
27
27
|
throw e;
|
|
28
28
|
}
|
|
29
|
-
return PresenceSchema.parse(JSON.parse(raw)).pid;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
export function clearPresence(input: { dir: string; id: string }): void {
|
|
@@ -47,13 +46,7 @@ export function livingPresences(dir: string): LivingPresence[] {
|
|
|
47
46
|
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") continue;
|
|
48
47
|
throw e;
|
|
49
48
|
}
|
|
50
|
-
const parsed = PresenceSchema.safeParse((
|
|
51
|
-
try {
|
|
52
|
-
return JSON.parse(raw);
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
})());
|
|
49
|
+
const parsed = PresenceSchema.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
57
50
|
if (!parsed.success) {
|
|
58
51
|
throw new Error(`${file} is not a readable presence record - it may belong to a RUNNING session, refusing to treat it as absent; remove the file (or respawn that session) to proceed`);
|
|
59
52
|
}
|
package/src/lib/sessions.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, rmSync, statSync, type Dirent } from "node:fs";
|
|
2
2
|
import { basename, join, sep } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { errorMessage, log } from "./log.ts";
|
|
5
5
|
import { codexPaths, grokPaths, opencodeGoPaths, paths } from "./paths.ts";
|
|
6
6
|
import { writeFileAtomic } from "./atomic.ts";
|
|
7
7
|
import { presencePid } from "./presence.ts";
|
|
8
|
+
import { readJsonFile } from "./state.ts";
|
|
8
9
|
import { spawnedThroughShellsBy } from "./proc.ts";
|
|
9
10
|
import type { RespawnMarkerSchema } from "./types.ts";
|
|
10
11
|
|
|
@@ -25,7 +26,7 @@ export function saveSessionFlags(sid: string, flags: string[], cwd: string): voi
|
|
|
25
26
|
function loadSession(sid: string): z.infer<typeof SessionSchema> | null {
|
|
26
27
|
const f = sessionFile(sid);
|
|
27
28
|
if (!existsSync(f)) return null;
|
|
28
|
-
return
|
|
29
|
+
return readJsonFile(f, SessionSchema);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export function loadSessionFlags(sid: string): string[] | null {
|
package/src/lib/settings.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { paths } from "./paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
|
+
import { readJsonFile } from "./state.ts";
|
|
7
|
+
import { JsonTextSchema } from "./types.ts";
|
|
6
8
|
|
|
7
9
|
export function installedBin(): string {
|
|
8
10
|
return join(paths.binDir, "tokenmaxxing");
|
|
@@ -31,7 +33,7 @@ const STOP_FAILURE_MATCHER = "rate_limit";
|
|
|
31
33
|
|
|
32
34
|
function readSettings(): Settings {
|
|
33
35
|
if (!existsSync(paths.claudeSettings)) return {};
|
|
34
|
-
return
|
|
36
|
+
return readJsonFile(paths.claudeSettings, SettingsSchema);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
function writeSettings(s: Settings): void {
|
|
@@ -71,13 +73,7 @@ export function isOurHookCommand(cmd: string, sub: string): boolean {
|
|
|
71
73
|
if (!cmd.endsWith(` ${sub}`)) return false;
|
|
72
74
|
const quotedPath = cmd.slice(0, cmd.length - (sub.length + 1));
|
|
73
75
|
if (!quotedPath.startsWith('"') || !quotedPath.endsWith('"')) return false;
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
path = JSON.parse(quotedPath);
|
|
77
|
-
} catch {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
const parsed = z.string().safeParse(path);
|
|
76
|
+
const parsed = z.string().safeParse(JsonTextSchema.safeParse(quotedPath).data);
|
|
81
77
|
return parsed.success && parsed.data.endsWith("/tokenmaxxing");
|
|
82
78
|
}
|
|
83
79
|
|
package/src/lib/state.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
AccountsIndexSchema,
|
|
8
8
|
ConfigSchema,
|
|
9
9
|
ErrnoSchema,
|
|
10
|
+
JsonTextSchema,
|
|
10
11
|
UsageStateSchema,
|
|
11
12
|
WaitQueueSchema,
|
|
12
13
|
type Account,
|
|
@@ -17,21 +18,19 @@ import {
|
|
|
17
18
|
type Window,
|
|
18
19
|
} from "./types.ts";
|
|
19
20
|
|
|
20
|
-
export function
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
raw = JSON.parse(readFileSync(paths.configJson, "utf8"));
|
|
25
|
-
} catch {
|
|
26
|
-
throw new Error(`${paths.configJson} is corrupt (unparsable JSON) - fix or remove it`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const parsed = ConfigSchema.safeParse(raw);
|
|
21
|
+
export function readJsonFile<T extends z.ZodType>(path: string, schema: T): z.output<T> {
|
|
22
|
+
const json = JsonTextSchema.safeParse(readFileSync(path, "utf8"));
|
|
23
|
+
if (!json.success) throw new Error(`${path} is corrupt (unparsable JSON) - repair or remove the file`);
|
|
24
|
+
const parsed = schema.safeParse(json.data);
|
|
30
25
|
if (!parsed.success) {
|
|
31
26
|
const fields = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
|
|
32
|
-
throw new Error(`${
|
|
27
|
+
throw new Error(`${path} does not match its schema (${fields}) - repair or remove the file`);
|
|
33
28
|
}
|
|
34
|
-
|
|
29
|
+
return parsed.data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadConfig(): Config {
|
|
33
|
+
const cfg = existsSync(paths.configJson) ? readJsonFile(paths.configJson, ConfigSchema) : ConfigSchema.parse({});
|
|
35
34
|
cfg.claudeBin = optionalEnv("TOKENMAXXING_CLAUDE_BIN") ?? cfg.claudeBin;
|
|
36
35
|
cfg.codexBin = optionalEnv("TOKENMAXXING_CODEX_BIN") ?? cfg.codexBin;
|
|
37
36
|
cfg.grokBin = optionalEnv("TOKENMAXXING_GROK_BIN") ?? cfg.grokBin;
|
|
@@ -42,10 +41,7 @@ export function loadConfig(): Config {
|
|
|
42
41
|
export type BinKey = "claudeBin" | "codexBin" | "grokBin" | "opencodeBin";
|
|
43
42
|
|
|
44
43
|
export function pinBinOverride(input: { key: BinKey; bin: string }): void {
|
|
45
|
-
|
|
46
|
-
if (existsSync(paths.configJson)) {
|
|
47
|
-
raw = z.record(z.string(), z.unknown()).parse(JSON.parse(readFileSync(paths.configJson, "utf8")));
|
|
48
|
-
}
|
|
44
|
+
const raw: Record<string, unknown> = existsSync(paths.configJson) ? readJsonFile(paths.configJson, z.record(z.string(), z.unknown())) : {};
|
|
49
45
|
raw[input.key] = input.bin;
|
|
50
46
|
writeFileAtomic(paths.configJson, JSON.stringify(raw, null, 2) + "\n");
|
|
51
47
|
}
|
|
@@ -54,17 +50,7 @@ const emptyIndex = (): AccountsIndex => ({ version: 2, accounts: [] });
|
|
|
54
50
|
|
|
55
51
|
export function loadAccounts(pool: PoolPaths): AccountsIndex {
|
|
56
52
|
if (!existsSync(pool.accountsJson)) return emptyIndex();
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
json = JSON.parse(readFileSync(pool.accountsJson, "utf8"));
|
|
60
|
-
} catch {
|
|
61
|
-
throw new Error(`${pool.accountsJson} is corrupt (unparsable JSON) - refusing to treat a damaged pool as empty; repair or remove the file`);
|
|
62
|
-
}
|
|
63
|
-
const parsed = AccountsIndexSchema.safeParse(json);
|
|
64
|
-
if (!parsed.success) {
|
|
65
|
-
throw new Error(`${pool.accountsJson} does not match the accounts schema - refusing to treat a damaged pool as empty; repair or remove the file`);
|
|
66
|
-
}
|
|
67
|
-
return parsed.data;
|
|
53
|
+
return readJsonFile(pool.accountsJson, AccountsIndexSchema);
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
export function saveAccounts(pool: PoolPaths, idx: AccountsIndex): void {
|
|
@@ -73,17 +59,7 @@ export function saveAccounts(pool: PoolPaths, idx: AccountsIndex): void {
|
|
|
73
59
|
|
|
74
60
|
function loadWaitQueue(): WaitClaim[] {
|
|
75
61
|
if (!existsSync(claudePool.waitQueueJson)) return [];
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
json = JSON.parse(readFileSync(claudePool.waitQueueJson, "utf8"));
|
|
79
|
-
} catch {
|
|
80
|
-
throw new Error(`${claudePool.waitQueueJson} is corrupt (unparsable JSON) - refusing to treat a damaged wait queue as empty; repair or remove the file`);
|
|
81
|
-
}
|
|
82
|
-
const parsed = WaitQueueSchema.safeParse(json);
|
|
83
|
-
if (!parsed.success) {
|
|
84
|
-
throw new Error(`${claudePool.waitQueueJson} does not match the wait-queue schema - refusing to treat a damaged wait queue as empty; repair or remove the file`);
|
|
85
|
-
}
|
|
86
|
-
return parsed.data.claims;
|
|
62
|
+
return readJsonFile(claudePool.waitQueueJson, WaitQueueSchema).claims;
|
|
87
63
|
}
|
|
88
64
|
|
|
89
65
|
function saveWaitQueue(claims: WaitClaim[]): void {
|
package/src/lib/update.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync
|
|
1
|
+
import { existsSync, lstatSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { writeFileAtomic } from "./atomic.ts";
|
|
@@ -8,6 +8,7 @@ import { isNixPackaged } from "./install.ts";
|
|
|
8
8
|
import { withLock } from "./lock.ts";
|
|
9
9
|
import { errorMessage, log } from "./log.ts";
|
|
10
10
|
import { env, HOME, optionalEnv, paths } from "./paths.ts";
|
|
11
|
+
import { readJsonFile } from "./state.ts";
|
|
11
12
|
|
|
12
13
|
const UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
13
14
|
const INSTALL_DEADLINE_MS = 60_000;
|
|
@@ -67,18 +68,12 @@ function detectGlobalRoot(): string | null {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function installedVersion(root: string): string {
|
|
70
|
-
return
|
|
71
|
+
return readJsonFile(join(packageDir(root), "package.json"), VersionSchema).version;
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function isDue(now: number): boolean {
|
|
74
75
|
if (!existsSync(updateJson)) return true;
|
|
75
|
-
|
|
76
|
-
try {
|
|
77
|
-
json = JSON.parse(readFileSync(updateJson, "utf8"));
|
|
78
|
-
} catch {
|
|
79
|
-
throw new Error(`${updateJson} is corrupt (unparsable JSON) - repair or remove the file`);
|
|
80
|
-
}
|
|
81
|
-
return now - AttemptSchema.parse(json).attemptedAt >= UPDATE_INTERVAL_MS;
|
|
76
|
+
return now - readJsonFile(updateJson, AttemptSchema).attemptedAt >= UPDATE_INTERVAL_MS;
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
async function updateToLatest(root: string): Promise<void> {
|
package/src/main.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { cmdCheck } from "./cli/check.ts";
|
|
|
23
23
|
import { cmdConfig } from "./cli/config.ts";
|
|
24
24
|
import { cmdSeat } from "./cli/seat.ts";
|
|
25
25
|
import { cmdServe } from "./cli/serve.ts";
|
|
26
|
-
import {
|
|
26
|
+
import { CHECK_JOB, deactivationHint, HUB_JOB, onLoginHome, uninstallSupervisor, uninstallTargets } from "./lib/install.ts";
|
|
27
27
|
import { errorMessage } from "./lib/log.ts";
|
|
28
28
|
import { HOME } from "./lib/paths.ts";
|
|
29
29
|
import { c, emitError } from "./cli/render.ts";
|
|
@@ -177,8 +177,8 @@ async function main(): Promise<number> {
|
|
|
177
177
|
...(out.pathLineRemoved ? ["rc PATH line"] : []),
|
|
178
178
|
];
|
|
179
179
|
console.log(`removed ${removed.join(", ")}`);
|
|
180
|
-
if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${
|
|
181
|
-
if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${
|
|
180
|
+
if (out.timer === "still-loaded") console.log(c.yellow(`⚠ the check job may still be loaded - run: ${deactivationHint(CHECK_JOB)}`));
|
|
181
|
+
if (out.hub === "still-loaded") console.log(c.yellow(`⚠ the usage hub job may still be loaded - run: ${deactivationHint(HUB_JOB)}`));
|
|
182
182
|
if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
|
|
183
183
|
console.log(`kept: accounts.json, config.json, and every account credential store (claude: stores/ and its keychain items on macOS; codex: codex-stores/; grok: grok-stores/; opencode-go: opencode-go-stores/) - remove accounts with \`xx rm\` to delete their credentials`);
|
|
184
184
|
return 0;
|