codemax-cli 0.0.0-stage → 0.1.3
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/LICENSE +661 -0
- package/NOTICE +27 -0
- package/README.md +163 -2
- package/TRADEMARKS.md +27 -0
- package/bin/codemax +2 -0
- package/bin/codemax.cjs +78 -0
- package/package.json +72 -4
- package/src/adapters/claude.ts +43 -0
- package/src/adapters/codex.ts +101 -0
- package/src/adapters/detect.ts +16 -0
- package/src/adapters/index.ts +21 -0
- package/src/adapters/simple.ts +89 -0
- package/src/adapters/types.ts +87 -0
- package/src/chat/app.ts +1934 -0
- package/src/chat/logo.ts +37 -0
- package/src/chat/markdown.ts +77 -0
- package/src/chat/palette.ts +179 -0
- package/src/chat/permission.ts +111 -0
- package/src/chat/theme.ts +158 -0
- package/src/chat/transcript.ts +342 -0
- package/src/cli.ts +254 -0
- package/src/config.ts +114 -0
- package/src/core/awake.ts +345 -0
- package/src/core/brief.ts +314 -0
- package/src/core/budget.ts +42 -0
- package/src/core/checkpoint.ts +139 -0
- package/src/core/doctor.ts +67 -0
- package/src/core/files.ts +62 -0
- package/src/core/git.ts +36 -0
- package/src/core/hotkey.ts +75 -0
- package/src/core/journal.ts +215 -0
- package/src/core/ledger.ts +153 -0
- package/src/core/menu.ts +134 -0
- package/src/core/orchestrator.ts +352 -0
- package/src/core/paths.ts +30 -0
- package/src/core/prices-live.ts +123 -0
- package/src/core/pricing.ts +162 -0
- package/src/core/pty.ts +99 -0
- package/src/core/quota.ts +55 -0
- package/src/core/review.ts +67 -0
- package/src/core/spawn.ts +20 -0
- package/src/core/terminal.ts +51 -0
- package/src/core/update.ts +90 -0
- package/src/core/usage.ts +116 -0
- package/src/engines/acp.ts +419 -0
- package/src/engines/codex.ts +330 -0
- package/src/engines/env.ts +17 -0
- package/src/engines/errors.ts +47 -0
- package/src/engines/index.ts +154 -0
- package/src/engines/types.ts +131 -0
- package/src/transcripts/claude-usage.ts +63 -0
- package/src/transcripts/claude.ts +69 -0
- package/src/transcripts/codex.ts +74 -0
- package/src/tui/app.ts +377 -0
- package/src/tui/menu.ts +126 -0
- package/src/tui/pane.ts +79 -0
package/src/tui/menu.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { BoxRenderable, TextRenderable, type CliRenderer, type KeyEvent } from "@opentui/core";
|
|
2
|
+
import type { MenuAction, MenuRow, MenuState } from "../core/menu.ts";
|
|
3
|
+
import { isExhausted, quotaLabel } from "../core/quota.ts";
|
|
4
|
+
import { hotkeyLabel } from "../config.ts";
|
|
5
|
+
|
|
6
|
+
const ROWS_MAX = 12;
|
|
7
|
+
|
|
8
|
+
/** The codemax agent menu, drawn as an overlay box above the agent pane. */
|
|
9
|
+
export class MenuOverlay {
|
|
10
|
+
readonly box: BoxRenderable;
|
|
11
|
+
private lines: TextRenderable[] = [];
|
|
12
|
+
private cursor = 0;
|
|
13
|
+
private fresh = false;
|
|
14
|
+
private state: MenuState;
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private renderer: CliRenderer,
|
|
18
|
+
state: MenuState,
|
|
19
|
+
) {
|
|
20
|
+
this.state = state;
|
|
21
|
+
const i = state.rows.findIndex((r) => r.adapter.id !== state.current && r.installed && !isExhausted(r.quota));
|
|
22
|
+
this.cursor = i >= 0 ? i : 0;
|
|
23
|
+
this.box = new BoxRenderable(renderer, {
|
|
24
|
+
id: "codemax-menu",
|
|
25
|
+
position: "absolute",
|
|
26
|
+
top: 1,
|
|
27
|
+
left: 2,
|
|
28
|
+
zIndex: 1000,
|
|
29
|
+
border: true,
|
|
30
|
+
borderStyle: "rounded",
|
|
31
|
+
borderColor: "#ff4f86",
|
|
32
|
+
backgroundColor: "#3a0d25",
|
|
33
|
+
title: " codemax ",
|
|
34
|
+
titleColor: "#ff4f86",
|
|
35
|
+
paddingX: 1,
|
|
36
|
+
flexDirection: "column",
|
|
37
|
+
});
|
|
38
|
+
for (let k = 0; k < ROWS_MAX + 5; k++) {
|
|
39
|
+
const t = new TextRenderable(renderer, { id: `codemax-menu-line-${k}`, content: "", fg: "#e5e7eb" });
|
|
40
|
+
this.lines.push(t);
|
|
41
|
+
this.box.add(t);
|
|
42
|
+
}
|
|
43
|
+
this.render();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
update(patch: Partial<MenuState>) {
|
|
47
|
+
Object.assign(this.state, patch);
|
|
48
|
+
this.render();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
const width = Math.min(Math.max(40, this.renderer.width - 6), 104);
|
|
53
|
+
this.box.width = width;
|
|
54
|
+
const out: { text: string; fg?: string }[] = [];
|
|
55
|
+
out.push({ text: this.state.title, fg: "#94a3b8" });
|
|
56
|
+
if (this.state.notice) out.push({ text: `! ${this.state.notice}`, fg: "#ff9dbb" });
|
|
57
|
+
out.push({ text: "" });
|
|
58
|
+
this.state.rows.slice(0, ROWS_MAX).forEach((r, i) => {
|
|
59
|
+
const sel = i === this.cursor;
|
|
60
|
+
const name = r.adapter.name + (r.adapter.id === this.state.current ? " (current)" : "");
|
|
61
|
+
const inst = r.installed ? `installed${r.version ? " " + r.version : ""}` : "not found";
|
|
62
|
+
const sess = r.session ? `session ${r.session.id.slice(0, 8)}… ${ago(r.session.lastUsedAt)}` : "no session";
|
|
63
|
+
const q = quotaLabel(r.quota);
|
|
64
|
+
const line = `${sel ? "›" : " "} ${i + 1}. ${pad(name, 24)} ${pad(inst, 24)} ${pad(sess, 30)} ${q}`;
|
|
65
|
+
out.push({ text: line.slice(0, width - 4), fg: sel ? "#ff4f86" : !r.installed ? "#64748b" : isExhausted(r.quota) ? "#ec3570" : "#e5e7eb" });
|
|
66
|
+
});
|
|
67
|
+
out.push({ text: "" });
|
|
68
|
+
out.push({ text: `↑/↓ or 1-9 pick · Enter switch · f: ${this.fresh ? "fresh session" : "resume if a session exists"}`, fg: "#94a3b8" });
|
|
69
|
+
out.push({ text: `${this.state.canClose ? "Esc/q back to agent · " : ""}x quit codemax · hotkey ${hotkeyLabel(this.state.hotkeys)}`, fg: "#94a3b8" });
|
|
70
|
+
this.lines.forEach((l, i) => {
|
|
71
|
+
const row = out[i];
|
|
72
|
+
l.content = row?.text ?? "";
|
|
73
|
+
l.fg = row?.fg ?? "#e5e7eb";
|
|
74
|
+
l.visible = i < out.length;
|
|
75
|
+
});
|
|
76
|
+
this.box.height = out.length + 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
handleKey(key: KeyEvent): MenuAction | null {
|
|
80
|
+
const n = key.name;
|
|
81
|
+
if (n === "up" || n === "k") this.move(-1);
|
|
82
|
+
else if (n === "down" || n === "j") this.move(1);
|
|
83
|
+
else if (n === "f") {
|
|
84
|
+
this.fresh = !this.fresh;
|
|
85
|
+
this.render();
|
|
86
|
+
} else if (n === "return" || n === "linefeed") return this.pick(this.cursor);
|
|
87
|
+
else if (/^[1-9]$/.test(n)) return this.pick(Number(n) - 1);
|
|
88
|
+
else if (n === "x" || (n === "c" && key.ctrl)) return { kind: "quit" };
|
|
89
|
+
else if ((n === "escape" || n === "q") && this.state.canClose) return { kind: "close" };
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private move(d: number) {
|
|
94
|
+
const n = Math.min(this.state.rows.length, ROWS_MAX);
|
|
95
|
+
this.cursor = (this.cursor + d + n) % n;
|
|
96
|
+
this.render();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private pick(i: number): MenuAction | null {
|
|
100
|
+
const r = this.state.rows[i];
|
|
101
|
+
if (!r) return null;
|
|
102
|
+
if (!r.installed) {
|
|
103
|
+
this.update({ notice: `${r.adapter.name} is not installed (looked for "${r.adapter.bin}" on PATH).` });
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return { kind: "switch", agent: r.adapter.id, fresh: this.fresh };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
destroy() {
|
|
110
|
+
this.box.destroyRecursively();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type { MenuRow };
|
|
115
|
+
|
|
116
|
+
function pad(s: string, n: number): string {
|
|
117
|
+
return s.length >= n ? s.slice(0, n - 1) + "…" : s + " ".repeat(n - s.length);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ago(ts: number): string {
|
|
121
|
+
const m = Math.round((Date.now() - ts) / 60_000);
|
|
122
|
+
if (m < 1) return "just now";
|
|
123
|
+
if (m < 60) return `${m}m ago`;
|
|
124
|
+
const h = Math.round(m / 60);
|
|
125
|
+
return h < 48 ? `${h}h ago` : `${Math.round(h / 24)}d ago`;
|
|
126
|
+
}
|
package/src/tui/pane.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { EmbeddedTerminalRenderable, type CliRenderer } from "@opentui/core";
|
|
2
|
+
import { AgentProcess } from "../core/pty.ts";
|
|
3
|
+
|
|
4
|
+
export interface AgentPaneOptions {
|
|
5
|
+
id: string;
|
|
6
|
+
cmd: string[];
|
|
7
|
+
cwd: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
dropEnv?: string[];
|
|
10
|
+
cols: number;
|
|
11
|
+
rows: number;
|
|
12
|
+
/** Decoded child output, for quota scanning and idle detection. */
|
|
13
|
+
onOutput: (text: string) => void;
|
|
14
|
+
onExit: (code: number | null) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One agent TUI: a PTY-backed process whose bytes are parsed by an embedded
|
|
19
|
+
* virtual terminal (Ghostty VT via OpenTUI) and painted inside codemax's own
|
|
20
|
+
* screen. The host terminal never sees the child's escape sequences, so alt
|
|
21
|
+
* screen, mouse and keyboard modes cannot leak between agents.
|
|
22
|
+
*/
|
|
23
|
+
export class AgentPane {
|
|
24
|
+
readonly terminal: EmbeddedTerminalRenderable;
|
|
25
|
+
readonly proc: AgentProcess;
|
|
26
|
+
private decoder = new TextDecoder();
|
|
27
|
+
private destroyed = false;
|
|
28
|
+
|
|
29
|
+
constructor(renderer: CliRenderer, opts: AgentPaneOptions) {
|
|
30
|
+
this.terminal = new EmbeddedTerminalRenderable(renderer, {
|
|
31
|
+
id: opts.id,
|
|
32
|
+
width: "100%",
|
|
33
|
+
flexGrow: 1,
|
|
34
|
+
flexShrink: 1,
|
|
35
|
+
cols: opts.cols,
|
|
36
|
+
rows: opts.rows,
|
|
37
|
+
maxScrollback: 8 * 1024 * 1024,
|
|
38
|
+
transparentBackground: true,
|
|
39
|
+
// Both key input and the emulator's own replies (DSR, DA, colour queries) go to the child.
|
|
40
|
+
onData: (data) => this.proc.write(data),
|
|
41
|
+
onTerminalResize: (cols, rows) => this.proc.resize(cols, rows),
|
|
42
|
+
});
|
|
43
|
+
this.proc = new AgentProcess({
|
|
44
|
+
cmd: opts.cmd,
|
|
45
|
+
cwd: opts.cwd,
|
|
46
|
+
env: { COLORTERM: "truecolor", ...(opts.env ?? {}) },
|
|
47
|
+
dropEnv: opts.dropEnv,
|
|
48
|
+
cols: opts.cols,
|
|
49
|
+
rows: opts.rows,
|
|
50
|
+
onData: (chunk) => {
|
|
51
|
+
if (this.destroyed) return;
|
|
52
|
+
this.terminal.write(chunk);
|
|
53
|
+
opts.onOutput(this.decoder.decode(chunk, { stream: true }));
|
|
54
|
+
},
|
|
55
|
+
onExit: opts.onExit,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get startedAt() {
|
|
60
|
+
return this.proc.startedAt;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get exited() {
|
|
64
|
+
return this.proc.exited;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Send text to the child as a paste, encoded for the child's current paste mode, then Enter. */
|
|
68
|
+
paste(text: string) {
|
|
69
|
+
this.proc.write(this.terminal.encodePaste(new TextEncoder().encode(text)));
|
|
70
|
+
setTimeout(() => this.proc.write("\r"), 200);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async destroy() {
|
|
74
|
+
if (this.destroyed) return;
|
|
75
|
+
this.destroyed = true;
|
|
76
|
+
await this.proc.kill();
|
|
77
|
+
this.terminal.destroy();
|
|
78
|
+
}
|
|
79
|
+
}
|