claude-coder 1.0.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 +114 -0
- package/bin/cli.js +140 -0
- package/docs/ARCHITECTURE.md +319 -0
- package/docs/README.en.md +94 -0
- package/package.json +42 -0
- package/src/config.js +211 -0
- package/src/indicator.js +111 -0
- package/src/init.js +144 -0
- package/src/prompts.js +189 -0
- package/src/runner.js +348 -0
- package/src/scanner.js +31 -0
- package/src/session.js +265 -0
- package/src/setup.js +385 -0
- package/src/tasks.js +146 -0
- package/src/validator.js +131 -0
- package/templates/CLAUDE.md +257 -0
- package/templates/SCAN_PROTOCOL.md +123 -0
- package/templates/requirements.example.md +56 -0
package/src/runner.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { execSync } = require('child_process');
|
|
7
|
+
const { paths, log, COLOR, loadConfig, ensureLoopDir, getProjectRoot, getRequirementsHash } = require('./config');
|
|
8
|
+
const { loadTasks, saveTasks, getFeatures, getStats, findNextTask } = require('./tasks');
|
|
9
|
+
const { validate } = require('./validator');
|
|
10
|
+
const { scan } = require('./scanner');
|
|
11
|
+
const { runCodingSession, runViewSession, runAddSession } = require('./session');
|
|
12
|
+
|
|
13
|
+
const MAX_RETRY = 3;
|
|
14
|
+
|
|
15
|
+
function requireSdk() {
|
|
16
|
+
try {
|
|
17
|
+
require.resolve('@anthropic-ai/claude-agent-sdk');
|
|
18
|
+
} catch {
|
|
19
|
+
console.error('错误:未找到 @anthropic-ai/claude-agent-sdk');
|
|
20
|
+
console.error('请先安装:npm install -g @anthropic-ai/claude-agent-sdk');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getHead() {
|
|
26
|
+
try {
|
|
27
|
+
return execSync('git rev-parse HEAD', { cwd: getProjectRoot(), encoding: 'utf8' }).trim();
|
|
28
|
+
} catch {
|
|
29
|
+
return 'none';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function allTasksDone() {
|
|
34
|
+
const data = loadTasks();
|
|
35
|
+
if (!data) return false;
|
|
36
|
+
const features = getFeatures(data);
|
|
37
|
+
if (features.length === 0) return true;
|
|
38
|
+
return features.every(f => f.status === 'done');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rollback(headBefore, reason) {
|
|
42
|
+
if (!headBefore || headBefore === 'none') return;
|
|
43
|
+
log('warn', `回滚到 ${headBefore} ...`);
|
|
44
|
+
try {
|
|
45
|
+
execSync(`git reset --hard ${headBefore}`, { cwd: getProjectRoot(), stdio: 'inherit' });
|
|
46
|
+
log('ok', '回滚完成');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
log('error', `回滚失败: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Record failure in progress.json
|
|
52
|
+
appendProgress({
|
|
53
|
+
type: 'rollback',
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
reason: reason || 'harness 校验失败',
|
|
56
|
+
rollbackTo: headBefore,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function markTaskFailed() {
|
|
61
|
+
const data = loadTasks();
|
|
62
|
+
if (!data) return;
|
|
63
|
+
const features = getFeatures(data);
|
|
64
|
+
for (const f of features) {
|
|
65
|
+
if (f.status === 'in_progress') {
|
|
66
|
+
f.status = 'failed';
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
saveTasks(data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tryPush() {
|
|
74
|
+
try {
|
|
75
|
+
const remotes = execSync('git remote', { cwd: getProjectRoot(), encoding: 'utf8' }).trim();
|
|
76
|
+
if (!remotes) return;
|
|
77
|
+
log('info', '正在推送代码...');
|
|
78
|
+
execSync('git push', { cwd: getProjectRoot(), stdio: 'inherit' });
|
|
79
|
+
log('ok', '推送成功');
|
|
80
|
+
} catch {
|
|
81
|
+
log('warn', '推送失败 (请检查网络或权限),继续执行...');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendProgress(entry) {
|
|
86
|
+
const p = paths();
|
|
87
|
+
let progress = { sessions: [] };
|
|
88
|
+
if (fs.existsSync(p.progressFile)) {
|
|
89
|
+
try { progress = JSON.parse(fs.readFileSync(p.progressFile, 'utf8')); } catch { /* reset */ }
|
|
90
|
+
}
|
|
91
|
+
if (!Array.isArray(progress.sessions)) progress.sessions = [];
|
|
92
|
+
progress.sessions.push(entry);
|
|
93
|
+
fs.writeFileSync(p.progressFile, JSON.stringify(progress, null, 2) + '\n', 'utf8');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function updateSessionHistory(sessionData, sessionNum) {
|
|
97
|
+
const p = paths();
|
|
98
|
+
let sr = { current: null, history: [] };
|
|
99
|
+
if (fs.existsSync(p.sessionResult)) {
|
|
100
|
+
try { sr = JSON.parse(fs.readFileSync(p.sessionResult, 'utf8')); } catch { /* reset */ }
|
|
101
|
+
if (!sr.history && sr.session_result) {
|
|
102
|
+
sr = { current: sr, history: [] };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Move current to history
|
|
107
|
+
if (sr.current) {
|
|
108
|
+
sr.history.push({
|
|
109
|
+
session: sessionNum - 1,
|
|
110
|
+
timestamp: new Date().toISOString(),
|
|
111
|
+
...sr.current,
|
|
112
|
+
});
|
|
113
|
+
sr.current = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (sessionData) {
|
|
117
|
+
sr.current = sessionData;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(p.sessionResult, JSON.stringify(sr, null, 2) + '\n', 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function printStats() {
|
|
124
|
+
const data = loadTasks();
|
|
125
|
+
if (!data) return;
|
|
126
|
+
const stats = getStats(data);
|
|
127
|
+
log('info', `进度: ${stats.done}/${stats.total} done, ${stats.in_progress} in_progress, ${stats.testing} testing, ${stats.failed} failed, ${stats.pending} pending`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function promptContinue() {
|
|
131
|
+
if (!process.stdin.isTTY) return true;
|
|
132
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
rl.question('是否继续?(y/n) ', answer => {
|
|
135
|
+
rl.close();
|
|
136
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function run(requirement, opts = {}) {
|
|
142
|
+
const p = paths();
|
|
143
|
+
const projectRoot = getProjectRoot();
|
|
144
|
+
ensureLoopDir();
|
|
145
|
+
|
|
146
|
+
const maxSessions = opts.max || 50;
|
|
147
|
+
const pauseEvery = opts.pause || 5;
|
|
148
|
+
const dryRun = opts.dryRun || false;
|
|
149
|
+
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log('============================================');
|
|
152
|
+
console.log(` Claude Coder${dryRun ? ' (预览模式)' : ''}`);
|
|
153
|
+
console.log('============================================');
|
|
154
|
+
console.log('');
|
|
155
|
+
|
|
156
|
+
// Load config
|
|
157
|
+
const config = loadConfig();
|
|
158
|
+
if (config.provider !== 'claude' && config.baseUrl) {
|
|
159
|
+
log('ok', `模型配置已加载: ${config.provider}${config.model ? ` (${config.model})` : ''}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read requirement from requirements.md or CLI
|
|
163
|
+
const reqFile = path.join(projectRoot, 'requirements.md');
|
|
164
|
+
if (fs.existsSync(reqFile) && !requirement) {
|
|
165
|
+
requirement = fs.readFileSync(reqFile, 'utf8');
|
|
166
|
+
log('ok', '已读取需求文件: requirements.md');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Ensure git repo
|
|
170
|
+
try {
|
|
171
|
+
execSync('git rev-parse --is-inside-work-tree', { cwd: projectRoot, stdio: 'ignore' });
|
|
172
|
+
} catch {
|
|
173
|
+
log('info', '初始化 git 仓库...');
|
|
174
|
+
execSync('git init', { cwd: projectRoot, stdio: 'inherit' });
|
|
175
|
+
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
176
|
+
cwd: projectRoot,
|
|
177
|
+
stdio: 'inherit',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Initialization (scan) if needed
|
|
182
|
+
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
183
|
+
if (!requirement) {
|
|
184
|
+
log('error', '首次运行需要提供需求描述');
|
|
185
|
+
console.log('');
|
|
186
|
+
console.log('用法(二选一):');
|
|
187
|
+
console.log(' 方式 1: 在项目根目录创建 requirements.md');
|
|
188
|
+
console.log(' claude-coder run');
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log(' 方式 2: 直接传入一句话需求');
|
|
191
|
+
console.log(' claude-coder run "你的需求描述"');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (dryRun) {
|
|
196
|
+
log('info', '[DRY-RUN] 将执行初始化扫描(跳过)');
|
|
197
|
+
const reqPreview = (requirement || '').slice(0, 100);
|
|
198
|
+
log('info', `[DRY-RUN] 需求: ${reqPreview}${reqPreview.length >= 100 ? '...' : ''}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
requireSdk();
|
|
203
|
+
const scanResult = await scan(requirement, { projectRoot });
|
|
204
|
+
if (!scanResult.success) {
|
|
205
|
+
console.log('');
|
|
206
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
207
|
+
console.log(`${COLOR.yellow} 若出现 "Credit balance is too low",请运行:${COLOR.reset}`);
|
|
208
|
+
console.log(` ${COLOR.green}claude-coder setup${COLOR.reset}`);
|
|
209
|
+
console.log(`${COLOR.yellow}═══════════════════════════════════════════════${COLOR.reset}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
log('ok', '检测到已有 project_profile.json + tasks.json,跳过初始化');
|
|
214
|
+
printStats();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Coding loop
|
|
218
|
+
if (!dryRun) requireSdk();
|
|
219
|
+
log('info', `开始编码循环 (最多 ${maxSessions} 个会话) ...`);
|
|
220
|
+
console.log('');
|
|
221
|
+
|
|
222
|
+
let consecutiveFailures = 0;
|
|
223
|
+
|
|
224
|
+
for (let session = 1; session <= maxSessions; session++) {
|
|
225
|
+
console.log('');
|
|
226
|
+
console.log('--------------------------------------------');
|
|
227
|
+
log('info', `Session ${session} / ${maxSessions}`);
|
|
228
|
+
console.log('--------------------------------------------');
|
|
229
|
+
|
|
230
|
+
if (allTasksDone()) {
|
|
231
|
+
console.log('');
|
|
232
|
+
log('ok', '所有任务已完成!');
|
|
233
|
+
printStats();
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
printStats();
|
|
238
|
+
|
|
239
|
+
if (dryRun) {
|
|
240
|
+
const next = findNextTask(loadTasks());
|
|
241
|
+
log('info', `[DRY-RUN] 下一个任务: ${next ? `${next.id} - ${next.description}` : '无'}`);
|
|
242
|
+
if (!next) break;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const headBefore = getHead();
|
|
247
|
+
|
|
248
|
+
// Run coding session
|
|
249
|
+
const sessionResult = await runCodingSession(session, {
|
|
250
|
+
projectRoot,
|
|
251
|
+
consecutiveFailures,
|
|
252
|
+
lastValidateLog: consecutiveFailures > 0 ? '上次校验失败' : '',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Validate
|
|
256
|
+
log('info', '开始 harness 校验 ...');
|
|
257
|
+
const validateResult = await validate(headBefore);
|
|
258
|
+
|
|
259
|
+
if (!validateResult.fatal) {
|
|
260
|
+
log('ok', `Session ${session} 校验通过`);
|
|
261
|
+
tryPush();
|
|
262
|
+
consecutiveFailures = 0;
|
|
263
|
+
|
|
264
|
+
// Update session history
|
|
265
|
+
updateSessionHistory(validateResult.sessionData, session);
|
|
266
|
+
|
|
267
|
+
// Update sync_state.json if requirements exist
|
|
268
|
+
const reqHash = getRequirementsHash();
|
|
269
|
+
if (reqHash) {
|
|
270
|
+
fs.writeFileSync(p.syncState, JSON.stringify({
|
|
271
|
+
last_requirements_hash: reqHash,
|
|
272
|
+
last_synced_at: new Date().toISOString(),
|
|
273
|
+
}, null, 2) + '\n', 'utf8');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Append to progress.json
|
|
277
|
+
appendProgress({
|
|
278
|
+
session,
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
result: 'success',
|
|
281
|
+
cost: sessionResult.cost,
|
|
282
|
+
taskId: validateResult.sessionData?.task_id || null,
|
|
283
|
+
statusAfter: validateResult.sessionData?.status_after || null,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
} else {
|
|
287
|
+
consecutiveFailures++;
|
|
288
|
+
log('error', `Session ${session} 校验失败 (连续失败: ${consecutiveFailures}/${MAX_RETRY})`);
|
|
289
|
+
|
|
290
|
+
rollback(headBefore, '校验失败');
|
|
291
|
+
|
|
292
|
+
if (consecutiveFailures >= MAX_RETRY) {
|
|
293
|
+
log('error', `连续失败 ${MAX_RETRY} 次,跳过当前任务`);
|
|
294
|
+
markTaskFailed();
|
|
295
|
+
consecutiveFailures = 0;
|
|
296
|
+
log('warn', '已将任务标记为 failed,继续下一个任务');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Periodic pause
|
|
301
|
+
if (session % pauseEvery === 0) {
|
|
302
|
+
console.log('');
|
|
303
|
+
printStats();
|
|
304
|
+
const shouldContinue = await promptContinue();
|
|
305
|
+
if (!shouldContinue) {
|
|
306
|
+
log('info', '手动停止');
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Final report
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log('============================================');
|
|
315
|
+
console.log(' 运行结束');
|
|
316
|
+
console.log('============================================');
|
|
317
|
+
console.log('');
|
|
318
|
+
printStats();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function view(requirement, opts = {}) {
|
|
322
|
+
requireSdk();
|
|
323
|
+
const projectRoot = getProjectRoot();
|
|
324
|
+
ensureLoopDir();
|
|
325
|
+
|
|
326
|
+
log('info', '观测模式:交互式运行,实时显示工具调用和决策过程');
|
|
327
|
+
log('info', '退出:Ctrl+C');
|
|
328
|
+
console.log('--------------------------------------------');
|
|
329
|
+
|
|
330
|
+
await runViewSession(requirement, { projectRoot, ...opts });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function add(instruction, opts = {}) {
|
|
334
|
+
requireSdk();
|
|
335
|
+
const p = paths();
|
|
336
|
+
const projectRoot = getProjectRoot();
|
|
337
|
+
ensureLoopDir();
|
|
338
|
+
|
|
339
|
+
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
340
|
+
log('error', 'add 需要先完成初始化(至少运行一次 claude-coder run)');
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await runAddSession(instruction, { projectRoot, ...opts });
|
|
345
|
+
printStats();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = { run, view, add };
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { paths, log, ensureLoopDir } = require('./config');
|
|
5
|
+
const { runScanSession } = require('./session');
|
|
6
|
+
|
|
7
|
+
async function scan(requirement, opts = {}) {
|
|
8
|
+
const p = paths();
|
|
9
|
+
ensureLoopDir();
|
|
10
|
+
|
|
11
|
+
const maxAttempts = 3;
|
|
12
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
13
|
+
log('info', `初始化尝试 ${attempt} / ${maxAttempts} ...`);
|
|
14
|
+
|
|
15
|
+
const result = await runScanSession(requirement, opts);
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(p.profile) && fs.existsSync(p.tasksFile)) {
|
|
18
|
+
log('ok', '初始化完成');
|
|
19
|
+
return { success: true, cost: result.cost };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (attempt < maxAttempts) {
|
|
23
|
+
log('warn', '初始化未完成,将重试...');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log('error', `初始化失败:已重试 ${maxAttempts} 次,关键文件仍未生成`);
|
|
28
|
+
return { success: false, cost: null };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { scan };
|
package/src/session.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { paths, loadConfig, buildEnvVars, getAllowedTools, log } = require('./config');
|
|
6
|
+
const { Indicator, inferPhaseStep } = require('./indicator');
|
|
7
|
+
const { buildSystemPrompt, buildCodingPrompt, buildScanPrompt, buildViewPrompt, buildAddPrompt } = require('./prompts');
|
|
8
|
+
|
|
9
|
+
function applyEnvConfig(config) {
|
|
10
|
+
Object.assign(process.env, buildEnvVars(config));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function runCodingSession(sessionNum, opts = {}) {
|
|
14
|
+
const { query } = require('@anthropic-ai/claude-agent-sdk');
|
|
15
|
+
const config = loadConfig();
|
|
16
|
+
applyEnvConfig(config);
|
|
17
|
+
const indicator = new Indicator();
|
|
18
|
+
|
|
19
|
+
const prompt = buildCodingPrompt(sessionNum, opts);
|
|
20
|
+
const systemPrompt = buildSystemPrompt(false);
|
|
21
|
+
|
|
22
|
+
const p = paths();
|
|
23
|
+
const logFile = path.join(p.logsDir, `session_${sessionNum}_${Date.now()}.log`);
|
|
24
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
25
|
+
|
|
26
|
+
indicator.start(sessionNum);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const session = query({
|
|
30
|
+
prompt,
|
|
31
|
+
options: {
|
|
32
|
+
systemPrompt,
|
|
33
|
+
allowedTools: getAllowedTools(config),
|
|
34
|
+
permissionMode: 'bypassPermissions',
|
|
35
|
+
verbose: true,
|
|
36
|
+
cwd: opts.projectRoot || process.cwd(),
|
|
37
|
+
timeout_ms: config.timeoutMs,
|
|
38
|
+
hooks: {
|
|
39
|
+
PreToolUse: [{
|
|
40
|
+
matcher: '*',
|
|
41
|
+
callback: (event) => {
|
|
42
|
+
inferPhaseStep(indicator, event.tool_name, event.tool_input);
|
|
43
|
+
return { decision: 'allow' };
|
|
44
|
+
}
|
|
45
|
+
}]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
let result;
|
|
51
|
+
for await (const message of session) {
|
|
52
|
+
if (message.content) {
|
|
53
|
+
const text = typeof message.content === 'string'
|
|
54
|
+
? message.content
|
|
55
|
+
: JSON.stringify(message.content);
|
|
56
|
+
process.stdout.write(text);
|
|
57
|
+
logStream.write(text);
|
|
58
|
+
}
|
|
59
|
+
result = message;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logStream.end();
|
|
63
|
+
indicator.stop();
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
exitCode: 0,
|
|
67
|
+
cost: result?.total_cost_usd || null,
|
|
68
|
+
tokenUsage: result?.message?.usage || null,
|
|
69
|
+
logFile,
|
|
70
|
+
};
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logStream.end();
|
|
73
|
+
indicator.stop();
|
|
74
|
+
log('error', `Claude SDK 错误: ${err.message}`);
|
|
75
|
+
return {
|
|
76
|
+
exitCode: 1,
|
|
77
|
+
cost: null,
|
|
78
|
+
tokenUsage: null,
|
|
79
|
+
logFile,
|
|
80
|
+
error: err.message,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runScanSession(requirement, opts = {}) {
|
|
86
|
+
const { query } = require('@anthropic-ai/claude-agent-sdk');
|
|
87
|
+
const config = loadConfig();
|
|
88
|
+
applyEnvConfig(config);
|
|
89
|
+
const indicator = new Indicator();
|
|
90
|
+
|
|
91
|
+
const projectType = hasCodeFiles(opts.projectRoot || process.cwd()) ? 'existing' : 'new';
|
|
92
|
+
const prompt = buildScanPrompt(projectType, requirement);
|
|
93
|
+
const systemPrompt = buildSystemPrompt(true);
|
|
94
|
+
|
|
95
|
+
const p = paths();
|
|
96
|
+
const logFile = path.join(p.logsDir, `scan_${Date.now()}.log`);
|
|
97
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
98
|
+
|
|
99
|
+
indicator.start(0);
|
|
100
|
+
log('info', `正在调用 Claude Code 执行项目扫描(${projectType}项目)...`);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const session = query({
|
|
104
|
+
prompt,
|
|
105
|
+
options: {
|
|
106
|
+
systemPrompt,
|
|
107
|
+
allowedTools: getAllowedTools(config),
|
|
108
|
+
permissionMode: 'bypassPermissions',
|
|
109
|
+
verbose: true,
|
|
110
|
+
cwd: opts.projectRoot || process.cwd(),
|
|
111
|
+
timeout_ms: config.timeoutMs,
|
|
112
|
+
hooks: {
|
|
113
|
+
PreToolUse: [{
|
|
114
|
+
matcher: '*',
|
|
115
|
+
callback: (event) => {
|
|
116
|
+
inferPhaseStep(indicator, event.tool_name, event.tool_input);
|
|
117
|
+
return { decision: 'allow' };
|
|
118
|
+
}
|
|
119
|
+
}]
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let result;
|
|
125
|
+
for await (const message of session) {
|
|
126
|
+
if (message.content) {
|
|
127
|
+
const text = typeof message.content === 'string'
|
|
128
|
+
? message.content
|
|
129
|
+
: JSON.stringify(message.content);
|
|
130
|
+
process.stdout.write(text);
|
|
131
|
+
logStream.write(text);
|
|
132
|
+
}
|
|
133
|
+
result = message;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
logStream.end();
|
|
137
|
+
indicator.stop();
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
exitCode: 0,
|
|
141
|
+
cost: result?.total_cost_usd || null,
|
|
142
|
+
logFile,
|
|
143
|
+
};
|
|
144
|
+
} catch (err) {
|
|
145
|
+
logStream.end();
|
|
146
|
+
indicator.stop();
|
|
147
|
+
log('error', `扫描失败: ${err.message}`);
|
|
148
|
+
return { exitCode: 1, cost: null, logFile, error: err.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runViewSession(requirement, opts = {}) {
|
|
153
|
+
const { query } = require('@anthropic-ai/claude-agent-sdk');
|
|
154
|
+
const p = paths();
|
|
155
|
+
const config = loadConfig();
|
|
156
|
+
applyEnvConfig(config);
|
|
157
|
+
|
|
158
|
+
let systemPrompt;
|
|
159
|
+
let prompt;
|
|
160
|
+
|
|
161
|
+
if (!fs.existsSync(p.profile) || !fs.existsSync(p.tasksFile)) {
|
|
162
|
+
systemPrompt = buildSystemPrompt(true);
|
|
163
|
+
const projectType = hasCodeFiles(opts.projectRoot || process.cwd()) ? 'existing' : 'new';
|
|
164
|
+
prompt = buildViewPrompt({ needsScan: true, projectType, requirement });
|
|
165
|
+
} else {
|
|
166
|
+
systemPrompt = buildSystemPrompt(false);
|
|
167
|
+
const { loadTasks, getFeatures } = require('./tasks');
|
|
168
|
+
const data = loadTasks();
|
|
169
|
+
const features = getFeatures(data);
|
|
170
|
+
const allDone = features.length > 0 && features.every(f => f.status === 'done');
|
|
171
|
+
prompt = buildViewPrompt({ allDone });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const session = query({
|
|
176
|
+
prompt,
|
|
177
|
+
options: {
|
|
178
|
+
systemPrompt,
|
|
179
|
+
allowedTools: getAllowedTools(config),
|
|
180
|
+
permissionMode: 'bypassPermissions',
|
|
181
|
+
verbose: true,
|
|
182
|
+
cwd: opts.projectRoot || process.cwd(),
|
|
183
|
+
timeout_ms: config.timeoutMs,
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
for await (const message of session) {
|
|
188
|
+
if (message.content) {
|
|
189
|
+
const text = typeof message.content === 'string'
|
|
190
|
+
? message.content
|
|
191
|
+
: JSON.stringify(message.content);
|
|
192
|
+
process.stdout.write(text);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
log('error', `观测模式错误: ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function runAddSession(instruction, opts = {}) {
|
|
201
|
+
const { query } = require('@anthropic-ai/claude-agent-sdk');
|
|
202
|
+
const config = loadConfig();
|
|
203
|
+
applyEnvConfig(config);
|
|
204
|
+
|
|
205
|
+
const systemPrompt = buildSystemPrompt(false);
|
|
206
|
+
const prompt = buildAddPrompt(instruction);
|
|
207
|
+
|
|
208
|
+
const p = paths();
|
|
209
|
+
const logFile = path.join(p.logsDir, `add_tasks_${Date.now()}.log`);
|
|
210
|
+
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const session = query({
|
|
214
|
+
prompt,
|
|
215
|
+
options: {
|
|
216
|
+
systemPrompt,
|
|
217
|
+
allowedTools: getAllowedTools(config),
|
|
218
|
+
permissionMode: 'bypassPermissions',
|
|
219
|
+
verbose: true,
|
|
220
|
+
cwd: opts.projectRoot || process.cwd(),
|
|
221
|
+
timeout_ms: config.timeoutMs,
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
for await (const message of session) {
|
|
226
|
+
if (message.content) {
|
|
227
|
+
const text = typeof message.content === 'string'
|
|
228
|
+
? message.content
|
|
229
|
+
: JSON.stringify(message.content);
|
|
230
|
+
process.stdout.write(text);
|
|
231
|
+
logStream.write(text);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
logStream.end();
|
|
236
|
+
log('ok', '任务追加完成');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
logStream.end();
|
|
239
|
+
log('error', `任务追加失败: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function hasCodeFiles(projectRoot) {
|
|
244
|
+
const markers = [
|
|
245
|
+
'package.json', 'pyproject.toml', 'requirements.txt', 'setup.py',
|
|
246
|
+
'Cargo.toml', 'go.mod', 'pom.xml', 'build.gradle',
|
|
247
|
+
'Makefile', 'Dockerfile', 'docker-compose.yml',
|
|
248
|
+
'README.md', 'main.py', 'app.py', 'index.js', 'index.ts',
|
|
249
|
+
];
|
|
250
|
+
for (const m of markers) {
|
|
251
|
+
if (fs.existsSync(path.join(projectRoot, m))) return true;
|
|
252
|
+
}
|
|
253
|
+
for (const d of ['src', 'lib', 'app', 'backend', 'frontend', 'web', 'server', 'client']) {
|
|
254
|
+
if (fs.existsSync(path.join(projectRoot, d)) && fs.statSync(path.join(projectRoot, d)).isDirectory()) return true;
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
runCodingSession,
|
|
261
|
+
runScanSession,
|
|
262
|
+
runViewSession,
|
|
263
|
+
runAddSession,
|
|
264
|
+
hasCodeFiles,
|
|
265
|
+
};
|