claude-coder 1.7.0 → 1.8.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 +177 -125
- package/bin/cli.js +159 -161
- package/package.json +52 -47
- package/src/commands/auth.js +294 -0
- package/src/commands/setup-modules/helpers.js +105 -0
- package/src/commands/setup-modules/index.js +26 -0
- package/src/commands/setup-modules/mcp.js +95 -0
- package/src/commands/setup-modules/provider.js +261 -0
- package/src/commands/setup-modules/safety.js +62 -0
- package/src/commands/setup-modules/simplify.js +53 -0
- package/src/commands/setup.js +172 -0
- package/src/common/assets.js +192 -0
- package/src/{config.js → common/config.js} +138 -201
- package/src/common/constants.js +57 -0
- package/src/{indicator.js → common/indicator.js} +222 -217
- package/src/common/interaction.js +170 -0
- package/src/common/logging.js +77 -0
- package/src/common/sdk.js +51 -0
- package/src/{tasks.js → common/tasks.js} +157 -172
- package/src/common/utils.js +147 -0
- package/src/core/base.js +54 -0
- package/src/core/coding.js +55 -0
- package/src/core/context.js +132 -0
- package/src/core/hooks.js +529 -0
- package/src/{init.js → core/init.js} +163 -144
- package/src/core/plan.js +318 -0
- package/src/core/prompts.js +253 -0
- package/src/core/query.js +48 -0
- package/src/core/repair.js +58 -0
- package/src/{runner.js → core/runner.js} +352 -420
- package/src/core/scan.js +89 -0
- package/src/core/simplify.js +59 -0
- package/src/core/validator.js +138 -0
- package/{prompts/ADD_GUIDE.md → templates/addGuide.md} +98 -98
- package/templates/addUser.md +26 -0
- package/{prompts/CLAUDE.md → templates/agentProtocol.md} +195 -199
- package/templates/bash-process.md +5 -0
- package/{prompts/coding_user.md → templates/codingUser.md} +31 -23
- package/templates/guidance.json +35 -0
- package/templates/playwright.md +17 -0
- package/templates/requirements.example.md +56 -56
- package/{prompts/SCAN_PROTOCOL.md → templates/scanProtocol.md} +118 -118
- package/{prompts/scan_user.md → templates/scanUser.md} +17 -17
- package/templates/test_rule.md +194 -194
- package/prompts/add_user.md +0 -24
- package/src/auth.js +0 -245
- package/src/hooks.js +0 -160
- package/src/prompts.js +0 -295
- package/src/scanner.js +0 -62
- package/src/session.js +0 -352
- package/src/setup.js +0 -579
- package/src/validator.js +0 -181
|
@@ -1,217 +1,222 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { COLOR } = require('./config');
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
this.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
return '
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* 提取 Bash 命令的主体部分(移除管道、重定向等)
|
|
153
|
-
* 正确处理引号内的内容,不会错误分割引号内的分隔符
|
|
154
|
-
*/
|
|
155
|
-
function extractBashTarget(cmd) {
|
|
156
|
-
// 移除开头的 cd xxx && 部分
|
|
157
|
-
let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
|
|
158
|
-
|
|
159
|
-
// 临时替换引号内的分隔符为占位符
|
|
160
|
-
const unescape = (s) => s.replace(/\x00/g, ';');
|
|
161
|
-
clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
162
|
-
clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
163
|
-
|
|
164
|
-
// 分割并取第一部分
|
|
165
|
-
clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
|
|
166
|
-
|
|
167
|
-
// 还原占位符
|
|
168
|
-
return unescape(clean).trim();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
172
|
-
const name = (toolName || '').toLowerCase();
|
|
173
|
-
|
|
174
|
-
indicator.
|
|
175
|
-
|
|
176
|
-
if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
177
|
-
indicator.updatePhase('coding');
|
|
178
|
-
indicator.updateStep('编辑文件');
|
|
179
|
-
indicator.toolTarget = extractFileTarget(toolInput);
|
|
180
|
-
} else if (name === 'bash' || name === 'shell') {
|
|
181
|
-
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
182
|
-
const label = extractBashLabel(cmd);
|
|
183
|
-
indicator.updateStep(label);
|
|
184
|
-
indicator.toolTarget = extractBashTarget(cmd);
|
|
185
|
-
if (label === '测试验证' || label === '执行脚本' || label === '执行命令') {
|
|
186
|
-
indicator.updatePhase('coding');
|
|
187
|
-
}
|
|
188
|
-
} else if (name === 'read' || name === 'glob' || name === 'grep' || name === 'ls') {
|
|
189
|
-
indicator.updatePhase('thinking');
|
|
190
|
-
indicator.updateStep('读取文件');
|
|
191
|
-
indicator.toolTarget = extractFileTarget(toolInput);
|
|
192
|
-
} else if (name === 'task') {
|
|
193
|
-
indicator.updatePhase('thinking');
|
|
194
|
-
indicator.updateStep('子 Agent 搜索');
|
|
195
|
-
indicator.toolTarget = '';
|
|
196
|
-
} else if (name === 'websearch' || name === 'webfetch') {
|
|
197
|
-
indicator.updatePhase('thinking');
|
|
198
|
-
indicator.updateStep('查阅文档');
|
|
199
|
-
indicator.toolTarget = '';
|
|
200
|
-
} else {
|
|
201
|
-
indicator.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { COLOR } = require('./config');
|
|
4
|
+
const { localTimestamp, truncatePath } = require('./utils');
|
|
5
|
+
|
|
6
|
+
const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
7
|
+
|
|
8
|
+
class Indicator {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.phase = 'thinking';
|
|
11
|
+
this.step = '';
|
|
12
|
+
this.toolTarget = '';
|
|
13
|
+
this.spinnerIndex = 0;
|
|
14
|
+
this.timer = null;
|
|
15
|
+
this.lastActivity = '';
|
|
16
|
+
this.lastToolTime = Date.now();
|
|
17
|
+
this.lastActivityTime = Date.now();
|
|
18
|
+
this.sessionNum = 0;
|
|
19
|
+
this.startTime = Date.now();
|
|
20
|
+
this.stallTimeoutMin = 30;
|
|
21
|
+
this.completionTimeoutMin = null;
|
|
22
|
+
this.toolRunning = false;
|
|
23
|
+
this.toolStartTime = 0;
|
|
24
|
+
this.currentToolName = '';
|
|
25
|
+
this._paused = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start(sessionNum, stallTimeoutMin) {
|
|
29
|
+
this.sessionNum = sessionNum;
|
|
30
|
+
this.startTime = Date.now();
|
|
31
|
+
this.lastActivityTime = Date.now();
|
|
32
|
+
if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
|
|
33
|
+
this.timer = setInterval(() => this._render(), 1000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
stop() {
|
|
37
|
+
if (this.timer) {
|
|
38
|
+
clearInterval(this.timer);
|
|
39
|
+
this.timer = null;
|
|
40
|
+
}
|
|
41
|
+
process.stderr.write('\r\x1b[K');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
updatePhase(phase) {
|
|
45
|
+
this.phase = phase;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
updateStep(step) {
|
|
49
|
+
this.step = step;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
appendActivity(toolName, summary) {
|
|
53
|
+
this.lastActivity = `${toolName}: ${summary}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setCompletionDetected(timeoutMin) {
|
|
57
|
+
this.completionTimeoutMin = timeoutMin;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
updateActivity() {
|
|
61
|
+
this.lastActivityTime = Date.now();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
startTool(name) {
|
|
65
|
+
this.toolRunning = true;
|
|
66
|
+
this.toolStartTime = Date.now();
|
|
67
|
+
this.currentToolName = name;
|
|
68
|
+
this.lastActivityTime = Date.now();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
endTool() {
|
|
72
|
+
if (!this.toolRunning) return;
|
|
73
|
+
this.toolRunning = false;
|
|
74
|
+
this.lastActivityTime = Date.now();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pauseRendering() { this._paused = true; }
|
|
78
|
+
resumeRendering() { this._paused = false; }
|
|
79
|
+
|
|
80
|
+
getStatusLine() {
|
|
81
|
+
const clock = localTimestamp();
|
|
82
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
83
|
+
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
84
|
+
const ss = String(elapsed % 60).padStart(2, '0');
|
|
85
|
+
const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
|
|
86
|
+
|
|
87
|
+
const phaseLabel = this.phase === 'thinking'
|
|
88
|
+
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
89
|
+
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
90
|
+
|
|
91
|
+
const idleMs = Date.now() - this.lastActivityTime;
|
|
92
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
93
|
+
|
|
94
|
+
let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
|
|
95
|
+
if (idleMin >= 2) {
|
|
96
|
+
if (this.toolRunning) {
|
|
97
|
+
const toolSec = Math.floor((Date.now() - this.toolStartTime) / 1000);
|
|
98
|
+
const toolMm = Math.floor(toolSec / 60);
|
|
99
|
+
const toolSs = toolSec % 60;
|
|
100
|
+
line += ` | ${COLOR.yellow}工具执行中 ${toolMm}:${String(toolSs).padStart(2, '0')}${COLOR.reset}`;
|
|
101
|
+
} else if (this.completionTimeoutMin) {
|
|
102
|
+
line += ` | ${COLOR.red}${idleMin}分无响应(session_result 已写入, ${this.completionTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
|
|
103
|
+
} else {
|
|
104
|
+
line += ` | ${COLOR.red}${idleMin}分无响应(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (this.step) {
|
|
108
|
+
line += ` | ${this.step}`;
|
|
109
|
+
if (this.toolTarget) {
|
|
110
|
+
// 动态获取终端宽度,默认 120 适配现代终端
|
|
111
|
+
const cols = process.stderr.columns || 120;
|
|
112
|
+
const usedWidth = line.replace(/\x1b\[[^m]*m/g, '').length;
|
|
113
|
+
const availWidth = Math.max(20, cols - usedWidth - 4);
|
|
114
|
+
const target = truncatePath(this.toolTarget, availWidth);
|
|
115
|
+
line += `: ${target}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return line;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_render() {
|
|
122
|
+
if (this._paused) return;
|
|
123
|
+
this.spinnerIndex++;
|
|
124
|
+
process.stderr.write(`\r\x1b[K${this.getStatusLine()}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function extractFileTarget(toolInput) {
|
|
129
|
+
const raw = typeof toolInput === 'object'
|
|
130
|
+
? (toolInput.file_path || toolInput.path || '')
|
|
131
|
+
: '';
|
|
132
|
+
if (!raw) return '';
|
|
133
|
+
return raw.split('/').slice(-2).join('/');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractBashLabel(cmd) {
|
|
137
|
+
if (cmd.includes('git ')) return 'Git 操作';
|
|
138
|
+
if (cmd.includes('npm ') || cmd.includes('pip ') || cmd.includes('pnpm ') || cmd.includes('yarn ')) return '安装依赖';
|
|
139
|
+
if (/\b(sleep|Start-Sleep|timeout\s+\/t)\b/i.test(cmd)) return '等待就绪';
|
|
140
|
+
if (cmd.includes('curl')) return '网络请求';
|
|
141
|
+
if (cmd.includes('pytest') || cmd.includes('jest') || /\btest\b/.test(cmd)) return '测试验证';
|
|
142
|
+
if (cmd.includes('python ') || cmd.includes('node ')) return '执行脚本';
|
|
143
|
+
return '执行命令';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function extractMcpTarget(toolInput) {
|
|
147
|
+
if (!toolInput || typeof toolInput !== 'object') return '';
|
|
148
|
+
return String(toolInput.url || toolInput.text || toolInput.element || '').slice(0, 60);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 提取 Bash 命令的主体部分(移除管道、重定向等)
|
|
153
|
+
* 正确处理引号内的内容,不会错误分割引号内的分隔符
|
|
154
|
+
*/
|
|
155
|
+
function extractBashTarget(cmd) {
|
|
156
|
+
// 移除开头的 cd xxx && 部分
|
|
157
|
+
let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
|
|
158
|
+
|
|
159
|
+
// 临时替换引号内的分隔符为占位符
|
|
160
|
+
const unescape = (s) => s.replace(/\x00/g, ';');
|
|
161
|
+
clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
162
|
+
clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
163
|
+
|
|
164
|
+
// 分割并取第一部分
|
|
165
|
+
clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
|
|
166
|
+
|
|
167
|
+
// 还原占位符
|
|
168
|
+
return unescape(clean).trim();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
172
|
+
const name = (toolName || '').toLowerCase();
|
|
173
|
+
|
|
174
|
+
indicator.startTool(toolName);
|
|
175
|
+
|
|
176
|
+
if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
177
|
+
indicator.updatePhase('coding');
|
|
178
|
+
indicator.updateStep('编辑文件');
|
|
179
|
+
indicator.toolTarget = extractFileTarget(toolInput);
|
|
180
|
+
} else if (name === 'bash' || name === 'shell') {
|
|
181
|
+
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
182
|
+
const label = extractBashLabel(cmd);
|
|
183
|
+
indicator.updateStep(label);
|
|
184
|
+
indicator.toolTarget = extractBashTarget(cmd);
|
|
185
|
+
if (label === '测试验证' || label === '执行脚本' || label === '执行命令') {
|
|
186
|
+
indicator.updatePhase('coding');
|
|
187
|
+
}
|
|
188
|
+
} else if (name === 'read' || name === 'glob' || name === 'grep' || name === 'ls') {
|
|
189
|
+
indicator.updatePhase('thinking');
|
|
190
|
+
indicator.updateStep('读取文件');
|
|
191
|
+
indicator.toolTarget = extractFileTarget(toolInput);
|
|
192
|
+
} else if (name === 'task') {
|
|
193
|
+
indicator.updatePhase('thinking');
|
|
194
|
+
indicator.updateStep('子 Agent 搜索');
|
|
195
|
+
indicator.toolTarget = '';
|
|
196
|
+
} else if (name === 'websearch' || name === 'webfetch') {
|
|
197
|
+
indicator.updatePhase('thinking');
|
|
198
|
+
indicator.updateStep('查阅文档');
|
|
199
|
+
indicator.toolTarget = '';
|
|
200
|
+
} else if (name.startsWith('mcp__')) {
|
|
201
|
+
indicator.updatePhase('coding');
|
|
202
|
+
const action = name.split('__').pop() || name;
|
|
203
|
+
indicator.updateStep(`浏览器: ${action}`);
|
|
204
|
+
indicator.toolTarget = extractMcpTarget(toolInput);
|
|
205
|
+
} else {
|
|
206
|
+
indicator.updateStep('工具调用');
|
|
207
|
+
indicator.toolTarget = '';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let summary;
|
|
211
|
+
if (typeof toolInput === 'object') {
|
|
212
|
+
const target = toolInput.file_path || toolInput.path || '';
|
|
213
|
+
const cmd = toolInput.command || '';
|
|
214
|
+
const pattern = toolInput.pattern || '';
|
|
215
|
+
summary = target || (cmd ? cmd.slice(0, 200) : '') || (pattern ? `pattern: ${pattern}` : JSON.stringify(toolInput).slice(0, 200));
|
|
216
|
+
} else {
|
|
217
|
+
summary = String(toolInput || '').slice(0, 200);
|
|
218
|
+
}
|
|
219
|
+
indicator.appendActivity(toolName, summary);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = { Indicator, inferPhaseStep };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { COLOR } = require('./config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 在终端渲染一个结构化问题并收集用户选择
|
|
8
|
+
*
|
|
9
|
+
* @param {object} question - AskUserQuestion 格式的单个问题
|
|
10
|
+
* @param {string} question.question - 问题文本
|
|
11
|
+
* @param {string} question.header - 短标签
|
|
12
|
+
* @param {Array} question.options - 选项列表 [{ label, description }]
|
|
13
|
+
* @param {boolean} question.multiSelect - 是否多选
|
|
14
|
+
* @returns {Promise<string>} 用户选择的文本
|
|
15
|
+
*/
|
|
16
|
+
async function renderQuestion(question) {
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stderr,
|
|
20
|
+
terminal: process.stdin.isTTY || false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return new Promise(resolve => {
|
|
24
|
+
const w = (s) => process.stderr.write(s);
|
|
25
|
+
|
|
26
|
+
w(`\n${COLOR.cyan}┌─ ${question.header || '问题'} ${'─'.repeat(Math.max(0, 40 - (question.header || '').length))}${COLOR.reset}\n`);
|
|
27
|
+
w(`${COLOR.cyan}│${COLOR.reset} ${COLOR.bold}${question.question}${COLOR.reset}\n`);
|
|
28
|
+
w(`${COLOR.cyan}│${COLOR.reset}\n`);
|
|
29
|
+
|
|
30
|
+
const options = question.options || [];
|
|
31
|
+
options.forEach((opt, i) => {
|
|
32
|
+
w(`${COLOR.cyan}│${COLOR.reset} ${COLOR.yellow}${i + 1}.${COLOR.reset} ${opt.label}\n`);
|
|
33
|
+
if (opt.description) {
|
|
34
|
+
w(`${COLOR.cyan}│${COLOR.reset} ${COLOR.dim}${opt.description}${COLOR.reset}\n`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
w(`${COLOR.cyan}│${COLOR.reset} ${COLOR.yellow}0.${COLOR.reset} ${COLOR.dim}其他 (自定义输入)${COLOR.reset}\n`);
|
|
39
|
+
|
|
40
|
+
if (question.multiSelect) {
|
|
41
|
+
w(`${COLOR.cyan}│${COLOR.reset}\n`);
|
|
42
|
+
w(`${COLOR.cyan}│${COLOR.reset} ${COLOR.dim}(多选: 用逗号分隔数字, 如 1,3)${COLOR.reset}\n`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
w(`${COLOR.cyan}└${'─'.repeat(44)}${COLOR.reset}\n`);
|
|
46
|
+
|
|
47
|
+
rl.question(` ${COLOR.green}>${COLOR.reset} `, answer => {
|
|
48
|
+
rl.close();
|
|
49
|
+
|
|
50
|
+
const trimmed = answer.trim();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
resolve(options[0]?.label || '');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (trimmed === '0') {
|
|
57
|
+
const rl2 = readline.createInterface({
|
|
58
|
+
input: process.stdin,
|
|
59
|
+
output: process.stderr,
|
|
60
|
+
terminal: process.stdin.isTTY || false,
|
|
61
|
+
});
|
|
62
|
+
rl2.question(` ${COLOR.cyan}请输入你的想法:${COLOR.reset} `, customAnswer => {
|
|
63
|
+
rl2.close();
|
|
64
|
+
resolve(customAnswer.trim() || options[0]?.label || '');
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nums = trimmed.split(/[,,\s]+/)
|
|
70
|
+
.map(n => parseInt(n, 10))
|
|
71
|
+
.filter(n => !isNaN(n) && n >= 1 && n <= options.length);
|
|
72
|
+
|
|
73
|
+
if (nums.length > 0) {
|
|
74
|
+
const selected = nums.map(n => options[n - 1].label);
|
|
75
|
+
resolve(question.multiSelect ? selected.join(', ') : selected[0]);
|
|
76
|
+
} else {
|
|
77
|
+
resolve(trimmed);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 处理完整的 AskUserQuestion 工具调用
|
|
85
|
+
* 逐个渲染问题,收集所有答案
|
|
86
|
+
*
|
|
87
|
+
* @param {object} toolInput - AskUserQuestion 的 tool_input
|
|
88
|
+
* @param {Array} toolInput.questions - 问题列表
|
|
89
|
+
* @returns {Promise<object>} { answers: { [question]: answer }, formatted: string }
|
|
90
|
+
*/
|
|
91
|
+
async function handleUserQuestions(toolInput) {
|
|
92
|
+
const questions = toolInput.questions || [];
|
|
93
|
+
if (questions.length === 0) {
|
|
94
|
+
return { answers: {}, formatted: '(no questions)' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.stderr.write(`\n${COLOR.magenta}╔══ 模型需要你的输入 ══════════════════════╗${COLOR.reset}\n`);
|
|
98
|
+
process.stderr.write(`${COLOR.magenta}║${COLOR.reset} 以下是模型提出的问题,请逐一回答 ${COLOR.magenta}║${COLOR.reset}\n`);
|
|
99
|
+
process.stderr.write(`${COLOR.magenta}╚══════════════════════════════════════════╝${COLOR.reset}\n`);
|
|
100
|
+
|
|
101
|
+
const answers = {};
|
|
102
|
+
for (const q of questions) {
|
|
103
|
+
const answer = await renderQuestion(q);
|
|
104
|
+
answers[q.question] = answer;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = Object.entries(answers)
|
|
108
|
+
.map(([q, a]) => `Q: ${q}\nA: ${a}`)
|
|
109
|
+
.join('\n\n');
|
|
110
|
+
|
|
111
|
+
process.stderr.write(`\n${COLOR.green}✓ 已收集回答:${COLOR.reset}\n`);
|
|
112
|
+
for (const [q, a] of Object.entries(answers)) {
|
|
113
|
+
const shortQ = q.length > 50 ? q.slice(0, 50) + '...' : q;
|
|
114
|
+
process.stderr.write(` ${COLOR.dim}${shortQ}${COLOR.reset} → ${COLOR.bold}${a}${COLOR.reset}\n`);
|
|
115
|
+
}
|
|
116
|
+
process.stderr.write('\n');
|
|
117
|
+
|
|
118
|
+
return { answers, formatted: lines };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 创建 AskUserQuestion 的 PreToolUse Hook 处理函数
|
|
123
|
+
*
|
|
124
|
+
* 工作原理:
|
|
125
|
+
* 1. 拦截模型的 AskUserQuestion 调用
|
|
126
|
+
* 2. 通过 readline 在终端展示问题
|
|
127
|
+
* 3. 收集用户答案
|
|
128
|
+
* 4. deny 工具调用,同时通过 systemMessage 将答案注入上下文
|
|
129
|
+
*
|
|
130
|
+
* @returns {Function} PreToolUse hook handler
|
|
131
|
+
*/
|
|
132
|
+
function createAskUserQuestionHook(indicator) {
|
|
133
|
+
return async (input, _toolUseID, _context) => {
|
|
134
|
+
if (input.tool_name !== 'AskUserQuestion') return {};
|
|
135
|
+
|
|
136
|
+
if (indicator) {
|
|
137
|
+
indicator.pauseRendering();
|
|
138
|
+
process.stderr.write('\r\x1b[K');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let formatted;
|
|
142
|
+
try {
|
|
143
|
+
({ formatted } = await handleUserQuestions(input.tool_input));
|
|
144
|
+
} finally {
|
|
145
|
+
if (indicator) indicator.resumeRendering();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
systemMessage: [
|
|
150
|
+
'The user has answered your questions via the terminal interface.',
|
|
151
|
+
'Here are their responses:',
|
|
152
|
+
'',
|
|
153
|
+
formatted,
|
|
154
|
+
'',
|
|
155
|
+
'Proceed based on these answers. Do NOT ask the same questions again.',
|
|
156
|
+
].join('\n'),
|
|
157
|
+
hookSpecificOutput: {
|
|
158
|
+
hookEventName: 'PreToolUse',
|
|
159
|
+
permissionDecision: 'deny',
|
|
160
|
+
permissionDecisionReason: `User answered via terminal. Answers:\n${formatted}`,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
renderQuestion,
|
|
168
|
+
handleUserQuestions,
|
|
169
|
+
createAskUserQuestionHook,
|
|
170
|
+
};
|