claude-coder 1.7.0 → 1.7.1
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/bin/cli.js +5 -5
- package/package.json +1 -1
- package/prompts/CLAUDE.md +1 -1
- package/prompts/coding_user.md +8 -0
- package/src/hooks.js +16 -10
- package/src/indicator.js +18 -2
- package/src/runner.js +38 -62
- package/src/session.js +2 -0
package/bin/cli.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
const pkg = require('../package.json');
|
|
5
5
|
|
|
6
6
|
const COMMANDS = {
|
|
7
|
-
run: { desc: '自动编码循环', usage: 'claude-coder run [
|
|
7
|
+
run: { desc: '自动编码循环', usage: 'claude-coder run [--max N] [--pause N] [--dry-run]' },
|
|
8
8
|
setup: { desc: '交互式模型配置', usage: 'claude-coder setup' },
|
|
9
9
|
init: { desc: '初始化项目环境', usage: 'claude-coder init' },
|
|
10
|
-
add: { desc: '
|
|
10
|
+
add: { desc: '追加任务并可选自动执行', usage: 'claude-coder add "指令" [--model M] | add -r [file]' },
|
|
11
11
|
auth: { desc: '导出 Playwright 登录状态', usage: 'claude-coder auth [url]' },
|
|
12
12
|
validate: { desc: '手动校验上次 session', usage: 'claude-coder validate' },
|
|
13
13
|
status: { desc: '查看任务进度和成本', usage: 'claude-coder status' },
|
|
@@ -22,11 +22,11 @@ function showHelp() {
|
|
|
22
22
|
}
|
|
23
23
|
console.log('\n示例:');
|
|
24
24
|
console.log(' claude-coder setup 配置模型和 API Key');
|
|
25
|
-
console.log(' claude-coder
|
|
25
|
+
console.log(' claude-coder add "实现用户登录" 添加任务(询问是否自动执行)');
|
|
26
|
+
console.log(' claude-coder run 执行所有待处理任务');
|
|
26
27
|
console.log(' claude-coder run --max 1 单次执行');
|
|
27
28
|
console.log(' claude-coder run --max 5 --pause 5 每 5 个 session 暂停确认');
|
|
28
29
|
console.log(' claude-coder run --dry-run 预览模式');
|
|
29
|
-
console.log(' claude-coder add "新增搜索功能" 追加任务');
|
|
30
30
|
console.log(' claude-coder add -r 从 requirements.md 追加任务');
|
|
31
31
|
console.log(' claude-coder add "..." --model opus-4 指定模型追加任务');
|
|
32
32
|
console.log(' claude-coder auth 导出 Playwright 登录状态');
|
|
@@ -97,7 +97,7 @@ async function main() {
|
|
|
97
97
|
switch (command) {
|
|
98
98
|
case 'run': {
|
|
99
99
|
const runner = require('../src/runner');
|
|
100
|
-
await runner.run(
|
|
100
|
+
await runner.run(opts);
|
|
101
101
|
break;
|
|
102
102
|
}
|
|
103
103
|
case 'setup': {
|
package/package.json
CHANGED
package/prompts/CLAUDE.md
CHANGED
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
|
|
183
183
|
### 第六步:收尾(每次会话必须执行)
|
|
184
184
|
|
|
185
|
-
1. **后台服务管理**:根据 prompt 提示决定——单次模式(`--max 1
|
|
185
|
+
1. **后台服务管理**:根据 prompt 提示决定——单次模式(`--max 1`)时停止所有后台服务,连续模式时保持服务运行。停止服务的跨平台命令见 coding prompt 中的「进程管理规范」
|
|
186
186
|
2. **按需更新文档和 profile**:
|
|
187
187
|
- **README / 用户文档**:仅当对外行为变化(新增功能、API 变更、使用方式变化)时更新
|
|
188
188
|
- **项目指令文件**:如果本次新增了模块、改变了模块职责或新增了 API 端点,更新 `.claude/CLAUDE.md`。同时确保 `project_profile.json` 的 `existing_docs` 列表包含此文件
|
package/prompts/coding_user.md
CHANGED
|
@@ -20,4 +20,12 @@ Session {{sessionNum}}。执行 6 步流程。
|
|
|
20
20
|
- 查文档/API: WebSearch + WebFetch
|
|
21
21
|
- 效率: 多个 Read/Glob/Grep 尽量合并为一次批量调用,减少工具轮次
|
|
22
22
|
|
|
23
|
+
进程管理规范(跨平台,严格遵守):
|
|
24
|
+
- 停止端口服务(Windows): `netstat -ano | findstr :PORT` 获取 PID,然后 `taskkill /F /T /PID <PID>`(/T 杀进程树,必须带 /T)
|
|
25
|
+
- 停止端口服务(Linux/Mac): `lsof -ti :PORT | xargs kill -9`
|
|
26
|
+
- 备选方案: `npx kill-port PORT`(跨平台)或 `powershell -Command "Get-NetTCPConnection -LocalPort PORT -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"`
|
|
27
|
+
- 杀进程失败时不要反复重试同一命令(最多 2 次),立即换用其他方法
|
|
28
|
+
- 重启服务前必须先确认端口已释放(netstat/lsof 无输出),再启动新进程
|
|
29
|
+
- Python venv 环境注意:uvicorn --reload 会创建父子进程树,必须用 /T 参数或杀父进程
|
|
30
|
+
|
|
23
31
|
完成后写入 session_result.json。{{retryContext}}
|
package/src/hooks.js
CHANGED
|
@@ -67,35 +67,39 @@ function createSessionHooks(indicator, logStream, options = {}) {
|
|
|
67
67
|
if (enableStallDetection) {
|
|
68
68
|
stallChecker = setInterval(() => {
|
|
69
69
|
const now = Date.now();
|
|
70
|
-
const idleMs = now - indicator.
|
|
70
|
+
const idleMs = now - indicator.lastActivityTime; // 使用活动时间而非工具调用时间
|
|
71
71
|
|
|
72
|
+
// 优先检测 completion 超时(session_result 写入后的缩短超时)
|
|
72
73
|
if (completionDetectedAt > 0) {
|
|
73
74
|
const sinceCompletion = now - completionDetectedAt;
|
|
74
75
|
if (sinceCompletion > completionTimeoutMs && !stallDetected) {
|
|
75
76
|
stallDetected = true;
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
78
|
+
const actualMin = Math.floor(sinceCompletion / 60000);
|
|
79
|
+
log('warn', `\nsession_result 已写入 ${actualMin} 分钟,超过 ${shortMin} 分钟上限,自动中断`);
|
|
78
80
|
if (logStream) {
|
|
79
|
-
logStream.write(
|
|
81
|
+
logStream.write(`\n[${new Date().toISOString()}] STALL: session_result 写入后 ${actualMin} 分钟(上限 ${shortMin} 分钟),自动中断\n`);
|
|
80
82
|
}
|
|
81
83
|
if (abortController) {
|
|
82
84
|
abortController.abort();
|
|
83
|
-
log('warn', '
|
|
85
|
+
log('warn', '\n已发送中断信号');
|
|
84
86
|
}
|
|
85
|
-
return;
|
|
86
87
|
}
|
|
88
|
+
// 已检测到 completion,不再执行 stall 检测,等待 completion 超时
|
|
89
|
+
return;
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
// 正常 stall 检测(仅在未检测到 completion 时执行)
|
|
89
93
|
if (idleMs > stallTimeoutMs && !stallDetected) {
|
|
90
94
|
stallDetected = true;
|
|
91
95
|
const idleMin = Math.floor(idleMs / 60000);
|
|
92
|
-
log('warn',
|
|
96
|
+
log('warn', `\n无响应超过 ${idleMin} 分钟,自动中断 session`);
|
|
93
97
|
if (logStream) {
|
|
94
|
-
logStream.write(
|
|
98
|
+
logStream.write(`\n[${new Date().toISOString()}] STALL: 无响应 ${idleMin} 分钟,自动中断\n`);
|
|
95
99
|
}
|
|
96
100
|
if (abortController) {
|
|
97
101
|
abortController.abort();
|
|
98
|
-
log('warn', '已发送中断信号');
|
|
102
|
+
log('warn', '\n已发送中断信号');
|
|
99
103
|
}
|
|
100
104
|
}
|
|
101
105
|
}, 30000);
|
|
@@ -138,9 +142,11 @@ function createSessionHooks(indicator, logStream, options = {}) {
|
|
|
138
142
|
if (isSessionResultWrite(input.tool_name, input.tool_input)) {
|
|
139
143
|
completionDetectedAt = Date.now();
|
|
140
144
|
const shortMin = Math.ceil(completionTimeoutMs / 60000);
|
|
145
|
+
indicator.setCompletionDetected(shortMin);
|
|
146
|
+
log('info', '');
|
|
141
147
|
log('info', `检测到 session_result 写入,${shortMin} 分钟内模型未终止将自动中断`);
|
|
142
148
|
if (logStream) {
|
|
143
|
-
logStream.write(
|
|
149
|
+
logStream.write(`\n[${new Date().toISOString()}] COMPLETION_DETECTED: session_result.json written, ${shortMin}min grace period\n`);
|
|
144
150
|
}
|
|
145
151
|
}
|
|
146
152
|
}
|
package/src/indicator.js
CHANGED
|
@@ -57,14 +57,17 @@ class Indicator {
|
|
|
57
57
|
this.timer = null;
|
|
58
58
|
this.lastActivity = '';
|
|
59
59
|
this.lastToolTime = Date.now();
|
|
60
|
+
this.lastActivityTime = Date.now(); // 最后活动时间(包括文字输出)
|
|
60
61
|
this.sessionNum = 0;
|
|
61
62
|
this.startTime = Date.now();
|
|
62
63
|
this.stallTimeoutMin = 30;
|
|
64
|
+
this.completionTimeoutMin = null; // session_result 写入后的缩短超时
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
start(sessionNum, stallTimeoutMin) {
|
|
66
68
|
this.sessionNum = sessionNum;
|
|
67
69
|
this.startTime = Date.now();
|
|
70
|
+
this.lastActivityTime = Date.now();
|
|
68
71
|
if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
|
|
69
72
|
this.timer = setInterval(() => this._render(), 1000);
|
|
70
73
|
}
|
|
@@ -89,6 +92,14 @@ class Indicator {
|
|
|
89
92
|
this.lastActivity = `${toolName}: ${summary}`;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
setCompletionDetected(timeoutMin) {
|
|
96
|
+
this.completionTimeoutMin = timeoutMin;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
updateActivity() {
|
|
100
|
+
this.lastActivityTime = Date.now();
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
getStatusLine() {
|
|
93
104
|
const now = new Date();
|
|
94
105
|
const hh = String(now.getHours()).padStart(2, '0');
|
|
@@ -105,12 +116,16 @@ class Indicator {
|
|
|
105
116
|
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
106
117
|
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
107
118
|
|
|
108
|
-
const idleMs = Date.now() - this.
|
|
119
|
+
const idleMs = Date.now() - this.lastActivityTime;
|
|
109
120
|
const idleMin = Math.floor(idleMs / 60000);
|
|
110
121
|
|
|
111
122
|
let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
|
|
112
123
|
if (idleMin >= 2) {
|
|
113
|
-
|
|
124
|
+
if (this.completionTimeoutMin) {
|
|
125
|
+
line += ` | ${COLOR.red}${idleMin}分无响应(session_result 已写入, ${this.completionTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
|
|
126
|
+
} else {
|
|
127
|
+
line += ` | ${COLOR.red}${idleMin}分无响应(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
|
|
128
|
+
}
|
|
114
129
|
}
|
|
115
130
|
if (this.step) {
|
|
116
131
|
line += ` | ${this.step}`;
|
|
@@ -172,6 +187,7 @@ function inferPhaseStep(indicator, toolName, toolInput) {
|
|
|
172
187
|
const name = (toolName || '').toLowerCase();
|
|
173
188
|
|
|
174
189
|
indicator.lastToolTime = Date.now();
|
|
190
|
+
indicator.lastActivityTime = Date.now();
|
|
175
191
|
|
|
176
192
|
if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
177
193
|
indicator.updatePhase('coding');
|
package/src/runner.js
CHANGED
|
@@ -148,7 +148,7 @@ async function promptContinue() {
|
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
async function run(
|
|
151
|
+
async function run(opts = {}) {
|
|
152
152
|
const p = paths();
|
|
153
153
|
const projectRoot = getProjectRoot();
|
|
154
154
|
ensureLoopDir();
|
|
@@ -168,12 +168,6 @@ async function run(requirement, opts = {}) {
|
|
|
168
168
|
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
const reqFile = path.join(projectRoot, 'requirements.md');
|
|
172
|
-
if (fs.existsSync(reqFile) && !requirement) {
|
|
173
|
-
requirement = fs.readFileSync(reqFile, 'utf8');
|
|
174
|
-
log('ok', '已读取需求文件: requirements.md');
|
|
175
|
-
}
|
|
176
|
-
|
|
177
171
|
try {
|
|
178
172
|
execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: 'ignore' });
|
|
179
173
|
} catch {
|
|
@@ -185,66 +179,18 @@ async function run(requirement, opts = {}) {
|
|
|
185
179
|
});
|
|
186
180
|
}
|
|
187
181
|
|
|
188
|
-
//
|
|
182
|
+
// 检查前置条件
|
|
189
183
|
if (!fs.existsSync(p.profile)) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log('');
|
|
193
|
-
console.log('用法(二选一):');
|
|
194
|
-
console.log(' 方式 1: 在项目根目录创建 requirements.md');
|
|
195
|
-
console.log(' claude-coder run');
|
|
196
|
-
console.log('');
|
|
197
|
-
console.log(' 方式 2: 直接传入一句话需求');
|
|
198
|
-
console.log(' claude-coder run "你的需求描述"');
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (dryRun) {
|
|
203
|
-
log('info', '[DRY-RUN] 将执行初始化扫描(跳过)');
|
|
204
|
-
const reqPreview = (requirement || '').slice(0, 100);
|
|
205
|
-
log('info', `[DRY-RUN] 需求: ${reqPreview}${reqPreview.length >= 100 ? '...' : ''}`);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
await loadSDK();
|
|
210
|
-
const scanResult = await scan(requirement, { projectRoot });
|
|
211
|
-
if (!scanResult.success) {
|
|
212
|
-
console.log('');
|
|
213
|
-
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
214
|
-
console.log(`${COLOR.yellow} 若出现 "Credit balance is too low",请运行:${COLOR.reset}`);
|
|
215
|
-
console.log(` ${COLOR.green}claude-coder setup${COLOR.reset}`);
|
|
216
|
-
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// --- Phase 2: 任务分解(scan 后衔接 add) ---
|
|
222
|
-
if (!fs.existsSync(p.tasksFile)) {
|
|
223
|
-
if (requirement) {
|
|
224
|
-
console.log('');
|
|
225
|
-
log('ok', '项目扫描完成,是否根据需求分解任务?');
|
|
226
|
-
const shouldAdd = await promptContinue();
|
|
227
|
-
if (shouldAdd) {
|
|
228
|
-
if (!dryRun) await loadSDK();
|
|
229
|
-
deployTestRule(p);
|
|
230
|
-
log('info', '正在分解任务...');
|
|
231
|
-
await runAddSession(requirement, { projectRoot });
|
|
232
|
-
} else {
|
|
233
|
-
log('info', '跳过任务分解。后续可通过 claude-coder add 手动添加任务。');
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
log('warn', 'tasks.json 不存在且无需求描述,请运行 claude-coder add 添加任务');
|
|
237
|
-
}
|
|
184
|
+
log('error', 'profile.json 不存在,请先运行 claude-coder add 添加任务');
|
|
185
|
+
process.exit(1);
|
|
238
186
|
}
|
|
239
187
|
|
|
240
188
|
if (!fs.existsSync(p.tasksFile)) {
|
|
241
|
-
log('error', 'tasks.json
|
|
189
|
+
log('error', 'tasks.json 不存在,请先运行 claude-coder add 添加任务');
|
|
242
190
|
process.exit(1);
|
|
243
191
|
}
|
|
244
192
|
|
|
245
|
-
|
|
246
|
-
printStats();
|
|
247
|
-
}
|
|
193
|
+
printStats();
|
|
248
194
|
|
|
249
195
|
if (!dryRun) await loadSDK();
|
|
250
196
|
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
@@ -377,6 +323,17 @@ async function run(requirement, opts = {}) {
|
|
|
377
323
|
printStats();
|
|
378
324
|
}
|
|
379
325
|
|
|
326
|
+
async function promptAutoRun() {
|
|
327
|
+
if (!process.stdin.isTTY) return false;
|
|
328
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
329
|
+
return new Promise(resolve => {
|
|
330
|
+
rl.question('任务分解完成后是否自动开始执行?(y/n) ', answer => {
|
|
331
|
+
rl.close();
|
|
332
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
380
337
|
async function add(instruction, opts = {}) {
|
|
381
338
|
await loadSDK();
|
|
382
339
|
const p = paths();
|
|
@@ -396,15 +353,34 @@ async function add(instruction, opts = {}) {
|
|
|
396
353
|
const displayModel = opts.model || config.model || '(default)';
|
|
397
354
|
log('ok', `模型配置已加载: ${config.provider || 'claude'} (add 使用: ${displayModel})`);
|
|
398
355
|
|
|
356
|
+
// 如果 profile 不存在,先执行项目扫描
|
|
399
357
|
if (!fs.existsSync(p.profile)) {
|
|
400
|
-
log('
|
|
401
|
-
|
|
358
|
+
log('info', '首次使用,正在执行项目扫描...');
|
|
359
|
+
const scanResult = await scan(instruction, { projectRoot });
|
|
360
|
+
if (!scanResult.success) {
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
363
|
+
console.log(`${COLOR.yellow} 若出现 "Credit balance is too low",请运行:${COLOR.reset}`);
|
|
364
|
+
console.log(` ${COLOR.green}claude-coder setup${COLOR.reset}`);
|
|
365
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
402
368
|
}
|
|
403
369
|
|
|
370
|
+
// 询问用户是否在完成后自动运行
|
|
371
|
+
const shouldAutoRun = await promptAutoRun();
|
|
372
|
+
|
|
404
373
|
deployTestRule(p);
|
|
405
374
|
|
|
406
375
|
await runAddSession(instruction, { projectRoot, ...opts });
|
|
407
376
|
printStats();
|
|
377
|
+
|
|
378
|
+
// 如果用户选择自动运行,则调用 run()
|
|
379
|
+
if (shouldAutoRun) {
|
|
380
|
+
console.log('');
|
|
381
|
+
log('info', '开始自动执行任务...');
|
|
382
|
+
await run(opts);
|
|
383
|
+
}
|
|
408
384
|
}
|
|
409
385
|
|
|
410
386
|
function deployTestRule(p) {
|
package/src/session.js
CHANGED
|
@@ -88,6 +88,8 @@ function logMessage(message, logStream, indicator) {
|
|
|
88
88
|
if (message.type === 'assistant' && message.message?.content) {
|
|
89
89
|
for (const block of message.message.content) {
|
|
90
90
|
if (block.type === 'text' && block.text) {
|
|
91
|
+
// 模型有文字输出,更新活动时间
|
|
92
|
+
if (indicator) indicator.updateActivity();
|
|
91
93
|
if (indicator) {
|
|
92
94
|
process.stderr.write('\r\x1b[K');
|
|
93
95
|
const contentKey = `${indicator.phase}|${indicator.step}|${indicator.toolTarget}`;
|