chatccc 0.2.196 → 0.2.198

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.
Files changed (53) hide show
  1. package/agent-prompts/cursor_specific.md +13 -13
  2. package/bin/cccagent.mjs +17 -17
  3. package/config.sample.json +27 -27
  4. package/package.json +2 -1
  5. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -0
  6. package/src/__tests__/builtin-chat-session.test.ts +277 -181
  7. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  8. package/src/__tests__/builtin-config.test.ts +33 -33
  9. package/src/__tests__/builtin-context.test.ts +163 -163
  10. package/src/__tests__/builtin-file-tools.test.ts +224 -196
  11. package/src/__tests__/builtin-session-select.test.ts +116 -116
  12. package/src/__tests__/builtin-sigint.test.ts +56 -56
  13. package/src/__tests__/cards.test.ts +109 -109
  14. package/src/__tests__/ccc-adapter.test.ts +114 -113
  15. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  16. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  17. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  18. package/src/__tests__/claude-raw-stream-log.test.ts +87 -0
  19. package/src/__tests__/codex-raw-stream-log.test.ts +163 -0
  20. package/src/__tests__/config-reload.test.ts +10 -10
  21. package/src/__tests__/config-sample.test.ts +18 -18
  22. package/src/__tests__/cursor-adapter.test.ts +167 -113
  23. package/src/__tests__/feishu-avatar.test.ts +40 -40
  24. package/src/__tests__/jsonl-stream.test.ts +79 -0
  25. package/src/__tests__/orchestrator.test.ts +181 -154
  26. package/src/__tests__/raw-stream-log.test.ts +106 -106
  27. package/src/__tests__/session.test.ts +40 -40
  28. package/src/__tests__/sim-platform.test.ts +12 -12
  29. package/src/__tests__/web-ui.test.ts +209 -209
  30. package/src/adapters/ccc-adapter.ts +121 -119
  31. package/src/adapters/claude-adapter.ts +603 -566
  32. package/src/adapters/codex-adapter.ts +65 -52
  33. package/src/adapters/cursor-adapter.ts +269 -276
  34. package/src/adapters/jsonl-stream.ts +157 -0
  35. package/src/adapters/raw-stream-log.ts +124 -124
  36. package/src/agent-reload-config-rpc.ts +34 -0
  37. package/src/builtin/cli.ts +473 -461
  38. package/src/builtin/context.ts +323 -323
  39. package/src/builtin/file-tools.ts +1072 -915
  40. package/src/builtin/index.ts +404 -353
  41. package/src/builtin/session-select.ts +48 -48
  42. package/src/builtin/sigint.ts +50 -50
  43. package/src/cards.ts +195 -195
  44. package/src/chatgpt-subscription-rpc.ts +27 -27
  45. package/src/chatgpt-subscription.ts +299 -299
  46. package/src/chrome-devtools-guard.ts +318 -318
  47. package/src/config.ts +125 -125
  48. package/src/feishu-api.ts +49 -49
  49. package/src/index.ts +8 -13
  50. package/src/orchestrator.ts +166 -145
  51. package/src/runtime-reload.ts +34 -0
  52. package/src/session.ts +141 -141
  53. package/src/web-ui.ts +205 -205
package/src/session.ts CHANGED
@@ -2,9 +2,9 @@ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import {
5
- CLAUDE_API_KEY,
6
- CLAUDE_BASE_URL,
7
- CLAUDE_MAX_TURN,
5
+ CLAUDE_API_KEY,
6
+ CLAUDE_BASE_URL,
7
+ CLAUDE_MAX_TURN,
8
8
  CLAUDE_MODEL,
9
9
  CLAUDE_SUBAGENT_MODEL,
10
10
  CHATCCC_PORT,
@@ -14,23 +14,23 @@ import {
14
14
  addRecentDir,
15
15
  anthropicConfigDisplay,
16
16
  config,
17
- fileLog,
18
- getDefaultCwd,
19
- getDefaultEffortForTool,
20
- isAnthropicConfigEmpty,
21
- toolDisplayName,
22
- ts,
23
- } from "./config.ts";
17
+ fileLog,
18
+ getDefaultCwd,
19
+ getDefaultEffortForTool,
20
+ isAnthropicConfigEmpty,
21
+ toolDisplayName,
22
+ ts,
23
+ } from "./config.ts";
24
24
  import { buildProgressCard, getToolEmoji, isCodeBlockOpen, truncateContent } from "./cards.ts";
25
25
  import { simplifyToolUse, simplifyToolResult } from "./simplify.ts";
26
26
  import { logTrace } from "./trace.ts";
27
27
  import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
28
28
  import type { ToolAdapter } from "./adapters/adapter-interface.ts";
29
29
  import type { ToolProcessInfo } from "./adapters/adapter-interface.ts";
30
- import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
31
- import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
32
- import { createCodexAdapter } from "./adapters/codex-adapter.ts";
33
- import { createCccAdapter } from "./adapters/ccc-adapter.ts";
30
+ import { createClaudeAdapter } from "./adapters/claude-adapter.ts";
31
+ import { createCursorAdapter } from "./adapters/cursor-adapter.ts";
32
+ import { createCodexAdapter } from "./adapters/codex-adapter.ts";
33
+ import { createCccAdapter } from "./adapters/ccc-adapter.ts";
34
34
  import { resourceMonitor, registerProcess, unregisterProcess } from "./adapters/resource-monitor.ts";
35
35
  import { buildImSkillsPromptCached, exportSkillSubDocs, clearImSkillsPromptCache } from "./im-skills.ts";
36
36
  import type { PlatformAdapter } from "./platform-adapter.ts";
@@ -334,14 +334,14 @@ export function resetState(): void {
334
334
  for (const prompt of activePrompts.values()) {
335
335
  if (prompt.processMonitor) clearInterval(prompt.processMonitor);
336
336
  }
337
- activePrompts.clear();
338
- displayCards.clear();
339
- sessionModelOverrides.clear();
340
- sessionEffortOverrides.clear();
341
- adapterCache.clear();
342
- stopUnifiedDisplayLoop();
343
- console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
344
- }
337
+ activePrompts.clear();
338
+ displayCards.clear();
339
+ sessionModelOverrides.clear();
340
+ sessionEffortOverrides.clear();
341
+ adapterCache.clear();
342
+ stopUnifiedDisplayLoop();
343
+ console.log(`[${ts()}] [RESET] State cleared (dedup + active sessions + bindings)`);
344
+ }
345
345
 
346
346
  // 注:`rebuildBindingsFromRegistry` 定义在下方与 loadSessionRegistry 同区域,
347
347
  // 是 onReady/onReconnected 取代 resetState 的正确入口。
@@ -353,8 +353,8 @@ export function resetState(): void {
353
353
  const adapterCache = new Map<string, ToolAdapter>();
354
354
 
355
355
  // Per-session 模型覆盖(/model 命令设置,不持久化)
356
- const sessionModelOverrides = new Map<string, string>();
357
- const sessionEffortOverrides = new Map<string, string>();
356
+ const sessionModelOverrides = new Map<string, string>();
357
+ const sessionEffortOverrides = new Map<string, string>();
358
358
 
359
359
  /** 返回 session 的生效模型:优先 per-session 覆盖,其次全局配置(Claude) */
360
360
  function getModelForSession(sessionId?: string): string {
@@ -366,27 +366,27 @@ function getModelForSession(sessionId?: string): string {
366
366
  }
367
367
 
368
368
  /** 返回指定 tool 的生效模型:优先 per-session 覆盖,其次 tool 默认配置 */
369
- export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
370
- if (sessionId) {
371
- const override = sessionModelOverrides.get(sessionId);
372
- if (override) return override;
373
- }
374
- if (tool === "cursor") return config.cursor.model;
375
- if (tool === "codex") return config.codex.model;
376
- if (tool === "ccc") return config.ccc.model;
377
- return CLAUDE_MODEL;
378
- }
379
-
380
- export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
381
- if (sessionId) {
382
- const override = sessionEffortOverrides.get(sessionId);
383
- if (override) return override;
384
- }
385
- if (tool === "claude" || tool === "codex") {
386
- return getDefaultEffortForTool(tool);
387
- }
388
- return "";
389
- }
369
+ export function getEffectiveModelForTool(tool: string, sessionId?: string): string {
370
+ if (sessionId) {
371
+ const override = sessionModelOverrides.get(sessionId);
372
+ if (override) return override;
373
+ }
374
+ if (tool === "cursor") return config.cursor.model;
375
+ if (tool === "codex") return config.codex.model;
376
+ if (tool === "ccc") return config.ccc.model;
377
+ return CLAUDE_MODEL;
378
+ }
379
+
380
+ export function getEffectiveEffortForTool(tool: string, sessionId?: string): string {
381
+ if (sessionId) {
382
+ const override = sessionEffortOverrides.get(sessionId);
383
+ if (override) return override;
384
+ }
385
+ if (tool === "claude" || tool === "codex") {
386
+ return getDefaultEffortForTool(tool);
387
+ }
388
+ return "";
389
+ }
390
390
 
391
391
  /** 为指定 session 设置模型覆盖(/model <name>) */
392
392
  export function setSessionModelOverride(sessionId: string, model: string): void {
@@ -395,43 +395,43 @@ export function setSessionModelOverride(sessionId: string, model: string): void
395
395
  }
396
396
 
397
397
  /** 清除指定 session 的模型覆盖(/model clear) */
398
- export function clearSessionModelOverride(sessionId: string): void {
399
- sessionModelOverrides.delete(sessionId);
400
- adapterCache.clear();
401
- }
402
-
403
- export function setSessionEffortOverride(sessionId: string, effort: string): void {
404
- sessionEffortOverrides.set(sessionId, effort);
405
- adapterCache.clear();
406
- }
407
-
408
- export function clearSessionEffortOverride(sessionId: string): void {
409
- sessionEffortOverrides.delete(sessionId);
410
- adapterCache.clear();
411
- }
412
-
413
- export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
414
- const effectiveModel = getEffectiveModelForTool(tool, sessionId);
415
- const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
416
- const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
417
- const cached = adapterCache.get(cacheKey);
418
- if (cached) return cached;
419
-
420
- let adapter: ToolAdapter;
421
- if (tool === "cursor") {
422
- adapter = createCursorAdapter({ model: effectiveModel || undefined });
423
- } else if (tool === "codex") {
424
- adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
425
- } else if (tool === "ccc") {
426
- adapter = createCccAdapter({ model: effectiveModel || undefined });
427
- } else {
428
- adapter = createClaudeAdapter({
429
- model: effectiveModel,
430
- subagentModel: CLAUDE_SUBAGENT_MODEL,
431
- effort: effectiveEffort,
432
- apiKey: CLAUDE_API_KEY,
433
- baseUrl: CLAUDE_BASE_URL,
434
- isEmpty: isAnthropicConfigEmpty,
398
+ export function clearSessionModelOverride(sessionId: string): void {
399
+ sessionModelOverrides.delete(sessionId);
400
+ adapterCache.clear();
401
+ }
402
+
403
+ export function setSessionEffortOverride(sessionId: string, effort: string): void {
404
+ sessionEffortOverrides.set(sessionId, effort);
405
+ adapterCache.clear();
406
+ }
407
+
408
+ export function clearSessionEffortOverride(sessionId: string): void {
409
+ sessionEffortOverrides.delete(sessionId);
410
+ adapterCache.clear();
411
+ }
412
+
413
+ export function getAdapterForTool(tool: string, sessionId?: string): ToolAdapter {
414
+ const effectiveModel = getEffectiveModelForTool(tool, sessionId);
415
+ const effectiveEffort = getEffectiveEffortForTool(tool, sessionId);
416
+ const cacheKey = `${tool}:${effectiveModel || ""}:${effectiveEffort || ""}`;
417
+ const cached = adapterCache.get(cacheKey);
418
+ if (cached) return cached;
419
+
420
+ let adapter: ToolAdapter;
421
+ if (tool === "cursor") {
422
+ adapter = createCursorAdapter({ model: effectiveModel || undefined });
423
+ } else if (tool === "codex") {
424
+ adapter = createCodexAdapter({ model: effectiveModel || undefined, effort: effectiveEffort || undefined });
425
+ } else if (tool === "ccc") {
426
+ adapter = createCccAdapter({ model: effectiveModel || undefined });
427
+ } else {
428
+ adapter = createClaudeAdapter({
429
+ model: effectiveModel,
430
+ subagentModel: CLAUDE_SUBAGENT_MODEL,
431
+ effort: effectiveEffort,
432
+ apiKey: CLAUDE_API_KEY,
433
+ baseUrl: CLAUDE_BASE_URL,
434
+ isEmpty: isAnthropicConfigEmpty,
435
435
  maxTurn: CLAUDE_MAX_TURN,
436
436
  });
437
437
  }
@@ -859,23 +859,23 @@ export async function switchChatBinding(args: SwitchChatBindingArgs): Promise<Sw
859
859
  function formatToolConfigForLog(tool: string, sessionModel?: string, sessionId?: string): string {
860
860
  if (tool === "cursor") {
861
861
  return `model=${sessionModel ?? "(由 cursor-agent 决定,init 事件后学习)"}`;
862
- }
863
- if (tool === "codex") {
864
- const m = getEffectiveModelForTool(tool, sessionId);
865
- const e = getEffectiveEffortForTool(tool, sessionId);
866
- const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
867
- const effortStr = e.trim() !== ""
868
- ? `effort=${e}`
869
- : "effort=(由 codex config.toml 决定)";
870
- return `model=${modelStr}, ${effortStr}`;
871
- }
872
- if (tool === "ccc") {
873
- const m = getEffectiveModelForTool(tool, sessionId);
874
- const modelStr = m.trim() !== "" ? m : "(not configured)";
875
- return `model=${modelStr}, baseURL=${config.ccc.DEEPSEEK_BASE_URL}`;
876
- }
877
- return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
878
- }
862
+ }
863
+ if (tool === "codex") {
864
+ const m = getEffectiveModelForTool(tool, sessionId);
865
+ const e = getEffectiveEffortForTool(tool, sessionId);
866
+ const modelStr = m.trim() !== "" ? m : "(由 codex config.toml 决定)";
867
+ const effortStr = e.trim() !== ""
868
+ ? `effort=${e}`
869
+ : "effort=(由 codex config.toml 决定)";
870
+ return `model=${modelStr}, ${effortStr}`;
871
+ }
872
+ if (tool === "ccc") {
873
+ const m = getEffectiveModelForTool(tool, sessionId);
874
+ const modelStr = m.trim() !== "" ? m : "(not configured)";
875
+ return `model=${modelStr}, baseURL=${config.ccc.DEEPSEEK_BASE_URL}`;
876
+ }
877
+ return `model=${anthropicConfigDisplay(getModelForSession(sessionId))}, subagentModel=${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}, effort=${anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId))}`;
878
+ }
879
879
 
880
880
  export async function initClaudeSession(tool: string, overrideCwd?: string, chatId?: string): Promise<{ sessionId: string; cwd: string }> {
881
881
  const cwd = overrideCwd ?? (await getDefaultCwd(chatId));
@@ -990,12 +990,12 @@ export async function runAgentSession(
990
990
  const imSkillsCacheDir = join(USER_DATA_DIR, "im-skills");
991
991
  const skillVariables = {
992
992
  cwd,
993
- session_id: sessionId,
994
- im_skills_cache_dir: imSkillsCacheDir,
995
- delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
996
- send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
997
- send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
998
- send_image_script: join(feishuSkillDir, "send-image.mjs"),
993
+ session_id: sessionId,
994
+ im_skills_cache_dir: imSkillsCacheDir,
995
+ delegate_task_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/delegate-task`,
996
+ send_image_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-image`,
997
+ send_file_url: `http://127.0.0.1:${CHATCCC_PORT}/api/agent/send-file`,
998
+ send_image_script: join(feishuSkillDir, "send-image.mjs"),
999
999
  send_file_script: join(feishuSkillDir, "send-file.mjs"),
1000
1000
  download_video_script: join(feishuSkillDir, "download-video.mjs"),
1001
1001
  wechat_send_image_script: join(wechatImageSkillDir, "send-image.mjs"),
@@ -1168,12 +1168,12 @@ export async function runAgentSession(
1168
1168
  chunkCount: 0,
1169
1169
  };
1170
1170
 
1171
- let lastFileWrite = Date.now();
1172
- const FILE_WRITE_INTERVAL_MS = 2000;
1173
- const toolCallMap = new Map<string, { name: string; input: unknown }>();
1174
- let streamErrored = false;
1175
-
1176
- try {
1171
+ let lastFileWrite = Date.now();
1172
+ const FILE_WRITE_INTERVAL_MS = 2000;
1173
+ const toolCallMap = new Map<string, { name: string; input: unknown }>();
1174
+ let streamErrored = false;
1175
+
1176
+ try {
1177
1177
  for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
1178
1178
  onProcessStart: (processInfo) => {
1179
1179
  startPromptProcessMonitor(sessionId, processInfo);
@@ -1223,11 +1223,11 @@ export async function runAgentSession(
1223
1223
  tool,
1224
1224
  });
1225
1225
  }
1226
- }
1227
- } catch (streamErr) {
1228
- streamErrored = true;
1229
- console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1230
- } finally {
1226
+ }
1227
+ } catch (streamErr) {
1228
+ streamErrored = true;
1229
+ console.error(`[${ts()}] [STREAM] Error in stream loop for ${sessionId}: ${(streamErr as Error).message}`);
1230
+ } finally {
1231
1231
  // 标记 prompt 结束
1232
1232
  resourceMonitor.off("stuck", onResourceStuck);
1233
1233
  const prompt = activePrompts.get(sessionId);
@@ -1241,7 +1241,7 @@ export async function runAgentSession(
1241
1241
  // 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
1242
1242
  // writeFile I/O(POLL 阶段)执行,display loop 会误以为旧轮仍在
1243
1243
  // 运行中并更新旧卡片,而不是新建卡片。
1244
- const finalStatus = (streamErrored || wasAbnormalExit || wasResourceStuck) ? "error" : wasStopped ? "stopped" : "done";
1244
+ const finalStatus = (streamErrored || wasAbnormalExit || wasResourceStuck) ? "error" : wasStopped ? "stopped" : "done";
1245
1245
  const finalReply = pickFinalReply(state).trim();
1246
1246
 
1247
1247
  // stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
@@ -1791,27 +1791,27 @@ async function resolveModelEffort(
1791
1791
  // adapter 异常时降级为占位符(不阻塞 /state 卡片)
1792
1792
  }
1793
1793
  return { model, effort: null };
1794
- }
1795
- if (tool === "codex") {
1796
- const m = getEffectiveModelForTool(tool, sessionId);
1797
- const e = getEffectiveEffortForTool(tool, sessionId);
1798
- return {
1799
- model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
1800
- effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
1801
- };
1802
- }
1803
- if (tool === "ccc") {
1804
- const m = getEffectiveModelForTool(tool, sessionId);
1805
- return {
1806
- model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
1807
- effort: null,
1808
- };
1809
- }
1810
- return {
1811
- model: anthropicConfigDisplay(getModelForSession(sessionId)),
1812
- effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
1813
- };
1814
- }
1794
+ }
1795
+ if (tool === "codex") {
1796
+ const m = getEffectiveModelForTool(tool, sessionId);
1797
+ const e = getEffectiveEffortForTool(tool, sessionId);
1798
+ return {
1799
+ model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
1800
+ effort: e.trim() !== "" ? e : UNKNOWN_MODEL_PLACEHOLDER,
1801
+ };
1802
+ }
1803
+ if (tool === "ccc") {
1804
+ const m = getEffectiveModelForTool(tool, sessionId);
1805
+ return {
1806
+ model: m.trim() !== "" ? m : UNKNOWN_MODEL_PLACEHOLDER,
1807
+ effort: null,
1808
+ };
1809
+ }
1810
+ return {
1811
+ model: anthropicConfigDisplay(getModelForSession(sessionId)),
1812
+ effort: anthropicConfigDisplay(getEffectiveEffortForTool(tool, sessionId)),
1813
+ };
1814
+ }
1815
1815
 
1816
1816
  export async function getSessionStatus(chatId: string): Promise<SessionStatus | null> {
1817
1817
  const info = sessionInfoMap.get(chatId);
@@ -1917,9 +1917,9 @@ export function _setAdapterForToolForTest(tool: string, adapter: ToolAdapter): v
1917
1917
  adapterCache.set(tool, adapter);
1918
1918
  // 同时设置当前配置模型对应的 key(getAdapterForTool 会优先 lookup 含 model 的 key)
1919
1919
  const effective = getEffectiveModelForTool(tool);
1920
- const effort = getEffectiveEffortForTool(tool);
1921
- adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
1922
- if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
1920
+ const effort = getEffectiveEffortForTool(tool);
1921
+ adapterCache.set(`${tool}:${effective || ""}:${effort || ""}`, adapter);
1922
+ if (effective) adapterCache.set(`${tool}:${effective}`, adapter);
1923
1923
  }
1924
1924
 
1925
1925
  export function clearAdapterCache(): void {