claude-coder 1.9.2 → 1.10.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/README.md +166 -141
- package/bin/cli.js +19 -4
- package/package.json +2 -2
- package/src/common/assets.js +66 -52
- package/src/common/config.js +22 -0
- package/src/common/sdk.js +1 -3
- package/src/common/utils.js +3 -1
- package/src/core/coding.js +3 -1
- package/src/core/design.js +268 -0
- package/src/core/go.js +3 -3
- package/src/core/hooks.js +30 -16
- package/src/core/init.js +9 -0
- package/src/core/plan.js +21 -15
- package/src/core/prompts.js +47 -2
- package/src/core/repair.js +1 -1
- package/src/core/runner.js +84 -117
- package/src/core/scan.js +4 -3
- package/src/core/session.js +30 -16
- package/src/core/simplify.js +4 -2
- package/src/core/state.js +23 -8
- package/src/index.js +4 -0
- package/templates/{codingUser.md → coding/user.md} +1 -0
- package/templates/design/base.md +103 -0
- package/templates/design/fixSystem.md +71 -0
- package/templates/design/fixUser.md +3 -0
- package/templates/design/init.md +304 -0
- package/templates/design/system.md +108 -0
- package/templates/design/user.md +11 -0
- package/templates/{coreProtocol.md → other/coreProtocol.md} +1 -0
- package/templates/{test_rule.md → other/test_rule.md} +0 -2
- package/templates/{planUser.md → plan/user.md} +2 -1
- /package/templates/{codingSystem.md → coding/system.md} +0 -0
- /package/templates/{goSystem.md → go/system.md} +0 -0
- /package/templates/{bash-process.md → other/bash-process.md} +0 -0
- /package/templates/{guidance.json → other/guidance.json} +0 -0
- /package/templates/{requirements.example.md → other/requirements.example.md} +0 -0
- /package/templates/{web-testing.md → other/web-testing.md} +0 -0
- /package/templates/{planSystem.md → plan/system.md} +0 -0
- /package/templates/{scanSystem.md → scan/system.md} +0 -0
- /package/templates/{scanUser.md → scan/user.md} +0 -0
package/src/common/config.js
CHANGED
|
@@ -115,9 +115,31 @@ function updateEnvVar(key, value) {
|
|
|
115
115
|
return true;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
const REPO_URL = 'https://lk19940215.github.io/claude-coder';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} command - 命令名(run / plan / design / go / simplify / scan)
|
|
122
|
+
* @param {string} detail - 右侧附加信息(模式、范围等)
|
|
123
|
+
* @param {string} [model] - 模型名
|
|
124
|
+
*/
|
|
125
|
+
function printModeBanner(command, detail, model) {
|
|
126
|
+
const sep = ` ${COLOR.dim}│${COLOR.reset} `;
|
|
127
|
+
const parts = [`${COLOR.bold}Claude Coder${COLOR.reset}`, command];
|
|
128
|
+
if (model) parts.push(`model: ${model}`);
|
|
129
|
+
if (detail) parts.push(detail);
|
|
130
|
+
const inner = parts.join(sep);
|
|
131
|
+
console.error('');
|
|
132
|
+
console.error(`${COLOR.cyan}╔══════════════════════════════════════════════╗${COLOR.reset}`);
|
|
133
|
+
console.error(`${COLOR.cyan}║${COLOR.reset} ${inner}`);
|
|
134
|
+
console.error(`${COLOR.cyan}║${COLOR.reset} ${COLOR.dim}${REPO_URL}${COLOR.reset}`);
|
|
135
|
+
console.error(`${COLOR.cyan}╚══════════════════════════════════════════════╝${COLOR.reset}`);
|
|
136
|
+
console.error('');
|
|
137
|
+
}
|
|
138
|
+
|
|
118
139
|
module.exports = {
|
|
119
140
|
COLOR,
|
|
120
141
|
log,
|
|
142
|
+
printModeBanner,
|
|
121
143
|
parseEnvFile,
|
|
122
144
|
loadConfig,
|
|
123
145
|
buildEnvVars,
|
package/src/common/sdk.js
CHANGED
|
@@ -43,9 +43,7 @@ async function loadSDK() {
|
|
|
43
43
|
} catch { /* try next */ }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
log('error', `请先安装:npm install -g ${pkgName}`);
|
|
48
|
-
process.exit(1);
|
|
46
|
+
throw new Error(`未找到 ${pkgName},请先安装:npm install -g ${pkgName}`);
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
module.exports = { loadSDK };
|
package/src/common/utils.js
CHANGED
|
@@ -133,6 +133,8 @@ function ensureGitignore(projectRoot) {
|
|
|
133
133
|
'.claude-coder/*',
|
|
134
134
|
'!.claude-coder/tasks.json',
|
|
135
135
|
'!.claude-coder/project_profile.json',
|
|
136
|
+
'!.claude-coder/design/',
|
|
137
|
+
'!.claude-coder/harness_state.json',
|
|
136
138
|
];
|
|
137
139
|
let added = false;
|
|
138
140
|
for (const p of patterns) {
|
|
@@ -171,7 +173,7 @@ function killServices(projectRoot) {
|
|
|
171
173
|
const { assets } = require('./assets');
|
|
172
174
|
const profile = assets.readJson('profile', null);
|
|
173
175
|
if (!profile) return;
|
|
174
|
-
const ports = (profile.services || []).map(s => s.port).filter(
|
|
176
|
+
const ports = (profile.services || []).map(s => s.port).filter(p => p && /^\d+$/.test(String(p)));
|
|
175
177
|
if (ports.length === 0) return;
|
|
176
178
|
|
|
177
179
|
for (const port of ports) {
|
package/src/core/coding.js
CHANGED
|
@@ -19,7 +19,9 @@ async function executeCoding(config, sessionNum, opts = {}) {
|
|
|
19
19
|
queryOpts.systemPrompt = buildSystemPrompt('coding');
|
|
20
20
|
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
21
21
|
|
|
22
|
-
const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts
|
|
22
|
+
const { subtype, cost, usage } = await session.runQuery(prompt, queryOpts, {
|
|
23
|
+
continue: true,
|
|
24
|
+
});
|
|
23
25
|
|
|
24
26
|
if (subtype && subtype !== 'success' && subtype !== 'unknown') {
|
|
25
27
|
log('warn', `session 结束原因: ${subtype}`);
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { buildSystemPrompt } = require('./prompts');
|
|
7
|
+
const { log, printModeBanner } = require('../common/config');
|
|
8
|
+
const { assets } = require('../common/assets');
|
|
9
|
+
const { saveDesignState } = require('./state');
|
|
10
|
+
const { Session } = require('./session');
|
|
11
|
+
|
|
12
|
+
// ─── Design Dir ───────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function getDesignDir() {
|
|
15
|
+
return assets.dir('design');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function scanPenFiles(designDir) {
|
|
19
|
+
const files = [];
|
|
20
|
+
const scan = (dir, prefix = '') => {
|
|
21
|
+
if (!fs.existsSync(dir)) return;
|
|
22
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
scan(path.join(dir, entry.name), prefix + entry.name + '/');
|
|
25
|
+
} else if (entry.name.endsWith('.pen')) {
|
|
26
|
+
files.push({ rel: prefix + entry.name, abs: path.join(dir, entry.name) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
scan(designDir);
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Type Resolution ─────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function resolveType(opts, designDir) {
|
|
37
|
+
if (opts.type) return opts.type;
|
|
38
|
+
|
|
39
|
+
const systemPenPath = path.join(designDir, 'system.lib.pen');
|
|
40
|
+
if (!fs.existsSync(systemPenPath)) return 'init';
|
|
41
|
+
|
|
42
|
+
return 'new';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── Prompt Builders ─────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function hasProjectCode(root) {
|
|
48
|
+
const markers = ['package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', 'pom.xml'];
|
|
49
|
+
const dirs = ['src', 'lib', 'app', 'frontend', 'web', 'client', 'pages'];
|
|
50
|
+
for (const m of markers) { if (fs.existsSync(path.join(root, m))) return true; }
|
|
51
|
+
for (const d of dirs) {
|
|
52
|
+
const p = path.join(root, d);
|
|
53
|
+
if (fs.existsSync(p) && fs.statSync(p).isDirectory()) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildProjectContext() {
|
|
59
|
+
const root = assets.projectRoot;
|
|
60
|
+
const hasCode = hasProjectCode(root);
|
|
61
|
+
let ctx = `### 项目类型\n${hasCode ? '已有代码项目(设计时应 Read 源码还原真实内容)' : '全新项目(无现有代码,根据需求从零设计)'}\n- 项目根路径: ${root}\n\n`;
|
|
62
|
+
|
|
63
|
+
if (hasCode) {
|
|
64
|
+
const pkgPath = path.join(root, 'package.json');
|
|
65
|
+
if (fs.existsSync(pkgPath)) {
|
|
66
|
+
try {
|
|
67
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
68
|
+
ctx += `### 项目信息\n- name: ${pkg.name || '未定义'}\n- description: ${pkg.description || '未定义'}\n\n`;
|
|
69
|
+
} catch { /* ignore */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return ctx;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function buildDesignPrompt(instruction, designDir) {
|
|
77
|
+
let designContext = `### 设计文件目录\n绝对路径: ${designDir}\n`;
|
|
78
|
+
|
|
79
|
+
const systemPenPath = path.join(designDir, 'system.lib.pen');
|
|
80
|
+
const isInit = !fs.existsSync(systemPenPath);
|
|
81
|
+
designContext += isInit
|
|
82
|
+
? '### 设计库\n尚未创建 system.lib.pen,请先根据下方「初始化模板」生成。\n\n'
|
|
83
|
+
: '### 设计库\n已有 system.lib.pen,请先 Read 查看并复用。\n\n';
|
|
84
|
+
|
|
85
|
+
if (isInit) {
|
|
86
|
+
const initTemplate = assets.read('designInit') || '';
|
|
87
|
+
if (initTemplate) {
|
|
88
|
+
designContext += `### 初始化模板\n\n${initTemplate}\n\n`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mapPath = path.join(designDir, 'design_map.json');
|
|
93
|
+
if (fs.existsSync(mapPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const map = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
|
96
|
+
const pages = Object.entries(map.pages || {});
|
|
97
|
+
if (pages.length > 0) {
|
|
98
|
+
designContext += '### 已有页面\n';
|
|
99
|
+
for (const [name, info] of pages) {
|
|
100
|
+
designContext += `- **${name}**: ${info.description} (${path.join(designDir, info.pen)})\n`;
|
|
101
|
+
}
|
|
102
|
+
designContext += '\n';
|
|
103
|
+
}
|
|
104
|
+
} catch { /* ignore */ }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
designContext += buildProjectContext();
|
|
108
|
+
|
|
109
|
+
return assets.render('designUser', {
|
|
110
|
+
designContext,
|
|
111
|
+
instruction: instruction
|
|
112
|
+
? `用户需求:\n${instruction}`
|
|
113
|
+
: '用户未提供需求,使用对话模式收集。',
|
|
114
|
+
modeHint: instruction
|
|
115
|
+
? '【自动模式】用户已提供需求,直接设计,不要提问。'
|
|
116
|
+
: '【对话模式】使用 AskUserQuestion 工具引导用户描述需求。',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildFixPrompt(designDir, userInput) {
|
|
121
|
+
const penFiles = scanPenFiles(designDir);
|
|
122
|
+
let designContext = '### 需要检查修复的 .pen 文件\n\n';
|
|
123
|
+
if (penFiles.length === 0) {
|
|
124
|
+
designContext += '(未发现 .pen 文件)\n';
|
|
125
|
+
} else {
|
|
126
|
+
for (const f of penFiles) {
|
|
127
|
+
designContext += `- ${f.abs}\n`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
designContext += '\n';
|
|
131
|
+
|
|
132
|
+
const instruction = userInput
|
|
133
|
+
? `用户反馈的问题:\n${userInput}\n\n请 Read 每个文件,检查并修复所有不合规内容。`
|
|
134
|
+
: '请 Read 每个文件,检查并修复所有不合规内容。';
|
|
135
|
+
|
|
136
|
+
return assets.render('designFixUser', { designContext, instruction });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Post-session Summary ─────────────────────────────────
|
|
140
|
+
|
|
141
|
+
function showDesignSummary(designDir) {
|
|
142
|
+
const penFiles = scanPenFiles(designDir);
|
|
143
|
+
if (penFiles.length === 0) {
|
|
144
|
+
log('warn', '设计目录中没有 .pen 文件');
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log('┌─ 设计文件 ─────────────────────────────────────┐');
|
|
150
|
+
for (const f of penFiles) {
|
|
151
|
+
console.log(`│ ${f.rel.padEnd(52)}│`);
|
|
152
|
+
}
|
|
153
|
+
console.log('└───────────────────────────────────────────────┘');
|
|
154
|
+
|
|
155
|
+
const hasMap = fs.existsSync(path.join(designDir, 'design_map.json'));
|
|
156
|
+
if (hasMap) log('info', 'design_map.json OK');
|
|
157
|
+
|
|
158
|
+
let hasJsonError = false;
|
|
159
|
+
for (const f of penFiles) {
|
|
160
|
+
try {
|
|
161
|
+
JSON.parse(fs.readFileSync(f.abs, 'utf8'));
|
|
162
|
+
} catch (e) {
|
|
163
|
+
hasJsonError = true;
|
|
164
|
+
log('error', `${f.rel}: JSON 语法错误 — ${e.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (hasJsonError) {
|
|
168
|
+
log('warn', '存在 JSON 格式问题,建议运行: claude-coder design --type fix');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return penFiles.length;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── User Confirm ────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
function askUser(question) {
|
|
177
|
+
if (!process.stdin.isTTY) return Promise.resolve('');
|
|
178
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
179
|
+
return new Promise(resolve => {
|
|
180
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Fix Session ─────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
async function runFixSession(config, designDir, userInput, opts) {
|
|
187
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
188
|
+
log('info', '正在修复 .pen 文件...');
|
|
189
|
+
|
|
190
|
+
await Session.run('design', config, {
|
|
191
|
+
logFileName: `design_fix_${ts}.log`,
|
|
192
|
+
label: 'design_fix',
|
|
193
|
+
async execute(session) {
|
|
194
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
195
|
+
queryOpts.systemPrompt = buildSystemPrompt('designFix');
|
|
196
|
+
return await session.runQuery(buildFixPrompt(designDir, userInput), queryOpts);
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
log('ok', '修复完成');
|
|
201
|
+
showDesignSummary(designDir);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Main Entry ──────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
async function executeDesign(config, input, opts = {}) {
|
|
207
|
+
if (opts.reset) {
|
|
208
|
+
saveDesignState({});
|
|
209
|
+
log('ok', 'Design 状态已重置');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const designDir = getDesignDir();
|
|
214
|
+
if (!fs.existsSync(designDir)) fs.mkdirSync(designDir, { recursive: true });
|
|
215
|
+
const pagesDir = path.join(designDir, 'pages');
|
|
216
|
+
if (!fs.existsSync(pagesDir)) fs.mkdirSync(pagesDir, { recursive: true });
|
|
217
|
+
|
|
218
|
+
const type = resolveType(opts, designDir);
|
|
219
|
+
const instruction = input || '';
|
|
220
|
+
const isAutoMode = !!instruction;
|
|
221
|
+
const designLabel = type === 'fix' ? '修复' : isAutoMode ? '自动' : '对话';
|
|
222
|
+
printModeBanner('design', `${type} · ${designLabel}`, config?.model);
|
|
223
|
+
|
|
224
|
+
if (!opts.model || !opts.model.includes('glm-5')) {
|
|
225
|
+
log('info', '提示: design 推荐使用 --model glm-5 获得最佳效果');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (type === 'fix') {
|
|
229
|
+
const penFiles = scanPenFiles(designDir);
|
|
230
|
+
if (penFiles.length === 0) {
|
|
231
|
+
log('warn', '设计目录中没有 .pen 文件需要修复');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const answer = await askUser(`\n发现 ${penFiles.length} 个 .pen 文件,是否进行修复?(Y/n) `);
|
|
235
|
+
if (answer.toLowerCase() === 'n') { log('info', '已取消'); return; }
|
|
236
|
+
await runFixSession(config, designDir, input, opts);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
241
|
+
|
|
242
|
+
const sessionResult = await Session.run('design', config, {
|
|
243
|
+
logFileName: `design_${ts}_1.log`,
|
|
244
|
+
label: isAutoMode ? 'design_auto' : 'design_dialogue',
|
|
245
|
+
async execute(session) {
|
|
246
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
247
|
+
queryOpts.systemPrompt = buildSystemPrompt('design');
|
|
248
|
+
return await session.runQuery(buildDesignPrompt(instruction, designDir), queryOpts);
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (sessionResult && !sessionResult.success) {
|
|
253
|
+
log('warn', 'AI 会话未正常完成,检查生成结果...');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const penCount = showDesignSummary(designDir);
|
|
257
|
+
if (penCount === 0) {
|
|
258
|
+
log('error', 'AI 未生成任何 .pen 文件');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
saveDesignState({ lastTimestamp: new Date().toISOString(), designDir, penCount, type });
|
|
263
|
+
log('ok', `设计完成! 文件: ${penCount}`);
|
|
264
|
+
log('info', '迭代调整: claude-coder design "修改xxx"');
|
|
265
|
+
log('info', '修复文件: claude-coder design --type fix');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { executeDesign };
|
package/src/core/go.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { buildSystemPrompt } = require('./prompts');
|
|
7
|
-
const { log } = require('../common/config');
|
|
7
|
+
const { log, printModeBanner } = require('../common/config');
|
|
8
8
|
const { assets } = require('../common/assets');
|
|
9
9
|
const { extractResultText } = require('../common/logging');
|
|
10
10
|
const { loadState, saveState } = require('./state');
|
|
@@ -184,8 +184,8 @@ async function executeGo(config, input, opts = {}) {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
const isAutoMode = !!(instruction || opts.reqFile);
|
|
187
|
-
const mode = isAutoMode ? '
|
|
188
|
-
|
|
187
|
+
const mode = isAutoMode ? '自动模式' : '对话模式';
|
|
188
|
+
printModeBanner('go', mode, config?.model);
|
|
189
189
|
|
|
190
190
|
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
191
191
|
|
package/src/core/hooks.js
CHANGED
|
@@ -88,8 +88,10 @@ class GuidanceInjector {
|
|
|
88
88
|
|
|
89
89
|
if (condition.field && condition.pattern !== undefined) {
|
|
90
90
|
const value = this.getFieldValue(input, condition.field);
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
let re = ruleName && this._compiledConditions?.get(ruleName);
|
|
92
|
+
if (!re) {
|
|
93
|
+
try { re = new RegExp(condition.pattern, 'i'); } catch { return false; }
|
|
94
|
+
}
|
|
93
95
|
return re.test(String(value || ''));
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -113,28 +115,41 @@ class GuidanceInjector {
|
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
117
|
* Get rule file content
|
|
118
|
+
* Uses assets module for template resolution (supports user overrides + bundled fallback)
|
|
116
119
|
* @param {object|string} file - File config or path string
|
|
117
|
-
* @param {string} basePath - Base directory for relative paths
|
|
118
120
|
*/
|
|
119
|
-
getFileContent(file
|
|
121
|
+
getFileContent(file) {
|
|
120
122
|
if (!file) return null;
|
|
121
123
|
|
|
122
124
|
const filePath = typeof file === 'string' ? file : file.path;
|
|
123
|
-
const absolutePath = path.isAbsolute(filePath)
|
|
124
|
-
? filePath
|
|
125
|
-
: path.join(basePath, filePath);
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
// Check if path matches a known registry entry (e.g., 'webTesting', 'bashProcess')
|
|
127
|
+
// by converting 'assets/web-testing.md' → ['other', 'web-testing.md']
|
|
128
|
+
if (filePath.startsWith('assets/')) {
|
|
129
|
+
const segments = filePath.replace(/^assets\//, '').split('/');
|
|
130
|
+
// Map to template directory structure: assets/web-testing.md → other/web-testing.md
|
|
131
|
+
if (segments.length === 1) {
|
|
132
|
+
segments.unshift('other');
|
|
133
|
+
}
|
|
134
|
+
const resolved = assets._resolveTemplate?.(segments);
|
|
135
|
+
if (resolved) {
|
|
136
|
+
try { return fs.readFileSync(resolved, 'utf8'); } catch { /* fall through */ }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fallback: try direct path resolution
|
|
141
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : assets.path(filePath);
|
|
142
|
+
if (absolutePath) {
|
|
143
|
+
try { return fs.readFileSync(absolutePath, 'utf8'); } catch { /* ignore */ }
|
|
131
144
|
}
|
|
145
|
+
|
|
146
|
+
return null;
|
|
132
147
|
}
|
|
133
148
|
|
|
134
149
|
/**
|
|
135
150
|
* Process a single rule and return guidance content
|
|
136
151
|
*/
|
|
137
|
-
processRule(rule, input
|
|
152
|
+
processRule(rule, input) {
|
|
138
153
|
const matcherRe = this._compiledMatchers?.get(rule.name) ?? new RegExp(rule.matcher);
|
|
139
154
|
if (!matcherRe.test(input.tool_name)) {
|
|
140
155
|
return null;
|
|
@@ -161,7 +176,7 @@ class GuidanceInjector {
|
|
|
161
176
|
// Get cached content or read file
|
|
162
177
|
const cacheKey = `${rule.name}_content`;
|
|
163
178
|
if (!this.cache[cacheKey]) {
|
|
164
|
-
this.cache[cacheKey] = this.getFileContent(fileConfig.path
|
|
179
|
+
this.cache[cacheKey] = this.getFileContent(fileConfig.path);
|
|
165
180
|
}
|
|
166
181
|
result.guidance = this.cache[cacheKey] || '';
|
|
167
182
|
}
|
|
@@ -198,8 +213,6 @@ class GuidanceInjector {
|
|
|
198
213
|
* Create hook function for PreToolUse
|
|
199
214
|
*/
|
|
200
215
|
createHook() {
|
|
201
|
-
const basePath = assets.dir('loop');
|
|
202
|
-
|
|
203
216
|
return async (input, _toolUseID, _context) => {
|
|
204
217
|
this.load();
|
|
205
218
|
|
|
@@ -209,7 +222,7 @@ class GuidanceInjector {
|
|
|
209
222
|
const tipParts = [];
|
|
210
223
|
|
|
211
224
|
for (const rule of this.rules) {
|
|
212
|
-
const result = this.processRule(rule, input
|
|
225
|
+
const result = this.processRule(rule, input);
|
|
213
226
|
if (result) {
|
|
214
227
|
if (result.guidance) guidanceParts.push(result.guidance);
|
|
215
228
|
if (result.tip) tipParts.push(result.tip);
|
|
@@ -394,6 +407,7 @@ const FEATURE_MAP = {
|
|
|
394
407
|
add: [FEATURES.STOP, FEATURES.STALL],
|
|
395
408
|
simplify: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
|
|
396
409
|
go: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
|
|
410
|
+
design: [FEATURES.STOP, FEATURES.STALL, FEATURES.INTERACTION],
|
|
397
411
|
custom: null
|
|
398
412
|
};
|
|
399
413
|
|
package/src/core/init.js
CHANGED
|
@@ -127,6 +127,15 @@ async function executeInit(config, opts = {}) {
|
|
|
127
127
|
throw new Error('project_profile.json 读取失败或已损坏');
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
const reqDest = path.join(projectRoot, 'requirements.md');
|
|
131
|
+
if (!fs.existsSync(reqDest)) {
|
|
132
|
+
const reqTemplate = assets.read('requirements');
|
|
133
|
+
if (reqTemplate) {
|
|
134
|
+
fs.writeFileSync(reqDest, reqTemplate, 'utf8');
|
|
135
|
+
log('ok', '已生成 requirements.md(需求模板)');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
if (opts.deployTemplates) {
|
|
131
140
|
for (const file of assets.deployAll()) log('ok', `已部署 → .claude-coder/assets/${file}`);
|
|
132
141
|
const recipes = assets.deployRecipes();
|
package/src/core/plan.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { buildSystemPrompt, buildPlanPrompt } = require('./prompts');
|
|
8
|
-
const { log } = require('../common/config');
|
|
8
|
+
const { log, printModeBanner } = require('../common/config');
|
|
9
9
|
const { assets } = require('../common/assets');
|
|
10
10
|
const { printStats } = require('../common/tasks');
|
|
11
11
|
const { syncAfterPlan } = require('./state');
|
|
@@ -26,7 +26,11 @@ function buildPlanOnlySystem(opts = {}) {
|
|
|
26
26
|
2. ${interactionRule}
|
|
27
27
|
3. 使用 Write 工具将完整计划写入 ~/.claude/plans/ 目录(.md 格式)
|
|
28
28
|
4. 写入后输出标记(独占一行):PLAN_FILE_PATH: <计划文件绝对路径>
|
|
29
|
-
5.
|
|
29
|
+
5. 简要总结计划要点
|
|
30
|
+
|
|
31
|
+
【关键文件】
|
|
32
|
+
- \`.claude-coder/project_profile.json\` — 项目元数据
|
|
33
|
+
- \`.claude-coder/design/\` — UI 设计稿目录(design_map.json 索引 + .pen 设计文件),存在时应在方案中参考`;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
function buildPlanOnlyPrompt(instruction, opts = {}) {
|
|
@@ -97,17 +101,19 @@ async function _executePlanGen(session, instruction, opts = {}) {
|
|
|
97
101
|
return { success: false, reason: 'no_path', targetPath: null };
|
|
98
102
|
}
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
if (!process.stdin.isTTY) return
|
|
104
|
+
function _askLine(question) {
|
|
105
|
+
if (!process.stdin.isTTY) return Promise.resolve('');
|
|
102
106
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
103
107
|
return new Promise(resolve => {
|
|
104
|
-
rl.question(
|
|
105
|
-
rl.close();
|
|
106
|
-
resolve(/^[Yy]/.test(answer.trim()));
|
|
107
|
-
});
|
|
108
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
async function promptAutoRun() {
|
|
113
|
+
const answer = await _askLine('任务分解完成后是否自动开始执行?(y/n) ');
|
|
114
|
+
return /^[Yy]/.test(answer);
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
// ─── Main Entry ──────────────────────────────────────────
|
|
112
118
|
|
|
113
119
|
async function executePlan(config, input, opts = {}) {
|
|
@@ -123,9 +129,8 @@ async function executePlan(config, input, opts = {}) {
|
|
|
123
129
|
throw new Error('用法: claude-coder plan "需求内容" 或 claude-coder plan -r [requirements.md]');
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
132
|
+
const modeLabel = opts.planOnly ? 'planOnly' : opts.interactive ? '交互模式' : '自动模式';
|
|
133
|
+
printModeBanner('plan', modeLabel, config?.model);
|
|
129
134
|
|
|
130
135
|
let shouldAutoRun = false;
|
|
131
136
|
if (!opts.planOnly) {
|
|
@@ -146,7 +151,7 @@ async function executePlan(config, input, opts = {}) {
|
|
|
146
151
|
const planResult = await _executePlanGen(session, instruction, opts);
|
|
147
152
|
|
|
148
153
|
if (!planResult.success) {
|
|
149
|
-
log('error', `\n计划生成失败: ${planResult.reason
|
|
154
|
+
log('error', `\n计划生成失败: ${planResult.reason}`);
|
|
150
155
|
return { success: false, reason: planResult.reason };
|
|
151
156
|
}
|
|
152
157
|
|
|
@@ -162,9 +167,10 @@ async function executePlan(config, input, opts = {}) {
|
|
|
162
167
|
const queryOpts = session.buildQueryOptions(opts);
|
|
163
168
|
queryOpts.systemPrompt = buildSystemPrompt('plan');
|
|
164
169
|
|
|
165
|
-
const { success } = await session.runQuery(tasksPrompt, queryOpts);
|
|
170
|
+
const { success } = await session.runQuery(tasksPrompt, queryOpts, { continue: true });
|
|
166
171
|
if (!success) {
|
|
167
172
|
log('warn', '任务分解查询未正常结束');
|
|
173
|
+
return { success: false, reason: '任务分解未正常完成' };
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
syncAfterPlan();
|
|
@@ -178,9 +184,9 @@ async function executePlan(config, input, opts = {}) {
|
|
|
178
184
|
|
|
179
185
|
if (shouldAutoRun) {
|
|
180
186
|
console.log('');
|
|
181
|
-
log('info', '
|
|
187
|
+
log('info', '开始自动执行任务(沿用会话上下文)...');
|
|
182
188
|
const { executeRun } = require('./runner');
|
|
183
|
-
await executeRun(config, opts);
|
|
189
|
+
await executeRun(config, { ...opts });
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
192
|
}
|
package/src/core/prompts.js
CHANGED
|
@@ -17,6 +17,12 @@ function buildSystemPrompt(type) {
|
|
|
17
17
|
case 'coding': specific = assets.read('codingSystem') || ''; break;
|
|
18
18
|
case 'plan': specific = assets.read('planSystem') || ''; break;
|
|
19
19
|
case 'go': specific = assets.read('goSystem') || ''; break;
|
|
20
|
+
case 'design': specific = assets.read('designSystem') || ''; break;
|
|
21
|
+
case 'designFix': specific = assets.read('designFixSystem') || ''; break;
|
|
22
|
+
}
|
|
23
|
+
if (type === 'design' || type === 'designFix') {
|
|
24
|
+
const base = assets.read('designBase') || '';
|
|
25
|
+
return base ? `${specific}\n\n---\n\n${base}` : specific;
|
|
20
26
|
}
|
|
21
27
|
return specific ? `${specific}\n\n${core}` : core;
|
|
22
28
|
}
|
|
@@ -156,6 +162,27 @@ function buildServiceHint(maxSessions) {
|
|
|
156
162
|
: '连续模式:收尾时不要停止后台服务,保持服务运行以便下个 session 继续使用。';
|
|
157
163
|
}
|
|
158
164
|
|
|
165
|
+
// --------------- Design Hint ---------------
|
|
166
|
+
|
|
167
|
+
function buildDesignHint(task) {
|
|
168
|
+
if (!needsWebTools(task)) return '';
|
|
169
|
+
if (!assets.exists('designMap')) return '';
|
|
170
|
+
|
|
171
|
+
const map = assets.readJson('designMap', null);
|
|
172
|
+
if (!map || !map.pages) return '';
|
|
173
|
+
|
|
174
|
+
const pages = Object.entries(map.pages);
|
|
175
|
+
if (pages.length === 0) return '';
|
|
176
|
+
|
|
177
|
+
const designDir = assets.dir('design');
|
|
178
|
+
let hint = `【设计稿参考】项目已有 UI 设计稿(design_map.json: ${assets.path('designMap')}):\n`;
|
|
179
|
+
for (const [name, info] of pages) {
|
|
180
|
+
hint += ` - ${name}: ${info.description || ''} → ${path.join(designDir, info.pen)}\n`;
|
|
181
|
+
}
|
|
182
|
+
hint += '涉及 UI 的任务可先 Read 对应 .pen 文件了解设计意图。';
|
|
183
|
+
return hint;
|
|
184
|
+
}
|
|
185
|
+
|
|
159
186
|
// --------------- Context Builders ---------------
|
|
160
187
|
|
|
161
188
|
function _resolveTask(taskId) {
|
|
@@ -187,6 +214,7 @@ function buildCodingContext(sessionNum, opts = {}) {
|
|
|
187
214
|
webTestHint: buildWebTestHint(config, task),
|
|
188
215
|
memoryHint: buildMemoryHint(),
|
|
189
216
|
serviceHint: buildServiceHint(opts.maxSessions || 50),
|
|
217
|
+
designHint: buildDesignHint(task),
|
|
190
218
|
});
|
|
191
219
|
}
|
|
192
220
|
|
|
@@ -223,17 +251,34 @@ function buildPlanPrompt(planPath) {
|
|
|
223
251
|
|
|
224
252
|
let testRuleHint = '';
|
|
225
253
|
if (assets.exists('testRule') && assets.exists('mcpConfig')) {
|
|
254
|
+
const testRulePath = assets.path('testRule');
|
|
226
255
|
testRuleHint = '【浏览器测试规则】项目已配置浏览器测试工具(.mcp.json),' +
|
|
227
|
-
|
|
228
|
-
|
|
256
|
+
`\`${testRulePath}\` 包含测试规范(Smart Snapshot、等待策略、步骤模板等)。` +
|
|
257
|
+
`前端页面 test 类任务 steps 首步加入 \`【规则】阅读 ${testRulePath}\`。`;
|
|
229
258
|
}
|
|
230
259
|
|
|
260
|
+
let designHint = '';
|
|
261
|
+
try {
|
|
262
|
+
if (assets.exists('designMap')) {
|
|
263
|
+
const map = assets.readJson('designMap', null);
|
|
264
|
+
if (map?.pages && Object.keys(map.pages).length > 0) {
|
|
265
|
+
const designDir = assets.dir('design');
|
|
266
|
+
designHint = `【设计稿】项目已有 UI 设计稿(${assets.path('designMap')}):\n`;
|
|
267
|
+
for (const [name, info] of Object.entries(map.pages)) {
|
|
268
|
+
designHint += ` - ${name}: ${info.description || ''} → ${path.join(designDir, info.pen)}\n`;
|
|
269
|
+
}
|
|
270
|
+
designHint += '涉及 UI 的 task steps 应包含「Read 对应 .pen 设计稿」步骤。';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch { /* ignore */ }
|
|
274
|
+
|
|
231
275
|
return assets.render('planUser', {
|
|
232
276
|
taskContext,
|
|
233
277
|
recentExamples,
|
|
234
278
|
projectRoot,
|
|
235
279
|
planPath,
|
|
236
280
|
testRuleHint,
|
|
281
|
+
designHint,
|
|
237
282
|
});
|
|
238
283
|
}
|
|
239
284
|
|
package/src/core/repair.js
CHANGED
|
@@ -23,7 +23,7 @@ async function executeRepair(config, filePath, opts = {}) {
|
|
|
23
23
|
|
|
24
24
|
async execute(session) {
|
|
25
25
|
const queryOpts = session.buildQueryOptions(opts);
|
|
26
|
-
await session.runQuery(prompt, queryOpts);
|
|
26
|
+
await session.runQuery(prompt, queryOpts, { continue: true });
|
|
27
27
|
log('ok', `AI 修复 ${fileName} 完成`);
|
|
28
28
|
return {};
|
|
29
29
|
},
|