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.
@@ -1,8 +1,9 @@
1
1
  import type { AgentId } from "../adapters/types.ts";
2
- import type { Engine, EngineEvent, EngineInfo, EngineSessionRef, EngineStartOptions, PermissionRequest, StopReason, ToolStatus } from "./types.ts";
2
+ import type { Engine, EngineEvent, EngineInfo, EngineOption, EngineSessionRef, EngineStartOptions, PermissionRequest, StopReason, ToolStatus } from "./types.ts";
3
3
  import { agentEnv } from "./env.ts";
4
4
  import { platformArgv } from "../core/spawn.ts";
5
5
  import { classifyError } from "./errors.ts";
6
+ import pkg from "../../package.json" with { type: "json" };
6
7
 
7
8
  export interface CodexEngineOptions {
8
9
  /** Command for the app server. Default: codex app-server. */
@@ -18,6 +19,10 @@ export interface CodexEngineOptions {
18
19
  * `answerPermission`.
19
20
  */
20
21
  export class CodexEngine implements Engine {
22
+ /** Models this account can use, from `model/list`. */
23
+ private catalog: { id: string; name: string; efforts: string[]; defaultEffort?: string }[] = [];
24
+ /** Reasoning effort chosen with /effort (sent with each turn); undefined = the model's default. */
25
+ private effort: string | undefined;
21
26
  readonly agent: AgentId = "codex";
22
27
  info: EngineInfo | null = null;
23
28
  session: EngineSessionRef | null = null;
@@ -49,7 +54,7 @@ export class CodexEngine implements Engine {
49
54
  this.cwd = o.cwd;
50
55
  this.transport = this.opts.transport ?? this.spawn(o.cwd);
51
56
  this.transport.onLine((line) => this.onLine(line));
52
- const init = await this.request("initialize", { clientInfo: { name: "codemax", version: "0.3.0" }, capabilities: {} });
57
+ const init = await this.request("initialize", { clientInfo: { name: "codemax", version: pkg.version }, capabilities: {} });
53
58
  this.notify("initialized", {});
54
59
 
55
60
  let thread: any;
@@ -65,6 +70,9 @@ export class CodexEngine implements Engine {
65
70
  this.model = thread.model ?? undefined;
66
71
  this.session = { agent: "codex", id: thread.id, cwd: o.cwd, startedAt: Date.now(), lastUsedAt: Date.now() };
67
72
  const account: any = await this.request("account/read", {}).catch(() => null);
73
+ // The models this account can use, straight from Codex: never a list of our own.
74
+ // An older Codex without model/list must not hold up startup: give it a few seconds.
75
+ this.catalog = await Promise.race([this.listModels(), Bun.sleep(4000).then(() => [])]).catch(() => []);
68
76
  this.info = {
69
77
  agent: "codex",
70
78
  name: "Codex",
@@ -74,8 +82,9 @@ export class CodexEngine implements Engine {
74
82
  { id: "never", name: "full auto (no approvals)" },
75
83
  ],
76
84
  currentMode: "on-request",
77
- models: [...new Set([this.model, "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"].filter(Boolean))].map((id) => ({ id: id!, name: id! })),
85
+ models: this.catalog.length ? this.catalog.map((m) => ({ id: m.id, name: m.name })) : this.model ? [{ id: this.model, name: this.model }] : [],
78
86
  currentModel: this.model,
87
+ options: this.effortOptions(),
79
88
  plan: account?.account?.planType ? `chatgpt ${account.account.planType}` : undefined,
80
89
  };
81
90
  this.emit({ type: "session", ref: this.session });
@@ -281,7 +290,7 @@ export class CodexEngine implements Engine {
281
290
  const done = new Promise<StopReason>((resolve) => (this.turn = { resolve }));
282
291
  try {
283
292
  const approvalPolicy = this.info?.currentMode === "never" ? "never" : "on-request";
284
- const res = await this.request("turn/start", { threadId: this.threadId, cwd: this.cwd, input: [{ type: "text", text }], model: this.model, approvalPolicy });
293
+ const res = await this.request("turn/start", { threadId: this.threadId, cwd: this.cwd, input: [{ type: "text", text }], model: this.model, approvalPolicy, ...(this.effort ? { effort: this.effort } : {}) });
285
294
  if (this.turn) this.turn.id = res?.turn?.id;
286
295
  } catch (e: any) {
287
296
  const msg = `${e?.message ?? e} ${JSON.stringify(e?.data ?? "")}`.trim();
@@ -314,6 +323,46 @@ export class CodexEngine implements Engine {
314
323
  async setModel(modelId: string) {
315
324
  this.model = modelId;
316
325
  if (this.info) this.info.currentModel = modelId;
326
+ // Reasoning efforts differ per model: keep the chosen one only if the new model has it.
327
+ const efforts = this.catalog.find((m) => m.id === modelId)?.efforts ?? [];
328
+ if (this.effort && !efforts.includes(this.effort)) this.effort = undefined;
329
+ if (this.info) this.info.options = this.effortOptions();
330
+ }
331
+
332
+ async setOption(optionId: string, value: string) {
333
+ if (optionId !== "effort") return;
334
+ this.effort = value;
335
+ if (this.info) this.info.options = this.effortOptions();
336
+ }
337
+
338
+ /** `model/list`, all pages, hidden models left out. */
339
+ private async listModels(): Promise<{ id: string; name: string; efforts: string[]; defaultEffort?: string }[]> {
340
+ const out: { id: string; name: string; efforts: string[]; defaultEffort?: string }[] = [];
341
+ let cursor: string | undefined;
342
+ for (let page = 0; page < 10; page++) {
343
+ const r: any = await this.request("model/list", cursor ? { cursor } : {});
344
+ for (const m of r?.data ?? []) {
345
+ if (m?.hidden) continue;
346
+ const id = String(m.model ?? m.id ?? "");
347
+ if (!id) continue;
348
+ out.push({
349
+ id,
350
+ name: String(m.displayName ?? id),
351
+ efforts: (m.supportedReasoningEfforts ?? []).map((e: any) => String(e?.reasoningEffort ?? e)).filter(Boolean),
352
+ defaultEffort: m.defaultReasoningEffort ? String(m.defaultReasoningEffort) : undefined,
353
+ });
354
+ }
355
+ cursor = r?.nextCursor ?? undefined;
356
+ if (!cursor) break;
357
+ }
358
+ return out;
359
+ }
360
+
361
+ /** The current model's reasoning efforts, as Codex lists them. */
362
+ private effortOptions(): EngineOption[] {
363
+ const m = this.catalog.find((x) => x.id === this.model);
364
+ if (!m?.efforts.length) return [];
365
+ return [{ id: "effort", name: "Reasoning effort", category: "thought_level", current: this.effort ?? m.defaultEffort ?? "default", values: m.efforts.map((e) => ({ value: e, name: e })) }];
317
366
  }
318
367
 
319
368
  async close() {
@@ -35,7 +35,7 @@ function grokUsageTail(cwd: string, sessionId: string): { read(): UsageSample[]
35
35
  };
36
36
  }
37
37
 
38
- const CLAUDE_ADAPTER_PKG = "@zed-industries/claude-code-acp@0.16.2";
38
+ const CLAUDE_ADAPTER_PKG = "@agentclientprotocol/claude-agent-acp@0.93.0";
39
39
 
40
40
  /**
41
41
  * Command for the Claude ACP adapter (it runs the user's own `claude`).
@@ -46,8 +46,8 @@ function claudeAdapterCmd(): string[] | null {
46
46
  const runningOnBun = basename(process.execPath).toLowerCase().startsWith("bun");
47
47
  if (process.platform !== "win32") {
48
48
  const candidates = [
49
- join(import.meta.dir, "..", "..", "node_modules", ".bin", "claude-code-acp"),
50
- join(process.cwd(), "node_modules", ".bin", "claude-code-acp"),
49
+ join(import.meta.dir, "..", "..", "node_modules", ".bin", "claude-agent-acp"),
50
+ join(process.cwd(), "node_modules", ".bin", "claude-agent-acp"),
51
51
  ];
52
52
  for (const c of candidates) if (existsSync(c)) return [c];
53
53
  }
@@ -55,15 +55,15 @@ function claudeAdapterCmd(): string[] | null {
55
55
  // do not (Windows) and when no `bun` is on PATH (installed through npm).
56
56
  if (runningOnBun) {
57
57
  try {
58
- const pkgJson = Bun.resolveSync("@zed-industries/claude-code-acp/package.json", import.meta.dir);
58
+ const pkgJson = Bun.resolveSync("@agentclientprotocol/claude-agent-acp/package.json", import.meta.dir);
59
59
  const bin = JSON.parse(readFileSync(pkgJson, "utf8")).bin;
60
- const entry = typeof bin === "string" ? bin : bin?.["claude-code-acp"];
60
+ const entry = typeof bin === "string" ? bin : bin?.["claude-agent-acp"];
61
61
  if (entry && existsSync(join(dirname(pkgJson), entry))) return [process.execPath, join(dirname(pkgJson), entry)];
62
62
  } catch {
63
63
  /* not installed next to codemax */
64
64
  }
65
65
  }
66
- const onPath = Bun.which("claude-code-acp");
66
+ const onPath = Bun.which("claude-agent-acp");
67
67
  if (onPath) return [onPath];
68
68
  const bun = Bun.which("bun") ?? (runningOnBun ? process.execPath : null);
69
69
  const bunx = Bun.which("bunx") ? [Bun.which("bunx")!] : bun ? [bun, "x"] : null;
@@ -27,6 +27,8 @@ export type EngineEvent =
27
27
  costUsd?: number;
28
28
  /** Rate-limit window consumption, when the agent exposes it. */
29
29
  usedPercent?: number;
30
+ /** Subscription plan limits the agent reported (Claude: 5-hour, weekly…), one entry per window. */
31
+ planLimits?: PlanLimit[];
30
32
  /** Context window fill, when reported (tokens used / size). */
31
33
  contextUsed?: number;
32
34
  contextSize?: number;
@@ -36,6 +38,15 @@ export type EngineEvent =
36
38
  | { type: "error"; kind: ErrorKind; message: string }
37
39
  | { type: "log"; level: "info" | "warn" | "error"; message: string };
38
40
 
41
+ export interface PlanLimit {
42
+ /** e.g. "five_hour", "seven_day", "seven_day_opus". */
43
+ window: string;
44
+ usedPercent?: number;
45
+ resetsAt?: number;
46
+ /** "allowed" | "allowed_warning" | "rejected" (out of this window's quota). */
47
+ status?: string;
48
+ }
49
+
39
50
  export type ToolStatus = "pending" | "in_progress" | "completed" | "failed";
40
51
  export type StopReason = "end_turn" | "max_tokens" | "cancelled" | "refusal" | "error" | "other";
41
52
  export type ErrorKind = "quota" | "auth" | "protocol" | "crashed" | "other";
package/src/tui/app.ts CHANGED
@@ -68,10 +68,15 @@ export class App {
68
68
  this.setStatus("");
69
69
  }
70
70
 
71
- async start(agent: AgentId, fresh: boolean) {
71
+ /** `explicit`: the user named this agent (--agent), so never start a different one. */
72
+ async start(agent: AgentId, fresh: boolean, o: { explicit?: boolean } = {}) {
72
73
  await this.refreshDetect();
73
- const first = this.detect.get(agent)?.installed ? agent : this.firstInstalled();
74
+ const installed = this.detect.get(agent)?.installed;
75
+ if (!installed && o.explicit) throw new Error(`${agent} is not installed. Installed: ${this.installedList() || "none"}`);
76
+ const first = installed ? agent : this.firstInstalled();
74
77
  if (!first) throw new Error("no supported agent CLI found on PATH");
78
+ // Under tests a fallback must never reach a real agent (no logins, no quota).
79
+ if (first !== agent && process.env.NODE_ENV === "test") throw new Error(`test asked for ${agent}, which is not available; refusing to fall back to ${first}`);
75
80
  if (first !== agent) this.setStatus(`${agent} not found, starting ${first}`);
76
81
  await this.launch(getAdapter(first), { fresh });
77
82
  return this.done;
@@ -282,6 +287,10 @@ export class App {
282
287
  );
283
288
  }
284
289
 
290
+ private installedList(): string {
291
+ return [...this.detect.entries()].filter(([, d]) => d.installed).map(([id]) => id).join(", ");
292
+ }
293
+
285
294
  private firstInstalled(): AgentId | undefined {
286
295
  return listAdapters(this.config.order).find((a) => this.detect.get(a.id)?.installed)?.id;
287
296
  }
@@ -360,11 +369,11 @@ export class App {
360
369
  }
361
370
 
362
371
  /** Run codemax on the real terminal until the user quits. */
363
- export async function runApp(config: Config, ledger: Ledger, cwd: string, agent: AgentId, fresh: boolean) {
372
+ export async function runApp(config: Config, ledger: Ledger, cwd: string, agent: AgentId, fresh: boolean, explicit = false) {
364
373
  const renderer = await createCliRenderer({ exitOnCtrlC: false, exitSignals: ["SIGTERM", "SIGHUP"], useMouse: true, targetFps: 60 });
365
374
  const app = new App(renderer, config, ledger, cwd);
366
375
  try {
367
- await app.start(agent, fresh);
376
+ await app.start(agent, fresh, { explicit });
368
377
  } finally {
369
378
  renderer.destroy();
370
379
  try {