claude-coder 1.8.4 → 1.9.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 +59 -12
- package/bin/cli.js +20 -37
- package/package.json +3 -1
- package/src/commands/auth.js +87 -15
- package/src/commands/setup-modules/helpers.js +4 -3
- package/src/commands/setup-modules/mcp.js +44 -24
- package/src/commands/setup-modules/safety.js +1 -15
- package/src/commands/setup.js +8 -8
- package/src/common/assets.js +10 -1
- package/src/common/config.js +2 -2
- package/src/common/indicator.js +158 -120
- package/src/common/utils.js +60 -8
- package/src/core/coding.js +16 -38
- package/src/core/go.js +31 -77
- package/src/core/hooks.js +56 -89
- package/src/core/init.js +94 -100
- package/src/core/plan.js +85 -223
- package/src/core/prompts.js +36 -16
- package/src/core/repair.js +7 -17
- package/src/core/runner.js +306 -43
- package/src/core/scan.js +38 -34
- package/src/core/session.js +253 -39
- package/src/core/simplify.js +45 -24
- package/src/core/state.js +105 -0
- package/src/index.js +76 -0
- package/templates/codingSystem.md +2 -2
- package/templates/codingUser.md +1 -1
- package/templates/guidance.json +22 -3
- package/templates/planSystem.md +2 -2
- package/templates/scanSystem.md +3 -3
- package/templates/scanUser.md +1 -1
- package/templates/web-testing.md +17 -0
- package/types/index.d.ts +217 -0
- package/src/core/context.js +0 -117
- package/src/core/harness.js +0 -484
- package/src/core/query.js +0 -50
- package/templates/playwright.md +0 -17
package/src/core/init.js
CHANGED
|
@@ -6,15 +6,27 @@ const http = require('http');
|
|
|
6
6
|
const { spawn, execSync } = require('child_process');
|
|
7
7
|
const { log } = require('../common/config');
|
|
8
8
|
const { assets } = require('../common/assets');
|
|
9
|
-
const {
|
|
9
|
+
const { isGitRepo, ensureGitignore } = require('../common/utils');
|
|
10
|
+
|
|
11
|
+
function ensureEnvironment(projectRoot) {
|
|
12
|
+
ensureGitignore(projectRoot);
|
|
13
|
+
if (!isGitRepo(projectRoot)) {
|
|
14
|
+
log('info', '初始化 git 仓库...');
|
|
15
|
+
execSync('git init', { cwd: projectRoot, stdio: 'inherit' });
|
|
16
|
+
execSync('git add -A && git commit -m "init: 项目初始化" --allow-empty', {
|
|
17
|
+
cwd: projectRoot,
|
|
18
|
+
stdio: 'inherit',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
function runCmd(cmd, cwd) {
|
|
24
|
+
try {
|
|
25
|
+
execSync(cmd, { cwd: cwd || assets.projectRoot, stdio: 'inherit', shell: true });
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
16
29
|
}
|
|
17
|
-
return data;
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
function isPortFree(port) {
|
|
@@ -31,9 +43,7 @@ function waitForHealth(url, timeoutMs = 15000) {
|
|
|
31
43
|
return new Promise(resolve => {
|
|
32
44
|
const check = () => {
|
|
33
45
|
if (Date.now() - start > timeoutMs) { resolve(false); return; }
|
|
34
|
-
const req = http.get(url, res => {
|
|
35
|
-
resolve(res.statusCode < 500);
|
|
36
|
-
});
|
|
46
|
+
const req = http.get(url, res => { resolve(res.statusCode < 500); });
|
|
37
47
|
req.on('error', () => setTimeout(check, 1000));
|
|
38
48
|
req.setTimeout(3000, () => { req.destroy(); setTimeout(check, 1000); });
|
|
39
49
|
};
|
|
@@ -41,116 +51,104 @@ function waitForHealth(url, timeoutMs = 15000) {
|
|
|
41
51
|
});
|
|
42
52
|
}
|
|
43
53
|
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
function buildEnvSteps(profile, projectRoot) {
|
|
55
|
+
const steps = [];
|
|
56
|
+
const envSetup = profile.env_setup || {};
|
|
57
|
+
|
|
58
|
+
if (envSetup.python_env && !['system', 'none'].includes(envSetup.python_env)) {
|
|
59
|
+
if (envSetup.python_env.startsWith('conda:')) {
|
|
60
|
+
const name = envSetup.python_env.slice(6);
|
|
61
|
+
steps.push({ label: `Python 环境: conda activate ${name}`, cmd: `conda activate ${name}` });
|
|
62
|
+
} else if (envSetup.python_env === 'venv') {
|
|
63
|
+
steps.push({ label: 'Python 环境: venv', cmd: 'source .venv/bin/activate || .venv\\Scripts\\activate' });
|
|
64
|
+
}
|
|
50
65
|
}
|
|
51
|
-
}
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
if (envSetup.node_version && envSetup.node_version !== 'none') {
|
|
68
|
+
steps.push({ label: `Node.js: v${envSetup.node_version}`, cmd: `nvm use ${envSetup.node_version}` });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const pkgManagers = profile.tech_stack?.package_managers || [];
|
|
72
|
+
for (const pm of pkgManagers) {
|
|
73
|
+
if (['npm', 'yarn', 'pnpm'].includes(pm)) {
|
|
74
|
+
if (fs.existsSync(`${projectRoot}/node_modules`)) {
|
|
75
|
+
steps.push({ label: `${pm} 依赖已安装,跳过`, skip: true });
|
|
76
|
+
} else {
|
|
77
|
+
steps.push({ label: `安装依赖: ${pm} install`, cmd: `${pm} install`, cwd: projectRoot });
|
|
78
|
+
}
|
|
79
|
+
} else if (pm === 'pip' && fs.existsSync(`${projectRoot}/requirements.txt`)) {
|
|
80
|
+
steps.push({ label: '安装依赖: pip install -r requirements.txt', cmd: 'pip install -r requirements.txt', cwd: projectRoot });
|
|
58
81
|
}
|
|
59
82
|
}
|
|
83
|
+
|
|
84
|
+
for (const cmd of (profile.custom_init || [])) {
|
|
85
|
+
steps.push({ label: `自定义: ${cmd}`, cmd, cwd: projectRoot });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return steps;
|
|
60
89
|
}
|
|
61
90
|
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
log('ok',
|
|
91
|
+
async function startService(svc, projectRoot, stepNum) {
|
|
92
|
+
const free = await isPortFree(svc.port);
|
|
93
|
+
if (!free) {
|
|
94
|
+
log('ok', `[${stepNum}] ${svc.name} 已在端口 ${svc.port} 运行,跳过`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log('info', `[${stepNum}] 启动 ${svc.name} (端口 ${svc.port})...`);
|
|
99
|
+
const cwd = svc.cwd ? `${projectRoot}/${svc.cwd}` : projectRoot;
|
|
100
|
+
const child = spawn(svc.command, { cwd, shell: true, detached: true, stdio: 'ignore' });
|
|
101
|
+
child.unref();
|
|
102
|
+
|
|
103
|
+
if (svc.health_check) {
|
|
104
|
+
const healthy = await waitForHealth(svc.health_check);
|
|
105
|
+
log(healthy ? 'ok' : 'warn',
|
|
106
|
+
healthy ? `${svc.name} 就绪: ${svc.health_check}` : `${svc.name} 健康检查超时 (${svc.health_check}),继续执行`);
|
|
66
107
|
}
|
|
67
108
|
}
|
|
68
109
|
|
|
69
|
-
async function
|
|
70
|
-
assets.ensureDirs();
|
|
110
|
+
async function executeInit(config, opts = {}) {
|
|
71
111
|
const projectRoot = assets.projectRoot;
|
|
72
112
|
|
|
113
|
+
ensureEnvironment(projectRoot);
|
|
114
|
+
|
|
73
115
|
if (!assets.exists('profile')) {
|
|
74
116
|
log('info', 'profile 不存在,正在执行项目扫描...');
|
|
75
|
-
const
|
|
117
|
+
const { executeScan } = require('./scan');
|
|
118
|
+
const scanResult = await executeScan(config, opts);
|
|
76
119
|
if (!scanResult.success) {
|
|
77
|
-
|
|
78
|
-
process.exit(1);
|
|
120
|
+
throw new Error('项目扫描失败');
|
|
79
121
|
}
|
|
80
122
|
}
|
|
81
123
|
|
|
82
|
-
const profile =
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
deployAssets();
|
|
86
|
-
deployRecipes();
|
|
87
|
-
|
|
88
|
-
const envSetup = profile.env_setup || {};
|
|
89
|
-
if (envSetup.python_env && envSetup.python_env !== 'system' && envSetup.python_env !== 'none') {
|
|
90
|
-
stepCount++;
|
|
91
|
-
if (envSetup.python_env.startsWith('conda:')) {
|
|
92
|
-
const envName = envSetup.python_env.slice(6);
|
|
93
|
-
log('info', `[${stepCount}] Python 环境: conda activate ${envName}`);
|
|
94
|
-
runCmd(`conda activate ${envName}`);
|
|
95
|
-
} else if (envSetup.python_env === 'venv') {
|
|
96
|
-
log('info', `[${stepCount}] Python 环境: venv`);
|
|
97
|
-
runCmd('source .venv/bin/activate || .venv\\Scripts\\activate');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (envSetup.node_version && envSetup.node_version !== 'none') {
|
|
101
|
-
stepCount++;
|
|
102
|
-
log('info', `[${stepCount}] Node.js: v${envSetup.node_version}`);
|
|
103
|
-
runCmd(`nvm use ${envSetup.node_version}`);
|
|
124
|
+
const profile = assets.readJson('profile', null);
|
|
125
|
+
if (!profile) {
|
|
126
|
+
throw new Error('project_profile.json 读取失败或已损坏');
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
if (fs.existsSync(`${projectRoot}/node_modules`)) {
|
|
111
|
-
log('ok', `[${stepCount}] ${pm} 依赖已安装,跳过`);
|
|
112
|
-
} else {
|
|
113
|
-
log('info', `[${stepCount}] 安装依赖: ${pm} install`);
|
|
114
|
-
runCmd(`${pm} install`, projectRoot);
|
|
115
|
-
}
|
|
116
|
-
} else if (pm === 'pip') {
|
|
117
|
-
const reqFile = fs.existsSync(`${projectRoot}/requirements.txt`);
|
|
118
|
-
if (reqFile) {
|
|
119
|
-
log('info', `[${stepCount}] 安装依赖: pip install -r requirements.txt`);
|
|
120
|
-
runCmd('pip install -r requirements.txt', projectRoot);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
129
|
+
if (opts.deployTemplates) {
|
|
130
|
+
for (const file of assets.deployAll()) log('ok', `已部署 → .claude-coder/assets/${file}`);
|
|
131
|
+
const recipes = assets.deployRecipes();
|
|
132
|
+
if (recipes.length > 0) log('ok', `已部署 ${recipes.length} 个食谱文件 → .claude-coder/recipes/`);
|
|
123
133
|
}
|
|
124
134
|
|
|
125
|
-
const
|
|
126
|
-
|
|
135
|
+
const envSteps = buildEnvSteps(profile, projectRoot);
|
|
136
|
+
let stepCount = 0;
|
|
137
|
+
|
|
138
|
+
for (const step of envSteps) {
|
|
127
139
|
stepCount++;
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
if (step.skip) {
|
|
141
|
+
log('ok', `[${stepCount}] ${step.label}`);
|
|
142
|
+
} else {
|
|
143
|
+
log('info', `[${stepCount}] ${step.label}`);
|
|
144
|
+
runCmd(step.cmd, step.cwd);
|
|
145
|
+
}
|
|
130
146
|
}
|
|
131
147
|
|
|
132
148
|
const services = profile.services || [];
|
|
133
149
|
for (const svc of services) {
|
|
134
150
|
stepCount++;
|
|
135
|
-
|
|
136
|
-
if (!free) {
|
|
137
|
-
log('ok', `[${stepCount}] ${svc.name} 已在端口 ${svc.port} 运行,跳过`);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
log('info', `[${stepCount}] 启动 ${svc.name} (端口 ${svc.port})...`);
|
|
142
|
-
const cwd = svc.cwd ? `${projectRoot}/${svc.cwd}` : projectRoot;
|
|
143
|
-
const child = spawn(svc.command, { cwd, shell: true, detached: true, stdio: 'ignore' });
|
|
144
|
-
child.unref();
|
|
145
|
-
|
|
146
|
-
if (svc.health_check) {
|
|
147
|
-
const healthy = await waitForHealth(svc.health_check);
|
|
148
|
-
if (healthy) {
|
|
149
|
-
log('ok', `${svc.name} 就绪: ${svc.health_check}`);
|
|
150
|
-
} else {
|
|
151
|
-
log('warn', `${svc.name} 健康检查超时 (${svc.health_check}),继续执行`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
151
|
+
await startService(svc, projectRoot, stepCount);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
if (stepCount === 0) {
|
|
@@ -159,13 +157,9 @@ async function init() {
|
|
|
159
157
|
log('ok', `初始化完成 (${stepCount} 步)`);
|
|
160
158
|
}
|
|
161
159
|
|
|
162
|
-
|
|
163
|
-
console.log(
|
|
164
|
-
for (const svc of services) {
|
|
165
|
-
console.log(` ${svc.name}: http://localhost:${svc.port}`);
|
|
166
|
-
}
|
|
167
|
-
console.log('');
|
|
160
|
+
for (const svc of services) {
|
|
161
|
+
console.log(` ${svc.name}: http://localhost:${svc.port}`);
|
|
168
162
|
}
|
|
169
163
|
}
|
|
170
164
|
|
|
171
|
-
module.exports = {
|
|
165
|
+
module.exports = { executeInit };
|
package/src/core/plan.js
CHANGED
|
@@ -4,126 +4,36 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const readline = require('readline');
|
|
7
|
-
const { runSession } = require('./session');
|
|
8
|
-
const { buildQueryOptions } = require('./query');
|
|
9
7
|
const { buildSystemPrompt, buildPlanPrompt } = require('./prompts');
|
|
10
|
-
const { log
|
|
8
|
+
const { log } = require('../common/config');
|
|
11
9
|
const { assets } = require('../common/assets');
|
|
12
|
-
const { extractResultText } = require('../common/logging');
|
|
13
10
|
const { printStats } = require('../common/tasks');
|
|
14
|
-
const { syncAfterPlan } = require('./
|
|
11
|
+
const { syncAfterPlan } = require('./state');
|
|
12
|
+
const { Session } = require('./session');
|
|
15
13
|
|
|
16
|
-
const EXIT_TIMEOUT_MS = 300000;
|
|
17
14
|
const PLANS_DIR = path.join(os.homedir(), '.claude', 'plans');
|
|
18
15
|
|
|
19
|
-
function
|
|
16
|
+
function buildPlanOnlySystem(opts = {}) {
|
|
20
17
|
const interactive = opts.interactive || false;
|
|
21
|
-
const reqFile = opts.reqFile || null;
|
|
22
|
-
|
|
23
|
-
const inputSection = reqFile
|
|
24
|
-
? `需求文件路径: ${reqFile}\n先读取该文件,理解用户需求和约束。`
|
|
25
|
-
: `用户需求:\n${instruction}`;
|
|
26
|
-
|
|
27
18
|
const interactionRule = interactive
|
|
28
|
-
? '
|
|
19
|
+
? '如有不确定的关键决策点,使用 AskUserQuestion 工具向用户提问,对话确认方案。'
|
|
29
20
|
: '不要提问,默认使用最佳推荐方案。';
|
|
30
21
|
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
${inputSection}
|
|
22
|
+
return `你是一个资深技术架构师。根据用户需求,探索项目代码库后输出完整的技术方案文档。
|
|
34
23
|
|
|
35
24
|
【流程】
|
|
36
25
|
1. 探索项目代码库,理解结构和技术栈
|
|
37
26
|
2. ${interactionRule}
|
|
38
27
|
3. 使用 Write 工具将完整计划写入 ~/.claude/plans/ 目录(.md 格式)
|
|
39
28
|
4. 写入后输出标记(独占一行):PLAN_FILE_PATH: <计划文件绝对路径>
|
|
40
|
-
5.
|
|
41
|
-
`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 从文本中提取计划文件路径
|
|
46
|
-
* 优先级:PLAN_FILE_PATH 标记 > .claude/plans/*.md > 反引号包裹 .md > 任意绝对 .md
|
|
47
|
-
*/
|
|
48
|
-
function extractPlanPath(text) {
|
|
49
|
-
if (!text) return null;
|
|
50
|
-
|
|
51
|
-
const tagMatch = text.match(/PLAN_FILE_PATH:\s*(\S+\.md)/);
|
|
52
|
-
if (tagMatch) return tagMatch[1];
|
|
53
|
-
|
|
54
|
-
const plansMatch = text.match(/([^\s`'"(]*\.claude\/plans\/[^\s`'"()]+\.md)/);
|
|
55
|
-
if (plansMatch) return plansMatch[1];
|
|
56
|
-
|
|
57
|
-
const backtickMatch = text.match(/`([^`]+\.md)`/);
|
|
58
|
-
if (backtickMatch) return backtickMatch[1];
|
|
59
|
-
|
|
60
|
-
const absMatch = text.match(/(\/[^\s`'"]+\.md)/);
|
|
61
|
-
if (absMatch) return absMatch[1];
|
|
62
|
-
|
|
63
|
-
return null;
|
|
29
|
+
5. 简要总结计划要点`;
|
|
64
30
|
}
|
|
65
31
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* 4. plans 目录最新文件(兜底)
|
|
72
|
-
*/
|
|
73
|
-
function extractPlanPathFromCollected(collected, startTime) {
|
|
74
|
-
// 第一层:从 Write 工具调用参数中直接获取
|
|
75
|
-
for (const msg of collected) {
|
|
76
|
-
if (msg.type !== 'assistant' || !msg.message?.content) continue;
|
|
77
|
-
for (const block of msg.message.content) {
|
|
78
|
-
if (block.type === 'tool_use' && block.name === 'Write') {
|
|
79
|
-
const target = block.input?.file_path || block.input?.path || '';
|
|
80
|
-
if (target.includes('.claude/plans/') && target.endsWith('.md')) {
|
|
81
|
-
if (fs.existsSync(target)) return target;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 第二层:从所有 assistant 文本中提取
|
|
88
|
-
let fullText = '';
|
|
89
|
-
for (const msg of collected) {
|
|
90
|
-
if (msg.type === 'assistant' && msg.message?.content) {
|
|
91
|
-
for (const block of msg.message.content) {
|
|
92
|
-
if (block.type === 'text' && block.text) fullText += block.text;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if (fullText) {
|
|
97
|
-
const p = extractPlanPath(fullText);
|
|
98
|
-
if (p && fs.existsSync(p)) return p;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 第三层:从 result.result 中提取
|
|
102
|
-
const resultText = extractResultText(collected);
|
|
103
|
-
if (resultText) {
|
|
104
|
-
const p = extractPlanPath(resultText);
|
|
105
|
-
if (p && fs.existsSync(p)) return p;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 第四层:扫描 plans 目录,找 session 期间新建的文件
|
|
109
|
-
if (fs.existsSync(PLANS_DIR)) {
|
|
110
|
-
try {
|
|
111
|
-
const files = fs.readdirSync(PLANS_DIR)
|
|
112
|
-
.filter(f => f.endsWith('.md'))
|
|
113
|
-
.map(f => {
|
|
114
|
-
const fp = path.join(PLANS_DIR, f);
|
|
115
|
-
return { path: fp, mtime: fs.statSync(fp).mtimeMs };
|
|
116
|
-
})
|
|
117
|
-
.filter(f => f.mtime >= startTime)
|
|
118
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
119
|
-
if (files.length > 0) {
|
|
120
|
-
log('info', `从 plans 目录发现新文件: ${path.basename(files[0].path)}`);
|
|
121
|
-
return files[0].path;
|
|
122
|
-
}
|
|
123
|
-
} catch { /* ignore */ }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return null;
|
|
32
|
+
function buildPlanOnlyPrompt(instruction, opts = {}) {
|
|
33
|
+
const reqFile = opts.reqFile || null;
|
|
34
|
+
return reqFile
|
|
35
|
+
? `需求文件路径: ${reqFile}\n先读取该文件,理解用户需求和约束。`
|
|
36
|
+
: instruction;
|
|
127
37
|
}
|
|
128
38
|
|
|
129
39
|
function copyPlanToProject(generatedPath) {
|
|
@@ -142,57 +52,43 @@ function copyPlanToProject(generatedPath) {
|
|
|
142
52
|
}
|
|
143
53
|
}
|
|
144
54
|
|
|
145
|
-
async function _executePlanGen(
|
|
55
|
+
async function _executePlanGen(session, instruction, opts = {}) {
|
|
56
|
+
const interactive = opts.interactive || false;
|
|
146
57
|
const prompt = buildPlanOnlyPrompt(instruction, opts);
|
|
147
58
|
const queryOpts = {
|
|
148
59
|
permissionMode: 'plan',
|
|
60
|
+
systemPrompt: buildPlanOnlySystem(opts),
|
|
149
61
|
cwd: opts.projectRoot || assets.projectRoot,
|
|
150
|
-
hooks:
|
|
62
|
+
hooks: session.hooks,
|
|
151
63
|
};
|
|
152
|
-
if (!
|
|
64
|
+
if (!interactive) {
|
|
153
65
|
queryOpts.disallowedTools = ['askUserQuestion'];
|
|
154
66
|
}
|
|
155
67
|
if (opts.model) queryOpts.model = opts.model;
|
|
156
68
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (exitPlanModeDetected && exitPlanModeTime) {
|
|
171
|
-
const elapsed = Date.now() - exitPlanModeTime;
|
|
172
|
-
if (elapsed > EXIT_TIMEOUT_MS && msg.type !== 'result') {
|
|
173
|
-
log('warn', '检测到 ExitPlanMode,等待审批超时,尝试从已收集消息中提取路径');
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
collected.push(msg);
|
|
179
|
-
ctx._logMessage(msg);
|
|
180
|
-
|
|
181
|
-
if (msg.type === 'assistant' && msg.message?.content) {
|
|
182
|
-
for (const block of msg.message.content) {
|
|
183
|
-
if (block.type === 'tool_use' && block.name === 'ExitPlanMode') {
|
|
184
|
-
exitPlanModeDetected = true;
|
|
185
|
-
exitPlanModeTime = Date.now();
|
|
69
|
+
let capturedPlanPath = null;
|
|
70
|
+
|
|
71
|
+
const { success } = await session.runQuery(prompt, queryOpts, {
|
|
72
|
+
onMessage(message) {
|
|
73
|
+
if (message.type !== 'assistant' || !message.message?.content) return;
|
|
74
|
+
for (const block of message.message.content) {
|
|
75
|
+
if (block.type === 'tool_use' && block.name === 'Write') {
|
|
76
|
+
const target = block.input?.file_path || block.input?.path || '';
|
|
77
|
+
if (target.includes('.claude/plans/') && target.endsWith('.md')) {
|
|
78
|
+
capturedPlanPath = target;
|
|
79
|
+
}
|
|
186
80
|
}
|
|
187
81
|
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
82
|
+
},
|
|
83
|
+
});
|
|
190
84
|
|
|
191
|
-
|
|
85
|
+
if (!success) {
|
|
86
|
+
log('warn', '计划生成查询未正常结束');
|
|
87
|
+
}
|
|
192
88
|
|
|
193
|
-
if (
|
|
194
|
-
const targetPath = copyPlanToProject(
|
|
195
|
-
return { success: true, targetPath, generatedPath:
|
|
89
|
+
if (capturedPlanPath && fs.existsSync(capturedPlanPath)) {
|
|
90
|
+
const targetPath = copyPlanToProject(capturedPlanPath);
|
|
91
|
+
return { success: true, targetPath, generatedPath: capturedPlanPath };
|
|
196
92
|
}
|
|
197
93
|
|
|
198
94
|
log('warn', '无法从输出中提取计划路径');
|
|
@@ -200,23 +96,53 @@ async function _executePlanGen(sdk, ctx, instruction, opts = {}) {
|
|
|
200
96
|
return { success: false, reason: 'no_path', targetPath: null };
|
|
201
97
|
}
|
|
202
98
|
|
|
203
|
-
async function
|
|
204
|
-
|
|
205
|
-
const
|
|
99
|
+
async function promptAutoRun() {
|
|
100
|
+
if (!process.stdin.isTTY) return false;
|
|
101
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
102
|
+
return new Promise(resolve => {
|
|
103
|
+
rl.question('任务分解完成后是否自动开始执行?(y/n) ', answer => {
|
|
104
|
+
rl.close();
|
|
105
|
+
resolve(/^[Yy]/.test(answer.trim()));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Main Entry ──────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
async function executePlan(config, input, opts = {}) {
|
|
113
|
+
const instruction = input || '';
|
|
114
|
+
|
|
115
|
+
if (opts.reqFile && instruction) {
|
|
116
|
+
log('info', `-r 模式下忽略文本输入,使用需求文件: ${opts.reqFile}`);
|
|
117
|
+
} else if (opts.reqFile) {
|
|
118
|
+
console.log(`需求文件: ${opts.reqFile}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!instruction && !opts.reqFile) {
|
|
122
|
+
throw new Error('用法: claude-coder plan "需求内容" 或 claude-coder plan -r [requirements.md]');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (opts.interactive) {
|
|
126
|
+
log('info', '交互模式已启用,模型可能会向您提问');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let shouldAutoRun = false;
|
|
130
|
+
if (!opts.planOnly) {
|
|
131
|
+
shouldAutoRun = await promptAutoRun();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const hookType = opts.interactive ? 'plan_interactive' : 'plan';
|
|
206
135
|
const ts = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 12);
|
|
207
|
-
const label = planOnly ? 'plan_only' : 'plan_tasks';
|
|
208
|
-
const hookType = interactive ? 'plan_interactive' : 'plan';
|
|
136
|
+
const label = opts.planOnly ? 'plan_only' : 'plan_tasks';
|
|
209
137
|
|
|
210
|
-
|
|
211
|
-
opts,
|
|
212
|
-
sessionNum: 0,
|
|
138
|
+
const result = await Session.run(hookType, config, {
|
|
213
139
|
logFileName: `plan_${ts}.log`,
|
|
214
140
|
label,
|
|
215
141
|
|
|
216
|
-
async execute(
|
|
142
|
+
async execute(session) {
|
|
217
143
|
log('info', '正在生成计划方案...');
|
|
218
144
|
|
|
219
|
-
const planResult = await _executePlanGen(
|
|
145
|
+
const planResult = await _executePlanGen(session, instruction, opts);
|
|
220
146
|
|
|
221
147
|
if (!planResult.success) {
|
|
222
148
|
log('error', `\n计划生成失败: ${planResult.reason || planResult.error}`);
|
|
@@ -225,90 +151,26 @@ async function runPlanSession(instruction, opts = {}) {
|
|
|
225
151
|
|
|
226
152
|
log('ok', `\n计划已生成: ${planResult.targetPath}`);
|
|
227
153
|
|
|
228
|
-
if (planOnly) {
|
|
154
|
+
if (opts.planOnly) {
|
|
229
155
|
return { success: true, planPath: planResult.targetPath };
|
|
230
156
|
}
|
|
231
157
|
|
|
232
158
|
log('info', '正在生成任务列表...');
|
|
233
159
|
|
|
234
160
|
const tasksPrompt = buildPlanPrompt(planResult.targetPath);
|
|
235
|
-
const queryOpts = buildQueryOptions(
|
|
161
|
+
const queryOpts = session.buildQueryOptions(opts);
|
|
236
162
|
queryOpts.systemPrompt = buildSystemPrompt('plan');
|
|
237
|
-
queryOpts.hooks = ctx.hooks;
|
|
238
|
-
queryOpts.abortController = ctx.abortController;
|
|
239
163
|
|
|
240
|
-
await
|
|
164
|
+
const { success } = await session.runQuery(tasksPrompt, queryOpts);
|
|
165
|
+
if (!success) {
|
|
166
|
+
log('warn', '任务分解查询未正常结束');
|
|
167
|
+
}
|
|
241
168
|
|
|
242
169
|
syncAfterPlan();
|
|
243
170
|
log('ok', '任务追加完成');
|
|
244
171
|
return { success: true, planPath: planResult.targetPath };
|
|
245
172
|
},
|
|
246
173
|
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function promptAutoRun() {
|
|
250
|
-
if (!process.stdin.isTTY) return false;
|
|
251
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
252
|
-
return new Promise(resolve => {
|
|
253
|
-
rl.question('任务分解完成后是否自动开始执行?(y/n) ', answer => {
|
|
254
|
-
rl.close();
|
|
255
|
-
resolve(/^[Yy]/.test(answer.trim()));
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function run(input, opts = {}) {
|
|
261
|
-
const instruction = input || '';
|
|
262
|
-
|
|
263
|
-
assets.ensureDirs();
|
|
264
|
-
const projectRoot = assets.projectRoot;
|
|
265
|
-
|
|
266
|
-
if (opts.readFile) {
|
|
267
|
-
const reqPath = path.resolve(projectRoot, opts.readFile);
|
|
268
|
-
if (!fs.existsSync(reqPath)) {
|
|
269
|
-
log('error', `文件不存在: ${reqPath}`);
|
|
270
|
-
process.exit(1);
|
|
271
|
-
}
|
|
272
|
-
opts.reqFile = reqPath;
|
|
273
|
-
if (instruction) {
|
|
274
|
-
log('info', `-r 模式下忽略文本输入,使用需求文件: ${reqPath}`);
|
|
275
|
-
} else {
|
|
276
|
-
console.log(`需求文件: ${reqPath}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (!instruction && !opts.reqFile) {
|
|
281
|
-
log('error', '用法: claude-coder plan "需求内容" 或 claude-coder plan -r [requirements.md]');
|
|
282
|
-
process.exit(1);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const config = loadConfig();
|
|
286
|
-
// if opts.model is not set, use the default opus model or default model, make sure the model is set.
|
|
287
|
-
if (!opts.model) {
|
|
288
|
-
if (config.defaultOpus) {
|
|
289
|
-
opts.model = config.defaultOpus;
|
|
290
|
-
} else if (config.model) {
|
|
291
|
-
opts.model = config.model;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const displayModel = opts.model || config.model || '(default)';
|
|
296
|
-
log('ok', `模型配置已加载: ${config.provider || 'claude'} (plan 使用: ${displayModel})`);
|
|
297
|
-
if (opts.interactive) {
|
|
298
|
-
log('info', '交互模式已启用,模型可能会向您提问');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (!assets.exists('profile')) {
|
|
302
|
-
log('error', 'profile 不存在,请先运行 claude-coder init 初始化项目');
|
|
303
|
-
process.exit(1);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
let shouldAutoRun = false;
|
|
307
|
-
if (!opts.planOnly) {
|
|
308
|
-
shouldAutoRun = await promptAutoRun();
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const result = await runPlanSession(instruction, { projectRoot, ...opts });
|
|
312
174
|
|
|
313
175
|
if (result.success) {
|
|
314
176
|
printStats();
|
|
@@ -316,10 +178,10 @@ async function run(input, opts = {}) {
|
|
|
316
178
|
if (shouldAutoRun) {
|
|
317
179
|
console.log('');
|
|
318
180
|
log('info', '开始自动执行任务...');
|
|
319
|
-
const {
|
|
320
|
-
await
|
|
181
|
+
const { executeRun } = require('./runner');
|
|
182
|
+
await executeRun(config, opts);
|
|
321
183
|
}
|
|
322
184
|
}
|
|
323
185
|
}
|
|
324
186
|
|
|
325
|
-
module.exports = {
|
|
187
|
+
module.exports = { executePlan };
|