tokenmaxxing 1.9.1 → 1.11.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 +3 -3
- package/README.md +13 -11
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/pool-status/SKILL.md +2 -2
- package/agent-plugin/skills/pool-status/references/commands.md +1 -0
- package/agent-plugin/skills/safe-contribution/SKILL.md +3 -4
- package/agent-plugin/skills/sdk-pairing/SKILL.md +2 -1
- package/agent-plugin/skills/switching-policy/SKILL.md +2 -2
- package/agent-plugin/skills/switching-policy/references/policy.md +2 -2
- package/package.json +2 -3
- package/src/cli/check.ts +17 -4
- package/src/cli/codexrm.ts +8 -7
- package/src/cli/codexswitch.ts +50 -20
- package/src/cli/config.ts +86 -42
- package/src/cli/doctor.ts +25 -9
- package/src/cli/ls.ts +37 -2
- package/src/cli/rename.ts +17 -14
- package/src/cli/render.ts +20 -0
- package/src/cli/rm.ts +12 -8
- package/src/cli/status.ts +243 -112
- package/src/cli/switch.ts +75 -31
- package/src/cli/watch.ts +13 -7
- package/src/entries/codexstophook.ts +2 -2
- package/src/entries/mcp.ts +1 -1
- package/src/lib/codexdecide.ts +2 -2
- package/src/lib/decide.ts +54 -21
- package/src/lib/picker.ts +15 -2
- package/src/lib/state.ts +3 -2
- package/src/lib/types.ts +18 -5
- package/src/main.ts +44 -22
package/src/cli/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { paths, realClaudeBinFromEnv, realCodexBinFromEnv } from "../lib/paths.ts";
|
|
6
6
|
import { ConfigFileSchema, loadConfig, mergeConfigFile } from "../lib/state.ts";
|
|
7
7
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
8
|
-
import { c } from "./render.ts";
|
|
8
|
+
import { c, emitError, emitJson } from "./render.ts";
|
|
9
9
|
|
|
10
10
|
export const KNOWN_KEYS = [
|
|
11
11
|
"thresholds.session",
|
|
@@ -49,22 +49,53 @@ function unknownFileKeys(raw: Record<string, unknown>): string[] {
|
|
|
49
49
|
return dottedKeys(raw).filter((key) => !known.has(key));
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function isKnownKey(key: string): boolean {
|
|
53
|
+
return KNOWN_KEYS.some((known) => known === key);
|
|
54
|
+
}
|
|
55
|
+
|
|
52
56
|
function envSourceFor(key: string): string | null {
|
|
53
57
|
if (key === "claudeBin" && realClaudeBinFromEnv()) return "TOKENMAXXING_CLAUDE_BIN";
|
|
54
58
|
if (key === "codexBin" && realCodexBinFromEnv()) return "TOKENMAXXING_CODEX_BIN";
|
|
55
59
|
return null;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
function
|
|
62
|
+
function sourceOf(input: { key: string; raw: Record<string, unknown> }): { source: "env" | "file" | "default"; env: string | null } {
|
|
63
|
+
const env = envSourceFor(input.key);
|
|
64
|
+
if (env) return { source: "env", env };
|
|
65
|
+
return { source: get(input.raw, input.key) !== undefined ? "file" : "default", env: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function unknownKey(input: { key: string; json: boolean }): number {
|
|
69
|
+
emitError({
|
|
70
|
+
json: input.json,
|
|
71
|
+
message: `unknown config key: ${input.key}`,
|
|
72
|
+
notes: [`known keys: ${KNOWN_KEYS.join(", ")}`],
|
|
73
|
+
extra: { knownKeys: KNOWN_KEYS },
|
|
74
|
+
});
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function printEffective(json: boolean): number {
|
|
59
79
|
const effective = loadConfig();
|
|
60
80
|
const raw = readRawFile();
|
|
81
|
+
const unknown = unknownFileKeys(raw);
|
|
82
|
+
if (json) {
|
|
83
|
+
const sources: Record<string, string> = {};
|
|
84
|
+
const envOverrides: Record<string, string> = {};
|
|
85
|
+
for (const key of KNOWN_KEYS) {
|
|
86
|
+
const { source, env } = sourceOf({ key, raw });
|
|
87
|
+
sources[key] = source;
|
|
88
|
+
if (env) envOverrides[key] = env;
|
|
89
|
+
}
|
|
90
|
+
emitJson({ ok: true, path: paths.configJson, config: effective, sources, envOverrides, unknownKeys: unknown });
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
61
93
|
console.log(c.dim(`config.json: ${paths.configJson}`));
|
|
62
94
|
for (const key of KNOWN_KEYS) {
|
|
63
|
-
const env =
|
|
64
|
-
const
|
|
65
|
-
console.log(` ${key.padEnd(28)} ${JSON.stringify(get(effective, key))} ${
|
|
95
|
+
const { source, env } = sourceOf({ key, raw });
|
|
96
|
+
const painted = source === "env" ? c.yellow(`env ${env}`) : source === "file" ? c.green("file") : c.dim("default");
|
|
97
|
+
console.log(` ${key.padEnd(28)} ${JSON.stringify(get(effective, key))} ${painted}`);
|
|
66
98
|
}
|
|
67
|
-
const unknown = unknownFileKeys(raw);
|
|
68
99
|
if (unknown.length > 0) {
|
|
69
100
|
console.log();
|
|
70
101
|
console.log(c.yellow(`unknown keys in the file (ignored by the loader): ${unknown.join(", ")}`));
|
|
@@ -73,13 +104,14 @@ function printEffective(): number {
|
|
|
73
104
|
return 0;
|
|
74
105
|
}
|
|
75
106
|
|
|
76
|
-
function cmdGet(key: string): number {
|
|
77
|
-
if (!
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
function cmdGet(key: string, json: boolean): number {
|
|
108
|
+
if (!isKnownKey(key)) return unknownKey({ key, json });
|
|
109
|
+
const value = get(loadConfig(), key);
|
|
110
|
+
if (json) {
|
|
111
|
+
emitJson({ ok: true, key, value, source: sourceOf({ key, raw: readRawFile() }).source });
|
|
112
|
+
return 0;
|
|
81
113
|
}
|
|
82
|
-
console.log(JSON.stringify(
|
|
114
|
+
console.log(JSON.stringify(value));
|
|
83
115
|
return 0;
|
|
84
116
|
}
|
|
85
117
|
|
|
@@ -91,31 +123,31 @@ function parseValue(text: string): unknown {
|
|
|
91
123
|
}
|
|
92
124
|
}
|
|
93
125
|
|
|
94
|
-
function cmdSet(key: string, valueText: string): number {
|
|
95
|
-
if (!
|
|
96
|
-
console.error(c.red(`unknown config key: ${key}`));
|
|
97
|
-
console.error(c.dim(`known keys: ${KNOWN_KEYS.join(", ")}`));
|
|
98
|
-
return 1;
|
|
99
|
-
}
|
|
126
|
+
function cmdSet(key: string, valueText: string, json: boolean): number {
|
|
127
|
+
if (!isKnownKey(key)) return unknownKey({ key, json });
|
|
100
128
|
const raw = readRawFile();
|
|
101
129
|
const next = structuredClone(raw);
|
|
102
130
|
set(next, key, parseValue(valueText));
|
|
103
131
|
const validated = ConfigFileSchema.safeParse(next);
|
|
104
132
|
if (!validated.success) {
|
|
105
|
-
|
|
133
|
+
emitError({ json, message: `rejected: ${validated.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}` });
|
|
106
134
|
return 1;
|
|
107
135
|
}
|
|
108
136
|
const mergedCheck = mergeConfigFile(validated.data);
|
|
109
137
|
if (!mergedCheck.ok) {
|
|
110
|
-
|
|
138
|
+
emitError({ json, message: `rejected: ${mergedCheck.detail}` });
|
|
111
139
|
return 1;
|
|
112
140
|
}
|
|
113
141
|
writeRawFile({ raw: next });
|
|
114
142
|
const beforeFile = get(raw, key);
|
|
143
|
+
const env = envSourceFor(key);
|
|
144
|
+
if (json) {
|
|
145
|
+
emitJson({ ok: true, key, before: beforeFile === undefined ? null : beforeFile, after: get(next, key), envOverride: env });
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
115
148
|
console.log(
|
|
116
149
|
`${key}: ${beforeFile === undefined ? "(default)" : JSON.stringify(beforeFile)} -> ${JSON.stringify(get(next, key))}`,
|
|
117
150
|
);
|
|
118
|
-
const env = envSourceFor(key);
|
|
119
151
|
if (env) console.log(c.yellow(`note: ${env} is set and overrides the file value in this environment`));
|
|
120
152
|
return 0;
|
|
121
153
|
}
|
|
@@ -128,14 +160,12 @@ function pruneEmptyParents(raw: Record<string, unknown>): void {
|
|
|
128
160
|
}
|
|
129
161
|
}
|
|
130
162
|
|
|
131
|
-
function cmdUnset(key: string): number {
|
|
132
|
-
if (!
|
|
133
|
-
console.error(c.red(`unknown config key: ${key}`));
|
|
134
|
-
return 1;
|
|
135
|
-
}
|
|
163
|
+
function cmdUnset(key: string, json: boolean): number {
|
|
164
|
+
if (!isKnownKey(key)) return unknownKey({ key, json });
|
|
136
165
|
const raw = readRawFile();
|
|
137
166
|
if (get(raw, key) === undefined) {
|
|
138
|
-
|
|
167
|
+
if (json) emitJson({ ok: true, key, unset: false, value: get(loadConfig(), key) });
|
|
168
|
+
else console.log(c.dim(`${key} has no file override (default already applies)`));
|
|
139
169
|
return 0;
|
|
140
170
|
}
|
|
141
171
|
const next = structuredClone(raw);
|
|
@@ -143,20 +173,25 @@ function cmdUnset(key: string): number {
|
|
|
143
173
|
pruneEmptyParents(next);
|
|
144
174
|
const validated = ConfigFileSchema.safeParse(next);
|
|
145
175
|
if (!validated.success) {
|
|
146
|
-
|
|
176
|
+
emitError({ json, message: `rejected: ${validated.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}` });
|
|
147
177
|
return 1;
|
|
148
178
|
}
|
|
149
179
|
const mergedCheck = mergeConfigFile(validated.data);
|
|
150
180
|
if (!mergedCheck.ok) {
|
|
151
|
-
|
|
181
|
+
emitError({ json, message: `rejected: ${mergedCheck.detail} (adjust the conflicting override before unsetting ${key})` });
|
|
152
182
|
return 1;
|
|
153
183
|
}
|
|
154
184
|
writeRawFile({ raw: next });
|
|
155
|
-
|
|
185
|
+
const value = get(loadConfig(), key);
|
|
186
|
+
if (json) {
|
|
187
|
+
emitJson({ ok: true, key, unset: true, value });
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
console.log(`${key} unset -> ${JSON.stringify(value)} (default)`);
|
|
156
191
|
return 0;
|
|
157
192
|
}
|
|
158
193
|
|
|
159
|
-
function cmdTidy(): number {
|
|
194
|
+
function cmdTidy(json: boolean): number {
|
|
160
195
|
const raw = readRawFile();
|
|
161
196
|
const dropped = unknownFileKeys(raw);
|
|
162
197
|
const parsed = ConfigFileSchema.parse(raw);
|
|
@@ -167,30 +202,39 @@ function cmdTidy(): number {
|
|
|
167
202
|
if (normalized != null) set(next, "policy.switchModels", normalized);
|
|
168
203
|
pruneEmptyParents(next);
|
|
169
204
|
|
|
170
|
-
|
|
205
|
+
const changed = JSON.stringify(next) !== JSON.stringify(raw);
|
|
206
|
+
if (changed) writeRawFile({ raw: next });
|
|
207
|
+
if (json) {
|
|
208
|
+
emitJson({ ok: true, changed, droppedKeys: dropped, casingNormalized: casingChanged });
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (!changed) {
|
|
171
212
|
console.log(c.dim("nothing to tidy"));
|
|
172
213
|
return 0;
|
|
173
214
|
}
|
|
174
|
-
writeRawFile({ raw: next });
|
|
175
215
|
if (dropped.length > 0) console.log(`dropped unknown keys: ${dropped.join(", ")}`);
|
|
176
216
|
if (casingChanged) console.log("normalized switchModels casing");
|
|
177
217
|
if (dropped.length === 0 && !casingChanged) console.log("canonicalized file layout (pruned empty sections / key order)");
|
|
178
218
|
return 0;
|
|
179
219
|
}
|
|
180
220
|
|
|
181
|
-
export function cmdConfig(args: string[]): number {
|
|
221
|
+
export function cmdConfig(args: string[], json = false): number {
|
|
182
222
|
const [sub, key, value] = args;
|
|
183
223
|
try {
|
|
184
|
-
if (sub === undefined) return printEffective();
|
|
185
|
-
if (sub === "get" && key !== undefined) return cmdGet(key);
|
|
186
|
-
if (sub === "set" && key !== undefined && value !== undefined) return cmdSet(key, value);
|
|
187
|
-
if (sub === "unset" && key !== undefined) return cmdUnset(key);
|
|
188
|
-
if (sub === "tidy") return cmdTidy();
|
|
224
|
+
if (sub === undefined) return printEffective(json);
|
|
225
|
+
if (sub === "get" && key !== undefined) return cmdGet(key, json);
|
|
226
|
+
if (sub === "set" && key !== undefined && value !== undefined) return cmdSet(key, value, json);
|
|
227
|
+
if (sub === "unset" && key !== undefined) return cmdUnset(key, json);
|
|
228
|
+
if (sub === "tidy") return cmdTidy(json);
|
|
189
229
|
} catch (e) {
|
|
190
|
-
|
|
191
|
-
|
|
230
|
+
emitError({
|
|
231
|
+
json,
|
|
232
|
+
message: `config.json is unreadable: ${e instanceof Error ? e.message : String(e)}`,
|
|
233
|
+
notes: [`fix or delete ${paths.configJson} (defaults apply when it is absent), then re-run`],
|
|
234
|
+
extra: { path: paths.configJson },
|
|
235
|
+
});
|
|
192
236
|
return 1;
|
|
193
237
|
}
|
|
194
|
-
|
|
238
|
+
emitError({ json, message: "usage: tokenmaxxing config [get <key> | set <key> <value> | unset <key> | tidy]" });
|
|
195
239
|
return 2;
|
|
196
240
|
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
|
7
7
|
import { readItem, liveTarget, parkedTarget } from "../lib/credstore.ts";
|
|
8
8
|
import { isAccessTokenExpiring, fetchTokenOrg } from "../lib/oauth.ts";
|
|
9
9
|
import { CredentialBlobSchema, type RolesResponse } from "../lib/types.ts";
|
|
10
|
-
import { c } from "./render.ts";
|
|
10
|
+
import { c, emitJson } from "./render.ts";
|
|
11
11
|
|
|
12
12
|
async function blobOrg(raw: string): Promise<RolesResponse | null> {
|
|
13
13
|
const creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
|
|
@@ -15,11 +15,21 @@ async function blobOrg(raw: string): Promise<RolesResponse | null> {
|
|
|
15
15
|
return fetchTokenOrg(creds.accessToken);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export async function cmdDoctor(): Promise<number> {
|
|
19
|
-
|
|
18
|
+
export async function cmdDoctor(json = false): Promise<number> {
|
|
19
|
+
const checks: { ok: boolean; label: string; hint: string | null }[] = [];
|
|
20
|
+
const notes: string[] = [];
|
|
21
|
+
const warnings: string[] = [];
|
|
20
22
|
const check = (cond: boolean, label: string, hint?: string) => {
|
|
21
|
-
|
|
22
|
-
if (!cond)
|
|
23
|
+
checks.push({ ok: cond, label, hint: cond ? null : (hint ?? null) });
|
|
24
|
+
if (!json) console.log(`${cond ? c.green("✓") : c.red("✗")} ${label}${!cond && hint ? c.dim(` - ${hint}`) : ""}`);
|
|
25
|
+
};
|
|
26
|
+
const note = (text: string) => {
|
|
27
|
+
notes.push(text);
|
|
28
|
+
if (!json) console.log(c.dim(` - ${text}`));
|
|
29
|
+
};
|
|
30
|
+
const warn = (text: string) => {
|
|
31
|
+
warnings.push(text);
|
|
32
|
+
if (!json) console.log(c.yellow(`⚠ ${text}`));
|
|
23
33
|
};
|
|
24
34
|
|
|
25
35
|
check(existsSync(paths.supervisorLink), "claude supervisor wrapper present", "run `tokenmaxxing init`");
|
|
@@ -46,7 +56,7 @@ export async function cmdDoctor(): Promise<number> {
|
|
|
46
56
|
try {
|
|
47
57
|
const org = await blobOrg(live);
|
|
48
58
|
if (org) check(org.organization_uuid === active.organizationUuid, `live credential identity matches active (${active.email})`, `token belongs to ${org.organization_name} - run \`tokenmaxxing switch\``);
|
|
49
|
-
else
|
|
59
|
+
else note("live credential identity unverifiable (access token expired)");
|
|
50
60
|
} catch (e) {
|
|
51
61
|
check(false, `live credential identity matches active (${active.email})`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
|
|
52
62
|
}
|
|
@@ -59,7 +69,7 @@ export async function cmdDoctor(): Promise<number> {
|
|
|
59
69
|
try {
|
|
60
70
|
const org = await blobOrg(parked);
|
|
61
71
|
if (org) check(org.organization_uuid === a.organizationUuid, `parked credential identity matches ${a.email}`, `token belongs to ${org.organization_name} - run \`tokenmaxxing auth ${a.label}\``);
|
|
62
|
-
else
|
|
72
|
+
else note(`${a.email} identity unverifiable (access token expired)`);
|
|
63
73
|
} catch (e) {
|
|
64
74
|
check(false, `parked credential identity matches ${a.email}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
|
|
65
75
|
}
|
|
@@ -77,11 +87,17 @@ export async function cmdDoctor(): Promise<number> {
|
|
|
77
87
|
const rc = shellRcPath();
|
|
78
88
|
if (rc && existsSync(rc)) {
|
|
79
89
|
for (const s of findClaudeShadowers(readFileSync(rc, "utf8"))) {
|
|
80
|
-
if (s.kind === "shadow")
|
|
81
|
-
else
|
|
90
|
+
if (s.kind === "shadow") warn(`${rc}: ${s.line.startsWith("alias ") ? "alias" : "function"} \`claude\` shadows the supervised claude wrapper - launches through it skip tokenmaxxing`);
|
|
91
|
+
else warn(`${rc}: alias \`${s.name}\` hardcodes a claude path and bypasses the supervisor - use plain \`claude\` in its body instead`);
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
const failed = checks.filter((entry) => !entry.ok).length;
|
|
96
|
+
const ok = failed === 0;
|
|
97
|
+
if (json) {
|
|
98
|
+
emitJson(ok ? { ok, checks, notes, warnings } : { ok, error: `issues found - ${failed} of ${checks.length} checks failed`, checks, notes, warnings });
|
|
99
|
+
return ok ? 0 : 1;
|
|
100
|
+
}
|
|
85
101
|
console.log();
|
|
86
102
|
console.log(ok ? c.green("all good ✓") : c.yellow("issues found - see above"));
|
|
87
103
|
return ok ? 0 : 1;
|
package/src/cli/ls.ts
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
1
|
import { loadAccounts } from "../lib/state.ts";
|
|
2
2
|
import { loadCodexAccounts } from "../lib/codexstate.ts";
|
|
3
3
|
import { liveCodexAccountId } from "../lib/codexsample.ts";
|
|
4
|
-
import { c, claudeTierLabel } from "./render.ts";
|
|
4
|
+
import { c, claudeTierLabel, emitJson } from "./render.ts";
|
|
5
5
|
|
|
6
|
-
export function cmdLs(): number {
|
|
6
|
+
export function cmdLs(json = false): number {
|
|
7
7
|
const idx = loadAccounts();
|
|
8
|
+
|
|
9
|
+
if (json) {
|
|
10
|
+
const codex = loadCodexAccounts();
|
|
11
|
+
const liveId = codex.accounts.length > 0 ? liveCodexAccountId() : null;
|
|
12
|
+
emitJson({
|
|
13
|
+
ok: true,
|
|
14
|
+
claude: {
|
|
15
|
+
activeAccountUuid: idx.activeAccountUuid,
|
|
16
|
+
accounts: idx.accounts.map((a) => ({
|
|
17
|
+
label: a.label,
|
|
18
|
+
email: a.email,
|
|
19
|
+
accountUuid: a.accountUuid,
|
|
20
|
+
organizationUuid: a.organizationUuid,
|
|
21
|
+
tier: claudeTierLabel(a),
|
|
22
|
+
active: a.accountUuid === idx.activeAccountUuid,
|
|
23
|
+
needsReauth: a.needsReauth === true,
|
|
24
|
+
addedAt: a.addedAt,
|
|
25
|
+
})),
|
|
26
|
+
},
|
|
27
|
+
codex: {
|
|
28
|
+
activeAccountId: liveId,
|
|
29
|
+
accounts: codex.accounts.map((account) => ({
|
|
30
|
+
label: account.label,
|
|
31
|
+
email: account.email,
|
|
32
|
+
accountId: account.accountId,
|
|
33
|
+
planType: account.planType,
|
|
34
|
+
active: account.accountId === liveId,
|
|
35
|
+
needsReauth: account.needsReauth === true,
|
|
36
|
+
addedAt: account.addedAt,
|
|
37
|
+
})),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
8
43
|
if (idx.accounts.length === 0) {
|
|
9
44
|
console.log(c.dim("no accounts yet - run `tokenmaxxing init` then `tokenmaxxing add`"));
|
|
10
45
|
}
|
package/src/cli/rename.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { withLock } from "../lib/lock.ts";
|
|
|
2
2
|
import { codexPaths, paths } from "../lib/paths.ts";
|
|
3
3
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
4
4
|
import { loadCodexAccounts, saveCodexAccounts } from "../lib/codexstate.ts";
|
|
5
|
-
import { c } from "./render.ts";
|
|
5
|
+
import { c, emitError, emitJson, plain } from "./render.ts";
|
|
6
6
|
import type { Account, CodexAccount } from "../lib/types.ts";
|
|
7
7
|
|
|
8
8
|
export function findAccount(accounts: Account[], selector: string): Account | undefined {
|
|
@@ -23,51 +23,54 @@ export function findCodexAccount(accounts: CodexAccount[], selector: string): Co
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
async function renameCodexAccount(input: { selector: string; newLabel: string }): Promise<number> {
|
|
26
|
+
async function renameCodexAccount(input: { selector: string; newLabel: string; json: boolean }): Promise<number> {
|
|
27
|
+
const { selector, newLabel, json } = input;
|
|
27
28
|
return withLock(codexPaths.lockFile, async () => {
|
|
28
29
|
const index = loadCodexAccounts();
|
|
29
|
-
const account = findCodexAccount(index.accounts,
|
|
30
|
+
const account = findCodexAccount(index.accounts, selector);
|
|
30
31
|
if (!account) {
|
|
31
|
-
|
|
32
|
+
emitError({ json, message: `no codex account matches "${selector}"` });
|
|
32
33
|
return 1;
|
|
33
34
|
}
|
|
34
|
-
const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() ===
|
|
35
|
+
const taken = index.accounts.find((x) => x.accountId !== account.accountId && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
35
36
|
if (taken) {
|
|
36
|
-
|
|
37
|
+
emitError({ json, message: `label "${newLabel}" is already used by ${taken.accountId.slice(0, 8)} - labels must be unique within the pool` });
|
|
37
38
|
return 1;
|
|
38
39
|
}
|
|
39
40
|
const old = account.label;
|
|
40
|
-
account.label =
|
|
41
|
+
account.label = newLabel;
|
|
41
42
|
saveCodexAccounts({ index });
|
|
42
|
-
|
|
43
|
+
if (json) emitJson({ ok: true, pool: "codex", from: old, to: newLabel });
|
|
44
|
+
else console.log(`renamed codex account ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
43
45
|
return 0;
|
|
44
46
|
});
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
export async function cmdRename(argv: string[]): Promise<number> {
|
|
49
|
+
export async function cmdRename(argv: string[], json = false): Promise<number> {
|
|
48
50
|
const codex = argv.includes("--codex");
|
|
49
51
|
const [selector, newLabel] = argv.filter((a) => a !== "--codex");
|
|
50
52
|
if (!selector || !newLabel) {
|
|
51
|
-
|
|
53
|
+
emitError({ json, message: "usage: tokenmaxxing rename [--codex] <email|label|id> <new-label>", paint: plain });
|
|
52
54
|
return 2;
|
|
53
55
|
}
|
|
54
|
-
if (codex) return renameCodexAccount({ selector, newLabel });
|
|
56
|
+
if (codex) return renameCodexAccount({ selector, newLabel, json });
|
|
55
57
|
return withLock(paths.lockFile, async () => {
|
|
56
58
|
const idx = loadAccounts();
|
|
57
59
|
const a = findAccount(idx.accounts, selector);
|
|
58
60
|
if (!a) {
|
|
59
|
-
|
|
61
|
+
emitError({ json, message: `no claude account matches "${selector}" (codex accounts rename via --codex)` });
|
|
60
62
|
return 1;
|
|
61
63
|
}
|
|
62
64
|
const taken = idx.accounts.find((x) => x.accountUuid !== a.accountUuid && x.label.toLowerCase() === newLabel.toLowerCase());
|
|
63
65
|
if (taken) {
|
|
64
|
-
|
|
66
|
+
emitError({ json, message: `label "${newLabel}" is already used by ${taken.email} - labels must be unique within the pool` });
|
|
65
67
|
return 1;
|
|
66
68
|
}
|
|
67
69
|
const old = a.label;
|
|
68
70
|
a.label = newLabel;
|
|
69
71
|
saveAccounts(idx);
|
|
70
|
-
|
|
72
|
+
if (json) emitJson({ ok: true, pool: "claude", from: old, to: newLabel });
|
|
73
|
+
else console.log(`renamed ${c.dim(old)} → ${c.bold(newLabel)}`);
|
|
71
74
|
return 0;
|
|
72
75
|
});
|
|
73
76
|
}
|
package/src/cli/render.ts
CHANGED
|
@@ -75,3 +75,23 @@ export function fmtAgo(epochMs: number, now = Date.now()): string {
|
|
|
75
75
|
return `${m}m ago`;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
export const plain = (s: string): string => s;
|
|
79
|
+
|
|
80
|
+
export function emitJson(value: unknown): void {
|
|
81
|
+
console.log(JSON.stringify(value, null, 2));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function emitError(input: {
|
|
85
|
+
json: boolean;
|
|
86
|
+
message: string;
|
|
87
|
+
notes?: string[];
|
|
88
|
+
extra?: Record<string, unknown>;
|
|
89
|
+
paint?: (s: string) => string;
|
|
90
|
+
}): void {
|
|
91
|
+
if (input.json) {
|
|
92
|
+
emitJson({ ok: false, error: input.message, ...input.extra });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.error((input.paint ?? c.red)(input.message));
|
|
96
|
+
for (const note of input.notes ?? []) console.error(c.dim(note));
|
|
97
|
+
}
|
package/src/cli/rm.ts
CHANGED
|
@@ -7,22 +7,22 @@ import { credItemFor, paths } from "../lib/paths.ts";
|
|
|
7
7
|
import { loadAccounts, saveAccounts } from "../lib/state.ts";
|
|
8
8
|
import { CredentialBlobSchema } from "../lib/types.ts";
|
|
9
9
|
import { findAccount } from "./rename.ts";
|
|
10
|
-
import { c } from "./render.ts";
|
|
10
|
+
import { c, emitError, emitJson, plain } from "./render.ts";
|
|
11
11
|
|
|
12
|
-
export async function cmdRm(selector?: string): Promise<number> {
|
|
12
|
+
export async function cmdRm(selector?: string, json = false): Promise<number> {
|
|
13
13
|
if (!selector) {
|
|
14
|
-
|
|
14
|
+
emitError({ json, message: "usage: tokenmaxxing rm <email|label|uuid>", paint: plain });
|
|
15
15
|
return 2;
|
|
16
16
|
}
|
|
17
17
|
return withLock(paths.lockFile, async () => {
|
|
18
18
|
const idx = loadAccounts();
|
|
19
19
|
const a = findAccount(idx.accounts, selector);
|
|
20
20
|
if (!a) {
|
|
21
|
-
|
|
21
|
+
emitError({ json, message: `no account matches "${selector}"` });
|
|
22
22
|
return 1;
|
|
23
23
|
}
|
|
24
24
|
if (a.accountUuid === idx.activeAccountUuid) {
|
|
25
|
-
|
|
25
|
+
emitError({ json, message: `${a.email} is the ACTIVE account - switch away before removing it.` });
|
|
26
26
|
return 1;
|
|
27
27
|
}
|
|
28
28
|
const live = await readItem(liveTarget());
|
|
@@ -32,11 +32,14 @@ export async function cmdRm(selector?: string): Promise<number> {
|
|
|
32
32
|
const liveCreds = CredentialBlobSchema.parse(JSON.parse(live)).claudeAiOauth;
|
|
33
33
|
liveOrg = (await fetchTokenOrg(liveCreds.accessToken)).organization_uuid;
|
|
34
34
|
} catch (e) {
|
|
35
|
-
|
|
35
|
+
emitError({
|
|
36
|
+
json,
|
|
37
|
+
message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the roles endpoint is reachable.`,
|
|
38
|
+
});
|
|
36
39
|
return 1;
|
|
37
40
|
}
|
|
38
41
|
if (liveOrg === a.organizationUuid) {
|
|
39
|
-
|
|
42
|
+
emitError({ json, message: `${a.email}'s credential is currently LIVE (the active label is stale - a manual /login drifted it); run \`tokenmaxxing switch\` to move off it first.` });
|
|
40
43
|
return 1;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -46,7 +49,8 @@ export async function cmdRm(selector?: string): Promise<number> {
|
|
|
46
49
|
rmSync(sampleDir, { recursive: true, force: true });
|
|
47
50
|
idx.accounts = idx.accounts.filter((x) => x.accountUuid !== a.accountUuid);
|
|
48
51
|
saveAccounts(idx);
|
|
49
|
-
|
|
52
|
+
if (json) emitJson({ ok: true, pool: "claude", removed: a.label, remaining: idx.accounts.length });
|
|
53
|
+
else console.log(`removed ${c.bold(a.label)} from the pool (${idx.accounts.length} left)`);
|
|
50
54
|
return 0;
|
|
51
55
|
});
|
|
52
56
|
}
|