claude-coder 1.6.2 → 1.7.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 +125 -127
- package/bin/cli.js +161 -197
- package/package.json +47 -44
- package/prompts/ADD_GUIDE.md +98 -0
- package/{templates → prompts}/CLAUDE.md +199 -238
- package/{templates → prompts}/SCAN_PROTOCOL.md +118 -123
- package/prompts/add_user.md +24 -0
- package/prompts/coding_user.md +31 -0
- package/prompts/scan_user.md +17 -0
- package/src/auth.js +245 -245
- package/src/config.js +201 -223
- package/src/hooks.js +166 -96
- package/src/indicator.js +233 -160
- package/src/init.js +144 -144
- package/src/prompts.js +295 -339
- package/src/runner.js +396 -394
- package/src/scanner.js +62 -62
- package/src/session.js +354 -320
- package/src/setup.js +579 -397
- package/src/tasks.js +172 -172
- package/src/validator.js +181 -170
- package/templates/requirements.example.md +56 -56
- package/templates/test_rule.md +194 -157
- package/docs/ARCHITECTURE.md +0 -516
- package/docs/PLAYWRIGHT_CREDENTIALS.md +0 -178
- package/docs/README.en.md +0 -103
package/src/indicator.js
CHANGED
|
@@ -1,160 +1,233 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { COLOR } = require('./config');
|
|
4
|
-
|
|
5
|
-
const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (this.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { COLOR } = require('./config');
|
|
4
|
+
|
|
5
|
+
const SPINNERS = ['⠋', '⠙', '⠸', '⠴', '⠦', '⠇'];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 中间截断字符串,保留首尾
|
|
9
|
+
*/
|
|
10
|
+
function truncateMiddle(str, maxLen) {
|
|
11
|
+
if (str.length <= maxLen) return str;
|
|
12
|
+
const startLen = Math.ceil((maxLen - 1) / 2);
|
|
13
|
+
const endLen = Math.floor((maxLen - 1) / 2);
|
|
14
|
+
return str.slice(0, startLen) + '…' + str.slice(-endLen);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 路径感知截断:优先保留文件名,截断目录中间
|
|
19
|
+
*/
|
|
20
|
+
function truncatePath(path, maxLen) {
|
|
21
|
+
if (path.length <= maxLen) return path;
|
|
22
|
+
|
|
23
|
+
const lastSlash = path.lastIndexOf('/');
|
|
24
|
+
if (lastSlash === -1) {
|
|
25
|
+
// 无路径分隔符,普通中间截断
|
|
26
|
+
return truncateMiddle(path, maxLen);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fileName = path.slice(lastSlash + 1);
|
|
30
|
+
const dirPath = path.slice(0, lastSlash);
|
|
31
|
+
|
|
32
|
+
// 文件名本身超长,截断文件名
|
|
33
|
+
if (fileName.length >= maxLen - 2) {
|
|
34
|
+
return truncateMiddle(path, maxLen);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 保留文件名,截断目录
|
|
38
|
+
const availableForDir = maxLen - fileName.length - 2; // -2 for '…/'
|
|
39
|
+
if (availableForDir <= 0) {
|
|
40
|
+
return '…/' + fileName.slice(0, maxLen - 2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 目录两端保留
|
|
44
|
+
const dirStart = Math.ceil(availableForDir / 2);
|
|
45
|
+
const dirEnd = Math.floor(availableForDir / 2);
|
|
46
|
+
const truncatedDir = dirPath.slice(0, dirStart) + '…' + (dirEnd > 0 ? dirPath.slice(-dirEnd) : '');
|
|
47
|
+
|
|
48
|
+
return truncatedDir + '/' + fileName;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class Indicator {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.phase = 'thinking';
|
|
54
|
+
this.step = '';
|
|
55
|
+
this.toolTarget = '';
|
|
56
|
+
this.spinnerIndex = 0;
|
|
57
|
+
this.timer = null;
|
|
58
|
+
this.lastActivity = '';
|
|
59
|
+
this.lastToolTime = Date.now();
|
|
60
|
+
this.lastActivityTime = Date.now(); // 最后活动时间(包括文字输出)
|
|
61
|
+
this.sessionNum = 0;
|
|
62
|
+
this.startTime = Date.now();
|
|
63
|
+
this.stallTimeoutMin = 30;
|
|
64
|
+
this.completionTimeoutMin = null; // session_result 写入后的缩短超时
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
start(sessionNum, stallTimeoutMin) {
|
|
68
|
+
this.sessionNum = sessionNum;
|
|
69
|
+
this.startTime = Date.now();
|
|
70
|
+
this.lastActivityTime = Date.now();
|
|
71
|
+
if (stallTimeoutMin > 0) this.stallTimeoutMin = stallTimeoutMin;
|
|
72
|
+
this.timer = setInterval(() => this._render(), 1000);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
stop() {
|
|
76
|
+
if (this.timer) {
|
|
77
|
+
clearInterval(this.timer);
|
|
78
|
+
this.timer = null;
|
|
79
|
+
}
|
|
80
|
+
process.stderr.write('\r\x1b[K');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
updatePhase(phase) {
|
|
84
|
+
this.phase = phase;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
updateStep(step) {
|
|
88
|
+
this.step = step;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
appendActivity(toolName, summary) {
|
|
92
|
+
this.lastActivity = `${toolName}: ${summary}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setCompletionDetected(timeoutMin) {
|
|
96
|
+
this.completionTimeoutMin = timeoutMin;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
updateActivity() {
|
|
100
|
+
this.lastActivityTime = Date.now();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getStatusLine() {
|
|
104
|
+
const now = new Date();
|
|
105
|
+
const hh = String(now.getHours()).padStart(2, '0');
|
|
106
|
+
const mi = String(now.getMinutes()).padStart(2, '0');
|
|
107
|
+
const sc = String(now.getSeconds()).padStart(2, '0');
|
|
108
|
+
const clock = `${hh}:${mi}:${sc}`;
|
|
109
|
+
|
|
110
|
+
const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
|
|
111
|
+
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
|
|
112
|
+
const ss = String(elapsed % 60).padStart(2, '0');
|
|
113
|
+
const spinner = SPINNERS[this.spinnerIndex % SPINNERS.length];
|
|
114
|
+
|
|
115
|
+
const phaseLabel = this.phase === 'thinking'
|
|
116
|
+
? `${COLOR.yellow}思考中${COLOR.reset}`
|
|
117
|
+
: `${COLOR.green}编码中${COLOR.reset}`;
|
|
118
|
+
|
|
119
|
+
const idleMs = Date.now() - this.lastActivityTime;
|
|
120
|
+
const idleMin = Math.floor(idleMs / 60000);
|
|
121
|
+
|
|
122
|
+
let line = `${spinner} [Session ${this.sessionNum}] ${clock} ${phaseLabel} ${mm}:${ss}`;
|
|
123
|
+
if (idleMin >= 2) {
|
|
124
|
+
if (this.completionTimeoutMin) {
|
|
125
|
+
line += ` | ${COLOR.red}${idleMin}分无响应(session_result 已写入, ${this.completionTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
|
|
126
|
+
} else {
|
|
127
|
+
line += ` | ${COLOR.red}${idleMin}分无响应(等待模型响应, ${this.stallTimeoutMin}分钟超时自动中断)${COLOR.reset}`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (this.step) {
|
|
131
|
+
line += ` | ${this.step}`;
|
|
132
|
+
if (this.toolTarget) {
|
|
133
|
+
// 动态获取终端宽度,默认 120 适配现代终端
|
|
134
|
+
const cols = process.stderr.columns || 120;
|
|
135
|
+
const usedWidth = line.replace(/\x1b\[[^m]*m/g, '').length;
|
|
136
|
+
const availWidth = Math.max(20, cols - usedWidth - 4);
|
|
137
|
+
const target = truncatePath(this.toolTarget, availWidth);
|
|
138
|
+
line += `: ${target}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return line;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_render() {
|
|
145
|
+
this.spinnerIndex++;
|
|
146
|
+
process.stderr.write(`\r\x1b[K${this.getStatusLine()}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractFileTarget(toolInput) {
|
|
151
|
+
const raw = typeof toolInput === 'object'
|
|
152
|
+
? (toolInput.file_path || toolInput.path || '')
|
|
153
|
+
: '';
|
|
154
|
+
if (!raw) return '';
|
|
155
|
+
return raw.split('/').slice(-2).join('/');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractBashLabel(cmd) {
|
|
159
|
+
if (cmd.includes('git ')) return 'Git 操作';
|
|
160
|
+
if (cmd.includes('npm ') || cmd.includes('pip ') || cmd.includes('pnpm ') || cmd.includes('yarn ')) return '安装依赖';
|
|
161
|
+
if (cmd.includes('curl') || cmd.includes('pytest') || cmd.includes('jest') || /\btest\b/.test(cmd)) return '测试验证';
|
|
162
|
+
if (cmd.includes('python ') || cmd.includes('node ')) return '执行脚本';
|
|
163
|
+
return '执行命令';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 提取 Bash 命令的主体部分(移除管道、重定向等)
|
|
168
|
+
* 正确处理引号内的内容,不会错误分割引号内的分隔符
|
|
169
|
+
*/
|
|
170
|
+
function extractBashTarget(cmd) {
|
|
171
|
+
// 移除开头的 cd xxx && 部分
|
|
172
|
+
let clean = cmd.replace(/^(?:cd\s+\S+\s*&&\s*)+/g, '').trim();
|
|
173
|
+
|
|
174
|
+
// 临时替换引号内的分隔符为占位符
|
|
175
|
+
const unescape = (s) => s.replace(/\x00/g, ';');
|
|
176
|
+
clean = clean.replace(/"[^"]*"/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
177
|
+
clean = clean.replace(/'[^']*'/g, m => m.replace(/[;|&]/g, '\x00'));
|
|
178
|
+
|
|
179
|
+
// 分割并取第一部分
|
|
180
|
+
clean = clean.split(/\s*(?:\|\|?|;|&&|2>&1|2>\/dev\/null|>\s*\/dev\/null)\s*/)[0];
|
|
181
|
+
|
|
182
|
+
// 还原占位符
|
|
183
|
+
return unescape(clean).trim();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function inferPhaseStep(indicator, toolName, toolInput) {
|
|
187
|
+
const name = (toolName || '').toLowerCase();
|
|
188
|
+
|
|
189
|
+
indicator.lastToolTime = Date.now();
|
|
190
|
+
indicator.lastActivityTime = Date.now();
|
|
191
|
+
|
|
192
|
+
if (name === 'write' || name === 'edit' || name === 'multiedit' || name === 'str_replace_editor' || name === 'strreplace') {
|
|
193
|
+
indicator.updatePhase('coding');
|
|
194
|
+
indicator.updateStep('编辑文件');
|
|
195
|
+
indicator.toolTarget = extractFileTarget(toolInput);
|
|
196
|
+
} else if (name === 'bash' || name === 'shell') {
|
|
197
|
+
const cmd = typeof toolInput === 'object' ? (toolInput.command || '') : String(toolInput || '');
|
|
198
|
+
const label = extractBashLabel(cmd);
|
|
199
|
+
indicator.updateStep(label);
|
|
200
|
+
indicator.toolTarget = extractBashTarget(cmd);
|
|
201
|
+
if (label === '测试验证' || label === '执行脚本' || label === '执行命令') {
|
|
202
|
+
indicator.updatePhase('coding');
|
|
203
|
+
}
|
|
204
|
+
} else if (name === 'read' || name === 'glob' || name === 'grep' || name === 'ls') {
|
|
205
|
+
indicator.updatePhase('thinking');
|
|
206
|
+
indicator.updateStep('读取文件');
|
|
207
|
+
indicator.toolTarget = extractFileTarget(toolInput);
|
|
208
|
+
} else if (name === 'task') {
|
|
209
|
+
indicator.updatePhase('thinking');
|
|
210
|
+
indicator.updateStep('子 Agent 搜索');
|
|
211
|
+
indicator.toolTarget = '';
|
|
212
|
+
} else if (name === 'websearch' || name === 'webfetch') {
|
|
213
|
+
indicator.updatePhase('thinking');
|
|
214
|
+
indicator.updateStep('查阅文档');
|
|
215
|
+
indicator.toolTarget = '';
|
|
216
|
+
} else {
|
|
217
|
+
indicator.updateStep('工具调用');
|
|
218
|
+
indicator.toolTarget = '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let summary;
|
|
222
|
+
if (typeof toolInput === 'object') {
|
|
223
|
+
const target = toolInput.file_path || toolInput.path || '';
|
|
224
|
+
const cmd = toolInput.command || '';
|
|
225
|
+
const pattern = toolInput.pattern || '';
|
|
226
|
+
summary = target || (cmd ? cmd.slice(0, 200) : '') || (pattern ? `pattern: ${pattern}` : JSON.stringify(toolInput).slice(0, 200));
|
|
227
|
+
} else {
|
|
228
|
+
summary = String(toolInput || '').slice(0, 200);
|
|
229
|
+
}
|
|
230
|
+
indicator.appendActivity(toolName, summary);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = { Indicator, inferPhaseStep };
|