tokenmaxxing 0.17.0 → 0.19.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 +17 -2
- package/README.md +1 -0
- package/package.json +5 -1
- package/src/cli/doctor.ts +1 -0
- package/src/cli/render.ts +27 -0
- package/src/cli/serve.ts +312 -0
- package/src/cli/status.ts +18 -3
- package/src/entries/statusline.ts +53 -53
- package/src/entries/subagentstatusline.ts +78 -0
- package/src/lib/decide.ts +1 -1
- package/src/lib/paths.ts +7 -0
- package/src/lib/picker.ts +11 -2
- package/src/lib/settings.ts +17 -6
- package/src/lib/slackbridge.ts +180 -0
- package/src/lib/slackstate.ts +131 -0
- package/src/lib/slackstream.ts +189 -0
- package/src/lib/types.ts +22 -0
- package/src/lib/usage.ts +1 -1
- package/src/main.ts +5 -0
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
/** Chat SDK channel ids are adapter-prefixed ("slack:C0123"); links store the
|
|
119
|
+
* bare Slack id, so lookups must strip the prefix. */
|
|
120
|
+
export function bareChannelId(id: string): string {
|
|
121
|
+
return id.startsWith("slack:") ? id.slice("slack:".length) : id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Strip a leading Slack mention token ("<@U0123> rest") from message text. */
|
|
125
|
+
export function stripLeadingMention(text: string): string {
|
|
126
|
+
const trimmed = text.trimStart();
|
|
127
|
+
if (!trimmed.startsWith("<@")) return text.trim();
|
|
128
|
+
const close = trimmed.indexOf(">");
|
|
129
|
+
if (close < 0) return text.trim();
|
|
130
|
+
return trimmed.slice(close + 1).trim();
|
|
131
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Maps Claude Agent SDK messages onto Chat SDK stream chunks so a relayed
|
|
2
|
+
// Slack turn shows the agent's process natively: task cards for thinking and
|
|
3
|
+
// tool calls (pending -> in_progress -> complete/error), streamed text via
|
|
4
|
+
// markdown_text, and a closing turn card with model/cost/duration. Structured
|
|
5
|
+
// chunks render only when the Slack app has the agent feature + assistant:write
|
|
6
|
+
// (the adapter drops them gracefully otherwise); plain text streams either way.
|
|
7
|
+
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
10
|
+
import type { StreamChunk } from "chat";
|
|
11
|
+
|
|
12
|
+
const DETAILS_MAX = 400;
|
|
13
|
+
const OUTPUT_MAX = 600;
|
|
14
|
+
|
|
15
|
+
/** Tool-input fields worth showing on a task card, most human-readable first. */
|
|
16
|
+
const SUMMARY_FIELDS = ["command", "description", "file_path", "pattern", "prompt", "query", "url"] as const;
|
|
17
|
+
|
|
18
|
+
const OpenBlockSchema = z.object({
|
|
19
|
+
kind: z.enum(["thinking", "tool"]),
|
|
20
|
+
id: z.string(),
|
|
21
|
+
title: z.string(),
|
|
22
|
+
/** accumulated thinking text or partial tool-input JSON. */
|
|
23
|
+
acc: z.string(),
|
|
24
|
+
});
|
|
25
|
+
type OpenBlock = z.infer<typeof OpenBlockSchema>;
|
|
26
|
+
|
|
27
|
+
export const StreamMapStateSchema = z.object({
|
|
28
|
+
/** open content blocks by stream index. */
|
|
29
|
+
open: z.record(z.string(), OpenBlockSchema),
|
|
30
|
+
/** tool_use id -> tool name, for labeling the eventual tool_result. */
|
|
31
|
+
toolTitles: z.record(z.string(), z.string()),
|
|
32
|
+
thinkingCount: z.number(),
|
|
33
|
+
/** reply text streamed since the last segment break. */
|
|
34
|
+
textSinceBreak: z.boolean(),
|
|
35
|
+
});
|
|
36
|
+
export type StreamMapState = z.infer<typeof StreamMapStateSchema>;
|
|
37
|
+
|
|
38
|
+
export function newStreamMapState(): StreamMapState {
|
|
39
|
+
return { open: {}, toolTitles: {}, thinkingCount: 0, textSinceBreak: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Emitted when a tool starts after streamed reply text: the bridge closes the
|
|
43
|
+
* current Slack message there and posts the rest as a new one, mirroring how
|
|
44
|
+
* an agent turn reads as separate messages around its tool runs. */
|
|
45
|
+
export const SegmentBreakSchema = z.object({ type: z.literal("segment_break") });
|
|
46
|
+
export type SegmentBreak = z.infer<typeof SegmentBreakSchema>;
|
|
47
|
+
|
|
48
|
+
export const StreamPartSchema = z.union([z.string(), z.custom<StreamChunk>(), SegmentBreakSchema]);
|
|
49
|
+
export type StreamPart = z.infer<typeof StreamPartSchema>;
|
|
50
|
+
|
|
51
|
+
function truncate(input: { text: string; max: number }): string {
|
|
52
|
+
return input.text.length > input.max ? `${input.text.slice(0, input.max)}...` : input.text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One human line out of a tool-input JSON blob; null when nothing fits. */
|
|
56
|
+
export function toolInputSummary(rawJson: string): string | null {
|
|
57
|
+
let parsed: unknown;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(rawJson);
|
|
60
|
+
} catch {
|
|
61
|
+
return null; // partial or empty input JSON - show the bare tool name
|
|
62
|
+
}
|
|
63
|
+
const obj = z.record(z.string(), z.unknown()).safeParse(parsed);
|
|
64
|
+
if (!obj.success) return null;
|
|
65
|
+
for (const field of SUMMARY_FIELDS) {
|
|
66
|
+
const value = z.string().min(1).safeParse(obj.data[field]);
|
|
67
|
+
if (value.success) return truncate({ text: value.data, max: DETAILS_MAX });
|
|
68
|
+
}
|
|
69
|
+
const compact = JSON.stringify(parsed);
|
|
70
|
+
return compact === "{}" ? null : truncate({ text: compact, max: DETAILS_MAX });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ToolResultBlockSchema = z.object({
|
|
74
|
+
type: z.literal("tool_result"),
|
|
75
|
+
tool_use_id: z.string(),
|
|
76
|
+
content: z.union([z.string(), z.array(z.unknown())]).optional(),
|
|
77
|
+
is_error: z.boolean().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const TextPartSchema = z.object({ type: z.literal("text"), text: z.string() });
|
|
81
|
+
|
|
82
|
+
function resultText(content: z.infer<typeof ToolResultBlockSchema>["content"]): string | undefined {
|
|
83
|
+
if (content === undefined) return undefined;
|
|
84
|
+
const joined = Array.isArray(content)
|
|
85
|
+
? content
|
|
86
|
+
.flatMap((part) => {
|
|
87
|
+
const p = TextPartSchema.safeParse(part);
|
|
88
|
+
return p.success ? [p.data.text] : [];
|
|
89
|
+
})
|
|
90
|
+
.join("\n")
|
|
91
|
+
: content;
|
|
92
|
+
const trimmed = joined.trim();
|
|
93
|
+
return trimmed === "" ? undefined : truncate({ text: trimmed, max: OUTPUT_MAX });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Consume one SDK message, mutating state, and return the stream chunks it
|
|
98
|
+
* produces (strings are streamed reply text; objects are native task cards).
|
|
99
|
+
* Subagent events (parent_tool_use_id set) contribute their TOOL cards to the
|
|
100
|
+
* timeline (user ask 2026-07-18: subagent activity shows as accordions like
|
|
101
|
+
* tool calls) but never reply text, thinking cards, or segment breaks: a
|
|
102
|
+
* subagent runs inside a top-level Task tool, so its churn decorates the
|
|
103
|
+
* current message rather than reshaping it. Open blocks are keyed per stream
|
|
104
|
+
* (parent + index) because concurrent subagent streams reuse index space.
|
|
105
|
+
*/
|
|
106
|
+
export function agentEventChunks(input: { state: StreamMapState; message: SDKMessage }): StreamPart[] {
|
|
107
|
+
const { state, message } = input;
|
|
108
|
+
if (message.type === "stream_event") {
|
|
109
|
+
const isMain = message.parent_tool_use_id === null;
|
|
110
|
+
const event = message.event;
|
|
111
|
+
if (event.type === "content_block_start") {
|
|
112
|
+
const key = `${message.parent_tool_use_id ?? "main"}:${event.index}`;
|
|
113
|
+
if (event.content_block.type === "thinking" && isMain) {
|
|
114
|
+
state.thinkingCount += 1;
|
|
115
|
+
const id = `thinking-${state.thinkingCount}`;
|
|
116
|
+
state.open[key] = { kind: "thinking", id, title: "Thinking", acc: "" };
|
|
117
|
+
return [{ type: "task_update", id, title: "Thinking", status: "in_progress" }];
|
|
118
|
+
}
|
|
119
|
+
if (event.content_block.type === "tool_use") {
|
|
120
|
+
const { id, name } = event.content_block;
|
|
121
|
+
state.open[key] = { kind: "tool", id, title: name, acc: "" };
|
|
122
|
+
state.toolTitles[id] = name;
|
|
123
|
+
const card: StreamPart = { type: "task_update", id, title: name, status: "in_progress" };
|
|
124
|
+
if (isMain && state.textSinceBreak) {
|
|
125
|
+
state.textSinceBreak = false;
|
|
126
|
+
return [{ type: "segment_break" }, card];
|
|
127
|
+
}
|
|
128
|
+
return [card];
|
|
129
|
+
}
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
if (event.type === "content_block_delta") {
|
|
133
|
+
const open = state.open[`${message.parent_tool_use_id ?? "main"}:${event.index}`];
|
|
134
|
+
if (event.delta.type === "text_delta") {
|
|
135
|
+
if (!isMain) return [];
|
|
136
|
+
state.textSinceBreak = true;
|
|
137
|
+
return [event.delta.text];
|
|
138
|
+
}
|
|
139
|
+
if (event.delta.type === "thinking_delta" && open) open.acc += event.delta.thinking;
|
|
140
|
+
if (event.delta.type === "input_json_delta" && open) open.acc += event.delta.partial_json;
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
if (event.type === "content_block_stop") {
|
|
144
|
+
const key = `${message.parent_tool_use_id ?? "main"}:${event.index}`;
|
|
145
|
+
const open = state.open[key];
|
|
146
|
+
if (!open) return [];
|
|
147
|
+
delete state.open[key];
|
|
148
|
+
if (open.kind === "thinking") {
|
|
149
|
+
return [{ type: "task_update", id: open.id, title: open.title, status: "complete", details: truncate({ text: open.acc.trim(), max: DETAILS_MAX }) }];
|
|
150
|
+
}
|
|
151
|
+
const details = toolInputSummary(open.acc);
|
|
152
|
+
return [{ type: "task_update", id: open.id, title: open.title, status: "in_progress", ...(details ? { details } : {}) }];
|
|
153
|
+
}
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (message.type === "user") {
|
|
157
|
+
const content = message.message.content;
|
|
158
|
+
if (!Array.isArray(content)) return [];
|
|
159
|
+
const chunks: StreamChunk[] = [];
|
|
160
|
+
for (const block of content) {
|
|
161
|
+
const parsed = ToolResultBlockSchema.safeParse(block);
|
|
162
|
+
if (!parsed.success) continue;
|
|
163
|
+
const title = state.toolTitles[parsed.data.tool_use_id];
|
|
164
|
+
if (title === undefined) continue;
|
|
165
|
+
const output = resultText(parsed.data.content);
|
|
166
|
+
chunks.push({
|
|
167
|
+
type: "task_update",
|
|
168
|
+
id: parsed.data.tool_use_id,
|
|
169
|
+
title,
|
|
170
|
+
status: parsed.data.is_error === true ? "error" : "complete",
|
|
171
|
+
...(output ? { output } : {}),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return chunks;
|
|
175
|
+
}
|
|
176
|
+
if (message.type === "result") {
|
|
177
|
+
const models = Object.keys(message.modelUsage).join(" ");
|
|
178
|
+
const cost = `$${message.total_cost_usd.toFixed(4)}`;
|
|
179
|
+
const secs = `${Math.round(message.duration_ms / 1000)}s`;
|
|
180
|
+
return [{
|
|
181
|
+
type: "task_update",
|
|
182
|
+
id: "turn",
|
|
183
|
+
title: "Turn",
|
|
184
|
+
status: message.subtype === "success" ? "complete" : "error",
|
|
185
|
+
details: [models, cost, secs].filter((p) => p !== "").join(" "),
|
|
186
|
+
}];
|
|
187
|
+
}
|
|
188
|
+
return [];
|
|
189
|
+
}
|
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/lib/usage.ts
CHANGED
|
@@ -76,7 +76,7 @@ export function matchedFamily(model: ModelInfo | null, families: string[]): stri
|
|
|
76
76
|
* unknown. The unknown case matters on headless boxes: a swap clears the
|
|
77
77
|
* snapshots and only an actively-rendering statusLine restores the model, so
|
|
78
78
|
* the periodic check ran model-blind for hours while the active account sat at
|
|
79
|
-
* its Fable cap (the 2026-07-12
|
|
79
|
+
* its Fable cap (the 2026-07-12 ARM-box incident). */
|
|
80
80
|
export function gatedFamilies(model: ModelInfo | null, families: string[]): string[] {
|
|
81
81
|
if (!model) return families;
|
|
82
82
|
const family = matchedFamily(model, families);
|
package/src/main.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
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";
|
|
@@ -25,6 +26,7 @@ import { cmdRename } from "./cli/rename.ts";
|
|
|
25
26
|
import { cmdSwitch } from "./cli/switch.ts";
|
|
26
27
|
import { cmdCheck } from "./cli/check.ts";
|
|
27
28
|
import { cmdConfig } from "./cli/config.ts";
|
|
29
|
+
import { cmdServe } from "./cli/serve.ts";
|
|
28
30
|
import { uninstallSupervisor } from "./lib/install.ts";
|
|
29
31
|
import { c } from "./cli/render.ts";
|
|
30
32
|
|
|
@@ -45,6 +47,7 @@ function printHelp(): void {
|
|
|
45
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
|
|
46
48
|
${c.cyan("tokenmaxxing watch")} [seconds] live status: re-render every N seconds (default 120, never pings)
|
|
47
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)
|
|
48
51
|
${c.cyan("tokenmaxxing doctor")} verify the install is intact
|
|
49
52
|
${c.cyan("tokenmaxxing rename")} [--codex] <sel> <label>
|
|
50
53
|
${c.cyan("tokenmaxxing rm")} <sel>
|
|
@@ -72,6 +75,7 @@ async function main(): Promise<number> {
|
|
|
72
75
|
|
|
73
76
|
switch (sub) {
|
|
74
77
|
case "__statusline": return runStatusline();
|
|
78
|
+
case "__subagent-statusline": return runSubagentStatusline();
|
|
75
79
|
case "__stop-hook": return runStopHook();
|
|
76
80
|
case "__session-start": return runSessionStart();
|
|
77
81
|
case "__codex-stop-hook": return runCodexStopHook();
|
|
@@ -80,6 +84,7 @@ async function main(): Promise<number> {
|
|
|
80
84
|
case "switch": return args[1] === "--codex" ? cmdCodexSwitch(args[2]) : cmdSwitch(args[1]);
|
|
81
85
|
case "check": return cmdCheck();
|
|
82
86
|
case "config": return cmdConfig(args.slice(1));
|
|
87
|
+
case "serve": return cmdServe(args.slice(1));
|
|
83
88
|
case "init": return args.includes("--codex") ? cmdCodexInit() : cmdInit();
|
|
84
89
|
case "add": return args.includes("--codex") ? cmdCodexAdd() : cmdAdd();
|
|
85
90
|
case "auth": return cmdAuth(args.slice(1));
|