deepccc 0.1.27 → 0.2.0

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `deepccc` 是一个轻量级本地编程 Agent,针对 DeepSeek 使用体验做了优化,同时支持其他 OpenAI-compatible 模型接口。
4
4
 
5
- 它提供交互式命令行、JSONL 流式输出、本地文件工具、命令执行、项目提示词自动注入和持久化上下文。
5
+ 它提供本地 Web UI、交互式命令行、JSONL 流式输出、本地文件工具、命令执行、项目提示词自动注入和持久化上下文。
6
6
 
7
7
  ## 当前状态
8
8
 
@@ -46,8 +46,10 @@ deepccc 的本地缓存优化实测命中率 **96.7%**,有效降低重复请
46
46
  ```bash
47
47
  export DEEPCCC_API_KEY="sk-..."
48
48
  export DEEPCCC_BASE_URL="https://api.deepseek.com/v1"
49
- export DEEPCCC_MODEL="deepseek-v4-pro"
50
- export DEEPCCC_STREAMING="true"
49
+ export DEEPCCC_MODEL="deepseek-v4-pro"
50
+ export DEEPCCC_EFFORT="high"
51
+ export DEEPCCC_MAX_OUTPUT_TOKENS="32768"
52
+ export DEEPCCC_STREAMING="true"
51
53
  ```
52
54
 
53
55
  Windows PowerShell:
@@ -56,8 +58,10 @@ Windows PowerShell:
56
58
  $env:DEEPCCC_API_KEY="sk-..."
57
59
  $env:DEEPCCC_PROVIDER="openai"
58
60
  $env:DEEPCCC_BASE_URL="https://api.deepseek.com/v1"
59
- $env:DEEPCCC_MODEL="deepseek-v4-pro"
60
- $env:DEEPCCC_STREAMING="true"
61
+ $env:DEEPCCC_MODEL="deepseek-v4-pro"
62
+ $env:DEEPCCC_EFFORT="high"
63
+ $env:DEEPCCC_MAX_OUTPUT_TOKENS="32768"
64
+ $env:DEEPCCC_STREAMING="true"
61
65
  ```
62
66
 
63
67
  也兼容这些 DeepSeek 别名:
@@ -75,27 +79,64 @@ $env:DEEPCCC_STREAMING="true"
75
79
  "apiKey": "sk-...",
76
80
  "baseURL": "https://api.deepseek.com/v1",
77
81
  "model": "deepseek-v4-pro",
78
- "subModel": "",
79
- "effort": "",
80
- "streaming": true,
82
+ "subModel": "",
83
+ "effort": "",
84
+ "maxOutputTokens": null,
85
+ "streaming": true,
81
86
  "contextWindow": 1048576,
82
- "rawStreamLogs": {
87
+ "git": {
88
+ "coAuthor": {
89
+ "enabled": true,
90
+ "name": "DeepCCC",
91
+ "email": "20184052+wzj998@users.noreply.github.com"
92
+ }
93
+ },
94
+ "rawStreamLogs": {
83
95
  "enabled": true,
84
96
  "maxBytesPerTurn": 1048576,
85
97
  "retentionDays": 7,
86
- "keepCompleted": false
87
- }
88
- }
89
- ```
98
+ "keepCompleted": false
99
+ },
100
+ "web": {
101
+ "port": 28080,
102
+ "openOnStart": true
103
+ }
104
+ }
105
+ ```
106
+
107
+ ## Web UI
108
+
109
+ 全局安装后可以直接启动本地网页版:
110
+
111
+ ```bash
112
+ deepccc web
113
+ ```
114
+
115
+ 默认只监听 `http://127.0.0.1:28080/`,不会暴露到局域网。可在
116
+ `~/.deepccc/config.json` 的 `web.port` 修改端口,`web.openOnStart` 控制启动时是否自动打开浏览器;也可以临时使用 `deepccc web --port 28081 --no-open`。
117
+
118
+ Web UI 支持新建、恢复、重命名和删除多会话,多个会话可以同时运行,即使它们指向同一个工作目录。每个会话可独立选择 model、subModel 和 effort,并持续复用 CLI 已保存的历史。注意:当前版本不自动创建 Git worktree;同目录的多个运行中 Agent 直接修改同一组文件,页面会提示覆盖与冲突风险。
119
+
120
+ API 设置采用单一 Provider 配置,支持 OpenAI-compatible 与 Anthropic Messages。完整 API Key 只保存在本机 `~/.deepccc/config.json`,浏览器读取设置时仅返回掩码。高风险命令会在网页中暂停并请求“拒绝、允许一次、本会话允许、永久允许”,浏览器断开或审批超时默认拒绝。
121
+
122
+ `git.coAuthor.enabled` 默认开启。DeepCCC 通过 `run_command` 创建 Git 提交时会保留用户为
123
+ 主 Author,并追加 `Co-authored-by: DeepCCC <20184052+wzj998@users.noreply.github.com>`。
124
+ 可设为 `false` 或用 `DEEPCCC_GIT_COAUTHOR=false` 全局关闭。ChatCCC 的
125
+ `ccc.gitCoAuthor` 是三态 override:`null`/缺失跟随这里,`true` 强制开启,`false` 强制关闭。
90
126
 
91
127
  `provider` 可选 `openai` 或 `anthropic`,默认 `openai`,也可以通过
92
128
  `DEEPCCC_PROVIDER` 或命令行 `--provider` 覆盖:
93
129
 
94
130
  - `openai` 使用 OpenAI-compatible Chat Completions 协议,兼容 DeepSeek、OpenAI、LiteLLM、vLLM 等服务。
95
- - `anthropic` 使用 Anthropic Messages 协议。配置中的 `baseURL` **完全按填写值使用,不自动补 `/v1`**,请填写到完整版本化地址,例如 DeepSeek 官方 Anthropic 端点为 `https://api.deepseek.com/anthropic/v1`(官方 Anthropic SDK 使用的 `https://api.deepseek.com/anthropic` 基址会拼接为 `.../anthropic/v1/messages`)。`effort` 是 OpenAI/DeepSeek 扩展参数,在 Anthropic 模式下不会发送。
131
+ - `anthropic` 使用 Anthropic Messages 协议。配置中的 `baseURL` **完全按填写值使用,不自动补 `/v1`**,请填写到完整版本化地址,例如 DeepSeek 官方 Anthropic 端点为 `https://api.deepseek.com/anthropic/v1`(官方 Anthropic SDK 使用的 `https://api.deepseek.com/anthropic` 基址会拼接为 `.../anthropic/v1/messages`)。`effort` 在 OpenAI-compatible 模式映射为 `reasoning_effort`,在 Anthropic 模式映射为 `output_config.effort`;目标服务不支持时应留空。
96
132
 
97
- `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
98
- `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
133
+ `streaming` 控制主对话是否使用流式请求,默认 `true`;也可以通过
134
+ `DEEPCCC_STREAMING=true|false` 覆盖。关闭后,终端会在整条模型响应完成后一次性显示结果。
135
+
136
+ `maxOutputTokens` 限制主对话单次最大输出 token,默认不配置(`null`/缺失),此时不向
137
+ Provider 发送 `max_tokens`,使用模型服务端默认值。可通过 `DEEPCCC_MAX_OUTPUT_TOKENS`
138
+ 或命令行 `--max-output-tokens` 覆盖;只接受正整数。该限制会同时覆盖模型思考内容、
139
+ 工具参数和最终回答,设置过小可能导致工具调用或长回复被截断。
99
140
 
100
141
  `contextWindow` 是模型上下文窗口(token),默认 `1048576`(1M,DeepSeek V4 Pro/Flash
101
142
  原生规格);上下文压缩阈值自动 = `contextWindow × 0.8`(超出即把较早消息压缩为摘要)。
@@ -166,7 +207,15 @@ deepccc --max-steps 20
166
207
  deepccc --effort high
167
208
  ```
168
209
 
169
- 可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
210
+ 可选值:`none` / `minimal` / `low` / `medium` / `high` / `xhigh` / `max`(留空则不传 `reasoning_effort` 请求字段)。
211
+
212
+ 限制主对话最大输出 token:
213
+
214
+ ```bash
215
+ deepccc --max-output-tokens 8192
216
+ ```
217
+
218
+ 不设置时使用 Provider 默认值。
170
219
 
171
220
  ## 权限机制
172
221
 
package/dist/cli.d.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * node bin/deepccc.mjs
6
6
  * node bin/deepccc.mjs --model deepseek-v4-pro
7
7
  * node bin/deepccc.mjs --stream-json --prompt "hello"
8
+ * node bin/deepccc.mjs web
8
9
  *
9
10
  * 交互模式(TTY)下,单轮回复渲染为固定"过程区块":状态行 + 折叠工具行 +
10
11
  * 原地更新正文,不再滚屏刷 JSON;完成/停止/异常后定型留在屏幕上。
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * node bin/deepccc.mjs
6
6
  * node bin/deepccc.mjs --model deepseek-v4-pro
7
7
  * node bin/deepccc.mjs --stream-json --prompt "hello"
8
+ * node bin/deepccc.mjs web
8
9
  *
9
10
  * 交互模式(TTY)下,单轮回复渲染为固定"过程区块":状态行 + 折叠工具行 +
10
11
  * 原地更新正文,不再滚屏刷 JSON;完成/停止/异常后定型留在屏幕上。
@@ -65,6 +66,10 @@ function parseArgs(argv = process.argv.slice(2)) {
65
66
  config.effort = next;
66
67
  i++;
67
68
  }
69
+ else if (arg === "--max-output-tokens" && next !== undefined) {
70
+ config.maxOutputTokens = parsePositiveIntegerOption("--max-output-tokens", next);
71
+ i++;
72
+ }
68
73
  else if (arg === "--base-url" && next !== undefined) {
69
74
  config.baseURL = next;
70
75
  i++;
@@ -131,6 +136,7 @@ function printHelp(appConfig) {
131
136
  ` --model <name> Model name (current default ${appConfig.model})`,
132
137
  ` --sub-model <name> Sub-model for lightweight steps (compaction/task; empty = follow main model)`,
133
138
  ` --effort <level> Reasoning effort: none/minimal/low/medium/high/xhigh/max (overrides config.effort)`,
139
+ ` --max-output-tokens <n> Maximum output tokens (unset = Provider default)`,
134
140
  ` --base-url <url> Provider API base URL (current default ${appConfig.baseURL})`,
135
141
  " --api-key <key> API key",
136
142
  " --cwd <path> Working directory",
@@ -140,6 +146,7 @@ function printHelp(appConfig) {
140
146
  " --stream-json One-shot mode: write JSONL events to stdout",
141
147
  " --prompt <text> Prompt text for --stream-json",
142
148
  " --plain Force plain streaming output (no progress block renderer)",
149
+ " web Start the local Web UI (default http://127.0.0.1:28080)",
143
150
  " --dangerously-bypass-permissions Skip all permission prompts (aligns with chatccc's bypass mode)",
144
151
  " --help, -h Show help",
145
152
  "",
@@ -603,6 +610,21 @@ function runSkillCreate(argv) {
603
610
  console.log("hot reload: 下一次对话自动生效,无需重启");
604
611
  }
605
612
  async function main() {
613
+ if (process.argv[2] === "web") {
614
+ const webArgs = process.argv.slice(3);
615
+ const portIndex = webArgs.indexOf("--port");
616
+ const port = portIndex >= 0 && webArgs[portIndex + 1] ? Number(webArgs[portIndex + 1]) : undefined;
617
+ if (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535)) {
618
+ throw new Error("--port must be an integer between 1 and 65535");
619
+ }
620
+ const { startDeepCccWebServer } = await import("./web-server.js");
621
+ const handle = await startDeepCccWebServer({
622
+ ...(port ? { port } : {}),
623
+ ...(webArgs.includes("--no-open") ? { openBrowser: false } : {}),
624
+ });
625
+ console.log(`DeepCCC Web UI: ${handle.url}${handle.reused ? " (reused existing server)" : ""}`);
626
+ return;
627
+ }
606
628
  // skill create 子命令:deepccc skill create <name> [--scope global|project] [--description "..."]
607
629
  // 默认创建在全局 ~/.deepccc/skills(Codex 目录结构),--scope project 创建到 <cwd>/.deepccc/skills。
608
630
  if (process.argv[2] === "skill" && process.argv[3] === "create") {
package/dist/config.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface DeepCccConfig {
12
12
  subModel: string;
13
13
  /** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
14
14
  effort: string;
15
+ /** 主对话最大输出 token;未配置时不传,使用 Provider 默认值。 */
16
+ maxOutputTokens?: number;
15
17
  /** 主对话是否使用流式请求;默认开启 */
16
18
  streaming: boolean;
17
19
  /**
@@ -19,19 +21,38 @@ export interface DeepCccConfig {
19
21
  * 上下文压缩阈值自动 = contextWindow × 0.8;超过模型/服务端实际上限会被 API 拒绝。
20
22
  */
21
23
  contextWindow: number;
24
+ git: {
25
+ coAuthor: {
26
+ enabled: boolean;
27
+ name: string;
28
+ email: string;
29
+ };
30
+ };
22
31
  rawStreamLogs: {
23
32
  enabled: boolean;
24
33
  maxBytesPerTurn: number;
25
34
  retentionDays: number;
26
35
  keepCompleted: boolean;
27
36
  };
37
+ web: {
38
+ port: number;
39
+ openOnStart: boolean;
40
+ };
28
41
  }
29
42
  export declare const DEEPCCC_HOME: string;
30
43
  export declare const RAW_STREAM_LOGS_DIR: string;
44
+ export declare const CONFIG_PATH: string;
31
45
  /**
32
46
  * 默认配置(不读环境/文件)。导出供测试断言默认值;运行时请用 loadConfig 结果。
33
47
  */
34
48
  export declare const DEFAULT_CONFIG: DeepCccConfig;
49
+ export declare function readConfigFile(): Partial<DeepCccConfig>;
35
50
  export declare function normalizeDeepCccProvider(value: unknown): DeepCccProvider;
51
+ export declare function loadConfig(): DeepCccConfig;
36
52
  export declare function ensureConfigDir(): void;
53
+ export type DeepCccConfigPatch = Partial<Pick<DeepCccConfig, "provider" | "apiKey" | "baseURL" | "model" | "subModel" | "effort" | "maxOutputTokens" | "streaming" | "contextWindow">> & {
54
+ web?: Partial<DeepCccConfig["web"]>;
55
+ };
56
+ /** Persist only user-editable Web fields while preserving advanced configuration. */
57
+ export declare function saveConfigPatch(patch: DeepCccConfigPatch): DeepCccConfig;
37
58
  export declare const config: DeepCccConfig;
package/dist/config.js CHANGED
@@ -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
  */
@@ -16,6 +16,13 @@ export const DEFAULT_CONFIG = {
16
16
  effort: "",
17
17
  streaming: true,
18
18
  contextWindow: 1_048_576,
19
+ git: {
20
+ coAuthor: {
21
+ enabled: true,
22
+ name: "DeepCCC",
23
+ email: "20184052+wzj998@users.noreply.github.com",
24
+ },
25
+ },
19
26
  rawStreamLogs: {
20
27
  // 默认开启:压缩后可通过 session_search(include_raw_logs=true)找回原文。
21
28
  // 如需关闭,在 ~/.deepccc/config.json 中设置 rawStreamLogs.enabled=false。
@@ -24,8 +31,12 @@ export const DEFAULT_CONFIG = {
24
31
  retentionDays: 7,
25
32
  keepCompleted: false,
26
33
  },
34
+ web: {
35
+ port: 28_080,
36
+ openOnStart: true,
37
+ },
27
38
  };
28
- function readConfigFile() {
39
+ export function readConfigFile() {
29
40
  if (!existsSync(CONFIG_PATH))
30
41
  return {};
31
42
  const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
@@ -49,6 +60,12 @@ function numberEnv(name) {
49
60
  const value = Number(env(name));
50
61
  return Number.isFinite(value) && value >= 0 ? value : undefined;
51
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
+ }
52
69
  export function normalizeDeepCccProvider(value) {
53
70
  if (value === undefined || value === null || String(value).trim() === "")
54
71
  return "openai";
@@ -57,11 +74,14 @@ export function normalizeDeepCccProvider(value) {
57
74
  return normalized;
58
75
  throw new Error(`DEEPCCC_PROVIDER/provider must be "openai" or "anthropic", received: ${String(value)}`);
59
76
  }
60
- function loadConfig() {
77
+ export function loadConfig() {
61
78
  const file = readConfigFile();
62
79
  const rawLogs = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
63
80
  ? file.rawStreamLogs
64
81
  : {};
82
+ const git = file.git && typeof file.git === "object" ? file.git : {};
83
+ const coAuthor = git.coAuthor && typeof git.coAuthor === "object" ? git.coAuthor : {};
84
+ const web = file.web && typeof file.web === "object" ? file.web : {};
65
85
  return {
66
86
  provider: normalizeDeepCccProvider(env("DEEPCCC_PROVIDER") ?? file.provider ?? DEFAULT_CONFIG.provider),
67
87
  apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
@@ -69,17 +89,66 @@ function loadConfig() {
69
89
  model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
70
90
  subModel: env("DEEPCCC_SUB_MODEL") ?? file.subModel ?? DEFAULT_CONFIG.subModel,
71
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),
72
94
  streaming: boolEnv("DEEPCCC_STREAMING") ?? file.streaming ?? DEFAULT_CONFIG.streaming,
73
95
  contextWindow: numberEnv("DEEPCCC_CONTEXT_WINDOW") ?? file.contextWindow ?? DEFAULT_CONFIG.contextWindow,
96
+ git: {
97
+ coAuthor: {
98
+ enabled: boolEnv("DEEPCCC_GIT_COAUTHOR") ?? coAuthor.enabled ?? DEFAULT_CONFIG.git.coAuthor.enabled,
99
+ name: coAuthor.name?.trim() || DEFAULT_CONFIG.git.coAuthor.name,
100
+ email: coAuthor.email?.trim() || DEFAULT_CONFIG.git.coAuthor.email,
101
+ },
102
+ },
74
103
  rawStreamLogs: {
75
104
  enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
76
105
  maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
77
106
  retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
78
107
  keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
79
108
  },
109
+ web: {
110
+ port: normalizeWebPort(web.port),
111
+ openOnStart: web.openOnStart ?? DEFAULT_CONFIG.web.openOnStart,
112
+ },
80
113
  };
81
114
  }
82
115
  export function ensureConfigDir() {
83
116
  mkdirSync(dirname(CONFIG_PATH), { recursive: true });
84
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
+ }
85
154
  export const config = loadConfig();
package/dist/context.d.ts CHANGED
@@ -66,6 +66,7 @@ export declare function normalizeBuiltinSessionId(value: string): string;
66
66
  export declare function defaultBuiltinSessionId(cwd?: string): string;
67
67
  export declare function newBuiltinSessionId(now?: Date, suffix?: string): string;
68
68
  export declare function getBuiltinContextSession(sessionId: string, contextDir?: string): BuiltinContextSessionInfo | null;
69
+ export declare function readBuiltinContextState(sessionId: string, contextDir?: string): BuiltinContextState | null;
69
70
  export declare function listBuiltinContextSessions(contextDir?: string): BuiltinContextSessionInfo[];
70
71
  export declare function latestBuiltinSessionForCwd(cwd: string, contextDir?: string): BuiltinContextSessionInfo | null;
71
72
  /**
package/dist/context.js CHANGED
@@ -135,6 +135,18 @@ function readSessionInfo(contextDir, sessionId) {
135
135
  export function getBuiltinContextSession(sessionId, contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
136
136
  return readSessionInfo(contextDir, sessionId);
137
137
  }
138
+ export function readBuiltinContextState(sessionId, contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
139
+ const normalizedSessionId = normalizeBuiltinSessionId(sessionId);
140
+ const filePath = contextFilePath(contextDir, normalizedSessionId);
141
+ if (!existsSync(filePath))
142
+ return null;
143
+ try {
144
+ return normalizeState(JSON.parse(readFileSync(filePath, "utf8")), normalizedSessionId);
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
138
150
  export function listBuiltinContextSessions(contextDir = DEFAULT_BUILTIN_CONTEXT_DIR) {
139
151
  if (!existsSync(contextDir))
140
152
  return [];
@@ -71,6 +71,13 @@ export interface RunCommandOutput {
71
71
  truncated: boolean;
72
72
  durationMs: number;
73
73
  }
74
+ export interface GitCoAuthorOptions {
75
+ enabled: boolean;
76
+ name: string;
77
+ email: string;
78
+ }
79
+ /** Adds DeepCCC attribution to git commit commands without replacing the user's author. */
80
+ export declare function withGitCoAuthor(command: string, coAuthor?: GitCoAuthorOptions): string;
74
81
  export interface FileEdit {
75
82
  oldText: string;
76
83
  newText: string;
@@ -143,7 +150,7 @@ export declare function expandHomePath(value: string): string;
143
150
  export declare function readFileForTool(cwd: string, input: ReadFileInput): Promise<ReadFileOutput>;
144
151
  export declare function listDirForTool(cwd: string, input?: ListDirInput): Promise<ListDirOutput>;
145
152
  export declare function searchCodeForTool(cwd: string, input: SearchCodeInput, signal?: AbortSignal, runtimeOptions?: SearchCodeRuntimeOptions): Promise<SearchCodeOutput>;
146
- export declare function runCommandForTool(cwd: string, input: RunCommandInput, abortSignal?: AbortSignal): Promise<RunCommandOutput>;
153
+ export declare function runCommandForTool(cwd: string, input: RunCommandInput, abortSignal?: AbortSignal, coAuthor?: GitCoAuthorOptions): Promise<RunCommandOutput>;
147
154
  export declare function editFileForTool(cwd: string, input: EditFileInput): Promise<EditFileOutput>;
148
155
  export declare function createFileForTool(cwd: string, input: CreateFileInput): Promise<FileWriteOutput>;
149
156
  export declare function deleteFileForTool(cwd: string, input: DeleteFileInput): Promise<DeleteFileOutput>;
@@ -152,6 +159,8 @@ export declare function applyPatchForTool(cwd: string, input: ApplyPatchInput):
152
159
  export interface BuiltinFileToolsOptions {
153
160
  /** 权限门控:副作用工具(run_command/文件写操作)执行前会先经过 gate.check */
154
161
  permissionGate?: PermissionGate;
162
+ /** Git commits created by DeepCCC receive this Co-authored-by trailer when enabled. */
163
+ gitCoAuthor?: GitCoAuthorOptions;
155
164
  /** session_search 工具的会话/原始日志目录(默认 ~/.deepccc/sessions 与 raw-stream-logs;测试可注入) */
156
165
  sessionSearch?: {
157
166
  contextDir?: string;
@@ -26,6 +26,13 @@ const MAX_COMMAND_TIMEOUT_MS = 900_000;
26
26
  export const MAX_TASK_OUTPUT_CHARS = 32_000;
27
27
  const requireFromHere = createRequire(import.meta.url);
28
28
  const FALLBACK_SKIPPED_DIRECTORIES = new Set([".git", "node_modules"]);
29
+ /** Adds DeepCCC attribution to git commit commands without replacing the user's author. */
30
+ export function withGitCoAuthor(command, coAuthor) {
31
+ if (!coAuthor?.enabled || command.includes(coAuthor.email))
32
+ return command;
33
+ const trailer = ` --trailer "Co-authored-by: ${coAuthor.name} <${coAuthor.email}>"`;
34
+ return command.replace(/(^|(?:&&|\|\||;|\|)\s*)(git\s+commit)\b/g, (_match, prefix, gitCommit) => `${prefix}${gitCommit}${trailer}`);
35
+ }
29
36
  /**
30
37
  * 把 `~` / `~/x`(含反斜杠 `~\\x`)展开为用户主目录绝对路径。
31
38
  * `~user/x` 形式不展开(Node 无内置支持),保持原样交给后续解析。
@@ -676,8 +683,8 @@ export async function searchCodeForTool(cwd, input, signal, runtimeOptions = {})
676
683
  : fallback.truncated,
677
684
  };
678
685
  }
679
- export async function runCommandForTool(cwd, input, abortSignal) {
680
- const command = input.command?.trim();
686
+ export async function runCommandForTool(cwd, input, abortSignal, coAuthor) {
687
+ const command = withGitCoAuthor(input.command?.trim(), coAuthor);
681
688
  if (!command)
682
689
  throw new Error("command is required");
683
690
  const commandCwd = resolveToolPath(cwd, input.cwd);
@@ -1016,14 +1023,14 @@ export function createBuiltinFileTools(cwd, options = {}) {
1016
1023
  },
1017
1024
  required: ["command"],
1018
1025
  }),
1019
- execute: async (input, options) => {
1026
+ execute: async (input, execOptions) => {
1020
1027
  await guard({
1021
1028
  tool: "run_command",
1022
1029
  action: input.command,
1023
1030
  reason: isDangerousCommand(input.command) ? "high-risk" : "rule",
1024
1031
  detail: `运行命令: ${input.command}`,
1025
1032
  });
1026
- return runCommandForTool(cwd, input, options.abortSignal);
1033
+ return runCommandForTool(cwd, input, execOptions.abortSignal, options.gitCoAuthor);
1027
1034
  },
1028
1035
  }),
1029
1036
  task: tool({
package/dist/index.d.ts CHANGED
@@ -37,6 +37,10 @@ export interface ChatSessionConfig {
37
37
  * overrides config.effort; empty omits the reasoning_effort request field.
38
38
  */
39
39
  effort?: string;
40
+ /** Maximum output tokens for the main conversation; unset uses the Provider default. */
41
+ maxOutputTokens?: number;
42
+ /** Override whether the main conversation uses streaming requests. */
43
+ streaming?: boolean;
40
44
  }
41
45
  export interface ChatSessionOptions {
42
46
  /** Session working directory. */
@@ -78,6 +82,8 @@ export interface ChatSessionOptions {
78
82
  * 调用)自动拒绝高危命令,常规文件操作与低危命令不受影响。
79
83
  */
80
84
  permissionResolver?: PermissionResolver;
85
+ /** Overrides ~/.deepccc/config.json git.coAuthor.enabled for this integration. */
86
+ gitCoAuthor?: boolean;
81
87
  }
82
88
  /**
83
89
  * 流式响应事件
@@ -136,9 +142,12 @@ export declare class ChatSession {
136
142
  private compactionTimeoutMs;
137
143
  private maxSteps?;
138
144
  private effort;
145
+ private maxOutputTokens?;
146
+ private streaming;
139
147
  private permissionMode;
140
148
  private permissionResolver?;
141
149
  private permissionGate;
150
+ private gitCoAuthorEnabled;
142
151
  private skillDirs;
143
152
  private customSystemPrompt;
144
153
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
package/dist/index.js CHANGED
@@ -77,6 +77,7 @@ const COMPACTION_RECOVERY_HINT_DISABLED = [
77
77
  ].join("\n");
78
78
  export const DEFAULT_COMPACTION_TIMEOUT_MS = 5 * 60 * 1000;
79
79
  const MAX_COMPACTION_OUTPUT_TOKENS = 16_384;
80
+ const OPENAI_COMPATIBLE_PROVIDER_NAME = "deepccc";
80
81
  /** task 子代理工具:子代理单轮对话的最大工具步数(防失控循环,结果收敛后即结束) */
81
82
  const TASK_MAX_STEPS = 20;
82
83
  /** 子代理最终输出截断:保留前 MAX_TASK_OUTPUT_CHARS 字符,尾部注明截断信息 */
@@ -219,6 +220,14 @@ function normalizeMaxSteps(value) {
219
220
  }
220
221
  return value;
221
222
  }
223
+ function normalizeMaxOutputTokens(value) {
224
+ if (value === undefined)
225
+ return undefined;
226
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
227
+ throw new Error("maxOutputTokens must be a positive integer when provided");
228
+ }
229
+ return value;
230
+ }
222
231
  function normalizeAnthropicBaseURL(baseURL) {
223
232
  // 完全按用户填写的地址使用,不自动补 /v1(AI SDK 仅对官方 api.anthropic.com
224
233
  // 特判补一次 /v1,其他地址原样拼接 /messages)。
@@ -239,9 +248,12 @@ export class ChatSession {
239
248
  compactionTimeoutMs;
240
249
  maxSteps;
241
250
  effort;
251
+ maxOutputTokens;
252
+ streaming;
242
253
  permissionMode;
243
254
  permissionResolver;
244
255
  permissionGate;
256
+ gitCoAuthorEnabled;
245
257
  skillDirs;
246
258
  customSystemPrompt;
247
259
  /** 最近一次 chat() 使用的 system prompt(供 history 等读取) */
@@ -266,6 +278,7 @@ export class ChatSession {
266
278
  baseURL: this.baseURL,
267
279
  model: this.subModelId || this.modelId,
268
280
  effort: this.effort,
281
+ maxOutputTokens: this.maxOutputTokens,
269
282
  }, {
270
283
  cwd: taskCwd,
271
284
  persist: false,
@@ -303,6 +316,8 @@ export class ChatSession {
303
316
  this.subModelId = (overrides.subModel ?? appConfig.subModel ?? "").trim();
304
317
  this.provider = normalizeDeepCccProvider(overrides.provider ?? appConfig.provider);
305
318
  this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
319
+ this.maxOutputTokens = normalizeMaxOutputTokens(overrides.maxOutputTokens ?? appConfig.maxOutputTokens);
320
+ this.streaming = overrides.streaming ?? appConfig.streaming;
306
321
  this.apiKey = apiKey;
307
322
  this.baseURL = baseURL;
308
323
  const provider = this.provider === "anthropic"
@@ -311,7 +326,7 @@ export class ChatSession {
311
326
  apiKey,
312
327
  })
313
328
  : createOpenAICompatible({
314
- name: "deepccc",
329
+ name: OPENAI_COMPATIBLE_PROVIDER_NAME,
315
330
  baseURL,
316
331
  apiKey,
317
332
  includeUsage: true,
@@ -325,6 +340,7 @@ export class ChatSession {
325
340
  this.customSystemPrompt = options.systemPrompt ?? "";
326
341
  this.permissionMode = options.permissionMode ?? "ask";
327
342
  this.permissionResolver = options.permissionResolver;
343
+ this.gitCoAuthorEnabled = options.gitCoAuthor ?? appConfig.git.coAuthor.enabled;
328
344
  // 技能目录在构造时确定;技能内容在每次 chat() 前重新扫描(mtime 热加载),
329
345
  // 因此创建/修改技能后下一次对话自动生效,无需重启。
330
346
  this.skillDirs =
@@ -413,14 +429,14 @@ export class ChatSession {
413
429
  ? addAnthropicToolJsonCompatibilityNote(hintedMessages)
414
430
  : hintedMessages;
415
431
  // effort 按协议映射:
416
- // - OpenAI 兼容:providerOptions.deepseek.reasoningEffort 由 @ai-sdk/openai-compatible
417
- // 自动映射为请求体 reasoning_effort 字段(DeepSeek 原生支持);
432
+ // - OpenAI 兼容:providerOptions 的 key 必须与 createOpenAICompatible.name 一致;
433
+ // reasoningEffort 由 SDK 映射为请求体 reasoning_effort 字段;
418
434
  // - Anthropic:providerOptions.anthropic.effort 由 @ai-sdk/anthropic 组装为请求体
419
435
  // output_config.effort(官方 Effort API,见 platform.claude.com/docs/en/build-with-claude/effort)
420
436
  let effortProviderOptions;
421
437
  if (this.effort) {
422
438
  effortProviderOptions = this.provider === "openai"
423
- ? { deepseek: { reasoningEffort: this.effort } }
439
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: this.effort } }
424
440
  : { anthropic: { effort: this.effort } };
425
441
  }
426
442
  const baseGenerationOptions = {
@@ -429,9 +445,16 @@ export class ChatSession {
429
445
  tools: createBuiltinFileTools(this.cwd, {
430
446
  permissionGate: this.permissionGate,
431
447
  runTask: this.runTask,
448
+ gitCoAuthor: {
449
+ ...appConfig.git.coAuthor,
450
+ enabled: this.gitCoAuthorEnabled,
451
+ },
432
452
  }),
433
453
  stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
434
454
  abortSignal: signal,
455
+ ...(this.maxOutputTokens !== undefined
456
+ ? { maxOutputTokens: this.maxOutputTokens }
457
+ : {}),
435
458
  ...(effortProviderOptions ? { providerOptions: effortProviderOptions } : {}),
436
459
  };
437
460
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -449,7 +472,7 @@ export class ChatSession {
449
472
  messages: attemptMessages,
450
473
  };
451
474
  let stream;
452
- if (appConfig.streaming) {
475
+ if (this.streaming) {
453
476
  const result = streamText(generationOptions);
454
477
  stream = result.fullStream ?? textStreamToFullStream(result.textStream);
455
478
  }
@@ -624,7 +647,7 @@ export class ChatSession {
624
647
  // output_config.effort=low),避免继承主对话的高 effort 拖慢"压缩上下文中"阶段。
625
648
  maxOutputTokens: MAX_COMPACTION_OUTPUT_TOKENS,
626
649
  providerOptions: this.provider === "openai"
627
- ? { deepseek: { reasoningEffort: "none" } }
650
+ ? { [OPENAI_COMPATIBLE_PROVIDER_NAME]: { reasoningEffort: "none" } }
628
651
  : { anthropic: { effort: "low" } },
629
652
  });
630
653
  if (!result.text.trim()) {