claude-coder 1.8.2 → 1.8.4

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.
Files changed (74) hide show
  1. package/README.md +167 -167
  2. package/bin/cli.js +172 -172
  3. package/package.json +53 -52
  4. package/recipes/_shared/roles/developer.md +11 -0
  5. package/recipes/_shared/roles/product.md +12 -0
  6. package/recipes/_shared/roles/tester.md +12 -0
  7. package/recipes/_shared/test/report-format.md +86 -0
  8. package/recipes/backend/base.md +27 -0
  9. package/recipes/backend/components/auth.md +18 -0
  10. package/recipes/backend/components/crud-api.md +18 -0
  11. package/recipes/backend/components/file-service.md +15 -0
  12. package/recipes/backend/manifest.json +20 -0
  13. package/recipes/backend/test/api-test.md +25 -0
  14. package/recipes/console/base.md +37 -0
  15. package/recipes/console/components/modal-form.md +20 -0
  16. package/recipes/console/components/pagination.md +17 -0
  17. package/recipes/console/components/search.md +17 -0
  18. package/recipes/console/components/table-list.md +18 -0
  19. package/recipes/console/components/tabs.md +14 -0
  20. package/recipes/console/components/tree.md +15 -0
  21. package/recipes/console/components/upload.md +15 -0
  22. package/recipes/console/manifest.json +24 -0
  23. package/recipes/console/test/crud-e2e.md +47 -0
  24. package/recipes/h5/base.md +26 -0
  25. package/recipes/h5/components/animation.md +11 -0
  26. package/recipes/h5/components/countdown.md +11 -0
  27. package/recipes/h5/components/share.md +11 -0
  28. package/recipes/h5/components/swiper.md +11 -0
  29. package/recipes/h5/manifest.json +21 -0
  30. package/recipes/h5/test/h5-e2e.md +20 -0
  31. package/src/commands/auth.js +290 -240
  32. package/src/commands/setup-modules/helpers.js +99 -99
  33. package/src/commands/setup-modules/index.js +25 -25
  34. package/src/commands/setup-modules/mcp.js +94 -94
  35. package/src/commands/setup-modules/provider.js +260 -260
  36. package/src/commands/setup-modules/safety.js +61 -61
  37. package/src/commands/setup-modules/simplify.js +52 -52
  38. package/src/commands/setup.js +172 -172
  39. package/src/common/assets.js +236 -236
  40. package/src/common/config.js +125 -125
  41. package/src/common/constants.js +55 -55
  42. package/src/common/indicator.js +222 -222
  43. package/src/common/interaction.js +170 -170
  44. package/src/common/logging.js +77 -77
  45. package/src/common/sdk.js +50 -50
  46. package/src/common/tasks.js +88 -88
  47. package/src/common/utils.js +161 -161
  48. package/src/core/coding.js +55 -55
  49. package/src/core/context.js +117 -117
  50. package/src/core/go.js +310 -310
  51. package/src/core/harness.js +484 -484
  52. package/src/core/hooks.js +533 -533
  53. package/src/core/init.js +171 -171
  54. package/src/core/plan.js +325 -325
  55. package/src/core/prompts.js +227 -227
  56. package/src/core/query.js +49 -49
  57. package/src/core/repair.js +46 -46
  58. package/src/core/runner.js +195 -195
  59. package/src/core/scan.js +89 -89
  60. package/src/core/session.js +56 -56
  61. package/src/core/simplify.js +53 -52
  62. package/templates/bash-process.md +12 -12
  63. package/templates/codingSystem.md +65 -65
  64. package/templates/codingUser.md +17 -17
  65. package/templates/coreProtocol.md +29 -29
  66. package/templates/goSystem.md +130 -130
  67. package/templates/guidance.json +52 -52
  68. package/templates/planSystem.md +78 -78
  69. package/templates/planUser.md +8 -8
  70. package/templates/playwright.md +16 -16
  71. package/templates/requirements.example.md +57 -57
  72. package/templates/scanSystem.md +120 -120
  73. package/templates/scanUser.md +10 -10
  74. package/templates/test_rule.md +194 -194
package/src/core/plan.js CHANGED
@@ -1,325 +1,325 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const readline = require('readline');
7
- const { runSession } = require('./session');
8
- const { buildQueryOptions } = require('./query');
9
- const { buildSystemPrompt, buildPlanPrompt } = require('./prompts');
10
- const { log, loadConfig } = require('../common/config');
11
- const { assets } = require('../common/assets');
12
- const { extractResultText } = require('../common/logging');
13
- const { printStats } = require('../common/tasks');
14
- const { syncAfterPlan } = require('./harness');
15
-
16
- const EXIT_TIMEOUT_MS = 300000;
17
- const PLANS_DIR = path.join(os.homedir(), '.claude', 'plans');
18
-
19
- function buildPlanOnlyPrompt(instruction, opts = {}) {
20
- const interactive = opts.interactive || false;
21
- const reqFile = opts.reqFile || null;
22
-
23
- const inputSection = reqFile
24
- ? `需求文件路径: ${reqFile}\n先读取该文件,理解用户需求和约束。`
25
- : `用户需求:\n${instruction}`;
26
-
27
- const interactionRule = interactive
28
- ? '如有不确定的关键决策点,向用户提问对话确认方案。'
29
- : '不要提问,默认使用最佳推荐方案。';
30
-
31
- return `你是一个资深技术架构师。根据以下需求,探索项目代码库后输出完整的技术方案文档。
32
-
33
- ${inputSection}
34
-
35
- 【流程】
36
- 1. 探索项目代码库,理解结构和技术栈
37
- 2. ${interactionRule}
38
- 3. 使用 Write 工具将完整计划写入 ~/.claude/plans/ 目录(.md 格式)
39
- 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;
64
- }
65
-
66
- /**
67
- * 多源提取计划路径(按可靠性从高到低)
68
- * 1. Write 工具调用参数(最可靠)
69
- * 2. assistant 消息流文本
70
- * 3. result.result 文本
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;
127
- }
128
-
129
- function copyPlanToProject(generatedPath) {
130
- const filename = path.basename(generatedPath);
131
- const targetDir = path.join(assets.projectRoot, '.claude-coder', 'plan');
132
- const targetPath = path.join(targetDir, filename);
133
-
134
- try {
135
- if (!fs.existsSync(targetDir)) {
136
- fs.mkdirSync(targetDir, { recursive: true });
137
- }
138
- fs.copyFileSync(generatedPath, targetPath);
139
- return targetPath;
140
- } catch {
141
- return generatedPath;
142
- }
143
- }
144
-
145
- async function _executePlanGen(sdk, ctx, instruction, opts = {}) {
146
- const prompt = buildPlanOnlyPrompt(instruction, opts);
147
- const queryOpts = {
148
- permissionMode: 'plan',
149
- cwd: opts.projectRoot || assets.projectRoot,
150
- hooks: ctx.hooks,
151
- };
152
- if (!opts.interactive) {
153
- queryOpts.disallowedTools = ['askUserQuestion'];
154
- }
155
- if (opts.model) queryOpts.model = opts.model;
156
-
157
- const startTime = Date.now();
158
- let exitPlanModeDetected = false;
159
- let exitPlanModeTime = null;
160
-
161
- const collected = [];
162
- const session = sdk.query({ prompt, options: queryOpts });
163
-
164
- for await (const msg of session) {
165
- if (ctx._isStalled && ctx._isStalled()) {
166
- log('warn', '停顿超时,中断 plan 生成');
167
- break;
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();
186
- }
187
- }
188
- }
189
- }
190
-
191
- const planPath = extractPlanPathFromCollected(collected, startTime);
192
-
193
- if (planPath) {
194
- const targetPath = copyPlanToProject(planPath);
195
- return { success: true, targetPath, generatedPath: planPath };
196
- }
197
-
198
- log('warn', '无法从输出中提取计划路径');
199
- log('info', `请手动查看: ${PLANS_DIR}`);
200
- return { success: false, reason: 'no_path', targetPath: null };
201
- }
202
-
203
- async function runPlanSession(instruction, opts = {}) {
204
- const planOnly = opts.planOnly || false;
205
- const interactive = opts.interactive || false;
206
- 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';
209
-
210
- return runSession(hookType, {
211
- opts,
212
- sessionNum: 0,
213
- logFileName: `plan_${ts}.log`,
214
- label,
215
-
216
- async execute(sdk, ctx) {
217
- log('info', '正在生成计划方案...');
218
-
219
- const planResult = await _executePlanGen(sdk, ctx, instruction, opts);
220
-
221
- if (!planResult.success) {
222
- log('error', `\n计划生成失败: ${planResult.reason || planResult.error}`);
223
- return { success: false, reason: planResult.reason };
224
- }
225
-
226
- log('ok', `\n计划已生成: ${planResult.targetPath}`);
227
-
228
- if (planOnly) {
229
- return { success: true, planPath: planResult.targetPath };
230
- }
231
-
232
- log('info', '正在生成任务列表...');
233
-
234
- const tasksPrompt = buildPlanPrompt(planResult.targetPath);
235
- const queryOpts = buildQueryOptions(ctx.config, opts);
236
- queryOpts.systemPrompt = buildSystemPrompt('plan');
237
- queryOpts.hooks = ctx.hooks;
238
- queryOpts.abortController = ctx.abortController;
239
-
240
- await ctx.runQuery(sdk, tasksPrompt, queryOpts);
241
-
242
- syncAfterPlan();
243
- log('ok', '任务追加完成');
244
- return { success: true, planPath: planResult.targetPath };
245
- },
246
- });
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
-
313
- if (result.success) {
314
- printStats();
315
-
316
- if (shouldAutoRun) {
317
- console.log('');
318
- log('info', '开始自动执行任务...');
319
- const { run: runCoding } = require('./runner');
320
- await runCoding(opts);
321
- }
322
- }
323
- }
324
-
325
- module.exports = { runPlanSession, run };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const readline = require('readline');
7
+ const { runSession } = require('./session');
8
+ const { buildQueryOptions } = require('./query');
9
+ const { buildSystemPrompt, buildPlanPrompt } = require('./prompts');
10
+ const { log, loadConfig } = require('../common/config');
11
+ const { assets } = require('../common/assets');
12
+ const { extractResultText } = require('../common/logging');
13
+ const { printStats } = require('../common/tasks');
14
+ const { syncAfterPlan } = require('./harness');
15
+
16
+ const EXIT_TIMEOUT_MS = 300000;
17
+ const PLANS_DIR = path.join(os.homedir(), '.claude', 'plans');
18
+
19
+ function buildPlanOnlyPrompt(instruction, opts = {}) {
20
+ const interactive = opts.interactive || false;
21
+ const reqFile = opts.reqFile || null;
22
+
23
+ const inputSection = reqFile
24
+ ? `需求文件路径: ${reqFile}\n先读取该文件,理解用户需求和约束。`
25
+ : `用户需求:\n${instruction}`;
26
+
27
+ const interactionRule = interactive
28
+ ? '如有不确定的关键决策点,向用户提问对话确认方案。'
29
+ : '不要提问,默认使用最佳推荐方案。';
30
+
31
+ return `你是一个资深技术架构师。根据以下需求,探索项目代码库后输出完整的技术方案文档。
32
+
33
+ ${inputSection}
34
+
35
+ 【流程】
36
+ 1. 探索项目代码库,理解结构和技术栈
37
+ 2. ${interactionRule}
38
+ 3. 使用 Write 工具将完整计划写入 ~/.claude/plans/ 目录(.md 格式)
39
+ 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;
64
+ }
65
+
66
+ /**
67
+ * 多源提取计划路径(按可靠性从高到低)
68
+ * 1. Write 工具调用参数(最可靠)
69
+ * 2. assistant 消息流文本
70
+ * 3. result.result 文本
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;
127
+ }
128
+
129
+ function copyPlanToProject(generatedPath) {
130
+ const filename = path.basename(generatedPath);
131
+ const targetDir = path.join(assets.projectRoot, '.claude-coder', 'plan');
132
+ const targetPath = path.join(targetDir, filename);
133
+
134
+ try {
135
+ if (!fs.existsSync(targetDir)) {
136
+ fs.mkdirSync(targetDir, { recursive: true });
137
+ }
138
+ fs.copyFileSync(generatedPath, targetPath);
139
+ return targetPath;
140
+ } catch {
141
+ return generatedPath;
142
+ }
143
+ }
144
+
145
+ async function _executePlanGen(sdk, ctx, instruction, opts = {}) {
146
+ const prompt = buildPlanOnlyPrompt(instruction, opts);
147
+ const queryOpts = {
148
+ permissionMode: 'plan',
149
+ cwd: opts.projectRoot || assets.projectRoot,
150
+ hooks: ctx.hooks,
151
+ };
152
+ if (!opts.interactive) {
153
+ queryOpts.disallowedTools = ['askUserQuestion'];
154
+ }
155
+ if (opts.model) queryOpts.model = opts.model;
156
+
157
+ const startTime = Date.now();
158
+ let exitPlanModeDetected = false;
159
+ let exitPlanModeTime = null;
160
+
161
+ const collected = [];
162
+ const session = sdk.query({ prompt, options: queryOpts });
163
+
164
+ for await (const msg of session) {
165
+ if (ctx._isStalled && ctx._isStalled()) {
166
+ log('warn', '停顿超时,中断 plan 生成');
167
+ break;
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();
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ const planPath = extractPlanPathFromCollected(collected, startTime);
192
+
193
+ if (planPath) {
194
+ const targetPath = copyPlanToProject(planPath);
195
+ return { success: true, targetPath, generatedPath: planPath };
196
+ }
197
+
198
+ log('warn', '无法从输出中提取计划路径');
199
+ log('info', `请手动查看: ${PLANS_DIR}`);
200
+ return { success: false, reason: 'no_path', targetPath: null };
201
+ }
202
+
203
+ async function runPlanSession(instruction, opts = {}) {
204
+ const planOnly = opts.planOnly || false;
205
+ const interactive = opts.interactive || false;
206
+ 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';
209
+
210
+ return runSession(hookType, {
211
+ opts,
212
+ sessionNum: 0,
213
+ logFileName: `plan_${ts}.log`,
214
+ label,
215
+
216
+ async execute(sdk, ctx) {
217
+ log('info', '正在生成计划方案...');
218
+
219
+ const planResult = await _executePlanGen(sdk, ctx, instruction, opts);
220
+
221
+ if (!planResult.success) {
222
+ log('error', `\n计划生成失败: ${planResult.reason || planResult.error}`);
223
+ return { success: false, reason: planResult.reason };
224
+ }
225
+
226
+ log('ok', `\n计划已生成: ${planResult.targetPath}`);
227
+
228
+ if (planOnly) {
229
+ return { success: true, planPath: planResult.targetPath };
230
+ }
231
+
232
+ log('info', '正在生成任务列表...');
233
+
234
+ const tasksPrompt = buildPlanPrompt(planResult.targetPath);
235
+ const queryOpts = buildQueryOptions(ctx.config, opts);
236
+ queryOpts.systemPrompt = buildSystemPrompt('plan');
237
+ queryOpts.hooks = ctx.hooks;
238
+ queryOpts.abortController = ctx.abortController;
239
+
240
+ await ctx.runQuery(sdk, tasksPrompt, queryOpts);
241
+
242
+ syncAfterPlan();
243
+ log('ok', '任务追加完成');
244
+ return { success: true, planPath: planResult.targetPath };
245
+ },
246
+ });
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
+
313
+ if (result.success) {
314
+ printStats();
315
+
316
+ if (shouldAutoRun) {
317
+ console.log('');
318
+ log('info', '开始自动执行任务...');
319
+ const { run: runCoding } = require('./runner');
320
+ await runCoding(opts);
321
+ }
322
+ }
323
+ }
324
+
325
+ module.exports = { runPlanSession, run };