tokenmaxxing 1.5.0 → 1.7.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.
Files changed (36) hide show
  1. package/DESIGN.md +2 -30
  2. package/LICENSE +21 -0
  3. package/README.md +28 -2
  4. package/agent-plugin/bin/tokenmaxxing-mcp +7 -0
  5. package/agent-plugin/mcp.json +10 -0
  6. package/agent-plugin/plugin.json +14 -0
  7. package/agent-plugin/skills/codex-pool/SKILL.md +23 -0
  8. package/agent-plugin/skills/codex-pool/references/codex.md +5 -0
  9. package/agent-plugin/skills/credentials-hygiene/SKILL.md +26 -0
  10. package/agent-plugin/skills/credentials-hygiene/references/credentials.md +6 -0
  11. package/agent-plugin/skills/doctor-diagnostics/SKILL.md +26 -0
  12. package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +5 -0
  13. package/agent-plugin/skills/pool-status/SKILL.md +27 -0
  14. package/agent-plugin/skills/pool-status/references/commands.md +8 -0
  15. package/agent-plugin/skills/safe-contribution/SKILL.md +27 -0
  16. package/agent-plugin/skills/safe-contribution/references/ship.md +5 -0
  17. package/agent-plugin/skills/sdk-pairing/SKILL.md +33 -0
  18. package/agent-plugin/skills/sdk-pairing/references/sdk.md +6 -0
  19. package/agent-plugin/skills/switching-policy/SKILL.md +29 -0
  20. package/agent-plugin/skills/switching-policy/references/policy.md +7 -0
  21. package/package.json +6 -7
  22. package/src/entries/mcp.ts +288 -0
  23. package/src/lib/decide.ts +2 -4
  24. package/src/lib/install.ts +62 -2
  25. package/src/lib/lock.ts +3 -7
  26. package/src/lib/log.ts +8 -11
  27. package/src/lib/paths.ts +0 -9
  28. package/src/lib/usage.ts +6 -5
  29. package/src/main.ts +1 -6
  30. package/src/cli/serve.ts +0 -1790
  31. package/src/lib/slackbridge.ts +0 -1363
  32. package/src/lib/slackstate.ts +0 -352
  33. package/src/lib/slackstream.ts +0 -300
  34. package/src/serve-plugin/.claude-plugin/plugin.json +0 -4
  35. package/src/serve-plugin/skills/ask-the-user/SKILL.md +0 -41
  36. package/src/serve-plugin/skills/serve-session/SKILL.md +0 -50
@@ -0,0 +1,288 @@
1
+ // Stdio MCP entry for the portable Agent Plugin (agent-plugin/).
2
+ // Tools wrap existing CLI commands. stdout is reserved for MCP JSON-RPC, so
3
+ // every CLI call captures console.log / console.error and returns the text.
4
+
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { cmdLs } from "../cli/ls.ts";
11
+ import { cmdStatus } from "../cli/status.ts";
12
+ import { cmdDoctor } from "../cli/doctor.ts";
13
+ import { cmdConfig } from "../cli/config.ts";
14
+ import { cmdSwitch } from "../cli/switch.ts";
15
+ import { cmdCodexSwitch } from "../cli/codexswitch.ts";
16
+ import { cmdCheck } from "../cli/check.ts";
17
+
18
+ const MUTATIONS_ENV = "TOKENMAXXING_AGENT_MUTATIONS";
19
+ const PACKAGE_ROOT = join(import.meta.dir, "../..");
20
+
21
+ function packageVersion(): string {
22
+ try {
23
+ const raw = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")) as { version?: string };
24
+ return raw.version ?? "0.0.0";
25
+ } catch {
26
+ return "0.0.0";
27
+ }
28
+ }
29
+
30
+ /** Refuse ambient Claude store overrides the same way the CLI and SDK do. */
31
+ export function refuseAmbientStoreEnv(): string | null {
32
+ const nonEmpty = (v: string | undefined) => (v != null && v !== "" ? v : null);
33
+ return nonEmpty(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR) ?? nonEmpty(process.env.CLAUDE_CONFIG_DIR);
34
+ }
35
+
36
+ /** Redact token-shaped spans so tool results never echo credentials. */
37
+ export function scrubSecrets(text: string): string {
38
+ return text
39
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._\-+/=]+/gi, "$1[redacted]")
40
+ .replace(/\b(sk-ant-[A-Za-z0-9_-]+)\b/g, "[redacted]")
41
+ .replace(/\b(accessToken|refreshToken|claudeAiOauth)\b\s*[:=]\s*["']?[^"'}\s,]+/gi, "$1=[redacted]");
42
+ }
43
+
44
+ type CaptureResult = { code: number; stdout: string; stderr: string };
45
+
46
+ /** Serialize captureCli: console.log/error are process-global, so concurrent
47
+ * tool calls would interleave stdout into each other and corrupt JSON-RPC. */
48
+ let captureChain: Promise<unknown> = Promise.resolve();
49
+
50
+ /** Run a CLI cmd while keeping stdout clean for the MCP transport. */
51
+ export async function captureCli(run: () => number | Promise<number>): Promise<CaptureResult> {
52
+ const job = async (): Promise<CaptureResult> => {
53
+ const out: string[] = [];
54
+ const err: string[] = [];
55
+ const joinArgs = (args: unknown[]) => args.map((a) => (typeof a === "string" ? a : String(a))).join(" ");
56
+ const log = console.log;
57
+ const error = console.error;
58
+ console.log = (...args: unknown[]) => { out.push(joinArgs(args)); };
59
+ console.error = (...args: unknown[]) => { err.push(joinArgs(args)); };
60
+ try {
61
+ const code = await run();
62
+ return { code, stdout: out.join("\n"), stderr: err.join("\n") };
63
+ } finally {
64
+ console.log = log;
65
+ console.error = error;
66
+ }
67
+ };
68
+ const next = captureChain.then(job, job);
69
+ captureChain = next.then(
70
+ () => undefined,
71
+ () => undefined,
72
+ );
73
+ return next;
74
+ }
75
+
76
+ function textResult(input: { text: string; isError?: boolean }) {
77
+ return {
78
+ content: [{ type: "text" as const, text: scrubSecrets(input.text) }],
79
+ ...(input.isError ? { isError: true } : {}),
80
+ };
81
+ }
82
+
83
+ function formatCapture(cap: CaptureResult): string {
84
+ const parts: string[] = [];
85
+ if (cap.stdout) parts.push(cap.stdout);
86
+ if (cap.stderr) parts.push(cap.stderr);
87
+ parts.push(`exit ${cap.code}`);
88
+ return parts.join("\n").trimEnd();
89
+ }
90
+
91
+ export function mutationsEnabled(): boolean {
92
+ return process.env[MUTATIONS_ENV] === "1";
93
+ }
94
+
95
+ function mutationDenied(confirm: boolean): string | null {
96
+ if (!confirm) {
97
+ return `Mutating tools require confirm=true. Also set ${MUTATIONS_ENV}=1 in the MCP server environment after the user approves.`;
98
+ }
99
+ if (!mutationsEnabled()) {
100
+ return `Mutations are disabled. Set ${MUTATIONS_ENV}=1 in the MCP server environment only after the user explicitly approves a pool mutation.`;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ const HELP_TEXT = `tokenmaxxing agent MCP
106
+
107
+ Read tools (always available):
108
+ pool_ls list pooled Claude and Codex accounts (labels/status only)
109
+ pool_status sample usage bars (free /usage path; never --force)
110
+ doctor verify install health (labels/status only)
111
+ config_get read effective config, or one key when provided
112
+ help this catalog
113
+
114
+ Mutating tools (confirm=true AND ${MUTATIONS_ENV}=1):
115
+ pool_switch Claude greedy/forced switch, or Codex when codex=true
116
+ pool_check one evaluate-and-maybe-swap pass
117
+ config_set write a config.json override
118
+ config_unset remove a config.json override
119
+
120
+ Hard deny (no tools):
121
+ status --force / metered pings
122
+ init / add / auth / rm / uninstall
123
+ credential blobs or token values
124
+ killing sessions or supervisors
125
+
126
+ Prefer these tools over raw shell for pool ops. Honor TOKENMAXXING_HOME for hermetic use.
127
+ `;
128
+
129
+ export function createTokenmaxxingMcpServer(): McpServer {
130
+ const server = new McpServer({
131
+ name: "tokenmaxxing",
132
+ version: packageVersion(),
133
+ });
134
+
135
+ server.registerTool(
136
+ "help",
137
+ {
138
+ description: "Catalog of tokenmaxxing MCP tools and hard safety rules. Use when deciding which pool tool to call.",
139
+ inputSchema: {},
140
+ },
141
+ async () => textResult({ text: HELP_TEXT }),
142
+ );
143
+
144
+ server.registerTool(
145
+ "pool_ls",
146
+ {
147
+ description: "List pooled Claude and Codex accounts with active and needs-reauth flags. Labels and status only; never credential material.",
148
+ inputSchema: {},
149
+ },
150
+ async () => {
151
+ const cap = await captureCli(() => cmdLs());
152
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
153
+ },
154
+ );
155
+
156
+ server.registerTool(
157
+ "pool_status",
158
+ {
159
+ description: "Show pool usage bars via the free /usage path. Never opens 5h windows. Do not request --force; that tool does not exist.",
160
+ inputSchema: {},
161
+ },
162
+ async () => {
163
+ const cap = await captureCli(() => cmdStatus(false));
164
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
165
+ },
166
+ );
167
+
168
+ server.registerTool(
169
+ "doctor",
170
+ {
171
+ description: "Verify supervisor, hooks, timer, and credential identity health. Reports labels and pass/fail only; never returns credential blobs.",
172
+ inputSchema: {},
173
+ },
174
+ async () => {
175
+ const cap = await captureCli(() => cmdDoctor());
176
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
177
+ },
178
+ );
179
+
180
+ server.registerTool(
181
+ "config_get",
182
+ {
183
+ description: "Read effective config with sources, or one dotted key when key is set (e.g. thresholds.session).",
184
+ inputSchema: {
185
+ key: z.string().optional().describe("Optional dotted config key; omit for the full effective table"),
186
+ },
187
+ },
188
+ async ({ key }) => {
189
+ const args = key ? ["get", key] : [];
190
+ const cap = await captureCli(() => cmdConfig(args));
191
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
192
+ },
193
+ );
194
+
195
+ server.registerTool(
196
+ "pool_switch",
197
+ {
198
+ description: "Switch the Claude pool (or Codex when codex=true). Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1. Hot-swaps live Claude; Codex takes effect on next start.",
199
+ inputSchema: {
200
+ confirm: z.boolean().describe("Must be true after the user approves the mutation"),
201
+ selector: z.string().optional().describe("Optional account selector; omit for greedy best"),
202
+ codex: z.boolean().optional().describe("When true, run the Codex pool switch instead"),
203
+ },
204
+ },
205
+ async ({ confirm, selector, codex }) => {
206
+ const denied = mutationDenied(confirm);
207
+ if (denied) return textResult({ text: denied, isError: true });
208
+ const cap = await captureCli(() => (codex ? cmdCodexSwitch(selector) : cmdSwitch(selector)));
209
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
210
+ },
211
+ );
212
+
213
+ server.registerTool(
214
+ "pool_check",
215
+ {
216
+ description: "Run one evaluate-and-maybe-swap pass (the periodic timer path). Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
217
+ inputSchema: {
218
+ confirm: z.boolean().describe("Must be true after the user approves the mutation"),
219
+ },
220
+ },
221
+ async ({ confirm }) => {
222
+ const denied = mutationDenied(confirm);
223
+ if (denied) return textResult({ text: denied, isError: true });
224
+ const cap = await captureCli(() => cmdCheck());
225
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
226
+ },
227
+ );
228
+
229
+ server.registerTool(
230
+ "config_set",
231
+ {
232
+ description: "Write a config.json override. Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
233
+ inputSchema: {
234
+ confirm: z.boolean().describe("Must be true after the user approves the mutation"),
235
+ key: z.string().describe("Dotted config key"),
236
+ value: z.string().describe("JSON or literal string value"),
237
+ },
238
+ },
239
+ async ({ confirm, key, value }) => {
240
+ const denied = mutationDenied(confirm);
241
+ if (denied) return textResult({ text: denied, isError: true });
242
+ const cap = await captureCli(() => cmdConfig(["set", key, value]));
243
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
244
+ },
245
+ );
246
+
247
+ server.registerTool(
248
+ "config_unset",
249
+ {
250
+ description: "Remove a config.json override. Requires confirm=true and TOKENMAXXING_AGENT_MUTATIONS=1.",
251
+ inputSchema: {
252
+ confirm: z.boolean().describe("Must be true after the user approves the mutation"),
253
+ key: z.string().describe("Dotted config key"),
254
+ },
255
+ },
256
+ async ({ confirm, key }) => {
257
+ const denied = mutationDenied(confirm);
258
+ if (denied) return textResult({ text: denied, isError: true });
259
+ const cap = await captureCli(() => cmdConfig(["unset", key]));
260
+ return textResult({ text: formatCapture(cap), isError: cap.code !== 0 });
261
+ },
262
+ );
263
+
264
+ return server;
265
+ }
266
+
267
+ /** Entrypoint for the Agent Plugin launcher (`agent-plugin/bin/tokenmaxxing-mcp`).
268
+ * Exported because that bin imports this module (so `import.meta.main` is false here). */
269
+ export async function main(): Promise<void> {
270
+ const ambient = refuseAmbientStoreEnv();
271
+ if (ambient != null) {
272
+ console.error(
273
+ `CLAUDE_CONFIG_DIR / CLAUDE_SECURESTORAGE_CONFIG_DIR is set (${ambient}): the pooled MCP surface requires the default Claude Code credential store. Unset it and retry.`,
274
+ );
275
+ process.exit(1);
276
+ }
277
+ const server = createTokenmaxxingMcpServer();
278
+ const transport = new StdioServerTransport();
279
+ await server.connect(transport);
280
+ console.error("tokenmaxxing MCP server running on stdio");
281
+ }
282
+
283
+ if (import.meta.main) {
284
+ main().catch((e) => {
285
+ console.error(e instanceof Error ? e.message : String(e));
286
+ process.exit(1);
287
+ });
288
+ }
package/src/lib/decide.ts CHANGED
@@ -42,8 +42,7 @@ const SwapDecisionSchema = z.object({
42
42
  reason: z.string(),
43
43
  /** set when every account is depleted and the soonest recovery is known:
44
44
  * epoch ms that account recovers. The wait target on depleted-wait;
45
- * informational on a bare all-depleted (callers like `xx serve` park on
46
- * it - nothing here waits). */
45
+ * informational on a bare all-depleted (nothing here waits). */
47
46
  waitUntil: z.number().optional(),
48
47
  });
49
48
  export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
@@ -318,8 +317,7 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
318
317
  // truly walled do we fall through to the depleted-wait park below. The wall
319
318
  // reading is the statusLine's authoritative rate_limits tee (the same data
320
319
  // /rate-limit-options renders); a single-turn overshoot is caught one
321
- // boundary later by the check timer or the next Stop hook, and the serve/SDK
322
- // path additionally stamps observed limits on errored results.
320
+ // boundary later by the check timer or the next Stop hook.
323
321
  const hardCtx = { now, thresholds: hardBars(cfg), currentAccountUuid: null, switchFamilies };
324
322
  const seat = seatOf(loadAccounts());
325
323
  if (seat && !seat.needsReauth && !isExhausted(seat, hardCtx)) {
@@ -33,14 +33,65 @@ export function isBinDirAhead(): boolean {
33
33
  }
34
34
  }
35
35
 
36
+ // Optional "1"/"true"/"yes" flag; unset/empty → undefined (feature off).
37
+ const EnvFlagSchema = z.enum(["1", "true", "yes"]).optional().catch(undefined);
38
+
39
+ /** True when this process is the Nix-packaged CLI (flake startScript sets
40
+ * TOKENMAXXING_NIX=1; store-path Bun.main is the fallback for wraps that
41
+ * forget the env). Env overrides parse at the read site. */
42
+ export function isNixPackaged(): boolean {
43
+ if (EnvFlagSchema.parse(process.env.TOKENMAXXING_NIX) != null) return true;
44
+ try {
45
+ return realpathSync(Bun.main).startsWith("/nix/store/");
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /** True when a Nix module owns the periodic check timer; init must not write
52
+ * a second imperative unit. */
53
+ export function skipImperativeTimer(): boolean {
54
+ return EnvFlagSchema.parse(process.env.TOKENMAXXING_SKIP_TIMER) != null;
55
+ }
56
+
57
+ /** Nix supervisor shim: prefer a PATH-stable `tokenmaxxing` (profile /
58
+ * current-system, excluding this binDir) so upgrades/GC of an old store
59
+ * generation stay reachable; fall back to bun+entry for the rare
60
+ * `nix run ... -- init` case where nothing is on PATH yet (works until that
61
+ * generation is GC'd — docs steer users to `nix profile install` first). */
62
+ function nixSupervisorShim(bun: string, entry: string): string {
63
+ return `#!/bin/sh
64
+ dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
65
+ old_ifs=$IFS
66
+ IFS=:
67
+ new_path=
68
+ for p in $PATH; do
69
+ [ "$p" = "$dir" ] && continue
70
+ if [ -n "$new_path" ]; then new_path="$new_path:$p"; else new_path="$p"; fi
71
+ done
72
+ IFS=$old_ifs
73
+ PATH=$new_path
74
+ export PATH
75
+ if command -v tokenmaxxing >/dev/null 2>&1; then
76
+ exec tokenmaxxing "$@"
77
+ fi
78
+ exec ${JSON.stringify(bun)} run ${JSON.stringify(entry)} "$@"
79
+ `;
80
+ }
81
+
36
82
  export function installSupervisor(): InstallOutcome {
37
83
  mkdirSync(paths.binDir, { recursive: true });
38
84
  const target = installedBin(); // binDir/tokenmaxxing
39
85
  // Resolve the entry through the global-bin symlink (bun add -g links
40
86
  // ~/.bun/bin/tokenmaxxing → the package's src/main.ts) so the shim points
41
- // into the installed package tree, where its imports resolve.
87
+ // into the installed package tree, where its imports resolve. Nix shims
88
+ // prefer PATH first (see nixSupervisorShim).
42
89
  const entry = realpathSync(Bun.main);
43
- writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} run ${JSON.stringify(entry)} "$@"\n`, 0o755);
90
+ if (isNixPackaged()) {
91
+ writeFileAtomic(target, nixSupervisorShim(process.execPath, entry), 0o755);
92
+ } else {
93
+ writeFileAtomic(target, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} run ${JSON.stringify(entry)} "$@"\n`, 0o755);
94
+ }
44
95
 
45
96
  // the on-PATH `claude` wrapper
46
97
  writeFileAtomic(paths.supervisorLink, `#!/bin/sh\nexec ${JSON.stringify(target)} __supervise "$@"\n`, 0o755);
@@ -174,6 +225,10 @@ function run(cmd: string[]): boolean {
174
225
  * place but activation failed (e.g. systemd user session absent over ssh) -
175
226
  * the caller prints the manual activation step. */
176
227
  function installCheckTimer(): boolean {
228
+ // Nix module owns the timer (TOKENMAXXING_SKIP_TIMER): do not write a second
229
+ // unit that would double-fire or clobber the declarative one.
230
+ if (skipImperativeTimer()) return true;
231
+
177
232
  if (process.platform === "darwin") {
178
233
  const plist = launchdPlist();
179
234
  writeFileAtomic(
@@ -243,6 +298,9 @@ export function timerActivationHint(): string {
243
298
 
244
299
  /** True when the timer unit exists AND the service manager reports it loaded. */
245
300
  export function checkTimerHealthy(): boolean {
301
+ // Declarative Nix timer: init wrote nothing; doctor must not demand the
302
+ // imperative unit.
303
+ if (skipImperativeTimer()) return true;
246
304
  if (process.platform === "darwin") {
247
305
  const domain = launchdDomain();
248
306
  return existsSync(launchdPlist()) && domain != null && run(["launchctl", "print", `${domain}/${LAUNCHD_LABEL}`]);
@@ -305,6 +363,8 @@ function systemdTimerActive(): "active" | "not-active" | "unavailable" {
305
363
  * loaded must deactivate successfully, and an unanswerable probe (service
306
364
  * manager unusable) reports false rather than pretending it is gone. */
307
365
  function uninstallCheckTimer(): boolean {
366
+ // Nix owns the timer: do not bootout/disable the declarative unit.
367
+ if (skipImperativeTimer()) return true;
308
368
  if (process.platform === "darwin") {
309
369
  const domain = launchdDomain();
310
370
  const loaded = launchdJobLoaded();
package/src/lib/lock.ts CHANGED
@@ -4,13 +4,9 @@
4
4
  // process exit).
5
5
  //
6
6
  // The acquire is NON-BLOCKING (LOCK_EX|LOCK_NB) with an async retry loop: a
7
- // blocking LOCK_EX from this runtime freezes the whole event loop, and in
8
- // `xx serve` (many actors, one process) a second actor's blocking acquire
9
- // would stop the holder from ever resuming to release - a true single-process
10
- // deadlock; a cross-process holder would freeze the daemon for its whole
11
- // critical section (adversarial review catch, 2026-07-19). EWOULDBLOCK is
12
- // told apart from real failures via errno, so a bad fd still fails fast
13
- // instead of spinning.
7
+ // blocking LOCK_EX from this runtime freezes the whole event loop.
8
+ // EWOULDBLOCK is told apart from real failures via errno, so a bad fd still
9
+ // fails fast instead of spinning.
14
10
 
15
11
  import { closeSync, mkdirSync, openSync } from "node:fs";
16
12
  import { dirname } from "node:path";
package/src/lib/log.ts CHANGED
@@ -18,10 +18,9 @@ function redact(s: string): string {
18
18
 
19
19
  let echo: ((input: { event: string; parts: string }) => void) | null = null;
20
20
 
21
- /** Tee every subsequent log() line to a terminal printer. Only the serve
22
- * daemon opts in: hooks and the statusline own their stdout protocol, so the
23
- * echo stays off by default. The printer receives the same redacted parts the
24
- * file line gets. */
21
+ /** Tee every subsequent log() line to a terminal printer. Off by default
22
+ * (hooks and the statusline own their stdout protocol). The printer receives
23
+ * the same redacted parts the file line gets. */
25
24
  export function setLogEcho(input: { printer: (input: { event: string; parts: string }) => void }): void {
26
25
  echo = input.printer;
27
26
  }
@@ -36,10 +35,9 @@ export function log(event: string, fields: Record<string, unknown> = {}): void {
36
35
  })
37
36
  .join(" ");
38
37
  mkdirSync(dirname(paths.logFile), { recursive: true });
39
- // Rotation cap: the check timer logs every 180s and the serve daemon
40
- // echoes every event, so an uncapped append-only file grows forever on a
41
- // live install (closing-review critic gap). One .old generation bounds
42
- // total disk at ~2x the cap; older history is disposable diagnostics.
38
+ // Rotation cap: the check timer logs every 180s, so an uncapped
39
+ // append-only file grows forever on a live install. One .old generation
40
+ // bounds total disk at ~2x the cap; older history is disposable diagnostics.
43
41
  if (existsSync(paths.logFile) && statSync(paths.logFile).size > LOG_MAX_BYTES) {
44
42
  renameSync(paths.logFile, `${paths.logFile}.old`);
45
43
  }
@@ -47,11 +45,10 @@ export function log(event: string, fields: Record<string, unknown> = {}): void {
47
45
  } catch {
48
46
  // logging must never throw into a hook / supervisor path
49
47
  }
50
- // separate from the file sink: an unwritable log file must not also silence
51
- // the terminal echo (that is exactly when the daemon needs to stay visible).
48
+ // separate from the file sink: an unwritable log file must not also silence the echo.
52
49
  try {
53
50
  echo?.({ event, parts: line });
54
51
  } catch {
55
- // the echo printer must never throw into the daemon either
52
+ // the echo printer must never throw into a caller path
56
53
  }
57
54
  }
package/src/lib/paths.ts CHANGED
@@ -37,15 +37,6 @@ export const paths = {
37
37
  /** linux only: parked credential .json files (0700 dir, 0600 files). */
38
38
  credsDir: join(TM_HOME, "creds"),
39
39
 
40
- /** `xx serve` slack bridge: tokens + channel->repo links (0600: holds the
41
- * xoxb-/xapp- tokens), per-thread claude session records, and the
42
- * single-instance flock (a new daemon generation blocks on it until the
43
- * previous one - possibly still draining an in-flight turn - fully exits,
44
- * so two generations never act on the same thread records or cwd). */
45
- slackJson: join(TM_HOME, "slack.json"),
46
- slackThreadsDir: join(TM_HOME, "slack-threads"),
47
- serveLockFile: join(TM_HOME, "serve-lock"),
48
-
49
40
  /** ~/.claude.json - holds the active `oauthAccount` identity object. */
50
41
  claudeJson: env("TOKENMAXXING_CLAUDE_JSON", join(HOME, ".claude.json")),
51
42
  /** ~/.claude/settings.json - user-owned; we merge four entries into it. */
package/src/lib/usage.ts CHANGED
@@ -199,11 +199,12 @@ export function parseUsageLimitEpoch(input: { text: string }): number | null {
199
199
 
200
200
  /**
201
201
  * Persist a limit observed in a turn RESULT into usage.json so the next
202
- * decision sees the depleted account immediately. A headless serve/SDK process
203
- * has no statusLine tee and `loadFreshSnapshots` skips re-probing inside the
204
- * poll TTL (and `/usage` is fail-silent against the just-limited active token
205
- * anyway), so without this write a post-limit retry re-decides off the stale
206
- * pre-limit snapshot and respawns the same depleted account. The session
202
+ * decision sees the depleted account immediately. Callers without a statusLine
203
+ * tee (headless Agent SDK integrations) should invoke this on errored limit
204
+ * results: `loadFreshSnapshots` skips re-probing inside the poll TTL and
205
+ * `/usage` is fail-silent against the just-limited active token, so without
206
+ * this write a post-limit retry re-decides off the stale pre-limit snapshot
207
+ * and respawns the same depleted account. The session
207
208
  * window is stamped 100% with the announced reset: whichever window actually
208
209
  * tripped, the account is unusable until then, and the hard path swaps away.
209
210
  * `org` is the identity captured AT THE SPAWN BOUNDARY of the turn that
package/src/main.ts CHANGED
@@ -3,9 +3,7 @@
3
3
  // `claude` (or `__supervise`), routes hook/statusLine subcommands, and otherwise
4
4
  // dispatches the `tokenmaxxing` CLI.
5
5
 
6
- import { existsSync } from "node:fs";
7
6
  import { basename } from "node:path";
8
- import { paths } from "./lib/paths.ts";
9
7
  import { runSupervisor } from "./entries/supervisor.ts";
10
8
  import { runStatusline } from "./entries/statusline.ts";
11
9
  import { runSubagentStatusline } from "./entries/subagentstatusline.ts";
@@ -29,7 +27,6 @@ import { cmdRename } from "./cli/rename.ts";
29
27
  import { cmdSwitch } from "./cli/switch.ts";
30
28
  import { cmdCheck } from "./cli/check.ts";
31
29
  import { cmdConfig } from "./cli/config.ts";
32
- import { cmdServe } from "./cli/serve.ts";
33
30
  import { timerDeactivationHint, uninstallSupervisor } from "./lib/install.ts";
34
31
  import { c } from "./cli/render.ts";
35
32
 
@@ -50,7 +47,6 @@ function printHelp(): void {
50
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
51
48
  ${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120, never pings)
52
49
  ${c.cyan("tokenmaxxing config")} [get|set|unset|tidy] inspect and edit config.json (bare = effective config with sources)
53
- ${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 in the repo checkout
54
50
  ${c.cyan("tokenmaxxing doctor")} verify the install is intact
55
51
  ${c.cyan("tokenmaxxing rename")} [--codex] <sel> <label>
56
52
  ${c.cyan("tokenmaxxing rm")} [--codex] <sel>
@@ -115,7 +111,6 @@ async function main(): Promise<number> {
115
111
  }
116
112
  case "check": return cmdCheck();
117
113
  case "config": return cmdConfig(args.slice(1));
118
- case "serve": return cmdServe(args.slice(1));
119
114
  case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
120
115
  case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
121
116
  case "auth": return cmdAuth(args.slice(1));
@@ -145,7 +140,7 @@ async function main(): Promise<number> {
145
140
  console.log(`removed ${removed.join(", ")}`);
146
141
  if (!out.timerDeactivated) console.log(c.yellow(`⚠ the check job may still be loaded - run: ${timerDeactivationHint()}`));
147
142
  if (!out.pathLineRemoved) console.log(c.dim("(no tokenmaxxing PATH line found in the shell rc)"));
148
- console.log(`kept: accounts.json, config.json${existsSync(paths.slackJson) ? ", slack.json (Slack tokens)" : ""}, and every parked credential (claude - macOS: keychain items, Linux: creds/; codex: codex-creds/) - remove accounts with \`xx rm\` to delete their credentials`);
143
+ console.log(`kept: accounts.json, config.json, and every parked credential (claude - macOS: keychain items, Linux: creds/; codex: codex-creds/) - remove accounts with \`xx rm\` to delete their credentials`);
149
144
  return 0;
150
145
  }
151
146
  case "help":