tokenmaxxing 0.16.0 → 0.18.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 +14 -1
- package/README.md +2 -0
- package/package.json +5 -1
- package/src/cli/add.ts +13 -92
- package/src/cli/auth.ts +177 -0
- package/src/cli/doctor.ts +4 -3
- package/src/cli/onboard.ts +112 -0
- package/src/cli/render.ts +27 -0
- package/src/cli/serve.ts +266 -0
- package/src/cli/status.ts +18 -3
- package/src/cli/switch.ts +2 -2
- package/src/entries/statusline.ts +53 -53
- package/src/entries/subagentstatusline.ts +78 -0
- package/src/lib/paths.ts +7 -0
- package/src/lib/picker.ts +11 -2
- package/src/lib/sample.ts +2 -2
- package/src/lib/settings.ts +17 -6
- package/src/lib/slackbridge.ts +107 -0
- package/src/lib/slackstate.ts +125 -0
- package/src/lib/types.ts +22 -0
- package/src/main.ts +8 -0
package/src/lib/sample.ts
CHANGED
|
@@ -68,13 +68,13 @@ function refreshPlanFields(account: Account, creds: OAuthCreds): void {
|
|
|
68
68
|
export async function probeParkedUsage(account: Account, opts: { ping?: boolean } = {}): Promise<SampleOutcome> {
|
|
69
69
|
const backup = parkedTarget(account.keychainItem);
|
|
70
70
|
const parkedRaw = await readItem(backup);
|
|
71
|
-
if (!parkedRaw) return { ok: false, reason: "no parked credential -
|
|
71
|
+
if (!parkedRaw) return { ok: false, reason: "no parked credential - run `tokenmaxxing auth`" };
|
|
72
72
|
|
|
73
73
|
let creds: OAuthCreds;
|
|
74
74
|
try {
|
|
75
75
|
creds = CredentialBlobSchema.parse(JSON.parse(parkedRaw)).claudeAiOauth;
|
|
76
76
|
} catch (e) {
|
|
77
|
-
return { ok: false, reason: `parked credential unreadable (${String((e as Error).message ?? e).slice(0, 80)}) -
|
|
77
|
+
return { ok: false, reason: `parked credential unreadable (${String((e as Error).message ?? e).slice(0, 80)}) - run \`tokenmaxxing auth\`` };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Hand claude a token with comfortable headroom so it won't run its own refresh
|
package/src/lib/settings.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Idempotent merge of tokenmaxxing's
|
|
2
|
-
// ~/.claude/settings.json: our statusLine, a Stop hook,
|
|
3
|
-
// Hooks APPEND to existing arrays; the statusLine
|
|
4
|
-
// tokenmaxxing renders
|
|
1
|
+
// Idempotent merge of tokenmaxxing's entries into the user-owned
|
|
2
|
+
// ~/.claude/settings.json: our statusLine + subagentStatusLine, a Stop hook,
|
|
3
|
+
// a SessionStart hook. Hooks APPEND to existing arrays; the statusLine slots
|
|
4
|
+
// are ours outright - tokenmaxxing renders them natively, so any other
|
|
5
|
+
// statusLine command is replaced.
|
|
5
6
|
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
@@ -19,6 +20,7 @@ const HookGroupSchema = z.looseObject({ matcher: z.string().optional(), hooks: z
|
|
|
19
20
|
const StatusLineSchema = z.looseObject({ type: z.string(), command: z.string() });
|
|
20
21
|
const SettingsSchema = z.looseObject({
|
|
21
22
|
statusLine: StatusLineSchema.optional(),
|
|
23
|
+
subagentStatusLine: StatusLineSchema.optional(),
|
|
22
24
|
hooks: z.record(z.string(), z.array(HookGroupSchema)).optional(),
|
|
23
25
|
});
|
|
24
26
|
type Settings = z.infer<typeof SettingsSchema>;
|
|
@@ -26,6 +28,7 @@ type HookGroup = z.infer<typeof HookGroupSchema>;
|
|
|
26
28
|
|
|
27
29
|
const SUBCMD = {
|
|
28
30
|
statusline: "__statusline",
|
|
31
|
+
subagentStatusline: "__subagent-statusline",
|
|
29
32
|
stop: "__stop-hook",
|
|
30
33
|
sessionStart: "__session-start",
|
|
31
34
|
} as const;
|
|
@@ -44,6 +47,7 @@ export function isOurCommand(cmd: string | undefined): boolean {
|
|
|
44
47
|
if (!cmd) return false;
|
|
45
48
|
return (
|
|
46
49
|
cmd.includes(SUBCMD.statusline) ||
|
|
50
|
+
cmd.includes(SUBCMD.subagentStatusline) ||
|
|
47
51
|
cmd.includes(SUBCMD.stop) ||
|
|
48
52
|
cmd.includes(SUBCMD.sessionStart) ||
|
|
49
53
|
// also match the installed bin path even if the subcommand text changes
|
|
@@ -70,29 +74,35 @@ function removeHook(s: Settings, event: string, sub: string): void {
|
|
|
70
74
|
if (s.hooks![event]!.length === 0) delete s.hooks![event];
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
/** Install the
|
|
77
|
+
/** Install the entries: take both statusLine slots, append our hooks. */
|
|
74
78
|
export function installSettings(): void {
|
|
75
79
|
const s = readSettings();
|
|
76
80
|
s.statusLine = {
|
|
77
81
|
type: "command",
|
|
78
82
|
command: `${JSON.stringify(installedBin())} ${SUBCMD.statusline}`,
|
|
79
83
|
};
|
|
84
|
+
s.subagentStatusLine = {
|
|
85
|
+
type: "command",
|
|
86
|
+
command: `${JSON.stringify(installedBin())} ${SUBCMD.subagentStatusline}`,
|
|
87
|
+
};
|
|
80
88
|
appendHook(s, "Stop", SUBCMD.stop);
|
|
81
89
|
appendHook(s, "SessionStart", SUBCMD.sessionStart);
|
|
82
90
|
writeSettings(s);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
/** Remove our
|
|
93
|
+
/** Remove our entries. The statusLine slots are deleted only if they are ours. */
|
|
86
94
|
export function uninstallSettings(): void {
|
|
87
95
|
const s = readSettings();
|
|
88
96
|
removeHook(s, "Stop", SUBCMD.stop);
|
|
89
97
|
removeHook(s, "SessionStart", SUBCMD.sessionStart);
|
|
90
98
|
if (s.statusLine && isOurCommand(s.statusLine.command)) delete s.statusLine;
|
|
99
|
+
if (s.subagentStatusLine && isOurCommand(s.subagentStatusLine.command)) delete s.subagentStatusLine;
|
|
91
100
|
writeSettings(s);
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
const SettingsCheckSchema = z.object({
|
|
95
104
|
statusLineOk: z.boolean(),
|
|
105
|
+
subagentStatusLineOk: z.boolean(),
|
|
96
106
|
stopOk: z.boolean(),
|
|
97
107
|
sessionStartOk: z.boolean(),
|
|
98
108
|
});
|
|
@@ -104,6 +114,7 @@ export function checkSettings(): SettingsCheck {
|
|
|
104
114
|
!!s.hooks?.[event]?.some((g) => g.hooks?.some((h) => h.command?.includes(sub)));
|
|
105
115
|
return {
|
|
106
116
|
statusLineOk: isOurCommand(s.statusLine?.command),
|
|
117
|
+
subagentStatusLineOk: isOurCommand(s.subagentStatusLine?.command),
|
|
107
118
|
stopOk: has("Stop", SUBCMD.stop),
|
|
108
119
|
sessionStartOk: has("SessionStart", SUBCMD.sessionStart),
|
|
109
120
|
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// The Slack->claude relay. One claude turn per Slack message via the Agent SDK
|
|
2
|
+
// (re-query with resume, never a persistent streaming query: the SDK subprocess
|
|
3
|
+
// reads credentials at spawn, so per-turn spawns are what let the pool decision
|
|
4
|
+
// pick the freshest account at every boundary and let the daemon restart
|
|
5
|
+
// without losing threads). Verified against @anthropic-ai/claude-agent-sdk
|
|
6
|
+
// 0.3.214 and code.claude.com/docs 2026-07-18; both change monthly.
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
12
|
+
import { ensureBestAccount, pooledOptions, stopHookCheck } from "../sdk.ts";
|
|
13
|
+
import { paths } from "./paths.ts";
|
|
14
|
+
import { threadKey, type SlackLink } from "./slackstate.ts";
|
|
15
|
+
import { log } from "./log.ts";
|
|
16
|
+
|
|
17
|
+
/** Mutated in place by relayTurn so the caller can hand the generator straight
|
|
18
|
+
* to thread.post(AsyncIterable) and still read the turn's outcome afterward. */
|
|
19
|
+
export const TurnOutcomeSchema = z.object({
|
|
20
|
+
sessionId: z.string().nullable(),
|
|
21
|
+
failed: z.boolean(),
|
|
22
|
+
});
|
|
23
|
+
export type TurnOutcome = z.infer<typeof TurnOutcomeSchema>;
|
|
24
|
+
|
|
25
|
+
/** Run one git command against a repo; throws with trimmed stderr on failure. */
|
|
26
|
+
function git(repo: string, args: string[]): string {
|
|
27
|
+
const r = Bun.spawnSync(["git", "-C", repo, ...args], { stdout: "pipe", stderr: "pipe" });
|
|
28
|
+
const stderr = new TextDecoder().decode(r.stderr).trim();
|
|
29
|
+
if (r.exitCode !== 0) throw new Error(`git ${args[0]} failed: ${stderr.slice(0, 200)}`);
|
|
30
|
+
return new TextDecoder().decode(r.stdout).trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The stable cwd a thread's turns run in. Worktree mode (default) creates
|
|
35
|
+
* `slack-worktrees/<threadKey>` on branch `tm-slack-<threadKey>` cut from the
|
|
36
|
+
* repo's current HEAD, once; both survive daemon restarts because resume is
|
|
37
|
+
* cwd-keyed. Worktrees are never auto-deleted (they hold the thread's work):
|
|
38
|
+
* clean up with `git worktree remove` when done.
|
|
39
|
+
*/
|
|
40
|
+
export function ensureThreadCwd(input: { link: SlackLink; threadId: string }): string {
|
|
41
|
+
if (!input.link.worktree) return input.link.repo;
|
|
42
|
+
const key = threadKey(input.threadId);
|
|
43
|
+
const dir = join(paths.slackWorktreesDir, key);
|
|
44
|
+
if (existsSync(dir)) return dir;
|
|
45
|
+
mkdirSync(paths.slackWorktreesDir, { recursive: true });
|
|
46
|
+
const branch = `tm-slack-${key}`;
|
|
47
|
+
const branchExists = Bun.spawnSync(["git", "-C", input.link.repo, "rev-parse", "--verify", "--quiet", branch]).exitCode === 0;
|
|
48
|
+
// a crash between branch creation and worktree add leaves the branch behind;
|
|
49
|
+
// reattach instead of failing on -b collision.
|
|
50
|
+
if (branchExists) git(input.link.repo, ["worktree", "add", dir, branch]);
|
|
51
|
+
else git(input.link.repo, ["worktree", "add", dir, "-b", branch]);
|
|
52
|
+
log("serve.worktree_created", { dir, branch });
|
|
53
|
+
return dir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* One claude turn, yielded as streaming text deltas (feed directly to
|
|
58
|
+
* thread.post). Never throws: a failure yields a short diagnostic line and
|
|
59
|
+
* sets outcome.failed (the daemon must keep serving other threads). Error
|
|
60
|
+
* text is message-only - a raw error body could echo request material.
|
|
61
|
+
*/
|
|
62
|
+
export async function* relayTurn(
|
|
63
|
+
input: { cwd: string; sessionId: string | null; prompt: string; link: SlackLink },
|
|
64
|
+
outcome: TurnOutcome,
|
|
65
|
+
): AsyncGenerator<string> {
|
|
66
|
+
let yieldedAny = false;
|
|
67
|
+
try {
|
|
68
|
+
// the switch decision runs at the spawn boundary, same as the CLI hooks.
|
|
69
|
+
await ensureBestAccount();
|
|
70
|
+
const q = query({
|
|
71
|
+
prompt: input.prompt,
|
|
72
|
+
options: {
|
|
73
|
+
...pooledOptions(),
|
|
74
|
+
cwd: input.cwd,
|
|
75
|
+
permissionMode: input.link.permissionMode,
|
|
76
|
+
includePartialMessages: true,
|
|
77
|
+
hooks: { Stop: [{ hooks: [stopHookCheck] }] },
|
|
78
|
+
...(input.link.model ? { model: input.link.model } : {}),
|
|
79
|
+
...(input.sessionId ? { resume: input.sessionId } : {}),
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
let result: string | null = null;
|
|
83
|
+
for await (const message of q) {
|
|
84
|
+
if (message.type === "system" && message.subtype === "init") outcome.sessionId = message.session_id;
|
|
85
|
+
if (message.type === "stream_event") {
|
|
86
|
+
const event = message.event;
|
|
87
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
88
|
+
yieldedAny = true;
|
|
89
|
+
yield event.delta.text;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (message.type === "result") {
|
|
93
|
+
outcome.sessionId = message.session_id;
|
|
94
|
+
if (message.subtype === "success") result = message.result;
|
|
95
|
+
else outcome.failed = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// a turn that produced no streamed text (tool-only turns) still reports.
|
|
99
|
+
if (!yieldedAny && result) yield result;
|
|
100
|
+
if (!yieldedAny && !result && outcome.failed) yield "the turn ended without a result (limit or error) - trying again may help";
|
|
101
|
+
} catch (e) {
|
|
102
|
+
outcome.failed = true;
|
|
103
|
+
const detail = (e instanceof Error ? e.message : String(e)).slice(0, 300);
|
|
104
|
+
log("serve.turn_error", { err: detail });
|
|
105
|
+
yield `${yieldedAny ? "\n\n" : ""}tokenmaxxing: turn failed: ${detail}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `xx serve` state. slack.json holds the Slack tokens plus the channel->repo
|
|
2
|
+
// links; it is credential material (xoxb-/xapp-), so it is written 0600 and its
|
|
3
|
+
// tokens must never be printed. Per-thread records under slack-threads/ pin the
|
|
4
|
+
// claude session id + cwd a Slack thread resumes into (resume is cwd-keyed, so
|
|
5
|
+
// the cwd must stay byte-stable for the thread's whole life). A present but
|
|
6
|
+
// unparseable slack.json THROWS (no silent empty config); absent = null.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { paths } from "./paths.ts";
|
|
12
|
+
import { writeFileAtomic } from "./atomic.ts";
|
|
13
|
+
|
|
14
|
+
/** Claude Agent SDK permission modes an unattended bridge may run under.
|
|
15
|
+
* acceptEdits (default) auto-approves file edits but not arbitrary Bash;
|
|
16
|
+
* bypassPermissions is full autonomy - a per-link, user-chosen risk posture. */
|
|
17
|
+
export const ServePermissionModeSchema = z.enum(["default", "acceptEdits", "bypassPermissions", "dontAsk", "plan"]);
|
|
18
|
+
|
|
19
|
+
export const SlackLinkSchema = z.object({
|
|
20
|
+
/** Slack channel id (C.../G...). Names are not stored - ids are stable. */
|
|
21
|
+
channel: z.string(),
|
|
22
|
+
/** absolute path of the git repo this channel drives. */
|
|
23
|
+
repo: z.string(),
|
|
24
|
+
/** run each thread in its own git worktree (user default 2026-07-18: true). */
|
|
25
|
+
worktree: z.boolean().default(true),
|
|
26
|
+
permissionMode: ServePermissionModeSchema.default("acceptEdits"),
|
|
27
|
+
/** optional model override for this repo's sessions. */
|
|
28
|
+
model: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
export type SlackLink = z.infer<typeof SlackLinkSchema>;
|
|
31
|
+
|
|
32
|
+
export const SlackConfigSchema = z.object({
|
|
33
|
+
botToken: z.string().startsWith("xoxb-"),
|
|
34
|
+
appToken: z.string().startsWith("xapp-"),
|
|
35
|
+
links: z.array(SlackLinkSchema).default([]),
|
|
36
|
+
});
|
|
37
|
+
export type SlackConfig = z.infer<typeof SlackConfigSchema>;
|
|
38
|
+
|
|
39
|
+
export function loadSlackConfig(): SlackConfig | null {
|
|
40
|
+
if (!existsSync(paths.slackJson)) return null;
|
|
41
|
+
return SlackConfigSchema.parse(JSON.parse(readFileSync(paths.slackJson, "utf8")));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function saveSlackConfig(cfg: SlackConfig): void {
|
|
45
|
+
writeFileAtomic(paths.slackJson, JSON.stringify(SlackConfigSchema.parse(cfg), null, 2) + "\n", 0o600);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A Slack channel id: C (public) or G (private/legacy) followed by uppercase
|
|
49
|
+
* alphanumerics. Structural check, no regex. */
|
|
50
|
+
export function isChannelId(s: string): boolean {
|
|
51
|
+
if (s.length < 2 || (s[0] !== "C" && s[0] !== "G")) return false;
|
|
52
|
+
const rest = s.slice(1);
|
|
53
|
+
return [...rest].every((ch) => (ch >= "0" && ch <= "9") || (ch >= "A" && ch <= "Z"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- per-thread session records -------------------------------------------
|
|
57
|
+
|
|
58
|
+
export const SlackThreadSchema = z.object({
|
|
59
|
+
/** chat-sdk thread id, e.g. "slack:C0123:1721300000.123456". */
|
|
60
|
+
threadId: z.string(),
|
|
61
|
+
repo: z.string(),
|
|
62
|
+
/** the dir every turn of this thread runs in (repo or its worktree). */
|
|
63
|
+
cwd: z.string(),
|
|
64
|
+
/** claude session id; null until the first turn's init message arrives. */
|
|
65
|
+
sessionId: z.string().nullable(),
|
|
66
|
+
createdAt: z.string(),
|
|
67
|
+
});
|
|
68
|
+
export type SlackThread = z.infer<typeof SlackThreadSchema>;
|
|
69
|
+
|
|
70
|
+
/** Filesystem-safe key for a thread id: alnum kept, everything else "-". */
|
|
71
|
+
export function threadKey(threadId: string): string {
|
|
72
|
+
return [...threadId]
|
|
73
|
+
.map((ch) => ((ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z") || (ch >= "0" && ch <= "9") ? ch : "-"))
|
|
74
|
+
.join("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function threadFile(threadId: string): string {
|
|
78
|
+
return join(paths.slackThreadsDir, `${threadKey(threadId)}.json`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function loadSlackThread(threadId: string): SlackThread | null {
|
|
82
|
+
const f = threadFile(threadId);
|
|
83
|
+
if (!existsSync(f)) return null;
|
|
84
|
+
return SlackThreadSchema.parse(JSON.parse(readFileSync(f, "utf8")));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function saveSlackThread(t: SlackThread): void {
|
|
88
|
+
writeFileAtomic(threadFile(t.threadId), JSON.stringify(SlackThreadSchema.parse(t), null, 2) + "\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function listSlackThreads(): SlackThread[] {
|
|
92
|
+
if (!existsSync(paths.slackThreadsDir)) return [];
|
|
93
|
+
const out: SlackThread[] = [];
|
|
94
|
+
for (const f of readdirSync(paths.slackThreadsDir)) {
|
|
95
|
+
if (!f.endsWith(".json")) continue;
|
|
96
|
+
out.push(SlackThreadSchema.parse(JSON.parse(readFileSync(join(paths.slackThreadsDir, f), "utf8"))));
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---- pure link edits (unit-tested) ----------------------------------------
|
|
102
|
+
|
|
103
|
+
export function upsertLink(cfg: SlackConfig, link: SlackLink): SlackConfig {
|
|
104
|
+
const links = cfg.links.filter((l) => l.channel !== link.channel);
|
|
105
|
+
links.push(link);
|
|
106
|
+
return { ...cfg, links };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function removeLink(cfg: SlackConfig, channel: string): SlackConfig | null {
|
|
110
|
+
if (!cfg.links.some((l) => l.channel === channel)) return null;
|
|
111
|
+
return { ...cfg, links: cfg.links.filter((l) => l.channel !== channel) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function linkForChannel(cfg: SlackConfig, channel: string): SlackLink | null {
|
|
115
|
+
return cfg.links.find((l) => l.channel === channel) ?? null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Strip a leading Slack mention token ("<@U0123> rest") from message text. */
|
|
119
|
+
export function stripLeadingMention(text: string): string {
|
|
120
|
+
const trimmed = text.trimStart();
|
|
121
|
+
if (!trimmed.startsWith("<@")) return text.trim();
|
|
122
|
+
const close = trimmed.indexOf(">");
|
|
123
|
+
if (close < 0) return text.trim();
|
|
124
|
+
return trimmed.slice(close + 1).trim();
|
|
125
|
+
}
|
package/src/lib/types.ts
CHANGED
|
@@ -196,6 +196,28 @@ export const StatusLineStdinSchema = RateLimitsStdinSchema.extend({
|
|
|
196
196
|
effort: z.looseObject({ level: z.string().optional() }).nullable().optional().catch(undefined),
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
/** subagentStatusLine stdin: base session fields plus one entry per active
|
|
200
|
+
* subagent task (verified against the 2.1.214 bundle + docs 2026-07-18;
|
|
201
|
+
* tasks[].model/contextWindowSize need claude >= 2.1.205, effort >= 2.1.214).
|
|
202
|
+
* Same loose + catch degradation contract as StatusLineStdinSchema. */
|
|
203
|
+
export const SubagentStatusLineStdinSchema = z.looseObject({
|
|
204
|
+
tasks: z
|
|
205
|
+
.array(
|
|
206
|
+
z.looseObject({
|
|
207
|
+
id: z.string().optional(),
|
|
208
|
+
name: z.string().nullable().optional().catch(undefined),
|
|
209
|
+
description: z.string().nullable().optional().catch(undefined),
|
|
210
|
+
label: z.string().nullable().optional().catch(undefined),
|
|
211
|
+
model: z.string().nullable().optional().catch(undefined),
|
|
212
|
+
effort: z.string().nullable().optional().catch(undefined),
|
|
213
|
+
contextWindowSize: z.number().nullable().optional().catch(undefined),
|
|
214
|
+
tokenCount: z.number().nullable().optional().catch(undefined),
|
|
215
|
+
}),
|
|
216
|
+
)
|
|
217
|
+
.optional()
|
|
218
|
+
.catch(undefined),
|
|
219
|
+
});
|
|
220
|
+
|
|
199
221
|
/** Success body of the OAuth refresh grant. */
|
|
200
222
|
export const RefreshResponseSchema = z.looseObject({
|
|
201
223
|
access_token: z.string(),
|
package/src/main.ts
CHANGED
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
import { basename } from "node:path";
|
|
7
7
|
import { runSupervisor } from "./entries/supervisor.ts";
|
|
8
8
|
import { runStatusline } from "./entries/statusline.ts";
|
|
9
|
+
import { runSubagentStatusline } from "./entries/subagentstatusline.ts";
|
|
9
10
|
import { runStopHook } from "./entries/stophook.ts";
|
|
10
11
|
import { runSessionStart } from "./entries/sessionstart.ts";
|
|
11
12
|
import { cmdInit } from "./cli/init.ts";
|
|
12
13
|
import { cmdAdd } from "./cli/add.ts";
|
|
14
|
+
import { cmdAuth } from "./cli/auth.ts";
|
|
13
15
|
import { cmdCodexAdd } from "./cli/codexadd.ts";
|
|
14
16
|
import { cmdCodexInit } from "./cli/codexinit.ts";
|
|
15
17
|
import { cmdCodexSwitch } from "./cli/codexswitch.ts";
|
|
@@ -24,6 +26,7 @@ import { cmdRename } from "./cli/rename.ts";
|
|
|
24
26
|
import { cmdSwitch } from "./cli/switch.ts";
|
|
25
27
|
import { cmdCheck } from "./cli/check.ts";
|
|
26
28
|
import { cmdConfig } from "./cli/config.ts";
|
|
29
|
+
import { cmdServe } from "./cli/serve.ts";
|
|
27
30
|
import { uninstallSupervisor } from "./lib/install.ts";
|
|
28
31
|
import { c } from "./cli/render.ts";
|
|
29
32
|
|
|
@@ -37,12 +40,14 @@ function printHelp(): void {
|
|
|
37
40
|
${c.cyan("tokenmaxxing init --codex")} same for codex: import login, install codex supervisor + Stop hook (trust it via /hooks)
|
|
38
41
|
${c.cyan("tokenmaxxing add")} register an additional account (isolated login)
|
|
39
42
|
${c.cyan("tokenmaxxing add --codex")} register an additional codex account (isolated login)
|
|
43
|
+
${c.cyan("tokenmaxxing auth")} [sel | --all] reauthenticate a pooled account in place (bare = pick from a list; --all = every needs-reauth account, one by one)
|
|
40
44
|
${c.cyan("tokenmaxxing switch --codex")} [sel] switch the codex pool (takes effect on next codex start)
|
|
41
45
|
${c.cyan("tokenmaxxing ls")} list pooled accounts
|
|
42
46
|
${c.cyan("tokenmaxxing status")} accounts with 5h / weekly / per-model usage bars
|
|
43
47
|
${c.cyan("tokenmaxxing status --force")} ping every account (one tiny haiku request each) so all 5h session timers start now, then sample fresh; ${c.cyan("xx --force")} works too
|
|
44
48
|
${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120, never pings)
|
|
45
49
|
${c.cyan("tokenmaxxing config")} [get|set|unset|tidy] inspect and edit config.json (bare = effective config with sources)
|
|
50
|
+
${c.cyan("tokenmaxxing serve")} [setup|link|unlink|links] Slack bridge daemon: mention the bot in a linked channel to open a claude session per thread (worktree by default)
|
|
46
51
|
${c.cyan("tokenmaxxing doctor")} verify the install is intact
|
|
47
52
|
${c.cyan("tokenmaxxing rename")} [--codex] <sel> <label>
|
|
48
53
|
${c.cyan("tokenmaxxing rm")} <sel>
|
|
@@ -70,6 +75,7 @@ async function main(): Promise<number> {
|
|
|
70
75
|
|
|
71
76
|
switch (sub) {
|
|
72
77
|
case "__statusline": return runStatusline();
|
|
78
|
+
case "__subagent-statusline": return runSubagentStatusline();
|
|
73
79
|
case "__stop-hook": return runStopHook();
|
|
74
80
|
case "__session-start": return runSessionStart();
|
|
75
81
|
case "__codex-stop-hook": return runCodexStopHook();
|
|
@@ -78,8 +84,10 @@ async function main(): Promise<number> {
|
|
|
78
84
|
case "switch": return args[1] === "--codex" ? cmdCodexSwitch(args[2]) : cmdSwitch(args[1]);
|
|
79
85
|
case "check": return cmdCheck();
|
|
80
86
|
case "config": return cmdConfig(args.slice(1));
|
|
87
|
+
case "serve": return cmdServe(args.slice(1));
|
|
81
88
|
case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
|
|
82
89
|
case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
|
|
90
|
+
case "auth": return cmdAuth(args.slice(1));
|
|
83
91
|
case "ls": return cmdLs();
|
|
84
92
|
case "status": return cmdStatus(args.includes("--force"));
|
|
85
93
|
case "watch": return cmdWatch(args[1]);
|