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/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-coder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Claude Coder — Autonomous coding agent harness powered by Claude Code SDK. Scan, plan, code, validate, git-commit in a loop.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"claude-coder": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"src/",
|
|
11
|
+
"templates/",
|
|
12
|
+
"docs/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude-coder",
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"ai",
|
|
19
|
+
"agent",
|
|
20
|
+
"autonomous",
|
|
21
|
+
"automation",
|
|
22
|
+
"coding",
|
|
23
|
+
"harness",
|
|
24
|
+
"loop",
|
|
25
|
+
"agent-harness",
|
|
26
|
+
"task-decomposition",
|
|
27
|
+
"code-generation"
|
|
28
|
+
],
|
|
29
|
+
"author": "lk19940215",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/lk19940215/claude-coder.git"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@anthropic-ai/claude-agent-sdk": ">=0.14.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {}
|
|
42
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const COLOR = {
|
|
8
|
+
red: '\x1b[0;31m',
|
|
9
|
+
green: '\x1b[0;32m',
|
|
10
|
+
yellow: '\x1b[1;33m',
|
|
11
|
+
blue: '\x1b[0;34m',
|
|
12
|
+
reset: '\x1b[0m',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function log(level, msg) {
|
|
16
|
+
const tags = {
|
|
17
|
+
info: `${COLOR.blue}[INFO]${COLOR.reset} `,
|
|
18
|
+
ok: `${COLOR.green}[OK]${COLOR.reset} `,
|
|
19
|
+
warn: `${COLOR.yellow}[WARN]${COLOR.reset} `,
|
|
20
|
+
error: `${COLOR.red}[ERROR]${COLOR.reset}`,
|
|
21
|
+
};
|
|
22
|
+
console.error(`${tags[level] || ''} ${msg}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getProjectRoot() {
|
|
26
|
+
return process.cwd();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getLoopDir() {
|
|
30
|
+
return path.join(getProjectRoot(), '.claude-coder');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ensureLoopDir() {
|
|
34
|
+
const dir = getLoopDir();
|
|
35
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
const runtime = path.join(dir, '.runtime');
|
|
37
|
+
if (!fs.existsSync(runtime)) fs.mkdirSync(runtime, { recursive: true });
|
|
38
|
+
const logs = path.join(runtime, 'logs');
|
|
39
|
+
if (!fs.existsSync(logs)) fs.mkdirSync(logs, { recursive: true });
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getTemplatePath(name) {
|
|
44
|
+
return path.join(__dirname, '..', 'templates', name);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function paths() {
|
|
48
|
+
const loopDir = getLoopDir();
|
|
49
|
+
const runtime = path.join(loopDir, '.runtime');
|
|
50
|
+
return {
|
|
51
|
+
loopDir,
|
|
52
|
+
envFile: path.join(loopDir, '.env'),
|
|
53
|
+
tasksFile: path.join(loopDir, 'tasks.json'),
|
|
54
|
+
progressFile: path.join(loopDir, 'progress.json'),
|
|
55
|
+
sessionResult: path.join(loopDir, 'session_result.json'),
|
|
56
|
+
profile: path.join(loopDir, 'project_profile.json'),
|
|
57
|
+
testsFile: path.join(loopDir, 'tests.json'),
|
|
58
|
+
syncState: path.join(loopDir, 'sync_state.json'),
|
|
59
|
+
reqHashFile: path.join(loopDir, 'requirements_hash.current'),
|
|
60
|
+
claudeMd: getTemplatePath('CLAUDE.md'),
|
|
61
|
+
scanProtocol: getTemplatePath('SCAN_PROTOCOL.md'),
|
|
62
|
+
runtime,
|
|
63
|
+
phaseFile: path.join(runtime, 'phase'),
|
|
64
|
+
stepFile: path.join(runtime, 'step'),
|
|
65
|
+
activityLog: path.join(runtime, 'activity.log'),
|
|
66
|
+
logsDir: path.join(runtime, 'logs'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --------------- .env parsing ---------------
|
|
71
|
+
|
|
72
|
+
function parseEnvFile(filepath) {
|
|
73
|
+
if (!fs.existsSync(filepath)) return {};
|
|
74
|
+
const content = fs.readFileSync(filepath, 'utf8');
|
|
75
|
+
const vars = {};
|
|
76
|
+
for (const line of content.split('\n')) {
|
|
77
|
+
const trimmed = line.trim();
|
|
78
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
79
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
80
|
+
if (match) {
|
|
81
|
+
vars[match[1]] = match[2].trim().replace(/^["']|["']$/g, '');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return vars;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --------------- Model mapping ---------------
|
|
88
|
+
|
|
89
|
+
function loadConfig() {
|
|
90
|
+
const p = paths();
|
|
91
|
+
const env = parseEnvFile(p.envFile);
|
|
92
|
+
const config = {
|
|
93
|
+
provider: env.MODEL_PROVIDER || 'claude',
|
|
94
|
+
baseUrl: env.ANTHROPIC_BASE_URL || '',
|
|
95
|
+
apiKey: env.ANTHROPIC_API_KEY || '',
|
|
96
|
+
authToken: env.ANTHROPIC_AUTH_TOKEN || '',
|
|
97
|
+
model: env.ANTHROPIC_MODEL || '',
|
|
98
|
+
timeoutMs: parseInt(env.API_TIMEOUT_MS, 10) || 3000000,
|
|
99
|
+
mcpToolTimeout: parseInt(env.MCP_TOOL_TIMEOUT, 10) || 30000,
|
|
100
|
+
mcpPlaywright: env.MCP_PLAYWRIGHT === 'true',
|
|
101
|
+
debug: env.CLAUDE_DEBUG || '',
|
|
102
|
+
disableNonessential: env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC || '',
|
|
103
|
+
effortLevel: env.CLAUDE_CODE_EFFORT_LEVEL || '',
|
|
104
|
+
smallFastModel: env.ANTHROPIC_SMALL_FAST_MODEL || '',
|
|
105
|
+
defaultOpus: env.ANTHROPIC_DEFAULT_OPUS_MODEL || '',
|
|
106
|
+
defaultSonnet: env.ANTHROPIC_DEFAULT_SONNET_MODEL || '',
|
|
107
|
+
defaultHaiku: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || '',
|
|
108
|
+
thinkingBudget: env.ANTHROPIC_THINKING_BUDGET || '',
|
|
109
|
+
raw: env,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// GLM: default model if not set
|
|
113
|
+
if (config.baseUrl && (config.baseUrl.includes('bigmodel.cn') || config.baseUrl.includes('z.ai'))) {
|
|
114
|
+
if (!config.model) config.model = 'glm-4.7';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// DeepSeek chat → haiku shim (prevent reasoner billing)
|
|
118
|
+
if (config.baseUrl.includes('deepseek') && config.model === 'deepseek-chat') {
|
|
119
|
+
config.model = 'claude-3-haiku-20240307';
|
|
120
|
+
config.defaultOpus = 'claude-3-haiku-20240307';
|
|
121
|
+
config.defaultSonnet = 'claude-3-haiku-20240307';
|
|
122
|
+
config.defaultHaiku = 'claude-3-haiku-20240307';
|
|
123
|
+
config.smallFastModel = 'claude-3-haiku-20240307';
|
|
124
|
+
config.thinkingBudget = '0';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return config;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildEnvVars(config) {
|
|
131
|
+
const env = { ...process.env };
|
|
132
|
+
if (config.baseUrl) env.ANTHROPIC_BASE_URL = config.baseUrl;
|
|
133
|
+
if (config.apiKey) env.ANTHROPIC_API_KEY = config.apiKey;
|
|
134
|
+
if (config.authToken) env.ANTHROPIC_AUTH_TOKEN = config.authToken;
|
|
135
|
+
if (config.model) env.ANTHROPIC_MODEL = config.model;
|
|
136
|
+
if (config.timeoutMs) env.API_TIMEOUT_MS = String(config.timeoutMs);
|
|
137
|
+
if (config.mcpToolTimeout) env.MCP_TOOL_TIMEOUT = String(config.mcpToolTimeout);
|
|
138
|
+
if (config.smallFastModel) env.ANTHROPIC_SMALL_FAST_MODEL = config.smallFastModel;
|
|
139
|
+
if (config.disableNonessential) env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = config.disableNonessential;
|
|
140
|
+
if (config.effortLevel) env.CLAUDE_CODE_EFFORT_LEVEL = config.effortLevel;
|
|
141
|
+
if (config.defaultOpus) env.ANTHROPIC_DEFAULT_OPUS_MODEL = config.defaultOpus;
|
|
142
|
+
if (config.defaultSonnet) env.ANTHROPIC_DEFAULT_SONNET_MODEL = config.defaultSonnet;
|
|
143
|
+
if (config.defaultHaiku) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = config.defaultHaiku;
|
|
144
|
+
if (config.thinkingBudget) env.ANTHROPIC_THINKING_BUDGET = config.thinkingBudget;
|
|
145
|
+
return env;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --------------- Allowed tools ---------------
|
|
149
|
+
|
|
150
|
+
function getAllowedTools(config) {
|
|
151
|
+
const tools = ['Read', 'Edit', 'Write', 'Bash', 'Glob', 'Grep'];
|
|
152
|
+
if (config.mcpPlaywright) tools.push('mcp__playwright__*');
|
|
153
|
+
return tools;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --------------- Sync to global claude settings ---------------
|
|
157
|
+
|
|
158
|
+
function syncToGlobal() {
|
|
159
|
+
const config = loadConfig();
|
|
160
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
161
|
+
const settingsDir = path.dirname(settingsPath);
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(settingsDir)) fs.mkdirSync(settingsDir, { recursive: true });
|
|
164
|
+
|
|
165
|
+
let settings = {};
|
|
166
|
+
if (fs.existsSync(settingsPath)) {
|
|
167
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); } catch { /* ignore */ }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const key of ['apiKey', 'anthropicBaseUrl', 'defaultSonnetModel', 'defaultOpusModel', 'defaultHaikuModel', 'model']) {
|
|
171
|
+
delete settings[key];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!settings.env || typeof settings.env !== 'object') settings.env = {};
|
|
175
|
+
|
|
176
|
+
const envVars = buildEnvVars(config);
|
|
177
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
178
|
+
if (key.startsWith('ANTHROPIC_') || key.endsWith('_TIMEOUT_MS') || key === 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC') {
|
|
179
|
+
settings.env[key] = value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
184
|
+
log('ok', `已同步配置到 ${settingsPath}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --------------- Requirements hash ---------------
|
|
188
|
+
|
|
189
|
+
function getRequirementsHash() {
|
|
190
|
+
const crypto = require('crypto');
|
|
191
|
+
const reqFile = path.join(getProjectRoot(), 'requirements.md');
|
|
192
|
+
if (!fs.existsSync(reqFile)) return '';
|
|
193
|
+
const content = fs.readFileSync(reqFile, 'utf8');
|
|
194
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
COLOR,
|
|
199
|
+
log,
|
|
200
|
+
getProjectRoot,
|
|
201
|
+
getLoopDir,
|
|
202
|
+
ensureLoopDir,
|
|
203
|
+
getTemplatePath,
|
|
204
|
+
paths,
|
|
205
|
+
parseEnvFile,
|
|
206
|
+
loadConfig,
|
|
207
|
+
buildEnvVars,
|
|
208
|
+
getAllowedTools,
|
|
209
|
+
syncToGlobal,
|
|
210
|
+
getRequirementsHash,
|
|
211
|
+
};
|
package/src/indicator.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { paths, COLOR } = require('./config');
|
|
5
|
+
|
|
6
|
+
const SPINNERS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
+
|
|
8
|
+
class Indicator {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.phase = 'thinking';
|
|
11
|
+
this.step = '';
|
|
12
|
+
this.spinnerIndex = 0;
|
|
13
|
+
this.timer = null;
|
|
14
|
+
this.lastActivity = '';
|
|
15
|
+
this.sessionNum = 0;
|
|
16
|
+
this.startTime = Date.now();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
start(sessionNum) {
|
|
20
|
+
this.sessionNum = sessionNum;
|
|
21
|
+
this.startTime = Date.now();
|
|
22
|
+
this.timer = setInterval(() => this._render(), 500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
stop() {
|
|
26
|
+
if (this.timer) {
|
|
27
|
+
clearInterval(this.timer);
|
|
28
|
+
this.timer = null;
|
|
29
|
+
}
|
|
30
|
+
process.stderr.write('\r\x1b[K');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
updatePhase(phase) {
|
|
34
|
+
this.phase = phase;
|
|
35
|
+
this._writePhaseFile();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
updateStep(step) {
|
|
39
|
+
this.step = step;
|
|
40
|
+
this._writeStepFile();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
appendActivity(toolName, summary) {
|
|
44
|
+
const ts = new Date().toISOString();
|
|
45
|
+
const entry = `[${ts}] ${toolName}: ${summary}`;
|
|
46
|
+
this.lastActivity = entry;
|
|
47
|
+
try {
|
|
48
|
+
const p = paths();
|
|
49
|
+
fs.appendFileSync(p.activityLog, entry + '\n', 'utf8');
|
|
50
|
+
} catch { /* ignore */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_writePhaseFile() {
|
|
54
|
+
try { fs.writeFileSync(paths().phaseFile, this.phase, 'utf8'); } catch { /* ignore */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_writeStepFile() {
|
|
58
|
+
try { fs.writeFileSync(paths().stepFile, this.step, 'utf8'); } catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_render() {
|
|
62
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
63
|
+
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
64
|
+
const ss = String(elapsed % 60).padStart(2, '0');
|
|
65
|
+
const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
|
|
66
|
+
this.spinnerIndex++;
|
|
67
|
+
|
|
68
|
+
const phaseLabel = this.phase === 'thinking'
|
|
69
|
+
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
70
|
+
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
71
|
+
|
|
72
|
+
let line = `\r${spinner} [Session ${this.sessionNum}] ${phaseLabel} ${mm}:${ss}`;
|
|
73
|
+
if (this.step) line += ` | ${this.step}`;
|
|
74
|
+
|
|
75
|
+
const maxWidth = process.stderr.columns || 80;
|
|
76
|
+
if (line.length > maxWidth + 20) line = line.slice(0, maxWidth + 20);
|
|
77
|
+
|
|
78
|
+
process.stderr.write(`\r\x1b[K${line}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Phase-signal logic: infer phase/step from tool calls
|
|
83
|
+
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
84
|
+
const name = (toolName || '').toLowerCase();
|
|
85
|
+
|
|
86
|
+
if (name === 'write' || name === 'edit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
87
|
+
indicator.updatePhase('coding');
|
|
88
|
+
} else if (name === 'bash' || name === 'shell') {
|
|
89
|
+
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
90
|
+
if (cmd.includes('git ')) {
|
|
91
|
+
indicator.updateStep('Git 操作');
|
|
92
|
+
} else if (cmd.includes('npm ') || cmd.includes('pip ') || cmd.includes('pnpm ')) {
|
|
93
|
+
indicator.updateStep('安装依赖');
|
|
94
|
+
} else if (cmd.includes('test') || cmd.includes('curl') || cmd.includes('pytest')) {
|
|
95
|
+
indicator.updateStep('测试验证');
|
|
96
|
+
indicator.updatePhase('coding');
|
|
97
|
+
} else {
|
|
98
|
+
indicator.updatePhase('coding');
|
|
99
|
+
}
|
|
100
|
+
} else if (name === 'read' || name === 'glob' || name === 'grep') {
|
|
101
|
+
indicator.updatePhase('thinking');
|
|
102
|
+
indicator.updateStep('读取文件');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const summary = typeof toolInput === 'object'
|
|
106
|
+
? (toolInput.path || toolInput.command || toolInput.pattern || JSON.stringify(toolInput).slice(0, 80))
|
|
107
|
+
: String(toolInput || '').slice(0, 80);
|
|
108
|
+
indicator.appendActivity(toolName, summary);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { Indicator, inferPhaseStep };
|
package/src/init.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const net = require('net');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { spawn, execSync } = require('child_process');
|
|
7
|
+
const { paths, log, getProjectRoot } = require('./config');
|
|
8
|
+
|
|
9
|
+
function loadProfile() {
|
|
10
|
+
const p = paths();
|
|
11
|
+
if (!fs.existsSync(p.profile)) {
|
|
12
|
+
log('error', '未找到 project_profile.json,请先运行 claude-coder run 完成项目扫描');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
return JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isPortFree(port) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const server = net.createServer();
|
|
21
|
+
server.once('error', () => resolve(false));
|
|
22
|
+
server.once('listening', () => { server.close(); resolve(true); });
|
|
23
|
+
server.listen(port, '127.0.0.1');
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function waitForHealth(url, timeoutMs = 15000) {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
return new Promise(resolve => {
|
|
30
|
+
const check = () => {
|
|
31
|
+
if (Date.now() - start > timeoutMs) { resolve(false); return; }
|
|
32
|
+
const req = http.get(url, res => {
|
|
33
|
+
resolve(res.statusCode < 500);
|
|
34
|
+
});
|
|
35
|
+
req.on('error', () => setTimeout(check, 1000));
|
|
36
|
+
req.setTimeout(3000, () => { req.destroy(); setTimeout(check, 1000); });
|
|
37
|
+
};
|
|
38
|
+
check();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runCmd(cmd, cwd) {
|
|
43
|
+
try {
|
|
44
|
+
execSync(cmd, { cwd: cwd || getProjectRoot(), stdio: 'inherit', shell: true });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function init() {
|
|
52
|
+
const profile = loadProfile();
|
|
53
|
+
const projectRoot = getProjectRoot();
|
|
54
|
+
let stepCount = 0;
|
|
55
|
+
|
|
56
|
+
// 1. Environment activation
|
|
57
|
+
const envSetup = profile.env_setup || {};
|
|
58
|
+
if (envSetup.python_env && envSetup.python_env !== 'system' && envSetup.python_env !== 'none') {
|
|
59
|
+
stepCount++;
|
|
60
|
+
if (envSetup.python_env.startsWith('conda:')) {
|
|
61
|
+
const envName = envSetup.python_env.slice(6);
|
|
62
|
+
log('info', `[${stepCount}] Python 环境: conda activate ${envName}`);
|
|
63
|
+
runCmd(`conda activate ${envName}`);
|
|
64
|
+
} else if (envSetup.python_env === 'venv') {
|
|
65
|
+
log('info', `[${stepCount}] Python 环境: venv`);
|
|
66
|
+
runCmd('source .venv/bin/activate || .venv\\Scripts\\activate');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (envSetup.node_version && envSetup.node_version !== 'none') {
|
|
70
|
+
stepCount++;
|
|
71
|
+
log('info', `[${stepCount}] Node.js: v${envSetup.node_version}`);
|
|
72
|
+
runCmd(`nvm use ${envSetup.node_version}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 2. Dependencies
|
|
76
|
+
const pkgManagers = (profile.tech_stack && profile.tech_stack.package_managers) || [];
|
|
77
|
+
for (const pm of pkgManagers) {
|
|
78
|
+
stepCount++;
|
|
79
|
+
if (pm === 'npm' || pm === 'yarn' || pm === 'pnpm') {
|
|
80
|
+
if (fs.existsSync(`${projectRoot}/node_modules`)) {
|
|
81
|
+
log('ok', `[${stepCount}] ${pm} 依赖已安装,跳过`);
|
|
82
|
+
} else {
|
|
83
|
+
log('info', `[${stepCount}] 安装依赖: ${pm} install`);
|
|
84
|
+
runCmd(`${pm} install`, projectRoot);
|
|
85
|
+
}
|
|
86
|
+
} else if (pm === 'pip') {
|
|
87
|
+
const reqFile = fs.existsSync(`${projectRoot}/requirements.txt`);
|
|
88
|
+
if (reqFile) {
|
|
89
|
+
log('info', `[${stepCount}] 安装依赖: pip install -r requirements.txt`);
|
|
90
|
+
runCmd('pip install -r requirements.txt', projectRoot);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Custom init commands
|
|
96
|
+
const customInit = profile.custom_init || [];
|
|
97
|
+
for (const cmd of customInit) {
|
|
98
|
+
stepCount++;
|
|
99
|
+
log('info', `[${stepCount}] 自定义: ${cmd}`);
|
|
100
|
+
runCmd(cmd, projectRoot);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. Services
|
|
104
|
+
const services = profile.services || [];
|
|
105
|
+
for (const svc of services) {
|
|
106
|
+
stepCount++;
|
|
107
|
+
const free = await isPortFree(svc.port);
|
|
108
|
+
if (!free) {
|
|
109
|
+
log('ok', `[${stepCount}] ${svc.name} 已在端口 ${svc.port} 运行,跳过`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
log('info', `[${stepCount}] 启动 ${svc.name} (端口 ${svc.port})...`);
|
|
114
|
+
const cwd = svc.cwd ? `${projectRoot}/${svc.cwd}` : projectRoot;
|
|
115
|
+
const child = spawn(svc.command, { cwd, shell: true, detached: true, stdio: 'ignore' });
|
|
116
|
+
child.unref();
|
|
117
|
+
|
|
118
|
+
if (svc.health_check) {
|
|
119
|
+
const healthy = await waitForHealth(svc.health_check);
|
|
120
|
+
if (healthy) {
|
|
121
|
+
log('ok', `${svc.name} 就绪: ${svc.health_check}`);
|
|
122
|
+
} else {
|
|
123
|
+
log('warn', `${svc.name} 健康检查超时 (${svc.health_check}),继续执行`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Summary
|
|
129
|
+
if (stepCount === 0) {
|
|
130
|
+
log('info', '无需初始化操作');
|
|
131
|
+
} else {
|
|
132
|
+
log('ok', `初始化完成 (${stepCount} 步)`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (services.length > 0) {
|
|
136
|
+
console.log('');
|
|
137
|
+
for (const svc of services) {
|
|
138
|
+
console.log(` ${svc.name}: http://localhost:${svc.port}`);
|
|
139
|
+
}
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = { init };
|
package/src/prompts.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { paths, loadConfig, getRequirementsHash } = require('./config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build system prompt by combining template files.
|
|
8
|
+
* @param {boolean} includeScanProtocol - Whether to append SCAN_PROTOCOL.md
|
|
9
|
+
*/
|
|
10
|
+
function buildSystemPrompt(includeScanProtocol = false) {
|
|
11
|
+
const p = paths();
|
|
12
|
+
let prompt = fs.readFileSync(p.claudeMd, 'utf8');
|
|
13
|
+
if (includeScanProtocol && fs.existsSync(p.scanProtocol)) {
|
|
14
|
+
prompt += '\n\n' + fs.readFileSync(p.scanProtocol, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
return prompt;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build user prompt for coding sessions.
|
|
21
|
+
* Includes conditional hints based on session state.
|
|
22
|
+
*/
|
|
23
|
+
function buildCodingPrompt(sessionNum, opts = {}) {
|
|
24
|
+
const p = paths();
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const consecutiveFailures = opts.consecutiveFailures || 0;
|
|
27
|
+
|
|
28
|
+
// Hint 1: Requirements change detection
|
|
29
|
+
const reqHash = getRequirementsHash();
|
|
30
|
+
let reqSyncHint = '';
|
|
31
|
+
if (reqHash) {
|
|
32
|
+
fs.writeFileSync(p.reqHashFile, reqHash, 'utf8');
|
|
33
|
+
let lastHash = '';
|
|
34
|
+
if (fs.existsSync(p.syncState)) {
|
|
35
|
+
try { lastHash = JSON.parse(fs.readFileSync(p.syncState, 'utf8')).last_requirements_hash || ''; } catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
if (lastHash !== reqHash) {
|
|
38
|
+
reqSyncHint = '需求已变更:第一步中请读取 requirements.md,将新增需求追加为 pending 任务到 tasks.json。';
|
|
39
|
+
}
|
|
40
|
+
} else if (fs.existsSync(p.reqHashFile)) {
|
|
41
|
+
fs.unlinkSync(p.reqHashFile);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Hint 2: Playwright MCP availability
|
|
45
|
+
const mcpHint = config.mcpPlaywright
|
|
46
|
+
? '前端/全栈任务可用 Playwright MCP(browser_navigate、browser_snapshot、browser_click 等)做端到端测试。'
|
|
47
|
+
: '';
|
|
48
|
+
|
|
49
|
+
// Hint 3: Retry context from previous failures
|
|
50
|
+
let retryContext = '';
|
|
51
|
+
if (consecutiveFailures > 0 && opts.lastValidateLog) {
|
|
52
|
+
retryContext = `\n注意:上次会话校验失败,原因:${opts.lastValidateLog}。请避免同样的问题。`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Hint 4: Environment readiness
|
|
56
|
+
let envHint = '';
|
|
57
|
+
if (consecutiveFailures === 0 && sessionNum > 1) {
|
|
58
|
+
envHint = '环境已就绪,第二步可跳过 claude-coder init,仅确认服务存活。涉及新依赖时仍需运行 claude-coder init。';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Hint 5: Existing test records
|
|
62
|
+
let testHint = '';
|
|
63
|
+
if (fs.existsSync(p.testsFile)) {
|
|
64
|
+
try {
|
|
65
|
+
const count = (JSON.parse(fs.readFileSync(p.testsFile, 'utf8')).test_cases || []).length;
|
|
66
|
+
if (count > 0) testHint = `tests.json 已有 ${count} 条验证记录,Step 5 时先查已有记录避免重复验证。`;
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Hint 6: Project documentation awareness
|
|
71
|
+
let docsHint = '';
|
|
72
|
+
if (fs.existsSync(p.profile)) {
|
|
73
|
+
try {
|
|
74
|
+
const profile = JSON.parse(fs.readFileSync(p.profile, 'utf8'));
|
|
75
|
+
const docs = profile.existing_docs || [];
|
|
76
|
+
if (docs.length > 0) {
|
|
77
|
+
docsHint = `项目文档: ${docs.join(', ')}。Step 4 编码前先读与任务相关的文档,了解接口约定和编码规范。完成后若新增了模块或 API,更新对应文档。`;
|
|
78
|
+
}
|
|
79
|
+
} catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
`Session ${sessionNum}。执行 6 步流程。`,
|
|
84
|
+
'效率要求:先规划后编码,完成全部编码后再统一测试,禁止编码-测试反复跳转。后端任务用 curl 验证,不启动浏览器。',
|
|
85
|
+
reqSyncHint,
|
|
86
|
+
mcpHint,
|
|
87
|
+
testHint,
|
|
88
|
+
docsHint,
|
|
89
|
+
envHint,
|
|
90
|
+
`完成后写入 session_result.json。${retryContext}`,
|
|
91
|
+
].filter(Boolean).join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build task decomposition guide for scan and add sessions.
|
|
96
|
+
* Placed in user prompt (recency zone) for maximum attention.
|
|
97
|
+
*/
|
|
98
|
+
function buildTaskGuide(projectType) {
|
|
99
|
+
const lines = [
|
|
100
|
+
'任务分解指导(严格遵守):',
|
|
101
|
+
'1. 粒度:每个任务是独立可测试的功能单元,1-3 session 可完成,不超 500 行新增',
|
|
102
|
+
'2. steps 具体可验证:最后一步必须是 curl/grep 等验证命令',
|
|
103
|
+
'3. depends_on 形成 DAG(有向无环图),不得循环依赖',
|
|
104
|
+
'4. 单任务 steps 不超过 5 步,超过则拆分为多个任务',
|
|
105
|
+
'5. category 准确:backend | frontend | fullstack | infra',
|
|
106
|
+
'6. 第一个任务从第一个有业务逻辑的功能开始,不重复脚手架内容',
|
|
107
|
+
'',
|
|
108
|
+
'验证命令模板:',
|
|
109
|
+
' API: curl -s -o /dev/null -w "%{http_code}" http://localhost:PORT/path → 200',
|
|
110
|
+
' 文件: grep -q "关键内容" path/to/file && echo "pass"',
|
|
111
|
+
' 构建: npm run build 2>&1 | tail -1 → 无 error',
|
|
112
|
+
'',
|
|
113
|
+
'反面案例(禁止出现):',
|
|
114
|
+
' X "实现用户功能" → 太模糊,应拆为具体接口',
|
|
115
|
+
' X "编写测试" → 无具体内容,测试应内嵌在 steps 末尾',
|
|
116
|
+
' X steps 只有 "实现xxx" 没有验证步骤',
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
if (projectType === 'new') {
|
|
120
|
+
lines.push('', '新项目注意:infra 任务合并为尽量少的条目,不拆碎');
|
|
121
|
+
}
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build user prompt for scan sessions.
|
|
127
|
+
*/
|
|
128
|
+
function buildScanPrompt(projectType, requirement) {
|
|
129
|
+
const taskGuide = buildTaskGuide(projectType);
|
|
130
|
+
return [
|
|
131
|
+
'你是项目初始化 Agent,同时也是资深的需求分析师。',
|
|
132
|
+
'',
|
|
133
|
+
`项目类型: ${projectType}`,
|
|
134
|
+
`用户需求: ${requirement || '(无指定需求)'}`,
|
|
135
|
+
'',
|
|
136
|
+
'步骤 1-2:按「项目扫描协议」扫描项目、生成 project_profile.json。',
|
|
137
|
+
'步骤 3:根据以下指导分解任务到 tasks.json(格式见 CLAUDE.md):',
|
|
138
|
+
'',
|
|
139
|
+
taskGuide,
|
|
140
|
+
'',
|
|
141
|
+
'步骤 4:写入 session_result.json 并 git commit。',
|
|
142
|
+
].join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build user prompt for view sessions.
|
|
147
|
+
* @param {Object} opts - { needsScan, projectType, requirement, allDone }
|
|
148
|
+
*/
|
|
149
|
+
function buildViewPrompt(opts = {}) {
|
|
150
|
+
if (opts.needsScan) {
|
|
151
|
+
return `你是项目初始化 Agent。项目类型: ${opts.projectType}。用户需求: ${opts.requirement || ''}。按照「项目扫描协议」执行。`;
|
|
152
|
+
}
|
|
153
|
+
if (opts.allDone) {
|
|
154
|
+
return '所有任务已完成,无需执行 6 步流程。直接与用户对话,按需回答问题或执行临时请求。';
|
|
155
|
+
}
|
|
156
|
+
return '执行 6 步流程,完成下一个任务。';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Build user prompt for add sessions.
|
|
161
|
+
*/
|
|
162
|
+
function buildAddPrompt(instruction) {
|
|
163
|
+
const taskGuide = buildTaskGuide();
|
|
164
|
+
return [
|
|
165
|
+
'重要:这是任务追加 session,不是常规编码 session。不执行 6 步流程。',
|
|
166
|
+
'',
|
|
167
|
+
'步骤:',
|
|
168
|
+
'1. 读取 .claude-coder/tasks.json 了解已有任务和最大 id/priority',
|
|
169
|
+
'2. 读取 .claude-coder/project_profile.json 了解项目技术栈',
|
|
170
|
+
'3. 根据用户指令追加新任务(status: pending)',
|
|
171
|
+
'',
|
|
172
|
+
taskGuide,
|
|
173
|
+
'',
|
|
174
|
+
'新任务 id 和 priority 从已有最大值递增。不修改已有任务,不实现代码。',
|
|
175
|
+
'git add -A && git commit -m "chore: add new tasks"',
|
|
176
|
+
'写入 session_result.json',
|
|
177
|
+
'',
|
|
178
|
+
`用户指令:${instruction}`,
|
|
179
|
+
].join('\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
buildSystemPrompt,
|
|
184
|
+
buildCodingPrompt,
|
|
185
|
+
buildTaskGuide,
|
|
186
|
+
buildScanPrompt,
|
|
187
|
+
buildViewPrompt,
|
|
188
|
+
buildAddPrompt,
|
|
189
|
+
};
|