chatccc 0.2.270 → 0.2.276

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 (42) hide show
  1. package/README.md +16 -10
  2. package/config.sample.json +4 -3
  3. package/deepccc-agent/README.md +147 -61
  4. package/deepccc-agent/package.json +5 -2
  5. package/dist/deepccc-agent/src/attachments.js +192 -0
  6. package/dist/deepccc-agent/src/cli.js +59 -13
  7. package/dist/deepccc-agent/src/config.js +57 -4
  8. package/dist/deepccc-agent/src/context.js +299 -16
  9. package/dist/deepccc-agent/src/file-tools.js +33 -0
  10. package/dist/deepccc-agent/src/index.js +68 -21
  11. package/dist/deepccc-agent/src/tool-protocol.js +14 -3
  12. package/dist/deepccc-agent/src/web-entry.js +72 -0
  13. package/dist/deepccc-agent/src/web-page.js +414 -0
  14. package/dist/deepccc-agent/src/web-runtime.js +331 -0
  15. package/dist/deepccc-agent/src/web-server.js +476 -0
  16. package/dist/deepccc-agent/src/web-session-store.js +162 -0
  17. package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
  18. package/dist/src/adapters/ccc-adapter.js +5 -1
  19. package/dist/src/agent-capability-grants.js +26 -0
  20. package/dist/src/agent-delegate-task.js +5 -2
  21. package/dist/src/agent-file-rpc.js +6 -1
  22. package/dist/src/agent-image-rpc.js +6 -1
  23. package/dist/src/agent-team/application/task-execution-service.js +330 -97
  24. package/dist/src/agent-team/domain/task-run.js +14 -1
  25. package/dist/src/agent-team/infrastructure/task-execution-runtime.js +7 -2
  26. package/dist/src/agent-team/main-agent-bootstrap.js +24 -1
  27. package/dist/src/agent-team/repositories/json-task-run-repository.js +22 -4
  28. package/dist/src/agent-team/web/agent-team-page.js +14 -7
  29. package/dist/src/cards.js +7 -4
  30. package/dist/src/config.js +12 -0
  31. package/dist/src/im-skills.js +9 -2
  32. package/dist/src/orchestrator.js +117 -29
  33. package/dist/src/safe-maintenance.js +4 -1
  34. package/dist/src/session-name.js +15 -0
  35. package/dist/src/session.js +54 -9
  36. package/dist/src/web-ui.js +76 -32
  37. package/im-skills/feishu-skill/receive-send-file.md +3 -2
  38. package/im-skills/feishu-skill/receive-send-image.md +3 -2
  39. package/im-skills/feishu-skill/send-file.mjs +6 -5
  40. package/im-skills/feishu-skill/send-image.mjs +6 -5
  41. package/im-skills/feishu-skill/skill.md +4 -2
  42. package/package.json +1 -1
@@ -2,9 +2,9 @@
2
2
  * DeepCCC terminal REPL and JSONL streaming entrypoint — 同步自 ChatCCC
3
3
  *
4
4
  * Usage:
5
- * node bin/deepccc.mjs
6
- * node bin/deepccc.mjs --model deepseek-v4-pro
7
- * node bin/deepccc.mjs --stream-json --prompt "hello"
5
+ * node bin/deepccc-cli.mjs
6
+ * node bin/deepccc-cli.mjs --model deepseek-v4-pro
7
+ * node bin/deepccc-cli.mjs --stream-json --prompt "hello"
8
8
  *
9
9
  * 交互模式(TTY)下,单轮回复渲染为固定"过程区块":状态行 + 折叠工具行 +
10
10
  * 原地更新正文,不再滚屏刷 JSON;完成/停止/异常后定型留在屏幕上。
@@ -24,6 +24,7 @@ import { reduceProgress } from "./progress/reducer.js";
24
24
  import { TerminalProgressRenderer } from "./progress/terminal-renderer.js";
25
25
  import { progressView } from "./progress/view.js";
26
26
  import { defaultLogDir, setupFileLogging } from "./file-log.js";
27
+ import { AttachmentStore, buildAttachmentPrompt } from "./attachments.js";
27
28
  function parsePositiveIntegerOption(name, value) {
28
29
  const parsed = Number(value);
29
30
  if (!Number.isInteger(parsed) || parsed <= 0) {
@@ -46,6 +47,7 @@ function parseArgs(argv = process.argv.slice(2)) {
46
47
  let streamJson = false;
47
48
  let prompt = null;
48
49
  let plain = false;
50
+ const images = [];
49
51
  for (let i = 0; i < argv.length; i++) {
50
52
  const arg = argv[i];
51
53
  const next = argv[i + 1];
@@ -65,6 +67,10 @@ function parseArgs(argv = process.argv.slice(2)) {
65
67
  config.effort = next;
66
68
  i++;
67
69
  }
70
+ else if (arg === "--max-output-tokens" && next !== undefined) {
71
+ config.maxOutputTokens = parsePositiveIntegerOption("--max-output-tokens", next);
72
+ i++;
73
+ }
68
74
  else if (arg === "--base-url" && next !== undefined) {
69
75
  config.baseURL = next;
70
76
  i++;
@@ -100,6 +106,10 @@ function parseArgs(argv = process.argv.slice(2)) {
100
106
  prompt = next;
101
107
  i++;
102
108
  }
109
+ else if (arg === "--image" && next !== undefined) {
110
+ images.push(next);
111
+ i++;
112
+ }
103
113
  else if (arg === "--plain") {
104
114
  plain = true;
105
115
  }
@@ -111,7 +121,7 @@ function parseArgs(argv = process.argv.slice(2)) {
111
121
  help = true;
112
122
  }
113
123
  }
114
- return { config, options, listSessions, resume, help, streamJson, prompt, plain };
124
+ return { config, options, listSessions, resume, help, streamJson, prompt, images, plain };
115
125
  }
116
126
  async function loadRuntime() {
117
127
  const [{ ChatSession }, { config: appConfig }] = await Promise.all([
@@ -124,13 +134,14 @@ function printHelp(appConfig) {
124
134
  console.log([
125
135
  "DeepCCC terminal agent",
126
136
  "",
127
- "Usage: deepccc [options]",
137
+ "Usage: deepccc-cli [options]",
128
138
  "",
129
139
  "Options:",
130
140
  ` --provider <name> API protocol: openai or anthropic (current default ${appConfig.provider})`,
131
141
  ` --model <name> Model name (current default ${appConfig.model})`,
132
142
  ` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
133
143
  ` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
144
+ ` --max-output-tokens <n> Maximum output tokens (unset = Provider default)`,
134
145
  ` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
135
146
  " --api-key <key> API key",
136
147
  " --cwd <path> Working directory",
@@ -139,6 +150,7 @@ function printHelp(appConfig) {
139
150
  " --list-sessions List saved sessions and exit",
140
151
  " --stream-json One-shot mode: write JSONL events to stdout",
141
152
  " --prompt <text> Prompt text for --stream-json",
153
+ " --image <path> Attach a PNG/JPEG/WebP file (repeatable; copied to the session attachment store)",
142
154
  " --plain Force plain streaming output (no progress block renderer)",
143
155
  " --dangerously-bypass-permissions Skip all permission prompts (aligns with chatccc's bypass mode)",
144
156
  " --help, -h Show help",
@@ -271,8 +283,8 @@ async function runStreamJson(args) {
271
283
  return 0;
272
284
  }
273
285
  const prompt = args.prompt ?? (!process.stdin.isTTY ? await readPromptFromStdin() : "");
274
- if (!prompt.trim()) {
275
- writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text> or stdin input" });
286
+ if (!prompt.trim() && !args.images.length) {
287
+ writeJsonLine({ type: "error", message: "--stream-json requires --prompt <text>, stdin input, or --image <path>" });
276
288
  return 1;
277
289
  }
278
290
  let runtime;
@@ -305,15 +317,29 @@ async function runStreamJson(args) {
305
317
  writeJsonLine({ type: "error", message: err.message });
306
318
  return 1;
307
319
  }
320
+ let attachments;
321
+ try {
322
+ attachments = await importCliAttachments(resolvedSession.sessionId, args.images);
323
+ }
324
+ catch (err) {
325
+ writeJsonLine({ type: "error", message: err.message });
326
+ return 1;
327
+ }
328
+ const promptWithAttachments = buildAttachmentPrompt(prompt, attachments);
308
329
  writeJsonLine({
309
330
  type: "start",
310
331
  session_id: resolvedSession.sessionId,
311
332
  mode: resolvedSession.mode,
312
333
  cwd,
313
334
  model: args.config.model ?? runtime.appConfig.model,
335
+ attachments: attachments.map((attachment) => ({
336
+ attachment_id: attachment.attachmentId,
337
+ name: attachment.originalName,
338
+ path: attachment.absolutePath,
339
+ })),
314
340
  });
315
341
  try {
316
- for await (const event of session.chat(prompt)) {
342
+ for await (const event of session.chat(promptWithAttachments)) {
317
343
  streamJsonEvent(event);
318
344
  }
319
345
  return 0;
@@ -420,13 +446,24 @@ async function runRepl(args) {
420
446
  console.error(`${C.yellow}${err.message}${C.reset}`);
421
447
  process.exit(1);
422
448
  }
449
+ let pendingAttachments;
450
+ try {
451
+ pendingAttachments = await importCliAttachments(resolvedSession.sessionId, args.images);
452
+ }
453
+ catch (err) {
454
+ console.error(`${C.yellow}${err.message}${C.reset}`);
455
+ process.exit(1);
456
+ }
457
+ if (pendingAttachments.length) {
458
+ console.log(`${C.dim}Attachments queued: ${pendingAttachments.map((attachment) => attachment.originalName).join(", ")}${C.reset}`);
459
+ }
423
460
  let currentAbort = null;
424
461
  const ctrlCState = createCtrlCState();
425
462
  rl.prompt();
426
463
  rl.on("line", async (line) => {
427
464
  ctrlCState.reset();
428
465
  const input = line.trim();
429
- if (!input) {
466
+ if (!input && !pendingAttachments.length) {
430
467
  rl.prompt();
431
468
  return;
432
469
  }
@@ -470,8 +507,10 @@ async function runRepl(args) {
470
507
  }
471
508
  let rendererEnded = false;
472
509
  try {
510
+ const chatInput = buildAttachmentPrompt(input, pendingAttachments);
511
+ pendingAttachments = [];
473
512
  let lastAccumulated = "";
474
- for await (const event of session.chat(input, signal)) {
513
+ for await (const event of session.chat(chatInput, signal)) {
475
514
  if (renderer && view) {
476
515
  view = reduceProgress(view, event);
477
516
  if (event.type === "text" || event.type === "compact" || event.type === "status") {
@@ -559,8 +598,15 @@ async function runRepl(args) {
559
598
  process.exit(0);
560
599
  });
561
600
  }
601
+ async function importCliAttachments(sessionId, paths) {
602
+ const store = new AttachmentStore();
603
+ const attachments = [];
604
+ for (const path of paths)
605
+ attachments.push(await store.importFile(sessionId, path));
606
+ return attachments;
607
+ }
562
608
  /**
563
- * skill create 子命令:deepccc skill create <name> [--scope global|project] [--description "..."]
609
+ * skill create 子命令:deepccc-cli skill create <name> [--scope global|project] [--description "..."]
564
610
  * 默认创建为全局技能(~/.deepccc/skills/<name>/SKILL.md,Codex 结构);
565
611
  * --scope project 创建为项目技能(<cwd>/.deepccc/skills/<name>/SKILL.md)。
566
612
  * 新技能在下一次对话自动生效(技能索引每次 chat() 前重扫)。
@@ -569,7 +615,7 @@ function runSkillCreate(argv) {
569
615
  const positional = argv.filter((a) => !a.startsWith("--"));
570
616
  const name = positional[0];
571
617
  if (!name) {
572
- console.error("usage: deepccc skill create <name> [--scope global|project] [--description \"...\"]");
618
+ console.error("usage: deepccc-cli skill create <name> [--scope global|project] [--description \"...\"]");
573
619
  process.exit(1);
574
620
  }
575
621
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
@@ -603,7 +649,7 @@ function runSkillCreate(argv) {
603
649
  console.log("hot reload: 下一次对话自动生效,无需重启");
604
650
  }
605
651
  async function main() {
606
- // skill create 子命令:deepccc skill create <name> [--scope global|project] [--description "..."]
652
+ // skill create 子命令:deepccc-cli skill create <name> [--scope global|project] [--description "..."]
607
653
  // 默认创建在全局 ~/.deepccc/skills(Codex 目录结构),--scope project 创建到 <cwd>/.deepccc/skills。
608
654
  if (process.argv[2] === "skill" && process.argv[3] === "create") {
609
655
  runSkillCreate(process.argv.slice(4));
@@ -1,9 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  export const DEEPCCC_HOME = join(homedir(), ".deepccc");
5
5
  export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
6
- const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
6
+ export const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
7
7
  /**
8
8
  * 默认配置(不读环境/文件)。导出供测试断言默认值;运行时请用 loadConfig 结果。
9
9
  */
@@ -31,8 +31,12 @@ export const DEFAULT_CONFIG = {
31
31
  retentionDays: 7,
32
32
  keepCompleted: false,
33
33
  },
34
+ web: {
35
+ port: 28_080,
36
+ openOnStart: true,
37
+ },
34
38
  };
35
- function readConfigFile() {
39
+ export function readConfigFile() {
36
40
  if (!existsSync(CONFIG_PATH))
37
41
  return {};
38
42
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
@@ -56,6 +60,12 @@ function numberEnv(name) {
56
60
  const value = Number(env(name));
57
61
  return Number.isFinite(value) && value >= 0 ? value : undefined;
58
62
  }
63
+ function optionalPositiveInteger(value) {
64
+ if (value === undefined || value === null || value === "")
65
+ return undefined;
66
+ const parsed = Number(value);
67
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
68
+ }
59
69
  export function normalizeDeepCccProvider(value) {
60
70
  if (value === undefined || value === null || String(value).trim() === "")
61
71
  return "openai";
@@ -64,13 +74,14 @@ export function normalizeDeepCccProvider(value) {
64
74
  return normalized;
65
75
  throw new Error(`DEEPCCC_PROVIDER/provider must be "openai" or "anthropic", received: ${String(value)}`);
66
76
  }
67
- function loadConfig() {
77
+ export function loadConfig() {
68
78
  const file = readConfigFile();
69
79
  const rawLogs = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
70
80
  ? file.rawStreamLogs
71
81
  : {};
72
82
  const git = file.git && typeof file.git === "object" ? file.git : {};
73
83
  const coAuthor = git.coAuthor && typeof git.coAuthor === "object" ? git.coAuthor : {};
84
+ const web = file.web && typeof file.web === "object" ? file.web : {};
74
85
  return {
75
86
  provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
76
87
  apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
@@ -78,6 +89,8 @@ function loadConfig() {
78
89
  model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
79
90
  subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
80
91
  effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
92
+ maxOutputTokens: optionalPositiveInteger(env("DEEPCCC_MAX_OUTPUT_TOKENS"))
93
+ ?? optionalPositiveInteger(file.maxOutputTokens),
81
94
  streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
82
95
  contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
83
96
  git: {
@@ -93,9 +106,49 @@ function loadConfig() {
93
106
  retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
94
107
  keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
95
108
  },
109
+ web: {
110
+ port: normalizeWebPort(web.port),
111
+ openOnStart: web.openOnStart ?? DEFAULT_CONFIG.web.openOnStart,
112
+ },
96
113
  };
97
114
  }
98
115
  export function ensureConfigDir() {
99
116
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
100
117
  }
118
+ /** Persist only user-editable Web fields while preserving advanced configuration. */
119
+ export function saveConfigPatch(patch) {
120
+ const existing = readConfigFile();
121
+ const { web: webPatch, ...flatPatch } = patch;
122
+ const next = {
123
+ ...existing,
124
+ ...flatPatch,
125
+ ...(webPatch ? { web: { ...DEFAULT_CONFIG.web, ...(existing.web ?? {}), ...webPatch } } : {}),
126
+ };
127
+ if (patch.provider !== undefined)
128
+ next.provider = normalizeDeepCccProvider(patch.provider);
129
+ if (patch.baseURL !== undefined && !patch.baseURL.trim())
130
+ throw new Error("baseURL must not be empty");
131
+ if (patch.model !== undefined && !patch.model.trim())
132
+ throw new Error("model must not be empty");
133
+ if (patch.effort !== undefined && !["", "none", "minimal", "low", "medium", "high", "xhigh", "max"].includes(patch.effort)) {
134
+ throw new Error(`Unsupported effort: ${patch.effort}`);
135
+ }
136
+ if (patch.contextWindow !== undefined && (!Number.isInteger(patch.contextWindow) || patch.contextWindow <= 0)) {
137
+ throw new Error("contextWindow must be a positive integer");
138
+ }
139
+ if (webPatch?.port !== undefined) {
140
+ next.web = { ...(next.web ?? DEFAULT_CONFIG.web), port: normalizeWebPort(webPatch.port) };
141
+ }
142
+ ensureConfigDir();
143
+ const tempPath = `${CONFIG_PATH}.${process.pid}.tmp`;
144
+ writeFileSync(tempPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
145
+ renameSync(tempPath, CONFIG_PATH);
146
+ return loadConfig();
147
+ }
148
+ function normalizeWebPort(value) {
149
+ const parsed = Number(value ?? DEFAULT_CONFIG.web.port);
150
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535)
151
+ return DEFAULT_CONFIG.web.port;
152
+ return parsed;
153
+ }
101
154
  export const config = loadConfig();