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 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 [需求] [--max N] [--pause N] [--dry-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: '追加任务到 tasks.json', usage: 'claude-coder add "指令" [--model M] | add -r [file]' },
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 run "实现用户登录" 开始自动编码');
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(positional[0] || null, opts);
100
+ await runner.run(opts);
101
101
  break;
102
102
  }
103
103
  case 'setup': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-coder",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Claude Coder — Autonomous coding agent harness powered by Claude Code SDK. Scan, plan, code, validate, git-commit in a loop.",
5
5
  "bin": {
6
6
  "claude-coder": "bin/cli.js"
package/prompts/CLAUDE.md CHANGED
@@ -182,7 +182,7 @@
182
182
 
183
183
  ### 第六步:收尾(每次会话必须执行)
184
184
 
185
- 1. **后台服务管理**:根据 prompt 提示决定——单次模式(`--max 1`)时停止所有后台服务(`lsof -ti :端口 | xargs kill`);连续模式时保持服务运行,下个 session 继续使用
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` 列表包含此文件
@@ -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.lastToolTime;
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 secSince = Math.floor(sinceCompletion / 1000);
77
- log('warn', `session_result 已写入 ${secSince} 秒前,模型未自行终止,自动中断`);
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(`[${new Date().toISOString()}] COMPLETION_TIMEOUT: session_result 写入后 ${secSince}s 无终止,自动中断\n`);
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', `无新工具调用超过 ${idleMin} 分钟,自动中断 session`);
96
+ log('warn', `\n无响应超过 ${idleMin} 分钟,自动中断 session`);
93
97
  if (logStream) {
94
- logStream.write(`[${new Date().toISOString()}] STALL: 无工具调用 ${idleMin} 分钟,自动中断\n`);
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(`[${new Date().toISOString()}] COMPLETION_DETECTED: session_result.json written, ${shortMin}min grace period\n`);
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.lastToolTime;
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
- line += ` | ${COLOR.red}${idleMin}分无工具调用(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
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(requirement, opts = {}) {
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
- // --- Phase 1: 项目扫描(生成 profile) ---
182
+ // 检查前置条件
189
183
  if (!fs.existsSync(p.profile)) {
190
- if (!requirement) {
191
- log('error', '首次运行需要提供需求描述');
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 不存在,无法进入编码循环。请先运行 claude-coder add 添加任务。');
189
+ log('error', 'tasks.json 不存在,请先运行 claude-coder add 添加任务');
242
190
  process.exit(1);
243
191
  }
244
192
 
245
- if (fs.existsSync(p.profile) && fs.existsSync(p.tasksFile)) {
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('error', 'add 需要先完成项目扫描(至少运行一次 claude-coder run)');
401
- process.exit(1);
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}`;