chatccc 0.2.10 → 0.2.12

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/src/feishu-api.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { readdir, stat } from "node:fs/promises";
1
+ import { readdir, stat, readFile } from "node:fs/promises";
2
+ import { resolve as resolvePath } from "node:path";
2
3
  import { join } from "node:path";
4
+ import sharp from "sharp";
3
5
 
4
6
  import {
5
7
  APP_ID,
@@ -9,9 +11,10 @@ import {
9
11
  PROJECT_ROOT,
10
12
  CLAUDE_SESSION_PREFIX,
11
13
  CURSOR_SESSION_PREFIX,
14
+ CODEX_SESSION_PREFIX,
12
15
  ts,
13
16
  } from "./config.ts";
14
- import { buildButtons } from "./cards.ts";
17
+ import { buildHelpCard } from "./cards.ts";
15
18
 
16
19
  // ---------------------------------------------------------------------------
17
20
  // Auth
@@ -78,6 +81,18 @@ const REQUIRED_PERMISSIONS: PermissionDef[] = [
78
81
  path: "/im/v1/messages/om_000000000000000000000000",
79
82
  body: JSON.stringify({ content: JSON.stringify({ elements: [{ tag: "markdown", content: " " }] }) }),
80
83
  },
84
+ {
85
+ scope: "cardkit:card",
86
+ description: "创建/更新群卡片(流式进度卡片、状态卡片等)",
87
+ method: "POST",
88
+ path: "/cardkit/v1/cards",
89
+ body: JSON.stringify({
90
+ schema: "2.0",
91
+ config: { update_multi: true },
92
+ header: { template: "blue", title: { tag: "plain_text", content: "permcheck" } },
93
+ body: { direction: "vertical", elements: [{ tag: "markdown", content: " " }] },
94
+ }),
95
+ },
81
96
  ];
82
97
 
83
98
  interface PermissionResult {
@@ -224,6 +239,7 @@ export function extractSessionInfo(description: string): { sessionId: string; to
224
239
  const PREFIXES: Array<{ prefix: string; tool: string }> = [
225
240
  { prefix: CLAUDE_SESSION_PREFIX, tool: "claude" },
226
241
  { prefix: CURSOR_SESSION_PREFIX, tool: "cursor" },
242
+ { prefix: CODEX_SESSION_PREFIX, tool: "codex" },
227
243
  ];
228
244
  for (const { prefix, tool } of PREFIXES) {
229
245
  const idx = description.indexOf(prefix);
@@ -240,6 +256,101 @@ export function extractSessionId(description: string): string | null {
240
256
  return extractSessionInfo(description)?.sessionId ?? null;
241
257
  }
242
258
 
259
+ // ---------------------------------------------------------------------------
260
+ // Avatar
261
+ // ---------------------------------------------------------------------------
262
+
263
+ const AVATAR_DIR = resolvePath(PROJECT_ROOT, "images", "avatars");
264
+ const AVATAR_SOURCES: Record<string, string> = {
265
+ new: process.env.CHATCCC_AVATAR_NEW_URL?.trim() || resolvePath(AVATAR_DIR, "status_new.png"),
266
+ busy: process.env.CHATCCC_AVATAR_BUSY_URL?.trim() || resolvePath(AVATAR_DIR, "status_busy.png"),
267
+ idle: process.env.CHATCCC_AVATAR_IDLE_URL?.trim() || resolvePath(AVATAR_DIR, "status_idle.png"),
268
+ };
269
+
270
+ const avatarKeyCache = new Map<string, string>();
271
+
272
+ function isHttpUrl(source: string): boolean {
273
+ return /^https?:\/\//i.test(source);
274
+ }
275
+
276
+ async function loadAvatarSource(source: string): Promise<{ buffer: Buffer; contentType: string; filename: string }> {
277
+ let input: Buffer;
278
+ if (isHttpUrl(source)) {
279
+ const resp = await fetch(source);
280
+ if (!resp.ok) throw new Error(`download avatar url failed: status=${resp.status}`);
281
+ input = Buffer.from(await resp.arrayBuffer());
282
+ } else {
283
+ input = await readFile(source);
284
+ }
285
+
286
+ const jpeg = await sharp(input)
287
+ .resize(256, 256, { fit: "cover", position: "center" })
288
+ .flatten({ background: "#ffffff" })
289
+ .removeAlpha()
290
+ .jpeg({ quality: 95, progressive: false })
291
+ .toBuffer();
292
+
293
+ return {
294
+ buffer: jpeg,
295
+ contentType: "image/jpeg",
296
+ filename: "avatar.jpg",
297
+ };
298
+ }
299
+
300
+ async function uploadImage(token: string, source: string): Promise<string> {
301
+ const image = await loadAvatarSource(source);
302
+ const blob = new Blob([new Uint8Array(image.buffer)], { type: image.contentType });
303
+ const form = new FormData();
304
+ form.append("image_type", "avatar");
305
+ form.append("image", blob, image.filename);
306
+
307
+ // Group avatars need an im/v1 image_key uploaded with image_type=avatar.
308
+ const resp = await fetch(`${BASE_URL}/im/v1/images`, {
309
+ method: "POST",
310
+ headers: { Authorization: `Bearer ${token}` },
311
+ body: form,
312
+ });
313
+ const text = await resp.text();
314
+ let data: { code: number; msg?: string; data?: { image_key?: string } };
315
+ try { data = JSON.parse(text); } catch {
316
+ throw new Error(`uploadImage non-JSON response: ${text.slice(0, 200)}`);
317
+ }
318
+ if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
319
+ return data.data!.image_key!;
320
+ }
321
+
322
+ async function getOrUploadAvatarKey(token: string, status: string): Promise<string> {
323
+ const cached = avatarKeyCache.get(status);
324
+ if (cached) return cached;
325
+ const key = await uploadImage(token, AVATAR_SOURCES[status]);
326
+ avatarKeyCache.set(status, key);
327
+ console.log(`[${ts()}] [AVATAR] Uploaded "${status}" → image_key=${key}`);
328
+ return key;
329
+ }
330
+
331
+ export async function setChatAvatar(token: string, chatId: string, status: string): Promise<void> {
332
+ try {
333
+ const avatarKey = await getOrUploadAvatarKey(token, status);
334
+ const resp = await fetch(`${BASE_URL}/im/v1/chats/${chatId}`, {
335
+ method: "PUT",
336
+ headers: {
337
+ Authorization: `Bearer ${token}`,
338
+ "Content-Type": "application/json",
339
+ },
340
+ body: JSON.stringify({ avatar: avatarKey }),
341
+ });
342
+ const text = await resp.text();
343
+ let data: { code: number; msg?: string };
344
+ try { data = JSON.parse(text); } catch {
345
+ if (resp.ok) return;
346
+ throw new Error(`setChatAvatar non-JSON response (status=${resp.status}): ${text.slice(0, 200)}`);
347
+ }
348
+ if (data.code !== 0) throw new Error(`[${data.code}] ${data.msg}`);
349
+ } catch (err) {
350
+ console.error(`[${ts()}] [AVATAR] setChatAvatar FAIL: chatId=${chatId} status=${status} ${(err as Error).message}`);
351
+ }
352
+ }
353
+
243
354
  // ---------------------------------------------------------------------------
244
355
  // Messaging
245
356
  // ---------------------------------------------------------------------------
@@ -334,19 +445,7 @@ export async function sendRestartCard(token: string): Promise<void> {
334
445
 
335
446
  console.log(`[${ts()}] [RESTART] Latest active chat: ${latestChatId} (mtime=${new Date(latestTime).toISOString()})`);
336
447
 
337
- const restartCard = JSON.stringify({
338
- config: { wide_screen_mode: true },
339
- header: { template: "green", title: { content: "ChatCCC Started", tag: "plain_text" } },
340
- elements: [
341
- { tag: "div", text: { tag: "lark_md", content: "Bot 已启动完成,可以继续使用。\n\n发送 **/new** 创建新会话(默认 Claude Code)\n发送 **/new claude** 创建新 Claude 对话\n发送 **/new cursor** 创建新 Cursor 会话(启动需要多等几秒)" } },
342
- buildButtons([
343
- { text: "新建 Claude Code 会话(/new claude)", value: JSON.stringify({ cmd: "new" }), type: "primary" },
344
- { text: "新建 Cursor 会话(/new cursor)", value: JSON.stringify({ cmd: "new cursor" }), type: "primary" },
345
- { text: "重启Chat CCC(/restart)", value: JSON.stringify({ cmd: "restart" }), type: "danger" },
346
- { text: "切换工作路径(/cd)", value: JSON.stringify({ cmd: "cd" }), type: "default" },
347
- ]),
348
- ],
349
- });
448
+ const restartCard = buildHelpCard("", { greeting: "Bot 已启动完成,可以继续使用。" });
350
449
  await fetch(`${BASE_URL}/im/v1/messages?receive_id_type=chat_id`, {
351
450
  method: "POST",
352
451
  headers: {
@@ -388,4 +487,4 @@ export async function updateCardMessage(token: string, messageId: string, conten
388
487
  return false;
389
488
  }
390
489
  return true;
391
- }
490
+ }
package/src/index.ts CHANGED
@@ -1,16 +1,17 @@
1
1
  /**
2
- * ChatCCC — Feishu Bot Bridge for Claude Code (TypeScript)
2
+ * ChatCCC — Feishu Bot Bridge for AI Coding Tools (TypeScript)
3
3
  * =================================================================
4
- * When a user sends "/new" to the bot:
5
- * 1. Create a Claude session via Agent SDK, get session ID from init event
4
+ * Supported tools: Claude Code, Cursor, Codex (OpenAI).
5
+ *
6
+ * When a user sends "/new [tool]" to the bot:
7
+ * 1. Create an AI tool session via the corresponding adapter, get session ID
6
8
  * 2. Create a new Feishu group chat and add the user
7
9
  * 3. Rename the group (name + description) to the session ID
8
- * 4. Stream SDK output to logs/session-<session-id>.jsonl
9
- * 5. Reply to the new group with session info and welcome message
10
+ * 4. Reply to the new group with session info and welcome message
10
11
  *
11
- * Auto-resume: when any message is received in a Claude session group
12
- * (group description contains "Claude Session:"), the bot extracts the
13
- * session ID, resumes the session via SDK, sends the user's text, and
12
+ * Auto-resume: when any message is received in a session group
13
+ * (group description contains a tool-specific prefix), the bot extracts the
14
+ * session ID, resumes the session, sends the user's text, and
14
15
  * streams the response to the session's jsonl file.
15
16
  *
16
17
  * Buttons: progress cards have a 停止 button; help messages have /new and /restart buttons.
@@ -23,7 +24,7 @@
23
24
 
24
25
  import { spawn } from "node:child_process";
25
26
  import { readdir, stat } from "node:fs/promises";
26
- import { resolve, dirname, basename } from "node:path";
27
+ import { resolve, dirname } from "node:path";
27
28
 
28
29
  import { WSClient, EventDispatcher } from "@larksuiteoapi/node-sdk";
29
30
  import WebSocket from "ws";
@@ -65,6 +66,7 @@ import {
65
66
  recallMessage,
66
67
  sendCardReply,
67
68
  sendTextReply,
69
+ setChatAvatar,
68
70
  updateCardMessage,
69
71
  updateChatInfo,
70
72
  sendRestartCard,
@@ -87,6 +89,19 @@ import {
87
89
  getAdapterForTool,
88
90
  } from "./session.ts";
89
91
 
92
+ export function cwdDisplayName(cwd: string): string {
93
+ const trimmed = cwd.trim().replace(/[\\/]+$/, "");
94
+ return trimmed.split(/[\\/]/).filter(Boolean).pop() || trimmed || "cwd";
95
+ }
96
+
97
+ export function sessionChatName(left: string, cwd: string): string {
98
+ return `${left}-${cwdDisplayName(cwd)}`;
99
+ }
100
+
101
+ function isUntitledSessionChatName(name: string): boolean {
102
+ return name === "新会话" || name.startsWith("新会话-");
103
+ }
104
+
90
105
  // ---------------------------------------------------------------------------
91
106
  // Event types
92
107
  // ---------------------------------------------------------------------------
@@ -177,7 +192,7 @@ function parseCardAction(data: unknown): CardActionResult | null {
177
192
  }
178
193
  if (!cmd) return null;
179
194
 
180
- const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new cursor": "/new cursor", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions" };
195
+ const CMD_MAP: Record<string, string> = { stop: "/stop", new: "/new", "new cursor": "/new cursor", "new codex": "/new codex", restart: "/restart", status: "/status", cd: "/cd", sessions: "/sessions", forget: "/forget" };
181
196
  let text = CMD_MAP[cmd] ?? "";
182
197
  if (cmd === "cd" && typeof action.value === "object" && action.value !== null) {
183
198
  const path = (action.value as Record<string, string>).path;
@@ -317,10 +332,10 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
317
332
  if (text === "/new" || text.startsWith("/new ")) {
318
333
  const toolArg = text.slice(5).trim();
319
334
  const tool = toolArg || "claude";
320
- const validTools = ["claude", "cursor"];
335
+ const validTools = ["claude", "cursor", "codex"];
321
336
  if (!validTools.includes(tool)) {
322
337
  const warnToken = await getTenantAccessToken();
323
- await sendCardReply(warnToken, chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor)。`, "red");
338
+ await sendCardReply(warnToken, chatId, "Error", `未知的工具类型: "${toolArg}"。支持: claude (Claude Code), cursor (Cursor), codex (Codex)。`, "red");
324
339
  return;
325
340
  }
326
341
  const toolLabel = toolDisplayName(tool);
@@ -335,8 +350,11 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
335
350
  const freshToken = await getTenantAccessToken();
336
351
 
337
352
  let sessionId: string;
353
+ let sessionCwd: string;
338
354
  try {
339
- sessionId = await initClaudeSession(tool);
355
+ const init = await initClaudeSession(tool);
356
+ sessionId = init.sessionId;
357
+ sessionCwd = init.cwd;
340
358
  console.log(`[${ts()}] [STEP 1/4] ${toolLabel} session created: ${sessionId} → OK`);
341
359
  } catch (err) {
342
360
  console.error(`[${ts()}] [STEP 1/4] FAIL: ${(err as Error).message}`);
@@ -348,9 +366,12 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
348
366
  return;
349
367
  }
350
368
 
369
+ const cwd = sessionCwd;
370
+ const initialName = sessionChatName("新会话", cwd);
371
+
351
372
  let newChatId: string;
352
373
  try {
353
- newChatId = await createGroupChat(freshToken, `新会话-${sessionId}`, [openId]);
374
+ newChatId = await createGroupChat(freshToken, initialName, [openId]);
354
375
  console.log(`[${ts()}] [STEP 2/4] Created Feishu group: ${newChatId} → OK`);
355
376
  } catch (err) {
356
377
  console.error(`[${ts()}] [STEP 2/4] FAIL: ${(err as Error).message}`);
@@ -359,7 +380,6 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
359
380
  }
360
381
 
361
382
  try {
362
- const initialName = `新会话-${sessionId}`;
363
383
  const descPrefix = sessionPrefixForTool(tool);
364
384
  await updateChatInfo(freshToken, newChatId, initialName, `${descPrefix} ${sessionId}`);
365
385
  console.log(`[${ts()}] [STEP 3/4] Renamed group → name="${initialName}" (${toolLabel}) → OK`);
@@ -369,7 +389,6 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
369
389
  return;
370
390
  }
371
391
 
372
- const cwd = await getDefaultCwd();
373
392
  const adapter = getAdapterForTool(tool);
374
393
  await sendCardReply(
375
394
  freshToken, newChatId, `${toolLabel} Session Ready`,
@@ -383,6 +402,7 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
383
402
  );
384
403
 
385
404
  console.log(`[${ts()}] [STEP 4/4] Replied to new group → OK`);
405
+ setChatAvatar(freshToken, newChatId, "new").catch(() => {});
386
406
  console.log(`${"=".repeat(60)}`);
387
407
  return;
388
408
  }
@@ -401,10 +421,13 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
401
421
 
402
422
  const freshToken = await getTenantAccessToken();
403
423
 
404
- if (chatInfo.name === `新会话-${sessionId}`) {
405
- const MAX_PREFIX = 20;
424
+ if (isUntitledSessionChatName(chatInfo.name)) {
425
+ const MAX_PREFIX = 10;
406
426
  const prefix = text.slice(0, MAX_PREFIX);
407
- const newName = `${prefix} ${sessionId}`;
427
+ const adapter = getAdapterForTool(descriptionTool);
428
+ const info = await adapter.getSessionInfo(sessionId).catch(() => undefined);
429
+ const sessionCwd = info?.cwd ?? (await getDefaultCwd());
430
+ const newName = sessionChatName(prefix, sessionCwd);
408
431
  try {
409
432
  await updateChatInfo(freshToken, chatId, newName, description);
410
433
  console.log(`[${ts()}] [RENAME] First message → group renamed to "${newName}"`);
@@ -492,6 +515,61 @@ async function handleCommand(text: string, chatId: string, openId: string, msgTi
492
515
  return;
493
516
  }
494
517
 
518
+ if (text === "/forget") {
519
+ const adapter = getAdapterForTool(descriptionTool);
520
+ let cwd: string;
521
+ try {
522
+ const info = await adapter.getSessionInfo(sessionId);
523
+ cwd = info?.cwd ?? (await getDefaultCwd());
524
+ } catch {
525
+ cwd = await getDefaultCwd();
526
+ }
527
+
528
+ const existing = chatSessionMap.get(chatId);
529
+ if (existing) {
530
+ existing.stopped = true;
531
+ if (existing.spinnerTimer) { clearInterval(existing.spinnerTimer); existing.spinnerTimer = null; }
532
+ existing.close();
533
+ chatSessionMap.delete(chatId);
534
+ }
535
+
536
+ let newSessionId: string;
537
+ try {
538
+ const init = await initClaudeSession(descriptionTool, cwd);
539
+ newSessionId = init.sessionId;
540
+ } catch (err) {
541
+ await sendCardReply(freshToken, chatId, "Error", `Failed to create new session:\n${(err as Error).message}`, "red");
542
+ return;
543
+ }
544
+
545
+ const descPrefix = sessionPrefixForTool(descriptionTool);
546
+ const initialName = sessionChatName("新会话", cwd);
547
+ await updateChatInfo(freshToken, chatId, initialName, `${descPrefix} ${newSessionId}`);
548
+ console.log(`[${ts()}] [FORGET] Group updated: name="${initialName}" desc="${descPrefix} ${newSessionId}"`);
549
+
550
+ sessionInfoMap.set(chatId, {
551
+ sessionId: newSessionId,
552
+ turnCount: 0,
553
+ lastContextTokens: 0,
554
+ startTime: Date.now(),
555
+ tool: descriptionTool,
556
+ });
557
+
558
+ setChatAvatar(freshToken, chatId, "new").catch(() => {});
559
+
560
+ await sendCardReply(
561
+ freshToken, chatId, `${toolLabel} Session Reset`,
562
+ `会话已重置为新的 **${toolLabel}** 会话。\n\n` +
563
+ `**Session ID:** ${newSessionId}\n` +
564
+ `**工作目录:** \`${cwd}\`\n\n` +
565
+ `直接在这里发消息即可继续对话。`,
566
+ "green"
567
+ );
568
+
569
+ console.log(`[${ts()}] [FORGET] Session ${sessionId} → ${newSessionId} (same cwd=${cwd})`);
570
+ return;
571
+ }
572
+
495
573
  // /git <args>:在「当前会话工作目录」执行 git 命令,把输出回发到群里。
496
574
  // 注意 cwd 必须取自 adapter.getSessionInfo(会话真实 cwd),而非
497
575
  // getDefaultCwd(下一次 /new 才会使用的默认路径)。
@@ -873,4 +951,4 @@ main().catch((err: Error) => {
873
951
  if (err.stack) console.error(err.stack);
874
952
  printServiceDidNotStart(`main() 异常: ${err.message}`);
875
953
  process.exit(1);
876
- });
954
+ });
package/src/session.ts CHANGED
@@ -19,11 +19,12 @@ import {
19
19
  sendCardKitMessage,
20
20
  updateCardKitCard,
21
21
  } from "./cardkit.ts";
22
- import { sendTextReply } from "./feishu-api.ts";
22
+ import { sendTextReply, setChatAvatar } from "./feishu-api.ts";
23
23
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
24
24
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
25
25
  import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
26
26
  import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
27
+ import { createCodexAdapter } from "./adapters/codex-adapter.ts";
27
28
 
28
29
  // ---------------------------------------------------------------------------
29
30
  // Shared state (imported by index.ts)
@@ -91,6 +92,8 @@ export function getAdapterForTool(tool: string): ToolAdapter {
91
92
  let adapter: ToolAdapter;
92
93
  if (tool === "cursor") {
93
94
  adapter = createCursorAdapter();
95
+ } else if (tool === "codex") {
96
+ adapter = createCodexAdapter();
94
97
  } else {
95
98
  adapter = createClaudeAdapter({
96
99
  model: CLAUDE_MODEL,
@@ -255,11 +258,20 @@ function formatToolConfigForLog(tool: string, sessionModel?: string): string {
255
258
  if (tool === "cursor") {
256
259
  return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
257
260
  }
261
+ if (tool === "codex") {
262
+ const m = process.env.CHATCCC_CODEX_MODEL?.trim();
263
+ const e = process.env.CHATCCC_CODEX_EFFORT?.trim();
264
+ const modelStr = m && m !== "default" ? m : "(由 codex config.toml 决定)";
265
+ const effortStr = e && e !== "default"
266
+ ? `effort=${e}`
267
+ : "effort=(由 codex config.toml 决定)";
268
+ return `model=${modelStr}, ${effortStr}`;
269
+ }
258
270
  return `model=${anthropicConfigDisplay(CLAUDE_MODEL)}, effort=${anthropicConfigDisplay(CLAUDE_EFFORT)}`;
259
271
  }
260
272
 
261
- export async function initClaudeSession(tool: string): Promise<string> {
262
- const cwd = await getDefaultCwd();
273
+ export async function initClaudeSession(tool: string, overrideCwd?: string): Promise<{ sessionId: string; cwd: string }> {
274
+ const cwd = overrideCwd ?? (await getDefaultCwd());
263
275
  const adapter = getAdapterForTool(tool);
264
276
  console.log(
265
277
  `[${ts()}] [STEP 1/5] Creating ${adapter.displayName} session (${formatToolConfigForLog(tool)}, cwd=${cwd})`
@@ -273,7 +285,7 @@ export async function initClaudeSession(tool: string): Promise<string> {
273
285
 
274
286
  await addRecentDir(cwd);
275
287
 
276
- return sessionId;
288
+ return { sessionId, cwd };
277
289
  }
278
290
 
279
291
  export async function resumeAndPrompt(
@@ -307,6 +319,8 @@ export async function resumeAndPrompt(
307
319
  });
308
320
  const myGen = sessionGen;
309
321
 
322
+ setChatAvatar(token, chatId, "busy").catch(() => {});
323
+
310
324
  const now = Date.now();
311
325
  const existingInfo = sessionInfoMap.get(chatId);
312
326
  sessionInfoMap.set(chatId, {
@@ -437,6 +451,7 @@ export async function resumeAndPrompt(
437
451
  if (!cEntry || cEntry.gen !== myGen) return;
438
452
  const wasStopped = cEntry.stopped;
439
453
  chatSessionMap.delete(chatId);
454
+ setChatAvatar(token, chatId, "idle").catch(() => {});
440
455
 
441
456
  const finalCardContent = state.accumulatedContent || " ";
442
457
  if (cardId) {
@@ -547,6 +562,14 @@ async function resolveModelEffort(
547
562
  }
548
563
  return { model, effort: null };
549
564
  }
565
+ if (tool === "codex") {
566
+ const m = process.env.CHATCCC_CODEX_MODEL?.trim();
567
+ const e = process.env.CHATCCC_CODEX_EFFORT?.trim();
568
+ return {
569
+ model: m && m !== "default" ? m : UNKNOWN_MODEL_PLACEHOLDER,
570
+ effort: e && e !== "default" ? e : UNKNOWN_MODEL_PLACEHOLDER,
571
+ };
572
+ }
550
573
  return {
551
574
  model: anthropicConfigDisplay(CLAUDE_MODEL),
552
575
  effort: anthropicConfigDisplay(CLAUDE_EFFORT),
@@ -618,4 +641,4 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
618
641
 
619
642
  export function _clearAdapterCacheForTest(): void {
620
643
  adapterCache.clear();
621
- }
644
+ }