codemax-cli 0.1.1 → 0.1.4
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/README.md +14 -1
- package/bin/codemax.cjs +3 -2
- package/package.json +13 -4
- package/src/adapters/index.ts +14 -8
- package/src/chat/app.ts +641 -59
- package/src/chat/logo.ts +37 -0
- package/src/chat/models.ts +33 -0
- package/src/chat/palette.ts +14 -6
- package/src/chat/theme.ts +3 -1
- package/src/chat/transcript.ts +27 -6
- package/src/cli.ts +52 -6
- package/src/config.ts +18 -0
- package/src/core/awake.ts +345 -0
- package/src/core/brief.ts +3 -2
- package/src/core/journal.ts +4 -2
- package/src/core/quota.ts +2 -1
- package/src/core/review.ts +67 -0
- package/src/core/time.ts +9 -0
- package/src/core/update.ts +90 -0
- package/src/engines/acp.ts +11 -2
- package/src/engines/codex.ts +53 -4
- package/src/engines/index.ts +6 -6
- package/src/engines/types.ts +11 -0
- package/src/tui/app.ts +13 -4
package/src/chat/logo.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codemax wordmark for the terminal: the same halftone grid the website
|
|
3
|
+
* uses ("codemax" in Inter Tight 800, 13 rows, 0 = empty … 9 = full), drawn
|
|
4
|
+
* with half blocks so two grid rows fit in one terminal line.
|
|
5
|
+
*/
|
|
6
|
+
const GRID = [
|
|
7
|
+
"0000000000000000000000000028870000000000000000000000000000000000000000000000",
|
|
8
|
+
"0000000000000000000000000029970000000000000000000000000000000000000000000000",
|
|
9
|
+
"0000000000000000000000000029970000000000000000000000000000000000000000000000",
|
|
10
|
+
"0036876200003687510000477429970004787410057715773027861000267863004773005773",
|
|
11
|
+
"0699999930069999993006999979970079999981079969999499998104999999501998029970",
|
|
12
|
+
"3999559980499856998039996599970599635996079996899976999319983399910599369920",
|
|
13
|
+
"7994006992899300799359960049970899100799079930299800699402210189920189899600",
|
|
14
|
+
"8992000000999100599569940029971999999999179920199700599401578999920049999100",
|
|
15
|
+
"8992000000999100599569940029971999777777179920199700599418998689920049999100",
|
|
16
|
+
"7994006992899300799359960049970999000121079920199700599449950079920199899600",
|
|
17
|
+
"3998459980599846999139996599970599634998079920199700599459951399920699369930",
|
|
18
|
+
"0699999930079999993007999979970189999993079920199700599429999989922997029980",
|
|
19
|
+
"0047886200004788620000577517750015788620057720177500477303787347725772004773",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/** Logo lines for a terminal this wide: the full 76-column logo from 78 columns, none below. */
|
|
23
|
+
export function logoLines(width: number): string[] {
|
|
24
|
+
// Squeezed to half width the letters run together, so narrow terminals get the text title instead.
|
|
25
|
+
if (width < 78) return [];
|
|
26
|
+
const on = (r: number, c: number) => Number(GRID[r]?.[c] ?? "0") >= 4;
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
for (let r = 0; r < GRID.length; r += 2) {
|
|
29
|
+
let line = "";
|
|
30
|
+
for (let c = 0; c < GRID[0]!.length; c++) {
|
|
31
|
+
const up = on(r, c), down = on(r + 1, c);
|
|
32
|
+
line += up && down ? "█" : up ? "▀" : down ? "▄" : " ";
|
|
33
|
+
}
|
|
34
|
+
lines.push(line.replace(/\s+$/, ""));
|
|
35
|
+
}
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model menus list exactly what the agent offers, newest first: "Default" /
|
|
3
|
+
* "Auto" stay on top, then models by the version in their name ("Opus 5.5"
|
|
4
|
+
* before "Opus 4.6", "GPT-6.1" before "GPT-5.6"); equal versions keep the
|
|
5
|
+
* agent's own order.
|
|
6
|
+
*/
|
|
7
|
+
export interface ModelEntry {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const PINNED = /^(default|auto)\b/i;
|
|
13
|
+
|
|
14
|
+
/** "Opus 5.5" → 5.5, "GPT-6.1-Sol" → 6.1, "claude-opus-4-7" → 4.7, "Sonnet 5" → 5; none → undefined. */
|
|
15
|
+
export function modelVersion(m: ModelEntry): number | undefined {
|
|
16
|
+
for (const s of [m.name, m.id]) {
|
|
17
|
+
// Skip date stamps like 20251001; a version is a small major with an optional minor.
|
|
18
|
+
const hit = s.match(/(?:^|[^\d])(\d{1,2})(?:[.-](\d{1,2}))?(?=$|[^\d])/);
|
|
19
|
+
if (hit) return Number(hit[1]) + (hit[2] ? Number(hit[2]) / 100 : 0);
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sortModels<T extends ModelEntry>(models: T[]): T[] {
|
|
25
|
+
return models
|
|
26
|
+
.map((m, i) => ({ m, i, pinned: PINNED.test(m.name) || PINNED.test(m.id), v: modelVersion(m) }))
|
|
27
|
+
.sort((a, b) => {
|
|
28
|
+
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
|
|
29
|
+
if (a.v !== b.v) return (b.v ?? -1) - (a.v ?? -1);
|
|
30
|
+
return a.i - b.i;
|
|
31
|
+
})
|
|
32
|
+
.map((x) => x.m);
|
|
33
|
+
}
|
package/src/chat/palette.ts
CHANGED
|
@@ -37,6 +37,8 @@ export class Palette {
|
|
|
37
37
|
private rows: TextRenderable[] = [];
|
|
38
38
|
private titleRow: TextRenderable;
|
|
39
39
|
private cursor = 0;
|
|
40
|
+
/** First visible row: the window scrolls so every item can be reached with the arrows. */
|
|
41
|
+
private offset = 0;
|
|
40
42
|
private filter: string;
|
|
41
43
|
private matches: PaletteItem[] = [];
|
|
42
44
|
private maxRows: number;
|
|
@@ -90,11 +92,16 @@ export class Palette {
|
|
|
90
92
|
|
|
91
93
|
render() {
|
|
92
94
|
const { top, width } = this.anchor();
|
|
93
|
-
|
|
95
|
+
// Keep the highlighted row inside the visible window.
|
|
96
|
+
if (this.cursor < this.offset) this.offset = this.cursor;
|
|
97
|
+
if (this.cursor >= this.offset + this.maxRows) this.offset = this.cursor - this.maxRows + 1;
|
|
98
|
+
this.offset = Math.max(0, Math.min(this.offset, this.matches.length - this.maxRows));
|
|
99
|
+
const visible = this.matches.slice(this.offset, this.offset + this.maxRows);
|
|
94
100
|
const shown = visible.length;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
this.titleRow.
|
|
101
|
+
const more = this.matches.length > this.maxRows;
|
|
102
|
+
const titleLines = this.opts.title || more ? 1 : 0;
|
|
103
|
+
this.titleRow.visible = titleLines > 0;
|
|
104
|
+
this.titleRow.content = `${this.opts.title ?? ""}${more ? `${this.opts.title ? " · " : ""}${this.cursor + 1}/${this.matches.length} · ↑↓ to scroll` : ""}`;
|
|
98
105
|
this.box.height = shown + titleLines + (shown === 0 ? 1 : 0);
|
|
99
106
|
this.box.top = Math.max(0, top - this.box.height);
|
|
100
107
|
const labelW = Math.min(Math.max(28, Math.floor(width * 0.35)), Math.max(10, ...visible.map((v) => v.label.length + 2)));
|
|
@@ -112,7 +119,7 @@ export class Palette {
|
|
|
112
119
|
return;
|
|
113
120
|
}
|
|
114
121
|
row.visible = true;
|
|
115
|
-
const sel = i === this.cursor;
|
|
122
|
+
const sel = this.offset + i === this.cursor;
|
|
116
123
|
const label = pad(it.label, labelW);
|
|
117
124
|
const desc = it.description ?? "";
|
|
118
125
|
const hint = it.hint ? ` ${it.hint}` : "";
|
|
@@ -150,7 +157,8 @@ export class Palette {
|
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
private move(d: number): boolean {
|
|
153
|
-
|
|
160
|
+
// Every match is reachable; the window in render() scrolls to follow.
|
|
161
|
+
const n = this.matches.length;
|
|
154
162
|
if (!n) return true;
|
|
155
163
|
this.cursor = (this.cursor + d + n) % n;
|
|
156
164
|
this.render();
|
package/src/chat/theme.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fmtTime12 } from "../core/time.ts";
|
|
1
2
|
/**
|
|
2
3
|
* Glyphs and colours. The codemax brand theme: pink is the accent and nothing
|
|
3
4
|
* else, plum is the one dark ink, and warnings, errors and success keep their
|
|
@@ -153,6 +154,7 @@ export function fmtDuration(ms: number): string {
|
|
|
153
154
|
return `${m}m ${s - m * 60}s`;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
/** 12-hour, always ("7:25 PM"). */
|
|
156
158
|
export function fmtTime(ts: number): string {
|
|
157
|
-
return
|
|
159
|
+
return fmtTime12(ts);
|
|
158
160
|
}
|
package/src/chat/transcript.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BoxRenderable, ScrollBoxRenderable, StyledText, TextRenderable, fg, italic, strikethrough, stringToStyledText, type CliRenderer, type TextChunk } from "@opentui/core";
|
|
2
2
|
import { C, G } from "./theme.ts";
|
|
3
3
|
import { styleMarkdown, styleToolOutput } from "./markdown.ts";
|
|
4
|
+
import { fmtTime12 } from "../core/time.ts";
|
|
4
5
|
|
|
5
6
|
export interface PlanEntry {
|
|
6
7
|
content: string;
|
|
@@ -70,7 +71,7 @@ export class Transcript {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/** Startup card: brand mark, what codemax is, the project, and any notes. */
|
|
73
|
-
header(lines: { title: string; subtitle: string; path: string; notes: string[] }) {
|
|
74
|
+
header(lines: { title: string; subtitle: string; path: string; notes: string[]; logo?: string[] }) {
|
|
74
75
|
if (this.gone) return;
|
|
75
76
|
const card = new BoxRenderable(this.renderer, {
|
|
76
77
|
id: `header-${++this.n}`,
|
|
@@ -82,18 +83,38 @@ export class Transcript {
|
|
|
82
83
|
paddingX: 1,
|
|
83
84
|
marginTop: 1,
|
|
84
85
|
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
if (lines.logo?.length) {
|
|
87
|
+
// The wordmark: one line per two rows of the website's halftone grid, never wrapped.
|
|
88
|
+
for (const [i, l] of lines.logo.entries()) card.add(new TextRenderable(this.renderer, { id: `h-l-${this.n}-${i}`, content: l, fg: C.accent, wrapMode: "none" } as any));
|
|
89
|
+
card.add(new TextRenderable(this.renderer, { id: `h-s-${this.n}`, content: `${lines.title} · ${lines.subtitle}`, fg: C.dim }));
|
|
90
|
+
} else {
|
|
91
|
+
const title = new StyledText([fg(C.accent)("✻ "), fg(C.accent)(lines.title)]);
|
|
92
|
+
card.add(new TextRenderable(this.renderer, { id: `h-t-${this.n}`, content: title, attributes: 1 } as any));
|
|
93
|
+
card.add(new TextRenderable(this.renderer, { id: `h-s-${this.n}`, content: lines.subtitle, fg: C.dim }));
|
|
94
|
+
}
|
|
88
95
|
card.add(new TextRenderable(this.renderer, { id: `h-gap-${this.n}`, content: "", height: 1 }));
|
|
89
96
|
card.add(new TextRenderable(this.renderer, { id: `h-p-${this.n}`, content: `cwd: ${lines.path}`, fg: C.dim, wrapMode: "char" } as any));
|
|
90
97
|
for (const [i, note] of lines.notes.entries()) card.add(new TextRenderable(this.renderer, { id: `h-n-${this.n}-${i}`, content: note, fg: C.dim, wrapMode: "word" } as any));
|
|
91
98
|
this.box.add(card);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
|
-
|
|
101
|
+
/** A side agent's note: a bordered card that stands apart from the conversation. */
|
|
102
|
+
noteCard(title: string, body: string, hint: string) {
|
|
103
|
+
if (this.gone) return;
|
|
104
|
+
this.gap();
|
|
105
|
+
const card = new BoxRenderable(this.renderer, { id: `note-${++this.n}`, width: "100%", flexDirection: "column", border: true, borderStyle: "rounded", borderColor: C.warn, paddingX: 1 });
|
|
106
|
+
card.add(new TextRenderable(this.renderer, { id: `nt-${this.n}`, content: title, fg: C.warn, attributes: 1 } as any));
|
|
107
|
+
card.add(new TextRenderable(this.renderer, { id: `nb-${this.n}`, content: styleMarkdown(body), wrapMode: "word", width: "100%" } as any));
|
|
108
|
+
card.add(new TextRenderable(this.renderer, { id: `nh-${this.n}`, content: hint, fg: C.dim, wrapMode: "word" } as any));
|
|
109
|
+
this.box.add(card);
|
|
110
|
+
this.scrollToEnd();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Your message, with the time it was sent ("· 7:25 PM", 12-hour). */
|
|
114
|
+
user(text: string, ts = Date.now()) {
|
|
95
115
|
this.gap();
|
|
96
|
-
this.line(`${G.prompt} `, text, { fg: undefined, glyphFg: C.accent });
|
|
116
|
+
const node = this.line(`${G.prompt} `, text, { fg: undefined, glyphFg: C.accent });
|
|
117
|
+
node.content = new StyledText([...stringToStyledText(text).chunks, fg(C.dimmer)(` · ${fmtTime12(ts)}`)]);
|
|
97
118
|
}
|
|
98
119
|
|
|
99
120
|
note(text: string, fg_: string = C.dim) {
|
package/src/cli.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { loadConfig, CONFIG_PATH, hotkeyLabel } from "./config.ts";
|
|
1
|
+
import { loadConfig, CONFIG_PATH, hotkeyLabel, saveConfigKey } from "./config.ts";
|
|
2
|
+
import { defaultAwakeDeps, recoverAwake } from "./core/awake.ts";
|
|
3
|
+
import { checkForUpdate, installKind, updatePlan } from "./core/update.ts";
|
|
4
|
+
import { platformArgv } from "./core/spawn.ts";
|
|
5
|
+
import pkg from "../package.json" with { type: "json" };
|
|
2
6
|
import { canonicalCwd } from "./core/paths.ts";
|
|
3
7
|
import { Ledger } from "./core/ledger.ts";
|
|
4
8
|
import { Orchestrator } from "./core/orchestrator.ts";
|
|
@@ -14,6 +18,7 @@ import { diagnose } from "./core/doctor.ts";
|
|
|
14
18
|
import { renderUsageTable, sumCost } from "./core/usage.ts";
|
|
15
19
|
import { fmtUsd, priceFor, priceProvenance, resetPriceCache } from "./core/pricing.ts";
|
|
16
20
|
import { fetchLivePrices, loadLivePrices, LIVE_PRICES_PATH } from "./core/prices-live.ts";
|
|
21
|
+
import { fmtDateTime12 } from "./core/time.ts";
|
|
17
22
|
|
|
18
23
|
const HELP = `codemax — one terminal for all your coding agents
|
|
19
24
|
|
|
@@ -27,6 +32,8 @@ usage:
|
|
|
27
32
|
codemax brief [--to <id>] print the handoff brief that would be injected
|
|
28
33
|
codemax config print config path and values
|
|
29
34
|
codemax keys show the raw bytes your terminal sends per keypress (hotkey debugging)
|
|
35
|
+
codemax update [--check] update codemax the way it was installed (--check only reports)
|
|
36
|
+
codemax awake [on|lid|off|status] keep the machine awake while codemax runs (lid: also with the lid closed)
|
|
30
37
|
codemax quota [clear [<id>]] show or clear remembered quota state for this project
|
|
31
38
|
codemax doctor [--fix] check agent auth/permission setup (API keys that would be billed, silent modes)
|
|
32
39
|
codemax usage [--all] tokens and API-equivalent cost per agent for this project (or every project)
|
|
@@ -67,9 +74,9 @@ async function main(argv: string[]) {
|
|
|
67
74
|
const orch = new Orchestrator(config, ledger, cwd);
|
|
68
75
|
await orch.start(agent, args.includes("--fresh"));
|
|
69
76
|
} else if (args.includes("--tui") || !hasEngine(agent)) {
|
|
70
|
-
await runApp(config, ledger, cwd, agent, args.includes("--fresh"));
|
|
77
|
+
await runApp(config, ledger, cwd, agent, args.includes("--fresh"), !!agentFlag);
|
|
71
78
|
} else {
|
|
72
|
-
await runChat(config, ledger, cwd, agent, args.includes("--fresh"), !agentFlag);
|
|
79
|
+
await runChat(config, ledger, cwd, agent, args.includes("--fresh"), !agentFlag, !!agentFlag);
|
|
73
80
|
}
|
|
74
81
|
process.exit(0);
|
|
75
82
|
}
|
|
@@ -99,7 +106,7 @@ async function main(argv: string[]) {
|
|
|
99
106
|
const last = events.slice(-5);
|
|
100
107
|
if (last.length) {
|
|
101
108
|
console.log("recent events:");
|
|
102
|
-
for (const e of last) console.log(` ${
|
|
109
|
+
for (const e of last) console.log(` ${fmtDateTime12(e.ts)} ${e.kind} ${e.agent ?? ""}${e.to ? ` → ${e.to}` : ""} ${e.detail ?? ""}`);
|
|
103
110
|
}
|
|
104
111
|
return;
|
|
105
112
|
}
|
|
@@ -108,7 +115,7 @@ async function main(argv: string[]) {
|
|
|
108
115
|
const turns = new Ledger(cwd).turns().slice(-n);
|
|
109
116
|
for (const t of turns) {
|
|
110
117
|
const who = t.role === "user" ? "USER" : `${t.agent.toUpperCase()}`;
|
|
111
|
-
console.log(`\n[${
|
|
118
|
+
console.log(`\n[${fmtDateTime12(t.ts)}] ${who}${t.tools?.length ? ` (tools: ${t.tools.join(", ")})` : ""}\n${t.text.slice(0, 600)}`);
|
|
112
119
|
}
|
|
113
120
|
return;
|
|
114
121
|
}
|
|
@@ -164,6 +171,45 @@ async function main(argv: string[]) {
|
|
|
164
171
|
}
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
174
|
+
case "update": {
|
|
175
|
+
const latest = await checkForUpdate(pkg.version, { force: true });
|
|
176
|
+
console.log(latest ? `codemax ${latest} is available (you have ${pkg.version})` : `codemax ${pkg.version} is the latest`);
|
|
177
|
+
if (args.includes("--check") || !latest) return;
|
|
178
|
+
const plan = updatePlan();
|
|
179
|
+
if (plan.manual) return console.log(plan.manual);
|
|
180
|
+
console.log(`updating with ${plan.label} (${installKind().kind} install)\n`);
|
|
181
|
+
for (const cmd of plan.cmd) {
|
|
182
|
+
const p = Bun.spawn(platformArgv(cmd), { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: { ...process.env, ...(plan.env ?? {}) } });
|
|
183
|
+
if ((await p.exited) !== 0) {
|
|
184
|
+
console.error(`\nupdate failed · run it yourself: ${plan.cmd.map((c) => c.join(" ")).join(" && ")}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.log("\ncodemax updated.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case "awake": {
|
|
192
|
+
const v = (args[0] ?? "status").toLowerCase();
|
|
193
|
+
const support = process.platform === "linux" ? "sleep, lid close and shutdown are blocked (systemd-inhibit)" : process.platform === "darwin" ? "sleep is blocked; lid close needs your admin password once; macOS cannot block shutdown" : process.platform === "win32" ? "sleep is blocked; lid close switches the lid action to 'do nothing' and back; Windows cannot block shutdown" : "not supported on this platform";
|
|
194
|
+
if (v === "status") {
|
|
195
|
+
console.log(`awake: ${config.awake ?? "off"} (applies while codemax runs)\n${process.platform}: ${support}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (v !== "on" && v !== "lid" && v !== "off") {
|
|
199
|
+
console.error("usage: codemax awake [on|lid|off|status]");
|
|
200
|
+
process.exit(2);
|
|
201
|
+
}
|
|
202
|
+
if (v === "off") for (const n of await recoverAwake({ ...defaultAwakeDeps(), pid: -1, interactive: (fn) => fn() })) console.log(n);
|
|
203
|
+
if (v === "lid" && process.platform === "darwin") {
|
|
204
|
+
console.log("macOS needs your admin password to keep working with the lid closed (pmset disablesleep).");
|
|
205
|
+
const ok = Bun.spawnSync(["sudo", "-v"], { stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exitCode === 0;
|
|
206
|
+
if (!ok) console.log("no admin rights: lid-close will not be enabled; the Mac still will not idle-sleep.");
|
|
207
|
+
}
|
|
208
|
+
saveConfigKey("awake", v);
|
|
209
|
+
console.log(v === "off" ? "awake off: the machine sleeps normally" : `awake ${v}: applies whenever codemax is running · ${support}`);
|
|
210
|
+
if (v === "lid") console.log("With the lid closed, keep the laptop somewhere it can breathe: not in a bag.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
167
213
|
case "quota": {
|
|
168
214
|
const ledger = new Ledger(cwd);
|
|
169
215
|
if (args[0] === "clear") {
|
|
@@ -173,7 +219,7 @@ async function main(argv: string[]) {
|
|
|
173
219
|
}
|
|
174
220
|
const all = ledger.quota();
|
|
175
221
|
if (!Object.keys(all).length) return console.log("no quota state recorded");
|
|
176
|
-
for (const q of Object.values(all)) console.log(`${q.agent.padEnd(9)} ${quotaLabel(q) || "ok"} (${q.source}, ${
|
|
222
|
+
for (const q of Object.values(all)) console.log(`${q.agent.padEnd(9)} ${quotaLabel(q) || "ok"} (${q.source}, ${fmtDateTime12(q.at)}${q.detail ? `, ${q.detail}` : ""})`);
|
|
177
223
|
return;
|
|
178
224
|
}
|
|
179
225
|
case "keys": {
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,20 @@ export interface Config {
|
|
|
28
28
|
checkpoints: boolean;
|
|
29
29
|
/** Refresh the public price list weekly for /usage (set false to stay offline). */
|
|
30
30
|
priceUpdates: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Keep the machine awake while codemax runs: "on" prevents sleep, "lid" also keeps
|
|
33
|
+
* working with the laptop lid closed. See src/core/awake.ts for each platform.
|
|
34
|
+
*/
|
|
35
|
+
awake: "off" | "on" | "lid";
|
|
36
|
+
/**
|
|
37
|
+
* Side agent: another agent that reviews the main agent's work after each turn and
|
|
38
|
+
* leaves a note when something needs attention. `forward: "auto"` sends notes to the
|
|
39
|
+
* main agent by itself; "ask" waits for /forward.
|
|
40
|
+
*/
|
|
41
|
+
/** `every`: review every Nth working turn (1 = each turn, 0 = only on /review). Each review is a request on the side agent's plan. */
|
|
42
|
+
sideAgent: { agent: AgentId | null; forward: "ask" | "auto"; model?: string; every?: number };
|
|
43
|
+
/** Check npm for a newer codemax at most every 12 hours and offer /update. */
|
|
44
|
+
updateCheck: boolean;
|
|
31
45
|
/** Colour theme: "auto" follows the terminal's background, or force "dark" / "light". */
|
|
32
46
|
theme: "auto" | "dark" | "light";
|
|
33
47
|
/**
|
|
@@ -49,6 +63,9 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
49
63
|
summarizeOnSwitch: true,
|
|
50
64
|
bellAfterSeconds: 20,
|
|
51
65
|
theme: "auto",
|
|
66
|
+
awake: "off",
|
|
67
|
+
updateCheck: true,
|
|
68
|
+
sideAgent: { agent: null, forward: "ask" },
|
|
52
69
|
brief: { turns: 10, maxChars: 8000, turnChars: 1200, resumeChars: 24000 },
|
|
53
70
|
};
|
|
54
71
|
|
|
@@ -71,6 +88,7 @@ export function loadConfig(): Config {
|
|
|
71
88
|
brief: { ...DEFAULT_CONFIG.brief, ...(raw.brief ?? {}) },
|
|
72
89
|
agentArgs: { ...(raw.agentArgs ?? {}) },
|
|
73
90
|
budgets: { ...(raw.budgets ?? {}) },
|
|
91
|
+
sideAgent: { ...DEFAULT_CONFIG.sideAgent, ...(raw.sideAgent ?? {}) },
|
|
74
92
|
routes: Array.isArray(raw.routes) ? raw.routes : [],
|
|
75
93
|
};
|
|
76
94
|
} catch {
|